Skip to content

Commit

Permalink
feat: Add manual settings test and do some cleanup (#60)
Browse files Browse the repository at this point in the history
  • Loading branch information
FrogTheFrog authored Jul 17, 2024
1 parent ae57fdf commit 94b803f
Show file tree
Hide file tree
Showing 5 changed files with 223 additions and 159 deletions.
176 changes: 76 additions & 100 deletions tests/fixtures/fixtures.cpp
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
// header include
#include "fixtures/fixtures.h"

// local includes
#include "displaydevice/logging.h"

BaseTest::BaseTest():
m_sbuf { nullptr }, m_pipe_stdout { nullptr }, m_pipe_stderr { nullptr } {
// intentionally empty
}
// system includes
#include <regex>

void
BaseTest::SetUp() {
Expand All @@ -16,29 +11,14 @@ BaseTest::SetUp() {
GTEST_SKIP() << skip_reason;
}

// todo: only run this one time, instead of every time a test is run
// see: https://stackoverflow.com/questions/2435277/googletest-accessing-the-environment-from-a-test
// get command line args from the test executable
m_test_args = ::testing::internal::GetArgvs();

// then get the directory of the test executable
// std::string path = ::testing::internal::GetArgvs()[0];
m_test_binary = m_test_args[0];

// get the directory of the test executable
m_test_binary_dir = std::filesystem::path(m_test_binary).parent_path();

// If testBinaryDir is empty or `.` then set it to the current directory
// maybe some better options here: https://stackoverflow.com/questions/875249/how-to-get-current-directory
if (m_test_binary_dir.empty() || m_test_binary_dir.string() == ".") {
m_test_binary_dir = std::filesystem::current_path();
if (isOutputSuppressed()) {
// See https://stackoverflow.com/a/58369622/11214013
m_sbuf = std::cout.rdbuf(); // save cout buffer (std::cout)
std::cout.rdbuf(m_cout_buffer.rdbuf()); // redirect cout to buffer (std::cout)
}

m_sbuf = std::cout.rdbuf(); // save cout buffer (std::cout)
std::cout.rdbuf(m_cout_buffer.rdbuf()); // redirect cout to buffer (std::cout)

// Default to the verbose level in case some test fails
display_device::Logger::get().setLogLevel(display_device::Logger::LogLevel::verbose);
// Set the default log level, before the test starts. Will default to verbose in case nothing was specified.
display_device::Logger::get().setLogLevel(getDefaultLogLevel().value_or(display_device::Logger::LogLevel::verbose));
}

void
Expand All @@ -50,33 +30,51 @@ BaseTest::TearDown() {
return;
}

display_device::Logger::get().setCustomCallback(nullptr); // restore the default callback to avoid potential leaks
std::cout.rdbuf(m_sbuf); // restore cout buffer

// get test info
const ::testing::TestInfo *const test_info = ::testing::UnitTest::GetInstance()->current_test_info();

if (test_info->result()->Failed()) {
std::cout << std::endl
<< "Test failed: " << test_info->name() << std::endl
<< std::endl
<< "Captured cout:" << std::endl
<< m_cout_buffer.str() << std::endl
<< "Captured stdout:" << std::endl
<< m_stdout_buffer.str() << std::endl
<< "Captured stderr:" << std::endl
<< m_stderr_buffer.str() << std::endl;
// reset the callback to avoid potential leaks
display_device::Logger::get().setCustomCallback(nullptr);

// Restore cout buffer and print the suppressed output out in case we have failed :/
if (isOutputSuppressed()) {
std::cout.rdbuf(m_sbuf);
m_sbuf = nullptr;

const auto test_info = ::testing::UnitTest::GetInstance()->current_test_info();
if (test_info && test_info->result()->Failed()) {
std::cout << std::endl
<< "Test failed: " << test_info->name() << std::endl
<< std::endl
<< "Captured cout:" << std::endl
<< m_cout_buffer.str() << std::endl;
}
}
}

m_sbuf = nullptr; // clear sbuf
if (m_pipe_stdout) {
pclose(m_pipe_stdout);
m_pipe_stdout = nullptr;
}
if (m_pipe_stderr) {
pclose(m_pipe_stderr);
m_pipe_stderr = nullptr;
const std::vector<std::string> &
BaseTest::getArgs() const {
static const auto args { ::testing::internal::GetArgvs() };
return args;
}

std::optional<std::string>
BaseTest::getArgWithMatchingPattern(const std::string &pattern, bool remove_match) const {
const auto &args { getArgs() };
if (!args.empty()) {
const std::regex re_pattern { pattern };

// We are skipping the first arg which is always binary name/path.
for (auto it { std::next(std::begin(args)) }; it != std::end(args); ++it) {
if (std::smatch match; std::regex_search(*it, match, re_pattern)) {
return remove_match ? std::regex_replace(*it, re_pattern, "") : *it;
}
}
}

return std::nullopt;
}

bool
BaseTest::isOutputSuppressed() const {
return true;
}

bool
Expand All @@ -101,52 +99,30 @@ BaseTest::skipTest() const {
return {};
}

int
BaseTest::exec(const char *cmd) {
std::array<char, 128> buffer {};
m_pipe_stdout = popen((std::string(cmd) + " 2>&1").c_str(), "r");
m_pipe_stderr = popen((std::string(cmd) + " 2>&1").c_str(), "r");
if (!m_pipe_stdout || !m_pipe_stderr) {
throw std::runtime_error("popen() failed!");
}
while (fgets(buffer.data(), buffer.size(), m_pipe_stdout) != nullptr) {
m_stdout_buffer << buffer.data();
}
while (fgets(buffer.data(), buffer.size(), m_pipe_stderr) != nullptr) {
m_stderr_buffer << buffer.data();
}
int return_code = pclose(m_pipe_stdout);
m_pipe_stdout = nullptr;
if (return_code != 0) {
std::cout << "Error: " << m_stderr_buffer.str() << std::endl
<< "Return code: " << return_code << std::endl;
}
return return_code;
}

std::string
LinuxTest::skipTest() const {
#ifndef __linux__
return "Skipping, this test is for Linux only.";
#else
return BaseTest::skipTest();
#endif
}

std::string
MacOSTest::skipTest() const {
#if !defined(__APPLE__) || !defined(__MACH__)
return "Skipping, this test is for macOS only.";
#else
return BaseTest::skipTest();
#endif
}

std::string
WindowsTest::skipTest() const {
#ifndef _WIN32
return "Skipping, this test is for Windows only.";
#else
return BaseTest::skipTest();
#endif
std::optional<display_device::Logger::LogLevel>
BaseTest::getDefaultLogLevel() const {
const static auto default_log_level {
[]() -> std::optional<display_device::Logger::LogLevel> {
const auto value { getEnv("LOG_LEVEL") };
if (value == "verbose") {
return display_device::Logger::LogLevel::verbose;
}
if (value == "debug") {
return display_device::Logger::LogLevel::debug;
}
if (value == "info") {
return display_device::Logger::LogLevel::info;
}
if (value == "warning") {
return display_device::Logger::LogLevel::warning;
}
if (value == "error") {
return display_device::Logger::LogLevel::error;
}

return std::nullopt;
}()
};

return default_log_level;
}
76 changes: 35 additions & 41 deletions tests/fixtures/include/fixtures/fixtures.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <gtest/gtest.h>

// local includes
#include "displaydevice/logging.h"
#include "testutils.h"

// Undefine the original TEST macro
Expand Down Expand Up @@ -38,19 +39,9 @@
* @brief Base class for tests.
*
* This class provides a base test fixture for all tests.
*
* ``cout``, ``stderr``, and ``stdout`` are redirected to a buffer, and the buffer is printed if the test fails.
*
* @todo Retain the color of the original output.
*/
class BaseTest: public ::testing::Test {
protected:
// https://stackoverflow.com/a/58369622/11214013

// we can possibly use some internal googletest functions to capture stdout and stderr, but I have not tested this
// https://stackoverflow.com/a/33186201/11214013

BaseTest();
~BaseTest() override = default;

void
Expand All @@ -59,6 +50,30 @@ class BaseTest: public ::testing::Test {
void
TearDown() override;

/**
* @brief Get available command line arguments.
* @return Command line args from GTest.
*/
[[nodiscard]] virtual const std::vector<std::string> &
getArgs() const;

/**
* @brief Get the command line argument that matches the pattern.
* @param pattern Pattern to look for.
* @param remove_match Specify if the matched pattern should be removed before returning argument.
* @return Matching command line argument or null optional if nothing matched.
*/
[[nodiscard]] virtual std::optional<std::string>
getArgWithMatchingPattern(const std::string &pattern, bool remove_match) const;

/**
* @brief Check if the test output is to be redirected and printed out only if test fails.
* @return True if output is to be suppressed, false otherwise.
* @note It is useful for suppressing noise in automatic tests, but not so much in manual ones.
*/
[[nodiscard]] virtual bool
isOutputSuppressed() const;

/**
* @brief Check if the test interacts/modifies with the system settings.
* @returns True if it does, false otherwise.
Expand All @@ -74,38 +89,17 @@ class BaseTest: public ::testing::Test {
[[nodiscard]] virtual std::string
skipTest() const;

int
exec(const char *cmd);
/**
* @brief Get the default log level for the test base.
* @returns A log level set in the env OR null optional if fallback should be used (verbose).
* @note By setting LOG_LEVEL=<level> env you can change the level (e.g. LOG_LEVEL=error).
*/
[[nodiscard]] virtual std::optional<display_device::Logger::LogLevel>
getDefaultLogLevel() const;

// functions and variables
std::vector<std::string> m_test_args; // CLI arguments used
std::filesystem::path m_test_binary; // full path of this binary
std::filesystem::path m_test_binary_dir; // full directory of this binary
std::stringstream m_cout_buffer; // declare cout_buffer
std::stringstream m_stdout_buffer; // declare stdout_buffer
std::stringstream m_stderr_buffer; // declare stderr_buffer
std::streambuf *m_sbuf;
FILE *m_pipe_stdout;
FILE *m_pipe_stderr;
std::stringstream m_cout_buffer; /**< Stores the cout in case the output is suppressed. */

private:
bool m_test_skipped_at_setup { false };
};

class LinuxTest: public BaseTest {
protected:
[[nodiscard]] std::string
skipTest() const override;
};

class MacOSTest: public BaseTest {
protected:
[[nodiscard]] std::string
skipTest() const override;
};

class WindowsTest: public BaseTest {
protected:
[[nodiscard]] std::string
skipTest() const override;
std::streambuf *m_sbuf { nullptr }; /**< Stores the handle to the original cout stream. */
bool m_test_skipped_at_setup { false }; /**< Indicates whether the SetUp method was skipped. */
};
Loading

0 comments on commit 94b803f

Please sign in to comment.