"""Юнит-тесты парсера CORE — чистая логика, без сети. Стерегут то, ради чего парсер написан иначе остальных: отбор статей с полным текстом, отсев дублей CORE (одна статья под разными id), нарезку выборки по архивам-поставщикам и позицию продолжения «архив:смещение». Нарезка по архивам — не украшение: связка условий через `AND` в API CORE не работает (запрос по архивам вместе с годом отдаёт годы вперемешку), поэтому в запрос идёт ровно одно условие, а годы отсекаются уже на нашей стороне. """ from core import MIN_CHARS, COREParser LONG = "слово " * 400 # заведомо длиннее MIN_CHARS SAMPLE = { "id": 123456, "title": "Нейронные сети в медицине", "authors": [{"name": "Кузьмин, Ярослав Вадимович"}, {"name": "Савенок А."}], "doi": "10.1234/x", "yearPublished": 2019, "language": {"code": "ru"}, "journals": [{"title": "Вестник"}], "downloadUrl": "https://core.ac.uk/download/1.pdf", "abstract": "Аннотация", "fullText": LONG, "resume_token": "1298:100", } def test_repos_from_or_expression(): q = "(repositories.id:1298 OR repositories.id:21908 OR repositories.id:949)" assert COREParser._repos(q) == ["1298", "21908", "949"] def test_repos_from_plain_list(): assert COREParser._repos("1298, 21908 949") == ["1298", "21908", "949"] def test_repos_without_numbers_falls_back_to_word_search(): # Номеров нет — единственная часть «q»: обычный поиск по словам assert COREParser._repos("нейронные сети") == ["q"] assert COREParser._repos("") == ["q"] def test_parse_token(): assert COREParser._parse_token("1298:4200") == ("1298", 4200) assert COREParser._parse_token("q:300") == ("q", 300) assert COREParser._parse_token(None) == (None, 0) assert COREParser._parse_token("мусор") == (None, 0) assert COREParser._parse_token("архив:смещение") == (None, 0) def test_transform_maps_fields(): t = COREParser().transform(SAMPLE) assert t["ext_id"] == "core:123456" assert t["source"] == "core" assert t["year"] == 2019 and t["lang"] == "ru" assert t["journal"] == "Вестник" assert t["text"] == LONG assert t["resume_token"] == "1298:100" # Автор приходит одной строкой «Фамилия, Имя Отчество» assert t["authors"][0]["last_name"] == "Кузьмин" assert t["authors"][0]["initials"] == "Я.В." def test_transform_skips_short_text(): assert COREParser().transform(dict(SAMPLE, fullText="коротко")) == {} assert COREParser().transform(dict(SAMPLE, fullText=None)) == {} assert COREParser().transform(dict(SAMPLE, id=None)) == {} def test_transform_drops_undefined_language(): # zz у CORE значит «язык не определён» — в корпус такое класть незачем assert COREParser().transform(dict(SAMPLE, language={"code": "zz"}))["lang"] is None assert COREParser().transform(dict(SAMPLE, language=None))["lang"] is None def _parser_with_pages(pages): """Парсер, у которого выдача подменена заранее заготовленными страницами.""" p = COREParser(api_key="test") calls = [] def fake_page(part, query, offset): calls.append((part, offset)) return pages.pop(0) if pages else [] p._page = fake_page # type: ignore[method-assign] p.calls = calls # type: ignore[attr-defined] return p def test_fetch_drops_core_duplicates(): # Одна и та же статья под разными id — CORE так отдаёт всегда page = [ {"id": 1, "fullText": LONG}, {"id": 2, "fullText": LONG}, {"id": 3, "fullText": LONG + "иное"}, {"id": 4, "fullText": "коротко"}, ] p = _parser_with_pages([page]) got = list(p.fetch(limit=10, query="repositories.id:1298")) assert [w["id"] for w in got] == [1, 3] def test_fetch_position_points_at_own_page(): # Токен обязан указывать на страницу, откуда пришла статья: пачка может # прерваться на середине, и следующий прогон перечитает её целиком pages = [[{"id": 1, "fullText": LONG}], [{"id": 2, "fullText": LONG + "два"}]] p = _parser_with_pages(pages) got = list(p.fetch(limit=10, query="repositories.id:1298")) assert [w["resume_token"] for w in got] == ["1298:0", "1298:100"] def test_fetch_walks_archives_and_resumes(): p = _parser_with_pages([[], []]) list(p.fetch(limit=10, query="(repositories.id:1298 OR repositories.id:949)", resume_token="1298:300")) # Начали с архива из токена и его смещения, пустой архив — переход к следующему assert p.calls == [("1298", 300), ("949", 0)] def test_fetch_ignores_token_of_unknown_archive(): p = _parser_with_pages([[]]) list(p.fetch(limit=10, query="repositories.id:1298", resume_token="99999:500")) assert p.calls == [("1298", 0)] def test_fetch_filters_years_on_our_side(): # Годы API не фильтрует, поэтому отсекаем сами — и только если заданы page = [ {"id": 1, "fullText": LONG, "yearPublished": 2004}, {"id": 2, "fullText": LONG + "два", "yearPublished": 2015}, {"id": 3, "fullText": LONG + "три", "yearPublished": 2030}, ] p = _parser_with_pages([list(page)]) got = list(p.fetch(limit=10, query="repositories.id:1298", year_from=2010, year_to=2026)) assert [w["id"] for w in got] == [2] p2 = _parser_with_pages([list(page)]) got2 = list(p2.fetch(limit=10, query="repositories.id:1298")) assert [w["id"] for w in got2] == [1, 2, 3] def test_min_chars_threshold_is_meaningful(): assert MIN_CHARS >= 1000