From 169982d3153a41ba63f0e8c89e6900ad5171ed9f Mon Sep 17 00:00:00 2001 From: Benjamin Bannier Date: Sat, 25 Jul 2026 13:43:43 +0200 Subject: [PATCH 1/6] Remove dead truthiness guards `Test` defines no `__bool__`, so every instance is always truthy. --- btest | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/btest b/btest index b548c57..bcd806d 100755 --- a/btest +++ b/btest @@ -410,9 +410,6 @@ class RunState: return None for i, t in enumerate(self._tests): - if not t: - continue - if t.serialize and t.serialize_hash() % Options.threads != worker_num: # Not ours. continue @@ -611,7 +608,7 @@ class RunState: timing = self.loadTiming() for t in tests: - if t and t.measure_time and t.utime >= 0: + if t.measure_time and t.utime >= 0: changed = True timing[t.name] = t.utime From 902e8a874ca78e2f822e175e878978badecd5b4e Mon Sep 17 00:00:00 2001 From: Benjamin Bannier Date: Sat, 25 Jul 2026 13:44:23 +0200 Subject: [PATCH 2/6] Move abort-on-failure queue-clear into `testFailed` Workers drain naturally when the queue empties rather than checking an abort flag on every dispatch. The queue is cleared at the point where the failure is recorded, which is where the abort decision logically belongs. --- btest | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/btest b/btest index bcd806d..ee146a5 100755 --- a/btest +++ b/btest @@ -402,13 +402,6 @@ class RunState: self._tests += [test.clone(increment=False)] def nextTests(self, worker_num): - if ( - Options.abort_on_failure - and self._failed > 0 - and self._failed > self._failed_expected - ): - return None - for i, t in enumerate(self._tests): if t.serialize and t.serialize_hash() % Options.threads != worker_num: # Not ours. @@ -574,6 +567,9 @@ class RunState: if test.reruns < Options.retries and not test.known_failure: self.rerun(test) + if Options.abort_on_failure and self._failed > self._failed_expected: + self._tests.clear() + def testSkipped(self, test): msg = "not available, skipped" From 16f2a252ea764ca163c6b600069ea24da564508d Mon Sep 17 00:00:00 2001 From: Benjamin Bannier Date: Sat, 25 Jul 2026 13:45:38 +0200 Subject: [PATCH 3/6] Extract `_expand_alternatives` helper Isolates the alternatives-expansion logic from the dispatch loop so the upcoming generator conversion of `nextTests` stays readable. --- btest | 64 +++++++++++++++++++++++++++-------------------------------- 1 file changed, 29 insertions(+), 35 deletions(-) diff --git a/btest b/btest index ee146a5..0dde2d8 100755 --- a/btest +++ b/btest @@ -370,6 +370,34 @@ class Abort(Exception): pass +def _expand_alternatives(t): + if Options.alternatives: + for alternative in Options.alternatives: + if alternative in t.ignore_alternatives: + continue + + if t.include_alternatives and alternative not in t.include_alternatives: + continue + + alternative_test = copy.deepcopy(t) + + if alternative == Alternative.DEFAULT: + alternative = "" + + alternative_test.setAlternative(alternative) + yield alternative_test + + else: + if t.include_alternatives and Alternative.DEFAULT not in t.include_alternatives: + return + + elif Alternative.DEFAULT in t.ignore_alternatives: + return + + else: + yield t + + class RunState: def __init__(self, output_handler, tests, failed_tests, ports, timing): self._output_handler = output_handler @@ -410,41 +438,7 @@ class RunState: # We'll execute it, delete from queue. del self._tests[i] - if Options.alternatives: - tests = [] - - for alternative in Options.alternatives: - if alternative in t.ignore_alternatives: - continue - - if ( - t.include_alternatives - and alternative not in t.include_alternatives - ): - continue - - alternative_test = copy.deepcopy(t) - - if alternative == Alternative.DEFAULT: - alternative = "" - - alternative_test.setAlternative(alternative) - tests += [alternative_test] - - else: - if ( - t.include_alternatives - and Alternative.DEFAULT not in t.include_alternatives - ): - tests = [] - - elif Alternative.DEFAULT in t.ignore_alternatives: - tests = [] - - else: - tests = [t] - - return tests + return list(_expand_alternatives(t)) # No more tests for us. return None From c2c6cbc0da4e62473c7689668b8463ef0ef6c5a3 Mon Sep 17 00:00:00 2001 From: Benjamin Bannier Date: Sat, 25 Jul 2026 13:46:40 +0200 Subject: [PATCH 4/6] Convert `nextTests` to a generator Replaces the `None`-sentinel protocol with a generator that yields individual tests. The worker loop simplifies to a plain `for` and the `saveTiming` call moves to after the loop, so the timing file is read and written once per worker rather than once per test. Reruns appended to the queue by `testFailed` are picked up naturally on the next outer `while True` iteration. --- btest | 37 ++++++++++++++++--------------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/btest b/btest index 0dde2d8..4eff80e 100755 --- a/btest +++ b/btest @@ -430,18 +430,17 @@ class RunState: self._tests += [test.clone(increment=False)] def nextTests(self, worker_num): - for i, t in enumerate(self._tests): - if t.serialize and t.serialize_hash() % Options.threads != worker_num: - # Not ours. - continue - - # We'll execute it, delete from queue. - del self._tests[i] - - return list(_expand_alternatives(t)) + while True: + for i, t in enumerate(self._tests): + if t.serialize and t.serialize_hash() % Options.threads != worker_num: + # Not ours. + continue - # No more tests for us. - return None + del self._tests[i] + yield from _expand_alternatives(t) + break + else: + return def returnPorts(self, ports): for p in ports: @@ -625,16 +624,12 @@ async def run_test(test, state): async def run_all_tests(state, n): async def worker(worker_num): all_tests = [] - while True: - worker_tests = state.nextTests(worker_num) - if worker_tests is None: - return - all_tests += worker_tests - for t in worker_tests: - t.worker_num = worker_num - await run_test(t, state) - if Options.update_times: - state.saveTiming(all_tests) + for t in state.nextTests(worker_num): + t.worker_num = worker_num + await run_test(t, state) + all_tests.append(t) + if Options.update_times: + state.saveTiming(all_tests) await asyncio.gather(*[worker(i) for i in range(n)]) From f399e4cb602eaf92f01d538ef037fc4dd2009d77 Mon Sep 17 00:00:00 2001 From: Benjamin Bannier Date: Sat, 25 Jul 2026 13:46:55 +0200 Subject: [PATCH 5/6] Rename `nextTests` to `tests` --- btest | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/btest b/btest index 4eff80e..195ad65 100755 --- a/btest +++ b/btest @@ -429,7 +429,7 @@ class RunState: test.reruns += 1 self._tests += [test.clone(increment=False)] - def nextTests(self, worker_num): + def tests(self, worker_num): while True: for i, t in enumerate(self._tests): if t.serialize and t.serialize_hash() % Options.threads != worker_num: @@ -624,7 +624,7 @@ async def run_test(test, state): async def run_all_tests(state, n): async def worker(worker_num): all_tests = [] - for t in state.nextTests(worker_num): + for t in state.tests(worker_num): t.worker_num = worker_num await run_test(t, state) all_tests.append(t) From f2f6f149d1c90b2399252579c41507e2998e6e74 Mon Sep 17 00:00:00 2001 From: Benjamin Bannier Date: Sat, 25 Jul 2026 13:47:12 +0200 Subject: [PATCH 6/6] Rename `getPorts` / `getAvailablePorts` Align with the snake_case convention used throughout the newer parts of the codebase. --- btest | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/btest b/btest index 195ad65..1ab78dc 100755 --- a/btest +++ b/btest @@ -446,7 +446,7 @@ class RunState: for p in ports: self._ports.append(p) - def getAvailablePorts(self, count): + def available_ports(self, count): if count > len(self._ports): return [] @@ -980,14 +980,14 @@ class Test: self.known_failure |= part.known_failure self.measure_time |= part.measure_time - def getPorts(self, state, count): + def allocate_ports(self, state, count): if not count: return [] attempts = 5 while True: - rval = state.getAvailablePorts(count) + rval = state.available_ports(count) if rval: return rval @@ -1004,7 +1004,7 @@ class Test: time.sleep(15) async def run(self, state): - bound_sockets = self.getPorts(state, len(self.ports)) + bound_sockets = self.allocate_ports(state, len(self.ports)) self.bound_ports = [s.getsockname()[1] for s in bound_sockets] for bs in bound_sockets: