commit f6fa69844bc790b6dbec56f88848254870b1326e
parent 0005c5b5ba41d13dbd3e331366956f9939816464
Author: Florian Dold <dold@taler.net>
Date: Mon, 20 Jul 2026 20:43:16 +0200
util: test duration parsing and timer lifetime
Diffstat:
2 files changed, 80 insertions(+), 0 deletions(-)
diff --git a/packages/taler-util/src/time.test.ts b/packages/taler-util/src/time.test.ts
@@ -38,3 +38,13 @@ test("duration parsing", (t) => {
Duration.fromPrettyString("5 5 s");
});
});
+
+test("duration parsing rejects a trailing number with no unit", (t) => {
+ // A number that is never flushed into the accumulator was silently
+ // discarded, so these all yielded a *smaller* duration with no error.
+ assert.throws(() => Duration.fromPrettyString("5"));
+ assert.throws(() => Duration.fromPrettyString("1h 30"));
+ assert.throws(() => Duration.fromPrettyString("1h30"));
+ assert.throws(() => Duration.fromPrettyString(""));
+ assert.throws(() => Duration.fromPrettyString(" "));
+});
diff --git a/packages/taler-util/src/timer.test.ts b/packages/taler-util/src/timer.test.ts
@@ -0,0 +1,70 @@
+/*
+ This file is part of GNU Taler
+ (C) 2026 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { test } from "node:test";
+import assert from "node:assert";
+import { TimerGroup, SetTimeoutTimerAPI } from "./timer.js";
+import { Duration } from "./time.js";
+import { CancellationToken } from "./CancellationToken.js";
+
+/**
+ * Number of timers the group is currently tracking. A one-shot timer that has
+ * already fired must not be tracked any more, otherwise the map grows for the
+ * lifetime of the group.
+ */
+function trackedTimers(tg: TimerGroup): number {
+ return Object.keys((tg as any).timerMap).length;
+}
+
+test("TimerGroup releases one-shot timers once they have fired", async (t) => {
+ const tg = new TimerGroup(new SetTimeoutTimerAPI());
+ for (let i = 0; i < 50; i++) {
+ tg.after(1, () => {});
+ }
+ await new Promise((r) => setTimeout(r, 80));
+ assert.strictEqual(trackedTimers(tg), 0);
+});
+
+test("TimerGroup releases timers created by resolveAfter", async (t) => {
+ const tg = new TimerGroup(new SetTimeoutTimerAPI());
+ await Promise.all(
+ Array.from({ length: 50 }, () =>
+ tg.resolveAfter(Duration.fromMilliseconds(1)),
+ ),
+ );
+ assert.strictEqual(trackedTimers(tg), 0);
+});
+
+test("TimerGroup keeps interval timers registered", async (t) => {
+ const tg = new TimerGroup(new SetTimeoutTimerAPI());
+ const h = tg.every(1, () => {});
+ await new Promise((r) => setTimeout(r, 40));
+ assert.strictEqual(trackedTimers(tg), 1);
+ h.clear();
+ assert.strictEqual(trackedTimers(tg), 0);
+});
+
+test("resolveAfter does not schedule when already cancelled", async (t) => {
+ // onCancelled fires synchronously for an already-cancelled token, at which
+ // point there is no timer handle to clear -- so the code must not go on to
+ // schedule one for a promise that has already settled.
+ const src = CancellationToken.create();
+ src.cancel();
+ const tg = new TimerGroup(new SetTimeoutTimerAPI());
+ const res = await tg.resolveAfter(Duration.fromMilliseconds(3000), src.token);
+ assert.strictEqual(res, false);
+ assert.strictEqual(trackedTimers(tg), 0);
+});