diff --git a/CHANGELOG.md b/CHANGELOG.md index ca61ae6..b8d10f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,16 @@ You should have received a copy of the GNU General Public License along with DrMock. If not, see . --> +# DrMock 0.4.0 + +Release 2020/08/16 + +### Added/Changed: + +* Add `DRTEST_ASSERT_DEATH` macro for death testing + + + # DrMock 0.3.0 Released 2020/07/05 @@ -50,6 +60,8 @@ Released 2020/07/05 defined, but rather serves as a catch-all (fallthru) state (the documentation regarding this has been clarified) + + # DrMock 0.2.0 Released 2020/05/15 @@ -108,6 +120,8 @@ Released 2020/05/15 * Throw error message if `DrMockTest` can't find files specified in `TESTS` + + # DrMock 0.1.0 Released 2020/01/10 diff --git a/CMakeLists.txt b/CMakeLists.txt index 1d5c3ab..5d38c87 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,7 +23,7 @@ cmake_minimum_required (VERSION 3.13) project( DrMock - VERSION 0.3.0 + VERSION 0.4.0 DESCRIPTION "C++17 testing and mocking framework" LANGUAGES CXX ) diff --git a/README.md b/README.md index e8869a8..d4cf223 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ features for mock object configuration. ### Announcments -Release v0.3.x is now available. For details, see +Release v0.4.x is now available. For details, see [changelog](CHANGELOG.md). ### Getting started diff --git a/docs/samples/death.md b/docs/samples/death.md new file mode 100644 index 0000000..86341a5 --- /dev/null +++ b/docs/samples/death.md @@ -0,0 +1,99 @@ + + +# samples/death + +DrMock v0.4.0 introduces death tests. A _death test_ checks if a certain +statement will cause the process to raise a certain signal. This may be +used to assert that in certain unrecoverable situations, the program +exits before causing further damage. + +This sample demonstrates the death testing capabilities of **DrMock**. + +### Table of contents + +* [Source code](#source-code) +* [Running the tests](#running-the-tests) +* [Details](#details) + + [Supported signals](#supported-signals) + + [clone, fork, signal, multi-threading](#clone-fork-signal-multi-threading) + +### Project structure + +``` +samples/death +│ CMakeLists.txt +│ Makefile +│ deathTest.cpp +``` + +## Source code + +Open `deathTest.cpp`! +The `DRTEST_ASSERT_DEATH(statement, expected)` macro checks if executing +`statement` will cause the signal `expected` to be raised. As with +`DRTEST_ASSERT_THROW`, statement may contain multiple lines of code, if +they are seperated by semicolons (see below). + +In the test `catch_segfault`, we test the classic segmentation fault +scenario, dereferencing a `nullptr`: +```cpp +DRTEST_TEST(catch_segfault) +{ + DRTEST_ASSERT_DEATH( + int* foo = nullptr; + *foo = 0, + SIGSEGV + ); +} +``` +We expect this to raise the `SIGSEGV` signal. The test will verify this. + +## Running the tests + +Do `make`. This should yield the following: + +``` + Start 1: deathTest +1/1 Test #1: deathTest ........................ Passed 0.00 sec + +100% tests passed, 0 tests failed out of 1 + +Total Test time (real) = 0.01 sec +``` + +## Details + +### Supported signals + +The following POSIX signals may be caught using `DRTEST_ASSERT_DEATH`: +```cpp +SIGABRT, SIGALRM, SIGBUS, SIGCHLD, +SIGCONT, SIGFPE, SIGHUP, SIGILL, +SIGINT, SIGPIPE, SIGPROF, SIGQUIT, +SIGSEGV, SIGTSTP, SIGSYS, SIGTERM, +SIGTRAP, SIGTTIN, SIGTTOU, SIGURG, +SIGUSR2, SIGVTALRM, SIGXCPU, SIGXFSZ +``` +Note that `SIGKILL`, `SIGSTOP` and `SIGUSR1` are not supported. + +### clone, fork, signal, multi-threading + +Using `clone()`, `fork()`, `signal()` or multi-threading are not allowed +when using `DRTEST_ASSERT_DEATH`. diff --git a/docs/tutorial.md b/docs/tutorial.md index ebfe30b..2ade822 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -51,3 +51,9 @@ Learn how to use **DrMock**'s mock objects as stubs for state verification. Learn how to use **DrMock** with Qt5. --- + +[samples/death](samples/death.md) + +Learn how to use **DrMock** for death tests. + +--- diff --git a/python/setup.py b/python/setup.py index 975a5f8..5750a4d 100644 --- a/python/setup.py +++ b/python/setup.py @@ -20,7 +20,7 @@ setup( name = "DrMockGenerator", author = "Ole Kliemann, Malte Kliemann", - version = "0.3.0", + version = "0.4.0", scripts = ["DrMockGenerator"], packages = ["mocker"], include_package_data = True, diff --git a/samples/Makefile b/samples/Makefile index f23e73f..3c2bb24 100644 --- a/samples/Makefile +++ b/samples/Makefile @@ -1,4 +1,4 @@ -dirs = basic example mock qt states +dirs = basic example mock qt states death .PHONY: default default: diff --git a/samples/death/CMakeLists.txt b/samples/death/CMakeLists.txt new file mode 100644 index 0000000..ea83bef --- /dev/null +++ b/samples/death/CMakeLists.txt @@ -0,0 +1,40 @@ +# Copyright 2020 Ole Kliemann, Malte Kliemann +# +# This file is part of DrMock. +# +# DrMock is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# DrMock is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with DrMock. If not, see . + +cmake_minimum_required(VERSION 3.10) + +####################################### +# Project data. +####################################### + +project(DrMockSampleBasic) +set(CMAKE_CXX_STANDARD 17) + +####################################### +# Dependencies. +####################################### + +find_package(DrMock COMPONENTS Core REQUIRED) + +####################################### +# Testing. +####################################### + +enable_testing() +DrMockTest(TESTS + deathTest.cpp +) diff --git a/samples/death/Makefile b/samples/death/Makefile new file mode 100644 index 0000000..58fefeb --- /dev/null +++ b/samples/death/Makefile @@ -0,0 +1,18 @@ +# Discover operating system. +uname := $(shell uname -s) + +# Get number of threads +ifeq ($(uname), Darwin) + num_threads := $(shell sysctl -n hw.activecpu) +else # Assuming Linux. + num_threads := $(shell nproc) +endif + +.PHONY: default +default: + mkdir -p build && cd build && cmake .. -DCMAKE_PREFIX_PATH="../../prefix" + cd build && make -j$(num_threads) && ctest --output-on-failure + +.PHONY: clean +clean: + rm -fr build && rm -fr prefix diff --git a/samples/death/deathTest.cpp b/samples/death/deathTest.cpp new file mode 100644 index 0000000..dbc0586 --- /dev/null +++ b/samples/death/deathTest.cpp @@ -0,0 +1,28 @@ +/* Copyright 2020 Ole Kliemann, Malte Kliemann + * + * This file is part of DrMock. + * + * DrMock is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DrMock is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DrMock. If not, see . +*/ + +#include "DrMock/Test.h" + +DRTEST_TEST(catch_segfault) +{ + DRTEST_ASSERT_DEATH( + int* foo = nullptr; + *foo = 0, + SIGSEGV + ); +} diff --git a/src/test/Death.h b/src/test/Death.h new file mode 100644 index 0000000..3b681c3 --- /dev/null +++ b/src/test/Death.h @@ -0,0 +1,130 @@ +/* Copyright 2020 Ole Kliemann, Malte Kliemann + * + * This file is part of DrMock. + * + * DrMock is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DrMock is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DrMock. If not, see . +*/ + +#ifndef DRMOCK_SRC_TEST_DEATH_H +#define DRMOCK_SRC_TEST_DEATH_H + +#include + +#if defined(__unix__) || defined(__APPLE__) +#include +#endif + +#include "TestFailure.h" + +namespace drtest { namespace death { + +static int pipe_[2]; +volatile std::sig_atomic_t atomic_pipe_; // Self-pipe write end; required due to https://en.cppreference.com/w/c/program/signal + +#if defined(__unix__) || defined(__APPLE__) +static std::vector signals_ = { // POSIX signals + SIGABRT, + SIGALRM, + SIGBUS, + SIGCHLD, + SIGCONT, + SIGFPE, + SIGHUP, + SIGILL, + SIGINT, + // SIGKILL, + SIGPIPE, + SIGPROF, + SIGQUIT, + SIGSEGV, + // SIGSTOP, + SIGTSTP, + SIGSYS, + SIGTERM, + SIGTRAP, + SIGTTIN, + SIGTTOU, + SIGURG, + // SIGUSR1, + SIGUSR2, + SIGVTALRM, + SIGXCPU, + SIGXFSZ + }; +#endif + +// Signal handler requires external linkage according to https://en.cppreference.com/w/c/program/signal +extern "C" { + void signal_handler(int x) + { + write(atomic_pipe_, &x, 4); + exit(0); + } +} // extern C + +}} // namespace drtest::death + +#define DRTEST_ASSERT_DEATH(statement, expected) \ +do \ +{ \ + pipe(drtest::death::pipe_); \ + drtest::death::atomic_pipe_ = drtest::death::pipe_[1]; \ +\ + pid_t pid = fork(); \ + if (pid == 0) \ + { \ + for (auto s: drtest::death::signals_) \ + { \ + std::signal(s, drtest::death::signal_handler); \ + } \ + statement; /* Child exits here if signal is raised. */ \ + int no_signal = -1; \ + write(drtest::death::atomic_pipe_, &no_signal, 4); /* Wake up parent if no signal was raised. */ \ + close(drtest::death::pipe_[0]); \ + close(drtest::death::pipe_[1]); \ + exit(0); \ + } \ + else \ + { \ + assert(PIPE_BUF >= 4); \ + std::vector buffer(4); \ + if (!read(drtest::death::pipe_[0], buffer.data(), 4)) \ + { \ + throw std::runtime_error{"read to pipe failed"}; \ + } \ +\ + int result = *(int*)buffer.data(); \ + if (result != expected) \ + { \ +\ + /* Error message */ \ + std::string e = strsignal(expected); \ + std::string r; \ + if (result != -1) \ + { \ + r = strsignal(result); \ + } \ + else \ + { \ + r = "No signal: -1"; \ + } \ + throw drtest::detail::TestFailure{__LINE__, "!=", "received", "expected", e, r}; \ + } \ + } \ +\ + close(drtest::death::pipe_[0]); \ + close(drtest::death::pipe_[1]); \ +} while(false) + +#endif /* DRMOCK_SRC_TEST_DEATH_H */ diff --git a/src/test/TestMacros.h b/src/test/TestMacros.h index de6f773..e24a947 100644 --- a/src/test/TestMacros.h +++ b/src/test/TestMacros.h @@ -19,6 +19,7 @@ #ifndef DRMOCK_SRC_TEST_TESTMACROS_H #define DRMOCK_SRC_TEST_TESTMACROS_H +#include "Death.h" #include "FunctionInvoker.h" #include "Global.h" #include "TestFailure.h" diff --git a/tests/Test.cpp b/tests/Test.cpp index 12d4e30..c8144fb 100644 --- a/tests/Test.cpp +++ b/tests/Test.cpp @@ -203,3 +203,22 @@ DRTEST_TEST(streamIfStreamable) // not having a streaming operator. DRTEST_ASSERT_EQ(A{}, A{}); } + +DRTEST_TEST(death_success) +{ + DRTEST_ASSERT_DEATH(raise(SIGSEGV), SIGSEGV); + DRTEST_ASSERT_DEATH(raise(SIGCHLD), SIGCHLD); + DRTEST_ASSERT_DEATH(raise(SIGABRT), SIGABRT); + DRTEST_ASSERT_DEATH(volatile int* foo = nullptr; *foo =123, SIGSEGV); + DRTEST_ASSERT_DEATH(assert(false), SIGABRT); +} + +DRTEST_TEST(death_failure_no_raise) +{ + DRTEST_ASSERT_TEST_FAIL(DRTEST_ASSERT_DEATH(int x = 0; (void)x, SIGSEGV)); +} + +DRTEST_TEST(death_failure_wrong_raise) +{ + DRTEST_ASSERT_TEST_FAIL(DRTEST_ASSERT_DEATH(raise(SIGXFSZ), SIGSEGV)); +}