diff options
author | galaxycrab <UgnineSirdis@ydb.tech> | 2023-11-27 16:33:16 +0300 |
---|---|---|
committer | galaxycrab <UgnineSirdis@ydb.tech> | 2023-11-27 17:27:37 +0300 |
commit | fab1896e3382b8a943398e3ef93fc03ef3d98613 (patch) | |
tree | 474fe934a510966762beafae65122ac2346279db /library/cpp | |
parent | 3536f8d1eba936f9bdd52a24cc3703e893c3c8da (diff) | |
download | ydb-fab1896e3382b8a943398e3ef93fc03ef3d98613.tar.gz |
KIKIMR-19979 Remove dependency on libxml of library/cpp/testing/unittest
Diffstat (limited to 'library/cpp')
-rw-r--r-- | library/cpp/testing/unittest/CMakeLists.darwin-arm64.txt | 2 | ||||
-rw-r--r-- | library/cpp/testing/unittest/CMakeLists.darwin-x86_64.txt | 2 | ||||
-rw-r--r-- | library/cpp/testing/unittest/CMakeLists.linux-aarch64.txt | 2 | ||||
-rw-r--r-- | library/cpp/testing/unittest/CMakeLists.linux-x86_64.txt | 2 | ||||
-rw-r--r-- | library/cpp/testing/unittest/CMakeLists.windows-x86_64.txt | 2 | ||||
-rw-r--r-- | library/cpp/testing/unittest/junit.cpp | 493 | ||||
-rw-r--r-- | library/cpp/testing/unittest/junit.h | 11 | ||||
-rw-r--r-- | library/cpp/testing/unittest/utmain.cpp | 37 | ||||
-rw-r--r-- | library/cpp/testing/unittest/ya.make | 2 |
9 files changed, 390 insertions, 163 deletions
diff --git a/library/cpp/testing/unittest/CMakeLists.darwin-arm64.txt b/library/cpp/testing/unittest/CMakeLists.darwin-arm64.txt index 1aeaa1a60b..baa59845ea 100644 --- a/library/cpp/testing/unittest/CMakeLists.darwin-arm64.txt +++ b/library/cpp/testing/unittest/CMakeLists.darwin-arm64.txt @@ -11,10 +11,10 @@ add_library(cpp-testing-unittest) target_link_libraries(cpp-testing-unittest PUBLIC contrib-libs-cxxsupp yutil - contrib-libs-libxml library-cpp-colorizer library-cpp-dbg_output library-cpp-diff + library-cpp-json cpp-json-writer cpp-testing-common cpp-testing-hook diff --git a/library/cpp/testing/unittest/CMakeLists.darwin-x86_64.txt b/library/cpp/testing/unittest/CMakeLists.darwin-x86_64.txt index 1aeaa1a60b..baa59845ea 100644 --- a/library/cpp/testing/unittest/CMakeLists.darwin-x86_64.txt +++ b/library/cpp/testing/unittest/CMakeLists.darwin-x86_64.txt @@ -11,10 +11,10 @@ add_library(cpp-testing-unittest) target_link_libraries(cpp-testing-unittest PUBLIC contrib-libs-cxxsupp yutil - contrib-libs-libxml library-cpp-colorizer library-cpp-dbg_output library-cpp-diff + library-cpp-json cpp-json-writer cpp-testing-common cpp-testing-hook diff --git a/library/cpp/testing/unittest/CMakeLists.linux-aarch64.txt b/library/cpp/testing/unittest/CMakeLists.linux-aarch64.txt index bb5187334f..0fc8fcbf73 100644 --- a/library/cpp/testing/unittest/CMakeLists.linux-aarch64.txt +++ b/library/cpp/testing/unittest/CMakeLists.linux-aarch64.txt @@ -12,10 +12,10 @@ target_link_libraries(cpp-testing-unittest PUBLIC contrib-libs-linux-headers contrib-libs-cxxsupp yutil - contrib-libs-libxml library-cpp-colorizer library-cpp-dbg_output library-cpp-diff + library-cpp-json cpp-json-writer cpp-testing-common cpp-testing-hook diff --git a/library/cpp/testing/unittest/CMakeLists.linux-x86_64.txt b/library/cpp/testing/unittest/CMakeLists.linux-x86_64.txt index bb5187334f..0fc8fcbf73 100644 --- a/library/cpp/testing/unittest/CMakeLists.linux-x86_64.txt +++ b/library/cpp/testing/unittest/CMakeLists.linux-x86_64.txt @@ -12,10 +12,10 @@ target_link_libraries(cpp-testing-unittest PUBLIC contrib-libs-linux-headers contrib-libs-cxxsupp yutil - contrib-libs-libxml library-cpp-colorizer library-cpp-dbg_output library-cpp-diff + library-cpp-json cpp-json-writer cpp-testing-common cpp-testing-hook diff --git a/library/cpp/testing/unittest/CMakeLists.windows-x86_64.txt b/library/cpp/testing/unittest/CMakeLists.windows-x86_64.txt index 1aeaa1a60b..baa59845ea 100644 --- a/library/cpp/testing/unittest/CMakeLists.windows-x86_64.txt +++ b/library/cpp/testing/unittest/CMakeLists.windows-x86_64.txt @@ -11,10 +11,10 @@ add_library(cpp-testing-unittest) target_link_libraries(cpp-testing-unittest PUBLIC contrib-libs-cxxsupp yutil - contrib-libs-libxml library-cpp-colorizer library-cpp-dbg_output library-cpp-diff + library-cpp-json cpp-json-writer cpp-testing-common cpp-testing-hook diff --git a/library/cpp/testing/unittest/junit.cpp b/library/cpp/testing/unittest/junit.cpp index 0c83625110..e4408d9597 100644 --- a/library/cpp/testing/unittest/junit.cpp +++ b/library/cpp/testing/unittest/junit.cpp @@ -1,7 +1,8 @@ #include "junit.h" -#include <libxml/parser.h> -#include <libxml/xmlwriter.h> +#include <library/cpp/json/json_reader.h> +#include <library/cpp/json/writer/json.h> +#include <library/cpp/json/writer/json_value.h> #include <util/charset/utf8.h> #include <util/generic/scope.h> @@ -28,7 +29,7 @@ namespace NUnitTest { extern const TString Y_UNITTEST_OUTPUT_CMDLINE_OPTION = "Y_UNITTEST_OUTPUT"; extern const TString Y_UNITTEST_TEST_FILTER_FILE_OPTION = "Y_UNITTEST_FILTER_FILE"; -static bool IsAllowedInXml(wchar32 c) { +static bool IsAllowed(wchar32 c) { // https://en.wikipedia.org/wiki/Valid_characters_in_XML return c == 0x9 || c == 0xA @@ -38,7 +39,7 @@ static bool IsAllowedInXml(wchar32 c) { || c >= 0x10000 && c <= 0x10FFFF; } -static TString SanitizeXmlString(TString s) { +static TString SanitizeString(TString s) { TString escaped; bool fixedSomeChars = false; const unsigned char* i = reinterpret_cast<const unsigned char*>(s.data()); @@ -56,7 +57,7 @@ static TString SanitizeXmlString(TString s) { size_t runeLen; const RECODE_RESULT result = SafeReadUTF8Char(rune, runeLen, i, end); if (result == RECODE_OK) { - if (IsAllowedInXml(rune)) { + if (IsAllowed(rune)) { if (fixedSomeChars) { escaped.insert(escaped.end(), reinterpret_cast<const char*>(i), reinterpret_cast<const char*>(i + runeLen)); } @@ -177,9 +178,10 @@ struct TJUnitProcessor::TOutputCapturer { TTempFile TmpFile; }; -TJUnitProcessor::TJUnitProcessor(TString file, TString exec) +TJUnitProcessor::TJUnitProcessor(TString file, TString exec, EOutputFormat outputFormat) : FileName(file) , ExecName(exec) + , OutputFormat(outputFormat) { } @@ -201,8 +203,8 @@ void TJUnitProcessor::OnError(const TError* descr) { if (!GetForkTests() || GetIsForked()) { auto* testCase = GetTestCase(descr->test); TFailure& failure = testCase->Failures.emplace_back(); - failure.Message = SanitizeXmlString(descr->msg); - failure.BackTrace = SanitizeXmlString(descr->BackTrace); + failure.Message = SanitizeString(descr->msg); + failure.BackTrace = SanitizeString(descr->BackTrace); } } @@ -212,7 +214,7 @@ void TJUnitProcessor::TransferFromCapturer(THolder<TJUnitProcessor::TOutputCaptu { TFileInput fileStream(capturer->GetTmpFileName()); TransferData(&fileStream, &outStream); - out = SanitizeXmlString(capturer->GetCapturedString()); + out = SanitizeString(capturer->GetCapturedString()); } capturer = nullptr; } @@ -244,6 +246,16 @@ TString TJUnitProcessor::BuildFileName(size_t index, const TStringBuf extension) return std::move(result); } +TStringBuf TJUnitProcessor::GetFileExtension() const { + switch (OutputFormat) { + case EOutputFormat::Xml: + return ".xml"sv; + case EOutputFormat::Json: + return ".json"sv; + } + return TStringBuf(); +} + void TJUnitProcessor::MakeReportFileName() { constexpr size_t MaxReps = 200; @@ -264,7 +276,7 @@ void TJUnitProcessor::MakeReportFileName() { NFs::MakeDirectoryRecursive(FileName); } for (size_t i = 0; i < MaxReps; ++i) { - TString uniqReportFileName = BuildFileName(i, ".xml"sv); + TString uniqReportFileName = BuildFileName(i, GetFileExtension()); try { TFile newUniqReportFile(uniqReportFileName, EOpenModeFlag::CreateNew); newUniqReportFile.Close(); @@ -296,7 +308,7 @@ void TJUnitProcessor::MakeTmpFileNameForForkedTests() { if (GetForkTests() && !GetIsForked()) { TmpReportFile.ConstructInPlace(MakeTempName()); // Replace option for child processes - SetEnv(Y_UNITTEST_OUTPUT_CMDLINE_OPTION, TStringBuilder() << "xml:" << TmpReportFile->Name()); + SetEnv(Y_UNITTEST_OUTPUT_CMDLINE_OPTION, TStringBuilder() << "json:" << TmpReportFile->Name()); } } @@ -363,114 +375,272 @@ void TJUnitProcessor::SignalHandler(int signal) { } } -#define CHECK_CALL(expr) if (int resultCode = (expr); resultCode < 0) { \ - Cerr << "Faield to write to xml. Result code: " << resultCode << Endl; \ - return; \ +void TJUnitProcessor::SerializeToFile() { + switch (OutputFormat) { + case EOutputFormat::Json: + SerializeToJson(); + break; + case EOutputFormat::Xml: + [[fallthrough]]; + default: + SerializeToXml(); + break; + } } -#define XML_STR(s) ((const xmlChar*)(s)) +void TJUnitProcessor::SerializeToJson() { + TFileOutput out(ResultReportFileName); + NJsonWriter::TBuf json(NJsonWriter::HEM_UNSAFE, &out); + json.SetIndentSpaces(1); + json.BeginObject(); + { + json.WriteKey("tests"sv).WriteInt(GetTestsCount()); + json.WriteKey("failures"sv).WriteInt(GetFailuresCount()); + json.WriteKey("testsuites"sv).BeginList(); + for (const auto& [suiteName, suite] : Suites) { + json.BeginObject(); + json.WriteKey("name"sv).WriteString(suiteName); + json.WriteKey("id"sv).WriteString(suiteName); + json.WriteKey("tests"sv).WriteInt(suite.GetTestsCount()); + json.WriteKey("failures"sv).WriteInt(suite.GetFailuresCount()); + json.WriteKey("time"sv).WriteDouble(suite.GetDurationSeconds()); + json.WriteKey("testcases"sv).BeginList(); + for (const auto& [testName, test] : suite.Cases) { + json.BeginObject(); + json.WriteKey("classname"sv).WriteString(suiteName); + json.WriteKey("name"sv).WriteString(testName); + json.WriteKey("id"sv).WriteString(testName); + json.WriteKey("time"sv).WriteDouble(test.DurationSecods); + json.WriteKey("failures"sv).BeginList(); + for (const auto& failure : test.Failures) { + json.BeginObject(); + json.WriteKey("message"sv).WriteString(failure.Message); + json.WriteKey("type"sv).WriteString("ERROR"sv); + if (failure.BackTrace) { + json.WriteKey("backtrace"sv).WriteString(failure.BackTrace); + } + json.EndObject(); + } + json.EndList(); -void TJUnitProcessor::SerializeToFile() { - auto file = xmlNewTextWriterFilename(ResultReportFileName.c_str(), 0); - if (!file) { - Cerr << "Failed to open xml file for writing: " << ResultReportFileName << Endl; - return; + if (!test.StdOut.empty()) { + json.WriteKey("system-out"sv).WriteString(test.StdOut); + } + if (!test.StdErr.empty()) { + json.WriteKey("system-err"sv).WriteString(test.StdErr); + } + json.EndObject(); + } + json.EndList(); + json.EndObject(); + } + json.EndList(); } + json.EndObject(); +} - Y_DEFER { - xmlFreeTextWriter(file); - }; +class TXmlWriter { +public: + class TTag { + friend class TXmlWriter; - CHECK_CALL(xmlTextWriterSetIndent(file, 1)); + explicit TTag(TXmlWriter* parent, TStringBuf name, size_t indent) + : Parent(parent) + , Name(name) + , Indent(indent) + { + Start(); + } - CHECK_CALL(xmlTextWriterStartDocument(file, nullptr, "UTF-8", nullptr)); - CHECK_CALL(xmlTextWriterStartElement(file, XML_STR("testsuites"))); - CHECK_CALL(xmlTextWriterWriteAttribute(file, XML_STR("tests"), XML_STR(ToString(GetTestsCount()).c_str()))); - CHECK_CALL(xmlTextWriterWriteAttribute(file, XML_STR("failures"), XML_STR(ToString(GetFailuresCount()).c_str()))); + public: + TTag(TTag&& tag) + : Parent(tag.Parent) + , Name(tag.Name) + { + tag.Parent = nullptr; + } - for (const auto& [suiteName, suite] : Suites) { - CHECK_CALL(xmlTextWriterStartElement(file, XML_STR("testsuite"))); - CHECK_CALL(xmlTextWriterWriteAttribute(file, XML_STR("name"), XML_STR(suiteName.c_str()))); - CHECK_CALL(xmlTextWriterWriteAttribute(file, XML_STR("id"), XML_STR(suiteName.c_str()))); - CHECK_CALL(xmlTextWriterWriteAttribute(file, XML_STR("tests"), XML_STR(ToString(suite.GetTestsCount()).c_str()))); - CHECK_CALL(xmlTextWriterWriteAttribute(file, XML_STR("failures"), XML_STR(ToString(suite.GetFailuresCount()).c_str()))); - CHECK_CALL(xmlTextWriterWriteAttribute(file, XML_STR("time"), XML_STR(ToString(suite.GetDurationSeconds()).c_str()))); + ~TTag() { + if (Parent) { + End(); + } + } - for (const auto& [testName, test] : suite.Cases) { - CHECK_CALL(xmlTextWriterStartElement(file, XML_STR("testcase"))); - CHECK_CALL(xmlTextWriterWriteAttribute(file, XML_STR("classname"), XML_STR(suiteName.c_str()))); - CHECK_CALL(xmlTextWriterWriteAttribute(file, XML_STR("name"), XML_STR(testName.c_str()))); - CHECK_CALL(xmlTextWriterWriteAttribute(file, XML_STR("id"), XML_STR(testName.c_str()))); - CHECK_CALL(xmlTextWriterWriteAttribute(file, XML_STR("time"), XML_STR(ToString(test.DurationSecods).c_str()))); + template <class T> + TTag& Attribute(TStringBuf name, const T& value) { + return Attribute(name, TStringBuf(ToString(value))); + } - for (const auto& failure : test.Failures) { - CHECK_CALL(xmlTextWriterStartElement(file, XML_STR("failure"))); - CHECK_CALL(xmlTextWriterWriteAttribute(file, XML_STR("message"), XML_STR(failure.Message.c_str()))); - CHECK_CALL(xmlTextWriterWriteAttribute(file, XML_STR("type"), XML_STR("ERROR"))); - if (failure.BackTrace) { - CHECK_CALL(xmlTextWriterWriteString(file, XML_STR(failure.BackTrace.c_str()))); - } - CHECK_CALL(xmlTextWriterEndElement(file)); + TTag& Attribute(TStringBuf name, const TStringBuf& value) { + Y_ABORT_UNLESS(!HasChildren); + Parent->Out << ' '; + Parent->Escape(name); + Parent->Out << "=\""; + Parent->Escape(value); + Parent->Out << '\"'; + return *this; + } + + TTag Tag(TStringBuf name) { + if (!HasChildren) { + HasChildren = true; + Close(); } + return TTag(Parent, name, Indent + 1); + } - if (!test.StdOut.empty()) { - CHECK_CALL(xmlTextWriterWriteElement(file, XML_STR("system-out"), XML_STR(test.StdOut.c_str()))); + TTag& Text(TStringBuf text) { + if (!HasChildren) { + HasChildren = true; + Close(); } - if (!test.StdErr.empty()) { - CHECK_CALL(xmlTextWriterWriteElement(file, XML_STR("system-err"), XML_STR(test.StdErr.c_str()))); + Parent->Escape(text); + if (!text.empty() && text.back() == '\n') { + NewLineBeforeIndent = false; } + return *this; + } + + private: + void Start() { + Parent->Indent(Indent); + Parent->Out << '<'; + Parent->Escape(Name); + } - CHECK_CALL(xmlTextWriterEndElement(file)); + void Close() { + Parent->Out << '>'; } - CHECK_CALL(xmlTextWriterEndElement(file)); + void End() { + if (HasChildren) { + Parent->Indent(Indent, NewLineBeforeIndent); + Parent->Out << "</"; + Parent->Escape(Name); + Parent->Out << ">"; + } else { + Parent->Out << "/>"; + } + } + + private: + TXmlWriter* Parent = nullptr; + TStringBuf Name; + size_t Indent = 0; + bool HasChildren = false; + bool NewLineBeforeIndent = true; + }; + +public: + explicit TXmlWriter(const TString& fileName) + : Out(fileName) + { + StartFile(); } - CHECK_CALL(xmlTextWriterEndElement(file)); - CHECK_CALL(xmlTextWriterEndDocument(file)); -} + ~TXmlWriter() { + Out << '\n'; + } -#define C_STR(s) ((const char*)(s)) -#define STRBUF(s) TStringBuf(C_STR(s)) + TTag Tag(TStringBuf name) { + return TTag(this, name, 0); + } -#define NODE_NAME(node) STRBUF((node)->name) -#define SAFE_CONTENT(node) (node && node->children ? STRBUF(node->children->content) : TStringBuf()) +private: + void StartFile() { + Out << R"(<?xml version="1.0" encoding="UTF-8"?>)"sv; + } -#define CHECK_NODE_NAME(node, expectedName) if (NODE_NAME(node) != (expectedName)) { \ - ythrow yexception() << "Expected node name: \"" << (expectedName) \ - << "\", but got \"" << TStringBuf(C_STR((node)->name)) << "\""; \ -} + void Indent(size_t count, bool insertNewLine = true) { + if (insertNewLine) { + Out << '\n'; + } -static TString GetAttrValue(xmlNodePtr node, TStringBuf name, bool required = true) { - for (xmlAttrPtr attr = node->properties; attr != nullptr; attr = attr->next) { - if (NODE_NAME(attr) == name) { - return TString(SAFE_CONTENT(attr)); + while (count--) { + Out << ' '; } } - if (required) { - ythrow yexception() << "Attribute \"" << name << "\" was not found"; - } - return {}; -} -static xmlNodePtr NextElement(xmlNodePtr node) { - if (!node) { - return nullptr; + void Escape(const TStringBuf str) { + const unsigned char* i = reinterpret_cast<const unsigned char*>(str.data()); + const unsigned char* end = i + str.size(); + while (i < end) { + wchar32 rune; + size_t runeLen; + const RECODE_RESULT result = SafeReadUTF8Char(rune, runeLen, i, end); + if (result == RECODE_OK) { // string is expected not to have unallowed characters now + switch (rune) { + case '\'': + Out.Write("'"); + break; + case '\"': + Out.Write("""); + break; + case '<': + Out.Write("<"); + break; + case '>': + Out.Write(">"); + break; + case '&': + Out.Write("&"); + break; + default: + Out.Write(i, runeLen); + break; + } + i += runeLen; + } + } } - do { - node = node->next; - } while (node && node->type != XML_ELEMENT_NODE); +private: + TFileOutput Out; +}; + +void TJUnitProcessor::SerializeToXml() { + TXmlWriter report(ResultReportFileName); + TXmlWriter::TTag testSuites = report.Tag("testsuites"sv); + testSuites + .Attribute("tests"sv, GetTestsCount()) + .Attribute("failures"sv, GetFailuresCount()) + .Attribute(""sv, GetFailuresCount()); - return node; -} + for (const auto& [suiteName, suite] : Suites) { + auto testSuite = testSuites.Tag("testsuite"sv); + testSuite + .Attribute("name"sv, suiteName) + .Attribute("id"sv, suiteName) + .Attribute("tests"sv, suite.GetTestsCount()) + .Attribute("failures"sv, suite.GetFailuresCount()) + .Attribute("time"sv, suite.GetDurationSeconds()); + + for (const auto& [testName, test] : suite.Cases) { + auto testCase = testSuite.Tag("testcase"sv); + testCase + .Attribute("classname"sv, suiteName) + .Attribute("name"sv, testName) + .Attribute("id"sv, testName) + .Attribute("time"sv, test.DurationSecods); + + for (const auto& failure : test.Failures) { + auto testFailure = testCase.Tag("failure"sv); + testFailure + .Attribute("message"sv, failure.Message) + .Attribute("type"sv, "ERROR"sv); + if (!failure.BackTrace.empty()) { + testFailure.Text(failure.BackTrace); + } + } -static xmlNodePtr ChildElement(xmlNodePtr node) { - xmlNodePtr child = node->children; - if (child && child->type != XML_ELEMENT_NODE) { - return NextElement(child); + if (!test.StdOut.empty()) { + testCase.Tag("system-out"sv).Text(test.StdOut); + } + if (!test.StdErr.empty()) { + testCase.Tag("system-err"sv).Text(test.StdErr); + } + } } - return child; } void TJUnitProcessor::MergeSubprocessReport() { @@ -490,70 +660,113 @@ void TJUnitProcessor::MergeSubprocessReport() { file.Close(); }; - xmlDocPtr doc = xmlParseFile(TmpReportFile->Name().c_str()); - if (!doc) { - Cerr << "Failed to parse xml output for subprocess" << Endl; + NJson::TJsonValue testsReportJson; + { + TFileInput in(TmpReportFile->Name()); + if (!NJson::ReadJsonTree(&in, &testsReportJson)) { + Cerr << "Failed to read json report for subprocess" << Endl; + return; + } + } + + if (!testsReportJson.IsMap()) { + Cerr << "Invalid subprocess report format: report is not a map" << Endl; return; } - Y_DEFER { - xmlFreeDoc(doc); - }; + const NJson::TJsonValue* testSuitesJson = nullptr; + if (!testsReportJson.GetValuePointer("testsuites"sv, &testSuitesJson)) { + // no tests for some reason + Cerr << "No tests found in subprocess report" << Endl; + return; + } - xmlNodePtr root = xmlDocGetRootElement(doc); - if (!root) { - Cerr << "Failed to parse xml output for subprocess: empty document" << Endl; + if (!testSuitesJson->IsArray()) { + Cerr << "Invalid subprocess report format: testsuites is not an array" << Endl; return; } - CHECK_NODE_NAME(root, "testsuites"); - for (xmlNodePtr suite = ChildElement(root); suite != nullptr; suite = NextElement(suite)) { - try { - CHECK_NODE_NAME(suite, "testsuite"); - TString suiteName = GetAttrValue(suite, "id"); - TTestSuite& suiteInfo = Suites[suiteName]; + for (const NJson::TJsonValue& suiteJson : testSuitesJson->GetArray()) { + if (!suiteJson.IsMap()) { + Cerr << "Invalid subprocess report format: suite is not a map" << Endl; + continue; + } + const NJson::TJsonValue* suiteIdJson = nullptr; + if (!suiteJson.GetValuePointer("id"sv, &suiteIdJson)) { + Cerr << "Invalid subprocess report format: suite does not have id" << Endl; + continue; + } - // Test cases - for (xmlNodePtr testCase = ChildElement(suite); testCase != nullptr; testCase = NextElement(testCase)) { - try { - CHECK_NODE_NAME(testCase, "testcase"); - TString caseName = GetAttrValue(testCase, "id"); - TTestCase& testCaseInfo = suiteInfo.Cases[caseName]; + const TString& suiteId = suiteIdJson->GetString(); + if (suiteId.empty()) { + Cerr << "Invalid subprocess report format: suite has empty id" << Endl; + continue; + } - if (TString duration = GetAttrValue(testCase, "time")) { - TryFromString(duration, testCaseInfo.DurationSecods); - } + TTestSuite& suiteInfo = Suites[suiteId]; + const NJson::TJsonValue* testCasesJson = nullptr; + if (!suiteJson.GetValuePointer("testcases"sv, &testCasesJson)) { + Cerr << "No test cases found in suite \"" << suiteId << "\"" << Endl; + continue; + } + if (!testCasesJson->IsArray()) { + Cerr << "Invalid subprocess report format: testcases value is not an array" << Endl; + continue; + } - // Failures/stderr/stdout - for (xmlNodePtr testProp = ChildElement(testCase); testProp != nullptr; testProp = NextElement(testProp)) { - try { - if (NODE_NAME(testProp) == "failure") { - TString message = GetAttrValue(testProp, "message"); - auto& failure = testCaseInfo.Failures.emplace_back(); - failure.Message = message; - failure.BackTrace = TString(SAFE_CONTENT(testProp)); - } else if (NODE_NAME(testProp) == "system-out") { - testCaseInfo.StdOut = TString(SAFE_CONTENT(testProp)); - } else if (NODE_NAME(testProp) == "system-err") { - testCaseInfo.StdErr = TString(SAFE_CONTENT(testProp)); - } else { - ythrow yexception() << "Unknown test case subprop: \"" << NODE_NAME(testProp) << "\""; - } - } catch (const std::exception& ex) { - auto& failure = testCaseInfo.Failures.emplace_back(); - failure.Message = TStringBuilder() << "Failed to read part of test case info from unit test tool: " << ex.what(); - Cerr << "Failed to load test case " << caseName << " failure in suite " << suiteName << ": " << ex.what() << Endl; - continue; - } - } - } catch (const std::exception& ex) { - Cerr << "Failed to load test case info in suite " << suiteName << ": " << ex.what() << Endl; - continue; + for (const NJson::TJsonValue& testCaseJson : testCasesJson->GetArray()) { + const NJson::TJsonValue* testCaseIdJson = nullptr; + if (!testCaseJson.GetValuePointer("id"sv, &testCaseIdJson)) { + Cerr << "Invalid subprocess report format: test case does not have id" << Endl; + continue; + } + + const TString& testCaseId = testCaseIdJson->GetString(); + if (testCaseId.empty()) { + Cerr << "Invalid subprocess report format: test case has empty id" << Endl; + continue; + } + + TTestCase& testCaseInfo = suiteInfo.Cases[testCaseId]; + + const NJson::TJsonValue* testCaseDurationJson = nullptr; + if (testCaseJson.GetValuePointer("time"sv, &testCaseDurationJson)) { + testCaseInfo.DurationSecods = testCaseDurationJson->GetDouble(); // Will handle also integers as double + } + + const NJson::TJsonValue* stdOutJson = nullptr; + if (testCaseJson.GetValuePointer("system-out"sv, &stdOutJson)) { + testCaseInfo.StdOut = stdOutJson->GetString(); + } + + const NJson::TJsonValue* stdErrJson = nullptr; + if (testCaseJson.GetValuePointer("system-err"sv, &stdErrJson)) { + testCaseInfo.StdErr = stdErrJson->GetString(); + } + + const NJson::TJsonValue* failuresJson = nullptr; + if (!testCaseJson.GetValuePointer("failures"sv, &failuresJson)) { + continue; + } + + if (!failuresJson->IsArray()) { + Cerr << "Invalid subprocess report format: failures is not an array" << Endl; + continue; + } + + for (const NJson::TJsonValue& failureJson : failuresJson->GetArray()) { + TFailure& failureInfo = testCaseInfo.Failures.emplace_back(); + + const NJson::TJsonValue* messageJson = nullptr; + if (failureJson.GetValuePointer("message"sv, &messageJson)) { + failureInfo.Message = messageJson->GetString(); + } + + const NJson::TJsonValue* backtraceJson = nullptr; + if (failureJson.GetValuePointer("backtrace"sv, &backtraceJson)) { + failureInfo.BackTrace = backtraceJson->GetString(); } } - } catch (const std::exception& ex) { - Cerr << "Failed to load test suite info from xml: " << ex.what() << Endl; - continue; } } } diff --git a/library/cpp/testing/unittest/junit.h b/library/cpp/testing/unittest/junit.h index 27b73013ca..3a9a965890 100644 --- a/library/cpp/testing/unittest/junit.h +++ b/library/cpp/testing/unittest/junit.h @@ -77,7 +77,12 @@ class TJUnitProcessor : public ITestSuiteProcessor { struct TOutputCapturer; public: - TJUnitProcessor(TString file, TString exec); + enum class EOutputFormat { + Xml, + Json, + }; + + TJUnitProcessor(TString file, TString exec, EOutputFormat outputFormat); ~TJUnitProcessor(); void SetForkTestsParams(bool forkTests, bool isForked) override; @@ -111,9 +116,12 @@ private: } void SerializeToFile(); + void SerializeToXml(); + void SerializeToJson(); void MergeSubprocessReport(); TString BuildFileName(size_t index, const TStringBuf extension) const; + TStringBuf GetFileExtension() const; void MakeReportFileName(); void MakeTmpFileNameForForkedTests(); static void TransferFromCapturer(THolder<TJUnitProcessor::TOutputCapturer>& capturer, TString& out, IOutputStream& outStream); @@ -125,6 +133,7 @@ private: private: const TString FileName; // cmd line param const TString ExecName; // cmd line param + const EOutputFormat OutputFormat; TString ResultReportFileName; TMaybe<TTempFile> TmpReportFile; TMap<TString, TTestSuite> Suites; diff --git a/library/cpp/testing/unittest/utmain.cpp b/library/cpp/testing/unittest/utmain.cpp index 1daabad4b8..6ecfc3a64f 100644 --- a/library/cpp/testing/unittest/utmain.cpp +++ b/library/cpp/testing/unittest/utmain.cpp @@ -754,7 +754,24 @@ int NUnitTest::RunMain(int argc, char** argv) { processor.FilterFromFile(filterFn); } - + auto processJunitOption = [&](const TStringBuf& v) { + if (!hasJUnitProcessor) { + hasJUnitProcessor = true; + bool xmlFormat = false; + constexpr TStringBuf xmlPrefix = "xml:"; + constexpr TStringBuf jsonPrefix = "json:"; + if ((xmlFormat = v.StartsWith(xmlPrefix)) || v.StartsWith(jsonPrefix)) { + TStringBuf fileName = v; + const TStringBuf prefix = xmlFormat ? xmlPrefix : jsonPrefix; + fileName = fileName.SubString(prefix.size(), TStringBuf::npos); + const TJUnitProcessor::EOutputFormat format = xmlFormat ? TJUnitProcessor::EOutputFormat::Xml : TJUnitProcessor::EOutputFormat::Json; + NUnitTest::ShouldColorizeDiff = false; + traceProcessors.push_back(std::make_shared<TJUnitProcessor>(TString(fileName), + std::filesystem::path(argv[0]).stem().string(), + format)); + } + } + }; for (size_t i = 1; i < (size_t)argc; ++i) { const char* name = argv[i]; @@ -814,14 +831,7 @@ int NUnitTest::RunMain(int argc, char** argv) { } else if (strcmp(name, "--output") == 0) { ++i; Y_ENSURE((int)i < argc); - TString param(argv[i]); - if (param.StartsWith("xml:") && !hasJUnitProcessor) { - TStringBuf fileName = param; - fileName = fileName.SubString(4, TStringBuf::npos); - NUnitTest::ShouldColorizeDiff = false; - traceProcessors.push_back(std::make_shared<TJUnitProcessor>(TString(fileName), argv[0])); - } - hasJUnitProcessor = true; + processJunitOption(argv[i]); } else if (strcmp(name, "--filter-file") == 0) { ++i; TString filename(argv[i]); @@ -842,13 +852,8 @@ int NUnitTest::RunMain(int argc, char** argv) { } if (!hasJUnitProcessor) { - TString oo(GetEnv(Y_UNITTEST_OUTPUT_CMDLINE_OPTION)); - if (oo.StartsWith("xml:")) { - TStringBuf fileName = oo; - fileName = fileName.SubString(4, TStringBuf::npos); - NUnitTest::ShouldColorizeDiff = false; - traceProcessors.push_back(std::make_shared<TJUnitProcessor>(TString(fileName), - std::filesystem::path(argv[0]).stem().string())); + if (TString oo = GetEnv(Y_UNITTEST_OUTPUT_CMDLINE_OPTION)) { + processJunitOption(oo); } } diff --git a/library/cpp/testing/unittest/ya.make b/library/cpp/testing/unittest/ya.make index f9fa7a09b3..67a3c2d85c 100644 --- a/library/cpp/testing/unittest/ya.make +++ b/library/cpp/testing/unittest/ya.make @@ -3,10 +3,10 @@ LIBRARY() PROVIDES(test_framework) PEERDIR( - contrib/libs/libxml library/cpp/colorizer library/cpp/dbg_output library/cpp/diff + library/cpp/json library/cpp/json/writer library/cpp/testing/common library/cpp/testing/hook |