using System.Diagnostics; using System.Globalization; using System.Runtime.InteropServices; using System.Runtime.Versioning; using Quorate.HostExec; namespace Quorate.HostExec.Tests; [TestClass] [SupportedOSPlatform("linux")] public sealed class HostExecTests { [TestMethod] public async Task InstallsAndRunsGnuProxy() { string root = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); try { Directory.CreateDirectory(Path.Combine(root, "usr/bin")); string probe = Path.Combine(AppContext.BaseDirectory, "HostExec.Probe"); File.CreateSymbolicLink(Path.Combine(root, "usr/bin/gcc"), probe); ToolRoot toolRoot = ToolRoot.Open(root); string proxy = GnuProxy.Install(toolRoot, GnuTool.Gcc); Assert.AreEqual( "#!/usr/share/dotnet/dotnet /opt/fsi/packages/HostExec.dll\n", await File.ReadAllTextAsync(proxy).ConfigureAwait(false)); Assert.IsTrue(File.GetUnixFileMode(proxy).HasFlag(UnixFileMode.UserExecute)); Assert.IsTrue(File.Exists($"{proxy}.json")); IReadOnlyList supplied = Proxy.WithSysroot(root, ["-c", "source.c"]); CollectionAssert.AreEqual(new[] { $"--sysroot={root}", "-c", "source.c" }, supplied.ToArray()); IReadOnlyList retained = Proxy.WithSysroot(root, [$"--sysroot={root}", "source.c"]); Assert.HasCount(2, retained); Assert.ThrowsExactly(() => Proxy.WithSysroot(root, ["--sysroot=/different", "source.c"])); Assert.ThrowsExactly(() => Proxy.WithSysroot(root, ["--sysroot"])); Assert.ThrowsExactly(() => ToolRoot.Open("relative")); string assembly = typeof(ToolRoot).Assembly.Location; string runtime = Path.GetDirectoryName(typeof(object).Assembly.Location)!; string dotnet = Path.Combine(Path.GetFullPath(Path.Combine(runtime, "../../..")), "dotnet"); await File.WriteAllTextAsync(proxy, $"#!{dotnet} {assembly}\n").ConfigureAwait(false); var start = new ProcessStartInfo(proxy) { RedirectStandardOutput = true, UseShellExecute = false }; start.ArgumentList.Add("probe.c"); using Process child = Process.Start(start) ?? throw new InvalidOperationException("proxy did not start"); string output = await child.StandardOutput.ReadToEndAsync().ConfigureAwait(false); await child.WaitForExitAsync().ConfigureAwait(false); Assert.AreEqual(0, child.ExitCode); Assert.AreEqual($"--sysroot={root} probe.c\n", output); } finally { if (Directory.Exists(root)) { Directory.Delete(root, true); } } } [TestMethod] public async Task ForwardsTermination() { string root = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); try { Directory.CreateDirectory(Path.Combine(root, "usr/bin")); string probe = Path.Combine(AppContext.BaseDirectory, "HostExec.Probe"); File.CreateSymbolicLink(Path.Combine(root, "usr/bin/gcc"), probe); string proxy = GnuProxy.Install(ToolRoot.Open(root), GnuTool.Gcc); string assembly = typeof(ToolRoot).Assembly.Location; string runtime = Path.GetDirectoryName(typeof(object).Assembly.Location)!; string dotnet = Path.Combine(Path.GetFullPath(Path.Combine(runtime, "../../..")), "dotnet"); await File.WriteAllTextAsync(proxy, $"#!{dotnet} {assembly}\n").ConfigureAwait(false); var start = new ProcessStartInfo(proxy) { RedirectStandardOutput = true, UseShellExecute = false }; start.ArgumentList.Add("--wait"); using Process child = Process.Start(start) ?? throw new InvalidOperationException("proxy did not start"); string childLine = await child.StandardOutput.ReadLineAsync() .WaitAsync(TimeSpan.FromSeconds(5)).ConfigureAwait(false) ?? throw new InvalidDataException("probe did not report its process identity"); int childId = int.Parse(childLine, CultureInfo.InvariantCulture); Assert.AreEqual(0, Kill(child.Id, 15)); await child.WaitForExitAsync().WaitAsync(TimeSpan.FromSeconds(5)).ConfigureAwait(false); Assert.AreEqual(143, child.ExitCode); Assert.AreEqual(-1, Kill(childId, 0)); } finally { if (Directory.Exists(root)) { Directory.Delete(root, true); } } } [DllImport("libc", EntryPoint = "kill", SetLastError = true)] [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] private static extern int Kill(int processId, int signal); }