junit.cpp 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773
  1. #include "junit.h"
  2. #include <library/cpp/json/json_reader.h>
  3. #include <library/cpp/json/writer/json.h>
  4. #include <library/cpp/json/writer/json_value.h>
  5. #include <util/charset/utf8.h>
  6. #include <util/generic/scope.h>
  7. #include <util/generic/size_literals.h>
  8. #include <util/stream/file.h>
  9. #include <util/stream/input.h>
  10. #include <util/system/backtrace.h>
  11. #include <util/system/env.h>
  12. #include <util/system/file.h>
  13. #include <util/system/fs.h>
  14. #include <util/system/file.h>
  15. #include <util/system/fstat.h>
  16. #include <util/system/tempfile.h>
  17. #include <stdio.h>
  18. #include <signal.h>
  19. #if defined(_win_)
  20. #include <io.h>
  21. #endif
  22. namespace NUnitTest {
  23. extern const TString Y_UNITTEST_OUTPUT_CMDLINE_OPTION = "Y_UNITTEST_OUTPUT";
  24. extern const TString Y_UNITTEST_TEST_FILTER_FILE_OPTION = "Y_UNITTEST_FILTER_FILE";
  25. static bool IsAllowed(wchar32 c) {
  26. // https://en.wikipedia.org/wiki/Valid_characters_in_XML
  27. return c == 0x9
  28. || c == 0xA
  29. || c == 0xD
  30. || c >= 0x20 && c <= 0xD7FF
  31. || c >= 0xE000 && c <= 0xFFFD
  32. || c >= 0x10000 && c <= 0x10FFFF;
  33. }
  34. static TString SanitizeString(TString s) {
  35. TString escaped;
  36. bool fixedSomeChars = false;
  37. const unsigned char* i = reinterpret_cast<const unsigned char*>(s.data());
  38. const unsigned char* end = i + s.size();
  39. auto replaceChar = [&]() {
  40. if (!fixedSomeChars) {
  41. fixedSomeChars = true;
  42. escaped.reserve(s.size());
  43. escaped.insert(escaped.end(), s.data(), reinterpret_cast<const char*>(i));
  44. }
  45. escaped.push_back('?');
  46. };
  47. while (i < end) {
  48. wchar32 rune;
  49. size_t runeLen;
  50. const RECODE_RESULT result = SafeReadUTF8Char(rune, runeLen, i, end);
  51. if (result == RECODE_OK) {
  52. if (IsAllowed(rune)) {
  53. if (fixedSomeChars) {
  54. escaped.insert(escaped.end(), reinterpret_cast<const char*>(i), reinterpret_cast<const char*>(i + runeLen));
  55. }
  56. } else {
  57. replaceChar();
  58. }
  59. i += runeLen;
  60. } else {
  61. replaceChar();
  62. ++i;
  63. }
  64. }
  65. if (fixedSomeChars) {
  66. return escaped;
  67. } else {
  68. return s;
  69. }
  70. }
  71. struct TJUnitProcessor::TOutputCapturer {
  72. static constexpr int STDOUT_FD = 1;
  73. static constexpr int STDERR_FD = 2;
  74. TOutputCapturer(int fd)
  75. : FdToCapture(fd)
  76. , TmpFile(MakeTempName())
  77. {
  78. {
  79. #if defined(_win_)
  80. TFileHandle f((FHANDLE)_get_osfhandle(FdToCapture));
  81. #else
  82. TFileHandle f(FdToCapture);
  83. #endif
  84. TFileHandle other(f.Duplicate());
  85. Original.Swap(other);
  86. f.Release();
  87. }
  88. TFileHandle captured(TmpFile.Name(), EOpenModeFlag::OpenAlways | EOpenModeFlag::RdWr);
  89. fflush(nullptr);
  90. captured.Duplicate2Posix(FdToCapture);
  91. }
  92. ~TOutputCapturer() {
  93. Uncapture();
  94. }
  95. void Uncapture() {
  96. if (Original.IsOpen()) {
  97. fflush(nullptr);
  98. Original.Duplicate2Posix(FdToCapture);
  99. Original.Close();
  100. }
  101. }
  102. TString GetTmpFileName() {
  103. Uncapture();
  104. return TmpFile.Name();
  105. }
  106. TString GetCapturedString() {
  107. Uncapture();
  108. TFile captured(TmpFile.Name(), EOpenModeFlag::RdOnly);
  109. i64 len = captured.GetLength();
  110. if (len > 0) {
  111. try {
  112. constexpr size_t LIMIT = 10_KB;
  113. constexpr size_t PART_LIMIT = 5_KB;
  114. TStringBuilder out;
  115. if (static_cast<size_t>(len) <= LIMIT) {
  116. out.resize(len);
  117. captured.Read((void*)out.data(), len);
  118. } else {
  119. // Read first 5_KB
  120. {
  121. TString first;
  122. first.resize(PART_LIMIT);
  123. captured.Read((void*)first.data(), PART_LIMIT);
  124. size_t lastNewLine = first.find_last_of('\n');
  125. if (lastNewLine == TString::npos) {
  126. out << first << Endl;
  127. } else {
  128. out << TStringBuf(first.c_str(), lastNewLine);
  129. }
  130. }
  131. out << Endl << Endl << "...SKIPPED..." << Endl << Endl;
  132. // Read last 5_KB
  133. {
  134. TString last;
  135. last.resize(PART_LIMIT);
  136. captured.Seek(-PART_LIMIT, sEnd);
  137. captured.Read((void*)last.data(), PART_LIMIT);
  138. size_t newLine = last.find_first_of('\n');
  139. if (newLine == TString::npos) {
  140. out << last << Endl;
  141. } else {
  142. out << TStringBuf(last.c_str() + newLine + 1);
  143. }
  144. }
  145. }
  146. if (out.back() != '\n') {
  147. out << Endl;
  148. }
  149. return std::move(out);
  150. } catch (const std::exception& ex) {
  151. Cerr << "Failed to read from captured output: " << ex.what() << Endl;
  152. }
  153. }
  154. return {};
  155. }
  156. const int FdToCapture;
  157. TFileHandle Original;
  158. TTempFile TmpFile;
  159. };
  160. TJUnitProcessor::TJUnitProcessor(TString file, TString exec, EOutputFormat outputFormat)
  161. : FileName(file)
  162. , ExecName(exec)
  163. , OutputFormat(outputFormat)
  164. {
  165. }
  166. TJUnitProcessor::~TJUnitProcessor() {
  167. Save();
  168. }
  169. void TJUnitProcessor::OnBeforeTest(const TTest* test) {
  170. CurrentTest.emplace(test);
  171. CaptureSignal(this);
  172. if (!GetForkTests() || GetIsForked()) {
  173. StdErrCapturer = MakeHolder<TOutputCapturer>(TOutputCapturer::STDERR_FD);
  174. StdOutCapturer = MakeHolder<TOutputCapturer>(TOutputCapturer::STDOUT_FD);
  175. StartCurrentTestTime = TInstant::Now();
  176. }
  177. }
  178. void TJUnitProcessor::OnError(const TError* descr) {
  179. if (!GetForkTests() || GetIsForked()) {
  180. auto* testCase = GetTestCase(descr->test);
  181. TFailure& failure = testCase->Failures.emplace_back();
  182. failure.Message = SanitizeString(descr->msg);
  183. failure.BackTrace = SanitizeString(descr->BackTrace);
  184. }
  185. }
  186. void TJUnitProcessor::TransferFromCapturer(THolder<TJUnitProcessor::TOutputCapturer>& capturer, TString& out, IOutputStream& outStream) {
  187. if (capturer) {
  188. capturer->Uncapture();
  189. {
  190. TFileInput fileStream(capturer->GetTmpFileName());
  191. TransferData(&fileStream, &outStream);
  192. out = SanitizeString(capturer->GetCapturedString());
  193. }
  194. capturer = nullptr;
  195. }
  196. }
  197. void TJUnitProcessor::OnFinish(const TFinish* descr) {
  198. if (!GetForkTests() || GetIsForked()) {
  199. auto* testCase = GetTestCase(descr->test);
  200. testCase->Success = descr->Success;
  201. if (StartCurrentTestTime != TInstant::Zero()) {
  202. testCase->DurationSecods = (TInstant::Now() - StartCurrentTestTime).SecondsFloat();
  203. }
  204. StartCurrentTestTime = TInstant::Zero();
  205. TransferFromCapturer(StdOutCapturer, testCase->StdOut, Cout);
  206. TransferFromCapturer(StdErrCapturer, testCase->StdErr, Cerr);
  207. } else {
  208. MergeSubprocessReport();
  209. }
  210. UncaptureSignal();
  211. }
  212. TString TJUnitProcessor::BuildFileName(size_t index, const TStringBuf extension) const {
  213. TStringBuilder result;
  214. result << FileName << ExecName;
  215. if (index > 0) {
  216. result << "-"sv << index;
  217. }
  218. result << extension;
  219. return std::move(result);
  220. }
  221. TStringBuf TJUnitProcessor::GetFileExtension() const {
  222. switch (OutputFormat) {
  223. case EOutputFormat::Xml:
  224. return ".xml"sv;
  225. case EOutputFormat::Json:
  226. return ".json"sv;
  227. }
  228. return TStringBuf();
  229. }
  230. void TJUnitProcessor::MakeReportFileName() {
  231. constexpr size_t MaxReps = 200;
  232. #if defined(_win_)
  233. constexpr char DirSeparator = '\\';
  234. #else
  235. constexpr char DirSeparator = '/';
  236. #endif
  237. if (!ResultReportFileName.empty()) {
  238. return;
  239. }
  240. if (GetIsForked() || !FileName.empty() && FileName.back() != DirSeparator) {
  241. ResultReportFileName = FileName;
  242. } else { // Directory is specified, => make unique report name
  243. if (!FileName.empty()) {
  244. NFs::MakeDirectoryRecursive(FileName);
  245. }
  246. for (size_t i = 0; i < MaxReps; ++i) {
  247. TString uniqReportFileName = BuildFileName(i, GetFileExtension());
  248. try {
  249. TFile newUniqReportFile(uniqReportFileName, EOpenModeFlag::CreateNew);
  250. newUniqReportFile.Close();
  251. ResultReportFileName = std::move(uniqReportFileName);
  252. break;
  253. } catch (const TFileError&) {
  254. // File already exists => try next name
  255. }
  256. }
  257. }
  258. if (ResultReportFileName.empty()) {
  259. Cerr << "Could not find a vacant file name to write report for path " << FileName << ", maximum number of reports: " << MaxReps << Endl;
  260. Y_ABORT("Cannot write report");
  261. }
  262. }
  263. void TJUnitProcessor::Save() {
  264. MakeReportFileName();
  265. SerializeToFile();
  266. }
  267. void TJUnitProcessor::SetForkTestsParams(bool forkTests, bool isForked) {
  268. ITestSuiteProcessor::SetForkTestsParams(forkTests, isForked);
  269. MakeTmpFileNameForForkedTests();
  270. }
  271. void TJUnitProcessor::MakeTmpFileNameForForkedTests() {
  272. if (GetForkTests() && !GetIsForked()) {
  273. TmpReportFile.ConstructInPlace(MakeTempName());
  274. // Replace option for child processes
  275. SetEnv(Y_UNITTEST_OUTPUT_CMDLINE_OPTION, TStringBuilder() << "json:" << TmpReportFile->Name());
  276. }
  277. }
  278. static TJUnitProcessor* CurrentJUnitProcessor = nullptr;
  279. void TJUnitProcessor::CaptureSignal(TJUnitProcessor* processor) {
  280. CurrentJUnitProcessor = processor;
  281. processor->PrevAbortHandler = signal(SIGABRT, &TJUnitProcessor::SignalHandler);
  282. if (processor->PrevAbortHandler == SIG_ERR) {
  283. processor->PrevAbortHandler = nullptr;
  284. }
  285. processor->PrevSegvHandler = signal(SIGSEGV, &TJUnitProcessor::SignalHandler);
  286. if (processor->PrevSegvHandler == SIG_ERR) {
  287. processor->PrevSegvHandler = nullptr;
  288. }
  289. }
  290. void TJUnitProcessor::UncaptureSignal() {
  291. if (CurrentJUnitProcessor) {
  292. if (CurrentJUnitProcessor->PrevAbortHandler != nullptr) {
  293. signal(SIGABRT, CurrentJUnitProcessor->PrevAbortHandler);
  294. } else {
  295. signal(SIGABRT, SIG_DFL);
  296. }
  297. if (CurrentJUnitProcessor->PrevSegvHandler != nullptr) {
  298. signal(SIGSEGV, CurrentJUnitProcessor->PrevSegvHandler);
  299. } else {
  300. signal(SIGSEGV, SIG_DFL);
  301. }
  302. }
  303. }
  304. void TJUnitProcessor::SignalHandler(int signal) {
  305. if (CurrentJUnitProcessor) {
  306. if (CurrentJUnitProcessor->CurrentTest) {
  307. TError errDesc;
  308. errDesc.test = *CurrentJUnitProcessor->CurrentTest;
  309. if (signal == SIGABRT) {
  310. errDesc.msg = "Test aborted";
  311. } else {
  312. errDesc.msg = "Segmentation fault";
  313. PrintBackTrace();
  314. }
  315. CurrentJUnitProcessor->OnError(&errDesc);
  316. TFinish finishDesc;
  317. finishDesc.Success = false;
  318. finishDesc.test = *CurrentJUnitProcessor->CurrentTest;
  319. CurrentJUnitProcessor->OnFinish(&finishDesc);
  320. }
  321. CurrentJUnitProcessor->Save();
  322. if (signal == SIGABRT) {
  323. if (CurrentJUnitProcessor->PrevAbortHandler) {
  324. CurrentJUnitProcessor->PrevAbortHandler(signal);
  325. }
  326. } else {
  327. if (CurrentJUnitProcessor->PrevSegvHandler) {
  328. CurrentJUnitProcessor->PrevSegvHandler(signal);
  329. }
  330. }
  331. }
  332. }
  333. void TJUnitProcessor::SerializeToFile() {
  334. switch (OutputFormat) {
  335. case EOutputFormat::Json:
  336. SerializeToJson();
  337. break;
  338. case EOutputFormat::Xml:
  339. [[fallthrough]];
  340. default:
  341. SerializeToXml();
  342. break;
  343. }
  344. }
  345. void TJUnitProcessor::SerializeToJson() {
  346. TFileOutput out(ResultReportFileName);
  347. NJsonWriter::TBuf json(NJsonWriter::HEM_UNSAFE, &out);
  348. json.SetIndentSpaces(1);
  349. json.BeginObject();
  350. {
  351. json.WriteKey("tests"sv).WriteInt(GetTestsCount());
  352. json.WriteKey("failures"sv).WriteInt(GetFailuresCount());
  353. json.WriteKey("testsuites"sv).BeginList();
  354. for (const auto& [suiteName, suite] : Suites) {
  355. json.BeginObject();
  356. json.WriteKey("name"sv).WriteString(suiteName);
  357. json.WriteKey("id"sv).WriteString(suiteName);
  358. json.WriteKey("tests"sv).WriteInt(suite.GetTestsCount());
  359. json.WriteKey("failures"sv).WriteInt(suite.GetFailuresCount());
  360. json.WriteKey("time"sv).WriteDouble(suite.GetDurationSeconds());
  361. json.WriteKey("testcases"sv).BeginList();
  362. for (const auto& [testName, test] : suite.Cases) {
  363. json.BeginObject();
  364. json.WriteKey("classname"sv).WriteString(suiteName);
  365. json.WriteKey("name"sv).WriteString(testName);
  366. json.WriteKey("id"sv).WriteString(testName);
  367. json.WriteKey("time"sv).WriteDouble(test.DurationSecods);
  368. json.WriteKey("failures"sv).BeginList();
  369. for (const auto& failure : test.Failures) {
  370. json.BeginObject();
  371. json.WriteKey("message"sv).WriteString(failure.Message);
  372. json.WriteKey("type"sv).WriteString("ERROR"sv);
  373. if (failure.BackTrace) {
  374. json.WriteKey("backtrace"sv).WriteString(failure.BackTrace);
  375. }
  376. json.EndObject();
  377. }
  378. json.EndList();
  379. if (!test.StdOut.empty()) {
  380. json.WriteKey("system-out"sv).WriteString(test.StdOut);
  381. }
  382. if (!test.StdErr.empty()) {
  383. json.WriteKey("system-err"sv).WriteString(test.StdErr);
  384. }
  385. json.EndObject();
  386. }
  387. json.EndList();
  388. json.EndObject();
  389. }
  390. json.EndList();
  391. }
  392. json.EndObject();
  393. }
  394. class TXmlWriter {
  395. public:
  396. class TTag {
  397. friend class TXmlWriter;
  398. explicit TTag(TXmlWriter* parent, TStringBuf name, size_t indent)
  399. : Parent(parent)
  400. , Name(name)
  401. , Indent(indent)
  402. {
  403. Start();
  404. }
  405. public:
  406. TTag(TTag&& tag)
  407. : Parent(tag.Parent)
  408. , Name(tag.Name)
  409. {
  410. tag.Parent = nullptr;
  411. }
  412. ~TTag() {
  413. if (Parent) {
  414. End();
  415. }
  416. }
  417. template <class T>
  418. TTag& Attribute(TStringBuf name, const T& value) {
  419. return Attribute(name, TStringBuf(ToString(value)));
  420. }
  421. TTag& Attribute(TStringBuf name, const TStringBuf& value) {
  422. Y_ABORT_UNLESS(!HasChildren);
  423. Parent->Out << ' ';
  424. Parent->Escape(name);
  425. Parent->Out << "=\"";
  426. Parent->Escape(value);
  427. Parent->Out << '\"';
  428. return *this;
  429. }
  430. TTag Tag(TStringBuf name) {
  431. if (!HasChildren) {
  432. HasChildren = true;
  433. Close();
  434. }
  435. return TTag(Parent, name, Indent + 1);
  436. }
  437. TTag& Text(TStringBuf text) {
  438. if (!HasChildren) {
  439. HasChildren = true;
  440. Close();
  441. }
  442. Parent->Escape(text);
  443. if (!text.empty() && text.back() == '\n') {
  444. NewLineBeforeIndent = false;
  445. }
  446. return *this;
  447. }
  448. private:
  449. void Start() {
  450. Parent->Indent(Indent);
  451. Parent->Out << '<';
  452. Parent->Escape(Name);
  453. }
  454. void Close() {
  455. Parent->Out << '>';
  456. }
  457. void End() {
  458. if (HasChildren) {
  459. Parent->Indent(Indent, NewLineBeforeIndent);
  460. Parent->Out << "</";
  461. Parent->Escape(Name);
  462. Parent->Out << ">";
  463. } else {
  464. Parent->Out << "/>";
  465. }
  466. }
  467. private:
  468. TXmlWriter* Parent = nullptr;
  469. TStringBuf Name;
  470. size_t Indent = 0;
  471. bool HasChildren = false;
  472. bool NewLineBeforeIndent = true;
  473. };
  474. public:
  475. explicit TXmlWriter(const TString& fileName)
  476. : Out(fileName)
  477. {
  478. StartFile();
  479. }
  480. ~TXmlWriter() {
  481. Out << '\n';
  482. }
  483. TTag Tag(TStringBuf name) {
  484. return TTag(this, name, 0);
  485. }
  486. private:
  487. void StartFile() {
  488. Out << R"(<?xml version="1.0" encoding="UTF-8"?>)"sv;
  489. }
  490. void Indent(size_t count, bool insertNewLine = true) {
  491. if (insertNewLine) {
  492. Out << '\n';
  493. }
  494. while (count--) {
  495. Out << ' ';
  496. }
  497. }
  498. void Escape(const TStringBuf str) {
  499. const unsigned char* i = reinterpret_cast<const unsigned char*>(str.data());
  500. const unsigned char* end = i + str.size();
  501. while (i < end) {
  502. wchar32 rune;
  503. size_t runeLen;
  504. const RECODE_RESULT result = SafeReadUTF8Char(rune, runeLen, i, end);
  505. if (result == RECODE_OK) { // string is expected not to have unallowed characters now
  506. switch (rune) {
  507. case '\'':
  508. Out.Write("&apos;");
  509. break;
  510. case '\"':
  511. Out.Write("&quot;");
  512. break;
  513. case '<':
  514. Out.Write("&lt;");
  515. break;
  516. case '>':
  517. Out.Write("&gt;");
  518. break;
  519. case '&':
  520. Out.Write("&amp;");
  521. break;
  522. default:
  523. Out.Write(i, runeLen);
  524. break;
  525. }
  526. i += runeLen;
  527. }
  528. }
  529. }
  530. private:
  531. TFileOutput Out;
  532. };
  533. void TJUnitProcessor::SerializeToXml() {
  534. TXmlWriter report(ResultReportFileName);
  535. TXmlWriter::TTag testSuites = report.Tag("testsuites"sv);
  536. testSuites
  537. .Attribute("tests"sv, GetTestsCount())
  538. .Attribute("failures"sv, GetFailuresCount());
  539. for (const auto& [suiteName, suite] : Suites) {
  540. auto testSuite = testSuites.Tag("testsuite"sv);
  541. testSuite
  542. .Attribute("name"sv, suiteName)
  543. .Attribute("id"sv, suiteName)
  544. .Attribute("tests"sv, suite.GetTestsCount())
  545. .Attribute("failures"sv, suite.GetFailuresCount())
  546. .Attribute("time"sv, suite.GetDurationSeconds());
  547. for (const auto& [testName, test] : suite.Cases) {
  548. auto testCase = testSuite.Tag("testcase"sv);
  549. testCase
  550. .Attribute("classname"sv, suiteName)
  551. .Attribute("name"sv, testName)
  552. .Attribute("id"sv, testName)
  553. .Attribute("time"sv, test.DurationSecods);
  554. for (const auto& failure : test.Failures) {
  555. auto testFailure = testCase.Tag("failure"sv);
  556. testFailure
  557. .Attribute("message"sv, failure.Message)
  558. .Attribute("type"sv, "ERROR"sv);
  559. if (!failure.BackTrace.empty()) {
  560. testFailure.Text(failure.BackTrace);
  561. }
  562. }
  563. if (!test.StdOut.empty()) {
  564. testCase.Tag("system-out"sv).Text(test.StdOut);
  565. }
  566. if (!test.StdErr.empty()) {
  567. testCase.Tag("system-err"sv).Text(test.StdErr);
  568. }
  569. }
  570. }
  571. }
  572. void TJUnitProcessor::MergeSubprocessReport() {
  573. {
  574. const i64 len = GetFileLength(TmpReportFile->Name());
  575. if (len < 0) {
  576. Cerr << "Failed to get length of the output file for subprocess" << Endl;
  577. return;
  578. }
  579. if (len == 0) {
  580. return; // Empty file
  581. }
  582. }
  583. Y_DEFER {
  584. TFile file(TmpReportFile->Name(), EOpenModeFlag::TruncExisting);
  585. file.Close();
  586. };
  587. NJson::TJsonValue testsReportJson;
  588. {
  589. TFileInput in(TmpReportFile->Name());
  590. if (!NJson::ReadJsonTree(&in, &testsReportJson)) {
  591. Cerr << "Failed to read json report for subprocess" << Endl;
  592. return;
  593. }
  594. }
  595. if (!testsReportJson.IsMap()) {
  596. Cerr << "Invalid subprocess report format: report is not a map" << Endl;
  597. return;
  598. }
  599. const NJson::TJsonValue* testSuitesJson = nullptr;
  600. if (!testsReportJson.GetValuePointer("testsuites"sv, &testSuitesJson)) {
  601. // no tests for some reason
  602. Cerr << "No tests found in subprocess report" << Endl;
  603. return;
  604. }
  605. if (!testSuitesJson->IsArray()) {
  606. Cerr << "Invalid subprocess report format: testsuites is not an array" << Endl;
  607. return;
  608. }
  609. for (const NJson::TJsonValue& suiteJson : testSuitesJson->GetArray()) {
  610. if (!suiteJson.IsMap()) {
  611. Cerr << "Invalid subprocess report format: suite is not a map" << Endl;
  612. continue;
  613. }
  614. const NJson::TJsonValue* suiteIdJson = nullptr;
  615. if (!suiteJson.GetValuePointer("id"sv, &suiteIdJson)) {
  616. Cerr << "Invalid subprocess report format: suite does not have id" << Endl;
  617. continue;
  618. }
  619. const TString& suiteId = suiteIdJson->GetString();
  620. if (suiteId.empty()) {
  621. Cerr << "Invalid subprocess report format: suite has empty id" << Endl;
  622. continue;
  623. }
  624. TTestSuite& suiteInfo = Suites[suiteId];
  625. const NJson::TJsonValue* testCasesJson = nullptr;
  626. if (!suiteJson.GetValuePointer("testcases"sv, &testCasesJson)) {
  627. Cerr << "No test cases found in suite \"" << suiteId << "\"" << Endl;
  628. continue;
  629. }
  630. if (!testCasesJson->IsArray()) {
  631. Cerr << "Invalid subprocess report format: testcases value is not an array" << Endl;
  632. continue;
  633. }
  634. for (const NJson::TJsonValue& testCaseJson : testCasesJson->GetArray()) {
  635. const NJson::TJsonValue* testCaseIdJson = nullptr;
  636. if (!testCaseJson.GetValuePointer("id"sv, &testCaseIdJson)) {
  637. Cerr << "Invalid subprocess report format: test case does not have id" << Endl;
  638. continue;
  639. }
  640. const TString& testCaseId = testCaseIdJson->GetString();
  641. if (testCaseId.empty()) {
  642. Cerr << "Invalid subprocess report format: test case has empty id" << Endl;
  643. continue;
  644. }
  645. TTestCase& testCaseInfo = suiteInfo.Cases[testCaseId];
  646. const NJson::TJsonValue* testCaseDurationJson = nullptr;
  647. if (testCaseJson.GetValuePointer("time"sv, &testCaseDurationJson)) {
  648. testCaseInfo.DurationSecods = testCaseDurationJson->GetDouble(); // Will handle also integers as double
  649. }
  650. const NJson::TJsonValue* stdOutJson = nullptr;
  651. if (testCaseJson.GetValuePointer("system-out"sv, &stdOutJson)) {
  652. testCaseInfo.StdOut = stdOutJson->GetString();
  653. }
  654. const NJson::TJsonValue* stdErrJson = nullptr;
  655. if (testCaseJson.GetValuePointer("system-err"sv, &stdErrJson)) {
  656. testCaseInfo.StdErr = stdErrJson->GetString();
  657. }
  658. const NJson::TJsonValue* failuresJson = nullptr;
  659. if (!testCaseJson.GetValuePointer("failures"sv, &failuresJson)) {
  660. continue;
  661. }
  662. if (!failuresJson->IsArray()) {
  663. Cerr << "Invalid subprocess report format: failures is not an array" << Endl;
  664. continue;
  665. }
  666. for (const NJson::TJsonValue& failureJson : failuresJson->GetArray()) {
  667. TFailure& failureInfo = testCaseInfo.Failures.emplace_back();
  668. const NJson::TJsonValue* messageJson = nullptr;
  669. if (failureJson.GetValuePointer("message"sv, &messageJson)) {
  670. failureInfo.Message = messageJson->GetString();
  671. }
  672. const NJson::TJsonValue* backtraceJson = nullptr;
  673. if (failureJson.GetValuePointer("backtrace"sv, &backtraceJson)) {
  674. failureInfo.BackTrace = backtraceJson->GetString();
  675. }
  676. }
  677. }
  678. }
  679. }
  680. } // namespace NUnitTest