1 //===- unittests/Basic/SarifTest.cpp - Test writing SARIF documents -------===//
3 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4 // See https://llvm.org/LICENSE.txt for license information.
5 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
7 //===----------------------------------------------------------------------===//
9 #include "clang/Basic/Sarif.h"
10 #include "clang/Basic/DiagnosticOptions.h"
11 #include "clang/Basic/FileManager.h"
12 #include "clang/Basic/FileSystemOptions.h"
13 #include "clang/Basic/LangOptions.h"
14 #include "clang/Basic/SourceLocation.h"
15 #include "clang/Basic/SourceManager.h"
16 #include "llvm/ADT/StringRef.h"
17 #include "llvm/Support/FormatVariadic.h"
18 #include "llvm/Support/JSON.h"
19 #include "llvm/Support/MemoryBuffer.h"
20 #include "llvm/Support/VirtualFileSystem.h"
21 #include "llvm/Support/raw_ostream.h"
22 #include "gmock/gmock.h"
23 #include "gtest/gtest.h"
27 using namespace clang
;
31 using LineCol
= std::pair
<unsigned int, unsigned int>;
33 static std::string
serializeSarifDocument(llvm::json::Object
&&Doc
) {
35 llvm::json::Value
Value(std::move(Doc
));
36 llvm::raw_string_ostream OS
{Output
};
37 OS
<< llvm::formatv("{0}", Value
);
42 class SarifDocumentWriterTest
: public ::testing::Test
{
44 SarifDocumentWriterTest()
45 : InMemoryFileSystem(new llvm::vfs::InMemoryFileSystem
),
46 FileMgr(FileSystemOptions(), InMemoryFileSystem
),
47 DiagID(new DiagnosticIDs()), DiagOpts(new DiagnosticOptions()),
48 Diags(DiagID
, DiagOpts
.get(), new IgnoringDiagConsumer()),
49 SourceMgr(Diags
, FileMgr
) {}
51 IntrusiveRefCntPtr
<llvm::vfs::InMemoryFileSystem
> InMemoryFileSystem
;
53 IntrusiveRefCntPtr
<DiagnosticIDs
> DiagID
;
54 IntrusiveRefCntPtr
<DiagnosticOptions
> DiagOpts
;
55 DiagnosticsEngine Diags
;
56 SourceManager SourceMgr
;
59 FileID
registerSource(llvm::StringRef Name
, const char *SourceText
,
60 bool IsMainFile
= false) {
61 std::unique_ptr
<llvm::MemoryBuffer
> SourceBuf
=
62 llvm::MemoryBuffer::getMemBuffer(SourceText
);
63 FileEntryRef SourceFile
=
64 FileMgr
.getVirtualFileRef(Name
, SourceBuf
->getBufferSize(), 0);
65 SourceMgr
.overrideFileContents(SourceFile
, std::move(SourceBuf
));
66 FileID FID
= SourceMgr
.getOrCreateFileID(SourceFile
, SrcMgr::C_User
);
68 SourceMgr
.setMainFileID(FID
);
72 CharSourceRange
getFakeCharSourceRange(FileID FID
, LineCol Begin
,
74 auto BeginLoc
= SourceMgr
.translateLineCol(FID
, Begin
.first
, Begin
.second
);
75 auto EndLoc
= SourceMgr
.translateLineCol(FID
, End
.first
, End
.second
);
76 return CharSourceRange
{SourceRange
{BeginLoc
, EndLoc
}, /* ITR = */ false};
80 TEST_F(SarifDocumentWriterTest
, canCreateEmptyDocument
) {
82 SarifDocumentWriter Writer
{SourceMgr
};
85 const llvm::json::Object
&EmptyDoc
= Writer
.createDocument();
86 std::vector
<StringRef
> Keys(EmptyDoc
.size());
87 std::transform(EmptyDoc
.begin(), EmptyDoc
.end(), Keys
.begin(),
88 [](auto Item
) { return Item
.getFirst(); });
91 ASSERT_THAT(Keys
, testing::UnorderedElementsAre("$schema", "version"));
94 // Test that a newly inserted run will associate correct tool names
95 TEST_F(SarifDocumentWriterTest
, canCreateDocumentWithOneRun
) {
97 SarifDocumentWriter Writer
{SourceMgr
};
98 const char *ShortName
= "sariftest";
99 const char *LongName
= "sarif writer test";
102 Writer
.createRun(ShortName
, LongName
);
104 const llvm::json::Object
&Doc
= Writer
.createDocument();
105 const llvm::json::Array
*Runs
= Doc
.getArray("runs");
109 ASSERT_THAT(Runs
, testing::NotNull());
111 // It is the only run
112 ASSERT_EQ(Runs
->size(), 1UL);
114 // The tool associated with the run was the tool
115 const llvm::json::Object
*Driver
=
116 Runs
->begin()->getAsObject()->getObject("tool")->getObject("driver");
117 ASSERT_THAT(Driver
, testing::NotNull());
119 ASSERT_TRUE(Driver
->getString("name").has_value());
120 ASSERT_TRUE(Driver
->getString("fullName").has_value());
121 ASSERT_TRUE(Driver
->getString("language").has_value());
123 EXPECT_EQ(*Driver
->getString("name"), ShortName
);
124 EXPECT_EQ(*Driver
->getString("fullName"), LongName
);
125 EXPECT_EQ(*Driver
->getString("language"), "en-US");
128 TEST_F(SarifDocumentWriterTest
, addingResultsWillCrashIfThereIsNoRun
) {
129 #if defined(NDEBUG) || !GTEST_HAS_DEATH_TEST
130 GTEST_SKIP() << "This death test is only available for debug builds.";
133 SarifDocumentWriter Writer
{SourceMgr
};
136 // A SarifDocumentWriter::createRun(...) was not called prior to
137 // SarifDocumentWriter::appendResult(...)
139 auto RuleIdx
= Writer
.createRule(SarifRule::create());
140 const SarifResult
&EmptyResult
= SarifResult::create(RuleIdx
);
143 auto Matcher
= ::testing::AnyOf(
144 ::testing::HasSubstr("create a run first"),
145 ::testing::HasSubstr("no runs associated with the document"));
146 ASSERT_DEATH(Writer
.appendResult(EmptyResult
), Matcher
);
149 TEST_F(SarifDocumentWriterTest
, settingInvalidRankWillCrash
) {
150 #if defined(NDEBUG) || !GTEST_HAS_DEATH_TEST
151 GTEST_SKIP() << "This death test is only available for debug builds.";
154 SarifDocumentWriter Writer
{SourceMgr
};
157 // A SarifReportingConfiguration is created with an invalid "rank"
158 // * Ranks below 0.0 are invalid
159 // * Ranks above 100.0 are invalid
161 // THEN: The builder will crash in either case
162 EXPECT_DEATH(SarifReportingConfiguration::create().setRank(-1.0),
163 ::testing::HasSubstr("Rule rank cannot be smaller than 0.0"));
164 EXPECT_DEATH(SarifReportingConfiguration::create().setRank(101.0),
165 ::testing::HasSubstr("Rule rank cannot be larger than 100.0"));
168 TEST_F(SarifDocumentWriterTest
, creatingResultWithDisabledRuleWillCrash
) {
169 #if defined(NDEBUG) || !GTEST_HAS_DEATH_TEST
170 GTEST_SKIP() << "This death test is only available for debug builds.";
174 SarifDocumentWriter Writer
{SourceMgr
};
177 // A disabled Rule is created, and a result is create referencing this rule
178 const auto &Config
= SarifReportingConfiguration::create().disable();
180 Writer
.createRule(SarifRule::create().setDefaultConfiguration(Config
));
181 const SarifResult
&Result
= SarifResult::create(RuleIdx
);
184 // SarifResult::create(...) will produce a crash
186 Writer
.appendResult(Result
),
187 ::testing::HasSubstr("Cannot add a result referencing a disabled Rule"));
190 // Test adding rule and result shows up in the final document
191 TEST_F(SarifDocumentWriterTest
, addingResultWithValidRuleAndRunIsOk
) {
193 SarifDocumentWriter Writer
{SourceMgr
};
194 const SarifRule
&Rule
=
196 .setRuleId("clang.unittest")
197 .setDescription("Example rule created during unit tests")
198 .setName("clang unit test");
201 Writer
.createRun("sarif test", "sarif test runner");
202 unsigned RuleIdx
= Writer
.createRule(Rule
);
203 const SarifResult
&Result
= SarifResult::create(RuleIdx
);
205 Writer
.appendResult(Result
);
206 const llvm::json::Object
&Doc
= Writer
.createDocument();
209 // A document with a valid schema and version exists
210 ASSERT_THAT(Doc
.get("$schema"), ::testing::NotNull());
211 ASSERT_THAT(Doc
.get("version"), ::testing::NotNull());
212 const llvm::json::Array
*Runs
= Doc
.getArray("runs");
214 // A run exists on this document
215 ASSERT_THAT(Runs
, ::testing::NotNull());
216 ASSERT_EQ(Runs
->size(), 1UL);
217 const llvm::json::Object
*TheRun
= Runs
->back().getAsObject();
219 // The run has slots for tools, results, rules and artifacts
220 ASSERT_THAT(TheRun
->get("tool"), ::testing::NotNull());
221 ASSERT_THAT(TheRun
->get("results"), ::testing::NotNull());
222 ASSERT_THAT(TheRun
->get("artifacts"), ::testing::NotNull());
223 const llvm::json::Object
*Driver
=
224 TheRun
->getObject("tool")->getObject("driver");
225 const llvm::json::Array
*Results
= TheRun
->getArray("results");
226 const llvm::json::Array
*Artifacts
= TheRun
->getArray("artifacts");
228 // The tool is as expected
229 ASSERT_TRUE(Driver
->getString("name").has_value());
230 ASSERT_TRUE(Driver
->getString("fullName").has_value());
232 EXPECT_EQ(*Driver
->getString("name"), "sarif test");
233 EXPECT_EQ(*Driver
->getString("fullName"), "sarif test runner");
235 // The results are as expected
236 EXPECT_EQ(Results
->size(), 1UL);
238 // The artifacts are as expected
239 EXPECT_TRUE(Artifacts
->empty());
242 TEST_F(SarifDocumentWriterTest
, checkSerializingResultsWithDefaultRuleConfig
) {
244 const std::string ExpectedOutput
=
245 R
"({"$schema
":"https
://docs.oasis-open.org/sarif/sarif/v2.1.0/cos02/schemas/sarif-schema-2.1.0.json","runs":[{"artifacts":[],"columnKind":"unicodeCodePoints","results":[{"level":"warning","message":{"text":""},"ruleId":"clang.unittest","ruleIndex":0}],"tool":{"driver":{"fullName":"sarif test runner","informationUri":"https://clang.llvm.org/docs/UsersManual.html","language":"en-US","name":"sarif test","rules":[{"defaultConfiguration":{"enabled":true,"level":"warning","rank":-1},"fullDescription":{"text":"Example rule created during unit tests"},"id":"clang.unittest","name":"clang unit test"}],"version":"1.0.0"}}}],"version":"2.1.0"})";
247 SarifDocumentWriter Writer
{SourceMgr
};
248 const SarifRule
&Rule
=
250 .setRuleId("clang.unittest")
251 .setDescription("Example rule created during unit tests")
252 .setName("clang unit test");
254 // WHEN: A run contains a result
255 Writer
.createRun("sarif test", "sarif test runner", "1.0.0");
256 unsigned RuleIdx
= Writer
.createRule(Rule
);
257 const SarifResult
&Result
= SarifResult::create(RuleIdx
);
258 Writer
.appendResult(Result
);
259 std::string Output
= serializeSarifDocument(Writer
.createDocument());
262 ASSERT_THAT(Output
, ::testing::StrEq(ExpectedOutput
));
265 TEST_F(SarifDocumentWriterTest
, checkSerializingResultsWithCustomRuleConfig
) {
267 const std::string ExpectedOutput
=
268 R
"({"$schema
":"https
://docs.oasis-open.org/sarif/sarif/v2.1.0/cos02/schemas/sarif-schema-2.1.0.json","runs":[{"artifacts":[],"columnKind":"unicodeCodePoints","results":[{"level":"error","message":{"text":""},"ruleId":"clang.unittest","ruleIndex":0}],"tool":{"driver":{"fullName":"sarif test runner","informationUri":"https://clang.llvm.org/docs/UsersManual.html","language":"en-US","name":"sarif test","rules":[{"defaultConfiguration":{"enabled":true,"level":"error","rank":35.5},"fullDescription":{"text":"Example rule created during unit tests"},"id":"clang.unittest","name":"clang unit test"}],"version":"1.0.0"}}}],"version":"2.1.0"})";
270 SarifDocumentWriter Writer
{SourceMgr
};
271 const SarifRule
&Rule
=
273 .setRuleId("clang.unittest")
274 .setDescription("Example rule created during unit tests")
275 .setName("clang unit test")
276 .setDefaultConfiguration(SarifReportingConfiguration::create()
277 .setLevel(SarifResultLevel::Error
)
280 // WHEN: A run contains a result
281 Writer
.createRun("sarif test", "sarif test runner", "1.0.0");
282 unsigned RuleIdx
= Writer
.createRule(Rule
);
283 const SarifResult
&Result
= SarifResult::create(RuleIdx
);
284 Writer
.appendResult(Result
);
285 std::string Output
= serializeSarifDocument(Writer
.createDocument());
288 ASSERT_THAT(Output
, ::testing::StrEq(ExpectedOutput
));
291 // Check that serializing artifacts from results produces valid SARIF
292 TEST_F(SarifDocumentWriterTest
, checkSerializingArtifacts
) {
294 const std::string ExpectedOutput
=
295 R
"({"$schema
":"https
://docs.oasis-open.org/sarif/sarif/v2.1.0/cos02/schemas/sarif-schema-2.1.0.json","runs":[{"artifacts":[{"length":40,"location":{"index":0,"uri":"file:///main.cpp"},"mimeType":"text/plain","roles":["resultFile"]}],"columnKind":"unicodeCodePoints","results":[{"level":"error","locations":[{"physicalLocation":{"artifactLocation":{"index":0,"uri":"file:///main.cpp"},"region":{"endColumn":14,"startColumn":14,"startLine":3}}}],"message":{"text":"expected ';' after top level declarator"},"ruleId":"clang.unittest","ruleIndex":0}],"tool":{"driver":{"fullName":"sarif test runner","informationUri":"https://clang.llvm.org/docs/UsersManual.html","language":"en-US","name":"sarif test","rules":[{"defaultConfiguration":{"enabled":true,"level":"warning","rank":-1},"fullDescription":{"text":"Example rule created during unit tests"},"id":"clang.unittest","name":"clang unit test"}],"version":"1.0.0"}}}],"version":"2.1.0"})";
297 SarifDocumentWriter Writer
{SourceMgr
};
298 const SarifRule
&Rule
=
300 .setRuleId("clang.unittest")
301 .setDescription("Example rule created during unit tests")
302 .setName("clang unit test");
304 // WHEN: A result is added with valid source locations for its diagnostics
305 Writer
.createRun("sarif test", "sarif test runner", "1.0.0");
306 unsigned RuleIdx
= Writer
.createRule(Rule
);
308 llvm::SmallVector
<CharSourceRange
, 1> DiagLocs
;
309 const char *SourceText
= "int foo = 0;\n"
314 registerSource("/main.cpp", SourceText
, /* IsMainFile = */ true);
315 CharSourceRange SourceCSR
=
316 getFakeCharSourceRange(MainFileID
, {3, 14}, {3, 14});
318 DiagLocs
.push_back(SourceCSR
);
320 const SarifResult
&Result
=
321 SarifResult::create(RuleIdx
)
322 .setLocations(DiagLocs
)
323 .setDiagnosticMessage("expected ';' after top level declarator")
324 .setDiagnosticLevel(SarifResultLevel::Error
);
325 Writer
.appendResult(Result
);
326 std::string Output
= serializeSarifDocument(Writer
.createDocument());
328 // THEN: Assert that the serialized SARIF is as expected
329 ASSERT_THAT(Output
, ::testing::StrEq(ExpectedOutput
));
332 TEST_F(SarifDocumentWriterTest
, checkSerializingCodeflows
) {
334 const std::string ExpectedOutput
=
335 R
"({"$schema
":"https
://docs.oasis-open.org/sarif/sarif/v2.1.0/cos02/schemas/sarif-schema-2.1.0.json","runs":[{"artifacts":[{"length":41,"location":{"index":0,"uri":"file:///main.cpp"},"mimeType":"text/plain","roles":["resultFile"]},{"length":27,"location":{"index":1,"uri":"file:///test-header-1.h"},"mimeType":"text/plain","roles":["resultFile"]},{"length":30,"location":{"index":2,"uri":"file:///test-header-2.h"},"mimeType":"text/plain","roles":["resultFile"]},{"length":28,"location":{"index":3,"uri":"file:///test-header-3.h"},"mimeType":"text/plain","roles":["resultFile"]}],"columnKind":"unicodeCodePoints","results":[{"codeFlows":[{"threadFlows":[{"locations":[{"importance":"essential","location":{"message":{"text":"Message #1"},"physicalLocation":{"artifactLocation":{"index":1,"uri":"file:///test-header-1.h"},"region":{"endColumn":8,"endLine":2,"startColumn":1,"startLine":1}}}},{"importance":"important","location":{"message":{"text":"Message #2"},"physicalLocation":{"artifactLocation":{"index":2,"uri":"file:///test-header-2.h"},"region":{"endColumn":8,"endLine":2,"startColumn":1,"startLine":1}}}},{"importance":"unimportant","location":{"message":{"text":"Message #3"},"physicalLocation":{"artifactLocation":{"index":3,"uri":"file:///test-header-3.h"},"region":{"endColumn":8,"endLine":2,"startColumn":1,"startLine":1}}}}]}]}],"level":"warning","locations":[{"physicalLocation":{"artifactLocation":{"index":0,"uri":"file:///main.cpp"},"region":{"endColumn":8,"endLine":2,"startColumn":5,"startLine":2}}}],"message":{"text":"Redefinition of 'foo'"},"ruleId":"clang.unittest","ruleIndex":0}],"tool":{"driver":{"fullName":"sarif test runner","informationUri":"https://clang.llvm.org/docs/UsersManual.html","language":"en-US","name":"sarif test","rules":[{"defaultConfiguration":{"enabled":true,"level":"warning","rank":-1},"fullDescription":{"text":"Example rule created during unit tests"},"id":"clang.unittest","name":"clang unit test"}],"version":"1.0.0"}}}],"version":"2.1.0"})";
337 const char *SourceText
= "int foo = 0;\n"
341 registerSource("/main.cpp", SourceText
, /* IsMainFile = */ true);
342 CharSourceRange DiagLoc
{getFakeCharSourceRange(MainFileID
, {2, 5}, {2, 8})};
344 SarifDocumentWriter Writer
{SourceMgr
};
345 const SarifRule
&Rule
=
347 .setRuleId("clang.unittest")
348 .setDescription("Example rule created during unit tests")
349 .setName("clang unit test");
351 constexpr unsigned int NumCases
= 3;
352 llvm::SmallVector
<ThreadFlow
, NumCases
> Threadflows
;
353 const char *HeaderTexts
[NumCases
]{("#pragma once\n"
361 const char *HeaderNames
[NumCases
]{"/test-header-1.h", "/test-header-2.h",
363 ThreadFlowImportance Importances
[NumCases
]{ThreadFlowImportance::Essential
,
364 ThreadFlowImportance::Important
,
365 ThreadFlowImportance::Unimportant
};
366 for (size_t Idx
= 0; Idx
!= NumCases
; ++Idx
) {
367 FileID FID
= registerSource(HeaderNames
[Idx
], HeaderTexts
[Idx
]);
368 CharSourceRange
&&CSR
= getFakeCharSourceRange(FID
, {1, 1}, {2, 8});
369 std::string Message
= llvm::formatv("Message #{0}", Idx
+ 1);
370 ThreadFlow Item
= ThreadFlow::create()
372 .setImportance(Importances
[Idx
])
373 .setMessage(Message
);
374 Threadflows
.push_back(Item
);
377 // WHEN: A result containing code flows and diagnostic locations is added
378 Writer
.createRun("sarif test", "sarif test runner", "1.0.0");
379 unsigned RuleIdx
= Writer
.createRule(Rule
);
380 const SarifResult
&Result
=
381 SarifResult::create(RuleIdx
)
382 .setLocations({DiagLoc
})
383 .setDiagnosticMessage("Redefinition of 'foo'")
384 .setThreadFlows(Threadflows
)
385 .setDiagnosticLevel(SarifResultLevel::Warning
);
386 Writer
.appendResult(Result
);
387 std::string Output
= serializeSarifDocument(Writer
.createDocument());
389 // THEN: Assert that the serialized SARIF is as expected
390 ASSERT_THAT(Output
, ::testing::StrEq(ExpectedOutput
));