From ca7321e37981466797c617d7bc234b4cbcf6e8e7 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Tue, 21 Jul 2026 08:50:47 -0500 Subject: [PATCH 1/3] Fix logcat startup race in device tests Process buffered adb logcat output before starting live monitoring so early startup lines are not missed when tests begin monitoring after app launch. Add focused unit tests for buffered logcat matching behavior. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ae324e4e-c922-4429-8222-3e37164d1e43 --- .../DeviceTestTests.cs | 48 +++++++++++++++++++ .../Utilities/DeviceTest.cs | 31 ++++++++++-- 2 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/DeviceTestTests.cs diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/DeviceTestTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/DeviceTestTests.cs new file mode 100644 index 00000000000..a88ccba625f --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/DeviceTestTests.cs @@ -0,0 +1,48 @@ +using System; +using System.IO; +using NUnit.Framework; + +namespace Xamarin.Android.Build.Tests +{ + [TestFixture] + public class DeviceTestTests + { + [Test] + public void TryMatchLogcatOutputMatchesBufferedLine () + { + var output = string.Join (Environment.NewLine, new [] { + "ActivityManager: Displayed com.example/.MainActivity", + "I/mono-stdout(11111): #TEST#", + }); + + using (var logcat = new StringWriter ()) { + bool result = DeviceTest.TryMatchLogcatOutput (output, logcat, line => line.Contains ("#TEST#")); + + Assert.IsTrue (result, "Expected buffered logcat output to contain a matching line."); + StringAssert.Contains ("ActivityManager: Displayed com.example/.MainActivity", logcat.ToString ()); + StringAssert.Contains ("I/mono-stdout(11111): #TEST#", logcat.ToString ()); + } + } + + [Test] + public void TryMatchLogcatOutputKeepsSearchingUntilFirstMatch () + { + var output = string.Join (Environment.NewLine, new [] { + "line-1", + "line-2", + "line-3", + }); + int invocations = 0; + + using (var logcat = new StringWriter ()) { + bool result = DeviceTest.TryMatchLogcatOutput (output, logcat, line => { + invocations++; + return line == "line-2"; + }); + + Assert.IsTrue (result, "Expected a match in buffered logcat output."); + Assert.AreEqual (2, invocations, "Line matcher should stop after the first match."); + } + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/DeviceTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/DeviceTest.cs index 5a639c3e12e..7ea6ee09666 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/DeviceTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/DeviceTest.cs @@ -366,15 +366,22 @@ protected static bool MonitorAdbLogcat (Func action, string logcat WindowStyle = ProcessWindowStyle.Hidden, }; - bool didActionSucceed = false; + int didActionSucceed = 0; ManualResetEventSlim stdout_done = new ManualResetEventSlim (); using (var sw = File.CreateText (logcatFilePath)) { + // Process already-buffered logcat lines first so startup messages emitted before + // this monitor starts are still visible to tests. + if (TryMatchLogcatOutput (RunAdbCommand ("logcat -d"), sw, action)) { + sw.Flush (); + return true; + } + using (var proc = Process.Start (info)) { proc.OutputDataReceived += (sender, e) => { if (e.Data != null) { sw.WriteLine (e.Data); if (action (e.Data)) { - didActionSucceed = true; + Interlocked.Exchange (ref didActionSucceed, 1); } } else { stdout_done.Set (); @@ -382,7 +389,7 @@ protected static bool MonitorAdbLogcat (Func action, string logcat }; proc.BeginOutputReadLine (); TimeSpan time = TimeSpan.FromSeconds (timeout); - while (!stdout_done.IsSet && !didActionSucceed && time.TotalMilliseconds > 0) { + while (!stdout_done.IsSet && Volatile.Read (ref didActionSucceed) == 0 && time.TotalMilliseconds > 0) { proc.WaitForExit (10); time -= TimeSpan.FromMilliseconds (10); } @@ -390,9 +397,25 @@ protected static bool MonitorAdbLogcat (Func action, string logcat proc.WaitForExit (); stdout_done.Wait (); sw.Flush (); - return didActionSucceed; + return Volatile.Read (ref didActionSucceed) == 1; + } + } + } + + internal static bool TryMatchLogcatOutput (string output, TextWriter logcatOutput, Func action) + { + bool didActionSucceed = false; + using (var sr = new StringReader (output ?? "")) { + string line = sr.ReadLine (); + while (line != null) { + logcatOutput.WriteLine (line); + if (!didActionSucceed && action (line)) { + didActionSucceed = true; + } + line = sr.ReadLine (); } } + return didActionSucceed; } protected static bool WaitForDebuggerToStart (string logcatFilePath, int timeout = 120) From 89e416da4cd0ddce1a7be0415c52b7eada4503b9 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Tue, 21 Jul 2026 09:06:47 -0500 Subject: [PATCH 2/3] Trim follow-up diff for logcat flake fix Drop the extra helper unit-test file and keep the success flag as bool, while retaining the buffered logcat pre-scan behavior change in MonitorAdbLogcat. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ae324e4e-c922-4429-8222-3e37164d1e43 --- .../DeviceTestTests.cs | 48 ------------------- .../Utilities/DeviceTest.cs | 8 ++-- 2 files changed, 4 insertions(+), 52 deletions(-) delete mode 100644 src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/DeviceTestTests.cs diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/DeviceTestTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/DeviceTestTests.cs deleted file mode 100644 index a88ccba625f..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/DeviceTestTests.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.IO; -using NUnit.Framework; - -namespace Xamarin.Android.Build.Tests -{ - [TestFixture] - public class DeviceTestTests - { - [Test] - public void TryMatchLogcatOutputMatchesBufferedLine () - { - var output = string.Join (Environment.NewLine, new [] { - "ActivityManager: Displayed com.example/.MainActivity", - "I/mono-stdout(11111): #TEST#", - }); - - using (var logcat = new StringWriter ()) { - bool result = DeviceTest.TryMatchLogcatOutput (output, logcat, line => line.Contains ("#TEST#")); - - Assert.IsTrue (result, "Expected buffered logcat output to contain a matching line."); - StringAssert.Contains ("ActivityManager: Displayed com.example/.MainActivity", logcat.ToString ()); - StringAssert.Contains ("I/mono-stdout(11111): #TEST#", logcat.ToString ()); - } - } - - [Test] - public void TryMatchLogcatOutputKeepsSearchingUntilFirstMatch () - { - var output = string.Join (Environment.NewLine, new [] { - "line-1", - "line-2", - "line-3", - }); - int invocations = 0; - - using (var logcat = new StringWriter ()) { - bool result = DeviceTest.TryMatchLogcatOutput (output, logcat, line => { - invocations++; - return line == "line-2"; - }); - - Assert.IsTrue (result, "Expected a match in buffered logcat output."); - Assert.AreEqual (2, invocations, "Line matcher should stop after the first match."); - } - } - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/DeviceTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/DeviceTest.cs index 7ea6ee09666..24cc6bac3f1 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/DeviceTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/DeviceTest.cs @@ -366,7 +366,7 @@ protected static bool MonitorAdbLogcat (Func action, string logcat WindowStyle = ProcessWindowStyle.Hidden, }; - int didActionSucceed = 0; + bool didActionSucceed = false; ManualResetEventSlim stdout_done = new ManualResetEventSlim (); using (var sw = File.CreateText (logcatFilePath)) { // Process already-buffered logcat lines first so startup messages emitted before @@ -381,7 +381,7 @@ protected static bool MonitorAdbLogcat (Func action, string logcat if (e.Data != null) { sw.WriteLine (e.Data); if (action (e.Data)) { - Interlocked.Exchange (ref didActionSucceed, 1); + didActionSucceed = true; } } else { stdout_done.Set (); @@ -389,7 +389,7 @@ protected static bool MonitorAdbLogcat (Func action, string logcat }; proc.BeginOutputReadLine (); TimeSpan time = TimeSpan.FromSeconds (timeout); - while (!stdout_done.IsSet && Volatile.Read (ref didActionSucceed) == 0 && time.TotalMilliseconds > 0) { + while (!stdout_done.IsSet && !didActionSucceed && time.TotalMilliseconds > 0) { proc.WaitForExit (10); time -= TimeSpan.FromMilliseconds (10); } @@ -397,7 +397,7 @@ protected static bool MonitorAdbLogcat (Func action, string logcat proc.WaitForExit (); stdout_done.Wait (); sw.Flush (); - return Volatile.Read (ref didActionSucceed) == 1; + return didActionSucceed; } } } From f150c74ea090a51d4a910c2ee638e9c14e1dc6d1 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Tue, 21 Jul 2026 09:16:50 -0500 Subject: [PATCH 3/3] Address PR review feedback in logcat matcher Limit both buffered and live predicate evaluation to the first successful match and make TryMatchLogcatOutput private to reduce API surface. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ae324e4e-c922-4429-8222-3e37164d1e43 --- .../Xamarin.Android.Build.Tests/Utilities/DeviceTest.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/DeviceTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/DeviceTest.cs index 24cc6bac3f1..a40d1b355fb 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/DeviceTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/DeviceTest.cs @@ -380,7 +380,7 @@ protected static bool MonitorAdbLogcat (Func action, string logcat proc.OutputDataReceived += (sender, e) => { if (e.Data != null) { sw.WriteLine (e.Data); - if (action (e.Data)) { + if (!didActionSucceed && action (e.Data)) { didActionSucceed = true; } } else { @@ -402,15 +402,16 @@ protected static bool MonitorAdbLogcat (Func action, string logcat } } - internal static bool TryMatchLogcatOutput (string output, TextWriter logcatOutput, Func action) + static bool TryMatchLogcatOutput (string output, TextWriter logcatOutput, Func action) { bool didActionSucceed = false; using (var sr = new StringReader (output ?? "")) { string line = sr.ReadLine (); while (line != null) { logcatOutput.WriteLine (line); - if (!didActionSucceed && action (line)) { + if (action (line)) { didActionSucceed = true; + break; } line = sr.ReadLine (); }