diff --git a/Makefile b/Makefile index 7918c36..e545c20 100644 --- a/Makefile +++ b/Makefile @@ -241,3 +241,19 @@ test-recovery: $(BUILD_DIR)/test_recovery ./$(BUILD_DIR)/test_recovery -include $(BUILD_DIR)/tests/integration/test_performance.d $(BUILD_DIR)/tests/integration/test_recovery.d + +# Linux/GNU-compatible linker test; no instrumentation in production objects. +$(BUILD_DIR)/test_reliability: $(BUILD_DIR)/tests/integration/test_reliability.o $(BUILD_DIR)/libxtc.a + $(CC) $(LDFLAGS) -o $@ $^ -lm -Wl,--wrap=malloc,--wrap=calloc,--wrap=realloc,--wrap=free + +.PHONY: test-reliability test-sanitize analyze +test-reliability: test $(BUILD_DIR)/test_reliability + ./$(BUILD_DIR)/test_reliability + +test-sanitize: + ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 UBSAN_OPTIONS=halt_on_error=1 $(MAKE) BUILD_DIR="$(BUILD_DIR)/sanitize" CC=clang CFLAGS="-O1 -g -std=c99 -Wall -Wextra -Wpedantic -Wconversion -Wshadow -Iinclude -fsanitize=address,undefined -fno-omit-frame-pointer" LDFLAGS="-fsanitize=address,undefined" test-reliability test-recovery + +analyze: + $(PYTHON) tools/analyze.py $(CORE_SOURCES) + +-include $(BUILD_DIR)/tests/integration/test_reliability.d diff --git a/docs/AUDIO_ENGINE.md b/docs/AUDIO_ENGINE.md index 9725436..9adbcbb 100644 --- a/docs/AUDIO_ENGINE.md +++ b/docs/AUDIO_ENGINE.md @@ -273,3 +273,10 @@ processing. Callback errors silence the block and reset the failing processor, without rolling back already advanced DSP. Native event instruments can render explicitly into graph instrument buffers. See NATIVE_API.md for exact lifecycle and serialization limits and PERFORMANCE.md for measured workload and budgets. + +## Phase 10 ownership audit + +[RELIABILITY.md](RELIABILITY.md) records resource owners, publication and +reclamation ordering, callback work bounds and repeatable diagnostic tests. +Graph stereo rendering completes both channels before writing caller outputs; +either output may equal a source, but the output buffers must not overlap. diff --git a/docs/BUILD.md b/docs/BUILD.md index 4a58312..18ca434 100644 --- a/docs/BUILD.md +++ b/docs/BUILD.md @@ -222,3 +222,13 @@ No-JACK core/GUI/install checks and Clang ASan/UBSan core/GUI checks also passed (the GUI retains only its existing fontconfig leak suppression). TSan recorder and realtime-handoff checks passed. Two independent optimized source exports and their repacked source archives were byte-identical on this toolchain. + +## Phase 10 reliability audit + +Run `make test-reliability` for the core suites, allocation-failure sweep, +callback allocation/free guard and native reclamation checks. Run +`make test-sanitize` for Clang ASan/UBSan/leak checks and `make analyze` for +Clang static analysis of all core sources. These need installed development +tools; the allocation guard uses a GNU-compatible linker. See +[RELIABILITY.md](RELIABILITY.md) for ownership, audit findings, +full verification commands and remaining limits. diff --git a/docs/RELIABILITY.md b/docs/RELIABILITY.md new file mode 100644 index 0000000..90ae431 --- /dev/null +++ b/docs/RELIABILITY.md @@ -0,0 +1,116 @@ +# Engine correctness and reliability + +Phase 10 audits the existing engine rather than adding musical features. The +review covers graph edits/compilation, offline DSP, plan publication, JACK +callbacks/lifecycle, audio/MIDI source plans and the recorder handoff. + +## Ownership and synchronization + +| Resource | Owner and lifetime | +| --- | --- | +| Editable graph, project, history and asset paths | One serialized control thread (the GUI thread in the application). Borrowed model pointers expire on mutation/destruction. Never traverse these objects in the callback. | +| Compiled graph routing/specifications | Copied during compilation; independent of the editable graph. Immutable after publication. All nodes, including disconnected nodes, are scheduled; only explicit outputs (or compatibility terminals) are mixed. | +| DSP buffers, native instances, phases, meter peaks and graph-local clock | One processing consumer. Reset and meter reads require that owner or quiescence; these are not atomic GUI snapshots. Native callback code must remain loaded until all instances are destroyed. | +| Pending graph | Single control producer publishes with release; the consumer acquires it at a block boundary. The revision is read before releasing the pending slot, preventing producer reuse of its metadata. A full pending slot rejects submission. | +| Active graph | Consumer only until the consumer is stopped/joined. No producer access to mutable DSP state. | +| Retired graph | Consumer releases only after its final access. Control acquires/exchanges the slot and destroys the plan. If the slot is occupied, adoption waits while processing the current graph continues. The callback never destroys plans. | +| Realtime status | Individual atomic counters/revisions; a status read is not a transaction spanning every field. Required pointer/integer atomics must be always lock-free. | +| JACK client, ports and backend control fields | Serialized control thread. JACK owns callback scheduling. Successful deactivate establishes processing quiescence; successful close synchronizes callbacks before freeing context. Failed deactivate/close preserves ownership for retry. Server-loss notification alone is not permission to free context. | +| Preloaded audio/MIDI plans and source buffers | Backend owns them; prepare/replace while stopped, callback alone renders while running. MIDI voices/panic state follow the same rule; panic requests use atomics. | +| Recorder | Caller owns it; backend borrows it. Attach/detach only while stopped. Audio is sole ring producer, control is sole drain/file writer. Acquire/release indices protect samples; errors are sticky. Detach/quiesce before finish/destroy. | +| Widgets and GUI timers | GUI thread only; status polling and retired-plan collection occur there, never through FLTK calls from audio. | + +The public realtime API is SPSC: multiple producers, simultaneous process calls, +collection from arbitrary threads, and destruction during processing are not +supported. Atomics do not remove those ownership requirements. + +## Bounded callback work + +Compilation, topological sorting, asset I/O, allocation and native preparation +are control-thread operations. Graph processing traverses at most 256 nodes, +65536 frames per accepted block and the precomputed edges (at most 256 squared +storage entries). Graph edits validate port direction/type/layout and reject +cycles transactionally using Kahn ordering; fixed public mono ports have no +implicit event/audio conversions. Stereo uses two mono graph passes, independent +native instances and one oscillator phase advance per frame. + +Audio regions, MIDI spans, voices, events and automation are bounded by public +project/MIDI capacities. Rendering scans bounded region/note arrays; automation +uses binary searches in prepared lanes. JACK MIDI accepts at most 256 events per +block; overflow requests panic and reports drops. Recorder push performs at most +one ring-capacity-sized copy and never drains a file. No callback path waits for +a producer, collector, GUI or disk. Realtime pointer handoff has no retry loop. + +Oversized JACK blocks are silenced without graph/source processing. Clearing +backend-provided output and computing peaks still costs the server's frame +count; the server's buffer contract is trusted. Public render APIs likewise +require caller-provided valid buffers. Lock-free does not promise a hardware +latency bound for atomic instructions, libc memory functions or libm. Native +callbacks are trusted: the host cannot prevent their allocation, blocking, +crashes or excessive runtime. Maximum supported capacities are safety bounds, +not a promise that every workload meets its audio deadline. + +## Findings and regression coverage + +Graph rendering previously mixed the left output before reading right-channel +sources. With external input equal to the left output, the right pass could +read already processed samples (gain applied twice). Both passes now finish +source reads before either output is written. Tests check mono input aliasing +and cross-channel source aliasing against exact expected samples. Output buffers +must not overlap each other. A redundant assignment in missing-asset preparation +was also removed after Clang's dead-store diagnostic; behavior is unchanged. + +`test_reliability` uses GNU-compatible linker allocation interposition in the +test executable only. It rejects malloc/calloc/realloc/free during rendering, +fails each preparation allocation in turn until compilation succeeds, verifies +that failed replacement preserves output and revisions, and counts both stereo +native instances through retirement, collection and destruction of a pending +plan. It repeats this lifecycle 100 times. A maximum 256-node reverse-insertion +chain checks dependency scheduling, long-cycle rejection/rollback and compiled +snapshot independence. The pthread handoff test submits 2000 native graphs +while processing, checks whole-block output/revision consistency, asserts that +prepare/destruction run on the control thread, and balances all native lifetimes +after joining the consumer. + +Existing backend fault tests cover repeated start/stop, partial registration, +callback setup, open/activate/deactivate/close failures and retries, server loss, +rate/buffer changes, missing buffers, MIDI disconnect/panic, and recorder detach. +Other suites cover deterministic variable-block DSP, finite-output handling, +project errors, record-ring saturation and concurrent draining, and crash/write +failure preservation. No critical realtime safety defect remains known within +these contracts; these tests are evidence for tested paths, not a formal proof. + +## Reproducing the audit + +From the repository root: + +```sh +make BUILD_DIR=build/audit all gui test-reliability test-recovery +make BUILD_DIR=build/audit test-sanitize +make analyze +make BUILD_DIR=build/audit test-gui test-gui-jack test-jack test-jack-audio test-jack-midi +make BUILD_DIR=build/nojack PKG_CONFIG=false all test-reliability gui +``` + +`test-sanitize` needs installed Clang and runs core, allocation-failure and recovery +tests with ASan/UBSan and Linux's default ASan leak checking. `analyze` uses +Clang's path-sensitive analyzer on every core translation unit, including JACK +when its development files are available, and fails on diagnostics. Python and +Clang are development tools, not new runtime dependencies. The allocation guard +requires a GNU-compatible linker. Live tests require an existing JACK-compatible +server; GUI tests use installed FLTK and Xvfb. No target installs dependencies or +changes server/system configuration. + +Phase 10 verification on GCC 16.2.1 / Clang 23.1.3 passed normal and no-JACK builds, +all core tests, ASan/UBSan/leak checks, Clang static analysis, GUI/mock and live +JACK integrations, plus GCC TSan on the pthread native-publication and recorder +concurrency tests. The GUI also passed ASan/UBSan with the existing +`tests/integration/lsan.supp` fontconfig-only suppression; core tests use no leak +suppression. The live graph test observed both replacement gains, ten restart +cycles and zero xruns during its five-second software run. + +Physical audio devices, hard realtime scheduling, multi-hour endurance and +arbitrary third-party native callbacks remain unqualified. Sanitizers change +timing and should not be used to measure production realtime performance. See +PERFORMANCE.md for workload measurements, RECOVERY.md for process-crash limits, +and NATIVE_API.md for the trusted extension boundary. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 99ef855..a85ef4a 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -136,6 +136,17 @@ physical-device/endurance qualification is claimed. - No known critical real-time safety defects remain. - Resource ownership and cleanup behavior are documented. +Implemented: graph/DSP and ownership audit; stereo source-alias correction with +exact-output regression tests; maximum-size reverse-order graph/cycle rollback; +allocation-failure sweeps and callback allocation/free guards; 2000 concurrent +native-plan replacements with control-thread destruction and balanced lifetimes; +repeatable ASan/UBSan/leak and Clang static-analysis targets. Core, no-JACK, +GUI/mock/live JACK, recovery and TSan publication/recorder tests pass. See +RELIABILITY.md for ownership, synchronization, bounded-work analysis, commands +and evidence. Physical-device/endurance validation and trusted native callback +behavior remain explicit limits; no critical realtime safety defect is known +within the documented contracts. + ## Phase 11 — Complete Minimal Music-Making Workflow **Goal:** Ensure a user can create, save, reopen, and play a small musical project without manually editing project files. diff --git a/include/xtc/xtc_engine.h b/include/xtc/xtc_engine.h index 2467aee..373976f 100644 --- a/include/xtc/xtc_engine.h +++ b/include/xtc/xtc_engine.h @@ -257,7 +257,7 @@ void xtc_plan_destroy(xtc_plan *plan); * Unconnected gain/meter/output inputs receive external input. */ /* Separate mono source buffers; NULL is silence. Existing wrappers omit MIDI. */ /* Stereo graph with paired source buffers and a mono external input duplicated - * to both sides. Output buffers must be distinct and must not alias sources. + * to both sides. Output buffers must not overlap each other; either may equal a source. * Stereo oscillator phase advances once per frame. NULL right selects mono. */ /** * @brief Processes a block with separate stereo timeline and instrument sources. diff --git a/src/audio/arrangement.c b/src/audio/arrangement.c index 6582d53..bc267df 100644 --- a/src/audio/arrangement.c +++ b/src/audio/arrangement.c @@ -169,7 +169,6 @@ xtc_result xtc_audio_plan_create(const xtc_project *project, unsigned int rate, if (!plan->first_issue[0]) strcpy(plan->first_issue, clip->asset_path); } - result = XTC_OK; total += plan->waves[a].frames; if (total > 67108864U) { result = XTC_ERR_CAPACITY; diff --git a/src/engine/offline_engine.c b/src/engine/offline_engine.c index 1bb5e3b..bae9525 100644 --- a/src/engine/offline_engine.c +++ b/src/engine/offline_engine.c @@ -208,7 +208,6 @@ xtc_result xtc_plan_process_stereo(xtc_plan *plan, const float *input, const flo for (channel = 0; channel < (right ? 2U : 1U); ++channel) { const float *audio_source = channel ? timeline_right : timeline; const float *midi_source = channel ? instrument_right : instrument; - float *destination = channel ? right : output; for (index = 0U; index < plan->node_count; ++index) { size_t node = plan->order[index]; xtc_node spec = plan->nodes[node]; @@ -261,6 +260,10 @@ xtc_result xtc_plan_process_stereo(xtc_plan *plan, const float *input, const flo if (spec.type == XTC_NODE_METER) plan->peaks[node] = channel && plan->peaks[node] > peak ? plan->peaks[node] : peak; } + } + /* Finish both channels before writing either caller buffer. */ + for (channel = 0; channel < (right ? 2U : 1U); ++channel) { + float *destination = channel ? right : output; for (frame = 0U; frame < frame_count; ++frame) { float sample = 0.0f; for (index = 0U; index < plan->node_count; ++index) diff --git a/tests/integration/README.md b/tests/integration/README.md index 2ddc19f..8f39e34 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -182,3 +182,13 @@ No-JACK core/GUI/install checks and Clang ASan/UBSan core/GUI checks also passed (the GUI retains only its existing fontconfig leak suppression). TSan recorder and realtime-handoff checks passed. Two independent optimized source exports and their repacked source archives were byte-identical on this toolchain. + +## Phase 10 reliability audit + +Run `make test-reliability` for the core suites, allocation-failure sweep, +callback allocation/free guard and native reclamation checks. Run +`make test-sanitize` for Clang ASan/UBSan/leak checks and `make analyze` for +Clang static analysis of all core sources. These need installed development +tools; the allocation guard uses a GNU-compatible linker. See +[RELIABILITY.md](../../docs/RELIABILITY.md) for ownership, audit findings, +full verification commands and remaining limits. diff --git a/tests/integration/test_reliability.c b/tests/integration/test_reliability.c new file mode 100644 index 0000000..2541560 --- /dev/null +++ b/tests/integration/test_reliability.c @@ -0,0 +1,130 @@ +/* GNU ld allocation interposition: fail preparation at every allocation and + * reject host allocation/reclamation inside processing. No production hooks. */ +#include "xtc/xtc_realtime.h" +#include "xtc/xtc_native.h" +#include +#include +#include + +void *__real_malloc(size_t size); +void *__real_calloc(size_t count, size_t size); +void *__real_realloc(void *pointer, size_t size); +void __real_free(void *pointer); +static int processing, fail_after = -1; +static unsigned int prepared, destroyed; +static int fail_allocation(void) +{ + assert(!processing); + if (fail_after < 0) return 0; + if (fail_after == 0) return 1; + --fail_after; + return 0; +} +void *__wrap_malloc(size_t size) { return fail_allocation() ? NULL : __real_malloc(size); } +void *__wrap_calloc(size_t count, size_t size) { return fail_allocation() ? NULL : __real_calloc(count, size); } +void *__wrap_realloc(void *pointer, size_t size) { return fail_allocation() ? NULL : __real_realloc(pointer, size); } +void __wrap_free(void *pointer) { assert(!processing); __real_free(pointer); } +static xtc_result prepare(double rate, size_t frames, float parameter, void **state) +{ + (void)rate; (void)frames; + assert(!processing); + *state = malloc(sizeof(float)); + if (!*state) return XTC_ERR_OUT_OF_MEMORY; + *(float *)*state = parameter; + ++prepared; + return XTC_OK; +} +static void reset(void *state) { (void)state; } +static xtc_result process(void *state, const xtc_native_block *block) +{ + size_t i; + assert(processing); + for (i = 0; i < block->frames; ++i) + block->output[i] = block->input[i] * *(float *)state; + return XTC_OK; +} +static void destroy(void *state) +{ + assert(!processing); + if (state) ++destroyed; + free(state); +} +static void render(xtc_realtime *engine, float expected) +{ + float input[17], left[17], right[17]; + size_t i; + for (i = 0; i < 17; ++i) input[i] = 1; + processing = 1; + assert(xtc_realtime_process_stereo(engine, input, NULL, NULL, NULL, NULL, left, right, 17) == XTC_OK); + processing = 0; + for (i = 0; i < 17; ++i) assert(left[i] == expected && right[i] == expected); +} +/* Maximum-sized reverse-insertion chain: long cycle rollback must preserve + * routing, and compiled snapshots survive destruction of the editable model. */ +static void graph_limits(void) +{ + xtc_graph *graph = NULL; + xtc_plan *plan = NULL; + size_t nodes[256], i; + float input[3] = {1, -0.5f, 0.25f}, output[3]; + assert(xtc_graph_create(256, &graph) == XTC_OK); + for (i = 0; i < 256; ++i) + assert(xtc_graph_add_gain(graph, 1, &nodes[i]) == XTC_OK); + for (i = 255; i > 0; --i) + assert(xtc_graph_connect(graph, nodes[i], nodes[i - 1]) == XTC_OK); + assert(xtc_graph_connect(graph, nodes[0], nodes[255]) == XTC_ERR_GRAPH_CYCLE); + assert(xtc_graph_validate(graph) == XTC_OK); + assert(xtc_graph_compile(graph, 48000, 3, &plan) == XTC_OK); + xtc_graph_destroy(graph); + processing = 1; + assert(xtc_plan_process(plan, input, output, 3) == XTC_OK); + processing = 0; + for (i = 0; i < 3; ++i) assert(output[i] == input[i]); + xtc_plan_destroy(plan); +} + +int main(void) +{ + graph_limits(); + xtc_native_descriptor descriptor = {XTC_NATIVE_ABI_VERSION, sizeof(descriptor), + "reliability.gain", XTC_NATIVE_EFFECT, prepare, reset, process, destroy}; + xtc_graph *graph = NULL; + size_t node; + unsigned int iteration; + assert(xtc_graph_create(1, &graph) == XTC_OK); + assert(xtc_graph_add_native(graph, &descriptor, 2, &node) == XTC_OK); + for (iteration = 0; iteration < 100; ++iteration) { + xtc_realtime *engine = NULL; + int failure, succeeded = 0; + assert(xtc_realtime_create(48000, 17, &engine) == XTC_OK); + assert(xtc_realtime_submit(engine, graph) == XTC_OK); + render(engine, 2); + for (failure = 0; failure < 64; ++failure) { + xtc_result result; + fail_after = failure; + result = xtc_realtime_submit(engine, graph); + fail_after = -1; + if (result == XTC_OK) { succeeded = 1; break; } + assert(result == XTC_ERR_OUT_OF_MEMORY); + /* Failed replacement preserves the active DSP and revisions. */ + { + xtc_realtime_status status; + assert(xtc_realtime_get_status(engine, &status) == XTC_OK); + assert(status.active_revision == 1 && status.submitted_revision == 1); + } + render(engine, 2); + } + assert(succeeded); + render(engine, 2); /* Leaves previous native instances retired. */ + assert(destroyed + 4 == prepared); + xtc_realtime_collect(engine); + assert(destroyed + 2 == prepared); + assert(xtc_realtime_submit(engine, graph) == XTC_OK); + /* Destruction must also clean an unadopted pending plan. */ + xtc_realtime_destroy(engine); + assert(prepared == destroyed); + } + xtc_graph_destroy(graph); + puts("Allocation failures, callback allocation guard, and native reclamation passed."); + return 0; +} diff --git a/tests/unit/test_offline.c b/tests/unit/test_offline.c index fd2ea87..5c48fb7 100644 --- a/tests/unit/test_offline.c +++ b/tests/unit/test_offline.c @@ -79,6 +79,33 @@ static int routing(void) return 0; } +/* External mono input is read by both channels, including when left aliases it. */ +static int stereo_alias(void) +{ + xtc_graph *graph = NULL; + xtc_plan *plan = NULL; + size_t gain, audio, output, i; + float left[4] = {1, 2, 3, 4}, right[4]; + CHECK(xtc_graph_create(3, &graph) == XTC_OK); + CHECK(xtc_graph_add_gain(graph, 2, &gain) == XTC_OK); + CHECK(xtc_graph_add_audio(graph, &audio) == XTC_OK); + CHECK(xtc_graph_add_output(graph, &output) == XTC_OK); + CHECK(xtc_graph_connect(graph, gain, output) == XTC_OK); + CHECK(xtc_graph_connect(graph, audio, output) == XTC_OK); + CHECK(xtc_graph_compile(graph, 48000, 4, &plan) == XTC_OK); + CHECK(xtc_plan_process_stereo(plan, left, NULL, NULL, NULL, NULL, left, right, 4) == XTC_OK); + for (i = 0; i < 4; ++i) + CHECK(left[i] == 2 * (float)(i + 1) && right[i] == left[i]); + for (i = 0; i < 4; ++i) right[i] = 10 + (float)i; + /* Cross-channel source alias must also retain the original source samples. */ + CHECK(xtc_plan_process_stereo(plan, NULL, right, left, NULL, NULL, left, right, 4) == XTC_OK); + for (i = 0; i < 4; ++i) + CHECK(left[i] == 10 + (float)i && right[i] == 2 * (float)(i + 1)); + xtc_plan_destroy(plan); + xtc_graph_destroy(graph); + return 0; +} + static int validation(void) { xtc_graph *graph = NULL; @@ -133,6 +160,7 @@ int main(void) CHECK(oscillator() == 0); CHECK(routing() == 0); CHECK(validation() == 0); + CHECK(stereo_alias() == 0); puts("All offline graph tests passed."); return 0; } diff --git a/tests/unit/test_realtime.c b/tests/unit/test_realtime.c index faaeeb8..3c61cc2 100644 --- a/tests/unit/test_realtime.c +++ b/tests/unit/test_realtime.c @@ -1,11 +1,46 @@ #define _POSIX_C_SOURCE 200809L #include "xtc/xtc_realtime.h" +#include "xtc/xtc_native.h" +#include #include #include #include #include #define CHECK(c) do { if (!(c)) { fprintf(stderr,"FAIL %s:%d: %s\n",__FILE__,__LINE__,#c); return 1; } } while (0) +/* Count native lifetimes on the serialized producer while a real consumer + * traverses plans. Destruction on that consumer is always a contract violation. */ +static pthread_t control_thread; +static unsigned int prepared, destroyed; +static xtc_result prepare(double rate, size_t frames, float gain, void **state) +{ + (void)rate; (void)frames; + assert(pthread_equal(pthread_self(), control_thread)); + *state = malloc(sizeof(float)); + if (!*state) return XTC_ERR_OUT_OF_MEMORY; + *(float *)*state = gain; + ++prepared; + return XTC_OK; +} +static void reset(void *state) { (void)state; } +static xtc_result process(void *state, const xtc_native_block *block) +{ + size_t i; + for (i = 0; i < block->frames; ++i) + block->output[i] = block->input[i] * *(float *)state; + return XTC_OK; +} +static void destroy(void *state) +{ + assert(pthread_equal(pthread_self(), control_thread)); + if (state) ++destroyed; + free(state); +} +static const xtc_native_descriptor descriptor = { + XTC_NATIVE_ABI_VERSION, sizeof(xtc_native_descriptor), "concurrent.gain", + XTC_NATIVE_EFFECT, prepare, reset, process, destroy +}; + typedef struct { xtc_realtime *engine; unsigned int stop, failed; } worker_state; static void *worker(void *context) { @@ -40,9 +75,9 @@ static int run(void) CHECK(xtc_realtime_create(48000.0, 32U, &engine) == XTC_OK); CHECK(xtc_realtime_process(engine, input, output, 4U) == XTC_OK && output[0] == 0.0f); CHECK(xtc_graph_create(1U, &one) == XTC_OK); - CHECK(xtc_graph_add_gain(one, 1.0f, &node) == XTC_OK); + CHECK(xtc_graph_add_native(one, &descriptor, 1.0f, &node) == XTC_OK); CHECK(xtc_graph_create(1U, &two) == XTC_OK); - CHECK(xtc_graph_add_gain(two, 2.0f, &node) == XTC_OK); + CHECK(xtc_graph_add_native(two, &descriptor, 2.0f, &node) == XTC_OK); CHECK(xtc_realtime_submit(engine, one) == XTC_OK); CHECK(xtc_realtime_submit(engine, two) == XTC_ERR_CAPACITY); CHECK(xtc_realtime_get_status(engine, &status) == XTC_OK && status.submitted_revision == 1U && status.active_revision == 0U); @@ -68,6 +103,7 @@ static int run(void) CHECK(state.failed == 0U); CHECK(xtc_realtime_get_status(engine, &status) == XTC_OK && status.processed_blocks > 3U); xtc_realtime_destroy(engine); /* Also cleans pending and retired plans. */ + CHECK(prepared == destroyed); xtc_graph_destroy(one); xtc_graph_destroy(two); for (i = 0U; i < 100U; ++i) { CHECK(xtc_realtime_create(48000.0, 4U, &engine) == XTC_OK); @@ -80,6 +116,7 @@ static int run(void) } int main(void) { + control_thread = pthread_self(); CHECK(run() == 0); puts("Realtime handoff and concurrent replacement tests passed."); return 0; diff --git a/tools/analyze.py b/tools/analyze.py new file mode 100644 index 0000000..aa71627 --- /dev/null +++ b/tools/analyze.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +"""Run Clang's path-sensitive analyzer; any diagnostic fails the audit.""" +import pathlib +import shutil +import subprocess +import sys + +if not shutil.which("clang"): + sys.exit("clang is required for make analyze") +jack = subprocess.run(["pkg-config", "--cflags", "jack"], capture_output=True, text=True) if shutil.which("pkg-config") else None +for source in sys.argv[1:]: + flags = [] + if source == "src/backend/jack_backend.c" and jack and jack.returncode == 0: + import shlex + flags = ["-DXTC_HAVE_JACK", *shlex.split(jack.stdout)] + result = subprocess.run(["clang", "--analyze", "-std=c99", "-Iinclude", + "-Xanalyzer", "-analyzer-output=text", *flags, + str(pathlib.Path(source))], capture_output=True, text=True) + if result.returncode or result.stderr: + sys.stderr.write(result.stdout + result.stderr) + sys.exit(result.returncode or 1) +print("Clang static analysis passed for all core sources.")