From 983c3a2a3d71dbb95a98a9820f7caa1a4c0fbc91 Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 4 Nov 2026 02:50:03 +0100 Subject: [PATCH 1/2] test: record scheduling test runs through one Timeline method The three timed fake tools each locked the timeline and wrote their run themselves. Timeline::record does it in one place, so that recording a run can also wake a test waiting for it. --- tests/tool_scheduling_tests.cpp | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/tool_scheduling_tests.cpp b/tests/tool_scheduling_tests.cpp index 5450f4c..56e9986 100644 --- a/tests/tool_scheduling_tests.cpp +++ b/tests/tool_scheduling_tests.cpp @@ -132,6 +132,13 @@ namespace std::mutex mutex; std::map> runs; + /// Records that `run` started at `start` and ends now. + void record(const std::string& run, Clock::time_point start) + { + const std::lock_guard lock(mutex); + runs[run] = {start, Clock::now()}; + } + [[nodiscard]] bool anyOverlap() { const std::lock_guard lock(mutex); @@ -168,10 +175,7 @@ namespace const Clock::time_point start = Clock::now(); std::this_thread::sleep_for(file == "slow.c" ? std::chrono::milliseconds(1000) : std::chrono::milliseconds(150)); - { - const std::lock_guard lock(m_timeline->mutex); - m_timeline->runs[file] = {start, Clock::now()}; - } + m_timeline->record(file, start); output.diagnostics( {{"timed", "rule", file, 1, 1, ctrace::Severity::Warning, "finding", ""}}); } @@ -222,8 +226,7 @@ namespace { const Clock::time_point start = Clock::now(); std::this_thread::sleep_for(std::chrono::milliseconds(100)); - const std::lock_guard lock(m_timeline->mutex); - m_timeline->runs[run] = {start, Clock::now()}; + m_timeline->record(run, start); } std::string m_name; @@ -245,8 +248,7 @@ namespace { const Clock::time_point start = Clock::now(); std::this_thread::sleep_for(std::chrono::milliseconds(200)); - const std::lock_guard lock(m_timeline->mutex); - m_timeline->runs[file] = {start, Clock::now()}; + m_timeline->record(file, start); } [[nodiscard]] std::size_t maxConcurrentRuns() const override From 0016879b1687a9ff9402b73b73d5d5644849c159 Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 4 Nov 2026 02:50:04 +0100 Subject: [PATCH 2/2] test: check the tool concurrency limit without a timing margin The check compared end times: the limited tool, 3 x 200 ms, had to be done after the other tool, 3 x 150 ms, a 150 ms margin. On GitHub's macOS runners a sleep or a worker is sometimes that late: since 2026-10-01 the check failed 8 times in about 100 macOS attempts, with no change to the scheduler. Each run of the limited tool now waits for the other tool to be done with every file, and throws if it is not after 10 s. The check is that no tool failed: it holds when the other tool runs while the limited one holds its worker, and fails when the scheduler makes it wait. --- tests/tool_scheduling_tests.cpp | 46 ++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/tests/tool_scheduling_tests.cpp b/tests/tool_scheduling_tests.cpp index 56e9986..ec334a8 100644 --- a/tests/tool_scheduling_tests.cpp +++ b/tests/tool_scheduling_tests.cpp @@ -9,11 +9,14 @@ #include #include +#include +#include #include #include #include #include #include +#include #include #include #include @@ -130,13 +133,24 @@ namespace struct Timeline { std::mutex mutex; + std::condition_variable recorded; std::map> runs; /// Records that `run` started at `start` and ends now. void record(const std::string& run, Clock::time_point start) { - const std::lock_guard lock(mutex); - runs[run] = {start, Clock::now()}; + { + const std::lock_guard lock(mutex); + runs[run] = {start, Clock::now()}; + } + recorded.notify_all(); + } + + /// Whether `count` runs are recorded before `limit` passes. + [[nodiscard]] bool waitForRuns(std::size_t count, std::chrono::milliseconds limit) + { + std::unique_lock lock(mutex); + return recorded.wait_for(lock, limit, [&] { return runs.size() >= count; }); } [[nodiscard]] bool anyOverlap() @@ -234,12 +248,15 @@ namespace std::shared_ptr m_timeline; }; - /// Takes a while on each file, records when, and allows one run at a time. + /// Allows one run at a time. Each run waits for another tool to record `otherRuns` runs, + /// and fails if it has not within 10 s: it can only succeed if that tool runs while this + /// one holds a worker. It then takes a while and records when. class OneAtATimeTool : public ctrace::AnalysisToolBase { public: - explicit OneAtATimeTool(std::shared_ptr timeline) - : m_timeline(std::move(timeline)) + OneAtATimeTool(std::shared_ptr timeline, std::shared_ptr other, + std::size_t otherRuns) + : m_timeline(std::move(timeline)), m_other(std::move(other)), m_otherRuns(otherRuns) { } @@ -247,8 +264,13 @@ namespace ctrace::ToolOutput&) const override { const Clock::time_point start = Clock::now(); + const bool otherDone = m_other->waitForRuns(m_otherRuns, std::chrono::seconds(10)); std::this_thread::sleep_for(std::chrono::milliseconds(200)); m_timeline->record(file, start); + if (!otherDone) + { + throw std::runtime_error("the other tool did not run while this one waited"); + } } [[nodiscard]] std::size_t maxConcurrentRuns() const override @@ -263,6 +285,8 @@ namespace private: std::shared_ptr m_timeline; + std::shared_ptr m_other; + std::size_t m_otherRuns; }; std::vector runTimedTool(std::launch policy, std::size_t workers, @@ -372,19 +396,21 @@ namespace } // A tool that allows one run at a time gets one, and does not keep the other tools waiting. + // The limited tool's runs wait for the other tool instead of racing it, so that the check + // does not depend on how fast the machine is. void testToolConcurrencyLimit(TestReport& report) { + const std::vector files = {"a.c", "b.c", "c.c"}; auto limited = std::make_shared(); auto timed = std::make_shared(); ctrace::ToolInvoker invoker(ctrace::ProgramConfig{}, poolFor(std::launch::async, 2)); - invoker.registerTool("one_at_a_time", std::make_unique(limited)); + invoker.registerTool("one_at_a_time", + std::make_unique(limited, timed, files.size())); invoker.registerTool("timed", std::make_unique(timed)); - invoker.runSpecificTools({"one_at_a_time", "timed"}, {"a.c", "b.c", "c.c"}); + invoker.runSpecificTools({"one_at_a_time", "timed"}, files); report.expect(!limited->anyOverlap(), "a tool limited to one run never runs twice at once"); - const Clock::time_point limitedEnd = limited->end("c.c"); - report.expect(timed->end("a.c") < limitedEnd && timed->end("b.c") < limitedEnd && - timed->end("c.c") < limitedEnd, + report.expect(invoker.failedTools().empty(), "the other tools run while the limited tool works through its files"); }