Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,5 +92,10 @@ and [package checks](packaging/yabridge/README.md).
that uses it.
- Tests that redirect `XDG_CONFIG_HOME` or `XDG_RUNTIME_DIR` join the
xUnit collection `xdg-config`.
- A test that needs a fake helper program builds it with
`ExecutableScript.Write`. A program the test process wrote itself
cannot be run while other test classes start helpers: the write
handle reaches their forked children and Linux answers exec with
"Text file busy".
- Hardware behaviour is verified on hardware. When you cannot, say so
in the pull request instead of describing a test you did not run.
9 changes: 7 additions & 2 deletions src/OpenXLR.Core/Mixing/PluginInstaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -905,17 +905,22 @@ public PluginSetup Setup()
};
}

/// <summary>
/// Null only when there is no controller to run. A controller that cannot
/// be started comes back as a failed run carrying the reason the operating
/// system gave, because that reason is the whole diagnosis.
/// </summary>
private ProcessResult? Run(params string[] arguments)
{
if (_yabridgectl is null) return null;
try { return ProcessRunner.Run(_yabridgectl, arguments, YabridgeTimeout, cLocale: false,
environment: _managed?.ControllerEnvironment()); }
catch (Exception) { return null; }
catch (Exception ex) { return new(-1, [], "it could not be started: " + ex.Message, TimedOut: false, Truncated: false); }
}

private static string Tail(ProcessResult? result)
{
if (result is null) return "it could not be started.";
if (result is null) return "it is not installed.";
if (result.TimedOut) return "it did not finish in time.";
string text = (result.Stderr.Length > 0 ? result.Stderr : Encoding.UTF8.GetString(result.Stdout)).Trim();
string[] lines = text.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
Expand Down
40 changes: 40 additions & 0 deletions src/OpenXLR.Tests/ExecutableScript.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
namespace OpenXLR.Tests;

/// <summary>
/// A fake helper program, for tests that hand one to code which runs it.
///
/// A test cannot write a program and then run it. Writing holds a write
/// handle on the file, a fork on any other thread copies that handle into
/// its child, and Linux refuses to run a file that any process can still
/// write, so the run fails with "Text file busy" until that child reaches
/// its own exec. Test classes run in parallel here, so the window is open
/// most of the time and the failure looks random.
///
/// Every fake program is therefore a link to <c>run-script.sh</c>, which the
/// build copies next to the test assembly, and what the program does is a
/// plain text file beside the link. The test process writes no file it runs.
/// </summary>
internal static class ExecutableScript
{
private static readonly string Runner = ShippedRunner();

/// <summary>Create <paramref name="path"/> as a program running <paramref name="body"/>.</summary>
public static string Write(string path, string body)
{
File.WriteAllText(path + ".body", body);
if (OperatingSystem.IsWindows()) return path; // no fork, and nothing here runs a shell script
File.Delete(path); // a stale link, not its target
File.CreateSymbolicLink(path, Runner);
return path;
}

private static string ShippedRunner()
{
string runner = Path.Combine(AppContext.BaseDirectory, "run-script.sh");
if (OperatingSystem.IsWindows()) return runner;
// chmod, never an open for writing, so this cannot make the file busy.
UnixFileMode mode = File.GetUnixFileMode(runner);
if (!mode.HasFlag(UnixFileMode.UserExecute)) File.SetUnixFileMode(runner, mode | UnixFileMode.UserExecute);
return runner;
}
}
7 changes: 1 addition & 6 deletions src/OpenXLR.Tests/ManagedYabridgeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,7 @@ public void ProcessEnvironmentOverridesAreConfinedToTheChild()
Assert.Equal(original, Environment.GetEnvironmentVariable("OPENXLR_BRIDGE_CHILD_TEST"));
}

private void Script(string name, string body)
{
string path = Path.Combine(Package, name);
File.WriteAllText(path, "#!/bin/sh\n" + body);
if (!OperatingSystem.IsWindows()) File.SetUnixFileMode(path, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
}
private void Script(string name, string body) => ExecutableScript.Write(Path.Combine(Package, name), body);

public void Dispose()
{
Expand Down
4 changes: 4 additions & 0 deletions src/OpenXLR.Tests/OpenXLR.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
</PackageReference>
</ItemGroup>

<ItemGroup>
<None Update="run-script.sh" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="../OpenXLR.Core/OpenXLR.Core.csproj" />
<ProjectReference Include="../OpenXLR.Daemon/OpenXLR.Daemon.csproj" />
Expand Down
17 changes: 11 additions & 6 deletions src/OpenXLR.Tests/PluginInstallerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -225,9 +225,7 @@ public void WindowsPluginsWithoutYabridgeSaySo()
private string FakeYabridgectl(string knownDirectory = "")
{
string log = Path.Combine(_root, "yabridgectl.log");
string script = Path.Combine(_root, "yabridgectl");
File.WriteAllText(script, $$"""
#!/bin/sh
return ExecutableScript.Write(Path.Combine(_root, "yabridgectl"), $$"""
echo "$@" >> "{{log}}"
case "$1" in
--version) echo "yabridgectl 5.1.1" ;;
Expand All @@ -237,9 +235,16 @@ private string FakeYabridgectl(string knownDirectory = "")
esac
exit 0
""");
if (!OperatingSystem.IsWindows()) // where these tests run; the analyser wants it said
File.SetUnixFileMode(script, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
return script;
}

[Fact]
public void AControllerThatCannotStartKeepsTheReasonTheSystemGave()
{
string missing = Path.Combine(_root, "no-yabridgectl-here");
WindowsPluginFiles listed = new PluginInstaller(_lv2, _clap, _vst3, missing, null).ListWindowsPlugins(_picked);
Assert.False(listed.Ok);
Assert.Contains("could not be started", listed.Message);
Assert.Contains(missing, listed.Message); // without the reason the failure cannot be diagnosed
}

private string[] YabridgeCalls() => File.Exists(Path.Combine(_root, "yabridgectl.log"))
Expand Down
5 changes: 2 additions & 3 deletions src/OpenXLR.Tests/PluginScanDiagnosticsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@ public void BridgeDiagnosticsRunsStatusOnlyAndPreservesItsFailure()
string dir = Directory.CreateTempSubdirectory("bridge-diagnostics-").FullName;
try
{
string controller = Path.Combine(dir, "yabridgectl");
File.WriteAllText(controller, "#!/bin/sh\n[ \"$1\" = status ] || exit 99\necho 'Example.vst3 -> missing wrapper'\necho 'bridge libraries missing' >&2\nexit 17\n");
File.SetUnixFileMode(controller, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
string controller = ExecutableScript.Write(Path.Combine(dir, "yabridgectl"),
"[ \"$1\" = status ] || exit 99\necho 'Example.vst3 -> missing wrapper'\necho 'bridge libraries missing' >&2\nexit 17\n");
var installer = new PluginInstaller(dir, dir, dir, controller, null, winePrefix: Path.Combine(dir, "prefix"));
var data = System.Text.Json.JsonSerializer.SerializeToElement(installer.Diagnostics(), new System.Text.Json.JsonSerializerOptions(System.Text.Json.JsonSerializerDefaults.Web));
Assert.Equal(controller, data.GetProperty("controller").GetString());
Expand Down
6 changes: 1 addition & 5 deletions src/OpenXLR.Tests/WindowsIndividualPluginTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,7 @@ private PluginInstaller Installer(string[] folders, string[]? excluded = null, s
{
File.WriteAllLines(Registry, folders);
File.WriteAllLines(Exclusions, excluded ?? []);
string controller = Path.Combine(_root, "yabridgectl");
File.WriteAllText(controller, $$"""
#!/bin/sh
string controller = ExecutableScript.Write(Path.Combine(_root, "yabridgectl"), $$"""
printf '%s\n' "$*" >> '{{_root}}/calls'
stage="$1"
if [ "$1" = blacklist ]; then stage="$1 $2"; fi
Expand All @@ -55,8 +53,6 @@ private PluginInstaller Installer(string[] folders, string[]? excluded = null, s
esac
exit 0
""");
if (!OperatingSystem.IsWindows()) File.SetUnixFileMode(controller,
UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
return new(Path.Combine(_root, "lv2"), Path.Combine(_root, "clap"), Path.Combine(_root, "vst3"), controller, "/bin/true",
windowsImportDirectory: Path.Combine(_root, "imports"));
}
Expand Down
5 changes: 1 addition & 4 deletions src/OpenXLR.Tests/WindowsPluginFolderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@ private string Source(string relative, byte[]? bytes = null)
private PluginInstaller Installer(string[] folders, string? fail = null, string? sync = null, bool wine = true)
{
File.WriteAllLines(Registry, folders);
string controller = Path.Combine(_root, "yabridgectl");
File.WriteAllText(controller, $$"""
#!/bin/sh
string controller = ExecutableScript.Write(Path.Combine(_root, "yabridgectl"), $$"""
printf '%s\n' "$*" >> '{{_root}}/calls'
if [ "$1" = '{{fail}}' ]; then printf 'test failure\n' >&2; exit 1; fi
case "$1" in
Expand All @@ -39,7 +37,6 @@ private PluginInstaller Installer(string[] folders, string? fail = null, string?
esac
exit 0
""");
if (!OperatingSystem.IsWindows()) File.SetUnixFileMode(controller, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
return new(Path.Combine(_root, "lv2"), Path.Combine(_root, "clap"), Path.Combine(_root, "vst3"), controller, wine ? "/bin/true" : null);
}

Expand Down
10 changes: 10 additions & 0 deletions src/OpenXLR.Tests/run-script.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/bin/sh
# The one program the test assembly runs that it did not write itself. What a
# fake helper does lives in a plain text file next to the link that started
# this, and this hands that file to the shell. See ExecutableScript.cs.
self=$0
case $self in
*/*) ;;
*) self=$(command -v -- "$self") ;;
esac
exec /bin/sh "$self.body" "$@"