Read-only mirrors. Development, issues and pull requests are on GitHub.

summaryrefslogtreecommitdiffhomepage
path: root/src/Progmasoft/Runner/Scheduler.cpp
diff options
context:
space:
mode:
authorLeitwolf112026-10-23 23:25:41 +0300
committerLeitwolf112026-10-23 23:25:41 +0300
commit93d4e7dbebab867e0a7f59fc25308460a7a76280 (patch)
tree54c568a3063d49e21f039b6d38f40f51be7991f0 /src/Progmasoft/Runner/Scheduler.cpp
parent83c6fc3ac13abe83fa39f7b5f8559ccaf4412233 (diff)
downloadcatch3-93d4e7dbebab867e0a7f59fc25308460a7a76280.tar.gz
Replace Catch2's runner with the Catch3 runner
Catch3 keeps Catch2's API for writing tests (test cases, sections, assertions, matchers, generators, listeners, benchmarks) and replaces its session, command line and reporters with a runner of its own. The runner starts the test binary again as worker processes that take the next test when they are free. A crash, an exit or a hang ends one test, not the run. On top of that it adds: - retries that tell deterministic, flaky and order-dependent failures apart, and a bisection that names the test leaving state behind; - a history of verdicts and durations, kept under a file lock, that drives automatic quarantine, adaptive time limits, predictive ordering, incremental runs, failure replay and analytics; - selection by change, past failure or prediction, dependency closure, and sharding by a stable hash or by recorded durations; - tags for serial and isolated tests, shared resources, dependencies, time limits and retries; - a failure category, a signature, an analysis and a replay command for every failure; - console, JUnit XML (with flakyFailure and rerunFailure), JSON and Chrome trace reports, and a versioned discovery listing (--list=json); - a temporary directory per test (TMPDIR, TMP, TEMP and TestTemporaryDirectory()); - Bazel's test environment: XML output file, test filter, sharding, seed, premature exit file and undeclared outputs. The command line is parsed by a hand-written, typed parser. Catch::Session remains as a thin facade over the runner for custom main functions. The result model in Progmasoft/Catch3/Results.hpp was rewritten around tests and attempts, and the JUnit writer now writes it. The property engine's example database is updated under a file lock with an atomic rename, so parallel workers can write it at the same time. Removed: Clara, the command-line and reporter-spec parsers, every reporter and CATCH_REGISTER_REPORTER, listing, text flow, console colour and width, the JSON and XML writers, sharding in the test registry, approval tests and their baselines, Conan packaging, and the unused third_party/clara.hpp. Listeners keep working; reporters' list and skip events are gone. The Catch2 library is now compiled as C++20; code that includes only Catch2's headers still needs C++14. Duplicate test cases are still refused at start-up. Version 0.3.0. ExtraTests now turn exceptions off for clang-cl with MSVC-style options, which -fno-exceptions did not do.
Diffstat (limited to 'src/Progmasoft/Runner/Scheduler.cpp')
-rw-r--r--src/Progmasoft/Runner/Scheduler.cpp1426
1 files changed, 1426 insertions, 0 deletions
diff --git a/src/Progmasoft/Runner/Scheduler.cpp b/src/Progmasoft/Runner/Scheduler.cpp
new file mode 100644
index 00000000..e6c8eec5
--- /dev/null
+++ b/src/Progmasoft/Runner/Scheduler.cpp
@@ -0,0 +1,1426 @@
+// Copyright (c) 2026 Progmasoft.
+// SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+#include <algorithm>
+#include <chrono>
+#include <condition_variable>
+#include <cstdio>
+#include <deque>
+#include <filesystem>
+#include <map>
+#include <memory>
+#include <mutex>
+#include <optional>
+#include <set>
+#include <thread>
+
+#include "Execution.hpp"
+#include "Files.hpp"
+#include "Process.hpp"
+#include "Protocol.hpp"
+#include "ResultsJson.hpp"
+#include "Scheduler.hpp"
+#include "SchedulerModel.hpp"
+
+namespace Progmasoft::Catch3::Runner
+{
+ namespace
+ {
+ using Scheduling::AppendBounded;
+ using Scheduling::Bisection;
+ using Scheduling::Clock;
+ using Scheduling::Event;
+ using Scheduling::Job;
+ using Scheduling::kMaximumProbes;
+ using Scheduling::kMaximumProcessOutput;
+ using Scheduling::kMaximumRecordedAssertions;
+ using Scheduling::kStartupFailuresToGiveUp;
+ using Scheduling::kStartupTimeout;
+ using Scheduling::Microseconds;
+ using Scheduling::Passing;
+ using Scheduling::Running;
+ using Scheduling::Satisfies;
+ using Scheduling::TestState;
+ using Scheduling::Worker;
+ using std::chrono::milliseconds;
+
+ class Scheduler final
+ {
+ public:
+ Scheduler(const Options &options,
+ const Plan &plan,
+ const Catalog &catalog,
+ std::string program,
+ std::string programName,
+ const SchedulerEvents &events)
+ : options_{ options }
+ , plan_{ plan }
+ , catalog_{ catalog }
+ , program_{ std::move(program) }
+ , programName_{ std::move(programName) }
+ , events_{ events }
+ , runStart_{ Clock::now() }
+ {
+ std::error_code error;
+ temporaryRoot_ = std::filesystem::temp_directory_path(error)
+ / ("catch3-" + UniqueName());
+ const unsigned hardware = HardwareThreads();
+ maximumWorkers_ = options.jobs == 0 ? hardware : options.jobs;
+ maximumWorkers_ = std::max<std::size_t>(
+ 1,
+ std::min(maximumWorkers_, plan.tests.size()));
+ retryBudget_ = std::max<std::size_t>(5, plan.tests.size() / 10);
+ states_.resize(plan.tests.size());
+ for (std::size_t position = 0; position < plan.tests.size();
+ ++position)
+ {
+ const PlannedTest &planned = plan.tests[position];
+ const TestEntry &entry = *planned.entry;
+ TestResult &result = states_[position].result;
+ result.id = entry.key;
+ result.name = entry.name;
+ result.className = entry.className;
+ result.file = entry.file;
+ result.line = entry.line;
+ result.tags = entry.tags;
+ result.selectionReason = planned.selectionReason;
+ result.quarantined = planned.quarantined;
+ result.predictedDurationMicroseconds
+ = planned.predictedMicroseconds;
+ result.predictedFailureProbability
+ = planned.failureProbability;
+ if (planned.quarantined)
+ {
+ result.analysis.push_back("in quarantine: "
+ + planned.quarantineReason);
+ }
+ states_[position].repeatsLeft
+ = options.repeat > 0 ? options.repeat - 1 : 0;
+ }
+ }
+
+ ScheduleOutcome
+ Run()
+ {
+ for (std::size_t position = 0; position < plan_.tests.size();
+ ++position)
+ {
+ Job job;
+ job.test = position;
+ const PlannedTest &planned = plan_.tests[position];
+ if (planned.replaySequence)
+ {
+ job.before = *planned.replaySequence;
+ job.fresh = true;
+ job.seed = planned.replaySeed;
+ }
+ Enqueue(std::move(job));
+ }
+ if (options_.inProcess)
+ {
+ RunInProcess();
+ }
+ else
+ {
+ RunInWorkers();
+ }
+ ScheduleOutcome outcome;
+ for (auto &state : states_)
+ {
+ if (!state.finished)
+ {
+ state.result.outcome = Outcome::kNotRun;
+ if (infrastructureFailed_)
+ {
+ state.result.category
+ = FailureCategory::kInfrastructure;
+ }
+ }
+ outcome.tests.push_back(std::move(state.result));
+ }
+ outcome.workers = workersStarted_;
+ outcome.notes = std::move(notes_);
+ outcome.infrastructureFailed = infrastructureFailed_;
+ std::error_code error;
+ // Only an empty root goes; kept directories hold failures.
+ std::filesystem::remove(temporaryRoot_, error);
+ return outcome;
+ }
+
+ private:
+ // Queue and dispatch.
+
+ void
+ Enqueue(Job job)
+ {
+ ++states_[job.test].pendingJobs;
+ states_[job.test].queued = true;
+ if (job.kind == Job::Kind::kProbe || !job.before.empty()
+ || states_[job.test].result.attempts.size() > 0)
+ {
+ // Retries and probes go ahead of first attempts: they
+ // finish a verdict that is already overdue.
+ queue_.push_front(std::move(job));
+ }
+ else
+ {
+ queue_.push_back(std::move(job));
+ }
+ }
+
+ /// Whether the dependencies of a test have their verdicts:
+ /// returns false while one is still open.
+ bool
+ DependenciesSettled(std::size_t position) const
+ {
+ for (const std::size_t dependency :
+ plan_.tests[position].dependencies)
+ {
+ if (!states_[dependency].finished)
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ bool
+ ResourcesFree(std::size_t position) const
+ {
+ for (const auto &resource :
+ plan_.tests[position].entry->resources)
+ {
+ if (heldResources_.count(resource) > 0)
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ std::size_t
+ RunningCount() const
+ {
+ std::size_t count = 0;
+ for (const auto &worker : workers_)
+ {
+ count += worker->job.has_value() ? 1 : 0;
+ }
+ return count;
+ }
+
+ /// The next job that may start now, removed from the queue.
+ std::optional<Job>
+ TakeRunnable()
+ {
+ for (auto job = queue_.begin(); job != queue_.end(); ++job)
+ {
+ const std::size_t position = job->test;
+ if (job->kind == Job::Kind::kAttempt
+ && !DependenciesSettled(position))
+ {
+ continue;
+ }
+ const bool serial = plan_.tests[position].entry->serial;
+ if (serial && RunningCount() > 0)
+ {
+ // Nothing else starts until the serial test has run.
+ return std::nullopt;
+ }
+ if (serialRunning_)
+ {
+ return std::nullopt;
+ }
+ if (!ResourcesFree(position))
+ {
+ continue;
+ }
+ Job taken = std::move(*job);
+ queue_.erase(job);
+ return taken;
+ }
+ return std::nullopt;
+ }
+
+ /// Settles tests whose dependencies failed, and those left when
+ /// the run stops early.
+ void
+ SettleBlocked()
+ {
+ bool changed = true;
+ while (changed)
+ {
+ changed = false;
+ for (auto job = queue_.begin(); job != queue_.end();)
+ {
+ const std::size_t position = job->test;
+ std::string reason;
+ if (job->kind == Job::Kind::kAttempt)
+ {
+ for (const std::size_t dependency :
+ plan_.tests[position].dependencies)
+ {
+ const auto &state = states_[dependency];
+ if (state.finished
+ && !Satisfies(state.result.outcome))
+ {
+ reason
+ = "depends on '" + state.result.name
+ + "', which "
+ + (state.result.outcome
+ == Outcome::kBlocked
+ ? std::string{ "was blocked" }
+ : std::string{ OutcomeName(
+ state.result.outcome) });
+ break;
+ }
+ }
+ }
+ if (reason.empty())
+ {
+ ++job;
+ continue;
+ }
+ auto &state = states_[position];
+ job = queue_.erase(job);
+ --state.pendingJobs;
+ if (state.pendingJobs == 0 && !state.finished
+ && state.result.attempts.empty())
+ {
+ state.result.outcome = Outcome::kBlocked;
+ state.result.category
+ = FailureCategory::kDependency;
+ state.result.analysis.insert(
+ state.result.analysis.begin(),
+ reason);
+ Finish(position);
+ changed = true;
+ }
+ }
+ }
+ }
+
+ std::string
+ TemporaryFor(std::size_t catalogIndex, std::size_t attempt)
+ {
+ return (temporaryRoot_
+ / ("t" + std::to_string(catalogIndex) + "-a"
+ + std::to_string(attempt) + "-"
+ + std::to_string(++temporaryCounter_)))
+ .string();
+ }
+
+ /// Starts the next step of a worker's job.
+ void
+ StartStep(Worker &worker)
+ {
+ const Job &job = *worker.job;
+ const bool target = worker.step >= job.before.size();
+ const std::size_t catalogIndex
+ = target ? plan_.tests[job.test].entry->index
+ : job.before[worker.step];
+ Running running;
+ running.catalogIndex = catalogIndex;
+ running.target = target;
+ running.start = Clock::now();
+ Attempt &attempt = running.attempt;
+ attempt.worker = worker.id;
+ attempt.freshProcess = worker.history.empty();
+ attempt.seed = worker.seed.value_or(options_.seed);
+ attempt.startMicroseconds
+ = Microseconds(running.start - runStart_);
+ for (const std::size_t before : worker.history)
+ {
+ attempt.precededBy.push_back(catalog_.Tests()[before].key);
+ }
+ if (target)
+ {
+ const PlannedTest &planned = plan_.tests[job.test];
+ attempt.number
+ = job.kind == Job::Kind::kAttempt
+ ? states_[job.test].result.attempts.size() + 1
+ : 0;
+ if (planned.timeout.count() > 0 && !options_.inProcess)
+ {
+ running.deadline = running.start + planned.timeout;
+ }
+ }
+ attempt.temporaryDirectory
+ = TemporaryFor(catalogIndex, attempt.number);
+ worker.running = std::move(running);
+ if (options_.inProcess)
+ {
+ return;
+ }
+ JsonValue command = Message("run");
+ command.Set("index", catalogIndex);
+ command.Set("attempt", worker.running->attempt.number);
+ command.Set("temp", worker.running->attempt.temporaryDirectory);
+ if (!worker.process->Input().Write(command.ToString() + "\n"))
+ {
+ // The worker died; its closed output reports it.
+ }
+ }
+
+ void
+ Assign(Worker &worker, Job job)
+ {
+ const PlannedTest &planned = plan_.tests[job.test];
+ for (const auto &resource : planned.entry->resources)
+ {
+ heldResources_.insert(resource);
+ }
+ if (planned.entry->serial)
+ {
+ serialRunning_ = true;
+ }
+ worker.job = std::move(job);
+ worker.step = 0;
+ StartStep(worker);
+ }
+
+ void
+ ReleaseJob(Worker &worker)
+ {
+ if (!worker.job)
+ {
+ return;
+ }
+ const PlannedTest &planned = plan_.tests[worker.job->test];
+ for (const auto &resource : planned.entry->resources)
+ {
+ heldResources_.erase(resource);
+ }
+ if (planned.entry->serial)
+ {
+ serialRunning_ = false;
+ }
+ worker.job.reset();
+ worker.running.reset();
+ }
+
+ // Results.
+
+ /// An attempt of the job's target ended.
+ void
+ Complete(Worker &worker, Attempt attempt)
+ {
+ Job job = std::move(*worker.job);
+ ReleaseJob(worker);
+ auto &state = states_[job.test];
+ --state.pendingJobs;
+ std::error_code error;
+ if (Passing(attempt.outcome)
+ && !attempt.temporaryDirectory.empty())
+ {
+ std::filesystem::remove_all(attempt.temporaryDirectory,
+ error);
+ attempt.temporaryDirectory.clear();
+ }
+ else if (!attempt.temporaryDirectory.empty()
+ && std::filesystem::is_empty(
+ attempt.temporaryDirectory,
+ error))
+ {
+ std::filesystem::remove(attempt.temporaryDirectory, error);
+ attempt.temporaryDirectory.clear();
+ }
+ if (job.kind == Job::Kind::kProbe)
+ {
+ ProbeEnded(job, IsFailure(attempt.outcome));
+ return;
+ }
+ state.result.attempts.push_back(std::move(attempt));
+ Decide(job.test);
+ }
+
+ /// What follows an attempt: a repeat, a retry, a bisection, or
+ /// the verdict.
+ void
+ Decide(std::size_t position)
+ {
+ auto &state = states_[position];
+ const auto &attempts = state.result.attempts;
+ const Attempt &last = attempts.back();
+ const PlannedTest &planned = plan_.tests[position];
+ if (stopping_)
+ {
+ Finish(position);
+ return;
+ }
+ if (state.repeatsLeft > 0)
+ {
+ --state.repeatsLeft;
+ NotifyRetry(position);
+ Job job;
+ job.test = position;
+ Enqueue(std::move(job));
+ return;
+ }
+ const bool failed = IsFailure(last.outcome);
+ const bool everFailed
+ = std::any_of(attempts.begin(),
+ attempts.end(),
+ [](const Attempt &attempt) {
+ return IsFailure(attempt.outcome);
+ });
+ if (failed && state.retriesUsed < planned.retryLimit
+ && retryBudget_ > 0)
+ {
+ bool retry = options_.retryMode == RetryMode::kAlways;
+ if (options_.retryMode == RetryMode::kAuto)
+ {
+ // One retry, alone in a fresh process, tells a
+ // deterministic failure from a flaky one and from
+ // one caused by a test before it. Beyond that only
+ // what is known to be unstable is worth more tries.
+ const bool unstable
+ = last.outcome == Outcome::kCrashed
+ || last.outcome == Outcome::kTimedOut
+ || planned.quarantined || planned.flakyHistory;
+ retry = state.retriesUsed == 0 || unstable;
+ }
+ if (retry)
+ {
+ ++state.retriesUsed;
+ --retryBudget_;
+ NotifyRetry(position);
+ Job job;
+ job.test = position;
+ job.fresh = true;
+ Enqueue(std::move(job));
+ return;
+ }
+ }
+ // A pass alone after a failure in company: flaky, or broken
+ // by a test before it. A probe tells which.
+ if (!failed && everFailed && options_.bisectOrder
+ && state.bisection.stage == Bisection::Stage::kNone)
+ {
+ const Attempt *sharedFailure = nullptr;
+ for (const auto &attempt : attempts)
+ {
+ if (IsFailure(attempt.outcome)
+ && !attempt.precededBy.empty()
+ && attempt.outcome != Outcome::kTimedOut)
+ {
+ sharedFailure = &attempt;
+ break;
+ }
+ }
+ if (sharedFailure != nullptr && last.freshProcess)
+ {
+ std::vector<std::size_t> sequence;
+ for (const auto &key : sharedFailure->precededBy)
+ {
+ if (const TestEntry *entry = catalog_.Find(key))
+ {
+ sequence.push_back(entry->index);
+ }
+ }
+ if (!sequence.empty())
+ {
+ state.bisection.stage
+ = Bisection::Stage::kConfirming;
+ state.bisection.candidates = sequence;
+ StartProbe(position, sequence);
+ return;
+ }
+ }
+ }
+ Finish(position);
+ }
+
+ void
+ StartProbe(std::size_t position, std::vector<std::size_t> sequence)
+ {
+ auto &bisection = states_[position].bisection;
+ ++bisection.probes;
+ bisection.probing = sequence;
+ Job job;
+ job.kind = Job::Kind::kProbe;
+ job.test = position;
+ job.before = sequence;
+ job.fresh = true;
+ job.probeSequence = std::move(sequence);
+ Enqueue(std::move(job));
+ }
+
+ void
+ ProbeEnded(const Job &job, bool failed)
+ {
+ const std::size_t position = job.test;
+ auto &bisection = states_[position].bisection;
+ if (bisection.stage == Bisection::Stage::kConfirming)
+ {
+ if (!failed)
+ {
+ // It does not fail again after the same tests: flaky.
+ bisection.stage = Bisection::Stage::kDone;
+ Finish(position);
+ return;
+ }
+ bisection.reproduced = true;
+ bisection.stage = Bisection::Stage::kSearching;
+ }
+ else if (bisection.stage == Bisection::Stage::kSearching)
+ {
+ if (failed)
+ {
+ bisection.candidates = bisection.probing;
+ }
+ else
+ {
+ std::vector<std::size_t> rest;
+ for (const std::size_t candidate : bisection.candidates)
+ {
+ if (std::find(bisection.probing.begin(),
+ bisection.probing.end(),
+ candidate)
+ == bisection.probing.end())
+ {
+ rest.push_back(candidate);
+ }
+ }
+ bisection.candidates = std::move(rest);
+ }
+ }
+ if (bisection.candidates.size() <= 1
+ || bisection.probes >= kMaximumProbes)
+ {
+ if (bisection.candidates.size() == 1)
+ {
+ bisection.culprit = bisection.candidates.front();
+ }
+ bisection.stage = Bisection::Stage::kDone;
+ Finish(position);
+ return;
+ }
+ const auto half = bisection.candidates.size() / 2;
+ StartProbe(position,
+ std::vector<std::size_t>(
+ bisection.candidates.begin(),
+ bisection.candidates.begin()
+ + static_cast<std::ptrdiff_t>(half)));
+ }
+
+ void
+ NotifyRetry(std::size_t position)
+ {
+ const auto &result = states_[position].result;
+ if (events_.retrying && !result.attempts.empty())
+ {
+ events_.retrying(result, result.attempts.back());
+ }
+ }
+
+ /// Gives a test its verdict.
+ void
+ Finish(std::size_t position)
+ {
+ auto &state = states_[position];
+ if (state.finished)
+ {
+ return;
+ }
+ state.finished = true;
+ TestResult &result = state.result;
+ const auto &attempts = result.attempts;
+ if (!attempts.empty())
+ {
+ const Attempt *firstFailure = nullptr;
+ const Attempt *lastPass = nullptr;
+ for (const auto &attempt : attempts)
+ {
+ if (IsFailure(attempt.outcome)
+ && firstFailure == nullptr)
+ {
+ firstFailure = &attempt;
+ }
+ if (Passing(attempt.outcome))
+ {
+ lastPass = &attempt;
+ }
+ }
+ const auto &bisection = state.bisection;
+ if (bisection.reproduced && firstFailure != nullptr)
+ {
+ // Broken by state another test leaves behind: a real
+ // defect that a retry must not hide.
+ result.outcome = firstFailure->outcome;
+ result.category = firstFailure->category;
+ std::string finding
+ = "fails only after other tests in the same "
+ "process and passes alone";
+ if (bisection.culprit)
+ {
+ finding
+ += "; it fails after '"
+ + catalog_.Tests()[*bisection.culprit].name
+ + "', which leaves state behind";
+ }
+ result.analysis.insert(result.analysis.begin(),
+ finding);
+ }
+ else if (firstFailure != nullptr && lastPass != nullptr)
+ {
+ result.flaky = true;
+ result.outcome = lastPass->outcome;
+ result.category = FailureCategory::kNone;
+ result.analysis.insert(
+ result.analysis.begin(),
+ "flaky: failed with "
+ + std::string{ CategoryName(
+ firstFailure->category) }
+ + " and passed on attempt "
+ + std::to_string(lastPass->number));
+ }
+ else
+ {
+ result.outcome = attempts.back().outcome;
+ result.category = attempts.back().category;
+ }
+ }
+ if (result.FailsRun())
+ {
+ ++failedTests_;
+ if (options_.failFast > 0
+ && failedTests_ >= options_.failFast && !stopping_)
+ {
+ stopping_ = true;
+ notes_.push_back("stopped after "
+ + std::to_string(failedTests_)
+ + " failed test(s) (--fail-fast)");
+ }
+ }
+ if (events_.testFinished)
+ {
+ events_.testFinished(result);
+ }
+ }
+
+ // Messages.
+
+ void
+ HandleLine(Worker &worker, const std::string &line)
+ {
+ auto parsed = ParseJson(line);
+ if (!parsed)
+ {
+ // Not a message: something wrote to the protocol stream.
+ if (worker.running)
+ {
+ AppendBounded(worker.running->attempt.processOutput,
+ line + "\n");
+ }
+ return;
+ }
+ const JsonValue &message = *parsed;
+ const std::string &kind = message["t"].AsString();
+ if (kind == "hello")
+ {
+ if (message["identity"].AsString() != catalog_.Identity())
+ {
+ notes_.push_back(
+ "a worker saw different tests than the runner; "
+ "the binary may have changed during the run");
+ KillWorker(worker);
+ return;
+ }
+ worker.ready = true;
+ startupFailures_ = 0;
+ return;
+ }
+ if (!worker.running)
+ {
+ return;
+ }
+ Running &running = *worker.running;
+ Attempt &attempt = running.attempt;
+ if (kind == "section")
+ {
+ if (message["begin"].AsBoolean())
+ {
+ SectionRecord section;
+ section.name = message["name"].AsString();
+ section.file = message["file"].AsString();
+ section.line = static_cast<std::size_t>(
+ message["line"].AsUnsigned());
+ section.depth = static_cast<std::size_t>(
+ message["depth"].AsUnsigned());
+ section.startMicroseconds = message["at"].AsInteger();
+ section.durationMicroseconds = -1;
+ if (attempt.sections.size() < 1000)
+ {
+ attempt.sections.push_back(section);
+ }
+ }
+ else
+ {
+ const auto depth = message["depth"].AsUnsigned();
+ for (auto section = attempt.sections.rbegin();
+ section != attempt.sections.rend();
+ ++section)
+ {
+ if (section->depth == depth
+ && section->durationMicroseconds < 0)
+ {
+ section->durationMicroseconds
+ = message["duration"].AsInteger();
+ break;
+ }
+ }
+ }
+ }
+ else if (kind == "at")
+ {
+ attempt.lastLocation = message["location"].AsString();
+ }
+ else if (kind == "assertion")
+ {
+ // Newer than any location sent before it, which is sent
+ // only now and then.
+ attempt.lastLocation
+ = message["file"].AsString() + ":"
+ + std::to_string(message["line"].AsUnsigned());
+ if (attempt.assertions.size() < kMaximumRecordedAssertions)
+ {
+ attempt.assertions.push_back(
+ AssertionFromJson(message));
+ }
+ }
+ else if (kind == "benchmark")
+ {
+ attempt.benchmarks.push_back(BenchmarkFromJson(message));
+ }
+ else if (kind == "fatal")
+ {
+ running.fatal = true;
+ attempt.fatalMessage = message["message"].AsString();
+ if (!message["location"].AsString().empty())
+ {
+ attempt.lastLocation = message["location"].AsString();
+ }
+ }
+ else if (kind == "end")
+ {
+ attempt.outcome
+ = OutcomeFromName(message["outcome"].AsString())
+ .value_or(Outcome::kFailed);
+ attempt.category
+ = CategoryFromName(message["category"].AsString())
+ .value_or(FailureCategory::kNone);
+ if (running.fatal)
+ {
+ attempt.outcome = Outcome::kCrashed;