Skip to content

Commit 31d3207

Browse files
authored
Merge pull request #242 from eseiler/test/use_vector
[TEST] Use a test fixture
2 parents 88336e5 + 87ddfbf commit 31d3207

14 files changed

+2341
-3001
lines changed

test/coverage/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ sharg_append_gcovr_args ("--exclude-lines-by-pattern" [['^\\s*[{}]{0,2}\\s*;*\\s
6666
sharg_append_gcovr_args ("--exclude-unreachable-branches")
6767
# Will exclude branches that are only generated for exception handling.
6868
sharg_append_gcovr_args ("--exclude-throw-branches")
69+
# Will exclude non-code lines, e.g. lines with closing braces.
70+
sharg_append_gcovr_args ("--exclude-noncode-lines")
6971
# Run up to this many gcov instances in parallel.
7072
sharg_append_gcovr_args ("-j" "${SHARG_COVERAGE_PARALLEL_LEVEL}")
7173

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
// SPDX-FileCopyrightText: 2006-2024, Knut Reinert & Freie Universität Berlin
2+
// SPDX-FileCopyrightText: 2016-2024, Knut Reinert & MPI für molekulare Genetik
3+
// SPDX-License-Identifier: BSD-3-Clause
4+
5+
/*!\file
6+
* \brief Provides sharg::test::test_fixture.
7+
* \author Enrico Seiler <enrico.seiler AT fu-berlin.de>
8+
*/
9+
10+
#pragma once
11+
12+
#include <gtest/gtest.h>
13+
14+
#include <sharg/parser.hpp>
15+
16+
namespace sharg::detail
17+
{
18+
19+
struct test_accessor
20+
{
21+
static void set_terminal_width(sharg::parser & parser, unsigned terminal_width)
22+
{
23+
auto visit_fn = [terminal_width]<typename format_t>(format_t & format)
24+
{
25+
if constexpr (std::same_as<format_t, sharg::detail::format_help>)
26+
format.layout = sharg::detail::format_help::console_layout_struct{terminal_width};
27+
};
28+
29+
std::visit(std::move(visit_fn), parser.format);
30+
}
31+
32+
static std::vector<std::string> & executable_name(sharg::parser & parser)
33+
{
34+
return parser.executable_name;
35+
}
36+
37+
static auto & version_check_future(sharg::parser & parser)
38+
{
39+
return parser.version_check_future;
40+
}
41+
};
42+
43+
} // namespace sharg::detail
44+
45+
namespace sharg::test
46+
{
47+
48+
class test_fixture : public ::testing::Test
49+
{
50+
private:
51+
friend class early_exit_guardian;
52+
53+
static sharg::parser impl(std::vector<std::string> arguments, std::vector<std::string> subcommands = {})
54+
{
55+
sharg::parser parser{"test_parser",
56+
std::move(arguments),
57+
sharg::update_notifications::off,
58+
std::move(subcommands)};
59+
sharg::detail::test_accessor::set_terminal_width(parser, 80u);
60+
return parser;
61+
}
62+
63+
static void toggle_guardian();
64+
65+
protected:
66+
template <typename... arg_ts>
67+
static sharg::parser get_parser(arg_ts &&... arguments)
68+
{
69+
return impl(std::vector<std::string>{"./test_parser", std::forward<arg_ts>(arguments)...});
70+
}
71+
72+
static sharg::parser get_subcommand_parser(std::vector<std::string> arguments, std::vector<std::string> subcommands)
73+
{
74+
arguments.insert(arguments.begin(), "./test_parser");
75+
return impl(std::move(arguments), std::move(subcommands));
76+
}
77+
78+
static std::string get_parse_cout_on_exit(sharg::parser & parser)
79+
{
80+
testing::internal::CaptureStdout();
81+
// EXPECT_EXIT will create a new thread via clone() and the destructor of the cloned early_exit_guardian will
82+
// be called. So we need to toggle the guardian to prevent the check inside the cloned thread, and toggle
83+
// it back after the EXPECT_EXIT call.
84+
toggle_guardian();
85+
EXPECT_EXIT(parser.parse(), ::testing::ExitedWithCode(EXIT_SUCCESS), "");
86+
toggle_guardian();
87+
return testing::internal::GetCapturedStdout();
88+
}
89+
};
90+
91+
class early_exit_guardian
92+
{
93+
public:
94+
early_exit_guardian()
95+
{
96+
::testing::AddGlobalTestEnvironment(new test_environment{this});
97+
}
98+
early_exit_guardian(early_exit_guardian const &) = delete;
99+
early_exit_guardian(early_exit_guardian &&) = delete;
100+
early_exit_guardian & operator=(early_exit_guardian const &) = delete;
101+
early_exit_guardian & operator=(early_exit_guardian &&) = delete;
102+
103+
~early_exit_guardian()
104+
{
105+
restore_stderr();
106+
check_all_tests_ran();
107+
}
108+
109+
private:
110+
//!\brief test_fixture::toggle_guardian is allowed to toggle `active` for `EXPECT_EXIT` tests.
111+
friend void test_fixture::toggle_guardian();
112+
//!\brief Flag to indicate that all tests are done.
113+
bool active{false};
114+
//!\brief Original file descriptor of stderr.
115+
int const stored_fd = dup(2);
116+
117+
/*!\brief Pointer to the current test unit.
118+
* GetInstance() creates the object on the first call and just return that pointer on subsequent calls (static).
119+
* It is important that this first call takes place on the "main" thread, and not in the destructor call.
120+
*/
121+
testing::UnitTest const * const unit_test = testing::UnitTest::GetInstance();
122+
123+
void activate()
124+
{
125+
assert(!active);
126+
active = true;
127+
}
128+
129+
void deactivate()
130+
{
131+
assert(active);
132+
active = false;
133+
}
134+
135+
/*!\brief Restore the original file descriptor of stderr.
136+
* testing::internal::CaptureStderr() manipulates the file descriptor of stderr.
137+
* There is no API (neither exposed nor internal) to check whether stderr is captured or not, and
138+
* testing::internal::GetCapturedStderr() is UB if stderr is not captured.
139+
* Therefore, we restore the original file descriptor of stderr by ourself.
140+
*/
141+
void restore_stderr() const
142+
{
143+
dup2(stored_fd, 2);
144+
}
145+
146+
/*!\brief Check if all tests were run.
147+
* Exits with EXIT_FAILURE if `sharg::test::early_exit_guard.deactivate()` was not called.
148+
*/
149+
void check_all_tests_ran() const
150+
{
151+
if (!active)
152+
return;
153+
154+
// LCOV_EXCL_START
155+
std::cerr << "\nNot all test cases were run!\n"
156+
<< "The following test unexpectedly terminated the execution:\n"
157+
<< get_current_test_name() << '\n';
158+
159+
std::exit(EXIT_FAILURE);
160+
// LCOV_EXCL_STOP
161+
}
162+
163+
// LCOV_EXCL_START
164+
std::string get_current_test_name() const
165+
{
166+
assert(unit_test && "This should never be a nullptr?!");
167+
168+
std::string result{};
169+
170+
if (::testing::TestSuite const * const test_suite = unit_test->current_test_suite())
171+
{
172+
result = test_suite->name();
173+
result += '.';
174+
}
175+
176+
if (::testing::TestInfo const * const test_info = unit_test->current_test_info())
177+
result += test_info->name();
178+
179+
return result;
180+
}
181+
// LCOV_EXCL_STOP
182+
183+
// See https://github.com/google/googletest/blob/main/docs/advanced.md#global-set-up-and-tear-down
184+
class test_environment : public ::testing::Environment
185+
{
186+
private:
187+
friend class early_exit_guardian;
188+
early_exit_guardian * guardian{nullptr};
189+
190+
test_environment(early_exit_guardian * guardian) : guardian{guardian}
191+
{}
192+
193+
public:
194+
void SetUp() override
195+
{
196+
assert(guardian);
197+
guardian->activate();
198+
}
199+
200+
void TearDown() override
201+
{
202+
assert(guardian);
203+
guardian->deactivate();
204+
}
205+
};
206+
};
207+
208+
early_exit_guardian early_exit_guard{};
209+
210+
inline void test_fixture::toggle_guardian()
211+
{
212+
early_exit_guard.active = !early_exit_guard.active;
213+
}
214+
215+
} // namespace sharg::test

test/unit/detail/format_ctd_test.cpp

Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,22 @@
55
#include <gtest/gtest.h>
66

77
#include <sharg/parser.hpp>
8+
#include <sharg/test/test_fixture.hpp>
89

9-
#if !SHARG_HAS_TDL
10-
TEST(format_ctd_test, skipped)
11-
{
12-
GTEST_SKIP() << "TDL is not available.";
13-
}
14-
#else
1510
// Reused global variables
16-
struct format_ctd_test : public ::testing::Test
11+
class format_ctd_test : public sharg::test::test_fixture
1712
{
13+
protected:
1814
int option_value{5};
1915
bool flag_value{false};
2016
int8_t non_list_pos_opt_value{1};
2117
std::vector<std::string> list_pos_opt_value{};
2218
std::string my_stdout{};
23-
static constexpr std::array argv{"./format_ctd_test", "--version-check", "false", "--export-help", "ctd"};
2419
std::string const version_str{sharg::sharg_version_cstring};
25-
std::string expected =
20+
std::string const expected =
2621
R"del(<?xml version="1.0" encoding="UTF-8"?>)del"
2722
"\n"
28-
R"del(<tool ctdVersion="1.7" version="01.01.01" name="default">)del"
23+
R"del(<tool ctdVersion="1.7" version="01.01.01" name="test_parser">)del"
2924
"\n"
3025
R"del( <description><![CDATA[description)del"
3126
"\n"
@@ -39,7 +34,7 @@ struct format_ctd_test : public ::testing::Test
3934
"\n"
4035
R"del(]]></manual>)del"
4136
"\n"
42-
R"del( <executableName><![CDATA[./format_ctd_test]]></executableName>)del"
37+
R"del( <executableName><![CDATA[./test_parser]]></executableName>)del"
4338
"\n"
4439
R"del( <citations />)del"
4540
"\n"
@@ -108,10 +103,16 @@ struct format_ctd_test : public ::testing::Test
108103
}
109104
};
110105

106+
#if !SHARG_HAS_TDL
107+
TEST_F(format_ctd_test, skipped)
108+
{
109+
GTEST_SKIP() << "TDL is not available.";
110+
}
111+
#else
111112
TEST_F(format_ctd_test, empty_information)
112113
{
113114
// Create the dummy parser.
114-
sharg::parser parser{"default", argv.size(), argv.data()};
115+
auto parser = get_parser("--export-help", "ctd");
115116
parser.info.date = "December 01, 1994";
116117
parser.info.version = "1.1.2-rc.1";
117118
parser.info.man_page_title = "default_man_page_title";
@@ -123,9 +124,9 @@ TEST_F(format_ctd_test, empty_information)
123124
"\n"
124125
R"(<tool ctdVersion="1.7" version=")"
125126
+ version_str
126-
+ R"(" name="default">)"
127+
+ R"(" name="test_parser">)"
127128
"\n"
128-
R"( <executableName><![CDATA[./format_ctd_test]]></executableName>)"
129+
R"( <executableName><![CDATA[./test_parser]]></executableName>)"
129130
"\n"
130131
R"( <citations />)"
131132
"\n"
@@ -135,25 +136,18 @@ TEST_F(format_ctd_test, empty_information)
135136
"\n";
136137

137138
// Test the dummy parser with minimal information.
138-
testing::internal::CaptureStdout();
139-
EXPECT_EXIT(parser.parse(), ::testing::ExitedWithCode(EXIT_SUCCESS), "");
140-
141-
my_stdout = testing::internal::GetCapturedStdout();
142-
EXPECT_EQ(my_stdout, expected_short);
139+
EXPECT_EQ(get_parse_cout_on_exit(parser), expected_short);
143140
}
144141

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

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

156-
my_stdout = testing::internal::GetCapturedStdout();
157-
EXPECT_EQ(my_stdout, expected);
150+
// Test the dummy parser without any copyright or citations.
151+
EXPECT_EQ(get_parse_cout_on_exit(parser), expected);
158152
}
159153
#endif

0 commit comments

Comments
 (0)