Skip to content

Commit 4978445

Browse files
authored
#332 Use UTBotCpp as static analyzer and report results in SARIF (#333)
#332 Use UTBotCpp as static analyzer and report results in SARIF - fix test and file creation logic - formatting changes - added "extensionPack" in VSCode plugin - introduce advertising for MS Sarif plugin if not installed - formatting and documentation - fixing progress
1 parent 6048615 commit 4978445

24 files changed

+860
-96
lines changed

server/src/KleeGenerator.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,8 +346,8 @@ void KleeGenerator::parseKTestsToFinalCode(
346346
}
347347
auto predicate =
348348
lineInfo ? lineInfo->predicateInfo : std::optional<LineInfo::PredicateInfo>{};
349-
testsPrinter.genCode(methodDescription, predicate, verbose);
350349

350+
testsPrinter.genCode(methodDescription, predicate, verbose);
351351
}
352352

353353
printer::HeaderPrinter(Paths::getSourceLanguage(tests.sourceFilePath)).print(tests.testHeaderFilePath, tests.sourceFilePath,

server/src/KleeRunner.cpp

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#include "Paths.h"
44
#include "TimeExecStatistics.h"
5+
#include "SARIFGenerator.h"
56
#include "exceptions/FileNotPresentedInArtifactException.h"
67
#include "exceptions/FileNotPresentedInCommandsException.h"
78
#include "tasks/RunKleeTask.h"
@@ -44,7 +45,9 @@ void KleeRunner::runKlee(const std::vector<tests::TestMethod> &testMethods,
4445
fileToMethods[method.sourceFilePath].push_back(method);
4546
}
4647

47-
std::function<void(tests::Tests &tests)> writeFunctor = [&](tests::Tests &tests) {
48+
nlohmann::json sarifResults;
49+
50+
std::function<void(tests::Tests &tests)> prepareTests = [&](tests::Tests &tests) {
4851
fs::path filePath = tests.sourceFilePath;
4952
const auto &batch = fileToMethods[filePath];
5053
if (!tests.isFilePresentedInCommands) {
@@ -87,10 +90,22 @@ void KleeRunner::runKlee(const std::vector<tests::TestMethod> &testMethods,
8790
}
8891
generator->parseKTestsToFinalCode(tests, methodNameToReturnTypeMap, ktests, lineInfo,
8992
settingsContext.verbose);
93+
94+
sarif::sarifAddTestsToResults(projectContext, tests, sarifResults);
9095
};
9196

92-
testsWriter->writeTestsWithProgress(testsMap, "Running klee", projectContext.testDirPath,
93-
std::move(writeFunctor));
97+
std::function<void()> prepareTotal = [&]() {
98+
testsWriter->writeReport(sarif::sarifPackResults(sarifResults),
99+
"Sarif Report was created",
100+
projectContext.projectPath / sarif::SARIF_DIR_NAME / sarif::SARIF_FILE_NAME);
101+
};
102+
103+
testsWriter->writeTestsWithProgress(
104+
testsMap,
105+
"Running klee",
106+
projectContext.testDirPath,
107+
std::move(prepareTests),
108+
std::move(prepareTotal));
94109
}
95110

96111
namespace {
@@ -136,8 +151,13 @@ static void processMethod(MethodKtests &ktestChunk,
136151
LOG_S(WARNING) << "Unable to open .ktestjson file";
137152
continue;
138153
}
139-
UTBotKTest::Status status = Paths::hasError(path) ? UTBotKTest::Status::FAILED
140-
: UTBotKTest::Status::SUCCESS;
154+
155+
const std::vector<fs::path> &errorDescriptorFiles =
156+
Paths::getErrorDescriptors(path);
157+
158+
UTBotKTest::Status status = errorDescriptorFiles.empty()
159+
? UTBotKTest::Status::SUCCESS
160+
: UTBotKTest::Status::FAILED;
141161
std::vector<ConcretizedObject> kTestObjects(
142162
ktestData->objects, ktestData->objects + ktestData->n_objects);
143163

@@ -146,7 +166,22 @@ static void processMethod(MethodKtests &ktestChunk,
146166
return UTBotKTestObject{ kTestObject };
147167
});
148168

149-
ktestChunk[method].emplace_back(objects, status);
169+
std::vector<std::string> errorDescriptors = CollectionUtils::transform(
170+
errorDescriptorFiles, [](const fs::path &errorFile) {
171+
std::ifstream fileWithError(errorFile.c_str(), std::ios_base::in);
172+
std::string content((std::istreambuf_iterator<char>(fileWithError)),
173+
std::istreambuf_iterator<char>());
174+
175+
const std::string &errorId = errorFile.stem().extension().string();
176+
if (!errorId.empty()) {
177+
// skip leading dot
178+
content += "\n" + sarif::ERROR_ID_KEY + ":" + errorId.substr(1);
179+
}
180+
return content;
181+
});
182+
183+
184+
ktestChunk[method].emplace_back(objects, status, errorDescriptors);
150185
}
151186
}
152187
}

server/src/Paths.cpp

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
#include "Paths.h"
22

33
#include "ProjectContext.h"
4-
#include "utils/StringUtils.h"
54
#include "utils/CLIUtils.h"
5+
#include "utils/StringUtils.h"
66

77
#include "loguru.h"
88

@@ -117,10 +117,12 @@ namespace Paths {
117117

118118
//region klee
119119

120+
static fs::path errorFile(const fs::path &path, std::string const& suffix) {
121+
return replaceExtension(path, StringUtils::stringFormat(".%s.err", suffix));
122+
}
123+
120124
static bool errorFileExists(const fs::path &path, std::string const& suffix) {
121-
fs::path file = replaceExtension(
122-
path, StringUtils::stringFormat(".%s.err", suffix));
123-
return fs::exists(file);
125+
return fs::exists(errorFile(path, suffix));
124126
}
125127

126128
bool hasInternalError(const fs::path &path) {
@@ -133,7 +135,7 @@ namespace Paths {
133135
[&path](auto const &suffix) { return errorFileExists(path, suffix); });
134136
}
135137

136-
bool hasError(const fs::path &path) {
138+
std::vector<fs::path> getErrorDescriptors(const fs::path &path) {
137139
static const auto internalErrorSuffixes = {
138140
"abort",
139141
"assert",
@@ -149,8 +151,14 @@ namespace Paths {
149151
"uncaught_exception",
150152
"unexpected_exception"
151153
};
152-
return std::any_of(internalErrorSuffixes.begin(), internalErrorSuffixes.end(),
153-
[&path](auto const &suffix) { return errorFileExists(path, suffix); });
154+
155+
std::vector<fs::path> errFiles;
156+
for (const auto &suffix : internalErrorSuffixes) {
157+
if (errorFileExists(path, suffix)) {
158+
errFiles.emplace_back(errorFile(path, suffix));
159+
}
160+
}
161+
return errFiles;
154162
}
155163

156164
fs::path kleeOutDirForEntrypoints(const utbot::ProjectContext &projectContext, const fs::path &projectTmpPath,

server/src/Paths.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
#include "utils/path/FileSystemPath.h"
1212
#include <optional>
13+
#include <vector>
1314
#include <unordered_set>
1415

1516
namespace Paths {
@@ -236,7 +237,7 @@ namespace Paths {
236237

237238
bool hasInternalError(fs::path const &path);
238239

239-
bool hasError(fs::path const &path);
240+
std::vector<fs::path> getErrorDescriptors(fs::path const &path);
240241

241242
fs::path kleeOutDirForEntrypoints(const utbot::ProjectContext &projectContext, const fs::path &projectTmpPath,
242243
const fs::path &srcFilePath, const std::string &methodName = "");

server/src/SARIFGenerator.cpp

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
#include "SARIFGenerator.h"
2+
#include "Paths.h"
3+
4+
#include "loguru.h"
5+
6+
#include <fstream>
7+
#include <regex>
8+
#include <unordered_map>
9+
10+
using namespace tests;
11+
12+
namespace sarif {
13+
// Here is a temporary solution that restores the correct project-relative path from
14+
// the abstract relative path, provided by KLEE in stack trace inside a `XXXX.err` file.
15+
// There is no clear reason why KLEE is using the wrong base for relative path.
16+
// The correct way to extract the full path for a stack file is in checking entries like
17+
// !820 = !DIFile(filename: "test/suites/cli/complex_structs.c", directory: "/home/utbot/tmp/UTBotCpp/server")
18+
// in upper laying file `assembly.ll`; then we may call the `fs::relative(src, path)`.
19+
// For example the function call:
20+
// getInProjectPath("/home/utbot/tmp/UTBotCpp/server/test/suites/object-file",
21+
// "test/suites/object-file/op/source2.c")
22+
// returns
23+
// "op/source2.c"
24+
fs::path getInProjectPath(const fs::path &path, const fs::path &src) {
25+
fs::path relToProject;
26+
auto p = path.begin();
27+
auto s = src.begin();
28+
bool foundStartFragment = false;
29+
while (p != path.end() && s != src.end()) {
30+
if (*p == *s) {
31+
foundStartFragment = true;
32+
++s;
33+
} else if (foundStartFragment) {
34+
break;
35+
}
36+
++p;
37+
}
38+
if (foundStartFragment && p == path.end()) {
39+
while (s != src.end()) {
40+
relToProject = relToProject / *s;
41+
++s;
42+
}
43+
}
44+
return relToProject;
45+
}
46+
47+
void sarifAddTestsToResults(const utbot::ProjectContext &projectContext,
48+
const Tests &tests,
49+
json &results) {
50+
LOG_S(INFO) << "{stack";
51+
for (const auto &it : tests.methods) {
52+
for (const auto &methodTestCase : it.second.testCases) {
53+
json result;
54+
const std::vector<std::string> &descriptors = methodTestCase.errorDescriptors;
55+
std::string key;
56+
std::string value;
57+
json stackLocations;
58+
json codeFlowsLocations;
59+
json testLocation;
60+
bool canAddThisTestToSARIF = false;
61+
for (const std::string &descriptor : descriptors) {
62+
std::stringstream streamOfDescriptor(descriptor);
63+
std::string lineInDescriptor;
64+
bool firstCallInStack = false;
65+
while (getline(streamOfDescriptor, lineInDescriptor)) {
66+
if (lineInDescriptor.empty() || lineInDescriptor[0] == '#')
67+
continue;
68+
if (isspace(lineInDescriptor[0])) {
69+
if (key == "Stack") {
70+
const std::regex stack_regex(
71+
R"regex(\s+#(.*) in ([^ ]*) [(][^)]*[)] at ([^:]*):(\d+))regex");
72+
std::smatch stack_match;
73+
if (!std::regex_match(lineInDescriptor, stack_match, stack_regex)) {
74+
LOG_S(ERROR) << "wrong `Stack` line: " << lineInDescriptor;
75+
} else {
76+
const fs::path &srcPath = fs::path(stack_match[3]);
77+
const fs::path &relPathInProject = getInProjectPath(projectContext.projectPath, srcPath);
78+
const fs::path &fullPathInProject = projectContext.projectPath / relPathInProject;
79+
if (Paths::isSubPathOf(projectContext.buildDir, fullPathInProject)) {
80+
continue;
81+
}
82+
if (!relPathInProject.empty() && fs::exists(fullPathInProject)) {
83+
// stackLocations from project source
84+
json locationWrapper;
85+
locationWrapper["module"] = "project";
86+
{
87+
json location;
88+
location["physicalLocation"]["artifactLocation"]["uri"] = relPathInProject;
89+
location["physicalLocation"]["artifactLocation"]["uriBaseId"] = "%SRCROOT%";
90+
location["physicalLocation"]["region"]["startLine"] = std::stoi(stack_match[4]); // line number
91+
// commented, duplicated in message
92+
// location["logicalLocations"][0]["fullyQualifiedName"] = stack_match[2]; // call name
93+
location["message"]["text"] = stack_match[2].str() + std::string(" (source)"); // info for ANALYSIS STEP
94+
if (firstCallInStack) {
95+
firstCallInStack = false;
96+
result["locations"].push_back(location);
97+
stackLocations["message"]["text"] = "UTBot generated";
98+
codeFlowsLocations["message"]["text"] = "UTBot generated";
99+
}
100+
locationWrapper["location"] = location;
101+
}
102+
stackLocations["frames"].push_back(locationWrapper);
103+
codeFlowsLocations["locations"].push_back(locationWrapper);
104+
} else if (firstCallInStack) {
105+
// stackLocations from runtime, that is called by tested function
106+
json locationWrapper;
107+
locationWrapper["module"] = "external";
108+
{
109+
json location;
110+
location["physicalLocation"]["artifactLocation"] ["uri"] = srcPath.filename(); // just a name
111+
location["physicalLocation"]["artifactLocation"] ["uriBaseId"] = "%PATH%";
112+
location["physicalLocation"]["region"]["startLine"] = std::stoi(stack_match[4]); // line number
113+
// commented, duplicated in message
114+
// location["logicalLocations"][0]["fullyQualifiedName"] = stack_match[2]; // call name
115+
location["message"]["text"] = stack_match[2].str() + std::string(" (external)"); // info for ANALYSIS STEP
116+
locationWrapper["location"] = location;
117+
}
118+
stackLocations["frames"].push_back(locationWrapper);
119+
codeFlowsLocations["locations"].push_back(locationWrapper);
120+
} else {
121+
// the rest is the KLEE calls that are not applicable for navigation
122+
LOG_S(INFO) << "Skip path in stack frame :" << srcPath;
123+
}
124+
}
125+
}
126+
} else {
127+
size_t pos = lineInDescriptor.find(':');
128+
if (pos == std::string::npos) {
129+
LOG_S(ERROR) << "no key:" << lineInDescriptor;
130+
} else {
131+
if (key == "Stack") {
132+
// Check stack validity
133+
if (firstCallInStack) {
134+
LOG_S(ERROR) << "no visible stack in descriptor:" << descriptor;
135+
} else {
136+
canAddThisTestToSARIF = true;
137+
}
138+
}
139+
firstCallInStack = true;
140+
141+
key = lineInDescriptor.substr(0, pos);
142+
value = lineInDescriptor.substr(pos + 1);
143+
if (key == "Error") {
144+
result["message"]["text"] = value;
145+
result["level"] = "error";
146+
result["kind"] = "fail";
147+
} else if (key == ERROR_ID_KEY) {
148+
result["ruleId"] = value;
149+
} else if (key == "Stack") {
150+
stackLocations = json();
151+
codeFlowsLocations = json();
152+
} else if (key == TEST_FILE_KEY) {
153+
testLocation = json();
154+
testLocation["physicalLocation"]["artifactLocation"]["uri"] = fs::relative(value, projectContext.projectPath);
155+
testLocation["physicalLocation"]["artifactLocation"]["uriBaseId"] = "%SRCROOT%";
156+
} else if (key == TEST_LINE_KEY) {
157+
testLocation["physicalLocation"]["region"]["startLine"] = std::stoi(value); // line number
158+
} else if (key == TEST_NAME_KEY) {
159+
// commented, duplicated in message
160+
// testLocation["logicalLocations"][0]["fullyQualifiedName"] = value; // call name
161+
testLocation["message"]["text"] = value + std::string(" (test)"); // info for ANALYSIS STEP
162+
{
163+
json locationWrapper;
164+
locationWrapper["location"] = testLocation;
165+
locationWrapper["module"] = "test";
166+
167+
stackLocations["frames"].push_back(locationWrapper);
168+
codeFlowsLocations["locations"].push_back(locationWrapper);
169+
}
170+
}
171+
}
172+
}
173+
}
174+
}
175+
176+
if (canAddThisTestToSARIF) {
177+
result["stacks"].push_back(stackLocations);
178+
result["codeFlows"][0]["threadFlows"].push_back(codeFlowsLocations);
179+
results.push_back(result);
180+
}
181+
}
182+
}
183+
LOG_S(INFO) << "}stack";
184+
}
185+
186+
std::string sarifPackResults(const json &results) {
187+
// POINT 3
188+
json sarifJson;
189+
sarifJson["$schema"] = "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json";
190+
sarifJson["version"] = "2.1.0";
191+
{
192+
json runs;
193+
{
194+
json runAkaTestCase;
195+
runAkaTestCase["tool"]["driver"]["name"] = "UTBotCpp";
196+
runAkaTestCase["tool"]["driver"]["informationUri"] = "https://utbot.org";
197+
runAkaTestCase["results"] = results;
198+
runs.push_back(runAkaTestCase);
199+
}
200+
sarifJson["runs"] = runs;
201+
}
202+
return sarifJson.dump(2);
203+
}
204+
}

server/src/SARIFGenerator.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#include "Tests.h"
2+
#include "ProjectContext.h"
3+
4+
namespace sarif {
5+
const std::string PREFIX_FOR_JSON_PATH = "// UTBOT_TEST_GENERATOR (function name,test index): ";
6+
7+
const std::string TEST_FILE_KEY = "TestFile";
8+
const std::string TEST_LINE_KEY = "TestLine";
9+
const std::string TEST_NAME_KEY = "TestName";
10+
const std::string ERROR_ID_KEY = "ErrorId";
11+
12+
const std::string SARIF_DIR_NAME = "codeAnalysis";
13+
const std::string SARIF_FILE_NAME = "project_code_analysis.sarif";
14+
15+
void sarifAddTestsToResults(const utbot::ProjectContext &projectContext,
16+
const tests::Tests &tests,
17+
nlohmann::json &results);
18+
std::string sarifPackResults(const nlohmann::json &results);
19+
}

0 commit comments

Comments
 (0)