Files
backend/backend/tests/test_pubmed_query_parser.py
T
34047007@qq.com 090938fa2b fix: 第11轮深度审计修复 — P0 keyset翻页崩溃 + P1布尔符语义/语法误报等16项修复
P0 (1项):
- keyset cursor: lit.title or "__NULL__" 空字符串误判为NULL导致后续翻页全空

P1 (6项):
- boolean_operator: 仅扫描depth=0的token,括号内AND/OR不再影响顶层操作符判定
- is_pubmed_syntax: 去除AND/OR/NOT检测的IGNORECASE(PubMed仅识别大写布尔符)
- _relevance_query: MeSH-only查询(如breast[MAJR])补充原始回退,避免退化到日期排序
- pmid_terms: 缺少try/except int(),补充ValueError+DOI兜底
- 部分日期展开: 2024-01[DP]等YYYY-MM partial date展开为整月范围(DP/EDAT/CRDT)
- 前端#N去重: resolveQuery/expandQuery dedup refs防重复引用误判为循环

P2 (9项):
- cache: filter-options加入invalidate_search_cache清理
- worker: daily_ftp_update/daily_citation_update异常时finally保证缓存清理
- cursor_val: 空字符串''通过is None检查,补充not cursor_val
- keyset UI: 模板条件sort==='date'改为KEYSET_SORTS.has(sort)
- resetAllFilters: 补充showCustomYear=false
2026-07-28 12:08:17 +08:00

213 lines
7.4 KiB
Python

"""PubMed 查询语法解析器测试"""
import pytest
from app.services.pubmed_query_parser import (
ParsedPubmedQuery,
parse_pubmed_query,
is_pubmed_syntax,
tokenise,
extract_pubmed_query_for_prisma,
)
class TestIsPubmedSyntax:
def test_plain_text_not_pubmed(self):
assert not is_pubmed_syntax("lung cancer")
def test_field_tag_detected(self):
assert is_pubmed_syntax('"lung cancer"[TI]')
def test_boolean_and_detected(self):
assert is_pubmed_syntax("cancer AND therapy")
def test_boolean_or_detected(self):
assert is_pubmed_syntax("cancer OR tumor")
def test_boolean_not_detected(self):
assert is_pubmed_syntax("cancer NOT review")
def test_empty_not_pubmed(self):
assert not is_pubmed_syntax("")
assert not is_pubmed_syntax(None)
def test_lowercase_boolean_detected(self):
"""小写的 and/or/not 不识别为 PubMed 语法(P11: 仅大写 AND/OR/NOT 才是 PubMed 布尔符)"""
assert not is_pubmed_syntax("cancer and therapy")
class TestTokenise:
def test_simple_tokens(self):
tokens = tokenise('"lung cancer"[TI]')
texts = [t.value for t in tokens if t.value]
assert '"lung cancer"' in texts
assert "[TI]" in texts
def test_boolean_tokens(self):
tokens = tokenise("a AND b OR c NOT d")
texts = [t.value for t in tokens if t.value]
assert "AND" in texts
assert "OR" in texts
assert "NOT" in texts
def test_parentheses(self):
tokens = tokenise("(a OR b)")
types = [t.type.name for t in tokens]
assert "LPAREN" in types
assert "RPAREN" in types
class TestParsePubmedQuery:
def test_plain_text_no_field(self):
"""纯文本查询返回 plain_terms"""
result = parse_pubmed_query("lung cancer")
assert len(result.plain_terms) == 2
assert result.plain_terms[0].text == "lung"
assert result.plain_terms[1].text == "cancer"
def test_title_field(self):
"""[TI] 映射到 title 字段"""
result = parse_pubmed_query('"lung cancer"[TI]')
assert len(result.title_terms) == 1
t = result.title_terms[0]
assert t.text == "lung cancer"
assert t.exact is True
assert t.field == "title"
def test_abstract_field(self):
result = parse_pubmed_query('"cancer"[AB]')
assert len(result.abstract_terms) == 1
assert result.abstract_terms[0].text == "cancer"
def test_tiab_field(self):
result = parse_pubmed_query('"cancer"[TIAB]')
assert len(result.tiab_terms) == 1
assert result.tiab_terms[0].text == "cancer"
def test_author_field(self):
result = parse_pubmed_query("smith j[AU]")
assert len(result.author_terms) == 1
assert result.author_terms[0].text == "j"
assert result.author_terms[0].field == "author"
# smith should be captured as plain term via implicit AND
assert len(result.plain_terms) >= 1
def test_journal_field(self):
result = parse_pubmed_query('"nature"[TA]')
assert len(result.journal_terms) == 1
assert result.journal_terms[0].text == "nature"
def test_mesh_field(self):
result = parse_pubmed_query('"lung neoplasms"[MH]')
assert len(result.mesh_terms) == 1
assert result.mesh_terms[0].text == "lung neoplasms"
assert result.mesh_terms[0].is_not is False
def test_majr_field(self):
result = parse_pubmed_query('"lung neoplasms"[MAJR]')
assert len(result.majr_terms) == 1
assert result.majr_terms[0].text == "lung neoplasms"
assert result.majr_terms[0].is_not is False
def test_pub_type(self):
result = parse_pubmed_query('"review"[PT]')
assert len(result.pub_types) == 1
assert result.pub_types[0].text == "review"
def test_pmid_field(self):
result = parse_pubmed_query("12345678[PMID]")
assert len(result.pmid_terms) == 1
assert result.pmid_terms[0].text == "12345678"
def test_doi_field(self):
result = parse_pubmed_query('"10.1000/test"[DOI]')
assert len(result.doi_terms) == 1
assert "10.1000/test" in result.doi_terms[0].text
def test_date_range_dp(self):
result = parse_pubmed_query("2024:2026[DP]")
assert result.year_from == 2024
assert result.year_to == 2026
def test_simple_and(self):
result = parse_pubmed_query("cancer AND therapy")
assert result.boolean_operator == "and"
assert len(result.plain_terms) == 2
def test_simple_or(self):
result = parse_pubmed_query("cancer OR tumor")
assert result.boolean_operator == "or"
assert len(result.plain_terms) == 2
def test_boolean_not(self):
result = parse_pubmed_query('"lung"[TI] NOT "review"[PT]')
assert result.has_not is True
assert len(result.not_terms) == 1
assert result.not_terms[0].text == "review"
def test_complex_nested(self):
result = parse_pubmed_query(
'("lung cancer"[TI] OR "breast cancer"[TI]) AND "immunotherapy"[TIAB]'
)
# 括号分组后,term 进入 groups 而非直接列在 title_terms
assert len(result.groups) > 0
assert len(result.tiab_terms) == 1
assert result.boolean_operator == "and"
# P11: 括号内的 OR 不影响顶层布尔操作符判定,顶层只有 AND
# 确保 groups 中包含括号内的两个 title 词
group_texts = [t.text for g in result.groups for t in g]
assert "lung cancer" in group_texts
assert "breast cancer" in group_texts
def test_implicit_and_multiple_terms(self):
result = parse_pubmed_query("a b c")
assert len(result.plain_terms) == 3
def test_lowercase_field_tag(self):
result = parse_pubmed_query('"lung cancer"[ti]')
assert len(result.title_terms) == 1
def test_mixedcase_field_tag(self):
result = parse_pubmed_query('"lung cancer"[Ti]')
assert len(result.title_terms) == 1
def test_quoted_phrase_exact(self):
result = parse_pubmed_query('"exact phrase"')
assert len(result.plain_terms) == 1
assert result.plain_terms[0].exact is True
def test_not_with_parens(self):
result = parse_pubmed_query('("cancer" AND "therapy") NOT "review"')
assert result.has_not is True
def test_empty_query(self):
result = parse_pubmed_query("")
assert len(result.plain_terms) == 0
def test_invalid_syntax_degrades(self):
"""非法语法退化到空 ParsedPubmedQuery"""
result = parse_pubmed_query("invalid[[syntax")
# Should not crash, return empty plain_terms
assert isinstance(result, ParsedPubmedQuery)
def test_field_with_dot(self):
result = parse_pubmed_query("john.doe[AU]")
assert len(result.author_terms) == 1
class TestExtractForPrisma:
def test_extract_mesh_and_normalize(self):
normalized, mesh = extract_pubmed_query_for_prisma(
'("lung cancer"[ti] OR "lung neoplasms"[mh]) AND 2024:2026[dp]'
)
# Field tags should be uppercased
assert "[TI]" in normalized or "[ti]" not in normalized
def test_pubmed_query_types(self):
normalized, mesh = extract_pubmed_query_for_prisma(
'"cancer"[TI] AND "immunotherapy"[MH]'
)
assert "cancer" in normalized
assert "immunotherapy" in normalized
assert "immunotherapy" in mesh