diff options
| author | Dirk Engling <erdgeist@erdgeist.org> | 2026-10-27 00:28:35 +0100 |
|---|---|---|
| committer | Dirk Engling <erdgeist@erdgeist.org> | 2026-10-27 00:28:35 +0100 |
| commit | 8c482cb2f7019c2491f3bf4f25c4fb5d395ad5da (patch) | |
| tree | 78b43b10eb798aeaa46428a3391c8fde5bd565a5 | |
| parent | b1cfed46d5ac6d73246540ebb915cd1314e27f6e (diff) | |
add more checkers to the sanitizer and integration check runs
| -rw-r--r-- | Makefile | 17 | ||||
| -rw-r--r-- | tests/integration/harness.py | 60 | ||||
| -rw-r--r-- | tests/runner.c | 72 |
3 files changed, 131 insertions, 18 deletions
| @@ -134,6 +134,19 @@ $(TESTBIN): $(TEST_SOURCES) $(TEST_UNITS) $(TEST_HEADERS) | |||
| 134 | check: $(TESTBIN) $(BINARY) | 134 | check: $(TESTBIN) $(BINARY) |
| 135 | ./$(TESTBIN) | 135 | ./$(TESTBIN) |
| 136 | 136 | ||
| 137 | # The same unit tests under the sanitizers. Use clang over $(CC) because the | ||
| 138 | # two toolchains do not check the same things | ||
| 139 | # Override CC_sanitize to use another compiler. | ||
| 140 | CC_sanitize?=clang | ||
| 141 | CFLAGS_sanitize=$(CFLAGS_test) -fsanitize=address,undefined -fno-omit-frame-pointer | ||
| 142 | LDFLAGS_sanitize=$(LDFLAGS) -fsanitize=address,undefined | ||
| 143 | |||
| 144 | $(TESTBIN).sanitize: $(TEST_SOURCES) $(TEST_UNITS) $(TEST_HEADERS) | ||
| 145 | $(CC_sanitize) -o $@ $(CFLAGS_sanitize) $(TEST_SOURCES) $(TEST_UNITS) $(LDFLAGS_sanitize) | ||
| 146 | |||
| 147 | check-sanitize: $(TESTBIN).sanitize | ||
| 148 | ./$(TESTBIN).sanitize | ||
| 149 | |||
| 137 | # Black box tests: these drive the real binary over the wire, so they cover | 150 | # Black box tests: these drive the real binary over the wire, so they cover |
| 138 | # the event loop and the parsers working together, which the unit tests | 151 | # the event loop and the parsers working together, which the unit tests |
| 139 | # cannot reach. Built with the sanitizers on, so the tracker reports its own | 152 | # cannot reach. Built with the sanitizers on, so the tracker reports its own |
| @@ -149,10 +162,10 @@ opentracker.integration: $(SOURCES) $(HEADERS) | |||
| 149 | check-integration: opentracker.integration | 162 | check-integration: opentracker.integration |
| 150 | OPENTRACKER=./opentracker.integration $(PYTHON) -m unittest discover -s tests/integration | 163 | OPENTRACKER=./opentracker.integration $(PYTHON) -m unittest discover -s tests/integration |
| 151 | 164 | ||
| 152 | .PHONY: all check check-integration clean install | 165 | .PHONY: all check check-sanitize check-integration clean install |
| 153 | 166 | ||
| 154 | clean: | 167 | clean: |
| 155 | rm -rf opentracker opentracker.debug opentracker.integration proxy proxy.debug $(TESTBIN) *.o *~ | 168 | rm -rf opentracker opentracker.debug opentracker.integration proxy proxy.debug $(TESTBIN) $(TESTBIN).sanitize *.o *~ |
| 156 | 169 | ||
| 157 | install: | 170 | install: |
| 158 | install -m 755 opentracker $(DESTDIR)$(BINDIR) | 171 | install -m 755 opentracker $(DESTDIR)$(BINDIR) |
diff --git a/tests/integration/harness.py b/tests/integration/harness.py index c8da1e2..868075d 100644 --- a/tests/integration/harness.py +++ b/tests/integration/harness.py | |||
| @@ -7,6 +7,8 @@ reports its own memory errors, which the teardown turns into test failures. | |||
| 7 | """ | 7 | """ |
| 8 | 8 | ||
| 9 | import os | 9 | import os |
| 10 | import shutil | ||
| 11 | import signal | ||
| 10 | import socket | 12 | import socket |
| 11 | import struct | 13 | import struct |
| 12 | import subprocess | 14 | import subprocess |
| @@ -61,15 +63,34 @@ class Tracker: | |||
| 61 | def __init__(self, extra_config=""): | 63 | def __init__(self, extra_config=""): |
| 62 | self.port = free_port() | 64 | self.port = free_port() |
| 63 | self.dir = tempfile.mkdtemp(prefix="ot-integration-") | 65 | self.dir = tempfile.mkdtemp(prefix="ot-integration-") |
| 64 | self.config = os.path.join(self.dir, "opentracker.conf") | 66 | self.log = None |
| 65 | with open(self.config, "w") as handle: | 67 | self.process = None |
| 66 | handle.write("listen.tcp 127.0.0.1:%d\n" % self.port) | 68 | try: |
| 67 | handle.write("listen.udp 127.0.0.1:%d\n" % self.port) | 69 | self.config = os.path.join(self.dir, "opentracker.conf") |
| 68 | handle.write(extra_config) | 70 | with open(self.config, "w") as handle: |
| 69 | self.log = open(os.path.join(self.dir, "tracker.log"), "w+b") | 71 | handle.write("listen.tcp 127.0.0.1:%d\n" % self.port) |
| 70 | environment = dict(os.environ, ASAN_OPTIONS="detect_leaks=0:abort_on_error=0") | 72 | handle.write("listen.udp 127.0.0.1:%d\n" % self.port) |
| 71 | self.process = subprocess.Popen([TRACKER, "-f", self.config], stdout=self.log, stderr=self.log, env=environment) | 73 | handle.write(extra_config) |
| 72 | self._wait_for_port() | 74 | self.log = open(os.path.join(self.dir, "tracker.log"), "w+b") |
| 75 | # Leak detection is left at the sanitizer's own default, which is on | ||
| 76 | # where LeakSanitizer exists and off where it does not. Asking for it | ||
| 77 | # by name makes the tracker refuse to start on macOS and FreeBSD. | ||
| 78 | environment = dict(os.environ, ASAN_OPTIONS="abort_on_error=0") | ||
| 79 | self.process = subprocess.Popen([TRACKER, "-f", self.config], stdout=self.log, stderr=self.log, env=environment) | ||
| 80 | self._wait_for_port() | ||
| 81 | except BaseException: | ||
| 82 | self._cleanup() | ||
| 83 | raise | ||
| 84 | |||
| 85 | def _cleanup(self): | ||
| 86 | """Close what we opened and take the scratch directory with it.""" | ||
| 87 | if self.process is not None and self.process.poll() is None: | ||
| 88 | self.process.kill() | ||
| 89 | self.process.wait() | ||
| 90 | if self.log is not None: | ||
| 91 | self.log.close() | ||
| 92 | self.log = None | ||
| 93 | shutil.rmtree(self.dir, ignore_errors=True) | ||
| 73 | 94 | ||
| 74 | def _wait_for_port(self): | 95 | def _wait_for_port(self): |
| 75 | for _ in range(200): | 96 | for _ in range(200): |
| @@ -84,21 +105,32 @@ class Tracker: | |||
| 84 | raise AssertionError("tracker never listened on %d:\n%s" % (self.port, self.output())) | 105 | raise AssertionError("tracker never listened on %d:\n%s" % (self.port, self.output())) |
| 85 | 106 | ||
| 86 | def output(self): | 107 | def output(self): |
| 108 | if self.log is None: | ||
| 109 | return "" | ||
| 87 | self.log.flush() | 110 | self.log.flush() |
| 88 | self.log.seek(0) | 111 | self.log.seek(0) |
| 89 | return self.log.read().decode("utf-8", "replace") | 112 | return self.log.read().decode("utf-8", "replace") |
| 90 | 113 | ||
| 91 | def stop(self): | 114 | def stop(self): |
| 92 | """Returns anything the tracker complained about on the way out.""" | 115 | """Returns anything the tracker complained about on the way out. |
| 93 | if self.process.poll() is None: | 116 | |
| 94 | self.process.kill() | 117 | The interrupt rather than a kill is what makes the leak check work at |
| 95 | self.process.wait() | 118 | all: LeakSanitizer reports from an exit handler, so a killed tracker |
| 119 | reports nothing and the suite passes over leaks it never looked for. | ||
| 120 | """ | ||
| 121 | if self.process is not None and self.process.poll() is None: | ||
| 122 | self.process.send_signal(signal.SIGINT) | ||
| 123 | try: | ||
| 124 | self.process.wait(timeout=5) | ||
| 125 | except subprocess.TimeoutExpired: | ||
| 126 | self.process.kill() | ||
| 127 | self.process.wait() | ||
| 96 | complaints = [ | 128 | complaints = [ |
| 97 | line | 129 | line |
| 98 | for line in self.output().splitlines() | 130 | for line in self.output().splitlines() |
| 99 | if "Sanitizer" in line or "runtime error" in line or line.startswith("ERROR") | 131 | if "Sanitizer" in line or "runtime error" in line or line.startswith("ERROR") |
| 100 | ] | 132 | ] |
| 101 | self.log.close() | 133 | self._cleanup() |
| 102 | return complaints | 134 | return complaints |
| 103 | 135 | ||
| 104 | # ----------------------------------------------------------- over http | 136 | # ----------------------------------------------------------- over http |
diff --git a/tests/runner.c b/tests/runner.c index a92315e..71a5e2a 100644 --- a/tests/runner.c +++ b/tests/runner.c | |||
| @@ -4,7 +4,11 @@ | |||
| 4 | $id$ */ | 4 | $id$ */ |
| 5 | 5 | ||
| 6 | /* System */ | 6 | /* System */ |
| 7 | #include <stdio.h> | ||
| 8 | #include <stdlib.h> | ||
| 7 | #include <string.h> | 9 | #include <string.h> |
| 10 | #include <sys/wait.h> | ||
| 11 | #include <unistd.h> | ||
| 8 | 12 | ||
| 9 | /* Opentracker */ | 13 | /* Opentracker */ |
| 10 | #include "test.h" | 14 | #include "test.h" |
| @@ -31,15 +35,79 @@ static const struct module modules[] = {{"ot_urlencoded", test_urlencoded}, | |||
| 31 | {"ot_accesslist", test_accesslist}, | 35 | {"ot_accesslist", test_accesslist}, |
| 32 | {NULL, NULL}}; | 36 | {NULL, NULL}}; |
| 33 | 37 | ||
| 34 | /* Run every module, or only those whose name contains argv[1] */ | 38 | struct tally { |
| 39 | unsigned long checks; | ||
| 40 | unsigned long failures; | ||
| 41 | }; | ||
| 42 | |||
| 43 | /* Each module runs in a child of its own, so that a crash or a sanitizer | ||
| 44 | abort costs that module's results rather than every module after it. The | ||
| 45 | child reports its tally through a pipe; one that dies without reporting is | ||
| 46 | counted as a failure under its own name. */ | ||
| 47 | static void run_forked(const struct module *m) { | ||
| 48 | struct tally tally = {0, 0}; | ||
| 49 | int pipefd[2], status = 0; | ||
| 50 | pid_t child; | ||
| 51 | ssize_t got; | ||
| 52 | |||
| 53 | if (pipe(pipefd)) { | ||
| 54 | test_failf(__FILE__, __LINE__, "pipe failed, cannot isolate %s", m->name); | ||
| 55 | return; | ||
| 56 | } | ||
| 57 | |||
| 58 | fflush(stdout); /* or the child inherits a copy and prints it again */ | ||
| 59 | child = fork(); | ||
| 60 | if (child < 0) { | ||
| 61 | close(pipefd[0]); | ||
| 62 | close(pipefd[1]); | ||
| 63 | test_failf(__FILE__, __LINE__, "fork failed, cannot isolate %s", m->name); | ||
| 64 | return; | ||
| 65 | } | ||
| 66 | |||
| 67 | if (!child) { | ||
| 68 | close(pipefd[0]); | ||
| 69 | test_checks = test_failures = 0; | ||
| 70 | m->run(); | ||
| 71 | tally.checks = test_checks; | ||
| 72 | tally.failures = test_failures; | ||
| 73 | if (write(pipefd[1], &tally, sizeof(tally)) != (ssize_t)sizeof(tally)) | ||
| 74 | _exit(1); | ||
| 75 | fflush(stdout); | ||
| 76 | _exit(0); | ||
| 77 | } | ||
| 78 | |||
| 79 | close(pipefd[1]); | ||
| 80 | got = read(pipefd[0], &tally, sizeof(tally)); | ||
| 81 | close(pipefd[0]); | ||
| 82 | while (waitpid(child, &status, 0) < 0) | ||
| 83 | ; | ||
| 84 | |||
| 85 | if (got == (ssize_t)sizeof(tally)) { | ||
| 86 | test_checks += tally.checks; | ||
| 87 | test_failures += tally.failures; | ||
| 88 | } | ||
| 89 | if (got != (ssize_t)sizeof(tally) || !WIFEXITED(status) || WEXITSTATUS(status)) | ||
| 90 | test_failf(__FILE__, __LINE__, "%s did not run to the end", m->name); | ||
| 91 | } | ||
| 92 | |||
| 93 | /* Run every module, or only those whose name contains argv[1]. | ||
| 94 | OT_TEST_FORK=0 keeps everything in one process, which is what a debugger | ||
| 95 | wants. */ | ||
| 35 | int main(int argc, char **argv) { | 96 | int main(int argc, char **argv) { |
| 97 | const char *fork_setting = getenv("OT_TEST_FORK"); | ||
| 98 | int isolate = !fork_setting || strcmp(fork_setting, "0"); | ||
| 36 | const struct module *m; | 99 | const struct module *m; |
| 37 | 100 | ||
| 38 | for (m = modules; m->name; ++m) { | 101 | for (m = modules; m->name; ++m) { |
| 39 | if (argc > 1 && !strstr(m->name, argv[1])) | 102 | if (argc > 1 && !strstr(m->name, argv[1])) |
| 40 | continue; | 103 | continue; |
| 41 | test_module(m->name); | 104 | test_module(m->name); |
| 42 | m->run(); | 105 | if (!isolate) { |
| 106 | m->run(); | ||
| 107 | continue; | ||
| 108 | } | ||
| 109 | test_case("the module runs to the end"); | ||
| 110 | run_forked(m); | ||
| 43 | } | 111 | } |
| 44 | 112 | ||
| 45 | return test_summary(); | 113 | return test_summary(); |
