From 72843aa11701c00f0f244a2524ae6bddb3df7162 Mon Sep 17 00:00:00 2001 From: kavehtoyser Date: Mon, 29 Jun 2026 21:10:31 +0200 Subject: [PATCH] Fix flaky test_read_timerange: neutralize the row-cleaner during the test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause (confirmed): clean_database() is the ONLY code path that deletes rows from a service-db table, and it re-reads DAEPLOY_SERVICE_DB_TABLE_LIMIT on every call. When a clean runs with a short *time* limit (the "1seconds" that the db_limit_second fixture sets) it trims the OLDEST rows as they age past the limit. If such a clean runs mid-test, test_read_timerange's reads disagree and a wider time window can return FEWER rows than a narrower one — observed as "assert 200 == 195/191/183/175". Demonstrated directly: clean_database() with a 1-second limit wipes a freshly-written 200-row series. Earlier attempts (unique timestamps 841d194; invariant assertions; ENGINE pool dispose) all missed this because the mutation is a delete, not a write drop or a stale snapshot. Fix: pin a huge DAEPLOY_SERVICE_DB_TABLE_LIMIT ("36500days") for the duration of the test via monkeypatch (auto-restored), so any concurrent clean_database is a no-op for this test's data. With deletion ruled out, all 200 rows persist and the original "== 200" filtering assertions hold deterministically. Co-Authored-By: Claude Opus 4.8 --- tests/sdk_test/daeploy_test.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/tests/sdk_test/daeploy_test.py b/tests/sdk_test/daeploy_test.py index 3e2dfd2..9fedf50 100644 --- a/tests/sdk_test/daeploy_test.py +++ b/tests/sdk_test/daeploy_test.py @@ -735,14 +735,22 @@ def test_edge_case_type_storing(database): assert db.read_from_ts("my.normal.float")[-1].value == 10.10 -def test_read_timerange(database): +def test_read_timerange(database, monkeypatch): + + # Root cause of this test's historic flakiness: clean_database() is the only + # code path that deletes rows, and it re-reads DAEPLOY_SERVICE_DB_TABLE_LIMIT + # on every call. A leaked periodic clean (e.g. with the "1seconds" limit that + # db_limit_second sets) trims the OLDEST rows as they age past the limit, + # mid-test — so successive reads disagreed and a wider window could return + # FEWER rows than a narrower one (200 == 195/191/...). Pin a huge limit for + # the duration of this test so any concurrent clean is a no-op for our data. + monkeypatch.setenv("DAEPLOY_SERVICE_DB_TABLE_LIMIT", "36500days") before = datetime.datetime.utcnow() # Explicit, strictly-increasing timestamps: the timestamp column is the # table's primary key, so repeated utcnow() within one microsecond would - # collide on the PK. (Unique timestamps alone did not fully de-flake this - # test — see the assertion comment below.) + # collide on the PK. timestamps = [before + datetime.timedelta(milliseconds=i + 1) for i in range(200)] mid = timestamps[100] @@ -752,23 +760,19 @@ def test_read_timerange(database): after = timestamps[-1] + datetime.timedelta(milliseconds=1) - # This test covers from_time/to_time *filtering*, not durable row counts - # (test_continuous_storing_of_and_reading_of_variables covers storage). Writes - # go through an async queue whose worker swallows the occasional failed write - # (e.g. a transient SQLite error on a loaded CI runner) yet still marks the - # queue task done, so asserting a hard-coded "== 200" flakes when a write is - # dropped. Assert the filtering invariants against the actual stored set: - # * open-ended from_time/to_time default to datetime.min / utcnow(), so all - # three "full window" queries must return the same set, and - # * narrowing to_time to the midpoint must return a strict, non-empty subset. + # Check the from_time/to_time defaults: open-ended from_time/to_time default + # to datetime.min / utcnow(), so all three "full window" forms return the + # same set; narrowing to_time to the midpoint returns a strict, non-empty + # subset. full = len(db.read_from_ts("float", from_time=before, to_time=after)) assert ( full == len(db.read_from_ts("float", to_time=after)) == len(db.read_from_ts("float", from_time=before)) + == 200 ) - assert 0 < len(db.read_from_ts("float", from_time=before, to_time=mid)) < full + assert 0 < len(db.read_from_ts("float", from_time=before, to_time=mid)) < 200 def test_database_limit_rows(database, db_limit_rows):