Skip to content

Commit 175f16e

Browse files
committed
fix(tests): render assertion failures as diagnostics
1 parent 25a9dfb commit 175f16e

1 file changed

Lines changed: 182 additions & 3 deletions

File tree

src/commands/TestsCommand.cpp

Lines changed: 182 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
#include <optional>
3434
#include <cstdlib>
3535
#include <algorithm>
36+
#include <regex>
3637
#include <cctype>
3738

3839
#include <nlohmann/json.hpp>
@@ -908,6 +909,23 @@ namespace
908909
{
909910
std::string name;
910911
std::string message;
912+
913+
fs::path file;
914+
std::size_t line{0};
915+
std::size_t column{1};
916+
917+
std::string function;
918+
std::string assertion;
919+
920+
bool has_location() const
921+
{
922+
return !file.empty() && line > 0;
923+
}
924+
925+
bool has_assertion() const
926+
{
927+
return !assertion.empty();
928+
}
911929
};
912930

913931
static std::string trim_copy(std::string value)
@@ -1198,6 +1216,156 @@ namespace
11981216
return rest;
11991217
}
12001218

1219+
static std::vector<std::string> read_test_code_frame_lines(
1220+
const fs::path &file,
1221+
std::size_t line,
1222+
std::size_t contextLines,
1223+
std::size_t maxLineWidth)
1224+
{
1225+
std::vector<std::string> out;
1226+
1227+
if (file.empty() || line == 0)
1228+
return out;
1229+
1230+
std::ifstream in(file);
1231+
if (!in)
1232+
return out;
1233+
1234+
const std::size_t startLine =
1235+
line > contextLines ? line - contextLines : 1;
1236+
1237+
const std::size_t endLine = line + contextLines;
1238+
1239+
std::string current;
1240+
std::size_t currentLine = 0;
1241+
1242+
while (std::getline(in, current))
1243+
{
1244+
++currentLine;
1245+
1246+
if (currentLine < startLine)
1247+
continue;
1248+
1249+
if (currentLine > endLine)
1250+
break;
1251+
1252+
if (maxLineWidth > 0 && current.size() > maxLineWidth)
1253+
{
1254+
current = current.substr(0, maxLineWidth);
1255+
current += "...";
1256+
}
1257+
1258+
out.push_back(current);
1259+
}
1260+
1261+
return out;
1262+
}
1263+
1264+
static void enrich_failure_from_assertion_line(
1265+
ParsedTestFailure &failure,
1266+
const std::string &line)
1267+
{
1268+
static const std::regex assertionRe(
1269+
R"((/[^:\n]+?\.(?:c|cc|cpp|cxx|h|hpp|hh|hxx)):(\d+):\s*(.*?):\s*Assertion\s+[`']([^`']+)[`']\s+failed\.?)",
1270+
std::regex::ECMAScript);
1271+
1272+
std::smatch match;
1273+
1274+
if (!std::regex_search(line, match, assertionRe))
1275+
return;
1276+
1277+
failure.file = fs::path(match[1].str());
1278+
1279+
try
1280+
{
1281+
failure.line = static_cast<std::size_t>(std::stoul(match[2].str()));
1282+
}
1283+
catch (...)
1284+
{
1285+
failure.line = 0;
1286+
}
1287+
1288+
failure.column = 1;
1289+
failure.function = trim_copy(match[3].str());
1290+
failure.assertion = trim_copy(match[4].str());
1291+
}
1292+
1293+
static void enrich_failure_from_message(ParsedTestFailure &failure)
1294+
{
1295+
if (failure.message.empty())
1296+
return;
1297+
1298+
std::istringstream lines(failure.message);
1299+
std::string line;
1300+
1301+
while (std::getline(lines, line))
1302+
{
1303+
enrich_failure_from_assertion_line(failure, line);
1304+
1305+
if (failure.has_location())
1306+
return;
1307+
}
1308+
}
1309+
1310+
static build::BuildDiagnostic make_test_failure_diagnostic(
1311+
const ParsedTestFailure &failure)
1312+
{
1313+
build::BuildDiagnostic diagnostic;
1314+
1315+
diagnostic.title = "Test failed";
1316+
diagnostic.message =
1317+
failure.name.empty()
1318+
? "A test failed"
1319+
: failure.name;
1320+
1321+
if (failure.has_assertion())
1322+
{
1323+
diagnostic.error =
1324+
"assertion failed: " + failure.assertion;
1325+
}
1326+
else if (!failure.message.empty())
1327+
{
1328+
diagnostic.error = failure.message;
1329+
}
1330+
else
1331+
{
1332+
diagnostic.error = "test process failed";
1333+
}
1334+
1335+
if (!failure.function.empty())
1336+
diagnostic.hint = "check the failing test function: " + failure.function;
1337+
else
1338+
diagnostic.hint = "run `vix tests --raw` to inspect the full runner output";
1339+
1340+
if (failure.has_location())
1341+
{
1342+
diagnostic.location.file = failure.file;
1343+
diagnostic.location.line = failure.line;
1344+
diagnostic.location.column = failure.column;
1345+
1346+
diagnostic.codeFrame.location = diagnostic.location;
1347+
diagnostic.codeFrame.lines =
1348+
read_test_code_frame_lines(
1349+
failure.file,
1350+
failure.line,
1351+
2,
1352+
120);
1353+
}
1354+
1355+
return diagnostic;
1356+
}
1357+
1358+
static void print_test_failure_diagnostic(
1359+
const ParsedTestFailure &failure)
1360+
{
1361+
const build::BuildDiagnostic diagnostic =
1362+
make_test_failure_diagnostic(failure);
1363+
1364+
build::print_build_diagnostic(
1365+
std::cout,
1366+
diagnostic);
1367+
}
1368+
12011369
static std::vector<ParsedTestFailure> parse_test_failures(
12021370
const std::string &output)
12031371
{
@@ -1215,6 +1383,7 @@ namespace
12151383
return;
12161384

12171385
current.message = trim_copy(current.message);
1386+
enrich_failure_from_message(current);
12181387

12191388
failures.push_back(current);
12201389
current = ParsedTestFailure{};
@@ -1303,11 +1472,21 @@ namespace
13031472

13041473
const auto &first = failures.front();
13051474

1306-
if (!first.message.empty())
1475+
std::cout << "\n";
1476+
1477+
if (first.has_location())
1478+
{
1479+
print_test_failure_diagnostic(first);
1480+
}
1481+
else if (!first.message.empty())
13071482
{
1308-
std::cout << "\n";
13091483
std::cout << " " << CYAN << "error:" << RESET << "\n";
1310-
std::cout << " " << first.message << "\n";
1484+
1485+
std::istringstream lines(first.message);
1486+
std::string line;
1487+
1488+
while (std::getline(lines, line))
1489+
std::cout << " " << line << "\n";
13111490
}
13121491
}
13131492
else

0 commit comments

Comments
 (0)