Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix capture/section state after unhandled exception(s) #178

Merged
merged 7 commits into from
Aug 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions codecov.yaml → codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ coverage:
round: down
range: "80...95"
status:
project:
default:
target: auto
# adjust accordingly based on how flaky your tests are
# this allows a drop from the previous base commit coverage
threshold: 1%
project:
default:
target: auto
# adjust accordingly based on how flaky your tests are
# this allows a drop from the previous base commit coverage
threshold: 1%
patch:
default:
target: 0%
Expand Down
12 changes: 5 additions & 7 deletions include/snitch/snitch_capture.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,10 @@

namespace snitch::impl {
struct scoped_capture {
capture_state& captures;
std::size_t count = 0;
test_state& state;
std::size_t count = 0;

~scoped_capture() {
captures.resize(captures.size() - count);
}
SNITCH_EXPORT ~scoped_capture();
};

SNITCH_EXPORT std::string_view extract_next_name(std::string_view& names) noexcept;
Expand All @@ -37,15 +35,15 @@ void add_capture(test_state& state, std::string_view& names, const T& arg) {
template<string_appendable... Args>
scoped_capture add_captures(test_state& state, std::string_view names, const Args&... args) {
(add_capture(state, names, args), ...);
return {state.captures, sizeof...(args)};
return {state, sizeof...(args)};
}

// Requires: number of captures < max_captures.
template<string_appendable... Args>
scoped_capture add_info(test_state& state, const Args&... args) {
auto& capture = add_capture(state);
append_or_truncate(capture, args...);
return {state.captures, 1};
return {state, 1};
}
} // namespace snitch::impl

Expand Down
11 changes: 9 additions & 2 deletions include/snitch/snitch_string_utility.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,22 @@ constexpr small_string<N> resize_or_truncate(const small_string<M>& str) noexcep
return str;
} else if constexpr (N > M) {
small_string<N> out;
append(out, str);
append(out, std::string_view{str});
return out;
} else {
small_string<N> out;
append_or_truncate(out, str);
append_or_truncate(out, std::string_view{str});
return out;
}
}

template<std::size_t N, std::size_t M>
constexpr small_string<N> resize_or_truncate(std::string_view str) noexcept {
small_string<N> out;
append(out, str);
return out;
}

SNITCH_EXPORT [[nodiscard]] bool replace_all(
small_string_span string, std::string_view pattern, std::string_view replacement) noexcept;

Expand Down
20 changes: 17 additions & 3 deletions include/snitch/snitch_test_data.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "snitch/snitch_vector.hpp"

#include <cstddef>
#include <optional>
#include <string_view>

namespace snitch {
Expand Down Expand Up @@ -278,12 +279,21 @@ using capture_state = small_vector<small_string<max_capture_length>, max_capture
// NB: +2 is because we need one for the test case location, and one for the check location
using location_state = small_vector<assertion_location, max_nested_sections + 2>;

struct test_state {
registry& reg;
test_case& test;
struct info_state {
section_state sections = {};
capture_state captures = {};
location_state locations = {};
};

struct test_state {
registry& reg;
test_case& test;

info_state info = {};

#if SNITCH_WITH_EXCEPTIONS
std::optional<info_state> held_info = {};
#endif

std::size_t asserts = 0;
std::size_t failures = 0;
Expand All @@ -292,6 +302,10 @@ struct test_state {
bool should_fail = false;
bool in_check = false;

#if SNITCH_WITH_EXCEPTIONS
bool unhandled_exception = false;
#endif

#if SNITCH_WITH_TIMINGS
float duration = 0.0f;
#endif
Expand Down
25 changes: 21 additions & 4 deletions src/snitch_capture.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,19 @@ void trim(std::string_view& str, std::string_view patterns) noexcept {
}
} // namespace

scoped_capture::~scoped_capture() {
#if SNITCH_WITH_EXCEPTIONS
if (std::uncaught_exceptions() > 0 && !state.held_info.has_value()) {
// We are unwinding the stack because an exception has been thrown;
// keep a copy of the full capture state since we will want to preserve the information
// when reporting the exception.
state.held_info = state.info;
}
#endif

state.info.captures.resize(state.info.captures.size() - count);
}

std::string_view extract_next_name(std::string_view& names) noexcept {
std::string_view result;

Expand Down Expand Up @@ -69,7 +82,7 @@ std::string_view extract_next_name(std::string_view& names) noexcept {
}

small_string<max_capture_length>& add_capture(test_state& state) {
if (state.captures.available() == 0) {
if (state.info.captures.available() == 0) {
state.reg.print(
make_colored("error:", state.reg.with_color, color::fail),
" max number of captures reached; "
Expand All @@ -78,8 +91,12 @@ small_string<max_capture_length>& add_capture(test_state& state) {
assertion_failed("max number of captures reached");
}

state.captures.grow(1);
state.captures.back().clear();
return state.captures.back();
#if SNITCH_WITH_EXCEPTIONS
state.held_info.reset();
#endif

state.info.captures.grow(1);
state.info.captures.back().clear();
return state.info.captures.back();
}
} // namespace snitch::impl
51 changes: 32 additions & 19 deletions src/snitch_registry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -413,30 +413,41 @@ void report_assertion_impl(

register_assertion(success, state);

const auto captures_buffer = impl::make_capture_buffer(state.captures);
const auto& last_location = state.locations.back();
#if SNITCH_WITH_EXCEPTIONS
const bool use_held_info = state.unhandled_exception && state.held_info.has_value();

const auto captures_buffer = impl::make_capture_buffer(
use_held_info ? state.held_info.value().captures : state.info.captures);

const auto& current_section = use_held_info ? state.held_info.value().sections.current_section
: state.info.sections.current_section;

const auto& last_location =
use_held_info ? state.held_info.value().locations.back() : state.info.locations.back();

const auto location =
state.in_check
? assertion_location{last_location.file, last_location.line, location_type::exact}
: last_location;
#else
const auto location =
const auto captures_buffer = impl::make_capture_buffer(state.info.captures);
const auto& current_section = state.info.sections.current_section;
const auto& last_location = state.info.locations.back();
const auto location =
assertion_location{last_location.file, last_location.line, location_type::exact};
#endif

if (success) {
if (r.verbose >= registry::verbosity::full) {
r.report_callback(
r, event::assertion_succeeded{
state.test.id, state.sections.current_section, captures_buffer.span(),
location, data});
state.test.id, current_section, captures_buffer.span(), location, data});
}
} else {
r.report_callback(
r, event::assertion_failed{
state.test.id, state.sections.current_section, captures_buffer.span(), location,
data, state.should_fail, state.may_fail});
state.test.id, current_section, captures_buffer.span(), location, data,
state.should_fail, state.may_fail});
}
}
} // namespace
Expand Down Expand Up @@ -473,13 +484,13 @@ void registry::report_skipped(std::string_view message) noexcept {
impl::test_state& state = impl::get_current_test();
impl::set_state(state.test, impl::test_case_state::skipped);

const auto captures_buffer = impl::make_capture_buffer(state.captures);
const auto& location = state.locations.back();
const auto captures_buffer = impl::make_capture_buffer(state.info.captures);
const auto& location = state.info.locations.back();

state.reg.report_callback(
state.reg, event::test_case_skipped{
state.test.id,
state.sections.current_section,
state.info.sections.current_section,
captures_buffer.span(),
{location.file, location.line, location_type::exact},
message});
Expand All @@ -506,7 +517,7 @@ impl::test_state registry::run(impl::test_case& test) noexcept {
impl::test_state state{
.reg = *this, .test = test, .may_fail = may_fail, .should_fail = should_fail};

state.locations.push_back(
state.info.locations.push_back(
{test.location.file, test.location.line, location_type::test_case_scope});

// Store previously running test, to restore it later.
Expand All @@ -525,24 +536,24 @@ impl::test_state registry::run(impl::test_case& test) noexcept {

do {
// Reset section state.
state.sections.leaf_executed = false;
for (std::size_t i = 0; i < state.sections.levels.size(); ++i) {
state.sections.levels[i].current_section_id = 0;
state.info.sections.leaf_executed = false;
for (std::size_t i = 0; i < state.info.sections.levels.size(); ++i) {
state.info.sections.levels[i].current_section_id = 0;
}

// Run the test case.
test.func();

if (state.sections.levels.size() == 1) {
if (state.info.sections.levels.size() == 1) {
// This test case contained sections; check if there are any more left to evaluate.
auto& child = state.sections.levels[0];
auto& child = state.info.sections.levels[0];
if (child.previous_section_id == child.max_section_id) {
// No more; clear the section state.
state.sections.levels.clear();
state.sections.current_section.clear();
state.info.sections.levels.clear();
state.info.sections.current_section.clear();
}
}
} while (!state.sections.levels.empty() &&
} while (!state.info.sections.levels.empty() &&
state.test.state != impl::test_case_state::skipped);

#if SNITCH_WITH_EXCEPTIONS
Expand All @@ -552,8 +563,10 @@ impl::test_state registry::run(impl::test_case& test) noexcept {
} catch (const impl::abort_exception&) {
// Test aborted, assume its state was already set accordingly.
} catch (const std::exception& e) {
state.unhandled_exception = true;
report_assertion(false, "unexpected std::exception caught; message: ", e.what());
} catch (...) {
state.unhandled_exception = true;
report_assertion(false, "unexpected unknown exception caught");
}
#endif
Expand Down
46 changes: 27 additions & 19 deletions src/snitch_section.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,33 @@

namespace snitch::impl {
section_entry_checker::~section_entry_checker() {
auto& sections = state.info.sections;

if (entered) {
#if SNITCH_WITH_EXCEPTIONS
if (std::uncaught_exceptions() > 0) {
if (std::uncaught_exceptions() > 0 && !state.held_info.has_value()) {
// We are unwinding the stack because an exception has been thrown;
// avoid touching the section state since we will want to report where
// the exception was thrown.
return;
// keep a copy of the full section state since we will want to preserve the information
// when reporting the exception.
state.held_info = state.info;
}
#endif

pop_location(state);

if (state.sections.depth == state.sections.levels.size()) {
if (sections.depth == sections.levels.size()) {
// We just entered this section, and there was no child section in it.
// This is a leaf; flag that a leaf has been executed so that no other leaf
// is executed in this run.
// Note: don't pop this level from the section state yet, it may have siblings
// that we don't know about yet. Popping will be done when we exit from the parent,
// since then we will know if there is any sibling.
state.sections.leaf_executed = true;
sections.leaf_executed = true;
} else {
// Check if there is any child section left to execute, at any depth below this one.
bool no_child_section_left = true;
for (std::size_t c = state.sections.depth; c < state.sections.levels.size(); ++c) {
auto& child = state.sections.levels[c];
for (std::size_t c = sections.depth; c < sections.levels.size(); ++c) {
auto& child = sections.levels[c];
if (child.previous_section_id != child.max_section_id) {
no_child_section_left = false;
break;
Expand All @@ -42,19 +44,25 @@ section_entry_checker::~section_entry_checker() {

if (no_child_section_left) {
// No more children, we can pop this level and never go back.
state.sections.levels.pop_back();
sections.levels.pop_back();
}
}

state.sections.current_section.pop_back();
sections.current_section.pop_back();
}

--state.sections.depth;
--sections.depth;
}

section_entry_checker::operator bool() {
if (state.sections.depth >= state.sections.levels.size()) {
if (state.sections.depth >= max_nested_sections) {
#if SNITCH_WITH_EXCEPTIONS
state.held_info.reset();
#endif

auto& sections = state.info.sections;

if (sections.depth >= sections.levels.size()) {
if (sections.depth >= max_nested_sections) {
using namespace snitch::impl;
state.reg.print(
make_colored("error:", state.reg.with_color, color::fail),
Expand All @@ -64,19 +72,19 @@ section_entry_checker::operator bool() {
assertion_failed("max number of nested sections reached");
}

state.sections.levels.push_back({});
sections.levels.push_back({});
}

++state.sections.depth;
++sections.depth;

auto& level = state.sections.levels[state.sections.depth - 1];
auto& level = sections.levels[sections.depth - 1];

++level.current_section_id;
if (level.current_section_id > level.max_section_id) {
level.max_section_id = level.current_section_id;
}

if (state.sections.leaf_executed) {
if (sections.leaf_executed) {
// We have already executed another leaf section; can't execute more
// on this run, so don't bother going inside this one now.
return false;
Expand All @@ -87,10 +95,10 @@ section_entry_checker::operator bool() {
// - This section was already entered in the previous run, and child sections exist in it.
if (level.current_section_id == level.previous_section_id + 1 ||
(level.current_section_id == level.previous_section_id &&
state.sections.depth < state.sections.levels.size())) {
sections.depth < sections.levels.size())) {

level.previous_section_id = level.current_section_id;
state.sections.current_section.push_back(data);
sections.current_section.push_back(data);
push_location(
state, {data.location.file, data.location.line, location_type::section_scope});
entered = true;
Expand Down
Loading
Loading