/*
* Cppcheck - A tool for static C/C++ code analysis
* Copyright (C) 2007-2026 Cppcheck team.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
#include "sarifreport.h"
#include "errorlogger.h"
#include "errortypes.h"
#include "fixture.h"
#include
#include
#include
#include
#include "json.h"
class TestSarifReport : public TestFixture
{
public:
TestSarifReport() : TestFixture("TestSarifReport")
{}
private:
void run() override
{
TEST_CASE(emptyReport);
TEST_CASE(singleError);
TEST_CASE(multipleErrors);
TEST_CASE(errorWithoutLocation);
TEST_CASE(errorWithMultipleLocations);
TEST_CASE(differentSeverityLevels);
TEST_CASE(securityRelatedErrors);
TEST_CASE(cweTagsPresent);
TEST_CASE(noCweNoSecurity);
TEST_CASE(inconclusiveCertainty);
TEST_CASE(criticalErrorId);
TEST_CASE(emptyDescriptions);
TEST_CASE(locationBoundaryValues);
TEST_CASE(duplicateRuleIds);
TEST_CASE(customProductName);
TEST_CASE(versionHandling);
TEST_CASE(securitySeverityMapping);
TEST_CASE(versionWithSpace);
TEST_CASE(customProductNameAndVersion);
TEST_CASE(normalizeLineColumnToOne);
TEST_CASE(internalAndDebugSeverity);
TEST_CASE(problemSeverityMapping);
TEST_CASE(mixedLocationAndNoLocation);
}
// Helper to create an ErrorMessage
static ErrorMessage createErrorMessage(const std::string& id,
Severity severity,
const std::string& msg,
const std::string& file = "test.cpp",
int line = 10,
int column = 5,
int cweId = 0,
Certainty certainty = Certainty::normal)
{
ErrorMessage::FileLocation loc(file, line, column);
ErrorMessage errorMessage({loc}, file, severity, msg, id, certainty);
if (cweId > 0)
{
errorMessage.cwe = CWE(cweId);
}
return errorMessage;
}
// Helper to parse JSON and validate structure
static bool parseAndValidateJson(const std::string& json, picojson::value& root)
{
std::string parseError = picojson::parse(root, json);
return parseError.empty() && root.is();
}
void emptyReport()
{
SarifReport report;
std::string sarif = report.serialize("TestProduct");
picojson::value json;
ASSERT(parseAndValidateJson(sarif, json));
const picojson::object& root = json.get();
ASSERT_EQUALS("2.1.0", root.at("version").get());
ASSERT(root.at("$schema").get().find("sarif-schema-2.1.0") != std::string::npos);
// From SARIF specification (https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/sarif-v2.1.0-errata01-os-complete.html#_Toc141790730):
// Although the order in which properties appear in a JSON object value is not semantically significant, the version property SHOULD appear first.
ASSERT_EQUALS("{\n \"version\": \"2.1.0\"", sarif.substr(0,22));
const picojson::array& runs = root.at("runs").get();
ASSERT_EQUALS(1U, runs.size());
const picojson::object& cur_run = runs[0].get();
const picojson::array& results = cur_run.at("results").get();
ASSERT_EQUALS(0U, results.size());
const picojson::object& tool = cur_run.at("tool").get();
const picojson::object& driver = tool.at("driver").get();
const picojson::array& rules = driver.at("rules").get();
ASSERT_EQUALS(0U, rules.size());
}
void singleError()
{
SarifReport report;
report.addFinding(createErrorMessage("nullPointer", Severity::error, "Null pointer dereference"));
std::string sarif = report.serialize("Cppcheck");
picojson::value json;
ASSERT(parseAndValidateJson(sarif, json));
const picojson::object& root = json.get();
const picojson::array& runs = root.at("runs").get();
const picojson::object& cur_run = runs[0].get();
// Check results
const picojson::array& results = cur_run.at("results").get();
ASSERT_EQUALS(1U, results.size());
const picojson::object& result = results[0].get();
ASSERT_EQUALS("nullPointer", result.at("ruleId").get());
ASSERT_EQUALS("error", result.at("level").get());
const picojson::object& message = result.at("message").get();
ASSERT_EQUALS("Null pointer dereference", message.at("text").get());
// Check rules
const picojson::object& tool = cur_run.at("tool").get();
const picojson::object& driver = tool.at("driver").get();
const picojson::array& rules = driver.at("rules").get();
ASSERT_EQUALS(1U, rules.size());
const picojson::object& rule = rules[0].get();
ASSERT_EQUALS("nullPointer", rule.at("id").get());
}
void multipleErrors()
{
SarifReport report;
report.addFinding(createErrorMessage("error1", Severity::error, "Error 1", "file1.cpp", 10, 5));
report.addFinding(createErrorMessage("error2", Severity::warning, "Error 2", "file2.cpp", 20, 10));
report.addFinding(createErrorMessage("error3", Severity::style, "Error 3", "file3.cpp", 30, 15));
std::string sarif = report.serialize("Cppcheck");
picojson::value json;
ASSERT(parseAndValidateJson(sarif, json));
const picojson::object& root = json.get();
const picojson::array& runs = root.at("runs").get();
const picojson::object& cur_run = runs[0].get();
const picojson::array& results = cur_run.at("results").get();
ASSERT_EQUALS(3U, results.size());
const picojson::object& tool = cur_run.at("tool").get();
const picojson::object& driver = tool.at("driver").get();
const picojson::array& rules = driver.at("rules").get();
ASSERT_EQUALS(3U, rules.size());
}
void errorWithoutLocation()
{
SarifReport report;
// Create error without location (empty callStack)
ErrorMessage errorMessage(
{}, "test.cpp", Severity::error, "Error without location", "testError", Certainty::normal);
report.addFinding(errorMessage);
std::string sarif = report.serialize("Cppcheck");
picojson::value json;
ASSERT(parseAndValidateJson(sarif, json));
const picojson::object& root = json.get();
const picojson::array& runs = root.at("runs").get();
const picojson::object& cur_run = runs[0].get();
// Should have no results (GitHub doesn't support findings without locations)
const picojson::array& results = cur_run.at("results").get();
ASSERT_EQUALS(0U, results.size());
const picojson::object& tool = cur_run.at("tool").get();
const picojson::object& driver = tool.at("driver").get();
const picojson::array& rules = driver.at("rules").get();
ASSERT_EQUALS(0U, rules.size());
}
void errorWithMultipleLocations()
{
SarifReport report;
ErrorMessage::FileLocation loc1("test1.cpp", 10, 5);
ErrorMessage::FileLocation loc2("test2.cpp", 20, 10);
ErrorMessage::FileLocation loc3("test3.cpp", 30, 15);
ErrorMessage errorMessage({loc1, loc2, loc3},
"test1.cpp",
Severity::error,
"Error with multiple locations",
"multiLocError",
Certainty::normal);
report.addFinding(errorMessage);
std::string sarif = report.serialize("Cppcheck");
picojson::value json;
ASSERT(parseAndValidateJson(sarif, json));
const picojson::object& root = json.get();
const picojson::array& runs = root.at("runs").get();
const picojson::object& cur_run = runs[0].get();
const picojson::array& results = cur_run.at("results").get();
ASSERT_EQUALS(1U, results.size());
const picojson::object& result = results[0].get();
const picojson::array& locations = result.at("locations").get();
ASSERT_EQUALS(3U, locations.size());
// Verify each location
const picojson::object& loc1Obj = locations[0].get();
const picojson::object& physLoc1 = loc1Obj.at("physicalLocation").get();
const picojson::object& region1 = physLoc1.at("region").get();
ASSERT_EQUALS(10, static_cast(region1.at("startLine").get()));
ASSERT_EQUALS(5, static_cast(region1.at("startColumn").get()));
}
void differentSeverityLevels()
{
SarifReport report;
report.addFinding(createErrorMessage("error1", Severity::error, "Error severity"));
report.addFinding(createErrorMessage("warning1", Severity::warning, "Warning severity"));
report.addFinding(createErrorMessage("style1", Severity::style, "Style severity"));
report.addFinding(createErrorMessage("perf1", Severity::performance, "Performance severity"));
report.addFinding(createErrorMessage("port1", Severity::portability, "Portability severity"));
report.addFinding(createErrorMessage("info1", Severity::information, "Information severity"));
std::string sarif = report.serialize("Cppcheck");
picojson::value json;
ASSERT(parseAndValidateJson(sarif, json));
const picojson::object& root = json.get();
const picojson::array& runs = root.at("runs").get();
const picojson::object& cur_run = runs[0].get();
const picojson::array& results = cur_run.at("results").get();
ASSERT_EQUALS(6U, results.size());
// Check severity mappings
ASSERT_EQUALS("error", results[0].get().at("level").get()); // error
ASSERT_EQUALS("error", results[1].get().at("level").get()); // warning
ASSERT_EQUALS("warning", results[2].get().at("level").get()); // style
ASSERT_EQUALS("warning", results[3].get().at("level").get()); // performance
ASSERT_EQUALS("warning", results[4].get().at("level").get()); // portability
ASSERT_EQUALS("note", results[5].get().at("level").get()); // information
}
void securityRelatedErrors()
{
SarifReport report;
// Add errors with CWE IDs
report.addFinding(createErrorMessage("nullPointer", Severity::error, "Null pointer", "test.cpp", 10, 5, 476));
report.addFinding(
createErrorMessage("bufferOverflow", Severity::error, "Buffer overflow", "test.cpp", 20, 5, 121));
report.addFinding(createErrorMessage("memleak", Severity::warning, "Memory leak", "test.cpp", 30, 5, 401));
std::string sarif = report.serialize("Cppcheck");
picojson::value json;
ASSERT(parseAndValidateJson(sarif, json));
const picojson::object& root = json.get();
const picojson::array& runs = root.at("runs").get();
const picojson::object& cur_run = runs[0].get();
const picojson::object& tool = cur_run.at("tool").get();
const picojson::object& driver = tool.at("driver").get();
const picojson::array& rules = driver.at("rules").get();
for (const auto& rule : rules)
{
const picojson::object& r = rule.get();
const picojson::object& props = r.at("properties").get();
// Should have security-severity
ASSERT(props.find("security-severity") != props.end());
// Should have tags with security and CWE
ASSERT(props.find("tags") != props.end());
const picojson::array& tags = props.at("tags").get();
bool hasSecurityTag = false;
bool hasCweTag = false;
for (const auto& tag : tags)
{
const std::string& tagStr = tag.get();
if (tagStr == "security")
hasSecurityTag = true;
if (tagStr.find("external/cwe/cwe-") == 0)
hasCweTag = true;
}
ASSERT(hasSecurityTag);
ASSERT(hasCweTag);
}
}
void cweTagsPresent()
{
SarifReport report;
report.addFinding(createErrorMessage("testError", Severity::error, "Test error", "test.cpp", 10, 5, 119));
std::string sarif = report.serialize("Cppcheck");
picojson::value json;
ASSERT(parseAndValidateJson(sarif, json));
const picojson::object& root = json.get();
const picojson::array& runs = root.at("runs").get();
const picojson::object& cur_run = runs[0].get();
const picojson::object& tool = cur_run.at("tool").get();
const picojson::object& driver = tool.at("driver").get();
const picojson::array& rules = driver.at("rules").get();
ASSERT_EQUALS(1U, rules.size());
const picojson::object& rule = rules[0].get();
const picojson::object& props = rule.at("properties").get();
const picojson::array& tags = props.at("tags").get();
bool foundCwe119 = false;
for (const auto& tag : tags)
{
if (tag.get() == "external/cwe/cwe-119")
foundCwe119 = true;
}
ASSERT(foundCwe119);
}
void noCweNoSecurity()
{
SarifReport report;
// Error without CWE ID should not have security properties
report.addFinding(createErrorMessage("styleError", Severity::style, "Style error", "test.cpp", 10, 5, 0));
std::string sarif = report.serialize("Cppcheck");
picojson::value json;
ASSERT(parseAndValidateJson(sarif, json));
const picojson::object& root = json.get();
const picojson::array& runs = root.at("runs").get();
const picojson::object& cur_run = runs[0].get();
const picojson::object& tool = cur_run.at("tool").get();
const picojson::object& driver = tool.at("driver").get();
const picojson::array& rules = driver.at("rules").get();
ASSERT_EQUALS(1U, rules.size());
const picojson::object& rule = rules[0].get();
const picojson::object& props = rule.at("properties").get();
// Should NOT have security-severity
ASSERT(props.find("security-severity") == props.end());
// Should NOT have tags (or if present, no security tag)
if (props.find("tags") != props.end())
{
const picojson::array& tags = props.at("tags").get();
for (const auto& tag : tags)
{
ASSERT(tag.get() != "security");
}
}
}
void inconclusiveCertainty()
{
SarifReport report;
report.addFinding(
createErrorMessage("test1", Severity::error, "Conclusive", "test.cpp", 10, 5, 0, Certainty::normal));
report.addFinding(createErrorMessage(
"test2", Severity::error, "Inconclusive", "test.cpp", 20, 5, 0, Certainty::inconclusive));
std::string sarif = report.serialize("Cppcheck");
picojson::value json;
ASSERT(parseAndValidateJson(sarif, json));
const picojson::object& root = json.get();
const picojson::array& runs = root.at("runs").get();
const picojson::object& cur_run = runs[0].get();
const picojson::object& tool = cur_run.at("tool").get();
const picojson::object& driver = tool.at("driver").get();
const picojson::array& rules = driver.at("rules").get();
ASSERT_EQUALS(2U, rules.size());
// Check precision values
const picojson::object& rule1 = rules[0].get();
const picojson::object& props1 = rule1.at("properties").get();
ASSERT_EQUALS("high", props1.at("precision").get());
const picojson::object& rule2 = rules[1].get();
const picojson::object& props2 = rule2.at("properties").get();
ASSERT_EQUALS("medium", props2.at("precision").get());
}
void criticalErrorId()
{
SarifReport report;
// Use a critical error ID (from ErrorLogger::isCriticalErrorId)
report.addFinding(createErrorMessage("syntaxError", Severity::error, "Syntax error"));
std::string sarif = report.serialize("Cppcheck");
picojson::value json;
ASSERT(parseAndValidateJson(sarif, json));
const picojson::object& root = json.get();
const picojson::array& runs = root.at("runs").get();
const picojson::object& cur_run = runs[0].get();
const picojson::array& results = cur_run.at("results").get();
ASSERT_EQUALS(1U, results.size());
const picojson::object& result = results[0].get();
// Critical errors should always map to "error" level
ASSERT_EQUALS("error", result.at("level").get());
}
void emptyDescriptions()
{
SarifReport report;
report.addFinding(createErrorMessage("testError", Severity::error, "Test error"));
std::string sarif = report.serialize("Cppcheck");
picojson::value json;
ASSERT(parseAndValidateJson(sarif, json));
const picojson::object& root = json.get();
const picojson::array& runs = root.at("runs").get();
const picojson::object& cur_run = runs[0].get();
const picojson::object& tool = cur_run.at("tool").get();
const picojson::object& driver = tool.at("driver").get();
const picojson::array& rules = driver.at("rules").get();
ASSERT_EQUALS(1U, rules.size());
const picojson::object& rule = rules[0].get();
// All descriptions should be empty for GitHub integration
ASSERT_EQUALS("", rule.at("name").get());
ASSERT_EQUALS("", rule.at("shortDescription").get().at("text").get());
ASSERT_EQUALS("", rule.at("fullDescription").get().at("text").get());
ASSERT_EQUALS("", rule.at("help").get().at("text").get());
}
void locationBoundaryValues()
{
SarifReport report;
// Test with line/column values that are 0
// Note: Negative values don't work correctly if FileLocation uses unsigned types
ErrorMessage::FileLocation loc1("test.cpp", 0, 0);
ErrorMessage::FileLocation loc2("test.cpp", 1, 1);
ErrorMessage errorMessage1({loc1}, "test.cpp", Severity::error, "Error at 0,0", "error1", Certainty::normal);
ErrorMessage errorMessage2({loc2}, "test.cpp", Severity::error, "Error at 1,1", "error2", Certainty::normal);
report.addFinding(errorMessage1);
report.addFinding(errorMessage2);
std::string sarif = report.serialize("Cppcheck");
picojson::value json;
ASSERT(parseAndValidateJson(sarif, json));
const picojson::object& root = json.get();
const picojson::array& runs = root.at("runs").get();
const picojson::object& cur_run = runs[0].get();
const picojson::array& results = cur_run.at("results").get();
ASSERT_EQUALS(2U, results.size());
// Check first result (0,0 should be normalized to 1,1)
{
const picojson::object& res = results[0].get();
const picojson::array& locations = res.at("locations").get();
const picojson::object& loc = locations[0].get();
const picojson::object& physLoc = loc.at("physicalLocation").get();
const picojson::object& region = physLoc.at("region").get();
int line = static_cast(region.at("startLine").get());
int column = static_cast(region.at("startColumn").get());
// 0 should be normalized to 1
ASSERT_EQUALS(1, line);
ASSERT_EQUALS(1, column);
}
// Check second result (1,1 should stay as 1,1)
{
const picojson::object& res = results[1].get();
const picojson::array& locations = res.at("locations").get();
const picojson::object& loc = locations[0].get();
const picojson::object& physLoc = loc.at("physicalLocation").get();
const picojson::object& region = physLoc.at("region").get();
int line = static_cast(region.at("startLine").get());
int column = static_cast(region.at("startColumn").get());
ASSERT_EQUALS(1, line);
ASSERT_EQUALS(1, column);
}
}
void duplicateRuleIds()
{
SarifReport report;
// Add multiple errors with the same rule ID
report.addFinding(createErrorMessage("duplicateId", Severity::error, "First error", "file1.cpp", 10, 5));
report.addFinding(createErrorMessage("duplicateId", Severity::error, "Second error", "file2.cpp", 20, 10));
report.addFinding(createErrorMessage("duplicateId", Severity::error, "Third error", "file3.cpp", 30, 15));
std::string sarif = report.serialize("Cppcheck");
picojson::value json;
ASSERT(parseAndValidateJson(sarif, json));
const picojson::object& root = json.get();
const picojson::array& runs = root.at("runs").get();
const picojson::object& cur_run = runs[0].get();
// Should have 3 results but only 1 rule
const picojson::array& results = cur_run.at("results").get();
ASSERT_EQUALS(3U, results.size());
const picojson::object& tool = cur_run.at("tool").get();
const picojson::object& driver = tool.at("driver").get();
const picojson::array& rules = driver.at("rules").get();
ASSERT_EQUALS(1U, rules.size());
const picojson::object& rule = rules[0].get();
ASSERT_EQUALS("duplicateId", rule.at("id").get());
}
void customProductName()
{
SarifReport report;
report.addFinding(createErrorMessage("testError", Severity::error, "Test error"));
// Test with custom product name
std::string sarif = report.serialize("CustomChecker");
picojson::value json;
ASSERT(parseAndValidateJson(sarif, json));
const picojson::object& root = json.get();
const picojson::array& runs = root.at("runs").get();
const picojson::object& cur_run = runs[0].get();
const picojson::object& tool = cur_run.at("tool").get();
const picojson::object& driver = tool.at("driver").get();
// Should use "Cppcheck" as default when custom name doesn't parse
ASSERT_EQUALS("Cppcheck", driver.at("name").get());
}
void versionHandling()
{
SarifReport report;
report.addFinding(createErrorMessage("testError", Severity::error, "Test error"));
std::string sarif = report.serialize("Cppcheck");
picojson::value json;
ASSERT(parseAndValidateJson(sarif, json));
const picojson::object& root = json.get();
const picojson::array& runs = root.at("runs").get();
const picojson::object& cur_run = runs[0].get();
const picojson::object& tool = cur_run.at("tool").get();
const picojson::object& driver = tool.at("driver").get();
// Should have a semantic version
ASSERT(driver.find("semanticVersion") != driver.end());
const std::string version = driver.at("semanticVersion").get();
// Version should not contain spaces (they should be stripped)
ASSERT(version.find(' ') == std::string::npos);
}
void securitySeverityMapping()
{
// Test the detailed security-severity mapping for different severity levels with CWE
SarifReport report;
// Error with CWE should get 9.9 (critical)
report.addFinding(createErrorMessage("error1", Severity::error, "Error with CWE", "test.cpp", 10, 5, 119));
// Warning with CWE should get 8.5 (high)
report.addFinding(
createErrorMessage("warning1", Severity::warning, "Warning with CWE", "test.cpp", 20, 5, 120));
// Style/Performance/Portability with CWE should get 5.5 (medium)
report.addFinding(createErrorMessage("style1", Severity::style, "Style with CWE", "test.cpp", 30, 5, 398));
report.addFinding(
createErrorMessage("perf1", Severity::performance, "Performance with CWE", "test.cpp", 40, 5, 407));
report.addFinding(
createErrorMessage("port1", Severity::portability, "Portability with CWE", "test.cpp", 50, 5, 562));
// Information with CWE should get 2.0 (low)
report.addFinding(createErrorMessage("info1", Severity::information, "Info with CWE", "test.cpp", 60, 5, 561));
std::string sarif = report.serialize("Cppcheck");
picojson::value json;
ASSERT(parseAndValidateJson(sarif, json));
const picojson::object& root = json.get();
const picojson::array& runs = root.at("runs").get();
const picojson::object& cur_run = runs[0].get();
const picojson::object& tool = cur_run.at("tool").get();
const picojson::object& driver = tool.at("driver").get();
const picojson::array& rules = driver.at("rules").get();
// Check each rule's security-severity value
for (const auto& rule : rules)
{
const picojson::object& r = rule.get();
const std::string& id = r.at("id").get();
const picojson::object& props = r.at("properties").get();
ASSERT(props.find("security-severity") != props.end());
const std::string& severity = props.at("security-severity").get();
double severityValue = std::stod(severity);
if (id == "error1")
{
// Use a tolerance for floating-point comparison to avoid warning
ASSERT(std::abs(severityValue - 9.9) < 0.01);
}
else if (id == "warning1")
{
ASSERT(std::abs(severityValue - 8.5) < 0.01);
}
else if (id == "style1" || id == "perf1" || id == "port1")
{
ASSERT(std::abs(severityValue - 5.5) < 0.01);
}
else if (id == "info1")
{
ASSERT(std::abs(severityValue - 2.0) < 0.01);
}
}
}
void versionWithSpace()
{
// Test that version strings with spaces are properly truncated
SarifReport report;
report.addFinding(createErrorMessage("testError", Severity::error, "Test error"));
// This test would need a way to inject a version with a space
// The current implementation gets version from CppCheck::version()
// This test verifies the space-trimming logic works
std::string sarif = report.serialize("Cppcheck");
picojson::value json;
ASSERT(parseAndValidateJson(sarif, json));
const picojson::object& root = json.get();
const picojson::array& runs = root.at("runs").get();
const picojson::object& cur_run = runs[0].get();
const picojson::object& tool = cur_run.at("tool").get();
const picojson::object& driver = tool.at("driver").get();
const std::string& version = driver.at("semanticVersion").get();
// Version should not contain any spaces
ASSERT(version.find(' ') == std::string::npos);
}
void customProductNameAndVersion()
{
// Test custom product name that includes version info
SarifReport report;
report.addFinding(createErrorMessage("testError", Severity::error, "Test error"));
// Test with product name that might parse differently
std::string sarif = report.serialize("MyChecker-1.0.0");
picojson::value json;
ASSERT(parseAndValidateJson(sarif, json));
const picojson::object& root = json.get();
const picojson::array& runs = root.at("runs").get();
const picojson::object& cur_run = runs[0].get();
const picojson::object& tool = cur_run.at("tool").get();
const picojson::object& driver = tool.at("driver").get();
// Should have a name (either parsed or default)
ASSERT(driver.find("name") != driver.end());
ASSERT(driver.find("semanticVersion") != driver.end());
}
void normalizeLineColumnToOne()
{
SarifReport report;
// Test with 0 values
ErrorMessage::FileLocation loc0("test.cpp", 0, 0);
ErrorMessage errorMessage0({loc0}, "test.cpp", Severity::error, "Error at 0", "error0", Certainty::normal);
report.addFinding(errorMessage0);
// Test with positive values
ErrorMessage::FileLocation locPos("test.cpp", 10, 5);
ErrorMessage errorMessagePos(
{locPos}, "test.cpp", Severity::error, "Error at positive", "errorPos", Certainty::normal);
report.addFinding(errorMessagePos);
std::string sarif = report.serialize("Cppcheck");
picojson::value json;
ASSERT(parseAndValidateJson(sarif, json));
const picojson::object& root = json.get();
const picojson::array& runs = root.at("runs").get();
const picojson::object& cur_run = runs[0].get();
const picojson::array& results = cur_run.at("results").get();
ASSERT_EQUALS(2U, results.size());
// Check first result with 0,0
const picojson::object& res0 = results[0].get();
const picojson::array& locations0 = res0.at("locations").get();
const picojson::object& loc0_obj = locations0[0].get();
const picojson::object& physLoc0 = loc0_obj.at("physicalLocation").get();
const picojson::object& region0 = physLoc0.at("region").get();
int line0 = static_cast(region0.at("startLine").get());
int column0 = static_cast(region0.at("startColumn").get());
// 0 values should be normalized to 1
ASSERT(line0 == 1);
ASSERT(column0 == 1);
// Check second result with positive values
const picojson::object& res1 = results[1].get();
const picojson::array& locations1 = res1.at("locations").get();
const picojson::object& loc1_obj = locations1[0].get();
const picojson::object& physLoc1 = loc1_obj.at("physicalLocation").get();
const picojson::object& region1 = physLoc1.at("region").get();
ASSERT_EQUALS(10, static_cast(region1.at("startLine").get()));
ASSERT_EQUALS(5, static_cast(region1.at("startColumn").get()));
}
void internalAndDebugSeverity()
{
// Test internal and debug severity levels
// Based on the implementation in sarifSeverity():
// - internal -> error
// - debug -> note
// - none -> note
SarifReport report;
// Create errors with internal and debug severities
ErrorMessage::FileLocation loc1("test.cpp", 10, 5);
ErrorMessage::FileLocation loc2("test.cpp", 20, 10);
ErrorMessage::FileLocation loc3("test.cpp", 30, 15);
ErrorMessage internal(
{loc1}, "test.cpp", Severity::internal, "Internal message", "internalError", Certainty::normal);
ErrorMessage debug({loc2}, "test.cpp", Severity::debug, "Debug message", "debugError", Certainty::normal);
ErrorMessage none({loc3}, "test.cpp", Severity::none, "None message", "noneError", Certainty::normal);
report.addFinding(internal);
report.addFinding(debug);
report.addFinding(none);
std::string sarif = report.serialize("Cppcheck");
picojson::value json;
ASSERT(parseAndValidateJson(sarif, json));
const picojson::object& root = json.get();
const picojson::array& runs = root.at("runs").get();
const picojson::object& cur_run = runs[0].get();
const picojson::array& results = cur_run.at("results").get();
ASSERT_EQUALS(3U, results.size());
// Check the actual mapping
const picojson::object& res0 = results[0].get();
const picojson::object& res1 = results[1].get();
const picojson::object& res2 = results[2].get();
const std::string level0 = res0.at("level").get();
const std::string level1 = res1.at("level").get();
const std::string level2 = res2.at("level").get();
// Actual implementation behavior:
ASSERT_EQUALS("error", level0); // internal -> error
ASSERT_EQUALS("note", level1); // debug -> note
ASSERT_EQUALS("note", level2); // none -> note
}
void problemSeverityMapping()
{
// Test that problem.severity property matches the SARIF severity
SarifReport report;
report.addFinding(createErrorMessage("error1", Severity::error, "Error"));
report.addFinding(createErrorMessage("warning1", Severity::warning, "Warning"));
report.addFinding(createErrorMessage("style1", Severity::style, "Style"));
std::string sarif = report.serialize("Cppcheck");
picojson::value json;
ASSERT(parseAndValidateJson(sarif, json));
const picojson::object& root = json.get();
const picojson::array& runs = root.at("runs").get();
const picojson::object& cur_run = runs[0].get();
const picojson::object& tool = cur_run.at("tool").get();
const picojson::object& driver = tool.at("driver").get();
const picojson::array& rules = driver.at("rules").get();
for (const auto& rule : rules)
{
const picojson::object& r = rule.get();
const picojson::object& props = r.at("properties").get();
const picojson::object& defaultConfig = r.at("defaultConfiguration").get();
// problem.severity should match defaultConfiguration.level
const std::string& problemSeverity = props.at("problem.severity").get();
const std::string& defaultLevel = defaultConfig.at("level").get();
ASSERT_EQUALS(defaultLevel, problemSeverity);
}
}
void mixedLocationAndNoLocation()
{
// Test a mix of findings with and without locations
SarifReport report;
// Add findings with locations
report.addFinding(createErrorMessage("withLoc1", Severity::error, "Error with location", "test.cpp", 10, 5));
report.addFinding(
createErrorMessage("withLoc2", Severity::warning, "Warning with location", "test.cpp", 20, 5));
// Add findings without locations
ErrorMessage noLoc1({}, "test.cpp", Severity::error, "Error without location", "noLoc1", Certainty::normal);
ErrorMessage noLoc2({}, "test.cpp", Severity::warning, "Warning without location", "noLoc2", Certainty::normal);
report.addFinding(noLoc1);
report.addFinding(noLoc2);
// Add more with locations
report.addFinding(createErrorMessage("withLoc3", Severity::style, "Style with location", "test.cpp", 30, 5));
std::string sarif = report.serialize("Cppcheck");
picojson::value json;
ASSERT(parseAndValidateJson(sarif, json));
const picojson::object& root = json.get();
const picojson::array& runs = root.at("runs").get();
const picojson::object& cur_run = runs[0].get();
// Should only have results for findings with locations
const picojson::array& results = cur_run.at("results").get();
ASSERT_EQUALS(3U, results.size());
// Should only have rules for findings with locations
const picojson::object& tool = cur_run.at("tool").get();
const picojson::object& driver = tool.at("driver").get();
const picojson::array& rules = driver.at("rules").get();
ASSERT_EQUALS(3U, rules.size());
// Verify the rule IDs are only for findings with locations
std::set ruleIds;
for (const auto& rule : rules)
{
const picojson::object& r = rule.get();
ruleIds.insert(r.at("id").get());
}
ASSERT(ruleIds.find("withLoc1") != ruleIds.end());
ASSERT(ruleIds.find("withLoc2") != ruleIds.end());
ASSERT(ruleIds.find("withLoc3") != ruleIds.end());
ASSERT(ruleIds.find("noLoc1") == ruleIds.end());
ASSERT(ruleIds.find("noLoc2") == ruleIds.end());
}
};
REGISTER_TEST(TestSarifReport)