Skip to content

Commit c763a73

Browse files
Add a statement macro that can assert SIGABRT
The `mlib_assert_aborts` macro checks that the following statement terminates the process with SIGABRT. This relies on `fork()`, so it only works on Unix systems. On Win32 it is a no-op.
1 parent a842546 commit c763a73

File tree

4 files changed

+117
-0
lines changed

4 files changed

+117
-0
lines changed

.clang-format

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ ForEachMacros:
103103
- Q_FOREACH
104104
- BOOST_FOREACH
105105
IfMacros:
106+
- mlib_assert_aborts
106107
- KJ_IF_MAYBE
107108
IncludeBlocks: Preserve
108109
IncludeCategories:

src/common/src/mlib/config.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,8 @@
238238
#define mlib_pragma(...) _Pragma (#__VA_ARGS__) mlib_static_assert (1, "")
239239
#endif
240240

241+
#define MLIB_FUNC MLIB_IF_GNU_LIKE (__func__) MLIB_IF_MSVC (__FUNCTION__)
242+
241243
#define mlib_diagnostic_push() \
242244
MLIB_IF_GNU_LIKE (mlib_pragma (GCC diagnostic push);) \
243245
MLIB_IF_MSVC (mlib_pragma (warning (push));) \

src/common/src/mlib/test.h

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/**
2+
* @file mlib/test.h
3+
* @brief Testing utilities
4+
* @date 2025-01-30
5+
*
6+
* @copyright Copyright (c) 2025
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License");
9+
* you may not use this file except in compliance with the License.
10+
* You may obtain a copy of the License at
11+
*
12+
* http://www.apache.org/licenses/LICENSE-2.0
13+
*
14+
* Unless required by applicable law or agreed to in writing, software
15+
* distributed under the License is distributed on an "AS IS" BASIS,
16+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* See the License for the specific language governing permissions and
18+
* limitations under the License.
19+
*/
20+
#pragma once
21+
22+
#include <mlib/config.h>
23+
#include <stdio.h>
24+
#include <stdlib.h>
25+
26+
/**
27+
* @brief Place this macro at the head of a (compound) statement to assert that
28+
* executing that statement aborts the program with SIGABRT.
29+
*
30+
* Internally, this will fork the calling process and wait for the child process
31+
* to terminate. It asserts that the child exits abnormally with SIGABRT. This
32+
* test assertion is a no-op on Win32, since it does not have a suitable `fork`
33+
* API.
34+
*
35+
* Beware that the child process runs in a forked environment, so it is not
36+
* safe to use any non-fork-safe functionality, and any modifications to program
37+
* state will not be visible in the parent. Behavior of attempting to escape the
38+
* statement (goto/return) is undefined.
39+
*
40+
* If the child process does not abort, it will call `_Exit(71)` to indicate
41+
* to the parent that it did not terminate (the number 71 is chosen arbitrarily)
42+
*
43+
* If the token `debug` is passed as a macro argument, then the forking behavior
44+
* is suppressed, allowing for easier debugging of the statement.
45+
*/
46+
#define mlib_assert_aborts(...) MLIB_PASTE_3 (_mlibAssertAbortsStmt, _, __VA_ARGS__) ()
47+
48+
#ifndef _WIN32
49+
#include <sys/wait.h>
50+
#define _mlibAssertAbortsStmt_() \
51+
for (int once = 1, other_pid = fork (); once; once = 0) \
52+
for (; once; once = 0) \
53+
if (other_pid != 0) { \
54+
/* We are the parent */ \
55+
int wstatus; \
56+
waitpid (other_pid, &wstatus, 0); \
57+
if (WIFEXITED (wstatus)) { \
58+
/* Normal exit! */ \
59+
_mlib_stmt_did_not_abort (__FILE__, MLIB_FUNC, __LINE__, WEXITSTATUS (wstatus)); \
60+
} else if (WIFSIGNALED (wstatus)) { \
61+
/* Signalled */ \
62+
if (WTERMSIG (wstatus) != SIGABRT) { \
63+
fprintf (stderr, \
64+
"%s:%d: [%s]: Child process did not exit with SIGABRT! (Exited %d)\n", \
65+
__FILE__, \
66+
__LINE__, \
67+
MLIB_FUNC, \
68+
WTERMSIG (wstatus)); \
69+
fflush (stderr); \
70+
abort (); \
71+
} \
72+
} \
73+
} else /* We are the child */ \
74+
for (;; _Exit (71)) \
75+
for (;; _Exit (71)) /* Double loop to prevent the block from `break`ing out */
76+
77+
#else
78+
#define _mlibAssertAbortsStmt_() \
79+
if (1) { \
80+
} else
81+
#endif
82+
83+
// Called when an assert-aborts statement does not terminate
84+
static inline void
85+
_mlib_stmt_did_not_abort (const char *file, const char *func, int line, int rc)
86+
{
87+
/* Normal exit! */
88+
if (rc == 71) {
89+
fprintf (stderr, "%s:%d: [%s]: Test case did not abort. The statement completed normally.\n", file, line, func);
90+
} else {
91+
fprintf (stderr, "%s:%d: [%s]: Test case did not abort (Exited %d)\n", file, line, func, rc);
92+
}
93+
fflush (stderr);
94+
abort ();
95+
}
96+
97+
#define _mlibAssertAbortsStmt_debug() \
98+
for (;; _mlib_stmt_did_not_abort (__FILE__, MLIB_FUNC, __LINE__, -1)) \
99+
for (;; _mlib_stmt_did_not_abort (__FILE__, MLIB_FUNC, __LINE__, -1))

src/common/tests/test-mlib.c

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include <mlib/intutil.h>
44
#include <mlib/config.h>
55
#include <mlib/cmp.h>
6+
#include <mlib/test.h>
67

78
#include <stddef.h>
89

@@ -362,13 +363,27 @@ _test_in_range (void)
362363
ASSERT (mlib_in_range (size_t, (size_t) ssize_max + 1u));
363364
}
364365

366+
void
367+
_test_assert_aborts (void)
368+
{
369+
int a = 0;
370+
mlib_assert_aborts () {
371+
a = 4;
372+
abort ();
373+
}
374+
// Parent process is unaffected:
375+
ASSERT (a == 0);
376+
}
377+
378+
365379
void
366380
test_mlib_install (TestSuite *suite)
367381
{
368382
TestSuite_Add (suite, "/mlib/intutil/minmax", _test_minmax);
369383
TestSuite_Add (suite, "/mlib/intutil/upsize", _test_upsize);
370384
TestSuite_Add (suite, "/mlib/cmp", _test_cmp);
371385
TestSuite_Add (suite, "/mlib/in-range", _test_in_range);
386+
TestSuite_Add (suite, "/mlib/assert-aborts", _test_assert_aborts);
372387
}
373388

374389
mlib_diagnostic_pop ();

0 commit comments

Comments
 (0)