test(search): keep one case per integration seam, not one per grammar spelling

whoosh-compat now owns the query grammar and asserts it directly, so the
parallel grammar corpus this tree accumulated re-proves the library rather
than paperless. Each surviving case was checked by mutation: break the seam
it claims (index `added` at day precision, resolve queries in a fixed
non-local timezone, tokenize on whitespace instead of separators, index the
metadata counters off by one) and the case fails.

- delete test_dash_prefix_negation.py: ported wholesale to the library, and
  the analyzer half it also covered is held by test_documented_syntax.py's
  documented leading-hyphen case, which the whitespace-tokenizer mutation
  fails.
- delete test_comma_value_lists.py: end to end the value-list and literal
  readings of a comma agree, because the analyzer splits the literal value
  on the comma anyway. Declaring comma_values on correspondent, or removing
  it from tag, left every case in that file passing, so it proved the
  grammar and not the registry. The registry fact moves to test_registry.py,
  where the same mutation does fail.
- trim the unit-abbreviation, compact-date and reversed-range files to one
  representative each.
- drop the second quoted zero-width keyword from test_documented_syntax.py;
  which keyword sits inside the quotes is library grammar.

Docstrings that justified a file by the absence of library coverage are
rewritten: that premise expired when the library absorbed the assertions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
stumpylog
2026-08-20 15:43:45 -07:00
co-authored by Claude Opus 5
parent c56f910cc9
commit cf1cce5f9d
7 changed files with 87 additions and 587 deletions
@@ -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"],
}
@@ -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"]}
@@ -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()
@@ -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
@@ -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
@@ -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"],
}
@@ -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<unit>`` .. 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"]}