/*
* 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 "fixture.h"
#include "errortypes.h"
#include "helpers.h"
#include "library.h"
#include "options.h"
#include "redirect.h"
#include "timer.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include "xml.h"
/**
* TestRegistry
**/
namespace {
struct CompareFixtures {
bool operator()(const TestInstance* lhs, const TestInstance* rhs) const {
return lhs->classname < rhs->classname;
}
};
}
using TestSet = std::set;
namespace {
class TestRegistry {
TestSet _tests;
public:
static TestRegistry &theInstance() {
static TestRegistry testreg;
return testreg;
}
void addTest(TestInstance *t) {
_tests.insert(t);
}
const TestSet &tests() const {
return _tests;
}
};
}
TestInstance::TestInstance(const char * _name)
: classname(_name)
{
TestRegistry::theInstance().addTest(this);
}
/**
* TestFixture
**/
std::ostringstream TestFixture::errmsg;
unsigned int TestFixture::countTests;
std::size_t TestFixture::fails_counter = 0;
std::size_t TestFixture::todos_counter = 0;
std::size_t TestFixture::succeeded_todos_counter = 0;
TestFixture::TestFixture(const char * const _name)
: classname(_name)
{}
TestFixture::~TestFixture() = default;
bool TestFixture::prepareTest(const char testname[])
{
mTemplateFormat.clear();
mTemplateLocation.clear();
prepareTestInternal();
// Check if tests should be executed
if (!testsToRun.empty()) {
const bool match = testsToRun.count(testname);
if ((match && exclude_tests) || (!match && !exclude_tests))
return false;
}
// Tests will be executed - prepare them
mTestname = testname;
++countTests;
std::string fullTestName = classname + "::" + mTestname;
if (quiet_tests) {
std::putchar('.'); // Use putchar to write through redirection of std::cout/cerr
std::fflush(stdout);
} else {
std::cout << fullTestName << std::endl;
}
if (timer_results)
mTimer.reset(new Timer(fullTestName, ShowTime::TOP5_SUMMARY, timer_results));
return !dry_run;
}
void TestFixture::teardownTest()
{
teardownTestInternal();
if (mTimer)
mTimer->stop();
{
const std::string s = errout_str();
if (!s.empty())
throw std::runtime_error("unconsumed ErrorLogger err: " + s);
}
{
const std::string s = output_str();
if (!s.empty())
throw std::runtime_error("unconsumed ErrorLogger out: " + s);
}
}
std::string TestFixture::getLocationStr(const char * const filename, const unsigned int linenr) const
{
return std::string(filename) + ':' + std::to_string(linenr) + '(' + classname + "::" + mTestname + ')';
}
static std::string writestr(const std::string &str, bool gccStyle = false)
{
std::ostringstream ostr;
if (gccStyle)
ostr << '\"';
for (auto i = str.cbegin(); i != str.cend(); ++i) {
if (*i == '\n') {
ostr << "\\n";
if ((i+1) != str.end() && !gccStyle)
ostr << std::endl;
} else if (*i == '\t')
ostr << "\\t";
else if (*i == '\"')
ostr << "\\\"";
else if (std::isprint(static_cast(*i)))
ostr << *i;
else
ostr << "\\x" << std::hex << short{*i};
}
if (!str.empty() && !gccStyle)
ostr << std::endl;
else if (gccStyle)
ostr << '\"';
return ostr.str();
}
void TestFixture::assert_(const char * const filename, const unsigned int linenr, const bool condition, const std::string& msg) const
{
if (!condition) {
++fails_counter;
errmsg << getLocationStr(filename, linenr) << ": Assertion failed." << std::endl << "_____" << std::endl;
if (!msg.empty())
errmsg << "Hint:" << std::endl << msg << std::endl;
throw AssertFailedError();
}
}
void TestFixture::assertFailure(const char* const filename, const unsigned int linenr, const std::string& expected, const std::string& actual, const std::string& msg) const
{
++fails_counter;
errmsg << getLocationStr(filename, linenr) << ": Assertion failed. " << std::endl
<< "Expected: " << std::endl
<< writestr(expected) << std::endl
<< "Actual: " << std::endl
<< writestr(actual) << std::endl;
if (!msg.empty())
errmsg << "Hint:" << std::endl << msg << std::endl;
errmsg << "_____" << std::endl;
throw AssertFailedError();
}
void TestFixture::assertEquals(const char * const filename, const unsigned int linenr, const std::string &expected, const std::string &actual, const std::string &msg) const
{
if (expected != actual) {
assertFailure(filename, linenr, expected, actual, msg);
}
}
std::string TestFixture::deleteLineNumber(const std::string &message)
{
std::string result(message);
// delete line number in "...:NUMBER:..."
std::string::size_type pos = 0;
while ((pos = result.find(':', pos)) != std::string::npos) {
// get number
if (pos + 1 == result.find_first_of("0123456789", pos + 1)) {
const std::string::size_type after = result.find_first_not_of("0123456789", pos + 1);
if (after != std::string::npos
&& result.at(after) == ':') {
// erase NUMBER
result.erase(pos + 1, after - pos - 1);
pos = after;
} else {
++pos;
}
} else {
++pos;
}
}
return result;
}
void TestFixture::assertEqualsWithoutLineNumbers(const char * const filename, const unsigned int linenr, const std::string &expected, const std::string &actual, const std::string &msg) const
{
assertEquals(filename, linenr, deleteLineNumber(expected), deleteLineNumber(actual), msg);
}
void TestFixture::assertEquals(const char * const filename, const unsigned int linenr, const char expected[], const std::string& actual, const std::string &msg) const
{
assertEquals(filename, linenr, std::string(expected), actual, msg);
}
void TestFixture::assertEquals(const char * const filename, const unsigned int linenr, const char expected[], const char actual[], const std::string &msg) const
{
assertEquals(filename, linenr, std::string(expected), std::string(actual), msg);
}
void TestFixture::assertEquals(const char * const filename, const unsigned int linenr, const std::string& expected, const char actual[], const std::string &msg) const
{
assertEquals(filename, linenr, expected, std::string(actual), msg);
}
void TestFixture::assertEquals(const char * const filename, const unsigned int linenr, const long long expected, const long long actual, const std::string &msg) const
{
if (expected != actual) {
assertEquals(filename, linenr, std::to_string(expected), std::to_string(actual), msg);
}
}
void TestFixture::assertEqualsDouble(const char * const filename, const unsigned int linenr, const double expected, const double actual, const double tolerance, const std::string &msg) const
{
if (expected < (actual - tolerance) || expected > (actual + tolerance)) {
std::ostringstream ostr1;
ostr1 << expected;
std::ostringstream ostr2;
ostr2 << actual;
assertEquals(filename, linenr, ostr1.str(), ostr2.str(), msg);
}
}
void TestFixture::todoAssertEquals(const char * const filename, const unsigned int linenr,
const std::string &wanted,
const std::string ¤t,
const std::string &actual) const
{
if (wanted == actual) {
errmsg << getLocationStr(filename, linenr) << ": Assertion succeeded unexpectedly. "
<< "Result: " << writestr(wanted, true) << std::endl << "_____" << std::endl;
++succeeded_todos_counter;
} else {
assertEquals(filename, linenr, current, actual);
++todos_counter;
}
}
void TestFixture::todoAssertEquals(const char* const filename, const unsigned int linenr,
const char wanted[],
const char current[],
const std::string& actual) const
{
todoAssertEquals(filename, linenr, std::string(wanted), std::string(current), actual);
}
void TestFixture::todoAssertEquals(const char * const filename, const unsigned int linenr, const long long wanted, const long long current, const long long actual) const
{
todoAssertEquals(filename, linenr, std::to_string(wanted), std::to_string(current), std::to_string(actual));
}
void TestFixture::assertThrow(const char * const filename, const unsigned int linenr) const
{
++fails_counter;
errmsg << getLocationStr(filename, linenr) << ": Assertion succeeded. "
<< "The expected exception was thrown" << std::endl << "_____" << std::endl;
throw AssertFailedError();
}
void TestFixture::assertThrowFail(const char * const filename, const unsigned int linenr) const
{
++fails_counter;
errmsg << getLocationStr(filename, linenr) << ": Assertion failed. "
<< "The expected exception was not thrown" << std::endl << "_____" << std::endl;
throw AssertFailedError();
}
void TestFixture::assertNoThrowFail(const char * const filename, const unsigned int linenr, bool bailout) const
{
std::string ex_msg;
try {
// cppcheck-suppress rethrowNoCurrentException
throw;
}
catch (const AssertFailedError&) {
return;
}
catch (const InternalError& e) {
ex_msg = e.errorMessage;
}
catch (const std::exception& e) {
ex_msg = e.what();
}
catch (...) {
ex_msg = "unknown exception";
}
++fails_counter;
errmsg << getLocationStr(filename, linenr) << ": Assertion failed. "
<< "Unexpected exception was thrown: " << ex_msg << std::endl << "_____" << std::endl;
if (bailout)
throw AssertFailedError();
}
void TestFixture::printHelp()
{
std::cout << "Testrunner - run Cppcheck tests\n"
"\n"
"Syntax:\n"
" testrunner [OPTIONS] [TestClass::TestCase...]\n"
" run all test cases:\n"
" testrunner\n"
" run all test cases in TestClass:\n"
" testrunner TestClass\n"
" run TestClass::TestCase:\n"
" testrunner TestClass::TestCase\n"
" run all test cases in TestClass1 and TestClass2::TestCase:\n"
" testrunner TestClass1 TestClass2::TestCase\n"
"\n"
"Options:\n"
" -q Do not print the test cases that have run.\n"
" -h, --help Print this help.\n"
" -n Print no summaries.\n"
" -d Do not execute any tests (dry run).\n"
" -x Exclude the specified tests.\n";
}
void TestFixture::run(const std::set &tests)
{
testsToRun = tests;
try {
if (quiet_tests) {
std::cout << '\n' << classname << ':';
SUPPRESS;
run();
}
else
run();
}
catch (const InternalError& e) {
++fails_counter;
errmsg << classname << "::" << mTestname << " - InternalError: " << e.errorMessage << std::endl;
}
catch (const std::exception& error) {
++fails_counter;
errmsg << classname << "::" << mTestname << " - Exception: " << error.what() << std::endl;
}
catch (...) {
++fails_counter;
errmsg << classname << "::" << mTestname << " - Unknown exception" << std::endl;
}
}
void TestFixture::processOptions(const options& args)
{
quiet_tests = args.quiet();
dry_run = args.dry_run();
exclude_tests = args.exclude_tests();
exename = args.exe();
timer_results = args.timer_results();
}
std::size_t TestFixture::runTests(const options& args)
{
countTests = 0;
errmsg.str("");
const auto& which_tests = args.which_tests();
const auto exclude_tests = args.exclude_tests();
// TODO: bail out when given class/test is not found?
for (TestInstance * test : TestRegistry::theInstance().tests())
{
std::set tests;
if (!which_tests.empty()) {
const auto it = which_tests.find(test->classname);
const bool match = it != which_tests.cend();
if (match && exclude_tests && it->second.empty()) // only bailout when the whole fixture is excluded
continue;
if (!match && !exclude_tests)
continue;
if (match)
tests = it->second;
}
TestFixture* fixture;
const auto f = [&](){
fixture = test->create();
};
// TODO: Timer::run() needs proper handling if no results should be collected
if (args.timer_results())
Timer::run(test->classname + " - create", ShowTime::TOP5_SUMMARY, args.timer_results(), f);
else
f();
fixture->processOptions(args);
fixture->run(tests);
}
if (args.summary() && !args.dry_run()) {
std::cout << "\n\nTesting Complete\nNumber of tests: " << countTests << std::endl;
std::cout << "Number of todos: " << todos_counter;
if (succeeded_todos_counter > 0)
std::cout << " (" << succeeded_todos_counter << " succeeded)";
std::cout << std::endl;
}
// calling flush here, to do all output before the error messages (in case the output is buffered)
std::cout.flush();
if (args.summary() && !args.dry_run()) {
std::cerr << "Tests failed: " << fails_counter << std::endl << std::endl;
}
std::cerr << errmsg.str();
std::cerr.flush();
return fails_counter + succeeded_todos_counter;
}
void TestFixture::reportOut(const std::string & outmsg, Color /*c*/)
{
mOutput << outmsg << std::endl;
}
void TestFixture::reportErr(const ErrorMessage &msg)
{
if (msg.severity == Severity::internal)
return;
if (msg.severity == Severity::information && msg.id == "normalCheckLevelMaxBranches")
return;
std::string errormessage;
if (!mTemplateFormat.empty()) {
errormessage = msg.toString(false, mTemplateFormat, mTemplateLocation);
}
else {
if (!msg.callStack.empty()) {
errormessage += ErrorLogger::callStackToString(msg.callStack, mNewTemplate);
errormessage += ": ";
}
if (msg.severity != Severity::none) {
errormessage += '(';
errormessage += severityToString(msg.severity);
if (msg.certainty == Certainty::inconclusive)
errormessage += ", inconclusive";
errormessage += ") ";
}
errormessage += msg.shortMessage();
if (mNewTemplate) {
errormessage += " [";
errormessage += msg.id;
errormessage += "]";
}
}
mErrout << errormessage << std::endl;
}
void TestFixture::setTemplateFormat(const std::string &templateFormat)
{
if (templateFormat == "multiline") {
mTemplateFormat = "[{file}:{line}:{column}]: {severity}:{inconclusive:inconclusive:} {message} [{id}]";
mTemplateLocation = "[{file}:{line}:{column}]: note: {info}";
}
else if (templateFormat == "simple") { // TODO: use the existing one in CmdLineParser
mTemplateFormat = "{file}:{line}:{column}: {severity}:{inconclusive:inconclusive:} {message} [{id}]";
mTemplateLocation = "";
}
else {
mTemplateFormat = templateFormat;
mTemplateLocation = "";
}
}
TestFixture::SettingsBuilder& TestFixture::SettingsBuilder::checkLevel(Settings::CheckLevel level) {
settings.setCheckLevel(level);
return *this;
}
TestFixture::SettingsBuilder& TestFixture::SettingsBuilder::library(const char lib[]) {
if (REDUNDANT_CHECK && std::find(settings.libraries.cbegin(), settings.libraries.cend(), lib) != settings.libraries.cend())
throw std::runtime_error("redundant setting: libraries (" + std::string(lib) + ")");
// TODO: exename is not yet set
const Library::ErrorCode lib_error = settings.library.load(fixture.exename.c_str(), lib).errorcode;
if (lib_error != Library::ErrorCode::OK)
throw std::runtime_error("loading library '" + std::string(lib) + "' failed - " + std::to_string(static_cast(lib_error)));
// strip extension
std::string lib_s(lib);
const std::string ext(".cfg");
const auto pos = lib_s.find(ext);
if (pos != std::string::npos)
lib_s.erase(pos, ext.size());
settings.libraries.emplace_back(lib_s);
return *this;
}
TestFixture::SettingsBuilder& TestFixture::SettingsBuilder::platform(Platform::Type type)
{
const std::string platformStr = Platform::toString(type);
if (REDUNDANT_CHECK && settings.platform.type == type)
throw std::runtime_error("redundant setting: platform (" + platformStr + ")");
std::string errstr;
// TODO: exename is not yet set
if (!settings.platform.set(platformStr, errstr, {fixture.exename}))
throw std::runtime_error("platform '" + platformStr + "' not found");
return *this;
}
TestFixture::SettingsBuilder& TestFixture::SettingsBuilder::libraryxml(const char xmldata[], std::size_t len)
{
tinyxml2::XMLDocument doc;
const tinyxml2::XMLError xml_error = doc.Parse(xmldata, len);
if (tinyxml2::XML_SUCCESS != xml_error)
throw std::runtime_error(std::string("loading library XML data failed - ") + tinyxml2::XMLDocument::ErrorIDToName(xml_error));
const Library::ErrorCode lib_error = LibraryHelper::loadxmldoc(settings.library, doc).errorcode;
if (lib_error != Library::ErrorCode::OK)
throw std::runtime_error("loading library XML failed - " + std::to_string(static_cast(lib_error)));
return *this;
}