| //===- unittests/Basic/SarifTest.cpp - Test writing SARIF documents -------===// |
| // |
| // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
| // See https://llvm.org/LICENSE.txt for license information. |
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| // |
| //===----------------------------------------------------------------------===// |
| |
| #include "clang/Basic/Sarif.h" |
| #include "clang/Basic/DiagnosticOptions.h" |
| #include "clang/Basic/FileManager.h" |
| #include "clang/Basic/FileSystemOptions.h" |
| #include "clang/Basic/LangOptions.h" |
| #include "clang/Basic/SourceLocation.h" |
| #include "clang/Basic/SourceManager.h" |
| #include "llvm/ADT/StringRef.h" |
| #include "llvm/Support/FormatVariadic.h" |
| #include "llvm/Support/JSON.h" |
| #include "llvm/Support/MemoryBuffer.h" |
| #include "llvm/Support/VirtualFileSystem.h" |
| #include "llvm/Support/raw_ostream.h" |
| #include "gmock/gmock.h" |
| #include "gtest/gtest.h" |
| |
| #include <algorithm> |
| |
| using namespace clang; |
| |
| namespace { |
| |
| using LineCol = std::pair<unsigned int, unsigned int>; |
| |
| static std::string serializeSarifDocument(llvm::json::Object &&Doc) { |
| std::string Output; |
| llvm::json::Value Value(std::move(Doc)); |
| llvm::raw_string_ostream OS{Output}; |
| OS << llvm::formatv("{0}", Value); |
| OS.flush(); |
| return Output; |
| } |
| |
| class SarifDocumentWriterTest : public ::testing::Test { |
| protected: |
| SarifDocumentWriterTest() |
| : InMemoryFileSystem(new llvm::vfs::InMemoryFileSystem), |
| FileMgr(FileSystemOptions(), InMemoryFileSystem), |
| DiagID(new DiagnosticIDs()), DiagOpts(new DiagnosticOptions()), |
| Diags(DiagID, DiagOpts.get(), new IgnoringDiagConsumer()), |
| SourceMgr(Diags, FileMgr) {} |
| |
| IntrusiveRefCntPtr<llvm::vfs::InMemoryFileSystem> InMemoryFileSystem; |
| FileManager FileMgr; |
| IntrusiveRefCntPtr<DiagnosticIDs> DiagID; |
| IntrusiveRefCntPtr<DiagnosticOptions> DiagOpts; |
| DiagnosticsEngine Diags; |
| SourceManager SourceMgr; |
| LangOptions LangOpts; |
| |
| FileID registerSource(llvm::StringRef Name, const char *SourceText, |
| bool IsMainFile = false) { |
| std::unique_ptr<llvm::MemoryBuffer> SourceBuf = |
| llvm::MemoryBuffer::getMemBuffer(SourceText); |
| FileEntryRef SourceFile = |
| FileMgr.getVirtualFileRef(Name, SourceBuf->getBufferSize(), 0); |
| SourceMgr.overrideFileContents(SourceFile, std::move(SourceBuf)); |
| FileID FID = SourceMgr.getOrCreateFileID(SourceFile, SrcMgr::C_User); |
| if (IsMainFile) |
| SourceMgr.setMainFileID(FID); |
| return FID; |
| } |
| |
| CharSourceRange getFakeCharSourceRange(FileID FID, LineCol Begin, |
| LineCol End) { |
| auto BeginLoc = SourceMgr.translateLineCol(FID, Begin.first, Begin.second); |
| auto EndLoc = SourceMgr.translateLineCol(FID, End.first, End.second); |
| return CharSourceRange{SourceRange{BeginLoc, EndLoc}, /* ITR = */ false}; |
| } |
| }; |
| |
| TEST_F(SarifDocumentWriterTest, canCreateEmptyDocument) { |
| // GIVEN: |
| SarifDocumentWriter Writer{SourceMgr}; |
| |
| // WHEN: |
| const llvm::json::Object &EmptyDoc = Writer.createDocument(); |
| std::vector<StringRef> Keys(EmptyDoc.size()); |
| std::transform(EmptyDoc.begin(), EmptyDoc.end(), Keys.begin(), |
| [](auto Item) { return Item.getFirst(); }); |
| |
| // THEN: |
| ASSERT_THAT(Keys, testing::UnorderedElementsAre("$schema", "version")); |
| } |
| |
| // Test that a newly inserted run will associate correct tool names |
| TEST_F(SarifDocumentWriterTest, canCreateDocumentWithOneRun) { |
| // GIVEN: |
| SarifDocumentWriter Writer{SourceMgr}; |
| const char *ShortName = "sariftest"; |
| const char *LongName = "sarif writer test"; |
| |
| // WHEN: |
| Writer.createRun(ShortName, LongName); |
| Writer.endRun(); |
| const llvm::json::Object &Doc = Writer.createDocument(); |
| const llvm::json::Array *Runs = Doc.getArray("runs"); |
| |
| // THEN: |
| // A run was created |
| ASSERT_THAT(Runs, testing::NotNull()); |
| |
| // It is the only run |
| ASSERT_EQ(Runs->size(), 1UL); |
| |
| // The tool associated with the run was the tool |
| const llvm::json::Object *Driver = |
| Runs->begin()->getAsObject()->getObject("tool")->getObject("driver"); |
| ASSERT_THAT(Driver, testing::NotNull()); |
| |
| ASSERT_TRUE(Driver->getString("name").has_value()); |
| ASSERT_TRUE(Driver->getString("fullName").has_value()); |
| ASSERT_TRUE(Driver->getString("language").has_value()); |
| |
| EXPECT_EQ(*Driver->getString("name"), ShortName); |
| EXPECT_EQ(*Driver->getString("fullName"), LongName); |
| EXPECT_EQ(*Driver->getString("language"), "en-US"); |
| } |
| |
| TEST_F(SarifDocumentWriterTest, addingResultsWillCrashIfThereIsNoRun) { |
| #if defined(NDEBUG) || !GTEST_HAS_DEATH_TEST |
| GTEST_SKIP() << "This death test is only available for debug builds."; |
| #endif |
| // GIVEN: |
| SarifDocumentWriter Writer{SourceMgr}; |
| |
| // WHEN: |
| // A SarifDocumentWriter::createRun(...) was not called prior to |
| // SarifDocumentWriter::appendResult(...) |
| // But a rule exists |
| auto RuleIdx = Writer.createRule(SarifRule::create()); |
| const SarifResult &EmptyResult = SarifResult::create(RuleIdx); |
| |
| // THEN: |
| auto Matcher = ::testing::AnyOf( |
| ::testing::HasSubstr("create a run first"), |
| ::testing::HasSubstr("no runs associated with the document")); |
| ASSERT_DEATH(Writer.appendResult(EmptyResult), Matcher); |
| } |
| |
| TEST_F(SarifDocumentWriterTest, settingInvalidRankWillCrash) { |
| #if defined(NDEBUG) || !GTEST_HAS_DEATH_TEST |
| GTEST_SKIP() << "This death test is only available for debug builds."; |
| #endif |
| // GIVEN: |
| SarifDocumentWriter Writer{SourceMgr}; |
| |
| // WHEN: |
| // A SarifReportingConfiguration is created with an invalid "rank" |
| // * Ranks below 0.0 are invalid |
| // * Ranks above 100.0 are invalid |
| |
| // THEN: The builder will crash in either case |
| EXPECT_DEATH(SarifReportingConfiguration::create().setRank(-1.0), |
| ::testing::HasSubstr("Rule rank cannot be smaller than 0.0")); |
| EXPECT_DEATH(SarifReportingConfiguration::create().setRank(101.0), |
| ::testing::HasSubstr("Rule rank cannot be larger than 100.0")); |
| } |
| |
| TEST_F(SarifDocumentWriterTest, creatingResultWithDisabledRuleWillCrash) { |
| #if defined(NDEBUG) || !GTEST_HAS_DEATH_TEST |
| GTEST_SKIP() << "This death test is only available for debug builds."; |
| #endif |
| |
| // GIVEN: |
| SarifDocumentWriter Writer{SourceMgr}; |
| |
| // WHEN: |
| // A disabled Rule is created, and a result is create referencing this rule |
| const auto &Config = SarifReportingConfiguration::create().disable(); |
| auto RuleIdx = |
| Writer.createRule(SarifRule::create().setDefaultConfiguration(Config)); |
| const SarifResult &Result = SarifResult::create(RuleIdx); |
| |
| // THEN: |
| // SarifResult::create(...) will produce a crash |
| ASSERT_DEATH( |
| Writer.appendResult(Result), |
| ::testing::HasSubstr("Cannot add a result referencing a disabled Rule")); |
| } |
| |
| // Test adding rule and result shows up in the final document |
| TEST_F(SarifDocumentWriterTest, addingResultWithValidRuleAndRunIsOk) { |
| // GIVEN: |
| SarifDocumentWriter Writer{SourceMgr}; |
| const SarifRule &Rule = |
| SarifRule::create() |
| .setRuleId("clang.unittest") |
| .setDescription("Example rule created during unit tests") |
| .setName("clang unit test"); |
| |
| // WHEN: |
| Writer.createRun("sarif test", "sarif test runner"); |
| unsigned RuleIdx = Writer.createRule(Rule); |
| const SarifResult &Result = SarifResult::create(RuleIdx); |
| |
| Writer.appendResult(Result); |
| const llvm::json::Object &Doc = Writer.createDocument(); |
| |
| // THEN: |
| // A document with a valid schema and version exists |
| ASSERT_THAT(Doc.get("$schema"), ::testing::NotNull()); |
| ASSERT_THAT(Doc.get("version"), ::testing::NotNull()); |
| const llvm::json::Array *Runs = Doc.getArray("runs"); |
| |
| // A run exists on this document |
| ASSERT_THAT(Runs, ::testing::NotNull()); |
| ASSERT_EQ(Runs->size(), 1UL); |
| const llvm::json::Object *TheRun = Runs->back().getAsObject(); |
| |
| // The run has slots for tools, results, rules and artifacts |
| ASSERT_THAT(TheRun->get("tool"), ::testing::NotNull()); |
| ASSERT_THAT(TheRun->get("results"), ::testing::NotNull()); |
| ASSERT_THAT(TheRun->get("artifacts"), ::testing::NotNull()); |
| const llvm::json::Object *Driver = |
| TheRun->getObject("tool")->getObject("driver"); |
| const llvm::json::Array *Results = TheRun->getArray("results"); |
| const llvm::json::Array *Artifacts = TheRun->getArray("artifacts"); |
| |
| // The tool is as expected |
| ASSERT_TRUE(Driver->getString("name").has_value()); |
| ASSERT_TRUE(Driver->getString("fullName").has_value()); |
| |
| EXPECT_EQ(*Driver->getString("name"), "sarif test"); |
| EXPECT_EQ(*Driver->getString("fullName"), "sarif test runner"); |
| |
| // The results are as expected |
| EXPECT_EQ(Results->size(), 1UL); |
| |
| // The artifacts are as expected |
| EXPECT_TRUE(Artifacts->empty()); |
| } |
| |
| TEST_F(SarifDocumentWriterTest, checkSerializingResultsWithDefaultRuleConfig) { |
| // GIVEN: |
| const std::string ExpectedOutput = |
| 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"})"; |
| |
| SarifDocumentWriter Writer{SourceMgr}; |
| const SarifRule &Rule = |
| SarifRule::create() |
| .setRuleId("clang.unittest") |
| .setDescription("Example rule created during unit tests") |
| .setName("clang unit test"); |
| |
| // WHEN: A run contains a result |
| Writer.createRun("sarif test", "sarif test runner", "1.0.0"); |
| unsigned RuleIdx = Writer.createRule(Rule); |
| const SarifResult &Result = SarifResult::create(RuleIdx); |
| Writer.appendResult(Result); |
| std::string Output = serializeSarifDocument(Writer.createDocument()); |
| |
| // THEN: |
| ASSERT_THAT(Output, ::testing::StrEq(ExpectedOutput)); |
| } |
| |
| TEST_F(SarifDocumentWriterTest, checkSerializingResultsWithCustomRuleConfig) { |
| // GIVEN: |
| const std::string ExpectedOutput = |
| 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"})"; |
| |
| SarifDocumentWriter Writer{SourceMgr}; |
| const SarifRule &Rule = |
| SarifRule::create() |
| .setRuleId("clang.unittest") |
| .setDescription("Example rule created during unit tests") |
| .setName("clang unit test") |
| .setDefaultConfiguration(SarifReportingConfiguration::create() |
| .setLevel(SarifResultLevel::Error) |
| .setRank(35.5)); |
| |
| // WHEN: A run contains a result |
| Writer.createRun("sarif test", "sarif test runner", "1.0.0"); |
| unsigned RuleIdx = Writer.createRule(Rule); |
| const SarifResult &Result = SarifResult::create(RuleIdx); |
| Writer.appendResult(Result); |
| std::string Output = serializeSarifDocument(Writer.createDocument()); |
| |
| // THEN: |
| ASSERT_THAT(Output, ::testing::StrEq(ExpectedOutput)); |
| } |
| |
| // Check that serializing artifacts from results produces valid SARIF |
| TEST_F(SarifDocumentWriterTest, checkSerializingArtifacts) { |
| // GIVEN: |
| const std::string ExpectedOutput = |
| 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"})"; |
| |
| SarifDocumentWriter Writer{SourceMgr}; |
| const SarifRule &Rule = |
| SarifRule::create() |
| .setRuleId("clang.unittest") |
| .setDescription("Example rule created during unit tests") |
| .setName("clang unit test"); |
| |
| // WHEN: A result is added with valid source locations for its diagnostics |
| Writer.createRun("sarif test", "sarif test runner", "1.0.0"); |
| unsigned RuleIdx = Writer.createRule(Rule); |
| |
| llvm::SmallVector<CharSourceRange, 1> DiagLocs; |
| const char *SourceText = "int foo = 0;\n" |
| "int bar = 1;\n" |
| "float x = 0.0\n"; |
| |
| FileID MainFileID = |
| registerSource("/main.cpp", SourceText, /* IsMainFile = */ true); |
| CharSourceRange SourceCSR = |
| getFakeCharSourceRange(MainFileID, {3, 14}, {3, 14}); |
| |
| DiagLocs.push_back(SourceCSR); |
| |
| const SarifResult &Result = |
| SarifResult::create(RuleIdx) |
| .setLocations(DiagLocs) |
| .setDiagnosticMessage("expected ';' after top level declarator") |
| .setDiagnosticLevel(SarifResultLevel::Error); |
| Writer.appendResult(Result); |
| std::string Output = serializeSarifDocument(Writer.createDocument()); |
| |
| // THEN: Assert that the serialized SARIF is as expected |
| ASSERT_THAT(Output, ::testing::StrEq(ExpectedOutput)); |
| } |
| |
| TEST_F(SarifDocumentWriterTest, checkSerializingCodeflows) { |
| // GIVEN: |
| const std::string ExpectedOutput = |
| 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"})"; |
| |
| const char *SourceText = "int foo = 0;\n" |
| "int foo = 1;\n" |
| "float x = 0.0;\n"; |
| FileID MainFileID = |
| registerSource("/main.cpp", SourceText, /* IsMainFile = */ true); |
| CharSourceRange DiagLoc{getFakeCharSourceRange(MainFileID, {2, 5}, {2, 8})}; |
| |
| SarifDocumentWriter Writer{SourceMgr}; |
| const SarifRule &Rule = |
| SarifRule::create() |
| .setRuleId("clang.unittest") |
| .setDescription("Example rule created during unit tests") |
| .setName("clang unit test"); |
| |
| constexpr unsigned int NumCases = 3; |
| llvm::SmallVector<ThreadFlow, NumCases> Threadflows; |
| const char *HeaderTexts[NumCases]{("#pragma once\n" |
| "#include <foo>"), |
| ("#ifndef FOO\n" |
| "#define FOO\n" |
| "#endif"), |
| ("#ifdef FOO\n" |
| "#undef FOO\n" |
| "#endif")}; |
| const char *HeaderNames[NumCases]{"/test-header-1.h", "/test-header-2.h", |
| "/test-header-3.h"}; |
| ThreadFlowImportance Importances[NumCases]{ThreadFlowImportance::Essential, |
| ThreadFlowImportance::Important, |
| ThreadFlowImportance::Unimportant}; |
| for (size_t Idx = 0; Idx != NumCases; ++Idx) { |
| FileID FID = registerSource(HeaderNames[Idx], HeaderTexts[Idx]); |
| CharSourceRange &&CSR = getFakeCharSourceRange(FID, {1, 1}, {2, 8}); |
| std::string Message = llvm::formatv("Message #{0}", Idx + 1); |
| ThreadFlow Item = ThreadFlow::create() |
| .setRange(CSR) |
| .setImportance(Importances[Idx]) |
| .setMessage(Message); |
| Threadflows.push_back(Item); |
| } |
| |
| // WHEN: A result containing code flows and diagnostic locations is added |
| Writer.createRun("sarif test", "sarif test runner", "1.0.0"); |
| unsigned RuleIdx = Writer.createRule(Rule); |
| const SarifResult &Result = |
| SarifResult::create(RuleIdx) |
| .setLocations({DiagLoc}) |
| .setDiagnosticMessage("Redefinition of 'foo'") |
| .setThreadFlows(Threadflows) |
| .setDiagnosticLevel(SarifResultLevel::Warning); |
| Writer.appendResult(Result); |
| std::string Output = serializeSarifDocument(Writer.createDocument()); |
| |
| // THEN: Assert that the serialized SARIF is as expected |
| ASSERT_THAT(Output, ::testing::StrEq(ExpectedOutput)); |
| } |
| |
| } // namespace |