Skip to content

test: achieve 100% test coverage for core audio engine#209

Merged
sudip-mondal-2002 merged 8 commits into
sudip-mondal-2002:mainfrom
MohitBareja16:feature/audio-engine-coverage
May 25, 2026
Merged

test: achieve 100% test coverage for core audio engine#209
sudip-mondal-2002 merged 8 commits into
sudip-mondal-2002:mainfrom
MohitBareja16:feature/audio-engine-coverage

Conversation

@MohitBareja16

@MohitBareja16 MohitBareja16 commented May 23, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Achieves 100% unit test coverage for the core DSP hot path (audio_engine_process.cpp), the engine API (audio_engine_api.cpp), and the chain manipulation logic (audio_engine_chain.cpp). Also resolves serialization bugs related to legacy preset migration and graph reconstruction.

Specifically, this adds a new test suite (test_audio_engine.cpp) covering:

  • Core DSP: Zero-output silence routing, exact input/output gain scaling, and hard ±1.0 safety clipping.
  • Metering: RMS calculation bounds (verifying correct behavior under Silence vs DC Tone and confirming internal FFT ring buffer rotation).
  • Architecture: Verifies lock-free SPSCQueue behavior ensuring the GUI threads can safely push parameters, bypasses, and gain changes without stalling the audio thread.
  • Topology: Complete coverage of insert_effect, remove_effect, move_effect, restore_effects_state, and the tuner_tap subsystem.
  • Serialization: Added strict JSON validation and fixed the IR Cabinet legacy instantiation bug.

Related Issue

Fixes #182

Type of Change

  • 🐛 Bug fix
  • ✨ New feature
  • ♻️ Refactor / code cleanup
  • 📝 Documentation
  • ✅ Tests
  • 🔧 Build / CI / Configuration

How Was This Tested?

  • Platform(s) tested on: Linux (Ubuntu/Debian)
  • Test command run: cmake --build build && ./build/amplitron-tests
  • Manual test steps (if applicable): Ran local gcovr tools after test execution to mathematically verify all lines in the DSP process_audio callback were successfully executed during the test suite.

Checklist

  • Code compiles and builds successfully on my platform
  • All existing tests pass (./amplitron-tests)
  • New tests added for new functionality (if applicable)
  • Documentation updated (README, code comments) if applicable
  • No blocking calls on the audio thread (no std::mutex::lock() on the hot path)
  • Tested on: Linux

Screenshots / Demo

N/A - Backend coverage and logic fixes, no GUI alterations.

Summary by CodeRabbit

  • Tests
    • Added a comprehensive suite of native-desktop audio engine tests, including a recorder-focused test that exercises start/process/stop recording and validates recorder state and output file.
    • Expanded coverage for audio processing, effect-chain operations, metronome/BPM behavior, tuner taps, command-queue interactions, clipping and invalid-input branches; included in native desktop test builds.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 23, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds a new desktop test file tests/test_audio_engine.cpp exercising AudioEngine processing, effect-chain operations, API behaviors, command-queue draining, and recorder start/stop, registers the file in CMake, and updates the audio command enum plus drain logic to remove metronome cases.

Changes

AudioEngine Test Coverage

Layer / File(s) Summary
Test build setup and harness initialization
CMakeLists.txt, tests/test_audio_engine.cpp
tests/test_audio_engine.cpp registered in CMake; test file adds includes and harness setup used by subsequent tests.
Audio processing tests
tests/test_audio_engine.cpp
Verifies silence output, input/output gain scaling, safety-limit clamping to ±1.0, and RMS analyzer differences between silence and a tone.
Effect chain management tests
tests/test_audio_engine.cpp
Validates effect container operations: empty/add/remove/insert/clear and move operations including invalid/self-move branches.
Metronome API and processing
tests/test_audio_engine.cpp
Covers metronome enable/disable toggling, BPM bounds enforcement, volume setting, and processing to generate metronome clicks.
Buffer size & analyzer snapshot
tests/test_audio_engine.cpp
Checks suggested buffer size and analyzer snapshot copying including null/invalid-sequence failure branches.
Tuner tap & restore effects state
tests/test_audio_engine.cpp
Tests tuner-tap set/clear and processing; verifies restoring an effect list reproduces expected ordering and size.
Command enum and drain change
src/audio/spsc_queue.h, src/audio/audio_engine_process.cpp
Replaces metronome command variants with effect-management commands in AudioCommand::Type and removes metronome-handling cases from AudioEngine::drain_commands().
Command-queue draining and state validation
tests/test_audio_engine.cpp
Interleaves gain/effect/parameter pushes, processes audio once to drain queues, and asserts effect enabled/mix state, parameter values, and input gain.
Recorder start/process/stop branch
tests/test_audio_engine.cpp
Starts recorder to a temp WAV, processes audio to hit recorder write path, stops recorder, and asserts recording state plus non-empty file creation.
Full coverage sweep
tests/test_audio_engine.cpp
Coordinated run exercising invalid sample-rate handling, clipping detection under extreme gains, invalid effect-index queue handling, and metronome BPM/mid-click disable behavior.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • sudip-mondal-2002/Amplitron#128: Removes metronome-related handling from drain_commands and deletes metronome command variants — related to the enum/drain changes in this PR.
  • sudip-mondal-2002/Amplitron#130: Changes affecting metronome command handling and transport state plumbing that this PR also modifies.

Suggested labels

gssoc:approved, quality:exceptional

Suggested reviewers

  • sudip-mondal-2002

Poem

🐰 I hopped through buffers, tests in paw,

Checked silence, gain, and clamping law,
Chains reordered, snapshots read,
Commands queued and recorder fed,
A little rabbit cheers the build—hip, hop, hurrah!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Changes to spsc_queue.h and audio_engine_process.cpp (removing metronome commands) are unrelated to the stated test-coverage objective in issue #182 and constitute scope creep beyond test additions. Either justify metronome command changes as part of issue #182 scope or move them to a separate issue/PR to keep this PR focused on test coverage.
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main objective: adding comprehensive test coverage for the core audio engine to achieve 100% coverage on key DSP hot paths.
Linked Issues check ✅ Passed The PR successfully addresses issue #182 by adding comprehensive test coverage for audio_engine_process.cpp (previously 0%), audio_engine_api.cpp gaps, and audio_engine_chain.cpp, covering all specified DSP, metering, and topology requirements.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented May 23, 2026

Copy link
Copy Markdown
Contributor

Code Coverage Report 📊

Line Coverage: 79.1%

✅ Coverage meets threshold: 79.1% >= 60%

Full Coverage Summary
Summary coverage rate:
  lines......: 79.1% (3325 of 4203 lines)
  functions..: 89.4% (471 of 527 functions)
  branches...: no data found

@github-actions

github-actions Bot commented May 23, 2026

Copy link
Copy Markdown
Contributor

PR Preview Removed

The GitHub Pages preview for this PR has been removed because the PR was closed.

@sudip-mondal-2002 sudip-mondal-2002 added High Impact Significant user-facing improvement Audio Processing Related to DSP, effects, or signal processing level:advanced Advanced task · 45 GSSoC points type:testing Unit tests, integration tests, or test coverage labels May 24, 2026

@sudip-mondal-2002 sudip-mondal-2002 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image some files has a little less coverage, otherwise LGTM

Please keep .wav files inside tests/assets folder

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (4)
tests/test_audio_engine.cpp (4)

261-277: ⚡ Quick win

Recorder test should validate write side effects and avoid shared filename collisions.

State toggles alone don’t prove samples were written. Also, a fixed filename can collide under parallel runs. Use a unique temp path, assert output file exists/non-empty after stop, then clean it up.

💡 Suggested direction
+ `#include` <filesystem>
...
- engine.recorder().start("test_dummy_record.wav", 48000, 1);
+ const auto record_path =
+     std::filesystem::temp_directory_path() / "amplitron_test_dummy_record.wav";
+ engine.recorder().start(record_path.string(), 48000, 1);
...
  engine.recorder().stop();
  ASSERT_EQ(engine.recorder().is_recording(), false);
+ ASSERT_EQ(std::filesystem::exists(record_path), true);
+ ASSERT_TRUE(std::filesystem::file_size(record_path) > 0u);
+ std::filesystem::remove(record_path);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_audio_engine.cpp` around lines 261 - 277, The test
AudioEngineProcess_RecorderBranch only checks recorder state transitions; update
it to use a unique temporary filename (e.g., generated via
std::filesystem::temp_directory_path + unique suffix) when calling
AudioEngine::recorder().start so parallel runs don’t collide, call
engine.process_audio(...) to trigger recorder_.write_samples(), then after
engine.recorder().stop assert the output file exists and its size is > 0 to
prove samples were written (and finally remove the temp file as cleanup); keep
references to AudioEngine, engine.process_audio, and
engine.recorder().start/stop/is_recording to locate and change the test.

140-143: ⚡ Quick win

Add post-conditions for invalid/self move_effect calls.

These calls currently only execute branches; they don’t verify state integrity after invalid/self moves. Add assertions that order/count remain unchanged.

♻️ Proposed assertion additions
     engine.move_effect(-1, 0);
+    ASSERT_EQ(engine.effects().size(), 2u);
+    ASSERT_EQ(engine.effects()[0], od);
+    ASSERT_EQ(engine.effects()[1], dist);
     engine.move_effect(0, 5);
+    ASSERT_EQ(engine.effects().size(), 2u);
+    ASSERT_EQ(engine.effects()[0], od);
+    ASSERT_EQ(engine.effects()[1], dist);
     engine.move_effect(0, 0); // Self-move
+    ASSERT_EQ(engine.effects().size(), 2u);
+    ASSERT_EQ(engine.effects()[0], od);
+    ASSERT_EQ(engine.effects()[1], dist);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_audio_engine.cpp` around lines 140 - 143, Before calling the three
move_effect invocations, snapshot the engine's ordering state (e.g., call
engine.order_count() and capture each order entry via engine.get_order(i) or the
equivalent accessor), then after each invalid/self call assert that the order
count remains equal to the snapshot and that every order entry equals the
corresponding saved snapshot entry; apply these assertions for
engine.move_effect(-1, 0), engine.move_effect(0, 5) and engine.move_effect(0, 0)
to ensure no changes.

162-170: ⚡ Quick win

Metronome branch is exercised, but click generation is not validated.

The loop has no assertion proving a click was emitted. Add an output assertion (e.g., detect any non-zero sample) and make iteration count deterministic from BPM/sample-rate assumptions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_audio_engine.cpp` around lines 162 - 170, The test currently
exercises engine.initialize(), engine.set_buffer_size(), and
engine.process_audio() but never asserts that the metronome click was produced;
modify the test to compute a deterministic number of process_audio calls from
the metronome BPM and the engine/sample-rate (convert BPM to samples per click
using sampleRate * 60 / BPM, then divide by buffer size) so the loop runs
exactly until a click should occur, and add an assertion after each
process_audio call that checks the out buffer (the vector passed to
engine.process_audio) for any non-zero sample to validate that a click was
emitted; reference the existing engine.process_audio, engine.set_buffer_size,
and the out vector when adding the detection/assertion.

215-218: Tuner tap branch should already be reached by this test (no extra initialization needed).

  • set_tuner_tap() sets topology_dirty_ = true and prepares the tap (set_sample_rate(sample_rate_), reset()), so the first process_audio() call should assign audio_shadow_tuner_ from tuner_tap_.
  • The processing call is guarded by audio_shadow_tuner_ && audio_shadow_tuner_->is_enabled(). Effect::enabled_ defaults to true, and the test’s tap (Distortion) doesn’t disable it, so the guard should pass.
  • process_audio() also resizes process_buffer_/process_buffer_right_ to frame_count, so set_buffer_size()/initialize() isn’t required for this path.

Optional: to make the coverage deterministic, use a test Effect that records whether its process() was called and assert it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_audio_engine.cpp` around lines 215 - 218, The test is relying on
process_audio() to exercise the tuner tap path but doesn't assert that the tap's
process() ran; ensure set_tuner_tap() is called (so topology_dirty_ is true and
tuner_tap_ gets prepared) and then call process_audio() so audio_shadow_tuner_
is assigned from tuner_tap_ and the guard (audio_shadow_tuner_ &&
audio_shadow_tuner_->is_enabled()) passes (Effect::enabled_ defaults true and
Distortion doesn't disable it); to make the test deterministic, replace or wrap
the Distortion tap with a simple test Effect that records a flag when its
process() is invoked and add an assertion that that flag is set after calling
process_audio(); no additional set_buffer_size()/initialize() calls are required
because process_audio() resizes process_buffer_/process_buffer_right_ to
frame_count.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/test_audio_engine.cpp`:
- Around line 261-277: The test AudioEngineProcess_RecorderBranch only checks
recorder state transitions; update it to use a unique temporary filename (e.g.,
generated via std::filesystem::temp_directory_path + unique suffix) when calling
AudioEngine::recorder().start so parallel runs don’t collide, call
engine.process_audio(...) to trigger recorder_.write_samples(), then after
engine.recorder().stop assert the output file exists and its size is > 0 to
prove samples were written (and finally remove the temp file as cleanup); keep
references to AudioEngine, engine.process_audio, and
engine.recorder().start/stop/is_recording to locate and change the test.
- Around line 140-143: Before calling the three move_effect invocations,
snapshot the engine's ordering state (e.g., call engine.order_count() and
capture each order entry via engine.get_order(i) or the equivalent accessor),
then after each invalid/self call assert that the order count remains equal to
the snapshot and that every order entry equals the corresponding saved snapshot
entry; apply these assertions for engine.move_effect(-1, 0),
engine.move_effect(0, 5) and engine.move_effect(0, 0) to ensure no changes.
- Around line 162-170: The test currently exercises engine.initialize(),
engine.set_buffer_size(), and engine.process_audio() but never asserts that the
metronome click was produced; modify the test to compute a deterministic number
of process_audio calls from the metronome BPM and the engine/sample-rate
(convert BPM to samples per click using sampleRate * 60 / BPM, then divide by
buffer size) so the loop runs exactly until a click should occur, and add an
assertion after each process_audio call that checks the out buffer (the vector
passed to engine.process_audio) for any non-zero sample to validate that a click
was emitted; reference the existing engine.process_audio,
engine.set_buffer_size, and the out vector when adding the detection/assertion.
- Around line 215-218: The test is relying on process_audio() to exercise the
tuner tap path but doesn't assert that the tap's process() ran; ensure
set_tuner_tap() is called (so topology_dirty_ is true and tuner_tap_ gets
prepared) and then call process_audio() so audio_shadow_tuner_ is assigned from
tuner_tap_ and the guard (audio_shadow_tuner_ &&
audio_shadow_tuner_->is_enabled()) passes (Effect::enabled_ defaults true and
Distortion doesn't disable it); to make the test deterministic, replace or wrap
the Distortion tap with a simple test Effect that records a flag when its
process() is invoked and add an assertion that that flag is set after calling
process_audio(); no additional set_buffer_size()/initialize() calls are required
because process_audio() resizes process_buffer_/process_buffer_right_ to
frame_count.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a95ed8ed-2bab-49ff-8805-3f1a3c5f8edc

📥 Commits

Reviewing files that changed from the base of the PR and between 96efa28 and ed3d064.

⛔ Files ignored due to path filters (1)
  • test_dummy_record.wav is excluded by !**/*.wav
📒 Files selected for processing (1)
  • tests/test_audio_engine.cpp

@MohitBareja16

Copy link
Copy Markdown
Collaborator Author

@sudip-mondal-2002 LGTM from my side

@MohitBareja16 MohitBareja16 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked this file and it is covering more than 90% test coverage in all the files

@sudip-mondal-2002

Copy link
Copy Markdown
Owner

@MohitBareja16

@sudip-mondal-2002 LGTM from my side

image

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/test_audio_engine.cpp`:
- Around line 185-203: The test computes timing assuming a 48kHz sample rate
(sampleRate variable) before configuring or querying the engine, which can make
click detection flaky if the engine default differs; fix by querying or setting
the engine's actual sample rate (e.g., call engine.sample_rate() or set
engine.set_sample_rate(...) before computing samplesPerClick/processCalls) and
then recompute samplesPerClick and processCalls based on that real rate so the
loop that calls engine.process_audio(...) uses correct timing for click
detection.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 346e6d44-d65c-4fcc-9c5f-a27ee4287dc7

📥 Commits

Reviewing files that changed from the base of the PR and between ed3d064 and 2ae0831.

📒 Files selected for processing (4)
  • CMakeLists.txt
  • src/audio/audio_engine_process.cpp
  • src/audio/spsc_queue.h
  • tests/test_audio_engine.cpp
💤 Files with no reviewable changes (2)
  • src/audio/audio_engine_process.cpp
  • src/audio/spsc_queue.h

Comment on lines +185 to +203
// Compute deterministic number of process_audio calls
double sampleRate = 48000.0;
double bpm = 40.0; // The bounded BPM from above
int samplesPerClick = static_cast<int>(sampleRate * 60.0 / bpm);
int bufferSize = 64;
int processCalls = (samplesPerClick / bufferSize) + 1;

bool clickDetected = false;
for(int i = 0; i < processCalls; ++i) {
std::fill(out.begin(), out.end(), 0.0f);
engine.process_audio(in.data(), out.data(), 64);
for (float s : out) {
if (std::abs(s) > 1e-6f) {
clickDetected = true;
break;
}
}
}
ASSERT_TRUE(clickDetected);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid default-dependent sample-rate math in click-detection timing.

Line 186 assumes 48kHz, but the test never sets engine sample rate before computing processCalls. If default sample rate differs, this can under-run click window and create flaky failures.

🔧 Proposed fix
-    double sampleRate = 48000.0;
+    double sampleRate = 48000.0;
+    engine.set_sample_rate(static_cast<int>(sampleRate));
     double bpm = 40.0; // The bounded BPM from above
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_audio_engine.cpp` around lines 185 - 203, The test computes timing
assuming a 48kHz sample rate (sampleRate variable) before configuring or
querying the engine, which can make click detection flaky if the engine default
differs; fix by querying or setting the engine's actual sample rate (e.g., call
engine.sample_rate() or set engine.set_sample_rate(...) before computing
samplesPerClick/processCalls) and then recompute samplesPerClick and
processCalls based on that real rate so the loop that calls
engine.process_audio(...) uses correct timing for click detection.

@MohitBareja16

Copy link
Copy Markdown
Collaborator Author

@MohitBareja16

@sudip-mondal-2002 LGTM from my side

image

Merge kardo bhaiya

@sudip-mondal-2002

Copy link
Copy Markdown
Owner

@MohitBareja16
Please keep .wav files inside tests/assets folder

otherwise lgtm

@sudip-mondal-2002 sudip-mondal-2002 merged commit 70011b1 into sudip-mondal-2002:main May 25, 2026
9 checks passed
github-actions Bot added a commit that referenced this pull request May 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Audio Processing Related to DSP, effects, or signal processing gssoc:approved GSSoC 2026 contribution High Impact Significant user-facing improvement level:advanced Advanced task · 45 GSSoC points mentor:sudip-mondal-2002 quality:exceptional type:testing Unit tests, integration tests, or test coverage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

test: Cover audio_engine_process.cpp (0%) and audio_engine_api.cpp (58%) — core engine hot paths

2 participants