Skip to content
Draft
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
94 changes: 58 additions & 36 deletions tests/dotnet/UnitTests/DotNetWatchTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ namespace Xamarin.Tests {
[TestFixture]
public class DotNetWatchTest : TestBaseClass {
[Test]
[TestCase (ApplePlatform.MacOSX)]
// [TestCase (ApplePlatform.MacCatalyst)]
// [TestCase (ApplePlatform.iOS)]
public void DotNetWatch (ApplePlatform platform)
[TestCase (ApplePlatform.MacOSX, false)]
[TestCase (ApplePlatform.MacCatalyst, false)]
[TestCase (ApplePlatform.iOS, false)]
[TestCase (ApplePlatform.MacCatalyst, true)]
[TestCase (ApplePlatform.iOS, true)]
public void DotNetWatch (ApplePlatform platform, bool useMonoRuntime)
{
Configuration.IgnoreIfIgnoredPlatform (platform);

Expand Down Expand Up @@ -77,8 +79,12 @@ static partial void ChangeVariable ()
debugLog.WriteLine ("Got 'Variable has changed'");
}
if (line.Contains ("Waiting for changes")) {
waitingForChanges.TrySetResult (true);
debugLog.WriteLine ("Got 'Waiting for changes'");
if (waitingForChanges.TrySetResult (true))
debugLog.WriteLine ("Got 'Waiting for changes'");
}
if (line.Contains ("Build FAILED.")) {
if (waitingForChanges.TrySetResult (false))
debugLog.WriteLine ("Got 'Build FAILED'");
}
});

Expand Down Expand Up @@ -117,6 +123,7 @@ static partial void ChangeVariable ()
var args = new List<string> {
"watch",
"--non-interactive",
"--disable-build-servers",
};

if (platform == ApplePlatform.iOS || platform == ApplePlatform.TVOS) {
Expand All @@ -129,6 +136,8 @@ static partial void ChangeVariable ()
var env = new Dictionary<string, string?> {
{ "HOTRELOAD_TEST_APP_LOGFILE", logPath },
{ "AdditionalFile", additionalFile },
{ "UseMonoRuntime", useMonoRuntime ? "true" : "false" },
{ "RunWithOpen", "false" }, // this makes it so that the watched process is a subprocess, which means that ctrl-c in the terminal will kill everything. It also means that it'll get killed if something times out in the test.
};

var watchTask = Execution.RunWithCallbacksAsync (
Expand All @@ -140,39 +149,52 @@ static partial void ChangeVariable ()
workingDirectory: projectDirectory,
timeout: TimeSpan.FromMinutes (10),
cancellationToken: cts.Token,
log: debugLog
log: debugLog,
closeStandardInput: true
);

// Wait for the app to start and show initial output
debugLog.WriteLine ("Waiting for app start...");
if (!appStarted.Task.Wait (TimeSpan.FromMinutes (1)))
Assert.Fail ($"Timed out waiting for the app to start. Output:\n{string.Join ("\n", output)}\nDebug output:\n{string.Join ("\n", File.ReadAllLines (debugLogPath))}");
debugLog.WriteLine ("App started!");

debugLog.WriteLine ("Waiting for 'dotnet watch' to be waiting for changes...");
if (!waitingForChanges.Task.Wait (TimeSpan.FromMinutes (1)))
Assert.Fail ($"Timed out waiting for the 'dotnet watch' to be waiting for changes. Output:\n{string.Join ("\n", output)}\nDebug output:\n{string.Join ("\n", File.ReadAllLines (debugLogPath))}");
debugLog.WriteLine ("Waiting for changes!");

// Write AdditionalFile.cs to trigger a rebuild via dotnet watch
File.WriteAllText (additionalFile, secondContent);

// Wait for dotnet watch to pick up the change and the app to show the updated output
debugLog.WriteLine ("Waiting for app restart...");
if (!variableChanged.Task.Wait (TimeSpan.FromMinutes (1)))
Assert.Fail ($"Timed out waiting for the variable to change. Output:\n{string.Join ("\n", output)}\nDebug output:\n{string.Join ("\n", File.ReadAllLines (debugLogPath))}");
debugLog.WriteLine ("App restarted!");

// Cancel the watch process
debugLog.WriteLine ("Terminating the watch process...");
cts.Cancel ();

try {
debugLog.WriteLine ("Waiting for exit...");
watchTask.Wait (TimeSpan.FromSeconds (30));
debugLog.WriteLine ("Waited for exit");
} catch {
// Expected - the process was cancelled
// Wait for the app to start and show initial output
debugLog.WriteLine ("Waiting for app start...");
var completedTask = Task.WhenAny (appStarted.Task, waitingForChanges.Task).GetAwaiter ().GetResult ();
if (!appStarted.Task.IsCompleted) {
if (waitingForChanges.Task.IsCompleted && !waitingForChanges.Task.Result)
Assert.Fail ($"Build failed before the app could start. Output:\n{string.Join ("\n", output)}\nDebug output:\n{string.Join ("\n", File.ReadAllLines (debugLogPath))}");
if (!appStarted.Task.Wait (TimeSpan.FromMinutes (1)))
Assert.Fail ($"Timed out waiting for the app to start. Output:\n{string.Join ("\n", output)}\nDebug output:\n{string.Join ("\n", File.ReadAllLines (debugLogPath))}");
}
debugLog.WriteLine ("App started!");

debugLog.WriteLine ("Waiting for 'dotnet watch' to be waiting for changes...");
if (!waitingForChanges.Task.Wait (TimeSpan.FromMinutes (1)))
Assert.Fail ($"Timed out waiting for the 'dotnet watch' to be waiting for changes. Output:\n{string.Join ("\n", output)}\nDebug output:\n{string.Join ("\n", File.ReadAllLines (debugLogPath))}");
if (!waitingForChanges.Task.Result)
Assert.Fail ($"Build failed. Output:\n{string.Join ("\n", output)}\nDebug output:\n{string.Join ("\n", File.ReadAllLines (debugLogPath))}");
debugLog.WriteLine ("Waiting for changes!");

// Write AdditionalFile.cs to trigger a rebuild via dotnet watch
File.WriteAllText (additionalFile, secondContent);

// Wait for dotnet watch to pick up the change and the app to show the updated output
debugLog.WriteLine ("Waiting for app restart...");
if (!variableChanged.Task.Wait (TimeSpan.FromMinutes (1)))
Assert.Fail ($"Timed out waiting for the variable to change. Output:\n{string.Join ("\n", output)}\nDebug output:\n{string.Join ("\n", File.ReadAllLines (debugLogPath))}");
debugLog.WriteLine ("App restarted!");
} finally {
// Always cancel the watch process, even if the test failed
debugLog.WriteLine ("Terminating the watch process...");
cts.Cancel ();

try {
debugLog.WriteLine ("Waiting for exit...");
if (!watchTask.Wait (TimeSpan.FromSeconds (30)))
debugLog.WriteLine ("Watch process did not exit within 30 seconds.");
else
debugLog.WriteLine ("Waited for exit");
} catch (Exception ex) {
// Expected - the process was cancelled
debugLog.WriteLine ($"Exception while waiting for exit (may be expected due to cancellation): {ex.Message}");
}
}
}

Expand Down
24 changes: 19 additions & 5 deletions tools/common/Execution.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ public class Execution {
public string? WorkingDirectory;
public TimeSpan? Timeout;
public CancellationToken? CancellationToken;
public bool CloseStandardInput;

public TextWriter? Log;

Expand Down Expand Up @@ -148,6 +149,15 @@ static Thread StartOutputThread (TaskCompletionSource<Execution> tcs, object loc
return thread;
}

static void KillProcess (Process p)
{
#if NET
p.Kill (true);
#else
p.Kill ();
#endif
}

public Task<Execution> RunAsync ()
{
var tcs = new TaskCompletionSource<Execution> ();
Expand All @@ -158,7 +168,7 @@ public Task<Execution> RunAsync ()
p.StartInfo.FileName = FileName;
p.StartInfo.Arguments = Arguments is not null ? StringUtils.FormatArguments (Arguments) : "";
p.StartInfo.UseShellExecute = false;
p.StartInfo.RedirectStandardInput = false;
p.StartInfo.RedirectStandardInput = CloseStandardInput;
p.StartInfo.RedirectStandardOutput = true;
p.StartInfo.RedirectStandardError = true;
if (!string.IsNullOrEmpty (WorkingDirectory))
Expand Down Expand Up @@ -193,6 +203,8 @@ public Task<Execution> RunAsync ()

var stopwatch = Stopwatch.StartNew ();
p.Start ();
if (CloseStandardInput)
p.StandardInput.Close ();
var pid = p.Id;

var stdoutThread = StartOutputThread (tcs, lockobj, p.StandardOutput, StandardOutputLineCallback, $"StandardOutput reader for {p.StartInfo.FileName} (PID: {pid})");
Expand All @@ -201,7 +213,8 @@ public Task<Execution> RunAsync ()
CancellationToken?.Register (() => {
// Don't call tcs.TrySetCanceled, that won't return an Execution result to the caller.
try {
p.Kill ();
Log?.WriteLine ($"Command '{p.StartInfo.FileName} {p.StartInfo.Arguments}' (pid: {pid}) was cancelled, and will be killed.");
KillProcess (p);
} catch (Exception ex) {
// The process could be disposed already. Just ignore any exceptions here.
Log?.WriteLine ($"Failed to cancel and kill PID {pid}: {ex.Message}");
Expand All @@ -210,10 +223,10 @@ public Task<Execution> RunAsync ()

if (Timeout.HasValue) {
if (!p.WaitForExit ((int) Timeout.Value.TotalMilliseconds)) {
Log?.WriteLine ($"Command '{p.StartInfo.FileName} {p.StartInfo.Arguments}' didn't finish in {Timeout.Value.TotalMilliseconds} ms, and will be killed.");
Log?.WriteLine ($"Command '{p.StartInfo.FileName} {p.StartInfo.Arguments}' (pid: {pid}) didn't finish in {Timeout.Value.TotalMilliseconds} ms, and will be killed.");
TimedOut = true;
try {
p.Kill ();
KillProcess (p);
} catch (Exception ex) {
Comment on lines +226 to 230
// According to the documentation, there can be exceptions here we can't prepare for, so just ignore them.
Log?.WriteLine ($"Failed to kill PID {pid}: {ex.Message}");
Expand Down Expand Up @@ -247,7 +260,7 @@ public Task<Execution> RunAsync ()
return tcs.Task;
}

public static Task<Execution> RunWithCallbacksAsync (string filename, IList<string> arguments, Dictionary<string, string?>? environment = null, Action<string>? standardOutput = null, Action<string>? standardError = null, TextWriter? log = null, string? workingDirectory = null, TimeSpan? timeout = null, CancellationToken? cancellationToken = null)
public static Task<Execution> RunWithCallbacksAsync (string filename, IList<string> arguments, Dictionary<string, string?>? environment = null, Action<string>? standardOutput = null, Action<string>? standardError = null, TextWriter? log = null, string? workingDirectory = null, TimeSpan? timeout = null, CancellationToken? cancellationToken = null, bool closeStandardInput = false)
{
return new Execution {
FileName = filename,
Expand All @@ -259,6 +272,7 @@ public static Task<Execution> RunWithCallbacksAsync (string filename, IList<stri
CancellationToken = cancellationToken,
Timeout = timeout,
Log = log,
CloseStandardInput = closeStandardInput,
}.RunAsync ();
}

Expand Down
Loading