Skip to content

Commit

Permalink
Merge pull request #242 from eseiler/test/use_vector
Browse files Browse the repository at this point in the history
[TEST] Use a test fixture
  • Loading branch information
eseiler authored Feb 20, 2024
2 parents 88336e5 + 87ddfbf commit 31d3207
Show file tree
Hide file tree
Showing 14 changed files with 2,341 additions and 3,001 deletions.
2 changes: 2 additions & 0 deletions test/coverage/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ sharg_append_gcovr_args ("--exclude-lines-by-pattern" [['^\\s*[{}]{0,2}\\s*;*\\s
sharg_append_gcovr_args ("--exclude-unreachable-branches")
# Will exclude branches that are only generated for exception handling.
sharg_append_gcovr_args ("--exclude-throw-branches")
# Will exclude non-code lines, e.g. lines with closing braces.
sharg_append_gcovr_args ("--exclude-noncode-lines")
# Run up to this many gcov instances in parallel.
sharg_append_gcovr_args ("-j" "${SHARG_COVERAGE_PARALLEL_LEVEL}")

Expand Down
215 changes: 215 additions & 0 deletions test/include/sharg/test/test_fixture.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
// SPDX-FileCopyrightText: 2006-2024, Knut Reinert & Freie Universität Berlin
// SPDX-FileCopyrightText: 2016-2024, Knut Reinert & MPI für molekulare Genetik
// SPDX-License-Identifier: BSD-3-Clause

/*!\file
* \brief Provides sharg::test::test_fixture.
* \author Enrico Seiler <enrico.seiler AT fu-berlin.de>
*/

#pragma once

#include <gtest/gtest.h>

#include <sharg/parser.hpp>

namespace sharg::detail
{

struct test_accessor
{
static void set_terminal_width(sharg::parser & parser, unsigned terminal_width)
{
auto visit_fn = [terminal_width]<typename format_t>(format_t & format)
{
if constexpr (std::same_as<format_t, sharg::detail::format_help>)
format.layout = sharg::detail::format_help::console_layout_struct{terminal_width};
};

std::visit(std::move(visit_fn), parser.format);
}

static std::vector<std::string> & executable_name(sharg::parser & parser)
{
return parser.executable_name;
}

static auto & version_check_future(sharg::parser & parser)
{
return parser.version_check_future;
}
};

} // namespace sharg::detail

namespace sharg::test
{

class test_fixture : public ::testing::Test
{
private:
friend class early_exit_guardian;

static sharg::parser impl(std::vector<std::string> arguments, std::vector<std::string> subcommands = {})
{
sharg::parser parser{"test_parser",
std::move(arguments),
sharg::update_notifications::off,
std::move(subcommands)};
sharg::detail::test_accessor::set_terminal_width(parser, 80u);
return parser;
}

static void toggle_guardian();

protected:
template <typename... arg_ts>
static sharg::parser get_parser(arg_ts &&... arguments)
{
return impl(std::vector<std::string>{"./test_parser", std::forward<arg_ts>(arguments)...});
}

static sharg::parser get_subcommand_parser(std::vector<std::string> arguments, std::vector<std::string> subcommands)
{
arguments.insert(arguments.begin(), "./test_parser");
return impl(std::move(arguments), std::move(subcommands));
}

static std::string get_parse_cout_on_exit(sharg::parser & parser)
{
testing::internal::CaptureStdout();
// EXPECT_EXIT will create a new thread via clone() and the destructor of the cloned early_exit_guardian will
// be called. So we need to toggle the guardian to prevent the check inside the cloned thread, and toggle
// it back after the EXPECT_EXIT call.
toggle_guardian();
EXPECT_EXIT(parser.parse(), ::testing::ExitedWithCode(EXIT_SUCCESS), "");
toggle_guardian();
return testing::internal::GetCapturedStdout();
}
};

class early_exit_guardian
{
public:
early_exit_guardian()
{
::testing::AddGlobalTestEnvironment(new test_environment{this});
}
early_exit_guardian(early_exit_guardian const &) = delete;
early_exit_guardian(early_exit_guardian &&) = delete;
early_exit_guardian & operator=(early_exit_guardian const &) = delete;
early_exit_guardian & operator=(early_exit_guardian &&) = delete;

~early_exit_guardian()
{
restore_stderr();
check_all_tests_ran();
}

private:
//!\brief test_fixture::toggle_guardian is allowed to toggle `active` for `EXPECT_EXIT` tests.
friend void test_fixture::toggle_guardian();
//!\brief Flag to indicate that all tests are done.
bool active{false};
//!\brief Original file descriptor of stderr.
int const stored_fd = dup(2);

/*!\brief Pointer to the current test unit.
* GetInstance() creates the object on the first call and just return that pointer on subsequent calls (static).
* It is important that this first call takes place on the "main" thread, and not in the destructor call.
*/
testing::UnitTest const * const unit_test = testing::UnitTest::GetInstance();

void activate()
{
assert(!active);
active = true;
}

void deactivate()
{
assert(active);
active = false;
}

/*!\brief Restore the original file descriptor of stderr.
* testing::internal::CaptureStderr() manipulates the file descriptor of stderr.
* There is no API (neither exposed nor internal) to check whether stderr is captured or not, and
* testing::internal::GetCapturedStderr() is UB if stderr is not captured.
* Therefore, we restore the original file descriptor of stderr by ourself.
*/
void restore_stderr() const
{
dup2(stored_fd, 2);
}

/*!\brief Check if all tests were run.
* Exits with EXIT_FAILURE if `sharg::test::early_exit_guard.deactivate()` was not called.
*/
void check_all_tests_ran() const
{
if (!active)
return;

// LCOV_EXCL_START
std::cerr << "\nNot all test cases were run!\n"
<< "The following test unexpectedly terminated the execution:\n"
<< get_current_test_name() << '\n';

std::exit(EXIT_FAILURE);
// LCOV_EXCL_STOP
}

// LCOV_EXCL_START
std::string get_current_test_name() const
{
assert(unit_test && "This should never be a nullptr?!");

std::string result{};

if (::testing::TestSuite const * const test_suite = unit_test->current_test_suite())
{
result = test_suite->name();
result += '.';
}

if (::testing::TestInfo const * const test_info = unit_test->current_test_info())
result += test_info->name();

return result;
}
// LCOV_EXCL_STOP

// See https://github.com/google/googletest/blob/main/docs/advanced.md#global-set-up-and-tear-down
class test_environment : public ::testing::Environment
{
private:
friend class early_exit_guardian;
early_exit_guardian * guardian{nullptr};

test_environment(early_exit_guardian * guardian) : guardian{guardian}
{}

public:
void SetUp() override
{
assert(guardian);
guardian->activate();
}

void TearDown() override
{
assert(guardian);
guardian->deactivate();
}
};
};

early_exit_guardian early_exit_guard{};

inline void test_fixture::toggle_guardian()
{
early_exit_guard.active = !early_exit_guard.active;
}

} // namespace sharg::test
44 changes: 19 additions & 25 deletions test/unit/detail/format_ctd_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,22 @@
#include <gtest/gtest.h>

#include <sharg/parser.hpp>
#include <sharg/test/test_fixture.hpp>

#if !SHARG_HAS_TDL
TEST(format_ctd_test, skipped)
{
GTEST_SKIP() << "TDL is not available.";
}
#else
// Reused global variables
struct format_ctd_test : public ::testing::Test
class format_ctd_test : public sharg::test::test_fixture
{
protected:
int option_value{5};
bool flag_value{false};
int8_t non_list_pos_opt_value{1};
std::vector<std::string> list_pos_opt_value{};
std::string my_stdout{};
static constexpr std::array argv{"./format_ctd_test", "--version-check", "false", "--export-help", "ctd"};
std::string const version_str{sharg::sharg_version_cstring};
std::string expected =
std::string const expected =
R"del(<?xml version="1.0" encoding="UTF-8"?>)del"
"\n"
R"del(<tool ctdVersion="1.7" version="01.01.01" name="default">)del"
R"del(<tool ctdVersion="1.7" version="01.01.01" name="test_parser">)del"
"\n"
R"del( <description><![CDATA[description)del"
"\n"
Expand All @@ -39,7 +34,7 @@ struct format_ctd_test : public ::testing::Test
"\n"
R"del(]]></manual>)del"
"\n"
R"del( <executableName><![CDATA[./format_ctd_test]]></executableName>)del"
R"del( <executableName><![CDATA[./test_parser]]></executableName>)del"
"\n"
R"del( <citations />)del"
"\n"
Expand Down Expand Up @@ -108,10 +103,16 @@ struct format_ctd_test : public ::testing::Test
}
};

#if !SHARG_HAS_TDL
TEST_F(format_ctd_test, skipped)
{
GTEST_SKIP() << "TDL is not available.";
}
#else
TEST_F(format_ctd_test, empty_information)
{
// Create the dummy parser.
sharg::parser parser{"default", argv.size(), argv.data()};
auto parser = get_parser("--export-help", "ctd");
parser.info.date = "December 01, 1994";
parser.info.version = "1.1.2-rc.1";
parser.info.man_page_title = "default_man_page_title";
Expand All @@ -123,9 +124,9 @@ TEST_F(format_ctd_test, empty_information)
"\n"
R"(<tool ctdVersion="1.7" version=")"
+ version_str
+ R"(" name="default">)"
+ R"(" name="test_parser">)"
"\n"
R"( <executableName><![CDATA[./format_ctd_test]]></executableName>)"
R"( <executableName><![CDATA[./test_parser]]></executableName>)"
"\n"
R"( <citations />)"
"\n"
Expand All @@ -135,25 +136,18 @@ TEST_F(format_ctd_test, empty_information)
"\n";

// Test the dummy parser with minimal information.
testing::internal::CaptureStdout();
EXPECT_EXIT(parser.parse(), ::testing::ExitedWithCode(EXIT_SUCCESS), "");

my_stdout = testing::internal::GetCapturedStdout();
EXPECT_EQ(my_stdout, expected_short);
EXPECT_EQ(get_parse_cout_on_exit(parser), expected_short);
}

TEST_F(format_ctd_test, full_information)
{
// Create the dummy parser.
sharg::parser parser{"default", argv.size(), argv.data()};
auto parser = get_parser("--export-help", "ctd");

// Fill out the dummy parser with options and flags and sections and subsections.
dummy_init(parser);
// Test the dummy parser without any copyright or citations.
testing::internal::CaptureStdout();
EXPECT_EXIT(parser.parse(), ::testing::ExitedWithCode(EXIT_SUCCESS), "");

my_stdout = testing::internal::GetCapturedStdout();
EXPECT_EQ(my_stdout, expected);
// Test the dummy parser without any copyright or citations.
EXPECT_EQ(get_parse_cout_on_exit(parser), expected);
}
#endif
Loading

0 comments on commit 31d3207

Please sign in to comment.