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