icinga2/test/test-ctest.cpp
Johannes Schmidt e11aad842a Discover Boost test cases automatically after build
This adds a global fixture that can parse an additional argument to
the test executables (`--generate_ctest_config`). When run by
CMake during build, this generates a CTest script containing all
the tests and their properties.

An additional decorator, that defines CTest properties for a test case
or suite that will be added to the tests during config generation.

This version needs no hacks, no huge CMake scripts, just a bit of
additional C++ code that iterates over all test-cases and collects
the information CTest needs.

One caveat is still that this does not work with cross-compilation,
which probably isn't an issue to begin with, but there are also ways
to fix that if necessary.
2025-08-19 09:24:42 +02:00

132 lines
3.4 KiB
C++

#include "test-ctest.hpp"
#include <boost/regex.hpp>
#include <iostream>
using namespace icinga;
BOOST_TEST_GLOBAL_FIXTURE(CTestFileGeneratorFixture);
boost::unit_test::decorator::base_ptr CTestProperties::clone() const
{
return boost::unit_test::decorator::base_ptr(new CTestProperties(m_Props));
}
CTestFileGenerator::CTestFileGenerator(const boost::filesystem::path& testexe, const boost::filesystem::path& outfile)
: m_Fp(outfile.string(), std::iostream::trunc), m_WorkDir(outfile.parent_path().string()),
m_TestExe(testexe.string())
{
m_Fp.exceptions(std::iostream::badbit | std::iostream::failbit);
}
void CTestFileGenerator::visit(boost::unit_test::test_case const& test)
{
std::vector<std::string> flatProps;
auto labels = test.p_labels.get();
if (!labels.empty()) {
flatProps.push_back(LabelsToProp(labels));
}
for (auto& props : m_PropsStack) {
for (auto& prop : props) {
flatProps.emplace_back(prop);
}
}
if (!test.is_enabled()) {
flatProps.emplace_back("DISABLED");
}
auto decs = test.p_decorators.get();
for (auto& dec : decs) {
auto ctp = boost::dynamic_pointer_cast<CTestProperties>(dec);
if (ctp) {
flatProps.emplace_back(ctp->m_Props);
}
}
m_Fp << "add_test(" << test.full_name() << " " << m_TestExe << " --run_test=" << test.full_name() << ")\n";
m_Fp << "set_tests_properties(" << test.full_name() << "\n PROPERTIES"
<< " WORKING_DIRECTORY " << m_WorkDir << "\n";
if (!flatProps.empty()) {
for (auto& prop : flatProps) {
m_Fp << " " << prop << "\n";
}
}
m_Fp << ")\n";
m_Fp.flush();
}
bool CTestFileGenerator::test_suite_start(boost::unit_test::test_suite const& suite)
{
m_PropsStack.emplace_back();
auto decs = suite.p_decorators.get();
for (auto& dec : decs) {
auto ctp = boost::dynamic_pointer_cast<CTestProperties>(dec);
if (ctp) {
m_PropsStack.back().push_back(ctp->m_Props);
}
}
auto labels = suite.p_labels.get();
if (!labels.empty()) {
m_PropsStack.back().push_back(LabelsToProp(labels));
}
return true;
}
void CTestFileGenerator::test_suite_finish(boost::unit_test::test_suite const&)
{
m_PropsStack.pop_back();
}
std::string CTestFileGenerator::LabelsToProp(std::vector<std::string> labels)
{
std::string labelsProp{"LABELS \""};
for (auto it = labels.begin(); it != labels.end(); ++it) {
if (it != labels.begin()) {
labelsProp.push_back(';');
}
for (std::size_t pos = it->find_first_of(';'); pos != std::string::npos; pos = it->find_first_of(';', pos)) {
it->replace(pos, 1, "\\;");
pos += 2;
}
labelsProp.append(*it);
}
labelsProp.push_back('"');
return labelsProp;
}
CTestFileGeneratorFixture::CTestFileGeneratorFixture()
{
auto& mts = boost::unit_test::framework::master_test_suite();
int argc = mts.argc;
for (int i = 1; i < argc; i++) {
std::string argument(mts.argv[i]);
if (argument == "--generate_ctest_config") {
if (argc >= i + 1) {
boost::filesystem::path testbin(mts.argv[0]);
boost::filesystem::path file(mts.argv[i + 1]);
try {
CTestFileGenerator visitor{testbin, file};
traverse_test_tree(mts, visitor);
} catch (const std::exception& ex) {
std::cerr << "Error: --generate_ctest_config failed with: " << ex.what() << '\n';
std::_Exit(EXIT_FAILURE);
}
} else {
std::cerr << "Error: --generate_ctest_config specified with no argument.\n";
std::_Exit(EXIT_FAILURE);
}
std::_Exit(EXIT_SUCCESS);
}
}
}