From 3debf8e07ce4a9dd4435d52913203eb93cc9df79 Mon Sep 17 00:00:00 2001 From: Peter Collins Date: Thu, 27 Jun 2024 13:12:43 -0400 Subject: [PATCH] [BaseTasks] Import Xamarin.Build.AsyncTask (#237) We've decided to move https://github.com/xamarin/Xamarin.Build.AsyncTask into this repository as part of dotnet org migration efforts. Source and test content from https://github.com/xamarin/Xamarin.Build.AsyncTask/commit/db4ce14dac has been added to Microsoft.Android.Build.BaseTasks. The `AndroidAsyncTask` and `AsyncTask` classes have been merged, we've decided we no longer need both levels of abstraction. `Microsoft.Android.Build.BaseTasks-Tests` has been updated to target net8.0 as I noticed the tests were no longer running: /Users/runner/hostedtoolcache/dotnet/dotnet test /Users/runner/work/1/s/bin/TestDebug-net6.0/Microsoft.Android.Build.BaseTasks-Tests.dll --logger trx --results-directory /Users/runner/work/_temp Starting test execution, please wait... A total of 1 test files matched the specified pattern. No test is available in /Users/runner/work/1/s/bin/TestDebug-net6.0/Microsoft.Android.Build.BaseTasks-Tests.dll. Make sure that test discoverer & executors are registered and platform & framework version settings are appropriate and try again. --- azure-pipelines.yaml | 5 + .../AndroidAsyncTask.cs | 52 --- .../AsyncTask.cs | 437 ++++++++++++++++++ .../AsyncTaskExtensions.cs | 1 - .../MSBuildExtensions.cs | 1 - .../MSBuildReferences.projitems | 1 - .../UnhandledExceptionLogger.cs | 1 - .../AsyncTaskExtensionsTests.cs | 18 +- .../AsyncTaskTests.cs | 105 +++++ ...osoft.Android.Build.BaseTasks-Tests.csproj | 2 +- 10 files changed, 559 insertions(+), 64 deletions(-) delete mode 100644 src/Microsoft.Android.Build.BaseTasks/AndroidAsyncTask.cs create mode 100644 src/Microsoft.Android.Build.BaseTasks/AsyncTask.cs create mode 100644 tests/Microsoft.Android.Build.BaseTasks-Tests/AsyncTaskTests.cs diff --git a/azure-pipelines.yaml b/azure-pipelines.yaml index 7c83e8f..ce23fb5 100644 --- a/azure-pipelines.yaml +++ b/azure-pipelines.yaml @@ -41,6 +41,11 @@ jobs: inputs: version: $(DotNetCoreVersion) + - task: UseDotNet@2 + displayName: Use .NET Core 8.0.x + inputs: + version: 8.0.x + - task: DotNetCoreCLI@2 displayName: Build solution Xamarin.Android.Tools.sln inputs: diff --git a/src/Microsoft.Android.Build.BaseTasks/AndroidAsyncTask.cs b/src/Microsoft.Android.Build.BaseTasks/AndroidAsyncTask.cs deleted file mode 100644 index a04d86f..0000000 --- a/src/Microsoft.Android.Build.BaseTasks/AndroidAsyncTask.cs +++ /dev/null @@ -1,52 +0,0 @@ -// https://github.com/xamarin/xamarin-android/blob/9fca138604c53989e1cff7fc0c2e939583b4da28/src/Xamarin.Android.Build.Tasks/Tasks/AndroidTask.cs#L27 - -using System; -using Xamarin.Build; -using static System.Threading.Tasks.TaskExtensions; - -namespace Microsoft.Android.Build.Tasks -{ - public abstract class AndroidAsyncTask : AsyncTask - { - public abstract string TaskPrefix { get; } - - public override bool Execute () - { - try { - return RunTask (); - } catch (Exception ex) { - this.LogUnhandledException (TaskPrefix, ex); - return false; - } - } - - /// - /// Typically `RunTaskAsync` will be the preferred method to override, - /// however this method can be overridden instead for Tasks that will - /// run quickly and do not need to be asynchronous. - /// - public virtual bool RunTask () - { - Yield (); - try { - this.RunTask (() => RunTaskAsync ()) - .Unwrap () - .ContinueWith (Complete); - - // This blocks on AsyncTask.Execute, until Complete is called - return base.Execute (); - } finally { - Reacquire (); - } - } - - /// - /// Override this method for simplicity of AsyncTask usage: - /// * Yield / Reacquire is handled for you - /// * RunTaskAsync is already on a background thread - /// - public virtual System.Threading.Tasks.Task RunTaskAsync () => System.Threading.Tasks.Task.CompletedTask; - - protected object ProjectSpecificTaskObjectKey (object key) => (key, WorkingDirectory); - } -} diff --git a/src/Microsoft.Android.Build.BaseTasks/AsyncTask.cs b/src/Microsoft.Android.Build.BaseTasks/AsyncTask.cs new file mode 100644 index 0000000..7a8a607 --- /dev/null +++ b/src/Microsoft.Android.Build.BaseTasks/AsyncTask.cs @@ -0,0 +1,437 @@ +// https://github.com/xamarin/xamarin-android/blob/9fca138604c53989e1cff7fc0c2e939583b4da28/src/Xamarin.Android.Build.Tasks/Tasks/AndroidTask.cs#L27 +// https://github.com/xamarin/Xamarin.Build.AsyncTask/blob/db4ce14dacfef47435c238b1b681c124e60ea1a0/Xamarin.Build.AsyncTask/AsyncTask.cs + +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using Microsoft.Build.Utilities; +using Microsoft.Build.Framework; +using System.Threading; +using static System.Threading.Tasks.TaskExtensions; +using System.Collections; +using System.Collections.Generic; + +namespace Microsoft.Android.Build.Tasks +{ + /// + /// Base class for tasks that need long-running cancellable asynchronous tasks + /// that don't block the UI thread in the IDE. + /// + public abstract class AsyncTask : Task, ICancelableTask + { + public abstract string TaskPrefix { get; } + + readonly CancellationTokenSource cts = new CancellationTokenSource (); + readonly Queue logMessageQueue = new Queue (); + readonly Queue warningMessageQueue = new Queue (); + readonly Queue errorMessageQueue = new Queue (); + readonly Queue customMessageQueue = new Queue (); + readonly Queue telemetryMessageQueue = new Queue (); + readonly ManualResetEvent logDataAvailable = new ManualResetEvent (false); + readonly ManualResetEvent errorDataAvailable = new ManualResetEvent (false); + readonly ManualResetEvent warningDataAvailable = new ManualResetEvent (false); + readonly ManualResetEvent customDataAvailable = new ManualResetEvent (false); + readonly ManualResetEvent telemetryDataAvailable = new ManualResetEvent (false); + readonly ManualResetEvent taskCancelled = new ManualResetEvent (false); + readonly ManualResetEvent completed = new ManualResetEvent (false); + bool isRunning = true; + object eventlock = new object (); + int uiThreadId = 0; + + /// + /// Indicates if the task will yield the node during tool execution. + /// + public bool YieldDuringToolExecution { get; set; } + + /// + /// The cancellation token to notify the cancellation requests + /// + public CancellationToken CancellationToken => cts.Token; + + /// + /// Gets the current working directory for the task, which is captured at task + /// creation time from . + /// + protected string WorkingDirectory { get; private set; } + + [Obsolete ("Do not use the Log.LogXXXX from within your Async task as it will Lock the Visual Studio UI. Use the this.LogXXXX methods instead.")] + private new TaskLoggingHelper Log => base.Log; + + /// + /// Initializes the task. + /// + protected AsyncTask () + { + YieldDuringToolExecution = false; + WorkingDirectory = Directory.GetCurrentDirectory (); + uiThreadId = Thread.CurrentThread.ManagedThreadId; + } + + public void Cancel () + => taskCancelled.Set (); + + protected void Complete (System.Threading.Tasks.Task task) + { + if (task.Exception != null) { + var ex = task.Exception.GetBaseException (); + this.LogUnhandledException (TaskPrefix, ex); + } + Complete (); + } + + public void Complete () + => completed.Set (); + + public void LogDebugTaskItems (string message, string [] items) + { + LogDebugMessage (message); + + if (items == null) + return; + + foreach (var item in items) + LogDebugMessage (" {0}", item); + } + + public void LogDebugTaskItems (string message, ITaskItem [] items) + { + LogDebugMessage (message); + + if (items == null) + return; + + foreach (var item in items) + LogDebugMessage (" {0}", item.ItemSpec); + } + + public void LogTelemetry (string eventName, IDictionary properties) + { + if (uiThreadId == Thread.CurrentThread.ManagedThreadId) { +#pragma warning disable 618 + Log.LogTelemetry (eventName, properties); + return; +#pragma warning restore 618 + } + + var data = new TelemetryEventArgs () { + EventName = eventName, + Properties = properties + }; + EnqueueMessage (telemetryMessageQueue, data, telemetryDataAvailable); + } + + public void LogMessage (string message) + => LogMessage (message, importance: MessageImportance.Normal); + + public void LogMessage (string message, params object [] messageArgs) + => LogMessage (string.Format (message, messageArgs)); + + public void LogDebugMessage (string message) + => LogMessage (message, importance: MessageImportance.Low); + + public void LogDebugMessage (string message, params object [] messageArgs) + => LogMessage (string.Format (message, messageArgs), importance: MessageImportance.Low); + + public void LogMessage (string message, MessageImportance importance = MessageImportance.Normal) + { + if (uiThreadId == Thread.CurrentThread.ManagedThreadId) { +#pragma warning disable 618 + Log.LogMessage (importance, message); + return; +#pragma warning restore 618 + } + + var data = new BuildMessageEventArgs ( + message: message, + helpKeyword: null, + senderName: null, + importance: importance + ); + EnqueueMessage (logMessageQueue, data, logDataAvailable); + } + + public void LogError (string message) + => LogCodedError (code: null, message: message, file: null, lineNumber: 0); + + public void LogError (string message, params object [] messageArgs) + => LogCodedError (code: null, message: string.Format (message, messageArgs)); + + public void LogCodedError (string code, string message) + => LogCodedError (code: code, message: message, file: null, lineNumber: 0); + + public void LogCodedError (string code, string message, params object [] messageArgs) + => LogCodedError (code: code, message: string.Format (message, messageArgs), file: null, lineNumber: 0); + + public void LogCodedError (string code, string file, int lineNumber, string message, params object [] messageArgs) + => LogCodedError (code: code, message: string.Format (message, messageArgs), file: file, lineNumber: lineNumber); + + public void LogCodedError (string code, string message, string file, int lineNumber) + { + if (uiThreadId == Thread.CurrentThread.ManagedThreadId) { +#pragma warning disable 618 + Log.LogError ( + subcategory: null, + errorCode: code, + helpKeyword: null, + file: file, + lineNumber: lineNumber, + columnNumber: 0, + endLineNumber: 0, + endColumnNumber: 0, + message: message + ); + return; +#pragma warning restore 618 + } + + var data = new BuildErrorEventArgs ( + subcategory: null, + code: code, + file: file, + lineNumber: lineNumber, + columnNumber: 0, + endLineNumber: 0, + endColumnNumber: 0, + message: message, + helpKeyword: null, + senderName: null + ); + EnqueueMessage (errorMessageQueue, data, errorDataAvailable); + } + + public void LogWarning (string message) + => LogCodedWarning (code: null, message: message, file: null, lineNumber: 0); + + public void LogWarning (string message, params object [] messageArgs) + => LogCodedWarning (code: null, message: string.Format (message, messageArgs)); + + public void LogCodedWarning (string code, string message) + => LogCodedWarning (code: code, message: message, file: null, lineNumber: 0); + + public void LogCodedWarning (string code, string message, params object [] messageArgs) + => LogCodedWarning (code: code, message: string.Format (message, messageArgs), file: null, lineNumber: 0); + + public void LogCodedWarning (string code, string file, int lineNumber, string message, params object [] messageArgs) + => LogCodedWarning (code: code, message: string.Format (message, messageArgs), file: file, lineNumber: lineNumber); + + public void LogCodedWarning (string code, string message, string file, int lineNumber) + { + if (uiThreadId == Thread.CurrentThread.ManagedThreadId) { +#pragma warning disable 618 + Log.LogWarning ( + subcategory: null, + warningCode: code, + helpKeyword: null, + file: file, + lineNumber: lineNumber, + columnNumber: 0, + endLineNumber: 0, + endColumnNumber: 0, + message: message + ); + return; +#pragma warning restore 618 + } + var data = new BuildWarningEventArgs ( + subcategory: null, + code: code, + file: file, + lineNumber: lineNumber, + columnNumber: 0, + endLineNumber: 0, + endColumnNumber: 0, + message: message, + helpKeyword: null, + senderName: null + ); + EnqueueMessage (warningMessageQueue, data, warningDataAvailable); + } + + public void LogCustomBuildEvent (CustomBuildEventArgs e) + { + if (uiThreadId == Thread.CurrentThread.ManagedThreadId) { + BuildEngine.LogCustomEvent (e); + return; + } + EnqueueMessage (customMessageQueue, e, customDataAvailable); + } + + bool ExecuteWaitForCompletion () + { + WaitForCompletion (); +#pragma warning disable 618 + return !Log.HasLoggedErrors; +#pragma warning restore 618 + } + + void EnqueueMessage (Queue queue, object item, ManualResetEvent resetEvent) + { + lock (queue.SyncRoot) { + queue.Enqueue (item); + lock (eventlock) { + if (isRunning) + resetEvent.Set (); + } + } + } + + void LogInternal (Queue queue, Action action, ManualResetEvent resetEvent) + { + lock (queue.SyncRoot) { + while (queue.Count > 0) { + var args = (T) queue.Dequeue (); + action (args); + } + resetEvent.Reset (); + } + } + + protected void Yield () + { + if (YieldDuringToolExecution && BuildEngine is IBuildEngine3) + ((IBuildEngine3) BuildEngine).Yield (); + } + + protected void Reacquire () + { + if (YieldDuringToolExecution && BuildEngine is IBuildEngine3) + ((IBuildEngine3) BuildEngine).Reacquire (); + } + + protected void WaitForCompletion () + { + WaitHandle [] handles = new WaitHandle [] { + logDataAvailable, + errorDataAvailable, + warningDataAvailable, + customDataAvailable, + telemetryDataAvailable, + taskCancelled, + completed, + }; + try { + while (isRunning) { + var index = (WaitHandleIndex) System.Threading.WaitHandle.WaitAny (handles, TimeSpan.FromMilliseconds (10)); + switch (index) { + case WaitHandleIndex.LogDataAvailable: + LogInternal (logMessageQueue, (e) => { +#pragma warning disable 618 + Log.LogMessage (e.Importance, e.Message); +#pragma warning restore 618 + }, logDataAvailable); + break; + case WaitHandleIndex.ErrorDataAvailable: + LogInternal (errorMessageQueue, (e) => { +#pragma warning disable 618 + Log.LogError ( + subcategory: null, + errorCode: e.Code, + helpKeyword: null, + file: e.File, + lineNumber: e.LineNumber, + columnNumber: e.ColumnNumber, + endLineNumber: e.EndLineNumber, + endColumnNumber: e.EndColumnNumber, + message: e.Message); +#pragma warning restore 618 + }, errorDataAvailable); + break; + case WaitHandleIndex.WarningDataAvailable: + LogInternal (warningMessageQueue, (e) => { +#pragma warning disable 618 + Log.LogWarning (subcategory: null, + warningCode: e.Code, + helpKeyword: null, + file: e.File, + lineNumber: e.LineNumber, + columnNumber: e.ColumnNumber, + endLineNumber: e.EndLineNumber, + endColumnNumber: e.EndColumnNumber, + message: e.Message); +#pragma warning restore 618 + }, warningDataAvailable); + break; + case WaitHandleIndex.CustomDataAvailable: + LogInternal (customMessageQueue, (e) => { + BuildEngine.LogCustomEvent (e); + }, customDataAvailable); + break; + case WaitHandleIndex.TelemetryDataAvailable: + LogInternal (telemetryMessageQueue, (e) => { + BuildEngine5.LogTelemetry (e.EventName, e.Properties); + }, telemetryDataAvailable); + break; + case WaitHandleIndex.TaskCancelled: + Cancel (); + cts.Cancel (); + isRunning = false; + break; + case WaitHandleIndex.Completed: + isRunning = false; + break; + } + } + + } finally { + + } + } + + public override bool Execute () + { + try { + return RunTask (); + } catch (Exception ex) { + this.LogUnhandledException (TaskPrefix, ex); + return false; + } + } + + /// + /// Typically `RunTaskAsync` will be the preferred method to override, + /// however this method can be overridden instead for Tasks that will + /// run quickly and do not need to be asynchronous. + /// + public virtual bool RunTask () + { + Yield (); + try { + this.RunTask (() => RunTaskAsync ()) + .Unwrap () + .ContinueWith (Complete); + + // This blocks on Execute, until Complete is called + return ExecuteWaitForCompletion (); + } finally { + Reacquire (); + } + } + + /// + /// Override this method for simplicity of AsyncTask usage: + /// + /// + /// Yield / Reacquire is handled for you + /// + /// + /// RunTaskAsync is already on a background thread + /// + /// + /// + public virtual System.Threading.Tasks.Task RunTaskAsync () => System.Threading.Tasks.Task.CompletedTask; + + protected object ProjectSpecificTaskObjectKey (object key) => (key, WorkingDirectory); + + private enum WaitHandleIndex + { + LogDataAvailable, + ErrorDataAvailable, + WarningDataAvailable, + CustomDataAvailable, + TelemetryDataAvailable, + TaskCancelled, + Completed, + } + } +} diff --git a/src/Microsoft.Android.Build.BaseTasks/AsyncTaskExtensions.cs b/src/Microsoft.Android.Build.BaseTasks/AsyncTaskExtensions.cs index fce4bc6..6b643ee 100644 --- a/src/Microsoft.Android.Build.BaseTasks/AsyncTaskExtensions.cs +++ b/src/Microsoft.Android.Build.BaseTasks/AsyncTaskExtensions.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using Xamarin.Build; namespace Microsoft.Android.Build.Tasks { diff --git a/src/Microsoft.Android.Build.BaseTasks/MSBuildExtensions.cs b/src/Microsoft.Android.Build.BaseTasks/MSBuildExtensions.cs index fd0909d..323c874 100644 --- a/src/Microsoft.Android.Build.BaseTasks/MSBuildExtensions.cs +++ b/src/Microsoft.Android.Build.BaseTasks/MSBuildExtensions.cs @@ -9,7 +9,6 @@ using Microsoft.Build.Utilities; using Microsoft.Build.Framework; -using Xamarin.Build; namespace Microsoft.Android.Build.Tasks { diff --git a/src/Microsoft.Android.Build.BaseTasks/MSBuildReferences.projitems b/src/Microsoft.Android.Build.BaseTasks/MSBuildReferences.projitems index 96c56c0..5d6a62a 100644 --- a/src/Microsoft.Android.Build.BaseTasks/MSBuildReferences.projitems +++ b/src/Microsoft.Android.Build.BaseTasks/MSBuildReferences.projitems @@ -17,7 +17,6 @@ - diff --git a/src/Microsoft.Android.Build.BaseTasks/UnhandledExceptionLogger.cs b/src/Microsoft.Android.Build.BaseTasks/UnhandledExceptionLogger.cs index 67d2086..4cfe3c0 100644 --- a/src/Microsoft.Android.Build.BaseTasks/UnhandledExceptionLogger.cs +++ b/src/Microsoft.Android.Build.BaseTasks/UnhandledExceptionLogger.cs @@ -5,7 +5,6 @@ using System.IO; using System.Threading.Tasks; using Microsoft.Build.Utilities; -using Xamarin.Build; namespace Microsoft.Android.Build.Tasks { diff --git a/tests/Microsoft.Android.Build.BaseTasks-Tests/AsyncTaskExtensionsTests.cs b/tests/Microsoft.Android.Build.BaseTasks-Tests/AsyncTaskExtensionsTests.cs index ecfc866..13a9762 100644 --- a/tests/Microsoft.Android.Build.BaseTasks-Tests/AsyncTaskExtensionsTests.cs +++ b/tests/Microsoft.Android.Build.BaseTasks-Tests/AsyncTaskExtensionsTests.cs @@ -2,7 +2,6 @@ using System.Threading.Tasks; using Microsoft.Android.Build.Tasks; using NUnit.Framework; -using Xamarin.Build; namespace Microsoft.Android.Build.BaseTasks.Tests { @@ -11,11 +10,16 @@ public class AsyncTaskExtensionsTests { const int Iterations = 32; + class TestAsyncTask : AsyncTask + { + public override string TaskPrefix => "TEST"; + } + [Test] public async Task RunTask () { bool set = false; - await new AsyncTask ().RunTask (delegate { set = true; }); // delegate { } has void return type + await new TestAsyncTask ().RunTask (delegate { set = true; }); // delegate { } has void return type Assert.IsTrue (set); } @@ -23,7 +27,7 @@ public async Task RunTask () public async Task RunTaskOfT () { bool set = false; - Assert.IsTrue (await new AsyncTask ().RunTask (() => set = true), "RunTask should return true"); + Assert.IsTrue (await new TestAsyncTask ().RunTask (() => set = true), "RunTask should return true"); Assert.IsTrue (set); } @@ -31,7 +35,7 @@ public async Task RunTaskOfT () public async Task WhenAll () { bool set = false; - await new AsyncTask ().WhenAll (new [] { 0 }, _ => set = true); + await new TestAsyncTask ().WhenAll (new [] { 0 }, _ => set = true); Assert.IsTrue (set); } @@ -40,7 +44,7 @@ public async Task WhenAllWithLock () { var input = new int [Iterations]; var output = new List (); - await new AsyncTask ().WhenAllWithLock (input, (i, l) => { + await new TestAsyncTask ().WhenAllWithLock (input, (i, l) => { lock (l) output.Add (i); }); Assert.AreEqual (Iterations, output.Count); @@ -50,7 +54,7 @@ public async Task WhenAllWithLock () public void ParallelForEach () { bool set = false; - new AsyncTask ().ParallelForEach (new [] { 0 }, _ => set = true); + new TestAsyncTask ().ParallelForEach (new [] { 0 }, _ => set = true); Assert.IsTrue (set); } @@ -59,7 +63,7 @@ public void ParallelForEachWithLock () { var input = new int [Iterations]; var output = new List (); - new AsyncTask ().ParallelForEachWithLock (input, (i, l) => { + new TestAsyncTask ().ParallelForEachWithLock (input, (i, l) => { lock (l) output.Add (i); }); Assert.AreEqual (Iterations, output.Count); diff --git a/tests/Microsoft.Android.Build.BaseTasks-Tests/AsyncTaskTests.cs b/tests/Microsoft.Android.Build.BaseTasks-Tests/AsyncTaskTests.cs new file mode 100644 index 0000000..912b4c1 --- /dev/null +++ b/tests/Microsoft.Android.Build.BaseTasks-Tests/AsyncTaskTests.cs @@ -0,0 +1,105 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Linq; +using Microsoft.Android.Build.BaseTasks.Tests.Utilities; +using Microsoft.Android.Build.Tasks; +using Microsoft.Build.Framework; +using NUnit.Framework; + +namespace Microsoft.Android.Build.BaseTasks.Tests +{ + [TestFixture] + public class AsyncTaskTests + { + List errors; + List warnings; + List messages; + MockBuildEngine engine; + + [SetUp] + public void TestSetup () + { + errors = new List (); + warnings = new List (); + messages = new List (); + engine = new MockBuildEngine (TestContext.Out, errors, warnings, messages); + } + + class AsyncTaskTest : AsyncTask + { + public override string TaskPrefix => "ATT"; + } + + public class AsyncMessage : AsyncTask + { + public override string TaskPrefix => "AM"; + + public string Text { get; set; } + + public override bool Execute () + { + LogTelemetry ("Test", new Dictionary () { { "Property", "Value" } }); + return base.Execute (); + } + + public override async Task RunTaskAsync () + { + await Task.Delay (5000); + LogMessage (Text); + Complete (); + } + } + + class AsyncTaskExceptionTest : AsyncTask + { + public override string TaskPrefix => "ATET"; + + public string ExceptionMessage { get; set; } + + public override Task RunTaskAsync () + { + throw new System.InvalidOperationException (ExceptionMessage); + } + } + + + [Test] + public void RunAsyncTask () + { + var task = new AsyncTaskTest () { + BuildEngine = engine, + }; + + Assert.IsTrue (task.Execute (), "Empty AsyncTask should have ran successfully."); + } + + [Test] + public void RunAsyncTaskOverride () + { + var message = "Hello Async World!"; + var task = new AsyncMessage () { + BuildEngine = engine, + Text = message, + }; + var taskSucceeded = task.Execute (); + Assert.IsTrue (messages.Any (e => e.Message.Contains (message)), + $"Task did not contain expected message text: '{message}'."); + } + + [Test] + public void RunAsyncTaskExpectedException () + { + var expectedException = "test exception!"; + var task = new AsyncTaskExceptionTest () { + BuildEngine = engine, + ExceptionMessage = expectedException, + }; + + Assert.IsFalse (task.Execute (), "Exception AsyncTask should have failed."); + Assert.IsTrue (errors.Count == 1, "Exception AsyncTask should have produced one error."); + Assert.IsTrue (errors[0].Message.Contains (expectedException), + $"Task did not contain expected error text: '{expectedException}'."); + } + + } +} diff --git a/tests/Microsoft.Android.Build.BaseTasks-Tests/Microsoft.Android.Build.BaseTasks-Tests.csproj b/tests/Microsoft.Android.Build.BaseTasks-Tests/Microsoft.Android.Build.BaseTasks-Tests.csproj index 7f0fb6d..99bdc26 100644 --- a/tests/Microsoft.Android.Build.BaseTasks-Tests/Microsoft.Android.Build.BaseTasks-Tests.csproj +++ b/tests/Microsoft.Android.Build.BaseTasks-Tests/Microsoft.Android.Build.BaseTasks-Tests.csproj @@ -4,7 +4,7 @@ - net6.0 + net8.0 Microsoft.Android.Build.BaseTasks.Tests false $(TestOutputFullPath)