Skip to content

test(db): one fixture builder instead of fourteen per-view seeds - #269

Merged
ryanlewis merged 1 commit into
mainfrom
test/issue-244-fixtures
Sep 10, 2026
Merged

ryanlewis merged 1 commit into
mainfrom
test/issue-244-fixtures

Conversation

@ryanlewis

Copy link
Copy Markdown
Owner

Closes #244

What changed

internal/db/tasks_test.go had fifteen seed helpers, one per PR this
week, each re-inserting its own areas, projects and to-dos, and the
shared assertion helpers uuidsOf, sameSet and keysOf sat beside
them rather than in testhelpers_test.go.

The three assertion helpers move to testhelpers_test.go, next to a
small fixture builder, and the fourteen per-view seeds are folded into
the tests that used them. A test now seeds only the rows it asserts on:
TestListTasksSomedayHidesChildOfSomedayProject went from nine rows to
two, TestListTasksTodayIncludeCompletedProject from twelve to three.

seedTasks stays. Twenty-two tests read it as the general fixture
covering every view and status, so folding it would rewrite most of the
file for no gain; it is now built through the same builder as everything
else.

The builder

d, fx := newFixture(t)
fx.area("area-work", "Work", 1)
fx.project("proj-today", "Runbook audit", 5, anytimeOn(today), inArea("area-work"), todayIndex(2005))
fx.todo("todo-loose", "Buy milk", 14, anytimeOn(today), todayIndexRef(today), todayIndex(7))
fx.heading("head-1", "Phase one", 11, inProject("proj-today"))
fx.tag("tg-urgent", "urgent", 1)
fx.tagged("t-today", "tg-urgent")

"index" is a required argument on every row and every date is passed
in. Nothing an assertion can turn on is defaulted, because eleven tests
assert order rather than membership and several seed their indexes
deliberately against the order they expect, which is what leaves the key
under test as the only thing that can produce it. Area and project
indexes stay negative, as Things writes them — the ordering CASE keys
exist because an unfiled row's COALESCE default of 0 would sort it
last instead of first (#217, #237).

Options name a Things bucket rather than the raw columns: inbox(),
anytime(), anytimeOn(date), evening(date), someday(),
somedayOn(date). The rest are one per column: inArea, inProject,
underHeading, notes, trashed, completed(stop), cancelled(stop),
status, deadline, todayIndex, todayIndexRef, repeats.

Also in this pass

TestListTasksTodayIncludeCompletedProject stopped a row a minute in
the past. The calendar day decides whether a closed row is still under
Today (#230), so a minute before midnight falls on the previous day and
the test failed in the first minute of every day. It now stops the row
at time.Now(), the convention the rest of the file follows.

TestListQueryGoldenSQL reported differences only from inside its
per-case loop, so a difference outside a ### view= block — trailing
whitespace, text before the first header — passed with got != want.
This is the two-line fix #264 applied to TestRenderGolden. Verified by
appending a blank line to the golden file: the test now fails with
generated SQL differs from testdata/list_query_golden.txt outside any case body.

The comment above TestPrintTasksNoRepeatedProjectHeader said today,
upcoming and anytime all list a scheduled project as a row. Anytime has
carried no project rows since #217.

Evidence

The passing test list is identical before and after — 263 tests across
internal/db and internal/output, no test renamed, added or removed:

go test ./internal/db ./internal/output -json \
  | jq -r 'select(.Action=="pass" and .Test) | .Test' | sort
diff before.txt after.txt   # no output

Per-function coverage is identical too, function for function across
both packages, so the folded fixtures still reach every line the fuller
ones did:

diff <(go tool cover -func=before.cov) <(go tool cover -func=after.cov)
# no output; total 95.9% either way

Every seed was first rewritten through the builder and pinned row for
row against a copy of the SQL it replaced — all four tables dumped and
compared column by column, with stopDate values bucketed to the minute
so two time.Now() calls a microsecond apart compare equal. All fifteen
matched exactly, including the todayIndex = 0 values that would sort
differently from NULL under the today and upcoming orderings. Only then
were rows dropped from a fixture, so the fold below removes rows and
never changes a value. That scaffolding is not part of the PR.

A differential mutation study confirms the smaller fixtures still bite.
Nine predicates in internal/db/tasks.go were broken one at a time —
untrashedRows, notHeading, todoOrProject, the template-row and
template-child exclusions, untrashedParent, unparented, todoOnly
and openRows — and each mutant run against both the old and the new
test file. Every mutation is still caught. Four more, on the view table
itself, are caught by the folded tests too: someday losing unparented,
anytime gaining includesProjects, trash losing it, and todoOrProject
admitting headings.

What the fold does cost is redundancy. Five of those nine mutations used
to fail TestListTasksDeadlinesIncludeProjects and
TestListTasksDeadlinesOrderWithProject as well, because the shared
seed handed them six rows they never asserted on and their exact-set
assertions failed on any leak. Each of those five is still caught by
TestListTasksDeadlinesProjectExclusions, which is the test that owns
those rows. The same trade, smaller, applies to
TestListTasksLogbookIncludesCancelled,
TestListTasksTodayOrderWithProjects and
TestListTasksViewsIncludeProjects. Putting the rows back is the
duplication this PR set out to remove.

testdata/list_query_golden.txt regenerates with no diff.

Notes for review

The three arrangement tests that seed negative area and project indexes
— TestAnytimeGroupsByAreaThenProject,
TestTodayGroupsLooseTodosBeforeProjectTodos and
TestSomedayGroupsUnfiledThenAreas — keep their own inline SQL. They
were never seed helpers, so converting them is outside what #244 asks
for, and their literal indexes are the whole point of those tests.

TestListTasksTrashProjectExclusions asserts that trash-head is not
listed, but the row is hidden by the trashed-parent fold rather than by
the heading exclusion — it stayed out even with todoOrProject mutated
to admit type 2. That is unchanged from before this PR; the fixture had
the same shape. Its live-proj and live-todo assertions do bite.

TestListTasksDeadlinesIncludeProjects and
TestListTasksDeadlinesOrderWithProject now visibly assert the same
listing, one with sameSet and one with reflect.DeepEqual, and
TestListTasksLogbookIncludesCancelled and
TestListTasksLogbookCancelledOrder are the same pair. Both predate
this PR and are left alone.

No production code changed.

The per-view seed helpers in internal/db/tasks_test.go had grown to
fifteen, one per PR this week, each re-inserting its own areas, projects
and to-dos, and the shared assertion helpers sat beside them rather than
in testhelpers_test.go.

Move uuidsOf, sameSet and keysOf into testhelpers_test.go next to a
small fixture builder, and fold the fourteen per-view seeds into the
tests that used them, so a test now seeds only the rows it asserts on.
seedTasks stays: twenty-two tests read it as the general fixture, and
folding it would rewrite most of the file for no gain.

The builder takes every value an assertion can turn on rather than
defaulting it. "index" is a required argument on every row and dates are
passed in, because several ordering tests seed those deliberately
against the order they expect, and area and project indexes are
negative, as Things writes them.

Each seed was first rewritten through the builder and pinned row for row
against a copy of the SQL it replaced, so the fold below only removes
rows from a fixture, never changes a value. The passing test list is
identical, 263 tests, and the list-query golden file regenerates with no
diff.

While here:

- TestListTasksTodayIncludeCompletedProject stopped a row a minute in
  the past, which falls on the previous calendar day in the first minute
  after midnight and failed the test there. It now stops it at
  time.Now(), the convention the rest of the file follows.
- TestListQueryGoldenSQL reported differences only from inside its
  per-case loop, so a difference outside a "### view=" block passed with
  got != want. This is the fix #264 applied to TestRenderGolden.
- The comment above TestPrintTasksNoRepeatedProjectHeader said today,
  upcoming and anytime all list a scheduled project as a row; anytime
  has carried no project rows since issue #217.

Closes #244
@ryanlewis
ryanlewis merged commit 49ad65b into main Sep 10, 2026
8 checks passed
@ryanlewis
ryanlewis deleted the test/issue-244-fixtures branch September 10, 2026 15:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

test(db): fixture builder instead of fourteen per-view seed helpers

1 participant