feat: read time via package:clock, fix flush() leaving a timer armed - #8
Merged
Conversation
Debounce read the wall clock directly through DateTime.now(), which made its timing behaviour impossible to control from a test. Reading through package:clock means a test can install its own clock, and in particular fakeAsync now drives both the Timer queue and the clock in lockstep. Adds test/fake_async_test.dart covering the debounce trailing edge, maxWait, and throttle — all of which run instantly instead of waiting on real time. These tests fail if the clock.now() calls are reverted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
package:clock 1.1.2 requires sdk ^3.4.0, so rate_limiter has to match. That is breaking for consumers on Dart 2.x, hence the 2.0.0 bump. The old workflow pinned Flutter 2.2.1 (Dart 2.13) via flutter-action v1.4.0, which cannot resolve the new constraint, so it moves to setup-dart. Coverage is now produced by package:coverage, with --base-directory so the paths in lcov.info stay repo-relative and Codecov can map them. The Check Code Coverage step has been failing since 4e1e2b0 added BackOffExtension.backOff() without a test, taking coverage from 100% to 96.97% (94.95% on CI) against a 97% gate. Covered by two tests in extension_test.dart; the first-attempt-succeeds path costs no delay. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #8 +/- ##
===========================================
+ Coverage 97.43% 100.00% +2.56%
===========================================
Files 3 4 +1
Lines 78 100 +22
===========================================
+ Hits 76 100 +24
+ Misses 2 0 -2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Adopts the very_good_workflows build layout, adapted to this repo: - setup-dart rather than flutter-action, since this is a pure Dart package with no Flutter dependency. - The matrix pins the package's minimum SDK (3.4.0) alongside stable, so the declared `environment` constraint is actually exercised. - Triggers on master, not main. - Actions moved to their current majors, and a concurrency group cancels superseded runs. Coverage comes from package:coverage with --base-directory so the paths in lcov.info stay repo-relative; the previous absolute paths would have left Codecov unable to map files. The very_good_coverage gate is kept at 97, unchanged from before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The 3.4.0 matrix leg reported debounce.dart:217,219,220 uncovered while stable reported 100%. That branch was reached only by the wall-clock "maxWait in a tight loop" test, so whether it ran at all depended on how fast the runner was — the same fragility behind the 96/99 vs 94/99 gap seen on master. fakeAsync's elapseBlocking advances the clock without running the pending timer, which is exactly what a synchronous tight loop does, so the branch is now exercised without depending on real time. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follows the layout dart-lang/coverage uses: the matrix job runs format, analyze and tests across the SDK range, and a separate job gated on it measures coverage once. Previously every matrix leg recomputed the same numbers. Keeping format_coverage rather than the test_with_coverage one-liner that pub.dev recommends: test_with_coverage has no --base-directory, and Codecov can only map files when the paths in lcov.info are relative to the repository root. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The debounce and throttle suites waited on real elapsed time: `delay()`
calls between assertions, and busy-wait loops burning wall-clock so real
Timers could fire. That made coverage depend on how fast the machine was,
which is what produced the 3.4.0 matrix failure earlier in this branch.
Every `delay()` becomes `elapse`, and the two busy-wait loops become
`elapseBlocking`, which advances the clock without running timers — what
a synchronous tight loop does to a real program. fake_async_test.dart is
folded back in, since its cases are now covered in place, and the unused
`delay()` helper is gone.
Some assertions were verifying less than they appeared to:
- Both `cancel` tests asserted callCount == 0 at 30ms, before the 32ms
trailing call was ever due. They passed whether or not `cancel` did
anything. They now elapse past the deadline.
- `isNot('b')` / `isNotNull` checks on returned results only ruled out one
wrong answer. A fixed clock makes the right answer knowable, so they
assert it.
- The throttle leading/trailing counts were compared against a snapshot
variable rather than stated outright.
Counts that encode loop iterations rather than documented behaviour are
left as `greaterThan(1)`.
Reverting clock.now() to DateTime.now() now fails 19 tests, up from 3.
Coverage is unchanged at 99/99 lines, verified line-by-line rather than by
total, and the suite runs in ~0.7s instead of ~3s.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`_trailingEdge` detached the timer without cancelling it. Reached through `flush`, where the timer is still armed, the orphan fired later, found nothing to invoke and rescheduled itself — so `isPending` reported `true` again roughly 23ms after a flush, and settled only once the reschedule expired. `isPending` is the getter the docs point users at, so it was returning the wrong answer for a documented workflow. Release is 1.1.0 rather than 2.0.0. The public API is unchanged, and raising the SDK floor is not breaking in pub's terms: consumers on an older SDK resolve to 1.0.0 and keep building. package:clock made the same 2.12 -> ^3.4.0 move in a patch release. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ported from super_paging, adapted to this repo: - Tag filter matches this repository's unprefixed tags (1.0.0, 0.1.1). The original only matched a `v` prefix, which would never have fired here. The prefixed form is accepted too, so either style works. - No flutter-action, and `dart pub get` rather than `flutter pub get`, matching the rest of the workflows for a pure Dart package. - Least-privilege permissions, following #7: read at the top level, with id-token: write scoped to the publishing job. Note this needs automated publishing enabled for the package on pub.dev, pointed at this repository and tag pattern, before the OIDC exchange in `dart pub publish --force` can succeed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Automated publishing on pub.dev is set to v[0-9]+.[0-9]+.[0-9]+*, and it matches that against the OIDC token. Triggering on unprefixed tags as well would start a run that could only fail at the publish step, so the workflow now accepts exactly what pub.dev does. Releases through 1.0.0 were tagged without the prefix; from here on the tag needs to be v1.1.0 rather than 1.1.0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dropped the file headers and the comments that paraphrased the assertion below them, and condensed the rest to a single line, matching the comment density already in the codebase. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`toBool` lost its last caller when the suite moved to fakeAsync, and the 1.1.0 heading was missing the date the other entries carry. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Debounceread the wall clock directly viaDateTime.now(), so its timing behaviour couldn't be controlled from a test. It now reads throughpackage:clock— three call sites inlib/src/debounce.dart.Throttledelegates toDebounceand benefits for free;BackOffusesFuture.delayedand needs no change.clockalone only makes the time source mockable —Timeris still real. The payoff isfakeAsync, which installs both a fakeTimerqueue and a clock that advances withelapse(). The whole debounce/throttle suite now runs on it.Public API is unchanged — the diff under
lib/is three time reads and oneTimer.cancel().Bug fix:
flush()left its timer armed_trailingEdgedetached the timer without cancelling it. Reached throughflush()— where the timer is still armed — the orphan fired later, found nothing to invoke, and rescheduled itself. Measured before the fix:No double invocation, but
isPendingreturned the wrong answer — and it's the getter the docs point users at (debounced.isPending ? "Pending..." : "Ready"). One line in_trailingEdge, with regression tests in both suites that fail without it.This has been shipping since 0.1.0. It was only findable once the suite moved to a fake clock; under real time the test always finished before the orphan fired.
Versioning:
1.1.0clock 1.1.2declaressdk: ^3.4.0, sorate_limitermatches. That is not breaking in pub's terms — consumers on an older SDK resolve to1.0.0and keep building.package:clockitself made the same>=2.12.0 <3.0.0→^3.4.0move in a patch release (1.1.1 → 1.1.2).Pinning
clockbelow 1.1.2 to hold the old floor was considered and rejected: it needs an upper bound like>=1.1.0 <1.1.2, which causes resolution conflicts for any consumer that also depends on something wantingclock ^1.1.2.Test suite now runs on a fake clock
Every
delay()becameelapse; the two busy-wait loops becameelapseBlocking, which advances the clock without running timers — what a synchronous tight loop does to a real program. ~0.7s instead of ~3s, with no dependence on machine speed.Reviewing them against a now-knowable clock turned up assertions verifying less than they appeared to:
canceltests assertedcallCount == 0at 30ms — before the 32ms trailing call was ever due. They passed whether or notcancel()did anything. They now elapse past the deadline.isNot('b')/isNotNullchecks only ruled out one wrong answer; they now assert the right one.Counts that encode loop iterations rather than documented behaviour are deliberately left as
greaterThan(1).Coverage
Check Code Coveragehas failed on master since4e1e2b0(BackOff) addedBackOffExtension.backOff()with no test:4e1e2b0~1(before BackOff)masterVerified line-by-line, not just by total, across the
fakeAsyncport — no line silently traded for another.CI
The old workflow pinned
flutter-action@v1.4.0→ Flutter 2.2.1 → Dart 2.13, which cannot resolvesdk: ^3.4.0. Rebuilt on thevery_good_workflowsbuild layout, adapted:dart-lang/setup-dartinstead offlutter-action— pure Dart package; Flutter was only ever a vehicle for its bundled Dart SDK.3.4.0,stable). It earned its keep immediately: it caught themaxWaittight-loop branch uncovered on3.4.0but notstable, reachable only via a wall-clock test. That finding is what motivated the suite port.needs: build), matchingdart-lang/coverage's layout.format_coverageover thetest_with_coverageone-liner pub.dev recommends: it has no--base-directory, and Codecov can only map files when lcov paths are repo-relative.very_good_coveragegate at 97,permissions: contents: readpreserved from CI: add least-privilege permissions to GitHub Actions workflows #7.New:
pub_publish.yamlPorted from
super_paging, adapted: noflutter-action,dart pub get, and least-privilege permissions withid-token: writescoped to the publish job. The tag filter isv[0-9]+.[0-9]+.[0-9]+*to match what pub.dev is configured with — note releases through 1.0.0 were tagged without thevprefix, so the next tag needs to bev1.1.0.--force(no confirmation). It requires automated publishing to be enabled for the package on pub.dev first; until then the job fails at the publish step.Verification
All checks green. Locally:
dart format --set-exit-if-changed .,dart analyze --fatal-infos ., 39 tests under randomized ordering, coverage 100%,dart pub publish --dry-runclean, andexample/resolves and analyzes against the new SDK floor.Mutation checks — reverting
clock.now()→DateTime.now()fails 19 tests (3 before the port); removing the_trailingEdgecancel fails both new flush tests.Known and accepted
clock.now()does aZonelookup per call, now onDebounce.call()'s hot path.lib, sotest/is not lint-gated. Passing.instead would gate it — an improvement rather than a restoration, since the old whole-repo check ran under a Dart 2.13 analyzer.🤖 Generated with Claude Code