Cleanup
publish / publish (push) Successful in 14m29s

This commit is contained in:
khodowany
2026-09-10 09:22:15 -05:00
parent 1241f3eb71
commit b58664d5f4
16 changed files with 582 additions and 68 deletions
+5 -2
View File
@@ -1,14 +1,17 @@
get_target_property(sonus_interface_libraries sonus INTERFACE_LINK_LIBRARIES)
if(sonus_interface_libraries AND sonus_interface_libraries MATCHES "miniaudio")
message(FATAL_ERROR "sonus::sonus must not expose miniaudio in its public link interface")
if(sonus_interface_libraries AND sonus_interface_libraries MATCHES "miniaudio|OpenSSL|SQLite|raylib")
message(FATAL_ERROR "sonus::sonus must not expose private audio or unrelated third-party libraries in its public link interface")
endif()
add_executable(sonus-test
test.cpp
render_snapshot.cpp
control.cpp
)
find_package(Threads REQUIRED)
target_include_directories(sonus-test PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${PROJECT_SOURCE_DIR}/src
)
target_link_libraries(sonus-test PRIVATE
sonus::sonus
+71
View File
@@ -0,0 +1,71 @@
#include <boost/test/unit_test.hpp>
#include <atomic>
#include <barrier>
#include <thread>
#include <vector>
#include <sonus.hpp>
#include "detail/playback_devices.hpp"
BOOST_AUTO_TEST_CASE(sonus_updates_cannot_undo_concurrent_start_or_stop) {
auto config = sonus::audio_config{};
config.no_device = true;
auto created = sonus::engine::create(config);
BOOST_REQUIRE(created);
auto rendezvous = std::barrier{2};
auto failed = std::atomic<bool>{false};
auto updater = std::thread{[&] {
for (auto index = 0; index < 1000; ++index) {
for (auto phase = 0; phase < 2; ++phase) {
rendezvous.arrive_and_wait();
if (!created->update_metronome({})) {
failed.store(true);
}
rendezvous.arrive_and_wait();
}
}
}};
for (auto index = 0; index < 1000; ++index) {
rendezvous.arrive_and_wait();
if (!created->start_metronome({})) {
failed.store(true);
}
rendezvous.arrive_and_wait();
if (!created->metronome_active()) {
failed.store(true);
}
rendezvous.arrive_and_wait();
created->stop_metronome();
rendezvous.arrive_and_wait();
if (created->metronome_active()) {
failed.store(true);
}
}
updater.join();
BOOST_TEST(!failed.load());
}
BOOST_AUTO_TEST_CASE(sonus_device_selection_handles_empty_and_duplicate_devices) {
auto devices = std::vector<sonus::device_info>{
{.id = "0:1:speaker", .name = "speaker", .is_default = true},
{.id = "1:0:speaker", .name = "speaker", .is_default = false},
};
auto select = [&](auto id, auto name) {
return sonus::detail::find_playback_device(devices, id, name);
};
auto selected = select(devices[1].id, "speaker");
BOOST_REQUIRE(selected);
BOOST_TEST(*selected == 1U);
selected = select("stale-id", "speaker");
BOOST_REQUIRE(selected);
BOOST_TEST(*selected == 0U);
selected = select("", "speaker");
BOOST_REQUIRE(selected);
BOOST_TEST(*selected == 0U);
BOOST_TEST(!select("stale-id", ""));
BOOST_TEST(!select("", ""));
BOOST_TEST(!select("", "missing"));
devices.clear();
BOOST_TEST(!select("0:1:speaker", "speaker"));
}
+88
View File
@@ -0,0 +1,88 @@
#include <boost/test/unit_test.hpp>
#include <array>
#include <atomic>
#include <barrier>
#include <memory>
#include <thread>
#include "detail/render_snapshot.hpp"
namespace {
struct snapshot {
std::atomic<int>& live;
std::array<int, 64> samples{};
explicit snapshot(std::atomic<int>& count, int value = 0) : live(count) {
++live;
samples.fill(value);
}
~snapshot() { --live; }
};
}
BOOST_AUTO_TEST_CASE(sonus_snapshots_are_reclaimed_without_rendering) {
auto live = std::atomic<int>{0};
auto state = sonus::detail::render_snapshot<snapshot>{};
for (auto index = 0; index < 1000; ++index) {
state.publish(std::make_unique<snapshot>(live, index));
BOOST_TEST(live.load() == 1);
}
state.publish(nullptr);
BOOST_TEST(live.load() == 0);
}
BOOST_AUTO_TEST_CASE(sonus_snapshots_outlive_readers_and_are_collected_on_control_thread) {
auto live = std::atomic<int>{0};
auto state = sonus::detail::render_snapshot<snapshot>{};
state.publish(std::make_unique<snapshot>(live, 42));
{
auto first = state.read();
state.publish(std::make_unique<snapshot>(live, 43));
{
auto second = state.read();
state.publish(std::make_unique<snapshot>(live, 44));
state.collect();
BOOST_TEST(live.load() == 3);
BOOST_TEST(second.get()->samples.front() == 43);
}
state.collect();
BOOST_TEST(live.load() == 3);
BOOST_TEST(first.get()->samples.back() == 42);
}
BOOST_TEST(live.load() == 3);
state.collect();
BOOST_TEST(live.load() == 1);
}
BOOST_AUTO_TEST_CASE(sonus_snapshot_publication_overlaps_readers) {
auto live = std::atomic<int>{0};
auto state = sonus::detail::render_snapshot<snapshot>{};
state.publish(std::make_unique<snapshot>(live));
auto start = std::barrier{3};
auto failed = std::atomic<bool>{false};
auto reader = [&] {
start.arrive_and_wait();
for (auto index = 0; index < 20000; ++index) {
auto guard = state.read();
auto const* value = guard.get();
for (auto sample : value->samples) {
if (sample != value->samples.front()) {
failed.store(true);
}
}
}
};
auto first = std::thread{reader};
auto second = std::thread{reader};
start.arrive_and_wait();
for (auto index = 0; index < 20000; ++index) {
state.publish(std::make_unique<snapshot>(live, index));
}
first.join();
second.join();
state.collect();
BOOST_TEST(!failed.load());
BOOST_TEST(live.load() == 1);
}
+94 -1
View File
@@ -30,7 +30,7 @@ auto test_file(std::string_view name) -> std::filesystem::path {
}
auto peak(std::vector<std::float32_t> const& samples, std::size_t begin_frame, std::size_t frame_count, std::size_t channels) -> std::float32_t {
auto result = 0.0F;
auto result = std::float32_t{};
auto const begin = begin_frame * channels;
auto const end = std::min(samples.size(), begin + frame_count * channels);
for (auto index = begin; index < end; ++index) {
@@ -126,6 +126,14 @@ BOOST_AUTO_TEST_CASE(sonus_validates_configs) {
audio.playback_device_name = "default";
BOOST_TEST(!sonus::validate(audio));
audio = {};
audio.no_device = true;
audio.playback_device_id = "0:1:default";
BOOST_TEST(!sonus::validate(audio));
BOOST_TEST(!sonus::engine::create(audio));
audio = {};
audio.playback_device_id.assign("a\0b", 3);
BOOST_TEST(!sonus::validate(audio));
audio = {};
audio.playback_device_name.assign("a\0b", 3);
BOOST_TEST(!sonus::validate(audio));
@@ -141,6 +149,26 @@ BOOST_AUTO_TEST_CASE(sonus_validates_configs) {
BOOST_TEST(!sonus::validate(metronome));
}
BOOST_AUTO_TEST_CASE(sonus_playback_devices_can_be_enumerated) {
auto enumerated = sonus::enumerate_playback_devices();
if (!enumerated) {
BOOST_TEST(!enumerated.error().message().empty());
return;
}
for (auto const& device : *enumerated) {
BOOST_TEST(!device.id.empty());
BOOST_TEST(device.id.find('\0') == std::string::npos);
BOOST_TEST(device.name.find('\0') == std::string::npos);
}
if (!enumerated->empty()) {
auto config = sonus::audio_config{};
config.playback_device_id = enumerated->front().id;
BOOST_TEST(static_cast<bool>(sonus::validate(config)));
}
}
BOOST_AUTO_TEST_CASE(sonus_no_device_engine_renders_silence_when_idle) {
auto created = sonus::engine::create(no_device_config());
BOOST_REQUIRE(created);
@@ -154,6 +182,40 @@ BOOST_AUTO_TEST_CASE(sonus_no_device_engine_renders_silence_when_idle) {
}));
}
BOOST_AUTO_TEST_CASE(sonus_master_volume_can_be_changed_concurrently) {
auto created = sonus::engine::create(no_device_config());
BOOST_REQUIRE(created);
auto failed = std::atomic_bool{false};
auto volume_worker = [&created, &failed](std::uint32_t offset) {
for (auto index = std::uint32_t{}; index < 250U; ++index) {
auto const volume = static_cast<std::float32_t>(((index + offset) % 101U)) / 100.0F;
if (!created->set_master_volume(volume)) {
failed.store(true, std::memory_order_release);
}
}
};
auto render_worker = [&created, &failed] {
auto samples = std::vector<std::float32_t>(256U * created->channels());
for (auto index = std::uint32_t{}; index < 250U; ++index) {
if (!created->render(samples.data(), 256U) || !created->update_metronome({})) {
failed.store(true, std::memory_order_release);
}
}
};
auto first = std::thread{volume_worker, 0U};
auto second = std::thread{volume_worker, 37U};
auto renderer = std::thread{render_worker};
first.join();
second.join();
renderer.join();
BOOST_TEST(!failed.load(std::memory_order_acquire));
BOOST_TEST(!created->set_master_volume(-0.01F));
BOOST_TEST(!created->set_master_volume(1.01F));
}
BOOST_AUTO_TEST_CASE(sonus_metronome_renders_accented_clicks) {
auto config = no_device_config();
config.sample_rate = 48000;
@@ -197,6 +259,36 @@ BOOST_AUTO_TEST_CASE(sonus_metronome_update_keeps_inactive_metronome_stopped) {
BOOST_TEST(created->metronome_active());
}
BOOST_AUTO_TEST_CASE(sonus_stopped_metronome_updates_stay_silent_and_restart) {
auto config = no_device_config();
config.sample_rate = 48000;
config.channels = 2;
auto created = sonus::engine::create(config);
BOOST_REQUIRE(created);
for (auto index = std::uint32_t{}; index < 500U; ++index) {
auto metro = sonus::metronome_config{};
metro.bpm = 60U + (index % 180U);
metro.beats_per_bar = 1U + (index % 8U);
metro.click_duration = std::chrono::milliseconds{1 + static_cast<int>(index % 40U)};
BOOST_REQUIRE(created->update_metronome(metro));
BOOST_TEST(!created->metronome_active());
}
auto samples = std::vector<std::float32_t>(4096U * config.channels, 1.0F);
BOOST_REQUIRE(created->render(samples.data(), 4096U));
BOOST_TEST(std::all_of(samples.begin(), samples.end(), [](auto sample) {
return sample == 0.0F;
}));
auto metro = sonus::metronome_config{};
metro.bpm = 120;
BOOST_REQUIRE(created->start_metronome(metro));
BOOST_TEST(created->metronome_active());
BOOST_REQUIRE(created->render(samples.data(), 4096U));
BOOST_TEST(peak(samples, 0, 4096U, config.channels) > 0.1F);
}
BOOST_AUTO_TEST_CASE(sonus_metronome_can_be_updated_concurrently) {
auto created = sonus::engine::create(no_device_config());
BOOST_REQUIRE(created);
@@ -206,6 +298,7 @@ BOOST_AUTO_TEST_CASE(sonus_metronome_can_be_updated_concurrently) {
auto metro = sonus::metronome_config{};
metro.bpm = 80U + ((index + offset) % 80U);
static_cast<void>(created->start_metronome(metro));
static_cast<void>(created->update_metronome(metro));
if (index % 7U == 0U) {
created->stop_metronome();
}