Tests: split-track recording coverage (per-peer + own send)
Local checkpoint - NOT for public release. Closes the second test gap. - RecordingController gets an internal SettingsSourceForTest seam so a self-test can drive a split (multi-track) recording without writing to the real shared settings store. - New self-test "Recording split tracks": with SplitTracks on + one connected peer, starts the real controller, feeds the "your send" track through the tap it wires onto the sender, and asserts it wrote a FOLDER of tracks (one per peer plus your own) with content - Ed's multi-track feature, now proven on every build. Gate 24/24. Both previously-deferred test gaps (split-track + main-window tabs) now closed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
c8fed26115
commit
499e9644d7
@@ -44,6 +44,11 @@ internal sealed class RecordingController
|
||||
/// recording starts, so one track file is created per peer. Set by MainForm.</summary>
|
||||
public Func<IReadOnlyList<(IPAddress Address, string Name)>>? ConnectedPeersProvider { get; set; }
|
||||
|
||||
/// <summary>Test seam: overrides where <see cref="Start"/> reads the recording settings from, so a
|
||||
/// self-test can drive a split (multi-track) recording without writing to the real shared settings
|
||||
/// store. Null (the default) = read from the store as normal.</summary>
|
||||
internal Func<RecordingSettings>? SettingsSourceForTest { get; set; }
|
||||
|
||||
public RecordingController(AudioSender sender, AudioReceiver receiver, RemSoundSettingsStore settings, Action<string> diagnostic)
|
||||
{
|
||||
this.sender = sender;
|
||||
@@ -71,7 +76,7 @@ internal sealed class RecordingController
|
||||
public void Start()
|
||||
{
|
||||
if (IsRecording) return;
|
||||
var s = settings.LoadRecordingSettings();
|
||||
var s = (SettingsSourceForTest ?? settings.LoadRecordingSettings)();
|
||||
var now = DateTime.Now;
|
||||
try
|
||||
{
|
||||
|
||||
@@ -66,6 +66,7 @@ internal static class SelfTest
|
||||
RunStep(results, "Service send host (headless stream + yield)", ServiceSendHostStream);
|
||||
RunStep(results, "Service registration args", ServiceRegistrationArgs);
|
||||
RunStep(results, "Recording engine (all formats + source gate + mono)", RecordingEngine);
|
||||
RunStep(results, "Recording split tracks (per-peer + own)", RecordingSplitTracks);
|
||||
RunStep(results, "Recording churn / soak", RecordingChurn);
|
||||
RunStep(results, "v5 settings and shaping round-trip", V5ConfigRoundTrip);
|
||||
RunStep(results, "Profile save and reload", ProfileRoundTrip);
|
||||
@@ -534,6 +535,63 @@ internal static class SelfTest
|
||||
finally { try { Directory.Delete(temp, recursive: true); } catch { /* best-effort */ } }
|
||||
}
|
||||
|
||||
/// <summary>Split-track (multi-track) recording: with SplitTracks on and one connected peer, the
|
||||
/// recorder must write a FOLDER of tracks — one per peer plus your own send — not a single mixed file.
|
||||
/// Drives the real RecordingController via a settings-injection seam (so it never touches the shared
|
||||
/// settings store), feeds the "your send" track through the tap the controller wires onto the sender,
|
||||
/// and asserts the track files land with content.</summary>
|
||||
private static string? RecordingSplitTracks()
|
||||
{
|
||||
var temp = Path.Combine(Path.GetTempPath(), "remsound-split-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(temp);
|
||||
using var receiver = new AudioReceiver();
|
||||
using var sender = new RemSound.Sender.AudioSender();
|
||||
try
|
||||
{
|
||||
var controller = new RecordingController(sender, receiver, new RemSoundSettingsStore("RemSound"), _ => { })
|
||||
{
|
||||
SettingsSourceForTest = () => new RecordingSettings
|
||||
{
|
||||
SplitTracks = true,
|
||||
Source = RecordingSource.Both,
|
||||
FileFormat = RecordingFileFormat.Wav,
|
||||
Folder = temp,
|
||||
},
|
||||
ConnectedPeersProvider = () => new[] { (IPAddress.Loopback, "TestPeer") },
|
||||
};
|
||||
|
||||
controller.Start();
|
||||
Check(controller.IsRecording, "split recording should be running after Start");
|
||||
|
||||
// Feed the "your send" track through the tap Start wired onto the sender.
|
||||
var tap = sender.OnSentSamples;
|
||||
if (tap is not null)
|
||||
{
|
||||
var chunk = new float[480 * 2];
|
||||
var phase = 0.0;
|
||||
for (var c = 0; c < 60; c++)
|
||||
{
|
||||
for (var i = 0; i < chunk.Length; i += 2)
|
||||
{
|
||||
var s = (float)(0.2 * Math.Sin(phase));
|
||||
phase += 2 * Math.PI * 440 / 48000;
|
||||
chunk[i] = s; chunk[i + 1] = s;
|
||||
}
|
||||
tap(chunk.AsMemory(), RenderRoute.Mixed);
|
||||
Thread.Sleep(2);
|
||||
}
|
||||
}
|
||||
controller.Stop();
|
||||
for (var i = 0; i < 40 && Directory.GetFiles(temp, "*.wav", SearchOption.AllDirectories).Length == 0; i++) Thread.Sleep(25);
|
||||
|
||||
var files = Directory.GetFiles(temp, "*.wav", SearchOption.AllDirectories);
|
||||
Check(files.Length >= 2, $"split recording must make one file per peer plus your own (found {files.Length})");
|
||||
Check(files.Any(f => new FileInfo(f).Length > 200), "at least one split track (your own send) must have real content");
|
||||
return $"split recording made {files.Length} track files, one with content";
|
||||
}
|
||||
finally { try { Directory.Delete(temp, recursive: true); } catch { /* best-effort */ } }
|
||||
}
|
||||
|
||||
/// <summary>Load/soak: rapidly start, feed, stop and dispose recordings across every format, checking
|
||||
/// nothing leaks handles across the churn — catches recorder/encoder lifecycle leaks and races that a
|
||||
/// single recording wouldn't surface. Set the env var REMSOUND_TEST_SOAK=<seconds> to keep
|
||||
|
||||
Reference in New Issue
Block a user