diff --git a/src/documents/tests/search/test_comma_value_lists.py b/src/documents/tests/search/test_comma_value_lists.py deleted file mode 100644 index a3701e674..000000000 --- a/src/documents/tests/search/test_comma_value_lists.py +++ /dev/null @@ -1,231 +0,0 @@ -"""Comma-separated value lists, at result level. - -``tag:foo,bar`` is a value list only for fields that opt into -``comma_values`` (``tag`` is the only one today). Nothing in the tree runs -a ``tag:`` fielded query against real documents; this covers foo-only, -bar-only and both-tags documents against a real index, plus a decoy -proving a non-comma_values field (``correspondent``) does NOT treat a -comma as a list separator -- ``correspondent:foo,bar`` searches for the -literal text "foo,bar", so a correspondent literally named that way -matches while foo-only/bar-only correspondents do not. - -The list semantics are AND (a document must carry every listed value), -not OR: a foo-only or bar-only document does not match ``tag:foo,bar``. - -A comma is only a value-list delimiter when what follows it is a value. A -comma directly before another field name separates two clauses instead, -on ``comma_values`` and ordinary fields alike -- ``tag:foo,added:2005-03-04`` -is "tagged foo AND added that day", not "tagged both foo and -added:2005-03-04". That reading was asserted before the whoosh-compat -migration and nowhere after it, so it is pinned here. -""" - -from __future__ import annotations - -from datetime import UTC -from datetime import datetime -from typing import TYPE_CHECKING - -import pytest - -from documents.models import Correspondent -from documents.models import Document -from documents.models import Tag - -if TYPE_CHECKING: - from documents.search._backend import TantivyBackend - -pytestmark = [pytest.mark.search, pytest.mark.django_db] - - -def _matched_ids(backend: TantivyBackend, query: str) -> set[int]: - return set(backend.search_ids(query, user=None)) - - -class TestTagCommaValueListIsConjunctive: - @pytest.fixture - def docs(self, backend: TantivyBackend) -> dict[str, int]: - foo = Tag.objects.create(name="foo") - bar = Tag.objects.create(name="bar") - baz = Tag.objects.create(name="baz") - - foo_only = Document.objects.create( - title="Foo only", - content="x", - checksum="comma-tag-foo-only", - ) - foo_only.tags.set([foo]) - backend.add_or_update(foo_only) - - bar_only = Document.objects.create( - title="Bar only", - content="x", - checksum="comma-tag-bar-only", - ) - bar_only.tags.set([bar]) - backend.add_or_update(bar_only) - - both = Document.objects.create( - title="Both tags", - content="x", - checksum="comma-tag-both", - ) - both.tags.set([foo, bar]) - backend.add_or_update(both) - - neither = Document.objects.create( - title="Neither tag", - content="x", - checksum="comma-tag-neither", - ) - neither.tags.set([baz]) - backend.add_or_update(neither) - - return { - "foo_only": foo_only.pk, - "bar_only": bar_only.pk, - "both": both.pk, - "neither": neither.pk, - } - - def test_only_the_document_carrying_both_tags_matches( - self, - backend: TantivyBackend, - docs: dict[str, int], - ) -> None: - assert _matched_ids(backend, "tag:foo,bar") == {docs["both"]} - - def test_foo_only_document_does_not_match( - self, - backend: TantivyBackend, - docs: dict[str, int], - ) -> None: - assert docs["foo_only"] not in _matched_ids(backend, "tag:foo,bar") - - def test_bar_only_document_does_not_match( - self, - backend: TantivyBackend, - docs: dict[str, int], - ) -> None: - assert docs["bar_only"] not in _matched_ids(backend, "tag:foo,bar") - - -class TestNonCommaValuesFieldTreatsCommaAsLiteralText: - def test_correspondent_comma_is_not_a_value_list( - self, - backend: TantivyBackend, - ) -> None: - literal = Correspondent.objects.create(name="foo,bar") - literal_doc = Document.objects.create( - title="Literal correspondent", - content="x", - checksum="comma-correspondent-literal", - correspondent=literal, - ) - backend.add_or_update(literal_doc) - - foo_correspondent = Correspondent.objects.create(name="foo") - foo_doc = Document.objects.create( - title="Foo correspondent", - content="x", - checksum="comma-correspondent-foo", - correspondent=foo_correspondent, - ) - backend.add_or_update(foo_doc) - - bar_correspondent = Correspondent.objects.create(name="bar") - bar_doc = Document.objects.create( - title="Bar correspondent", - content="x", - checksum="comma-correspondent-bar", - correspondent=bar_correspondent, - ) - backend.add_or_update(bar_doc) - - # A value-list reading would match foo_doc and/or bar_doc (an OR) - # or neither (an AND, since no single correspondent carries both - # values). Either way it would NOT match literal_doc alone. - assert _matched_ids(backend, "correspondent:foo,bar") == {literal_doc.pk} - - -class TestCommaBeforeAKnownFieldIsAClauseSeparator: - """The discriminating half: the value-list reading and the clause-separator - reading disagree about every query below. - - On ``tag`` (the one ``comma_values`` field) a value-list reading of - ``tag:foo,added:2005-03-04`` demands a tag literally named - "added:2005-03-04", so it matches nothing at all. On ``title`` (not - ``comma_values``) the alternative is the literal reading proven in - TestNonCommaValuesFieldTreatsCommaAsLiteralText, i.e. a title containing - the text "Alpha,tag:foo", which matches nothing either. Both are exact-set - assertions against a corpus that separates the readings, so neither - alternative survives. - """ - - @pytest.fixture - def docs(self, backend: TantivyBackend) -> dict[str, int]: - foo = Tag.objects.create(name="foo") - bar = Tag.objects.create(name="bar") - - both = Document.objects.create( - title="Alpha", - content="x", - checksum="comma-clause-both", - added=datetime(2005, 3, 4, 15, 30, tzinfo=UTC), - ) - both.tags.set([foo]) - backend.add_or_update(both) - - tag_only = Document.objects.create( - title="Alpha", - content="x", - checksum="comma-clause-tag-only", - added=datetime(2010, 7, 1, 15, 30, tzinfo=UTC), - ) - tag_only.tags.set([foo]) - backend.add_or_update(tag_only) - - date_only = Document.objects.create( - title="Beta", - content="x", - checksum="comma-clause-date-only", - added=datetime(2005, 3, 4, 15, 30, tzinfo=UTC), - ) - date_only.tags.set([bar]) - backend.add_or_update(date_only) - - return { - "both": both.pk, - "tag_only": tag_only.pk, - "date_only": date_only.pk, - } - - def test_comma_separates_a_comma_values_field_from_a_date_clause( - self, - backend: TantivyBackend, - docs: dict[str, int], - ) -> None: - assert _matched_ids(backend, "tag:foo,added:2005-03-04") == {docs["both"]} - - def test_comma_separates_an_ordinary_field_from_a_tag_clause( - self, - backend: TantivyBackend, - docs: dict[str, int], - ) -> None: - assert _matched_ids(backend, "title:Alpha,tag:foo") == { - docs["both"], - docs["tag_only"], - } - - def test_each_clause_alone_matches_more_than_the_pair( - self, - backend: TantivyBackend, - docs: dict[str, int], - ) -> None: - """Both clauses must be doing work: if the separator dropped either - side, the pair would match whatever the surviving side matches.""" - assert _matched_ids(backend, "tag:foo") == {docs["both"], docs["tag_only"]} - assert _matched_ids(backend, "added:2005-03-04") == { - docs["both"], - docs["date_only"], - } diff --git a/src/documents/tests/search/test_compact_date_forms.py b/src/documents/tests/search/test_compact_date_forms.py index ce4cf344a..80ddacbd2 100644 --- a/src/documents/tests/search/test_compact_date_forms.py +++ b/src/documents/tests/search/test_compact_date_forms.py @@ -1,18 +1,15 @@ -"""Whoosh's compact, separator-free date spellings: ``20050304`` and -``20050304153000``. +"""Whoosh's compact, separator-free date spelling, resolved end to end. -Both were asserted before the whoosh-compat migration (the old -``test_8digit_created_date_field_always_uses_utc_midnight`` and -``test_14digit_compact_datetime``) and by the deleted ``_translate.py``'s own -suite. Afterwards the 8-digit form survived only incidentally, in one -pre-existing API test, and the 14-digit form was asserted nowhere -- the -regression class where a spelling silently stops matching with nothing to -notice. - -The two forms differ in width, not just in length: 8 digits is a calendar-day -window, 14 digits a single instant. The corpus separates them, so a form that -degrades into the other one -- or into a non-match -- fails rather than passing -on the one document that would match either way. +whoosh-compat owns both widths of this spelling and asserts them directly +(``test_compact_numeric_datetime`` for the 8-digit calendar-day form and +``test_compact_numeric_datetime_full_width_is_a_single_second_instant`` for +the 14-digit instant). The 14-digit form is kept here as the single +representative because it is the one that exercises paperless's ``added`` +DATETIME fast field at full precision: the corpus separates a document at +the named instant from one on the same calendar day at another hour and one +on the next day at the same hour, so a query that degrades into a whole-day +window, or drops the time of day, matches the wrong set rather than passing +on a corpus that could not tell the difference. """ from __future__ import annotations @@ -68,40 +65,10 @@ def docs(backend: TantivyBackend) -> dict[str, int]: } -class TestCompactDateForms: - def test_eight_digits_is_a_calendar_day_window( - self, - backend: TantivyBackend, - docs: dict[str, int], - ) -> None: - assert _matched_ids(backend, "added:20050304") == { - docs["instant"], - docs["same_day"], - } - - def test_eight_digits_agrees_with_the_hyphenated_spelling( - self, - backend: TantivyBackend, - docs: dict[str, int], - ) -> None: - assert _matched_ids(backend, "added:20050304") == _matched_ids( - backend, - "added:2005-03-04", - ) - - def test_fourteen_digits_is_a_single_instant( - self, - backend: TantivyBackend, - docs: dict[str, int], - ) -> None: - # same_day is what tells this apart from the 8-digit form, next_day - # from a form that ignored the time altogether. - assert _matched_ids(backend, "added:20050304153000") == {docs["instant"]} - - def test_fourteen_digits_addresses_the_hour_it_names( - self, - backend: TantivyBackend, - docs: dict[str, int], - ) -> None: - assert _matched_ids(backend, "added:20050304090000") == {docs["same_day"]} - assert _matched_ids(backend, "added:20050305153000") == {docs["next_day"]} +def test_fourteen_digits_is_a_single_instant( + backend: TantivyBackend, + docs: dict[str, int], +) -> None: + # same_day is what tells this apart from the 8-digit day-window form, + # next_day from a form that ignored the time altogether. + assert _matched_ids(backend, "added:20050304153000") == {docs["instant"]} diff --git a/src/documents/tests/search/test_dash_prefix_negation.py b/src/documents/tests/search/test_dash_prefix_negation.py deleted file mode 100644 index 91e7190ae..000000000 --- a/src/documents/tests/search/test_dash_prefix_negation.py +++ /dev/null @@ -1,120 +0,0 @@ -"""Pins the current, deferred-negation behavior of a leading ``-``. - -Negation via a bare ``-`` prefix (as opposed to the ``NOT`` keyword) is -deferred to the companion plan's G1 item. Until G1 lands, a leading ``-`` -is not negation at all: - -- Unfielded (``-taxes``): the separator is stripped at index time, so the - term becomes an ordinary, *required* positive match on ``taxes`` -- the - exact inverse of what a user typing ``-taxes`` to exclude a term intends. -- Fielded (``-title:alpha``): the leading hyphen detaches from the field - clause entirely (the parse tree emits it as its own token), and the - field clause itself is left as an ordinary positive match. The - negation is dropped, not inverted. - -If G1 changes either of these, the corresponding test below inverts -- -that inversion is the intended, visible signal that G1 landed. -""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -import pytest - -from documents.models import Document - -if TYPE_CHECKING: - from documents.search._backend import TantivyBackend - -pytestmark = [pytest.mark.search, pytest.mark.django_db] - - -def _matched_ids(backend: TantivyBackend, query: str) -> set[int]: - return set(backend.search_ids(query, user=None)) - - -def _index(backend: TantivyBackend, **kwargs: object) -> Document: - doc = Document.objects.create(**kwargs) - backend.add_or_update(doc) - return doc - - -class TestUnfieldedDashBecomesARequiredTerm: - @pytest.fixture - def docs(self, backend: TantivyBackend) -> dict[str, int]: - return { - "with_taxes": _index( - backend, - title="Invoice one", - content="invoice taxes included", - checksum="dash-unfielded-with", - ).pk, - "without_taxes": _index( - backend, - title="Invoice two", - content="invoice ordinary contents", - checksum="dash-unfielded-without", - ).pk, - } - - def test_dash_prefixed_term_matches_only_the_document_containing_it( - self, - backend: TantivyBackend, - docs: dict[str, int], - ) -> None: - # If this were real negation, it would match "without_taxes" (the - # document that does NOT contain "taxes"). It instead matches - # "with_taxes" -- the document that DOES. - assert _matched_ids(backend, "invoice -taxes") == {docs["with_taxes"]} - - def test_bare_dash_prefixed_term_alone_is_a_positive_search( - self, - backend: TantivyBackend, - docs: dict[str, int], - ) -> None: - assert _matched_ids(backend, "-taxes") == {docs["with_taxes"]} - - -class TestFieldedDashDropsTheNegation: - @pytest.fixture - def docs(self, backend: TantivyBackend) -> dict[str, int]: - return { - "alpha": _index( - backend, - title="Alpha Title", - content="alpha body", - checksum="dash-fielded-alpha", - ).pk, - "beta": _index( - backend, - title="Beta Title", - content="beta body", - checksum="dash-fielded-beta", - ).pk, - } - - def test_dash_fielded_term_matches_the_field_value_it_names( - self, - backend: TantivyBackend, - docs: dict[str, int], - ) -> None: - # Real negation would exclude the "beta" document. Dropped - # negation instead matches it, identically to "title:beta". - assert _matched_ids(backend, "-title:beta") == {docs["beta"]} - assert _matched_ids(backend, "title:beta") == _matched_ids( - backend, - "-title:beta", - ) - - def test_dash_fielded_term_conjoined_still_requires_both_sides( - self, - backend: TantivyBackend, - docs: dict[str, int], - ) -> None: - # Discriminating shape: if the dash-fielded clause were dropped - # from the query entirely (rather than kept as a positive - # requirement), this would match "alpha" alone. Because the - # dropped negation still leaves an ordinary AND-ed requirement - # behind, and no document has both titles, nothing matches. - assert _matched_ids(backend, "title:alpha -title:beta") == set() diff --git a/src/documents/tests/search/test_documented_syntax.py b/src/documents/tests/search/test_documented_syntax.py index 34b93e3dc..0fea3f0c5 100644 --- a/src/documents/tests/search/test_documented_syntax.py +++ b/src/documents/tests/search/test_documented_syntax.py @@ -267,8 +267,9 @@ class TestDocumentedDateForms: # Quoting is what rescues the other multi-word date expressions, # so pin that it does not rescue these: the problem is the width # of the resulting range, not the way the value is delimited. + # One quoted spelling is enough for that; which keyword sits + # inside the quotes is grammar whoosh-compat owns. 'added:"now"', - 'added:"midnight"', # A relative offset, which the warning in the docs names by this # exact spelling. Standing alone it is an instant like the rest of # this list; the same offset used as a range bound is a real diff --git a/src/documents/tests/search/test_registry.py b/src/documents/tests/search/test_registry.py index 712fff770..b591bde0b 100644 --- a/src/documents/tests/search/test_registry.py +++ b/src/documents/tests/search/test_registry.py @@ -88,6 +88,13 @@ class TestFieldRegistry: def test_tag_is_comma_values(self, registry: FieldRegistry) -> None: assert _resolve(registry, "tag").spec.comma_values is True + def test_correspondent_is_not_comma_values(self, registry: FieldRegistry) -> None: + # "tag" is the only field that opts in. This is only observable here: + # end to end the two readings of "correspondent:foo,bar" agree, + # because the analyzer splits the literal value on the comma anyway, + # so a result-level test cannot tell a value list from literal text. + assert _resolve(registry, "correspondent").spec.comma_values is False + def test_created_is_date_kind(self, registry: FieldRegistry) -> None: resolved = _resolve(registry, "created") assert resolved.spec.kind is FieldKind.DATE diff --git a/src/documents/tests/search/test_reversed_date_ranges.py b/src/documents/tests/search/test_reversed_date_ranges.py index 2c196edc1..4f252050b 100644 --- a/src/documents/tests/search/test_reversed_date_ranges.py +++ b/src/documents/tests/search/test_reversed_date_ranges.py @@ -1,23 +1,19 @@ -"""Reversed date ranges swap their bounds back into order. +"""A reversed relative range, resolved end to end against the index. -Measured end to end (FROZEN_NOW = 2026-06-15T12:00:00Z): +whoosh-compat owns the bound swap itself and asserts it directly, for both +the absolute and the relative spelling +(``test_reversed_relative_range_swaps_like_the_absolute_case`` in +``tests/test_parser_dates.py``, DIVERGENCES.md entry 53). Paperless does not +rewrite reversed ranges anywhere; ``documents/search/_query.py`` passes the +query through untouched. - added:[now-1h to now+1h] -> 2h window, correct order - added:[now+1h to now-1h] -> the same 2h window, SWAPPED - added:[2020-01-01 to 2019-01-01] -> 366 days, SWAPPED to the forward order - added:[2019-01-01 to 2020-01-01] -> 366 days (same result either way) - -Both range kinds behave the same way, matching whoosh's own behavior. -Relative (``now±``) reversed ranges used to differ: whoosh-compat's date -grammar added a day to the upper bound instead of swapping, producing a -much wider window than either the forward or a swapped reading gives. That -was a library-level inconsistency between the two range kinds, not an -application-level rewrite paperless performs (there is still no -reversed-range handling in documents/search/_query.py), and it was fixed in -whoosh-compat's date grammar rather than in a pre-parse rewrite on this -side (see also the CJK/pre-parse-rewrites docstrings elsewhere in this -suite for the same discipline). Both cases are pinned below so a future -divergence between them is visible again. +What is kept here is the relative spelling only, because it is the one whose +bounds depend on paperless's own plumbing: ``now±1h`` is resolved against the +timezone paperless hands the parser, so the emitted window is paperless's +result rather than the library's alone. The corpus separates a document +inside the two-hour window from one that is outside it but inside the wider +window a non-swapping reading produces, so a bound that resolves in the wrong +timezone, or a swap that does not happen, matches the wrong set. """ from __future__ import annotations @@ -49,91 +45,37 @@ def _index(backend: TantivyBackend, **kwargs: object) -> Document: return doc -class TestRelativeReversedRangeSwaps: - @pytest.fixture - def docs(self, backend: TantivyBackend) -> dict[str, int]: - with time_machine.travel(FROZEN_NOW, tick=False): - return { - # Inside the 2h window [11:00, 13:00] that both the forward - # and the reversed spelling resolve to. - "in_forward_window": _index( - backend, - title="Forward window doc", - content="x", - checksum="reversed-relative-forward", - added=FROZEN_NOW, - ).pk, - # Outside that window, but inside the wider window the - # reversed spelling produced when it day-bumped instead of - # swapping. It must not match either query now. - "outside_the_window": _index( - backend, - title="Later same-week doc", - content="x", - checksum="reversed-relative-outside", - added=datetime(2026, 6, 16, 8, 0, tzinfo=UTC), - ).pk, - } - - def test_forward_range_matches_only_the_two_hour_window( - self, - backend: TantivyBackend, - docs: dict[str, int], - ) -> None: - with time_machine.travel(FROZEN_NOW, tick=False): - assert _matched_ids(backend, "added:[now-1h to now+1h]") == { - docs["in_forward_window"], - } - - def test_reversed_range_swaps_to_the_same_window( - self, - backend: TantivyBackend, - docs: dict[str, int], - ) -> None: - # The same set as the forward query, and in particular not the - # document that only a day-bumped upper bound would have reached. - with time_machine.travel(FROZEN_NOW, tick=False): - assert _matched_ids(backend, "added:[now+1h to now-1h]") == { - docs["in_forward_window"], - } - - -class TestAbsoluteReversedRangeSwaps: - @pytest.fixture - def docs(self, backend: TantivyBackend) -> dict[str, int]: +@pytest.fixture +def docs(backend: TantivyBackend) -> dict[str, int]: + with time_machine.travel(FROZEN_NOW, tick=False): return { - "in_range": _index( + # Inside the 2h window [11:00, 13:00] that both the forward and + # the reversed spelling resolve to. + "in_window": _index( backend, - title="In-range doc", + title="Forward window doc", content="x", - checksum="reversed-absolute-in-range", - added=datetime(2019, 6, 1, tzinfo=UTC), + checksum="reversed-relative-forward", + added=FROZEN_NOW, ).pk, - "out_of_range": _index( + # Outside that window, but inside the wider window a reversed + # range that day-bumped its upper bound instead of swapping + # would reach. + "outside_the_window": _index( backend, - title="Out-of-range doc", + title="Later same-week doc", content="x", - checksum="reversed-absolute-out-of-range", - added=datetime(2018, 6, 1, tzinfo=UTC), + checksum="reversed-relative-outside", + added=datetime(2026, 6, 16, 8, 0, tzinfo=UTC), ).pk, } - def test_forward_range_matches_the_in_range_document( - self, - backend: TantivyBackend, - docs: dict[str, int], - ) -> None: - assert _matched_ids( - backend, - "added:[2019-01-01 to 2020-01-01]", - ) == {docs["in_range"]} - def test_reversed_range_swaps_to_match_the_same_document( - self, - backend: TantivyBackend, - docs: dict[str, int], - ) -> None: - assert _matched_ids( - backend, - "added:[2020-01-01 to 2019-01-01]", - ) == {docs["in_range"]} +def test_reversed_relative_range_matches_the_two_hour_window( + backend: TantivyBackend, + docs: dict[str, int], +) -> None: + with time_machine.travel(FROZEN_NOW, tick=False): + assert _matched_ids(backend, "added:[now+1h to now-1h]") == { + docs["in_window"], + } diff --git a/src/documents/tests/search/test_whoosh_unit_abbreviations.py b/src/documents/tests/search/test_whoosh_unit_abbreviations.py index f599c7528..59c472314 100644 --- a/src/documents/tests/search/test_whoosh_unit_abbreviations.py +++ b/src/documents/tests/search/test_whoosh_unit_abbreviations.py @@ -1,20 +1,18 @@ -"""Whoosh-style relative date unit abbreviations: yrs/mos/wks/hrs/mins/secs. +"""One relative unit abbreviation, resolved end to end against the index. -Zero assertions existed anywhere in this tree before this file -- the old -v2-era ``TestWhooshUnitAbbreviations`` is gone, and whoosh-compat's own -test corpus only carries these spellings as allowlisted (assertion- -inverted) lines, i.e. lines it knows do not fully match its own grammar's -documented behavior yet accepts anyway. This is the exact class of -regression #13482 was: a spelling silently stops parsing (or silently -stops matching) with nothing anywhere to notice. +whoosh-compat owns which unit words exist and what offset each resolves to, +and asserts all six (yrs/mos/wks/hrs/mins/secs, quoted and bare) directly in +``tests/test_relative_date_unit_abbreviations.py``. Repeating that matrix here +would only re-prove the library's grammar; "hrs" is kept as a single +representative so that the paperless-side path the library cannot see -- the +``added`` DATETIME fast field, the timezone the query is resolved in and the +range the backend emits -- is still exercised by an abbreviation-spelled +offset rather than only by absolute dates. -Each abbreviation produces a relative, zero-width instant -(``now - N`` .. same instant), not a span -- pinned separately in -the date-keyword tests. What matters here is that each of the six -spellings resolves to the *correct* instant, checked by indexing one -document at exactly that instant per unit: a wrong offset (an "hrs" typo -that resolves as minutes, say) lands on nothing, or on a sibling -document's instant, rather than passing by accident. +The offset is sub-day on purpose: a whole-day or whole-year offset survives a +wrong timezone, five hours does not. The decoy document sits at a different +offset from the same instant, so an offset that resolves to the wrong width +lands on nothing or on the decoy instead of passing by accident. """ from __future__ import annotations @@ -48,32 +46,11 @@ def _index(backend: TantivyBackend, **kwargs: object) -> Document: @pytest.fixture def unit_documents(backend: TantivyBackend) -> dict[str, int]: - """One document at the exact instant each abbreviation should resolve - to, all indexed together so a wrong offset lands on the wrong (or no) - document rather than passing coincidentally.""" + """One document five hours before the frozen instant, one ten minutes + before it, both indexed together so a mis-resolved offset lands on the + wrong document or on none rather than passing coincidentally.""" with time_machine.travel(FROZEN_NOW, tick=False): return { - "yrs": _index( - backend, - title="Years doc", - content="x", - checksum="unit-abbrev-yrs", - added=datetime(2024, 6, 15, 12, 0, tzinfo=UTC), - ).pk, - "mos": _index( - backend, - title="Months doc", - content="x", - checksum="unit-abbrev-mos", - added=datetime(2026, 3, 15, 12, 0, tzinfo=UTC), - ).pk, - "wks": _index( - backend, - title="Weeks doc", - content="x", - checksum="unit-abbrev-wks", - added=datetime(2026, 6, 1, 12, 0, tzinfo=UTC), - ).pk, "hrs": _index( backend, title="Hours doc", @@ -88,55 +65,12 @@ def unit_documents(backend: TantivyBackend) -> dict[str, int]: checksum="unit-abbrev-mins", added=datetime(2026, 6, 15, 11, 50, tzinfo=UTC), ).pk, - "secs": _index( - backend, - title="Seconds doc", - content="x", - checksum="unit-abbrev-secs", - added=datetime(2026, 6, 15, 11, 59, 30, tzinfo=UTC), - ).pk, } -class TestUnitAbbreviationsResolveToTheCorrectInstant: - @pytest.mark.parametrize( - ("query", "label"), - [ - pytest.param('added:"-2yrs"', "yrs", id="years"), - pytest.param('added:"-3mos"', "mos", id="months"), - pytest.param('added:"-2wks"', "wks", id="weeks"), - pytest.param('added:"-5hrs"', "hrs", id="hours"), - pytest.param('added:"-10mins"', "mins", id="minutes"), - pytest.param('added:"-30secs"', "secs", id="seconds"), - ], - ) - def test_quoted_abbreviation_matches_only_its_own_instant( - self, - backend: TantivyBackend, - unit_documents: dict[str, int], - query: str, - label: str, - ) -> None: - with time_machine.travel(FROZEN_NOW, tick=False): - assert _matched_ids(backend, query) == {unit_documents[label]} - - @pytest.mark.parametrize( - ("query", "label"), - [ - pytest.param("added:-2yrs", "yrs", id="years"), - pytest.param("added:-3mos", "mos", id="months"), - pytest.param("added:-2wks", "wks", id="weeks"), - pytest.param("added:-5hrs", "hrs", id="hours"), - pytest.param("added:-10mins", "mins", id="minutes"), - pytest.param("added:-30secs", "secs", id="seconds"), - ], - ) - def test_bare_unquoted_abbreviation_matches_only_its_own_instant( - self, - backend: TantivyBackend, - unit_documents: dict[str, int], - query: str, - label: str, - ) -> None: - with time_machine.travel(FROZEN_NOW, tick=False): - assert _matched_ids(backend, query) == {unit_documents[label]} +def test_hours_abbreviation_matches_only_its_own_instant( + backend: TantivyBackend, + unit_documents: dict[str, int], +) -> None: + with time_machine.travel(FROZEN_NOW, tick=False): + assert _matched_ids(backend, 'added:"-5hrs"') == {unit_documents["hrs"]}