#include "junit.h"
#include <libxml/parser.h>
#include <libxml/xmlwriter.h>
#include <util/charset/utf8.h>
#include <util/generic/scope.h>
#include <util/generic/size_literals.h>
#include <util/stream/file.h>
#include <util/stream/input.h>
#include <util/system/backtrace.h>
#include <util/system/env.h>
#include <util/system/file.h>
#include <util/system/fs.h>
#include <util/system/file.h>
#include <util/system/fstat.h>
#include <util/system/tempfile.h>
#include <stdio.h>
#include <signal.h>
#if defined(_win_)
#include <io.h>
#endif
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) {
// https://en.wikipedia.org/wiki/Valid_characters_in_XML
return c == 0x9
|| c == 0xA
|| c == 0xD
|| c >= 0x20 && c <= 0xD7FF
|| c >= 0xE000 && c <= 0xFFFD
|| c >= 0x10000 && c <= 0x10FFFF;
}
static TString SanitizeXmlString(TString s) {
TString escaped;
bool fixedSomeChars = false;
const unsigned char* i = reinterpret_cast<const unsigned char*>(s.data());
const unsigned char* end = i + s.size();
auto replaceChar = [&]() {
if (!fixedSomeChars) {
fixedSomeChars = true;
escaped.reserve(s.size());
escaped.insert(escaped.end(), s.data(), reinterpret_cast<const char*>(i));
}
escaped.push_back('?');
};
while (i < end) {
wchar32 rune;
size_t runeLen;
const RECODE_RESULT result = SafeReadUTF8Char(rune, runeLen, i, end);
if (result == RECODE_OK) {
if (IsAllowedInXml(rune)) {
if (fixedSomeChars) {
escaped.insert(escaped.end(), reinterpret_cast<const char*>(i), reinterpret_cast<const char*>(i + runeLen));
}
} else {
replaceChar();
}
i += runeLen;
} else {
replaceChar();
++i;
}
}
if (fixedSomeChars) {
return escaped;
} else {
return s;
}
}
struct TJUnitProcessor::TOutputCapturer {
static constexpr int STDOUT_FD = 1;
static constexpr int STDERR_FD = 2;
TOutputCapturer(int fd)
: FdToCapture(fd)
, TmpFile(MakeTempName())
{
{
#if defined(_win_)
TFileHandle f((FHANDLE)_get_osfhandle(FdToCapture));
#else
TFileHandle f(FdToCapture);
#endif
TFileHandle other(f.Duplicate());
Original.Swap(other);
f.Release();
}
TFileHandle captured(TmpFile.Name(), EOpenModeFlag::OpenAlways | EOpenModeFlag::RdWr);
fflush(nullptr);
captured.Duplicate2Posix(FdToCapture);
}
~TOutputCapturer() {
Uncapture();
}
void Uncapture() {
if (Original.IsOpen()) {
fflush(nullptr);
Original.Duplicate2Posix(FdToCapture);
Original.Close();
}
}
TString GetTmpFileName() {
Uncapture();
return TmpFile.Name();
}
TString GetCapturedString() {
Uncapture();
TFile captured(TmpFile.Name(), EOpenModeFlag::RdOnly);
i64 len = captured.GetLength();
if (len > 0) {
try {
constexpr size_t LIMIT = 10_KB;
constexpr size_t PART_LIMIT = 5_KB;
TStringBuilder out;
if (static_cast<size_t>(len) <= LIMIT) {
out.resize(len);
captured.Read((void*)out.data(), len);
} else {
// Read first 5_KB
{
TString first;
first.resize(PART_LIMIT);
captured.Read((void*)first.data(), PART_LIMIT);
size_t lastNewLine = first.find_last_of('\n');
if (lastNewLine == TString::npos) {
out << first << Endl;
} else {
out << TStringBuf(first.c_str(), lastNewLine);
}
}
out << Endl << Endl << "...SKIPPED..." << Endl << Endl;
// Read last 5_KB
{
TString last;
last.resize(PART_LIMIT);
captured.Seek(-PART_LIMIT, sEnd);
captured.Read((void*)last.data(), PART_LIMIT);
size_t newLine = last.find_first_of('\n');
if (newLine == TString::npos) {
out << last << Endl;
} else {
out << TStringBuf(last.c_str() + newLine + 1);
}
}
}
if (out.back() != '\n') {
out << Endl;
}
return std::move(out);
} catch (const std::exception& ex) {
Cerr << "Failed to read from captured output: " << ex.what() << Endl;
}
}
return {};
}
const int FdToCapture;
TFileHandle Original;
TTempFile TmpFile;
};
TJUnitProcessor::TJUnitProcessor(TString file, TString exec)
: FileName(file)
, ExecName(exec)
{
}
TJUnitProcessor::~TJUnitProcessor() {
Save();
}
void TJUnitProcessor::OnBeforeTest(const TTest* test) {
CurrentTest.emplace(test);
CaptureSignal(this);
if (!GetForkTests() || GetIsForked()) {
StdErrCapturer = MakeHolder<TOutputCapturer>(TOutputCapturer::STDERR_FD);
StdOutCapturer = MakeHolder<TOutputCapturer>(TOutputCapturer::STDOUT_FD);
StartCurrentTestTime = TInstant::Now();
}
}
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);
}
}
void TJUnitProcessor::TransferFromCapturer(THolder<TJUnitProcessor::TOutputCapturer>& capturer, TString& out, IOutputStream& outStream) {
if (capturer) {
capturer->Uncapture();
{
TFileInput fileStream(capturer->GetTmpFileName());
TransferData(&fileStream, &outStream);
out = SanitizeXmlString(capturer->GetCapturedString());
}
capturer = nullptr;
}
}
void TJUnitProcessor::OnFinish(const TFinish* descr) {
if (!GetForkTests() || GetIsForked()) {
auto* testCase = GetTestCase(descr->test);
testCase->Success = descr->Success;
if (StartCurrentTestTime != TInstant::Zero()) {
testCase->DurationSecods = (TInstant::Now() - StartCurrentTestTime).SecondsFloat();
}
StartCurrentTestTime = TInstant::Zero();
TransferFromCapturer(StdOutCapturer, testCase->StdOut, Cout);
TransferFromCapturer(StdErrCapturer, testCase->StdErr, Cerr);
} else {
MergeSubprocessReport();
}
UncaptureSignal();
}
TString TJUnitProcessor::BuildFileName(size_t index, const TStringBuf extension) const {
TStringBuilder result;
result << FileName << ExecName;
if (index > 0) {
result << "-"sv << index;
}
result << extension;
return std::move(result);
}
void TJUnitProcessor::MakeReportFileName() {
constexpr size_t MaxReps = 200;
#if defined(_win_)
constexpr char DirSeparator = '\\';
#else
constexpr char DirSeparator = '/';
#endif
if (!ResultReportFileName.empty()) {
return;
}
if (GetIsForked() || !FileName.empty() && FileName.back() != DirSeparator) {
ResultReportFileName = FileName;
} else { // Directory is specified, => make unique report name
if (!FileName.empty()) {
NFs::MakeDirectoryRecursive(FileName);
}
for (size_t i = 0; i < MaxReps; ++i) {
TString uniqReportFileName = BuildFileName(i, ".xml"sv);
try {
TFile newUniqReportFile(uniqReportFileName, EOpenModeFlag::CreateNew);
newUniqReportFile.Close();
ResultReportFileName = std::move(uniqReportFileName);
break;
} catch (const TFileError&) {
// File already exists => try next name
}
}
}
if (ResultReportFileName.empty()) {
Cerr << "Could not find a vacant file name to write report for path " << FileName << ", maximum number of reports: " << MaxReps << Endl;
Y_FAIL("Cannot write report");
}
}
void TJUnitProcessor::Save() {
MakeReportFileName();
SerializeToFile();
}
void TJUnitProcessor::SetForkTestsParams(bool forkTests, bool isForked) {
ITestSuiteProcessor::SetForkTestsParams(forkTests, isForked);
MakeTmpFileNameForForkedTests();
}
void TJUnitProcessor::MakeTmpFileNameForForkedTests() {
if (GetForkTests() && !GetIsForked()) {
TmpReportFile.ConstructInPlace(MakeTempName());
// Replace option for child processes
SetEnv(Y_UNITTEST_OUTPUT_CMDLINE_OPTION, TStringBuilder() << "xml:" << TmpReportFile->Name());
}
}
static TJUnitProcessor* CurrentJUnitProcessor = nullptr;
void TJUnitProcessor::CaptureSignal(TJUnitProcessor* processor) {
CurrentJUnitProcessor = processor;
processor->PrevAbortHandler = signal(SIGABRT, &TJUnitProcessor::SignalHandler);
if (processor->PrevAbortHandler == SIG_ERR) {
processor->PrevAbortHandler = nullptr;
}
processor->PrevSegvHandler = signal(SIGSEGV, &TJUnitProcessor::SignalHandler);
if (processor->PrevSegvHandler == SIG_ERR) {
processor->PrevSegvHandler = nullptr;
}
}
void TJUnitProcessor::UncaptureSignal() {
if (CurrentJUnitProcessor) {
if (CurrentJUnitProcessor->PrevAbortHandler != nullptr) {
signal(SIGABRT, CurrentJUnitProcessor->PrevAbortHandler);
} else {
signal(SIGABRT, SIG_DFL);
}
if (CurrentJUnitProcessor->PrevSegvHandler != nullptr) {
signal(SIGSEGV, CurrentJUnitProcessor->PrevSegvHandler);
} else {
signal(SIGSEGV, SIG_DFL);
}
}
}
void TJUnitProcessor::SignalHandler(int signal) {
if (CurrentJUnitProcessor) {
if (CurrentJUnitProcessor->CurrentTest) {
TError errDesc;
errDesc.test = *CurrentJUnitProcessor->CurrentTest;
if (signal == SIGABRT) {
errDesc.msg = "Test aborted";
} else {
errDesc.msg = "Segmentation fault";
PrintBackTrace();
}
CurrentJUnitProcessor->OnError(&errDesc);
TFinish finishDesc;
finishDesc.Success = false;
finishDesc.test = *CurrentJUnitProcessor->CurrentTest;
CurrentJUnitProcessor->OnFinish(&finishDesc);
}
CurrentJUnitProcessor->Save();
if (signal == SIGABRT) {
if (CurrentJUnitProcessor->PrevAbortHandler) {
CurrentJUnitProcessor->PrevAbortHandler(signal);
}
} else {
if (CurrentJUnitProcessor->PrevSegvHandler) {
CurrentJUnitProcessor->PrevSegvHandler(signal);
}
}
}
}
#define CHECK_CALL(expr) if (int resultCode = (expr); resultCode < 0) { \
Cerr << "Faield to write to xml. Result code: " << resultCode << Endl; \
return; \
}
#define XML_STR(s) ((const xmlChar*)(s))
void TJUnitProcessor::SerializeToFile() {
auto file = xmlNewTextWriterFilename(ResultReportFileName.c_str(), 0);
if (!file) {
Cerr << "Failed to open xml file for writing: " << ResultReportFileName << Endl;
return;
}
Y_DEFER {
xmlFreeTextWriter(file);
};
CHECK_CALL(xmlTextWriterSetIndent(file, 1));
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())));
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())));
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())));
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));
}
if (!test.StdOut.empty()) {
CHECK_CALL(xmlTextWriterWriteElement(file, XML_STR("system-out"), XML_STR(test.StdOut.c_str())));
}
if (!test.StdErr.empty()) {
CHECK_CALL(xmlTextWriterWriteElement(file, XML_STR("system-err"), XML_STR(test.StdErr.c_str())));
}
CHECK_CALL(xmlTextWriterEndElement(file));
}
CHECK_CALL(xmlTextWriterEndElement(file));
}
CHECK_CALL(xmlTextWriterEndElement(file));
CHECK_CALL(xmlTextWriterEndDocument(file));
}
#define C_STR(s) ((const char*)(s))
#define STRBUF(s) TStringBuf(C_STR(s))
#define NODE_NAME(node) STRBUF((node)->name)
#define SAFE_CONTENT(node) (node && node->children ? STRBUF(node->children->content) : TStringBuf())
#define CHECK_NODE_NAME(node, expectedName) if (NODE_NAME(node) != (expectedName)) { \
ythrow yexception() << "Expected node name: \"" << (expectedName) \
<< "\", but got \"" << TStringBuf(C_STR((node)->name)) << "\""; \
}
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));
}
}
if (required) {
ythrow yexception() << "Attribute \"" << name << "\" was not found";
}
return {};
}
static xmlNodePtr NextElement(xmlNodePtr node) {
if (!node) {
return nullptr;
}
do {
node = node->next;
} while (node && node->type != XML_ELEMENT_NODE);
return node;
}
static xmlNodePtr ChildElement(xmlNodePtr node) {
xmlNodePtr child = node->children;
if (child && child->type != XML_ELEMENT_NODE) {
return NextElement(child);
}
return child;
}
void TJUnitProcessor::MergeSubprocessReport() {
{
const i64 len = GetFileLength(TmpReportFile->Name());
if (len < 0) {
Cerr << "Failed to get length of the output file for subprocess" << Endl;
return;
}
if (len == 0) {
return; // Empty file
}
}
Y_DEFER {
TFile file(TmpReportFile->Name(), EOpenModeFlag::TruncExisting);
file.Close();
};
xmlDocPtr doc = xmlParseFile(TmpReportFile->Name().c_str());
if (!doc) {
Cerr << "Failed to parse xml output for subprocess" << Endl;
return;
}
Y_DEFER {
xmlFreeDoc(doc);
};
xmlNodePtr root = xmlDocGetRootElement(doc);
if (!root) {
Cerr << "Failed to parse xml output for subprocess: empty document" << 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];
// 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];
if (TString duration = GetAttrValue(testCase, "time")) {
TryFromString(duration, testCaseInfo.DurationSecods);
}
// 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;
}
}
} catch (const std::exception& ex) {
Cerr << "Failed to load test suite info from xml: " << ex.what() << Endl;
continue;
}
}
}
} // namespace NUnitTest