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

summaryrefslogtreecommitdiffhomepage
path: root/src/Progmasoft/Runner/ResultImportJson.cpp
diff options
context:
space:
mode:
authorLeitwolf2026-11-01 15:29:13 +0300
committerGitHub2026-11-01 15:29:13 +0300
commitb7fe8c2061c98e7aec886e9b1e6282f48c3b5456 (patch)
treef933300c29658c3eb89d126d74b6c30fdff9dd04 /src/Progmasoft/Runner/ResultImportJson.cpp
parent1559307a6ac7731ffd6a8e28147552b3987dbd93 (diff)
parent569620dad14df7e13bb9fd31d256e9fd873ef6ec (diff)
downloadcatch3-b7fe8c2061c98e7aec886e9b1e6282f48c3b5456.tar.gz
Merge pull request #28 from Progmasoft/feature/result-import
Import the results of other test frameworks
Diffstat (limited to 'src/Progmasoft/Runner/ResultImportJson.cpp')
-rw-r--r--src/Progmasoft/Runner/ResultImportJson.cpp274
1 files changed, 274 insertions, 0 deletions
diff --git a/src/Progmasoft/Runner/ResultImportJson.cpp b/src/Progmasoft/Runner/ResultImportJson.cpp
new file mode 100644
index 00000000..1651f938
--- /dev/null
+++ b/src/Progmasoft/Runner/ResultImportJson.cpp
@@ -0,0 +1,274 @@
+// Copyright (c) 2026 Progmasoft.
+// SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
+
+// The JSON reports: Catch3's own and CTRF.
+#include <Progmasoft/Runner/Json.hpp>
+#include <Progmasoft/Runner/ResultImport.hpp>
+#include <Progmasoft/Runner/ResultsJson.hpp>
+#include <utility>
+
+namespace Progmasoft::Catch3::Runner
+{
+ namespace
+ {
+ namespace ImportJson
+ {
+ /// A format of Catch3's own or CTRF, from the parsed document.
+ std::optional<ImportFormat>
+ FormatOf(const JsonValue &value)
+ {
+ if (value["format"].AsString() == "catch3-run"
+ || value["format"].AsString() == "catch3-results")
+ {
+ return ImportFormat::kCatch3;
+ }
+ if (value["results"]["tests"].IsArray())
+ {
+ return ImportFormat::kCtrf;
+ }
+ return std::nullopt;
+ }
+
+ /// What a test owes the verdict of an earlier run is worked out
+ /// again here, from this run's point of view.
+ void
+ Forget(TestResult &test)
+ {
+ test.quarantined = false;
+ test.history = FailureHistory::kUnknown;
+ test.selectionReason.clear();
+ test.signature.clear();
+ test.analysis.clear();
+ test.replayCommand.clear();
+ test.predictedDurationMicroseconds = 0;
+ test.predictedFailureProbability = 0.0;
+ }
+
+ Expected<TestResult>
+ ReadLine(const JsonValue &value)
+ {
+ TestResult test = TestResultFromJson(value);
+ Forget(test);
+ if (test.name.empty())
+ {
+ test.name = test.id;
+ }
+ if (test.name.empty())
+ {
+ return Error{ "a test needs an 'id' or a 'name'" };
+ }
+ const std::string &outcome = value["outcome"].AsString();
+ if (!OutcomeFromName(outcome))
+ {
+ return Error{ "'" + test.name + "' has the outcome '"
+ + outcome + "'" };
+ }
+ if (test.attempts.empty())
+ {
+ Attempt attempt = ImportedAttempt(
+ test.outcome,
+ value["durationMicroseconds"].AsInteger(),
+ value["message"].AsString(),
+ value["details"].AsString(),
+ test.category);
+ attempt.standardOutput = value["standardOutput"].AsString();
+ attempt.standardError = value["standardError"].AsString();
+ test.attempts.push_back(std::move(attempt));
+ }
+ return test;
+ }
+
+ Expected<std::vector<TestResult>>
+ ParseLines(std::string_view text)
+ {
+ std::vector<TestResult> tests;
+ std::size_t number = 0;
+ bool started = false;
+ for (const auto line : ImportLines(text))
+ {
+ ++number;
+ if (ImportTrim(line).empty())
+ {
+ continue;
+ }
+ const std::string where = "line " + std::to_string(number);
+ auto value = ParseJson(line);
+ if (!value)
+ {
+ return value.GetError().Context(where);
+ }
+ if (!started)
+ {
+ started = true;
+ if ((*value)["format"].AsString() != "catch3-results")
+ {
+ return Error{ "the first line must name the format "
+ "catch3-results" };
+ }
+ if ((*value)["version"].AsInteger()
+ > kResultsFormatVersion)
+ {
+ return Error{ "the format is newer than this "
+ "version of Catch3 reads" };
+ }
+ continue;
+ }
+ auto test = ReadLine(*value);
+ if (!test)
+ {
+ return test.GetError().Context(where);
+ }
+ tests.push_back(std::move(*test));
+ }
+ return tests;
+ }
+
+ Expected<std::vector<TestResult>>
+ ParseCatch3(std::string_view text)
+ {
+ auto whole = ParseJson(text);
+ if (!whole)
+ {
+ return ParseLines(text);
+ }
+ if ((*whole)["format"].AsString() != "catch3-run")
+ {
+ return ParseLines(text);
+ }
+ auto run = RunResultFromJson(*whole);
+ if (!run)
+ {
+ return run.GetError();
+ }
+ for (auto &test : run->tests)
+ {
+ Forget(test);
+ }
+ return std::move(run->tests);
+ }
+
+ std::string
+ TextOf(const JsonValue &value)
+ {
+ if (!value.IsArray())
+ {
+ return value.AsString();
+ }
+ std::string text;
+ for (const auto &item : value.Items())
+ {
+ text += item.AsString() + "\n";
+ }
+ return text;
+ }
+
+ Outcome
+ CtrfOutcome(const std::string &status)
+ {
+ if (status == "passed")
+ {
+ return Outcome::kPassed;
+ }
+ if (status == "failed")
+ {
+ return Outcome::kFailed;
+ }
+ if (status == "skipped" || status == "pending")
+ {
+ return Outcome::kSkipped;
+ }
+ return Outcome::kNotRun;
+ }
+
+ /// A test, or one retry of it: the two share these fields.
+ Attempt
+ CtrfAttempt(const JsonValue &value)
+ {
+ Attempt attempt = ImportedAttempt(
+ CtrfOutcome(value["status"].AsString()),
+ ImportedMicroseconds(value["duration"].AsNumber() / 1e3),
+ value["message"].AsString(),
+ value["trace"].AsString());
+ attempt.standardOutput = TextOf(value["stdout"]);
+ attempt.standardError = TextOf(value["stderr"]);
+ return attempt;
+ }
+
+ std::string
+ CtrfSuite(const JsonValue &suite)
+ {
+ if (!suite.IsArray())
+ {
+ return suite.AsString();
+ }
+ std::string text;
+ for (const auto &item : suite.Items())
+ {
+ text += (text.empty() ? "" : " > ") + item.AsString();
+ }
+ return text;
+ }
+
+ Expected<std::vector<TestResult>>
+ ParseCtrf(std::string_view text)
+ {
+ auto document = ParseJson(text);
+ if (!document)
+ {
+ return document.GetError();
+ }
+ const JsonValue &tests = (*document)["results"]["tests"];
+ if (!tests.IsArray())
+ {
+ return Error{ "a CTRF report has results.tests" };
+ }
+ std::vector<TestResult> results;
+ for (const auto &item : tests.Items())
+ {
+ const std::string &name = item["name"].AsString();
+ if (name.empty())
+ {
+ return Error{ "a CTRF test has no name" };
+ }
+ TestResult test
+ = ImportedTest(name, CtrfSuite(item["suite"]));
+ test.file = item["filePath"].AsString();
+ test.line
+ = static_cast<std::size_t>(item["line"].AsUnsigned());
+ test.flaky = item["flaky"].AsBoolean();
+ for (const auto &tag : item["tags"].Items())
+ {
+ test.tags.push_back(tag.AsString());
+ }
+ for (const auto &retry : item["retryAttempts"].Items())
+ {
+ test.attempts.push_back(CtrfAttempt(retry));
+ }
+ // The retries end before the last attempt, which is the
+ // test itself.
+ test.attempts.push_back(CtrfAttempt(item));
+ results.push_back(std::move(test));
+ }
+ return results;
+ }
+ } // namespace ImportJson
+ } // namespace
+
+ std::optional<ImportFormat>
+ ImportJsonFormat(const JsonValue &value)
+ {
+ return ImportJson::FormatOf(value);
+ }
+
+ Expected<std::vector<TestResult>>
+ ParseCatch3Report(std::string_view text)
+ {
+ return ImportJson::ParseCatch3(text);
+ }
+
+ Expected<std::vector<TestResult>>
+ ParseCtrf(std::string_view text)
+ {
+ return ImportJson::ParseCtrf(text);
+ }
+} // namespace Progmasoft::Catch3::Runner