diff options
| author | galaxycrab <[email protected]> | 2023-11-27 16:33:16 +0300 | 
|---|---|---|
| committer | galaxycrab <[email protected]> | 2023-11-27 17:27:37 +0300 | 
| commit | fab1896e3382b8a943398e3ef93fc03ef3d98613 (patch) | |
| tree | 474fe934a510966762beafae65122ac2346279db /library/cpp/testing/unittest | |
| parent | 3536f8d1eba936f9bdd52a24cc3703e893c3c8da (diff) | |
KIKIMR-19979 Remove dependency on libxml of library/cpp/testing/unittest
Diffstat (limited to 'library/cpp/testing/unittest')
| -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 1aeaa1a60b0..baa59845ea6 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 1aeaa1a60b0..baa59845ea6 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 bb5187334f2..0fc8fcbf733 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 bb5187334f2..0fc8fcbf733 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 1aeaa1a60b0..baa59845ea6 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 0c836251106..e4408d95975 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 27b73013ca0..3a9a9658906 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 1daabad4b84..6ecfc3a64f3 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 f9fa7a09b3d..67a3c2d85cf 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 | 
