diff --git a/.gitignore b/.gitignore index 3bb49919..e0f9b3b8 100644 --- a/.gitignore +++ b/.gitignore @@ -349,3 +349,6 @@ MigrationBackup/ # dotnet tool local install directory .store/ + +# File built by MacOS +.DS_Store diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0893b308..e9a82c73 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,6 +33,9 @@ The easiest way to restore packages may be to run `init.ps1` which automatically to the feeds that packages for this repo come from, if any. `dotnet restore` or `nuget restore` also work but may require extra steps to authenticate to any applicable feeds. +MacOS specific code that was added in #68 requires that `init.ps1` be run in the repo within a terminal *and devenv.exe must be spawned from that same window* in order for Visual Studio for Windows to be able to find the required macos workload. +Note that `init.ps1 -InstallLocality machine` in broken due to dotnet workloads problems such as https://github.com/dotnet/sdk/issues/30230. + ## Building Building, testing, and packing this repository can be done by using the standard dotnet CLI commands (e.g. `dotnet build`, `dotnet test`, `dotnet pack`, etc.). diff --git a/Directory.Build.props b/Directory.Build.props index 2ca46b18..17232cc4 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -14,6 +14,7 @@ true true true + true true diff --git a/Directory.Packages.props b/Directory.Packages.props index 12841338..94385735 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -9,6 +9,7 @@ + diff --git a/Xunit.StaFact.sln b/Xunit.StaFact.sln index 95fc7dea..c8fbed0c 100644 --- a/Xunit.StaFact.sln +++ b/Xunit.StaFact.sln @@ -21,6 +21,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Xunit.StaFact.Tests", "test\Xunit.StaFact.Tests\Xunit.StaFact.Tests.csproj", "{9F99EC1E-7FF7-419D-84F7-3CE96D653C2E}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Xunit.StaFact.Tests.Mac", "test\Xunit.StaFact.Tests.Mac\Xunit.StaFact.Tests.Mac.csproj", "{077DFB77-0E63-4399-BD0B-4F9D9AC80495}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{B9635B8C-19E8-4D04-ACA4-A391D558D80A}" ProjectSection(SolutionItems) = preProject src\.editorconfig = src\.editorconfig @@ -38,17 +40,33 @@ EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + DebugMac|Any CPU = DebugMac|Any CPU Release|Any CPU = Release|Any CPU + ReleaseMac|Any CPU = ReleaseMac|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {8C500C20-0CD6-4257-A9ED-C571DA6E68D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8C500C20-0CD6-4257-A9ED-C571DA6E68D6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8C500C20-0CD6-4257-A9ED-C571DA6E68D6}.DebugMac|Any CPU.ActiveCfg = Debug|Any CPU + {8C500C20-0CD6-4257-A9ED-C571DA6E68D6}.DebugMac|Any CPU.Build.0 = Debug|Any CPU {8C500C20-0CD6-4257-A9ED-C571DA6E68D6}.Release|Any CPU.ActiveCfg = Release|Any CPU {8C500C20-0CD6-4257-A9ED-C571DA6E68D6}.Release|Any CPU.Build.0 = Release|Any CPU + {8C500C20-0CD6-4257-A9ED-C571DA6E68D6}.ReleaseMac|Any CPU.ActiveCfg = Release|Any CPU + {8C500C20-0CD6-4257-A9ED-C571DA6E68D6}.ReleaseMac|Any CPU.Build.0 = Release|Any CPU {9F99EC1E-7FF7-419D-84F7-3CE96D653C2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9F99EC1E-7FF7-419D-84F7-3CE96D653C2E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9F99EC1E-7FF7-419D-84F7-3CE96D653C2E}.DebugMac|Any CPU.ActiveCfg = Debug|Any CPU + {9F99EC1E-7FF7-419D-84F7-3CE96D653C2E}.DebugMac|Any CPU.Build.0 = Debug|Any CPU {9F99EC1E-7FF7-419D-84F7-3CE96D653C2E}.Release|Any CPU.ActiveCfg = Release|Any CPU {9F99EC1E-7FF7-419D-84F7-3CE96D653C2E}.Release|Any CPU.Build.0 = Release|Any CPU + {9F99EC1E-7FF7-419D-84F7-3CE96D653C2E}.ReleaseMac|Any CPU.ActiveCfg = Release|Any CPU + {9F99EC1E-7FF7-419D-84F7-3CE96D653C2E}.ReleaseMac|Any CPU.Build.0 = Release|Any CPU + {077DFB77-0E63-4399-BD0B-4F9D9AC80495}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {077DFB77-0E63-4399-BD0B-4F9D9AC80495}.DebugMac|Any CPU.ActiveCfg = Debug|Any CPU + {077DFB77-0E63-4399-BD0B-4F9D9AC80495}.DebugMac|Any CPU.Build.0 = Debug|Any CPU + {077DFB77-0E63-4399-BD0B-4F9D9AC80495}.Release|Any CPU.ActiveCfg = Release|Any CPU + {077DFB77-0E63-4399-BD0B-4F9D9AC80495}.ReleaseMac|Any CPU.ActiveCfg = Release|Any CPU + {077DFB77-0E63-4399-BD0B-4F9D9AC80495}.ReleaseMac|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -56,6 +74,7 @@ Global GlobalSection(NestedProjects) = preSolution {8C500C20-0CD6-4257-A9ED-C571DA6E68D6} = {B9635B8C-19E8-4D04-ACA4-A391D558D80A} {9F99EC1E-7FF7-419D-84F7-3CE96D653C2E} = {1E94DC16-537C-4D14-A201-ED3140A994EB} + {077DFB77-0E63-4399-BD0B-4F9D9AC80495} = {1E94DC16-537C-4D14-A201-ED3140A994EB} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DA3510A0-C12F-4470-B8A6-197F0D32CB7C} diff --git a/azure-pipelines/build.yml b/azure-pipelines/build.yml index 56169012..0c86ec34 100644 --- a/azure-pipelines/build.yml +++ b/azure-pipelines/build.yml @@ -48,6 +48,7 @@ jobs: - template: dotnet.yml parameters: RunTests: ${{ parameters.RunTests }} + RunMacTests: true - job: WrapUp dependsOn: diff --git a/azure-pipelines/dotnet.yml b/azure-pipelines/dotnet.yml index 79babd4d..9e283b6d 100644 --- a/azure-pipelines/dotnet.yml +++ b/azure-pipelines/dotnet.yml @@ -1,5 +1,8 @@ parameters: - RunTests: +- name: RunTests +- name: RunMacTests + type: boolean + default: false steps: @@ -10,6 +13,19 @@ steps: displayName: 🧪 dotnet test condition: and(succeeded(), ${{ parameters.RunTests }}) +- ${{ if parameters.RunMacTests }}: + - script: dotnet build --no-restore -c $(BuildConfiguration) ./test/Xunit.StaFact.Tests.Mac/Xunit.StaFact.Tests.Mac.csproj -t:Test -p:BuildProjectReferences=false + displayName: 🧪 Mac-specific tests + + - task: PublishTestResults@2 + inputs: + testResultsFormat: XUnit + testResultsFiles: ./test_logs/Xunit.StaFact.Tests.Mac.xml + testRunTitle: Xunit.StaFact.Tests.Mac (net6.0-macos, MacOS) + displayName: 📢 Publish Mac-specific tests results + condition: succeededOrFailed() + continueOnError: true + - powershell: azure-pipelines/variables/_pipelines.ps1 failOnStderr: true displayName: ⚙ Update pipeline variables based on build outputs diff --git a/init.ps1 b/init.ps1 index 774ead58..2bb7fb94 100755 --- a/init.ps1 +++ b/init.ps1 @@ -65,6 +65,9 @@ if (!$NoPrerequisites) { if ($env:OS -eq 'Windows_NT') { $EnvVars['PROCDUMP_PATH'] = & "$PSScriptRoot\azure-pipelines\Get-ProcDump.ps1" } + + # Install specific macOS workload version in order to support usage on older .NET and macOS + dotnet workload install macos --from-rollback-file $PSScriptRoot/workloads.json } # Workaround nuget credential provider bug that causes very unreliable package restores on Azure Pipelines @@ -77,7 +80,10 @@ try { if (!$NoRestore -and $PSCmdlet.ShouldProcess("NuGet packages", "Restore")) { Write-Host "Restoring NuGet packages" -ForegroundColor $HeaderColor - dotnet restore + if ($IsMacOS) { + $restoreArgs = "/p:Configuration=DebugMac" + } + dotnet restore $restoreArgs if ($lastexitcode -ne 0) { throw "Failure while restoring packages." } diff --git a/src/Xunit.StaFact/Mac/CocoaFactAttribute.cs b/src/Xunit.StaFact/Mac/CocoaFactAttribute.cs new file mode 100644 index 00000000..32f34f5b --- /dev/null +++ b/src/Xunit.StaFact/Mac/CocoaFactAttribute.cs @@ -0,0 +1,16 @@ +// Copyright (c) Andrew Arnott. All rights reserved. +// Licensed under the Ms-PL license. See LICENSE file in the project root for full license information. + +using Xunit.Sdk; + +namespace Xunit; + +/// +/// Identifies an xunit test that starts on with a +/// running on . +/// +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] +[XunitTestCaseDiscoverer("Xunit.Sdk.CocoaFactDiscoverer", ThisAssembly.AssemblyName)] +public class CocoaFactAttribute : FactAttribute +{ +} diff --git a/src/Xunit.StaFact/Mac/CocoaTheoryAttribute.cs b/src/Xunit.StaFact/Mac/CocoaTheoryAttribute.cs new file mode 100644 index 00000000..7a2ec440 --- /dev/null +++ b/src/Xunit.StaFact/Mac/CocoaTheoryAttribute.cs @@ -0,0 +1,16 @@ +// Copyright (c) Andrew Arnott. All rights reserved. +// Licensed under the Ms-PL license. See LICENSE file in the project root for full license information. + +using Xunit.Sdk; + +namespace Xunit; + +/// +/// Identifies an xunit theory that starts on with a +/// running on . +/// +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] +[XunitTestCaseDiscoverer("Xunit.Sdk.CocoaTheoryDiscoverer", ThisAssembly.AssemblyName)] +public class CocoaTheoryAttribute : TheoryAttribute +{ +} diff --git a/src/Xunit.StaFact/Sdk.Mac/CocoaFactDiscoverer.cs b/src/Xunit.StaFact/Sdk.Mac/CocoaFactDiscoverer.cs new file mode 100644 index 00000000..121dded5 --- /dev/null +++ b/src/Xunit.StaFact/Sdk.Mac/CocoaFactDiscoverer.cs @@ -0,0 +1,35 @@ +// Copyright (c) Andrew Arnott. All rights reserved. +// Licensed under the Ms-PL license. See LICENSE file in the project root for full license information. + +using System.Runtime.CompilerServices; + +namespace Xunit.Sdk; + +/// +/// The discovery class for . +/// +public class CocoaFactDiscoverer : FactDiscoverer +{ + private readonly IMessageSink diagnosticMessageSink; + + /// + /// Initializes a new instance of the class. + /// + /// The diagnostic message sink. + public CocoaFactDiscoverer(IMessageSink diagnosticMessageSink) + : base(diagnosticMessageSink) + { + this.diagnosticMessageSink = diagnosticMessageSink; + } + + protected override IXunitTestCase CreateTestCase(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute) + { + if (testMethod.Method.ReturnType.Name == "System.Void" && + testMethod.Method.GetCustomAttributes(typeof(AsyncStateMachineAttribute)).Any()) + { + return new ExecutionErrorTestCase(this.diagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), TestMethodDisplayOptions.None, testMethod, "Async void methods are not supported."); + } + + return new UITestCase(UITestCase.SyncContextType.Cocoa, this.diagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), testMethod); + } +} diff --git a/src/Xunit.StaFact/Sdk.Mac/CocoaSynchronizationContext.cs b/src/Xunit.StaFact/Sdk.Mac/CocoaSynchronizationContext.cs new file mode 100644 index 00000000..85290a59 --- /dev/null +++ b/src/Xunit.StaFact/Sdk.Mac/CocoaSynchronizationContext.cs @@ -0,0 +1,190 @@ +// Copyright (c) Andrew Arnott. All rights reserved. +// Licensed under the Ms-PL license. See LICENSE file in the project root for full license information. + +namespace Xunit.Sdk; + +internal sealed class CocoaSynchronizationContext : SynchronizationContext +{ + private readonly Queue> messageQueue = new(); + private readonly int mainThread = Environment.CurrentManagedThreadId; + private readonly AsyncAutoResetEvent workItemDone = new(); + private readonly string name; + private readonly bool shouldSetAsCurrent; + private int activeOperations; + private bool pumping; + private ExceptionAggregator? aggregator; + + /// + /// Initializes a new instance of the class. + /// + public CocoaSynchronizationContext(string name, bool shouldSetAsCurrent) + { + this.name = name; + this.shouldSetAsCurrent = shouldSetAsCurrent; + } + + internal bool IsInContext => this.mainThread == Environment.CurrentManagedThreadId; + + private bool AnyMessagesInQueue + { + get + { + lock (this.messageQueue) + { + return this.messageQueue.Count > 0; + } + } + } + + private bool AnyPendingOperations => Volatile.Read(ref this.activeOperations) > 0; + + public override SynchronizationContext CreateCopy() + => new CocoaSynchronizationContext(this.name, this.shouldSetAsCurrent); + + public override void Post(SendOrPostCallback d, object? state) + => NSRunLoop.Main.BeginInvokeOnMainThread(() => d(state)); + + public override void Send(SendOrPostCallback d, object? state) + => NSRunLoop.Main.InvokeOnMainThread(() => d(state)); + + /// + /// Blocks the calling thread to pump messages until a task has completed. + /// + /// The task that must complete to break out of the message loop. + public void PumpMessages(Task untilCompleted) + { + this.VerifyState(); + + this.pumping = true; + try + { + // Arrange to wake up immediately when the task completes. + untilCompleted.ContinueWith( + _ => + { + lock (this.messageQueue) + { + Monitor.Pulse(this.messageQueue); + } + }, + TaskScheduler.Default); + + // Now run the message loop until the task completes. + while (!untilCompleted.IsCompleted) + { + this.TryOneWorkItem(); + } + } + finally + { + this.pumping = false; + } + } + + public async Task WaitForOperationCompletionAsync() + { + while (this.AnyPendingOperations || this.AnyMessagesInQueue) + { + await this.workItemDone.WaitAsync().ConfigureAwait(false); + } + } + + /// + /// Pump messages until all pending operations have completed + /// and the message queue is empty. + /// + public void CompleteOperations() + { + this.VerifyState(); + this.pumping = true; + try + { + while (this.AnyPendingOperations || this.AnyMessagesInQueue) + { + this.TryOneWorkItem(); + } + } + finally + { + this.pumping = false; + } + } + + /// + public override void OperationStarted() + { + Interlocked.Increment(ref this.activeOperations); + } + + /// + public override void OperationCompleted() + { + int result = Interlocked.Decrement(ref this.activeOperations); + if (result == 0) + { + // Give any message waiter a heads up that the operation count has reached zero, + // in case the queue is empty at the same time the operation count is, which + // is usually a sign to return to its caller. + lock (this.messageQueue) + { + Monitor.Pulse(this.messageQueue); + } + } + } + + internal void SetExceptionAggregator(ExceptionAggregator aggregator) + { + this.aggregator = aggregator; + } + + private void VerifyState() + { + if (Environment.CurrentManagedThreadId != this.mainThread) + { + throw new InvalidOperationException("Wrong thread"); + } + + if (Current != this && this.shouldSetAsCurrent) + { + throw new InvalidOperationException("Wrong sync context"); + } + + if (this.pumping) + { + throw new InvalidOperationException("Already pumping"); + } + } + + private bool TryOneWorkItem() + { + KeyValuePair work = default; + lock (this.messageQueue) + { + if (this.messageQueue.Count == 0) + { + Monitor.Wait(this.messageQueue); + return false; + } + + work = this.messageQueue.Dequeue(); + } + + try + { + if (this.aggregator is object) + { + this.aggregator.Run(() => work.Key(work.Value)); + } + else + { + work.Key(work.Value); + } + + return true; + } + finally + { + this.workItemDone.Set(); + } + } +} diff --git a/src/Xunit.StaFact/Sdk.Mac/CocoaSynchronizationContextAdapter.cs b/src/Xunit.StaFact/Sdk.Mac/CocoaSynchronizationContextAdapter.cs new file mode 100644 index 00000000..b474c46f --- /dev/null +++ b/src/Xunit.StaFact/Sdk.Mac/CocoaSynchronizationContextAdapter.cs @@ -0,0 +1,24 @@ +// Copyright (c) Andrew Arnott. All rights reserved. +// Licensed under the Ms-PL license. See LICENSE file in the project root for full license information. + +namespace Xunit.Sdk; + +internal class CocoaSynchronizationContextAdapter : SyncContextAdapter +{ + internal static readonly SyncContextAdapter Default = new CocoaSynchronizationContextAdapter(); + + private CocoaSynchronizationContextAdapter() + { + } + + internal override bool CanCompleteOperations => true; + + internal override SynchronizationContext Create(string name) => new CocoaSynchronizationContext(name, this.ShouldSetAsCurrent); + + internal override Task WaitForOperationCompletionAsync(SynchronizationContext syncContext) => ((CocoaSynchronizationContext)syncContext).WaitForOperationCompletionAsync(); + + internal override void PumpTill(SynchronizationContext synchronizationContext, Task task) + { + ((CocoaSynchronizationContext)synchronizationContext).PumpMessages(task); + } +} diff --git a/src/Xunit.StaFact/Sdk.Mac/CocoaTheoryDiscoverer.cs b/src/Xunit.StaFact/Sdk.Mac/CocoaTheoryDiscoverer.cs new file mode 100644 index 00000000..7ce9e4cf --- /dev/null +++ b/src/Xunit.StaFact/Sdk.Mac/CocoaTheoryDiscoverer.cs @@ -0,0 +1,29 @@ +// Copyright (c) Andrew Arnott. All rights reserved. +// Licensed under the Ms-PL license. See LICENSE file in the project root for full license information. + +namespace Xunit.Sdk; + +/// +/// The discovery class for . +/// +public class CocoaTheoryDiscoverer : TheoryDiscoverer +{ + /// + /// Initializes a new instance of the class. + /// + /// The diagnostic message sink. + public CocoaTheoryDiscoverer(IMessageSink diagnosticMessageSink) + : base(diagnosticMessageSink) + { + } + + protected override IEnumerable CreateTestCasesForDataRow(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute, object[] dataRow) + { + yield return new UITestCase(UITestCase.SyncContextType.Cocoa, this.DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), testMethod, dataRow); + } + + protected override IEnumerable CreateTestCasesForTheory(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute) + { + yield return new UITheoryTestCase(UITestCase.SyncContextType.Cocoa, this.DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), TestMethodDisplayOptions.None, testMethod); + } +} diff --git a/src/Xunit.StaFact/Sdk/UITestCase.cs b/src/Xunit.StaFact/Sdk/UITestCase.cs index d6796661..f0ed6fe9 100644 --- a/src/Xunit.StaFact/Sdk/UITestCase.cs +++ b/src/Xunit.StaFact/Sdk/UITestCase.cs @@ -55,6 +55,13 @@ public enum SyncContextType /// Portable, +#if MACOS + /// + /// Use a running on , which is only available on macOS. + /// + Cocoa, +#endif + #if NETFRAMEWORK || WINDOWS /// /// Use the , which is only available on Desktop. @@ -115,6 +122,10 @@ internal static SyncContextAdapter GetAdapter(SyncContextType syncContextType) case SyncContextType.Portable: return UISynchronizationContext.Adapter.Default; +#if MACOS + case SyncContextType.Cocoa: + return CocoaSynchronizationContextAdapter.Default; +#endif #if NETFRAMEWORK || WINDOWS case SyncContextType.WPF: return DispatcherSynchronizationContextAdapter.Default; diff --git a/src/Xunit.StaFact/Xunit.StaFact.csproj b/src/Xunit.StaFact/Xunit.StaFact.csproj index 839526d7..0d788046 100644 --- a/src/Xunit.StaFact/Xunit.StaFact.csproj +++ b/src/Xunit.StaFact/Xunit.StaFact.csproj @@ -1,6 +1,10 @@  - net472;netstandard2.0;net6.0;net6.0-windows + + net6.0-macos10.15 + $(TargetFrameworks);net472;netstandard2.0;net6.0;net6.0-windows Xunit Run your xunit-based tests on an STA thread with the WPF Dispatcher, a WinForms SynchronizationContext, or even a cross-platform generic UI thread emulation with a SynchronizationContext that keeps code running on a "main thread" for that test. @@ -14,6 +18,10 @@ Theory variants of these attributes allow for parameterized testing. Check out t + + + + diff --git a/src/Xunit.StaFact/net6.0-macos10.15/PublicAPI.Shipped.txt b/src/Xunit.StaFact/net6.0-macos10.15/PublicAPI.Shipped.txt new file mode 100644 index 00000000..e69de29b diff --git a/src/Xunit.StaFact/net6.0-macos10.15/PublicAPI.Unshipped.txt b/src/Xunit.StaFact/net6.0-macos10.15/PublicAPI.Unshipped.txt new file mode 100644 index 00000000..5ce3afb1 --- /dev/null +++ b/src/Xunit.StaFact/net6.0-macos10.15/PublicAPI.Unshipped.txt @@ -0,0 +1,55 @@ +#nullable enable +override Xunit.Sdk.CocoaFactDiscoverer.CreateTestCase(Xunit.Abstractions.ITestFrameworkDiscoveryOptions! discoveryOptions, Xunit.Abstractions.ITestMethod! testMethod, Xunit.Abstractions.IAttributeInfo! factAttribute) -> Xunit.Sdk.IXunitTestCase! +override Xunit.Sdk.CocoaTheoryDiscoverer.CreateTestCasesForDataRow(Xunit.Abstractions.ITestFrameworkDiscoveryOptions! discoveryOptions, Xunit.Abstractions.ITestMethod! testMethod, Xunit.Abstractions.IAttributeInfo! theoryAttribute, object![]! dataRow) -> System.Collections.Generic.IEnumerable! +override Xunit.Sdk.CocoaTheoryDiscoverer.CreateTestCasesForTheory(Xunit.Abstractions.ITestFrameworkDiscoveryOptions! discoveryOptions, Xunit.Abstractions.ITestMethod! testMethod, Xunit.Abstractions.IAttributeInfo! theoryAttribute) -> System.Collections.Generic.IEnumerable! +override Xunit.Sdk.StaFactDiscoverer.CreateTestCase(Xunit.Abstractions.ITestFrameworkDiscoveryOptions! discoveryOptions, Xunit.Abstractions.ITestMethod! testMethod, Xunit.Abstractions.IAttributeInfo! factAttribute) -> Xunit.Sdk.IXunitTestCase! +override Xunit.Sdk.StaTheoryDiscoverer.CreateTestCasesForDataRow(Xunit.Abstractions.ITestFrameworkDiscoveryOptions! discoveryOptions, Xunit.Abstractions.ITestMethod! testMethod, Xunit.Abstractions.IAttributeInfo! theoryAttribute, object![]! dataRow) -> System.Collections.Generic.IEnumerable! +override Xunit.Sdk.StaTheoryDiscoverer.CreateTestCasesForTheory(Xunit.Abstractions.ITestFrameworkDiscoveryOptions! discoveryOptions, Xunit.Abstractions.ITestMethod! testMethod, Xunit.Abstractions.IAttributeInfo! theoryAttribute) -> System.Collections.Generic.IEnumerable! +override Xunit.Sdk.UIFactDiscoverer.CreateTestCase(Xunit.Abstractions.ITestFrameworkDiscoveryOptions! discoveryOptions, Xunit.Abstractions.ITestMethod! testMethod, Xunit.Abstractions.IAttributeInfo! factAttribute) -> Xunit.Sdk.IXunitTestCase! +override Xunit.Sdk.UITestCase.Deserialize(Xunit.Abstractions.IXunitSerializationInfo! data) -> void +override Xunit.Sdk.UITestCase.RunAsync(Xunit.Abstractions.IMessageSink! diagnosticMessageSink, Xunit.Sdk.IMessageBus! messageBus, object![]! constructorArguments, Xunit.Sdk.ExceptionAggregator! aggregator, System.Threading.CancellationTokenSource! cancellationTokenSource) -> System.Threading.Tasks.Task! +override Xunit.Sdk.UITestCase.Serialize(Xunit.Abstractions.IXunitSerializationInfo! data) -> void +override Xunit.Sdk.UITheoryDiscoverer.CreateTestCasesForDataRow(Xunit.Abstractions.ITestFrameworkDiscoveryOptions! discoveryOptions, Xunit.Abstractions.ITestMethod! testMethod, Xunit.Abstractions.IAttributeInfo! theoryAttribute, object![]! dataRow) -> System.Collections.Generic.IEnumerable! +override Xunit.Sdk.UITheoryDiscoverer.CreateTestCasesForTheory(Xunit.Abstractions.ITestFrameworkDiscoveryOptions! discoveryOptions, Xunit.Abstractions.ITestMethod! testMethod, Xunit.Abstractions.IAttributeInfo! theoryAttribute) -> System.Collections.Generic.IEnumerable! +override Xunit.Sdk.UITheoryTestCase.Deserialize(Xunit.Abstractions.IXunitSerializationInfo! data) -> void +override Xunit.Sdk.UITheoryTestCase.RunAsync(Xunit.Abstractions.IMessageSink! diagnosticMessageSink, Xunit.Sdk.IMessageBus! messageBus, object![]! constructorArguments, Xunit.Sdk.ExceptionAggregator! aggregator, System.Threading.CancellationTokenSource! cancellationTokenSource) -> System.Threading.Tasks.Task! +override Xunit.Sdk.UITheoryTestCase.Serialize(Xunit.Abstractions.IXunitSerializationInfo! data) -> void +Xunit.CocoaFactAttribute +Xunit.CocoaFactAttribute.CocoaFactAttribute() -> void +Xunit.CocoaTheoryAttribute +Xunit.CocoaTheoryAttribute.CocoaTheoryAttribute() -> void +Xunit.Sdk.CocoaFactDiscoverer +Xunit.Sdk.CocoaFactDiscoverer.CocoaFactDiscoverer(Xunit.Abstractions.IMessageSink! diagnosticMessageSink) -> void +Xunit.Sdk.CocoaTheoryDiscoverer +Xunit.Sdk.CocoaTheoryDiscoverer.CocoaTheoryDiscoverer(Xunit.Abstractions.IMessageSink! diagnosticMessageSink) -> void +Xunit.Sdk.StaFactDiscoverer +Xunit.Sdk.StaFactDiscoverer.StaFactDiscoverer(Xunit.Abstractions.IMessageSink! diagnosticMessageSink) -> void +Xunit.Sdk.StaTheoryDiscoverer +Xunit.Sdk.StaTheoryDiscoverer.StaTheoryDiscoverer(Xunit.Abstractions.IMessageSink! diagnosticMessageSink) -> void +Xunit.Sdk.UIFactDiscoverer +Xunit.Sdk.UIFactDiscoverer.UIFactDiscoverer(Xunit.Abstractions.IMessageSink! diagnosticMessageSink) -> void +Xunit.Sdk.UITestCase +Xunit.Sdk.UITestCase.SyncContextType +Xunit.Sdk.UITestCase.SyncContextType.Cocoa = 2 -> Xunit.Sdk.UITestCase.SyncContextType +Xunit.Sdk.UITestCase.SyncContextType.None = 0 -> Xunit.Sdk.UITestCase.SyncContextType +Xunit.Sdk.UITestCase.SyncContextType.Portable = 1 -> Xunit.Sdk.UITestCase.SyncContextType +Xunit.Sdk.UITestCase.UITestCase() -> void +Xunit.Sdk.UITestCase.UITestCase(Xunit.Sdk.UITestCase.SyncContextType synchronizationContextType, Xunit.Abstractions.IMessageSink! diagnosticMessageSink, Xunit.Sdk.TestMethodDisplay defaultMethodDisplay, Xunit.Abstractions.ITestMethod! testMethod, object?[]? testMethodArguments = null) -> void +Xunit.Sdk.UITestCaseRunner +Xunit.Sdk.UITestInvoker +Xunit.Sdk.UITestInvoker.RunAsync() -> System.Threading.Tasks.Task! +Xunit.Sdk.UITestRunner +Xunit.Sdk.UITheoryDiscoverer +Xunit.Sdk.UITheoryDiscoverer.UITheoryDiscoverer(Xunit.Abstractions.IMessageSink! diagnosticMessageSink) -> void +Xunit.Sdk.UITheoryTestCase +Xunit.Sdk.UITheoryTestCase.UITheoryTestCase() -> void +Xunit.Sdk.UITheoryTestCase.UITheoryTestCase(Xunit.Sdk.UITestCase.SyncContextType synchronizationContextType, Xunit.Abstractions.IMessageSink! diagnosticMessageSink, Xunit.Sdk.TestMethodDisplay defaultMethodDisplay, Xunit.Sdk.TestMethodDisplayOptions defaultMethodDisplayOptions, Xunit.Abstractions.ITestMethod! testMethod) -> void +Xunit.Sdk.UITheoryTestCaseRunner +Xunit.StaFactAttribute +Xunit.StaFactAttribute.StaFactAttribute() -> void +Xunit.StaTheoryAttribute +Xunit.StaTheoryAttribute.StaTheoryAttribute() -> void +Xunit.UIFactAttribute +Xunit.UIFactAttribute.UIFactAttribute() -> void +Xunit.UITheoryAttribute +Xunit.UITheoryAttribute.UITheoryAttribute() -> void diff --git a/test/Xunit.StaFact.Tests.Mac/AppDelegate.cs b/test/Xunit.StaFact.Tests.Mac/AppDelegate.cs new file mode 100644 index 00000000..8ab08124 --- /dev/null +++ b/test/Xunit.StaFact.Tests.Mac/AppDelegate.cs @@ -0,0 +1,48 @@ +// Copyright (c) Andrew Arnott. All rights reserved. +// Licensed under the Ms-PL license. See LICENSE file in the project root for full license information. + +using System.Runtime.InteropServices; + +namespace Xunit.StaFact.Tests.Mac; + +[Register("AppDelegate")] +public class AppDelegate : NSApplicationDelegate +{ + private readonly List unitTestDriverArguments = new List(); + + public AppDelegate() + { + var args = NSProcessInfo.ProcessInfo.Arguments; + for (int i = 1; i < args.Length; i++) + { + this.unitTestDriverArguments.Add(args[i]); + } + } + + public override void DidFinishLaunching(NSNotification notification) + { + // Disable AppDomain support since we need all tests to run + // in the main app domain to be able to run on or dispatch to + // the main thread. + this.unitTestDriverArguments.Add("-appdomains"); + this.unitTestDriverArguments.Add("denied"); + + // We also cannot run tests in parallel since our tests depend + // on ordered execution on the main thread. + this.unitTestDriverArguments.Add("-parallel"); + this.unitTestDriverArguments.Add("none"); + + var args = this.unitTestDriverArguments.ToArray(); + + ThreadPool.QueueUserWorkItem(o => + { + var exitCode = Xunit.ConsoleClient.Program.Main(args); + _exit(exitCode); + }); + } + + [DllImport(ObjCRuntime.Constants.libcLibrary)] +#pragma warning disable SA1300 // Element should begin with upper-case letter + private static extern void _exit(int exitCode); +#pragma warning restore SA1300 // Element should begin with upper-case letter +} diff --git a/test/Xunit.StaFact.Tests.Mac/CocoaFactTests.cs b/test/Xunit.StaFact.Tests.Mac/CocoaFactTests.cs new file mode 100644 index 00000000..4c8e41f4 --- /dev/null +++ b/test/Xunit.StaFact.Tests.Mac/CocoaFactTests.cs @@ -0,0 +1,30 @@ +// Copyright (c) Andrew Arnott. All rights reserved. +// Licensed under the Ms-PL license. See LICENSE file in the project root for full license information. + +using Xunit; + +public class CocoaFactTests +{ + [CocoaFact] + public void AssertMainThreadSync() + => NSApplication.EnsureUIThread(); + + [Fact] + public void AssertMainThreadSyncAndFail() + => Assert.Throws(NSApplication.EnsureUIThread); + + [CocoaFact] + public Task AssertMainThreadAsyncReturnCompletedTask() + { + NSApplication.EnsureUIThread(); + return Task.CompletedTask; + } + + [CocoaFact] + public async Task AssertMainThreadAsyncWithAwait() + { + await Task.Yield(); + NSApplication.EnsureUIThread(); + await Task.Delay(100); + } +} diff --git a/test/Xunit.StaFact.Tests.Mac/Info.plist b/test/Xunit.StaFact.Tests.Mac/Info.plist new file mode 100644 index 00000000..65c1f660 --- /dev/null +++ b/test/Xunit.StaFact.Tests.Mac/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleName + Xunit.StaFact.Tests.Mac + CFBundleIdentifier + com.companyname.Xunit.StaFact.Tests.Mac + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + CFBundleDevelopmentRegion + en + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + APPL + CFBundleSignature + ???? + NSHumanReadableCopyright + ${AuthorCopyright:HtmlEncode} + NSPrincipalClass + NSApplication + + diff --git a/test/Xunit.StaFact.Tests.Mac/Main.cs b/test/Xunit.StaFact.Tests.Mac/Main.cs new file mode 100644 index 00000000..8f217142 --- /dev/null +++ b/test/Xunit.StaFact.Tests.Mac/Main.cs @@ -0,0 +1,11 @@ +// Copyright (c) Andrew Arnott. All rights reserved. +// Licensed under the Ms-PL license. See LICENSE file in the project root for full license information. + +using Xunit.StaFact.Tests.Mac; + +NSApplication.Init(); + +// Need to manually set up AppDelegate due to lack of UI +NSApplication.SharedApplication.Delegate = new AppDelegate(); + +NSApplication.Main(args); diff --git a/test/Xunit.StaFact.Tests.Mac/Xunit.StaFact.Tests.Mac.csproj b/test/Xunit.StaFact.Tests.Mac/Xunit.StaFact.Tests.Mac.csproj new file mode 100644 index 00000000..510d4d79 --- /dev/null +++ b/test/Xunit.StaFact.Tests.Mac/Xunit.StaFact.Tests.Mac.csproj @@ -0,0 +1,39 @@ + + + net6.0-macos + Exe + 10.15 + true + $([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture) + $(HostArchitecture.ToLower()) + $(RepoRootPath)\bin\Xunit.StaFact.Tests.Mac\$(Configuration)\$(TargetFramework)\osx-$(HostArchitecture) + $(PartialTargetPath)\Xunit.StaFact.Tests.Mac.dll + + + + + + + + + + + + + + + <_AppExePath>$(PartialTargetPath)\Xunit.StaFact.Tests.Mac.app\Contents\MacOS\Xunit.StaFact.Tests.Mac + + + + + + + + + + + diff --git a/workloads.json b/workloads.json new file mode 100644 index 00000000..1a354fb5 --- /dev/null +++ b/workloads.json @@ -0,0 +1,3 @@ +{ + "microsoft.net.sdk.macos": "12.3.2372/7.0.100" +}