Skip to content
Open
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
187 changes: 187 additions & 0 deletions BeefLibs/corlib/src/Diagnostics/SpawnedProcess.bf
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,191 @@ namespace System.Diagnostics
}
}
}

#if TEST && !BF_PLATFORM_WINDOWS
class SpawnedProcessTests
{
static void ReadAll(FileStream stream, String outText)
{
StreamReader reader = scope .(stream, null, false, 4096);
reader.ReadToEnd(outText).IgnoreError();
}

[Test]
public static void WorkingDirectory_AppliesOnlyToChild()
{
String cwdBefore = scope .();
Directory.GetCurrentDirectory(cwdBefore);

ProcessStartInfo info = scope .();
info.UseShellExecute = false;
info.RedirectStandardOutput = true;
info.SetFileName("/bin/pwd");
info.SetWorkingDirectory("/");

SpawnedProcess process = scope .();
Test.Assert(process.Start(info) case .Ok);
FileStream stdOut = scope .();
Test.Assert(process.AttachStandardOutput(stdOut) case .Ok);
String output = scope .();
ReadAll(stdOut, output);
output.Trim();

String cwdAfter = scope .();
Directory.GetCurrentDirectory(cwdAfter);
// Restore before asserting so a failure doesn't leak into other tests
Directory.SetCurrentDirectory(cwdBefore).IgnoreError();

Test.Assert(process.ExitCode == 0);
Test.Assert(output == "/");
Test.Assert(cwdAfter == cwdBefore);
}

[Test]
public static void WorkingDirectory_MissingFailsStart()
{
ProcessStartInfo info = scope .();
info.UseShellExecute = false;
info.SetFileName("/bin/pwd");
info.SetWorkingDirectory("/nonexistent_bf_spawn_test_dir");

SpawnedProcess process = scope .();
Test.Assert(process.Start(info) case .Err);
}

[Test]
public static void RedirectedStreams_RoundTrip()
{
ProcessStartInfo info = scope .();
info.UseShellExecute = false;
info.RedirectStandardInput = true;
info.RedirectStandardOutput = true;
info.RedirectStandardError = true;
info.SetFileName("/bin/sh");
info.SetArguments("-c \"tr a-z A-Z; echo err >&2\"");

SpawnedProcess process = scope .();
Test.Assert(process.Start(info) case .Ok);
FileStream stdIn = scope .();
FileStream stdOut = scope .();
FileStream stdErr = scope .();
Test.Assert(process.AttachStandardInput(stdIn) case .Ok);
Test.Assert(process.AttachStandardOutput(stdOut) case .Ok);
Test.Assert(process.AttachStandardError(stdErr) case .Ok);

stdIn.WriteStrUnsized("hello").IgnoreError();
stdIn.Close().IgnoreError();

String output = scope .();
ReadAll(stdOut, output);
String error = scope .();
ReadAll(stdErr, error);

Test.Assert(process.ExitCode == 0);
Test.Assert(output == "HELLO");
Test.Assert(error == "err\n");
}

[Test]
public static void RedirectedPipes_NotInheritedByOtherSpawns()
{
ProcessStartInfo catInfo = scope .();
catInfo.UseShellExecute = false;
catInfo.RedirectStandardInput = true;
catInfo.SetFileName("/bin/cat");

SpawnedProcess cat = scope .();
Test.Assert(cat.Start(catInfo) case .Ok);
FileStream catIn = scope .();
Test.Assert(cat.AttachStandardInput(catIn) case .Ok);

// An unrelated spawn made while cat's stdin pipe is open must not hold a copy of it
ProcessStartInfo sleepInfo = scope .();
sleepInfo.UseShellExecute = false;
sleepInfo.SetFileName("/bin/sleep");
sleepInfo.SetArguments("5");

SpawnedProcess sleeper = scope .();
Test.Assert(sleeper.Start(sleepInfo) case .Ok);

catIn.Close().IgnoreError();
bool catExited = cat.WaitFor(2000);

sleeper.Kill();
sleeper.WaitFor();
if (!catExited)
{
cat.Kill();
cat.WaitFor();
}

Test.Assert(catExited);
}

[Test]
public static void StdHandles_OnlyRedirectedStreamsAttachOnce()
{
ProcessStartInfo info = scope .();
info.UseShellExecute = false;
info.RedirectStandardOutput = true;
info.SetFileName("/bin/true");

SpawnedProcess process = scope .();
Test.Assert(process.Start(info) case .Ok);

// A wrongly attached stream would own fd 0, this process's stdin, so leak it rather than close it
FileStream unredirected = new .();
bool attachedUnredirected = process.AttachStandardError(unredirected) case .Ok;
if (!attachedUnredirected)
delete unredirected;

FileStream first = scope .();
bool attachedFirst = process.AttachStandardOutput(first) case .Ok;
FileStream second = new .();
bool attachedSecond = process.AttachStandardOutput(second) case .Ok;
if (!attachedSecond)
delete second;

process.WaitFor();

Test.Assert(!attachedUnredirected);
Test.Assert(attachedFirst);
Test.Assert(!attachedSecond);
}

[Test]
public static void Close_ReleasesUntakenStdIn()
{
String markerPath = scope .();
Path.GetTempPath(markerPath).IgnoreError();
markerPath.Append("/bf_spawn_close_test.tmp");
File.Delete(markerPath).IgnoreError();

// sh only creates the marker once cat sees EOF on the stdin pipe nobody took
ProcessStartInfo info = scope .();
info.UseShellExecute = false;
info.RedirectStandardInput = true;
info.SetFileName("/bin/sh");
info.SetArguments(scope String()..AppendF("-c \"cat > /dev/null; touch '{}'\"", markerPath));

SpawnedProcess process = scope .();
Test.Assert(process.Start(info) case .Ok);
process.Close();

bool markerCreated = false;
for (int i = 0; i < 40; i++)
{
if (File.Exists(markerPath))
{
markerCreated = true;
break;
}
System.Threading.Thread.Sleep(50);
}
File.Delete(markerPath).IgnoreError();

Test.Assert(markerCreated);
}
}
#endif
}
Loading
Loading