Skip to content

Commit 5abd626

Browse files
committed
implement output capture
1 parent f787429 commit 5abd626

File tree

10 files changed

+191
-62
lines changed

10 files changed

+191
-62
lines changed

example/always_passes.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
#include <rsl/test>
2+
#include <iostream>
23

34
namespace demo {
45

56
[[=rsl::test]]
67
void always_passes() {
7-
// ASSERT(false, "testing");
8+
std::cout << "foo\n";
9+
std::cerr << "bar\n";
10+
ASSERT(false, "testing");
811
}
912

1013
[[=rsl::test, =rsl::expect_failure]]

include/rsl/testing/test_case.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,12 @@ namespace rsl::testing {
66
struct TestResult {
77
class Test const* test;
88
std::string name;
9+
910
bool passed;
1011
std::string error;
12+
13+
std::string stdout;
14+
std::string stderr;
1115
double duration_ms;
1216
};
1317

src/CMakeLists.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
target_sources(rsltest PUBLIC
2-
discovery.cpp
2+
capture.cpp
33
test.cpp
44
)
55

6-
target_sources(rsltest_main PUBLIC main.cpp)
6+
target_sources(rsltest_main PRIVATE main.cpp)
77
target_include_directories(rsltest PRIVATE .)
88
target_include_directories(rsltest_main PRIVATE .)
99

src/capture.cpp

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#include "capture.hpp"
2+
3+
#include <string>
4+
#include <string_view>
5+
6+
#ifdef _WIN32
7+
#define WIN32_LEAN_AND_MEAN
8+
#include <fcntl.h>
9+
#include <io.h>
10+
#include <windows.h>
11+
#else
12+
#include <errno.h>
13+
#include <fcntl.h>
14+
#include <unistd.h>
15+
#endif
16+
17+
18+
namespace rsl::testing {
19+
20+
RedirectedOutput::RedirectedOutput(FILE* redirected_stream, int original_fd)
21+
: redirected(redirected_stream)
22+
, underlying_fd(original_fd) {
23+
underlying = fdopen(original_fd, "w");
24+
redirected_fd = fileno(redirected_stream);
25+
}
26+
27+
Capture::Capture(FILE* stream, std::string& target, bool echo )
28+
: target(&target)
29+
, echo(echo) {
30+
fflush(stream);
31+
out = {stream, dup(fileno(stream))};
32+
#ifdef _WIN32
33+
_pipe(pipe_fds_, 8192, _O_BINARY);
34+
#else
35+
pipe(pipe_fds_);
36+
fcntl(pipe_fds_[0], F_SETFL, O_NONBLOCK);
37+
#endif
38+
dup2(pipe_fds_[1], out.redirected_fd);
39+
}
40+
41+
Capture::~Capture() {
42+
fflush(out.redirected);
43+
dup2(out.underlying_fd, out.redirected_fd);
44+
close(pipe_fds_[1]);
45+
46+
drain(); // Final flush
47+
48+
close(pipe_fds_[0]);
49+
close(out.underlying_fd);
50+
}
51+
52+
static int read_pipe(int fd, char* buffer, size_t size) {
53+
#ifdef _WIN32
54+
HANDLE h = reinterpret_cast<HANDLE>(_get_osfhandle(fd));
55+
DWORD available = 0;
56+
if (PeekNamedPipe(h, nullptr, 0, nullptr, &available, nullptr) && available > 0)
57+
return _read(fd, buffer, static_cast<unsigned>(size));
58+
return 0;
59+
#else
60+
ssize_t n = read(fd, buffer, size);
61+
if (n > 0)
62+
return static_cast<int>(n);
63+
if (errno == EAGAIN || errno == EWOULDBLOCK)
64+
return 0;
65+
return -1;
66+
#endif
67+
}
68+
69+
void Capture::drain() {
70+
fflush(out.redirected);
71+
char buffer[256];
72+
while (true) {
73+
int n = read_pipe(pipe_fds_[0], buffer, sizeof(buffer));
74+
if (n > 0) {
75+
*target += std::string_view(buffer, n);
76+
if (echo) {
77+
write(out.underlying_fd, buffer, n);
78+
}
79+
} else {
80+
break;
81+
}
82+
}
83+
}
84+
85+
} // namespace rsl::testing

src/capture.hpp

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#pragma once
2+
#include <cstdio>
3+
#include <string>
4+
5+
namespace rsl::testing {
6+
7+
struct RedirectedOutput {
8+
FILE* redirected = nullptr;
9+
FILE* underlying = nullptr;
10+
11+
int redirected_fd = -1;
12+
int underlying_fd = -1;
13+
14+
RedirectedOutput() = default;
15+
16+
RedirectedOutput(FILE* redirected_stream, int original_fd);
17+
};
18+
19+
class Capture {
20+
int pipe_fds_[2]{};
21+
std::string* target;
22+
bool echo;
23+
24+
public:
25+
RedirectedOutput out;
26+
27+
Capture(FILE* stream, std::string& target, bool echo = false);
28+
~Capture();
29+
void drain();
30+
};
31+
} // namespace rsl::testing

src/discovery.cpp

Lines changed: 0 additions & 9 deletions
This file was deleted.

src/main.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313

1414
#include <rsl/config>
1515
#include <rsl/testing/_testing_impl/factory.hpp>
16-
1716
#include "output.hpp"
17+
1818
namespace {
1919
template <std::ranges::range R>
2020
std::string join(R&& values, std::string_view delimiter) {

src/reporters/catch2xml.cpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
#include <rsl/testing/output.hpp>
77
#include <rsl/xml>
8-
#include "rsl/testing/annotations.hpp"
98

109
namespace rsl::testing::_xml_impl {
1110
struct OverallResult {

src/reporters/terminal.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ class [[=rename("plain")]] ConsoleReporter : public Reporter::Registrar<ConsoleR
2727
result.name,
2828
result.duration_ms);
2929
std::print("{}ERROR{}: {}\n", color[1], reset, result.error);
30+
std::print("==== {}stdout{} ====\n{}\n", color[1], reset, result.stdout);
31+
std::print("==== {}stderr{} ====\n{}\n", color[1], reset, result.stderr);
3032
}
3133
}
3234
void after_run(std::span<TestResult> results) override {

src/test.cpp

Lines changed: 62 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44
#include <libassert/assert.hpp>
55
#include <rsl/testing/test.hpp>
66
#include <rsl/testing/output.hpp>
7+
#include <rsl/testing/_testing_impl/discovery.hpp>
8+
9+
#include "capture.hpp"
710

8-
template <bool Colorize>
911
void failure_handler(libassert::assertion_info const& info) {
1012
// libassert::enable_virtual_terminal_processing_if_needed(); // for terminal colors on windows
11-
auto width = libassert::terminal_width(libassert::stderr_fileno);
13+
constexpr bool Colorize = true;
14+
auto width = libassert::terminal_width(libassert::stderr_fileno);
1215
const auto& scheme = Colorize ? libassert::get_color_scheme() : libassert::color_scheme::blank;
1316
std::string message = std::string(info.action()) + " at " + info.location() + ":";
1417
if (info.message) {
@@ -21,12 +24,57 @@ void failure_handler(libassert::assertion_info const& info) {
2124
}
2225

2326
namespace rsl::testing {
24-
namespace _testing_impl {
25-
std::set<TestDef>& registry();
27+
std::set<TestDef>& _testing_impl::registry() {
28+
static std::set<TestDef> data;
29+
return data;
30+
}
31+
32+
bool TestRoot::run(Reporter* reporter) {
33+
libassert::set_failure_handler(failure_handler);
34+
std::println("failure handler set");
35+
reporter->before_run(*this);
36+
bool status = TestNamespace::run(reporter);
37+
libassert::set_failure_handler(libassert::default_failure_handler);
38+
// TODO after_run
39+
reporter->after_run({});
40+
return status;
41+
}
42+
43+
bool TestNamespace::run(Reporter* reporter) {
44+
if (!name.empty()) {
45+
reporter->enter_namespace(name);
46+
}
47+
bool status = true;
48+
for (auto& ns : children) {
49+
status &= ns.run(reporter);
50+
}
51+
52+
for (auto& test : tests) {
53+
auto runs = test.get_tests();
54+
reporter->before_test_group(test);
55+
56+
std::vector<TestResult> results;
57+
for (auto const& test_run : test.get_tests()) {
58+
reporter->before_test(test_run);
59+
auto result = test_run.run();
60+
reporter->after_test(result);
61+
results.push_back(result);
62+
}
63+
64+
reporter->after_test_group(results);
65+
}
66+
if (!name.empty()) {
67+
reporter->exit_namespace(name);
68+
}
69+
return status;
2670
}
71+
2772
TestResult TestCase::run() const {
2873
auto ret = TestResult{.test = test, .name = name};
2974
try {
75+
Capture _out(stdout, ret.stdout);
76+
Capture _err(stderr, ret.stderr);
77+
3078
auto t0 = std::chrono::steady_clock::now();
3179
fnc();
3280
auto t1 = std::chrono::steady_clock::now();
@@ -35,11 +83,16 @@ TestResult TestCase::run() const {
3583
ret.duration_ms = std::chrono::duration<double, std::milli>(t1 - t0).count();
3684
return ret;
3785
} catch (assertion_failure const& failure) {
38-
ret.error = failure.what();
39-
} catch (std::exception const& exc) {
40-
ret.error = exc.what();
41-
} //
42-
catch (...) {}
86+
ret.error += failure.what();
87+
} catch (std::exception const& exc) { //
88+
ret.error += exc.what();
89+
} catch (std::string const& msg) { //
90+
ret.error += msg;
91+
} catch (std::string_view msg) { //
92+
ret.error += msg;
93+
} catch (char const* msg) { //
94+
ret.error += msg;
95+
} catch (...) { ret.error += "unknown exception thrown"; }
4396

4497
ret.passed = test->expect_failure;
4598
return ret;
@@ -111,45 +164,6 @@ std::size_t TestNamespace::count() const {
111164
return total;
112165
}
113166

114-
bool TestNamespace::run(Reporter* reporter) {
115-
if (!name.empty()) {
116-
reporter->enter_namespace(name);
117-
}
118-
bool status = true;
119-
for (auto& ns : children) {
120-
status &= ns.run(reporter);
121-
}
122-
123-
for (auto& test : tests) {
124-
auto runs = test.get_tests();
125-
reporter->before_test_group(test);
126-
127-
std::vector<TestResult> results;
128-
for (auto const& test_run : test.get_tests()) {
129-
reporter->before_test(test_run);
130-
auto result = test_run.run();
131-
reporter->after_test(result);
132-
results.push_back(result);
133-
}
134-
135-
reporter->after_test_group(results);
136-
}
137-
if (!name.empty()) {
138-
reporter->exit_namespace(name);
139-
}
140-
return status;
141-
}
142-
143-
bool TestRoot::run(Reporter* reporter) {
144-
libassert::set_failure_handler(failure_handler<true>);
145-
reporter->before_run(*this);
146-
bool status = TestNamespace::run(reporter);
147-
libassert::set_failure_handler(libassert::default_failure_handler);
148-
// TODO after_run
149-
reporter->after_run({});
150-
return status;
151-
}
152-
153167
TestRoot get_tests() {
154168
TestRoot root;
155169
for (auto test_def : rsl::testing::_testing_impl::registry()) {

0 commit comments

Comments
 (0)