From 1c24a01a5f768db9b28f5f15e73df64a24555eed Mon Sep 17 00:00:00 2001 From: Thomas Flament Date: Fri, 13 Mar 2026 15:59:11 +0100 Subject: [PATCH] Fix Go duration parsing in kafkaCleaner test The KafkaCleanerInterval parameter is a Go duration string (e.g. "1m") read from the Zenko CR spec.kafkaCleaner.interval. The test used parseInt("1m") to parse it, which silently returned 1 instead of 60, since parseInt stops at the first non-numeric character. This made the test timeout after ~60s (1 * 6000ms * 10) instead of ~600s (60 * 6000ms * 10), giving the kafkacleaner only one cleaning cycle to process all topics instead of ten. Under normal conditions one cycle was often enough, but when operator reconciliation caused topic recreation during the run, the kafkacleaner needed several cycles to catch up, causing the test to fail with: "Kafka cleaner did not clean the topics within the expected time" Replace parseInt with a proper Go duration parser that handles compound durations (e.g. "2h45m"), fractional values, and all standard Go time units (ns, us, ms, s, m, h). Issue: ZENKO-5218 --- tests/ctst/common/common.ts | 4 +-- tests/ctst/common/parseGoDuration.test.ts | 34 +++++++++++++++++++++++ tests/ctst/common/utils.ts | 26 +++++++++++++++++ 3 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 tests/ctst/common/parseGoDuration.test.ts diff --git a/tests/ctst/common/common.ts b/tests/ctst/common/common.ts index fa489a350..4c882039a 100644 --- a/tests/ctst/common/common.ts +++ b/tests/ctst/common/common.ts @@ -2,7 +2,7 @@ import { ListObjectVersionsOutput } from '@aws-sdk/client-s3'; import { Given, setDefaultTimeout, Then, When } from '@cucumber/cucumber'; import { CacheHelper, Constants, Identity, IdentityEnum, S3, Utils } from 'cli-testing'; import Zenko from 'world/Zenko'; -import { safeJsonParse } from './utils'; +import { parseGoDuration, safeJsonParse } from './utils'; import assert from 'assert'; import { Admin, Kafka } from 'kafkajs'; import { @@ -300,7 +300,7 @@ Then('i {string} be able to add user metadata to object {string}', Then('kafka consumed messages should not take too much place on disk', { timeout: -1 }, async function (this: Zenko) { - const kfkcIntervalSeconds = parseInt(this.parameters.KafkaCleanerInterval); + const kfkcIntervalSeconds = parseGoDuration(this.parameters.KafkaCleanerInterval); const checkInterval = kfkcIntervalSeconds * (1000 + 5000); const timeoutID = setTimeout(() => { diff --git a/tests/ctst/common/parseGoDuration.test.ts b/tests/ctst/common/parseGoDuration.test.ts new file mode 100644 index 000000000..195ec9193 --- /dev/null +++ b/tests/ctst/common/parseGoDuration.test.ts @@ -0,0 +1,34 @@ +import assert from 'assert'; +import { parseGoDuration } from './utils'; + +const cases: [string, number][] = [ + ['1m', 60], + ['2h', 7200], + ['30s', 30], + ['2h45m', 9900], + ['500ms', 0.5], + ['1.5s', 1.5], + ['1h30m10s', 5410], + ['100ns', 1e-7], + ['10us', 1e-5], + ['10µs', 1e-5], + ['0s', 0], +]; + +for (const [input, expected] of cases) { + const result = parseGoDuration(input); + assert.strictEqual( + Math.abs(result - expected) < 1e-12, true, + `parseGoDuration("${input}") = ${result}, expected ${expected}`, + ); +} + +const invalid = ['', 'abc', '1x', '5h 3m', '1', 'm', ' 1m']; +for (const input of invalid) { + assert.throws( + () => parseGoDuration(input), + { message: /Invalid duration/ }, + `parseGoDuration("${input}") should throw`, + ); +} + diff --git a/tests/ctst/common/utils.ts b/tests/ctst/common/utils.ts index 42deecbfc..7b1a61d90 100644 --- a/tests/ctst/common/utils.ts +++ b/tests/ctst/common/utils.ts @@ -100,6 +100,32 @@ export const s3FunctionExtraParams: { [key: string]: Record[] } }], }; +/** + * Parses a duration string in Go's time.ParseDuration format + * (https://pkg.go.dev/time#ParseDuration) and returns the equivalent in seconds. + * @param {string} duration - the duration string to parse (e.g. "1h30m", "500ms") + * @return {number} - the duration in seconds + */ +export function parseGoDuration(duration: string): number { + const units: Record = { + ns: 1e-9, us: 1e-6, µs: 1e-6, ms: 1e-3, s: 1, m: 60, h: 3600, + }; + let remaining = duration; + if (remaining.length === 0) { + throw new Error(`Invalid duration: "${duration}"`); + } + let totalSeconds = 0; + while (remaining.length > 0) { + const match = remaining.match(/^(\d+(?:\.\d*)?)(ns|us|µs|ms|s|m|h)/); + if (!match) { + throw new Error(`Invalid duration: "${duration}" (unparsed: "${remaining}")`); + } + totalSeconds += parseFloat(match[1]) * units[match[2]]; + remaining = remaining.slice(match[0].length); + } + return totalSeconds; +} + export function safeJsonParse(jsonString: string): { ok: boolean, result: T | null, error?: Error | null } { let result: T; try {