diff --git a/cmd/things/add_test.go b/cmd/things/add_test.go index 1ae3d068..e7ba6166 100644 --- a/cmd/things/add_test.go +++ b/cmd/things/add_test.go @@ -1124,7 +1124,7 @@ func TestAddInvalidRefusedBeforeTagsCreated(t *testing.T) { // Things understood gives the item a date, which the read-back cannot // check, so the add is reported not confirmed (whenPlaced). func TestAddWhenPhraseIgnored(t *testing.T) { - dated := "start = 1, startDate = " + strconv.Itoa(int(model.ThingsDateFromTime(time.Now().AddDate(0, 0, 3)))) + dated := "start = 1, startDate = " + strconv.Itoa(int(model.ThingsDateFromTime(time.Now()).AddDays(3))) cases := []struct { name string extra string diff --git a/cmd/things/edit_test.go b/cmd/things/edit_test.go index 4f4d10d3..0f3e46b3 100644 --- a/cmd/things/edit_test.go +++ b/cmd/things/edit_test.go @@ -331,7 +331,7 @@ func TestEditCertainNoOpSkipsTheWait(t *testing.T) { deadline := int(model.ThingsDateFromTime(time.Date(2026, 10, 15, 0, 0, 0, 0, time.Local))) day := func(offset int) string { return now.AddDate(0, 0, offset).Format("2006-01-02") } dayInt := func(offset int) string { - return strconv.Itoa(int(model.ThingsDateFromTime(now.AddDate(0, 0, offset)))) + return strconv.Itoa(int(model.ThingsDateFromTime(now).AddDays(offset))) } cases := []struct { @@ -573,6 +573,9 @@ func TestEditReadsTagListOnce(t *testing.T) { t.Fatalf("apply %q: %v", stmt, err) } } + if err := rows.Err(); err != nil { + t.Fatalf("read schema: %v", err) + } _ = rows.Close() fx := dbtest.NewFixture(t, sqlDB) fx.Todo("one-1", "Post letter", 1) diff --git a/cmd/things/importcreate_test.go b/cmd/things/importcreate_test.go index 7f32a5cc..4d187b3f 100644 --- a/cmd/things/importcreate_test.go +++ b/cmd/things/importcreate_test.go @@ -1238,7 +1238,7 @@ func TestImportNestedInUpdateNotWarned(t *testing.T) { func TestImportChecksWhen(t *testing.T) { past := time.Now().AddDate(0, 0, -4).Format("2006-01-02") today := "start = 1, startDate = " + strconv.Itoa(int(model.ThingsDateFromTime(testNow))) - tomorrow := "start = 2, startDate = " + strconv.Itoa(int(model.ThingsDateFromTime(testNow.AddDate(0, 0, 1)))) + tomorrow := "start = 2, startDate = " + strconv.Itoa(int(model.ThingsDateFromTime(testNow).AddDays(1))) for _, tc := range []struct { name string attrs string diff --git a/cmd/things/when_test.go b/cmd/things/when_test.go index b14e862f..5e4a293c 100644 --- a/cmd/things/when_test.go +++ b/cmd/things/when_test.go @@ -68,7 +68,7 @@ func TestPlaceWhen(t *testing.T) { func TestWhenUnchangedTime(t *testing.T) { now := time.Date(2026, 10, 7, 15, 0, 0, 0, time.Local) today := model.ThingsDateFromTime(now) - tomorrow := model.ThingsDateFromTime(now.AddDate(0, 0, 1)) + tomorrow := model.ThingsDateFromTime(now).AddDays(1) reminder := func(min int, err error) func() (int, error) { return func() (int, error) { return min, err } } cases := []struct { name string @@ -298,7 +298,7 @@ func TestPlaceWhenNearOffsetChange(t *testing.T) { func TestEditUnmovedRowAcceptsEitherOutcome(t *testing.T) { now := testNow today := int(model.ThingsDateFromTime(now)) - old := int(model.ThingsDateFromTime(now.AddDate(0, 0, -3))) + old := int(model.ThingsDateFromTime(now).AddDays(-3)) past := now.AddDate(0, 0, -1).Format("2006-01-02") type row struct { typ model.TaskType @@ -376,7 +376,7 @@ func TestEditUnmovedRowAcceptsEitherOutcome(t *testing.T) { func TestEditUnmovedRowWithOtherFieldsNeedsAChange(t *testing.T) { fastVerify(t) database, sqlDB := seedWritable(t) - old := int(model.ThingsDateFromTime(testNow.AddDate(0, 0, -3))) + old := int(model.ThingsDateFromTime(testNow).AddDays(-3)) if _, err := sqlDB.Exec(`UPDATE TMTask SET start = 2, startDate = ?, startBucket = 0 WHERE uuid = 'one-1'`, old); err != nil { t.Fatal(err) } @@ -399,7 +399,7 @@ func TestEditUnmovedRowWithOtherFieldsNeedsAChange(t *testing.T) { // the dropping stub fails it unless the edit skips the read-back wait. func TestEditCarriedOverRow(t *testing.T) { today := int(model.ThingsDateFromTime(testNow)) - yesterday := int(model.ThingsDateFromTime(testNow.AddDate(0, 0, -1))) + yesterday := int(model.ThingsDateFromTime(testNow).AddDays(-1)) todayDate := testNow.Format("2006-01-02") const reminder = 1207959552 seed := func(t *testing.T, sqlDB *sql.DB, start, date, bucket int, reminder any) { @@ -614,8 +614,8 @@ func TestEditCarriedOverRow(t *testing.T) { // part of the day, only for a place that is today. func TestWhenPlaceHoldsCarriedOver(t *testing.T) { today := model.ThingsDateFromTime(testNow) - yesterday := model.ThingsDateFromTime(testNow.AddDate(0, 0, -1)) - tomorrow := model.ThingsDateFromTime(testNow.AddDate(0, 0, 1)) + yesterday := model.ThingsDateFromTime(testNow).AddDays(-1) + tomorrow := model.ThingsDateFromTime(testNow).AddDays(1) for _, tc := range []struct { name string place whenPlace @@ -653,7 +653,7 @@ func TestWhenPlaceHoldsCarriedOver(t *testing.T) { // certain no-op: --when today on a to-do in the day part with no reminder. // The rest are sent and read back. func TestWhenUnchangedCarriedOver(t *testing.T) { - yesterday := model.ThingsDateFromTime(testNow.AddDate(0, 0, -1)) + yesterday := model.ThingsDateFromTime(testNow).AddDays(-1) noReminder := func() (int, error) { return reminderNone, nil } at := func(min int) func() (int, error) { return func() (int, error) { return min, nil } } later := testNow.Add(2 * time.Hour) diff --git a/internal/db/membership_test.go b/internal/db/membership_test.go index dae6851c..3a0feda6 100644 --- a/internal/db/membership_test.go +++ b/internal/db/membership_test.go @@ -23,8 +23,8 @@ func listSet(t *testing.T, d *DB, view string, opts TaskFilter) []string { func TestDeadlineProjectInTodayAndUpcoming(t *testing.T) { d, fx := newFixture(t) today := int64(model.ThingsDateFromTime(testNow)) - earlier := int64(model.ThingsDateFromTime(testNow.AddDate(0, 0, -2))) - later := int64(model.ThingsDateFromTime(testNow.AddDate(0, 0, 3))) + earlier := int64(model.ThingsDateFromTime(testNow).AddDays(-2)) + later := int64(model.ThingsDateFromTime(testNow).AddDays(3)) fx.Project("p-due-today", "Due today", 1, anytime(), deadline(today)) fx.Project("p-overdue", "Overdue", 2, anytime(), deadline(earlier)) @@ -54,7 +54,7 @@ func TestDeadlineProjectInTodayAndUpcoming(t *testing.T) { func TestSomedayDeadlineInUpcomingAndToday(t *testing.T) { d, fx := newFixture(t) today := int64(model.ThingsDateFromTime(testNow)) - later := int64(model.ThingsDateFromTime(testNow.AddDate(0, 0, 3))) + later := int64(model.ThingsDateFromTime(testNow).AddDays(3)) fx.Todo("t-someday-later", "Someday to-do due later", 1, someday(), deadline(later)) fx.Project("p-someday-later", "Someday project due later", 2, someday(), deadline(later)) @@ -199,7 +199,7 @@ func TestSearchSkipsTrashedProjectChildren(t *testing.T) { // untrashedParent. Pinned here so search and the lists cannot drift apart. func TestListViewsSkipTrashedProjectChildren(t *testing.T) { d, fx := newFixture(t) - later := int64(model.ThingsDateFromTime(testNow.AddDate(0, 0, 3))) + later := int64(model.ThingsDateFromTime(testNow).AddDays(3)) fx.Area("area-work", "Work", 1) fx.Tag("tg-urgent", "urgent", 1) diff --git a/internal/db/projects.go b/internal/db/projects.go index 86ae4dc4..c1afb133 100644 --- a/internal/db/projects.go +++ b/internal/db/projects.go @@ -9,7 +9,7 @@ import ( // ListProjects lists the projects, in area order. includeCompleted lists the // closed ones too, logged or not. Without it the listing is the open projects // and, unless openOnly is set, the ones closed and not yet logged -// (closedTodayUnlogged), which the app's own project list still holds: on 9 +// (closedUnlogged), which the app's own project list still holds: on 9 // Oct 2026 AppleScript's `projects` returned a project completed that day // that `things projects` left out. That is the rule the task lists follow. func (d *DB) ListProjects(areaFilter string, includeCompleted, openOnly bool) ([]model.Project, error) { diff --git a/internal/db/tasks.go b/internal/db/tasks.go index 0150ba16..3cee36d7 100644 --- a/internal/db/tasks.go +++ b/internal/db/tasks.go @@ -216,9 +216,11 @@ func scanTask(row rowScanner) (model.Task, error) { // project, never rows in a list, so they stay out. const todoOrProject = "t.type IN (0, 1)" -// closedTodayUnlogged is the app's rule for a closed item Things has not yet filed -// into the Logbook. Two conditions hold at once, and measuring against the app -// on 10 Sep 2026 is what established the pair (issue #230). +// closedUnlogged is the app's rule for a closed item Things has not yet filed +// into the Logbook, for each choice of its "Move completed items to Logbook" +// setting (see the end of this comment). Under "daily", two conditions hold +// at once, and measuring against the app on 10 Sep 2026 is what established +// the pair (issue #230). // // The calendar day is the boundary Things actually keeps: the app's Today held // six closed items, all closed that day, while its Logbook held forty items @@ -246,9 +248,9 @@ const todoOrProject = "t.type IN (0, 1)" // Now", whatever day it closed. Any other value, or no setting row, keeps the // daily rule. The other two were read from the app's preferences, not measured // in the live app, as that would mean changing the user's setting. -var closedTodayUnlogged = closedUnloggedOf("t") +var closedUnlogged = closedUnloggedOf("t") -// closedUnloggedOf is closedTodayUnlogged asked of the TMTask row aliased +// closedUnloggedOf is closedUnlogged asked of the TMTask row aliased // alias. func closedUnloggedOf(alias string) string { stopDate := "COALESCE(" + alias + ".stopDate, 0)" @@ -336,7 +338,7 @@ const todayDate = "COALESCE(t.startDate, " + thingsToday + ")" // thingsToday is today's local date in the ThingsDate encoding // (year<<16 | month<<12 | day<<7), so it compares directly with startDate and -// deadline. It comes from today_clock, as closedTodayUnlogged's day does. +// deadline. It comes from today_clock, as closedUnlogged's daily rule does. const thingsToday = `(SELECT day FROM ` + todayClock + `)` // heldInPlace is the set the Logbook withholds: every closed row Things has @@ -356,7 +358,7 @@ const thingsToday = `(SELECT day FROM ` + todayClock + `)` // withholding such a row would take it out of every list. The Logbook keeps // them (includesTemplates) at once. Both halves of notATemplate are "IS NULL" // tests, which are never NULL themselves. -var heldInPlace = "(" + closedTodayUnlogged + " AND " + notATemplate + ")" +var heldInPlace = "(" + closedUnlogged + " AND " + notATemplate + ")" // notATemplate excludes the rows those views never carry: the template // row itself, and a to-do inside a repeating project template, which carries @@ -382,7 +384,7 @@ const parentClosed = "COALESCE(p.status, 0) IN (2, 3)" // table gives. const parentNotClosed = "NOT (" + parentClosed + ")" -// parentCloseUnlogged is closedTodayUnlogged asked of the parent project. +// parentCloseUnlogged is closedUnlogged asked of the parent project. var parentCloseUnlogged = closedUnloggedOf("p") // parentNotClosedOrUnlogged is the fold the views that show unlogged rows apply: @@ -444,7 +446,7 @@ func openOrJustClosed(o whereOpts, foldsUnlogged bool) string { // projects` and AddTarget all take. fold, when not empty, is a further test // on the closed rows alone. func openOrUnlogged(fold string) string { - closed := closedRows + " AND " + closedTodayUnlogged + closed := closedRows + " AND " + closedUnlogged if fold != "" { closed += " AND " + fold } diff --git a/internal/db/tasks_test.go b/internal/db/tasks_test.go index a733b06b..849972dd 100644 --- a/internal/db/tasks_test.go +++ b/internal/db/tasks_test.go @@ -177,10 +177,10 @@ func TestListTasksTodayCompletedItemFiltering(t *testing.T) { d, fx := newFixture(t) seedTasks(t, d) - // AddDate keeps the ThingsDate valid across month boundaries; raw bit + // AddDays keeps the ThingsDate valid across month boundaries; raw bit // subtraction would underflow the day field to 0 on the 1st. today := int64(model.ThingsDateFromTime(testNow)) - yesterday := int64(model.ThingsDateFromTime(testNow.AddDate(0, 0, -1))) + yesterday := int64(model.ThingsDateFromTime(testNow).AddDays(-1)) // Now, not "a minute ago": the calendar day decides membership since issue // #230, and a minute before midnight falls on the previous day. 25 hours // back is safely not today whatever the hour. @@ -343,7 +343,7 @@ func TestLogbookWithholdsEveryRowClosedToday(t *testing.T) { now := testNow stop := model.TimeToUnix(now) today := int64(model.ThingsDateFromTime(now)) - later := int64(model.ThingsDateFromTime(now.AddDate(0, 0, 4))) + later := int64(model.ThingsDateFromTime(now).AddDays(4)) cases := []struct { name string @@ -444,7 +444,7 @@ func TestLogbookWithholdsEveryRowClosedToday(t *testing.T) { // the Logbook or in those lists, never both and never neither. func TestClosedTodayOutsideTodayLandsInOneList(t *testing.T) { today := int64(model.ThingsDateFromTime(testNow)) - future := int64(model.ThingsDateFromTime(testNow.AddDate(0, 0, 3))) + future := int64(model.ThingsDateFromTime(testNow).AddDays(3)) stopNow := model.TimeToUnix(testNow) cases := []struct { @@ -508,7 +508,7 @@ func TestClosedTodayOutsideTodayLandsInOneList(t *testing.T) { // of the day, not under Today and not in the Logbook. func TestClosedSuppressedInboxToDoStaysInInbox(t *testing.T) { d, fx := newFixture(t) - earlier := int64(model.ThingsDateFromTime(testNow.AddDate(0, 0, -2))) + earlier := int64(model.ThingsDateFromTime(testNow).AddDays(-2)) fx.Todo("t-closed", "Closed", 1, inbox(), deadline(earlier), suppressed(earlier), completed(model.TimeToUnix(testNow))) logged := mustList(t, d, "logbook", TaskFilter{}) @@ -598,9 +598,9 @@ func TestListTasksDateFilters(t *testing.T) { // Upcoming holds only the days after today (issue #363), so the dates // are days to come rather than fixed ones. now := testNow - d1 := int64(model.ThingsDateFromTime(now.AddDate(0, 0, 9))) - d2 := int64(model.ThingsDateFromTime(now.AddDate(0, 0, 10))) - d3 := int64(model.ThingsDateFromTime(now.AddDate(0, 0, 11))) + d1 := int64(model.ThingsDateFromTime(now).AddDays(9)) + d2 := int64(model.ThingsDateFromTime(now).AddDays(10)) + d3 := int64(model.ThingsDateFromTime(now).AddDays(11)) fx.Todo("u-09", "Day 9", 1, somedayOn(d1), inArea("a")) fx.Todo("u-10", "Day 10", 2, somedayOn(d2), inArea("a")) @@ -1400,7 +1400,7 @@ func TestProjectFilterIncludeCompletedKeepsClosedToday(t *testing.T) { stopToday := model.TimeToUnix(testNow) stopYesterday := model.TimeToUnix(testNow.Add(-25 * time.Hour)) - later := int64(model.ThingsDateFromTime(testNow.AddDate(0, 0, 2))) + later := int64(model.ThingsDateFromTime(testNow).AddDays(2)) fx.Project("proj-open", "Live", 1) fx.Heading("head", "Phase 1", 2, inProject("proj-open")) @@ -1441,7 +1441,7 @@ func TestAreaFilterIncludeCompletedKeepsClosedToday(t *testing.T) { stopToday := model.TimeToUnix(testNow) stopYesterday := model.TimeToUnix(testNow.Add(-25 * time.Hour)) - later := int64(model.ThingsDateFromTime(testNow.AddDate(0, 0, 2))) + later := int64(model.ThingsDateFromTime(testNow).AddDays(2)) fx.Area("ar", "Work", 1) fx.Project("proj-open", "Live", 1, inArea("ar")) fx.Project("proj-done", "Shipped", 2, inArea("ar"), completed(stopToday)) @@ -1535,8 +1535,8 @@ func TestListTasksProjectOrderMatchesProjectPage(t *testing.T) { d, fx := newFixture(t) now := testNow today := int64(model.ThingsDateFromTime(now)) - sooner := int64(model.ThingsDateFromTime(now.AddDate(0, 0, 7))) - later := int64(model.ThingsDateFromTime(now.AddDate(0, 0, 17))) + sooner := int64(model.ThingsDateFromTime(now).AddDays(7)) + later := int64(model.ThingsDateFromTime(now).AddDays(17)) fx.Project("proj", "Ship v2", 1) fx.Heading("head-1", "Phase one", -357, inProject("proj")) @@ -2221,7 +2221,7 @@ func TestAnytimeLeavesOutToDosInDeferredProjects(t *testing.T) { now := testNow today := int64(model.ThingsDateFromTime(now)) - later := int64(model.ThingsDateFromTime(now.AddDate(0, 0, 7))) + later := int64(model.ThingsDateFromTime(now).AddDays(7)) fx.Project("proj-someday", "Someday project", 1, someday()) fx.Project("proj-scheduled", "Scheduled project", 2, somedayOn(later)) @@ -2258,7 +2258,7 @@ func TestAnytimeLeavesOutToDosInDeferredProjects(t *testing.T) { // day (issue #238). func TestClosedTodayInDeferredProjectLandsInOneList(t *testing.T) { now := testNow - later := int64(model.ThingsDateFromTime(now.AddDate(0, 0, 7))) + later := int64(model.ThingsDateFromTime(now).AddDays(7)) stop := model.TimeToUnix(now) cases := []struct { @@ -2405,9 +2405,9 @@ func TestUpcomingListsAnytimeToDosDueLater(t *testing.T) { now := testNow today := int64(model.ThingsDateFromTime(now)) - tomorrow := int64(model.ThingsDateFromTime(now.AddDate(0, 0, 1))) - later := int64(model.ThingsDateFromTime(now.AddDate(0, 0, 3))) - yesterday := int64(model.ThingsDateFromTime(now.AddDate(0, 0, -1))) + tomorrow := int64(model.ThingsDateFromTime(now).AddDays(1)) + later := int64(model.ThingsDateFromTime(now).AddDays(3)) + yesterday := int64(model.ThingsDateFromTime(now).AddDays(-1)) fx.Todo("sched-tomorrow", "Scheduled tomorrow", 1, somedayOn(tomorrow), todayIndex(-100)) fx.Todo("due-tomorrow", "Due tomorrow", 2, anytime(), deadline(tomorrow), todayIndex(-500)) @@ -2441,8 +2441,8 @@ func TestTodayListsUndatedToDosDueOrOverdue(t *testing.T) { now := testNow today := int64(model.ThingsDateFromTime(now)) - tomorrow := int64(model.ThingsDateFromTime(now.AddDate(0, 0, 1))) - earlier := int64(model.ThingsDateFromTime(now.AddDate(0, 0, -2))) + tomorrow := int64(model.ThingsDateFromTime(now).AddDays(1)) + earlier := int64(model.ThingsDateFromTime(now).AddDays(-2)) fx.Todo("sched-today", "Scheduled today", 1, anytimeOn(today)) fx.Todo("anytime-due-today", "Anytime due today", 2, anytime(), deadline(today)) @@ -2468,7 +2468,7 @@ func TestTodayListsUndatedToDosDueOrOverdue(t *testing.T) { on := model.ThingsDate(today) got = mustList(t, d, "today", TaskFilter{On: &on}) assertSet(t, got, want, "today --on today") - from := model.ThingsDate(int64(model.ThingsDateFromTime(now.AddDate(0, 0, -1)))) + from := model.ThingsDate(int64(model.ThingsDateFromTime(now).AddDays(-1))) got = mustList(t, d, "today", TaskFilter{From: &from}) assertSet(t, got, want, "today --from yesterday") } @@ -2484,8 +2484,8 @@ func TestScheduledRowNotYetMovedIsToday(t *testing.T) { now := testNow today := int64(model.ThingsDateFromTime(now)) - yesterday := int64(model.ThingsDateFromTime(now.AddDate(0, 0, -1))) - tomorrow := int64(model.ThingsDateFromTime(now.AddDate(0, 0, 1))) + yesterday := int64(model.ThingsDateFromTime(now).AddDays(-1)) + tomorrow := int64(model.ThingsDateFromTime(now).AddDays(1)) fx.Todo("stuck-today", "Scheduled today, not yet moved", 1, somedayOn(today)) fx.Todo("stuck-yesterday", "Scheduled yesterday, not yet moved", 2, somedayOn(yesterday)) @@ -2529,7 +2529,7 @@ func TestScheduledRowNotYetMovedReportsAnytime(t *testing.T) { now := testNow today := int64(model.ThingsDateFromTime(now)) - tomorrow := int64(model.ThingsDateFromTime(now.AddDate(0, 0, 1))) + tomorrow := int64(model.ThingsDateFromTime(now).AddDays(1)) done := float64(now.Unix()) fx.Todo("stuck-today", "Scheduled today, not yet moved", 1, somedayOn(today)) @@ -2593,7 +2593,7 @@ func TestAnytimeKeepsToDosOfProjectNotYetMoved(t *testing.T) { now := testNow today := int64(model.ThingsDateFromTime(now)) - tomorrow := int64(model.ThingsDateFromTime(now.AddDate(0, 0, 1))) + tomorrow := int64(model.ThingsDateFromTime(now).AddDays(1)) fx.Project("proj-stuck", "Project scheduled today, not yet moved", 1, somedayOn(today)) fx.Project("proj-tomorrow", "Project scheduled tomorrow", 2, somedayOn(tomorrow)) @@ -2614,7 +2614,7 @@ func TestProjectPageOrdersNotYetMovedRowAsToday(t *testing.T) { now := testNow today := int64(model.ThingsDateFromTime(now)) - tomorrow := int64(model.ThingsDateFromTime(now.AddDate(0, 0, 1))) + tomorrow := int64(model.ThingsDateFromTime(now).AddDays(1)) fx.Project("proj", "Project", 1, anytime()) fx.Todo("sched-tomorrow", "Scheduled tomorrow", 1, somedayOn(tomorrow), inProject("proj")) @@ -2635,8 +2635,8 @@ func TestInboxLeavesOutToDosTodayHolds(t *testing.T) { now := testNow today := int64(model.ThingsDateFromTime(now)) - tomorrow := int64(model.ThingsDateFromTime(now.AddDate(0, 0, 1))) - earlier := int64(model.ThingsDateFromTime(now.AddDate(0, 0, -2))) + tomorrow := int64(model.ThingsDateFromTime(now).AddDays(1)) + earlier := int64(model.ThingsDateFromTime(now).AddDays(-2)) fx.Todo("inbox-plain", "Inbox plain", 1, inbox()) fx.Todo("inbox-due-today", "Inbox due today", 2, inbox(), deadline(today)) @@ -2665,8 +2665,8 @@ func TestAnytimeListsInboxToDosTodayHolds(t *testing.T) { now := testNow today := int64(model.ThingsDateFromTime(now)) - tomorrow := int64(model.ThingsDateFromTime(now.AddDate(0, 0, 1))) - earlier := int64(model.ThingsDateFromTime(now.AddDate(0, 0, -2))) + tomorrow := int64(model.ThingsDateFromTime(now).AddDays(1)) + earlier := int64(model.ThingsDateFromTime(now).AddDays(-2)) fx.Todo("inbox-plain", "Inbox plain", 1, inbox()) fx.Todo("inbox-due-today", "Inbox due today", 2, inbox(), deadline(today)) @@ -2874,8 +2874,8 @@ func TestTodayOrdersByReferenceDateThenTodayIndex(t *testing.T) { d, fx := newFixture(t) today := int64(model.ThingsDateFromTime(testNow)) - yesterday := int64(model.ThingsDateFromTime(testNow.AddDate(0, 0, -1))) - earlier := int64(model.ThingsDateFromTime(testNow.AddDate(0, 0, -3))) + yesterday := int64(model.ThingsDateFromTime(testNow).AddDays(-1)) + earlier := int64(model.ThingsDateFromTime(testNow).AddDays(-3)) stop := model.TimeToUnix(testNow) fx.Todo("old-first", "One", 1, anytimeOn(today), todayIndex(-900), todayIndexRef(earlier)) fx.Todo("new-second", "Two", 2, anytimeOn(today), todayIndex(-100), todayIndexRef(today)) @@ -2928,7 +2928,7 @@ func TestUpcomingIncludeCompleted(t *testing.T) { stopToday := model.TimeToUnix(testNow) stopYesterday := model.TimeToUnix(testNow.Add(-25 * time.Hour)) - later := int64(model.ThingsDateFromTime(testNow.AddDate(0, 0, 2))) + later := int64(model.ThingsDateFromTime(testNow).AddDays(2)) fx.Todo("open-later", "Open", 1, somedayOn(later)) fx.Todo("done-ahead", "Done ahead", 2, somedayOn(later), completed(stopToday)) diff --git a/internal/db/view_order_test.go b/internal/db/view_order_test.go index 6ef887d1..92ab9e17 100644 --- a/internal/db/view_order_test.go +++ b/internal/db/view_order_test.go @@ -35,8 +35,8 @@ func TestTodayListsEveningRowsLast(t *testing.T) { func TestTodayOrdersProjectGroupsBySchedule(t *testing.T) { d, fx := newFixture(t) today := int64(model.ThingsDateFromTime(testNow)) - soon := int64(model.ThingsDateFromTime(testNow.AddDate(0, 0, 3))) - later := int64(model.ThingsDateFromTime(testNow.AddDate(0, 0, 6))) + soon := int64(model.ThingsDateFromTime(testNow).AddDays(3)) + later := int64(model.ThingsDateFromTime(testNow).AddDays(6)) fx.Project("p-someday-b", "Someday B", 0, someday()) fx.Project("p-later", "Later", 0, somedayOn(later)) @@ -103,8 +103,8 @@ func TestSomedayListsProjectRowsFirst(t *testing.T) { // CLI keeps them after the loose ones, the order Anytime uses. func TestAreaListingOrdersProjectsThenLooseThenChildren(t *testing.T) { d, fx := newFixture(t) - soon := int64(model.ThingsDateFromTime(testNow.AddDate(0, 0, 3))) - later := int64(model.ThingsDateFromTime(testNow.AddDate(0, 0, 6))) + soon := int64(model.ThingsDateFromTime(testNow).AddDays(3)) + later := int64(model.ThingsDateFromTime(testNow).AddDays(6)) fx.Area("ar", "Personal", -3070) fx.Project("p1", "P1", -4767, anytime(), inArea("ar")) diff --git a/tools/compare/run.sh b/tools/compare/run.sh new file mode 100755 index 00000000..fc749d0d --- /dev/null +++ b/tools/compare/run.sh @@ -0,0 +1,76 @@ +#!/bin/bash +# Compare two things binaries on the same input, to check a change keeps +# behaviour. Each binary runs against its own fresh copy of a seeded test +# database, with `open` and `osascript` replaced by shims that only log +# (nothing reaches the Things app). The script then diffs stdout, stderr, +# the exit code, the shim log and the task statuses left in the database. +# +# Usage: +# OLD=path/to/things-old NEW=path/to/things-new tools/compare/run.sh NAME STDIN ARGS... +# +# NAME a label for this case; outputs go to $OUT/NAME.old and $OUT/NAME.new +# STDIN a file to feed on stdin, or - for none +# ARGS the things command line, e.g. show TODO1 +# +# Example: +# git worktree add /tmp/things-main origin/main +# (cd /tmp/things-main && go build -o /tmp/things-old ./cmd/things) +# go build -o /tmp/things-new ./cmd/things +# OLD=/tmp/things-old NEW=/tmp/things-new tools/compare/run.sh show-1 - show TODO1 +# +# Prints "SAME NAME (exit N, shim N)", or "DIFF NAME" and the first lines of +# the diff and exits 1. Set SHIMAPPLY=1 for the osascript shim to apply +# complete and cancel to the database copy, so a read-back can see them. The database is +# built from internal/db/dbtest/schema.sql and tools/compare/seed.sql. OUT +# defaults to a fresh temporary directory. +# +# A refusal that only the new binary makes means the old one writes: compare +# only inputs both refuse, or use the test stubs. +set -u +here=$(cd "$(dirname "$0")" && pwd) +root=$(cd "$here/../.." && pwd) +: "${OLD:?set OLD to the old things binary}" +: "${NEW:?set NEW to the new things binary}" +OUT=${OUT:-$(mktemp -d)} +mkdir -p "$OUT" +[ $# -ge 2 ] || { echo "usage: OLD=... NEW=... $0 NAME STDIN|- ARGS..." >&2; exit 2; } +name=$1; shift; in=$1; shift + +# Resolve both binaries now: under env -i PATH is the shim directory, so a +# bare name would not be found and both runs would fail alike as "SAME". +OLD=$(command -v "$OLD") || { echo "OLD not found" >&2; exit 2; } +NEW=$(command -v "$NEW") || { echo "NEW not found" >&2; exit 2; } +case $OLD in /*) ;; *) OLD=$PWD/$OLD;; esac +case $NEW in /*) ;; *) NEW=$PWD/$NEW;; esac + +# Built under a temporary name and moved into place, so a failed seed never +# leaves a half-built base for the next case to reuse. +base=$OUT/base.sqlite +if [ ! -f "$base" ]; then + rm -f "$base.tmp" + if ! /usr/bin/sqlite3 "$base.tmp" < "$root/internal/db/dbtest/schema.sql" || + ! /usr/bin/sqlite3 "$base.tmp" < "$here/seed.sql"; then + rm -f "$base.tmp"; exit 1 + fi + mv "$base.tmp" "$base" +fi + +for v in old new; do + bin=$OLD; [ $v = new ] && bin=$NEW + d=$OUT/$name.$v; rm -rf "$d"; mkdir -p "$d/home" + cp "$base" "$d/db.sqlite" + : > "$d/shim.log" + if [ "$in" = "-" ]; then inf=/dev/null; else inf=$in; fi + env -i HOME="$d/home" XDG_CONFIG_HOME="$d/home/.config" PATH="$here/shim" \ + SHIMLOG="$d/shim.log" SHIMDB="$d/db.sqlite" SHIMAPPLY="${SHIMAPPLY:-}" TZ=Europe/London \ + "$bin" --db "$d/db.sqlite" --color never --verify-timeout 600ms "$@" <"$inf" >"$d/stdout" 2>"$d/stderr" + echo $? > "$d/exit" + sed -i '' "s#$d##g" "$d/stdout" "$d/stderr" "$d/shim.log" + /usr/bin/sqlite3 "$d/db.sqlite" "select uuid,status,stopDate from TMTask order by uuid" > "$d/dbstate" +done +if diff -r -x db.sqlite -x home "$OUT/$name.old" "$OUT/$name.new" >/dev/null; then + echo "SAME $name (exit $(cat "$OUT/$name.new/exit"), shim $(wc -l < "$OUT/$name.new/shim.log" | tr -d ' '))" +else + echo "DIFF $name"; diff -r -x db.sqlite -x home "$OUT/$name.old" "$OUT/$name.new" | head -30 + exit 1 +fi diff --git a/tools/compare/seed.sql b/tools/compare/seed.sql new file mode 100644 index 00000000..b03f4cf4 --- /dev/null +++ b/tools/compare/seed.sql @@ -0,0 +1,22 @@ +-- Test data for tools/compare/run.sh, applied over internal/db/dbtest/schema.sql. +INSERT INTO TMSettings VALUES ('s','tok123',1,NULL); +INSERT INTO TMArea VALUES ('AREA1','Work',1,1); +INSERT INTO TMTag VALUES ('TAG1','urgent',NULL,NULL,1); +-- project open, project done +INSERT INTO TMTask (uuid,title,notes,type,status,creationDate,userModificationDate,trashed,start,startBucket,"index",area,untrashedLeafActionsCount,openUntrashedLeafActionsCount) VALUES + ('PROJ1','Alpha project','',1,0,1760000000,1760000000,0,1,0,1,'AREA1',2,1), + ('PROJ2','Beta project','',1,3,1760000000,1760000000,0,1,0,2,NULL,0,0); +INSERT INTO TMTask (uuid,title,notes,type,status,creationDate,userModificationDate,trashed,start,startBucket,"index",project) VALUES + ('HEAD1','Phase one','',2,0,1760000000,1760000000,0,1,0,1,'PROJ1'); +INSERT INTO TMTask (uuid,title,notes,type,status,stopDate,creationDate,userModificationDate,trashed,start,startBucket,startDate,deadline,"index",todayIndex,area,project,heading,rt1_recurrenceRule) VALUES + ('TODO1','Buy milk','some notes',0,0,NULL,1760000000,1760000000,0,1,0,132818176,NULL,1,1,NULL,NULL,NULL,NULL), + ('TODO2','Write report','',0,0,NULL,1760000000,1760000000,0,1,0,NULL,132818304,2,0,NULL,'PROJ1','HEAD1',NULL), + ('TODO3','Done thing','',0,3,1760090000,1760000000,1760000000,0,1,0,NULL,NULL,3,0,NULL,NULL,NULL,NULL), + ('TODO4','Cancelled thing','',0,2,1760090000,1760000000,1760000000,0,1,0,NULL,NULL,4,0,NULL,NULL,NULL,NULL), + ('TODO5','Repeat thing','',0,0,NULL,1760000000,1760000000,0,1,0,NULL,NULL,5,0,NULL,NULL,NULL,x'00'), + ('TODO6','Trashed thing','',0,0,NULL,1760000000,1760000000,1,1,0,NULL,NULL,6,0,NULL,NULL,NULL,NULL), + ('TODO7','Inbox item','',0,0,NULL,1760000000,1760000000,0,0,0,NULL,NULL,7,0,NULL,NULL,NULL,NULL), + ('TODO8','Someday item','',0,0,NULL,1760000000,1760000000,0,2,0,NULL,NULL,8,0,'AREA1',NULL,NULL,NULL), + ('TODO9','Buy milk','dup title',0,0,NULL,1760000000,1760000000,0,1,0,NULL,NULL,9,0,NULL,NULL,NULL,NULL); +INSERT INTO TMTaskTag VALUES ('TODO1','TAG1'); +INSERT INTO TMChecklistItem VALUES ('CL1','eggs',0,NULL,1,'TODO1'),('CL2','bread',3,1760090000,2,'TODO1'); diff --git a/tools/compare/shim/open b/tools/compare/shim/open new file mode 100755 index 00000000..5ba68ae5 --- /dev/null +++ b/tools/compare/shim/open @@ -0,0 +1,3 @@ +#!/bin/sh +echo "open $*" >> "$SHIMLOG" +exit 0 diff --git a/tools/compare/shim/osascript b/tools/compare/shim/osascript new file mode 100755 index 00000000..1f81aa04 --- /dev/null +++ b/tools/compare/shim/osascript @@ -0,0 +1,11 @@ +#!/bin/sh +printf 'osascript %s\n' "$*" >> "$SHIMLOG" +if [ -n "$SHIMAPPLY" ]; then + id=$(printf '%s' "$2" | /usr/bin/sed -n 's/.* id "\([^"]*\)".*/\1/p') + case "$2" in *"to completed"*) st=3;; *"to canceled"*) st=2;; *) exit 0;; esac + # Today's local midnight: a close the app would still hold unlogged, and + # the same value for both runs of a case. + stop=$(/bin/date -j -v0H -v0M -v0S +%s) + /usr/bin/sqlite3 "$SHIMDB" "UPDATE TMTask SET status=$st, stopDate=$stop, userModificationDate=$((stop + 1)) WHERE uuid='$id';" +fi +exit 0