diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 1d2e91d1fe7..4054ae55df9 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -180,8 +180,8 @@ stages: -ci -restore -test - -projects $(Build.SourcesDirectory)/tests/UnitTests.XHarness.iOS.proj - /bl:$(Build.SourcesDirectory)/artifacts/log/$(_BuildConfig)/Helix.XHarness.iOS.binlog + -projects $(Build.SourcesDirectory)/tests/UnitTests.XHarness.iOS.Simulator.proj + /bl:$(Build.SourcesDirectory)/artifacts/log/$(_BuildConfig)/UnitTests.XHarness.iOS.Simulator.binlog /p:RestoreUsingNuGetTargets=false displayName: XHarness iOS Simulator Helix Testing env: @@ -201,19 +201,6 @@ stages: # env: # SYSTEM_ACCESSTOKEN: $(System.AccessToken) # HelixAccessToken: '' - - script: eng/common/build.sh - -configuration $(_BuildConfig) - -prepareMachine - -ci - -restore - -test - -projects $(Build.SourcesDirectory)/tests/UnitTests.XHarness.iOS.IncludeCliOnly.proj - /bl:$(Build.SourcesDirectory)/artifacts/log/$(_BuildConfig)/Helix.XHarness.CLI.binlog - /p:RestoreUsingNuGetTargets=false - displayName: XHarness CLI pre-install Helix Testing - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) - HelixAccessToken: '' - script: eng/common/build.sh -configuration $(_BuildConfig) -prepareMachine diff --git a/src/Common/Microsoft.Arcade.Common/IZipArchiveManager.cs b/src/Common/Microsoft.Arcade.Common/IZipArchiveManager.cs index 910f5530a11..87a59d883d5 100644 --- a/src/Common/Microsoft.Arcade.Common/IZipArchiveManager.cs +++ b/src/Common/Microsoft.Arcade.Common/IZipArchiveManager.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.IO; using System.Threading.Tasks; namespace Microsoft.Arcade.Common @@ -16,6 +17,22 @@ public interface IZipArchiveManager /// New name of the file in the archive Task AddResourceFileToArchive(string archivePath, string resourceName, string targetFileName = null); + /// + /// Creates a file with given content in a given archive. + /// + /// Path to the archive + /// New path of the file in the archive + /// Content of the file + Task AddContentToArchive(string archivePath, string targetFilename, Stream content); + + /// + /// Creates a file with given content in a given archive. + /// + /// Path to the archive + /// New path of the file in the archive + /// Content of the file + Task AddContentToArchive(string archivePath, string targetFilename, string content); + /// /// Compresses a directory into an archive on a given path. /// diff --git a/src/Common/Microsoft.Arcade.Common/ZipArchiveManager.cs b/src/Common/Microsoft.Arcade.Common/ZipArchiveManager.cs index a805eaf98a0..93edcb7bb03 100644 --- a/src/Common/Microsoft.Arcade.Common/ZipArchiveManager.cs +++ b/src/Common/Microsoft.Arcade.Common/ZipArchiveManager.cs @@ -4,6 +4,7 @@ using System.IO; using System.IO.Compression; using System.Reflection; +using System.Text; using System.Threading.Tasks; namespace Microsoft.Arcade.Common @@ -17,9 +18,7 @@ public async Task AddResourceFileToArchive(string archivePath, string } public void ArchiveDirectory(string directoryPath, string archivePath, bool includeBaseDirectory) - { - ZipFile.CreateFromDirectory(directoryPath, archivePath, CompressionLevel.Fastest, includeBaseDirectory); - } + => ZipFile.CreateFromDirectory(directoryPath, archivePath, CompressionLevel.Fastest, includeBaseDirectory); public void ArchiveFile(string filePath, string archivePath) { @@ -30,7 +29,10 @@ public void ArchiveFile(string filePath, string archivePath) } } - private async Task AddContentToArchive(string archivePath, string targetFilename, Stream content) + public Task AddContentToArchive(string archivePath, string targetFilename, string content) + => AddContentToArchive(archivePath, targetFilename, new MemoryStream(Encoding.UTF8.GetBytes(content))); + + public async Task AddContentToArchive(string archivePath, string targetFilename, Stream content) { using FileStream archiveStream = new FileStream(archivePath, FileMode.Open); using ZipArchive archive = new ZipArchive(archiveStream, ZipArchiveMode.Update); diff --git a/src/Microsoft.DotNet.Helix/Sdk.Tests/Microsoft.DotNet.Helix.Sdk.Tests/CreateXHarnessAppleWorkItemsTests.cs b/src/Microsoft.DotNet.Helix/Sdk.Tests/Microsoft.DotNet.Helix.Sdk.Tests/CreateXHarnessAppleWorkItemsTests.cs index 2cfea1576b8..c8da05191ec 100644 --- a/src/Microsoft.DotNet.Helix/Sdk.Tests/Microsoft.DotNet.Helix.Sdk.Tests/CreateXHarnessAppleWorkItemsTests.cs +++ b/src/Microsoft.DotNet.Helix/Sdk.Tests/Microsoft.DotNet.Helix.Sdk.Tests/CreateXHarnessAppleWorkItemsTests.cs @@ -86,16 +86,19 @@ public void AppleXHarnessWorkItemIsCreated() _fileSystem.FileExists(payloadArchive).Should().BeTrue(); var command = workItem.GetMetadata("Command"); - command.Should().Contain("--command test"); + command.Should().Contain("System.Foo.app"); + command.Should().Contain("--targets \"ios-device_13.5\""); command.Should().Contain("--timeout \"00:08:55\""); command.Should().Contain("--launch-timeout \"00:02:33\""); _profileProvider - .Verify(x => x.AddProfilesToBundles(It.Is(bundles => bundles.Any(b => b.ItemSpec == "/apps/System.Foo.app"))), Times.AtLeastOnce); + .Verify(x => x.AddProfilesToBundles(It.Is(bundles => bundles.Any(b => b.ItemSpec == "/apps/System.Foo.app"))), Times.Once); _zipArchiveManager - .Verify(x => x.AddResourceFileToArchive(payloadArchive, It.Is(s => s.Contains("xharness-helix-job.apple.sh")), "xharness-helix-job.apple.sh"), Times.AtLeastOnce); + .Verify(x => x.AddResourceFileToArchive(payloadArchive, It.Is(s => s.Contains("xharness-helix-job.apple.sh")), "xharness-helix-job.apple.sh"), Times.Once); _zipArchiveManager - .Verify(x => x.AddResourceFileToArchive(payloadArchive, It.Is(s => s.Contains("xharness-runner.apple.sh")), "xharness-runner.apple.sh"), Times.AtLeastOnce); + .Verify(x => x.AddResourceFileToArchive(payloadArchive, It.Is(s => s.Contains("xharness-runner.apple.sh")), "xharness-runner.apple.sh"), Times.Once); + _zipArchiveManager + .Verify(x => x.AddContentToArchive(payloadArchive, "command.sh", It.Is(s => s.Contains("xharness apple test"))), Times.Once); } [Fact] @@ -130,6 +133,34 @@ public void ArchivePayloadIsOverwritten() _fileSystem.RemovedFiles.Should().Contain(payloadArchive); } + [Fact] + public void CustomCommandsAreExecuted() + { + var collection = CreateMockServiceCollection(); + _task.ConfigureServices(collection); + _task.AppBundles = new[] + { + CreateAppBundle("apps/System.Foo.app", "ios-simulator-64_13.5", customCommands: "echo foo"), + }; + + // Act + using var provider = collection.BuildServiceProvider(); + _task.InvokeExecute(provider).Should().BeTrue(); + + // Verify + _task.WorkItems.Length.Should().Be(1); + + var workItem = _task.WorkItems.First(); + workItem.GetMetadata("Identity").Should().Be("System.Foo"); + + var payloadArchive = workItem.GetMetadata("PayloadArchive"); + payloadArchive.Should().NotBeNullOrEmpty(); + _fileSystem.FileExists(payloadArchive).Should().BeTrue(); + + _zipArchiveManager + .Verify(x => x.AddContentToArchive(payloadArchive, "command.sh", "echo foo"), Times.Once); + } + [Fact] public void AreDependenciesRegistered() { @@ -155,31 +186,37 @@ private ITaskItem CreateAppBundle( string? testTimeout = null, string? launchTimeout = null, int expectedExitCode = 0, - bool includesTestRunner = true) + bool includesTestRunner = true, + string? customCommands = null) { var mockBundle = new Mock(); mockBundle.SetupGet(x => x.ItemSpec).Returns(path); - mockBundle.Setup(x => x.GetMetadata(CreateXHarnessAppleWorkItems.TargetPropName)).Returns(targets); - mockBundle.Setup(x => x.GetMetadata("IncludesTestRunner")).Returns(includesTestRunner.ToString()); + mockBundle.Setup(x => x.GetMetadata(CreateXHarnessAppleWorkItems.MetadataNames.Targets)).Returns(targets); + mockBundle.Setup(x => x.GetMetadata(CreateXHarnessAppleWorkItems.MetadataNames.IncludesTestRunner)).Returns(includesTestRunner.ToString()); if (workItemTimeout != null) { - mockBundle.Setup(x => x.GetMetadata("WorkItemTimeout")).Returns(workItemTimeout); + mockBundle.Setup(x => x.GetMetadata(XHarnessTaskBase.MetadataName.WorkItemTimeout)).Returns(workItemTimeout); } if (testTimeout != null) { - mockBundle.Setup(x => x.GetMetadata("TestTimeout")).Returns(testTimeout); + mockBundle.Setup(x => x.GetMetadata(XHarnessTaskBase.MetadataName.TestTimeout)).Returns(testTimeout); } if (launchTimeout != null) { - mockBundle.Setup(x => x.GetMetadata("LaunchTimeout")).Returns(launchTimeout); + mockBundle.Setup(x => x.GetMetadata(CreateXHarnessAppleWorkItems.MetadataNames.LaunchTimeout)).Returns(launchTimeout); } if (expectedExitCode != 0) { - mockBundle.Setup(x => x.GetMetadata("ExpectedExitCode")).Returns(expectedExitCode.ToString()); + mockBundle.Setup(x => x.GetMetadata(XHarnessTaskBase.MetadataName.ExpectedExitCode)).Returns(expectedExitCode.ToString()); + } + + if (customCommands != null) + { + mockBundle.Setup(x => x.GetMetadata(XHarnessTaskBase.MetadataName.CustomCommands)).Returns(customCommands); } _fileSystem.CreateDirectory(path); diff --git a/src/Microsoft.DotNet.Helix/Sdk.Tests/Microsoft.DotNet.Helix.Sdk.Tests/ProvisioningProfileProviderTests.cs b/src/Microsoft.DotNet.Helix/Sdk.Tests/Microsoft.DotNet.Helix.Sdk.Tests/ProvisioningProfileProviderTests.cs index f6794c98dba..0c079af5ced 100644 --- a/src/Microsoft.DotNet.Helix/Sdk.Tests/Microsoft.DotNet.Helix.Sdk.Tests/ProvisioningProfileProviderTests.cs +++ b/src/Microsoft.DotNet.Helix/Sdk.Tests/Microsoft.DotNet.Helix.Sdk.Tests/ProvisioningProfileProviderTests.cs @@ -133,7 +133,7 @@ private static ITaskItem CreateAppBundle(string path, string targets) { var mockBundle = new Mock(); mockBundle.SetupGet(x => x.ItemSpec).Returns(path); - mockBundle.Setup(x => x.GetMetadata(CreateXHarnessAppleWorkItems.TargetPropName)).Returns(targets); + mockBundle.Setup(x => x.GetMetadata(CreateXHarnessAppleWorkItems.MetadataNames.Targets)).Returns(targets); return mockBundle.Object; } } diff --git a/src/Microsoft.DotNet.Helix/Sdk/CreateXHarnessAndroidWorkItems.cs b/src/Microsoft.DotNet.Helix/Sdk/CreateXHarnessAndroidWorkItems.cs index 1af2dd4286b..d2910307991 100644 --- a/src/Microsoft.DotNet.Helix/Sdk/CreateXHarnessAndroidWorkItems.cs +++ b/src/Microsoft.DotNet.Helix/Sdk/CreateXHarnessAndroidWorkItems.cs @@ -66,7 +66,7 @@ private async Task PrepareWorkItem(IZipArchiveManager zipArchiveManag { string workItemName = fileSystem.GetFileNameWithoutExtension(appPackage.ItemSpec); - var (testTimeout, workItemTimeout, expectedExitCode) = ParseMetadata(appPackage); + var (testTimeout, workItemTimeout, expectedExitCode, customCommands) = ParseMetadata(appPackage); string command = ValidateMetadataAndGetXHarnessAndroidCommand(appPackage, testTimeout, expectedExitCode); diff --git a/src/Microsoft.DotNet.Helix/Sdk/CreateXHarnessAppleWorkItems.cs b/src/Microsoft.DotNet.Helix/Sdk/CreateXHarnessAppleWorkItems.cs index 38bab231be6..a3effada738 100644 --- a/src/Microsoft.DotNet.Helix/Sdk/CreateXHarnessAppleWorkItems.cs +++ b/src/Microsoft.DotNet.Helix/Sdk/CreateXHarnessAppleWorkItems.cs @@ -15,12 +15,15 @@ namespace Microsoft.DotNet.Helix.Sdk /// public class CreateXHarnessAppleWorkItems : XHarnessTaskBase { - public const string TargetPropName = "Targets"; public const string iOSTargetName = "ios-device"; public const string tvOSTargetName = "tvos-device"; - private const string LaunchTimeoutPropName = "LaunchTimeout"; - private const string IncludesTestRunnerPropName = "IncludesTestRunner"; + public static class MetadataNames + { + public const string Targets = "Targets"; + public const string LaunchTimeout = "LaunchTimeout"; + public const string IncludesTestRunner = "IncludesTestRunner"; + } private const string EntryPointScript = "xharness-helix-job.apple.sh"; private const string RunnerScript = "xharness-runner.apple.sh"; @@ -73,7 +76,7 @@ public bool ExecuteTask( var tasks = AppBundles.Select(bundle => PrepareWorkItem(zipArchiveManager, fileSystem, bundle)); WorkItems = Task.WhenAll(tasks).GetAwaiter().GetResult().Where(wi => wi != null).ToArray(); - + return !Log.HasLoggedErrors; } @@ -88,38 +91,38 @@ private async Task PrepareWorkItem( ITaskItem appBundleItem) { string appFolderPath = appBundleItem.ItemSpec.TrimEnd(Path.DirectorySeparatorChar); - + string workItemName = fileSystem.GetFileName(appFolderPath); if (workItemName.EndsWith(".app")) { workItemName = workItemName.Substring(0, workItemName.Length - 4); } - var (testTimeout, workItemTimeout, expectedExitCode) = ParseMetadata(appBundleItem); + var (testTimeout, workItemTimeout, expectedExitCode, customCommands) = ParseMetadata(appBundleItem); // Validation of any metadata specific to iOS stuff goes here - if (!appBundleItem.TryGetMetadata(TargetPropName, out string target)) + if (!appBundleItem.TryGetMetadata(MetadataNames.Targets, out string targets)) { - Log.LogError($"'{TargetPropName}' metadata must be specified - " + + Log.LogError($"'{MetadataNames.Targets}' metadata must be specified - " + "expecting list of target device/simulator platforms to execute tests on (e.g. ios-simulator-64)"); return null; } - target = target.ToLowerInvariant(); + targets = targets.ToLowerInvariant(); // Optional timeout for the how long it takes for the app to be installed, booted and tests start executing TimeSpan launchTimeout = s_defaultLaunchTimeout; - if (appBundleItem.TryGetMetadata(LaunchTimeoutPropName, out string launchTimeoutProp)) + if (appBundleItem.TryGetMetadata(MetadataNames.LaunchTimeout, out string launchTimeoutProp)) { if (!TimeSpan.TryParse(launchTimeoutProp, out launchTimeout) || launchTimeout.Ticks < 0) { - Log.LogError($"Invalid value \"{launchTimeoutProp}\" provided in <{LaunchTimeoutPropName}>"); + Log.LogError($"Invalid value \"{launchTimeoutProp}\" provided in <{MetadataNames.LaunchTimeout}>"); return null; } } bool includesTestRunner = true; - if (appBundleItem.TryGetMetadata(IncludesTestRunnerPropName, out string includesTestRunnerProp)) + if (appBundleItem.TryGetMetadata(MetadataNames.IncludesTestRunner, out string includesTestRunnerProp)) { if (includesTestRunnerProp.ToLowerInvariant() == "false") { @@ -127,40 +130,58 @@ private async Task PrepareWorkItem( } } - if (includesTestRunner && expectedExitCode != 0) + if (includesTestRunner && expectedExitCode != 0 && customCommands != null) { Log.LogWarning("The ExpectedExitCode property is ignored in the `apple test` scenario"); } + if (customCommands == null) + { + // In case user didn't specify custom commands, we use our default one + customCommands = $"xharness apple {(includesTestRunner ? "test" : "run")} " + + "--app \"$app\" " + + "--output-directory \"$output_directory\" " + + "--targets \"$targets\" " + + "--timeout \"$timeout\" " + + (includesTestRunner + ? $"--launch-timeout \"$launch_timeout\" " + : $"--expected-exit-code $expected_exit_code ") + + "--xcode \"$xcode_path\" " + + "-v " + + (!string.IsNullOrEmpty(AppArguments) ? "-- " + AppArguments : string.Empty); + } + string appName = fileSystem.GetFileName(appBundleItem.ItemSpec); - string command = GetHelixCommand(appName, target, testTimeout, launchTimeout, includesTestRunner, expectedExitCode); - string payloadArchivePath = await CreateZipArchiveOfFolder(zipArchiveManager, fileSystem, appFolderPath); + string helixCommand = GetHelixCommand(appName, targets, testTimeout, launchTimeout, includesTestRunner, expectedExitCode); + string payloadArchivePath = await CreateZipArchiveOfFolder(zipArchiveManager, fileSystem, appFolderPath, customCommands); - Log.LogMessage($"Creating work item with properties Identity: {workItemName}, Payload: {appFolderPath}, Command: {command}"); + Log.LogMessage($"Creating work item with properties Identity: {workItemName}, Payload: {appFolderPath}, Command: {helixCommand}"); return new Build.Utilities.TaskItem(workItemName, new Dictionary() { { "Identity", workItemName }, { "PayloadArchive", payloadArchivePath }, - { "Command", command }, + { "Command", helixCommand }, { "Timeout", workItemTimeout.ToString() }, }); } private string GetHelixCommand(string appName, string targets, TimeSpan testTimeout, TimeSpan launchTimeout, bool includesTestRunner, int expectedExitCode) => $"chmod +x {EntryPointScript} && ./{EntryPointScript} " + - $"--app \"$HELIX_WORKITEM_ROOT/{appName}\" " + - "--output-directory \"$HELIX_WORKITEM_UPLOAD_ROOT\" " + + $"--app \"{appName}\" " + $"--targets \"{targets}\" " + $"--timeout \"{testTimeout}\" " + $"--launch-timeout \"{launchTimeout}\" " + - "--xharness-cli-path \"$XHARNESS_CLI_PATH\" " + - "--command " + (includesTestRunner ? "test" : "run") + - (expectedExitCode != 0 ? $" --expected-exit-code \"{expectedExitCode}\"" : string.Empty) + + (includesTestRunner ? "--includes-test-runner " : string.Empty) + + $"--expected-exit-code \"{expectedExitCode}\" " + (!string.IsNullOrEmpty(XcodeVersion) ? $" --xcode-version \"{XcodeVersion}\"" : string.Empty) + (!string.IsNullOrEmpty(AppArguments) ? $" --app-arguments \"{AppArguments}\"" : string.Empty); - private async Task CreateZipArchiveOfFolder(IZipArchiveManager zipArchiveManager, IFileSystem fileSystem, string folderToZip) + private async Task CreateZipArchiveOfFolder( + IZipArchiveManager zipArchiveManager, + IFileSystem fileSystem, + string folderToZip, + string injectedCommands) { if (!fileSystem.DirectoryExists(folderToZip)) { @@ -183,6 +204,7 @@ private async Task CreateZipArchiveOfFolder(IZipArchiveManager zipArchiv Log.LogMessage($"Adding the XHarness job scripts into the payload archive"); await zipArchiveManager.AddResourceFileToArchive(outputZipPath, ScriptNamespace + EntryPointScript, EntryPointScript); await zipArchiveManager.AddResourceFileToArchive(outputZipPath, ScriptNamespace + RunnerScript, RunnerScript); + await zipArchiveManager.AddContentToArchive(outputZipPath, CustomCommandsScript + ".sh", injectedCommands); return outputZipPath; } diff --git a/src/Microsoft.DotNet.Helix/Sdk/ProvisioningProfileProvider.cs b/src/Microsoft.DotNet.Helix/Sdk/ProvisioningProfileProvider.cs index 4b8dd572c67..1adb6080b39 100644 --- a/src/Microsoft.DotNet.Helix/Sdk/ProvisioningProfileProvider.cs +++ b/src/Microsoft.DotNet.Helix/Sdk/ProvisioningProfileProvider.cs @@ -62,7 +62,7 @@ public void AddProfilesToBundles(ITaskItem[] appBundles) foreach (var appBundle in appBundles) { - if (!appBundle.TryGetMetadata(CreateXHarnessAppleWorkItems.TargetPropName, out string bundleTargets)) + if (!appBundle.TryGetMetadata(CreateXHarnessAppleWorkItems.MetadataNames.Targets, out string bundleTargets)) { _log.LogError("'Targets' metadata must be specified - " + "expecting list of target device/simulator platforms to execute tests on (e.g. ios-simulator-64)"); diff --git a/src/Microsoft.DotNet.Helix/Sdk/SendHelixJob.cs b/src/Microsoft.DotNet.Helix/Sdk/SendHelixJob.cs index a4f18c4e2b4..6212882bbea 100644 --- a/src/Microsoft.DotNet.Helix/Sdk/SendHelixJob.cs +++ b/src/Microsoft.DotNet.Helix/Sdk/SendHelixJob.cs @@ -14,6 +14,27 @@ namespace Microsoft.DotNet.Helix.Sdk { public class SendHelixJob : HelixTask { + public static class MetadataNames + { + public const string Identity = "Identity"; + public const string Value = "Value"; + + // HelixWorkItem + public const string PayloadDirectory = "PayloadDirectory"; + public const string PayloadArchive = "PayloadArchive"; + public const string PayloadUri = "PayloadUri"; + public const string Timeout = "Timeout"; + public const string Command = "Command"; + public const string PreCommands = "PreCommands"; + public const string PostCommands = "PostCommands"; + + // Correlation payload + public const string FullPath = "FullPath"; + public const string Uri = "Uri"; + public const string Destination = "Destination"; + public const string IncludeDirectoryName = "IncludeDirectoryName"; + } + /// /// The 'type' value reported to Helix /// @@ -208,7 +229,7 @@ protected override async Task ExecuteCore(CancellationToken cancellationToken) def = AddProperty(def, helixProperty); } } - + def = AddBuildVariableProperty(def, "CollectionUri", "System.CollectionUri"); def = AddBuildVariableProperty(def, "Project", "System.TeamProject"); def = AddBuildVariableProperty(def, "BuildNumber", "Build.BuildNumber"); @@ -216,7 +237,7 @@ protected override async Task ExecuteCore(CancellationToken cancellationToken) def = AddBuildVariableProperty(def, "DefinitionName", "Build.DefinitionName"); def = AddBuildVariableProperty(def, "DefinitionId", "System.DefinitionId"); def = AddBuildVariableProperty(def, "Reason", "Build.Reason"); - var variablesToCopy = new [] + var variablesToCopy = new[] { "System.JobId", "System.JobName", @@ -274,12 +295,12 @@ private static string FromAzdoVariableNameToEnvironmentVariableName(string name) private IJobDefinition AddProperty(IJobDefinition def, ITaskItem property) { - if (!property.GetRequiredMetadata(Log, "Identity", out string key)) + if (!property.GetRequiredMetadata(Log, MetadataNames.Identity, out string key)) { return def; } - if (!property.GetRequiredMetadata(Log, "Value", out string value)) + if (!property.GetRequiredMetadata(Log, MetadataNames.Value, out string value)) { return def; } @@ -291,12 +312,12 @@ private IJobDefinition AddProperty(IJobDefinition def, ITaskItem property) private IJobDefinition AddWorkItem(IJobDefinition def, ITaskItem workItem) { - if (!workItem.GetRequiredMetadata(Log, "Identity", out string name)) + if (!workItem.GetRequiredMetadata(Log, MetadataNames.Identity, out string name)) { return def; } - if(name.Contains('%')) + if (name.Contains('%')) { Log.LogWarning($"Work Item named '{name}' contains encoded characters which is not recommended."); } @@ -310,7 +331,7 @@ private IJobDefinition AddWorkItem(IJobDefinition def, ITaskItem workItem) name = cleanedName; - if (!workItem.GetRequiredMetadata(Log, "Command", out string command)) + if (!workItem.GetRequiredMetadata(Log, MetadataNames.Command, out string command)) { return def; } @@ -339,9 +360,9 @@ private IJobDefinition AddWorkItem(IJobDefinition def, ITaskItem workItem) } } - string payloadDirectory = workItem.GetMetadata("PayloadDirectory"); - string payloadArchive = workItem.GetMetadata("PayloadArchive"); - string payloadUri = workItem.GetMetadata("PayloadUri"); + string payloadDirectory = workItem.GetMetadata(MetadataNames.PayloadDirectory); + string payloadArchive = workItem.GetMetadata(MetadataNames.PayloadArchive); + string payloadUri = workItem.GetMetadata(MetadataNames.PayloadUri); IWorkItemDefinition wi; if (!string.IsNullOrEmpty(payloadUri)) { @@ -365,7 +386,7 @@ private IJobDefinition AddWorkItem(IJobDefinition def, ITaskItem workItem) } - string timeoutString = workItem.GetMetadata("Timeout"); + string timeoutString = workItem.GetMetadata(MetadataNames.Timeout); if (!string.IsNullOrEmpty(timeoutString)) { if (TimeSpan.TryParse(timeoutString, CultureInfo.InvariantCulture, out TimeSpan timeout)) @@ -396,7 +417,7 @@ private IEnumerable GetCommands(ITaskItem workItem, string workItemComma } } - if (workItem.TryGetMetadata("PreCommands", out string workItemPreCommandsString)) + if (workItem.TryGetMetadata(MetadataNames.PreCommands, out string workItemPreCommandsString)) { foreach (string command in SplitCommands(workItemPreCommandsString)) { @@ -412,7 +433,7 @@ private IEnumerable GetCommands(ITaskItem workItem, string workItemComma // this way we can exit the process honoring that exit code, needed for retry. yield return IsPosixShell ? $"export {exitCodeVariableName}=$?" : $"set {exitCodeVariableName}=%ERRORLEVEL%"; - if (workItem.TryGetMetadata("PostCommands", out string workItemPostCommandsString)) + if (workItem.TryGetMetadata(MetadataNames.PostCommands, out string workItemPostCommandsString)) { foreach (string command in SplitCommands(workItemPostCommandsString)) { @@ -479,9 +500,9 @@ private IEnumerable SplitCommands(string value) private IJobDefinition AddCorrelationPayload(IJobDefinition def, ITaskItem correlationPayload) { - string path = correlationPayload.GetMetadata("FullPath"); - string uri = correlationPayload.GetMetadata("Uri"); - string destination = correlationPayload.GetMetadata("Destination") ?? ""; + string path = correlationPayload.GetMetadata(MetadataNames.FullPath); + string uri = correlationPayload.GetMetadata(MetadataNames.Uri); + string destination = correlationPayload.GetMetadata(MetadataNames.Destination) ?? ""; if (!string.IsNullOrEmpty(uri)) { @@ -499,7 +520,7 @@ private IJobDefinition AddCorrelationPayload(IJobDefinition def, ITaskItem corre if (Directory.Exists(path)) { - string includeDirectoryNameStr = correlationPayload.GetMetadata("IncludeDirectoryName"); + string includeDirectoryNameStr = correlationPayload.GetMetadata(MetadataNames.IncludeDirectoryName); bool.TryParse(includeDirectoryNameStr, out bool includeDirectoryName); Log.LogMessage(MessageImportance.Low, $"Adding Correlation Payload Directory '{path}', destination '{destination}'"); diff --git a/src/Microsoft.DotNet.Helix/Sdk/XharnessTaskBase.cs b/src/Microsoft.DotNet.Helix/Sdk/XharnessTaskBase.cs index b861b8f8816..0797b49f38e 100644 --- a/src/Microsoft.DotNet.Helix/Sdk/XharnessTaskBase.cs +++ b/src/Microsoft.DotNet.Helix/Sdk/XharnessTaskBase.cs @@ -12,11 +12,16 @@ public abstract class XHarnessTaskBase : MSBuildTaskBase private static readonly TimeSpan s_defaultWorkItemTimeout = TimeSpan.FromMinutes(20); private static readonly TimeSpan s_defaultTestTimeout = TimeSpan.FromMinutes(12); - private const string TestTimeoutPropName = "TestTimeout"; - private const string WorkItemTimeoutPropName = "WorkItemTimeout"; - private const string ExpectedExitCodePropName = "ExpectedExitCode"; + public class MetadataName + { + public const string TestTimeout = "TestTimeout"; + public const string WorkItemTimeout = "WorkItemTimeout"; + public const string ExpectedExitCode = "ExpectedExitCode"; + public const string CustomCommands = "CustomCommands"; + } protected const string ScriptNamespace = "tools.xharness_runner."; + protected const string CustomCommandsScript = "command"; /// /// Extra arguments that will be passed to the iOS/Android/... app that is being run @@ -39,25 +44,31 @@ public abstract class XHarnessTaskBase : MSBuildTaskBase /// - WorkItemTimeout - Optional timeout for the whole Helix work item run (includes SDK and tool installation) /// - ExpectedExitCode - Optional expected exit code parameter that is forwarded to XHarness /// - protected (TimeSpan TestTimeout, TimeSpan WorkItemTimeout, int ExpectedExitCode) ParseMetadata(ITaskItem xHarnessAppItem) + protected (TimeSpan TestTimeout, TimeSpan WorkItemTimeout, int ExpectedExitCode, string CustomCommands) ParseMetadata(ITaskItem xHarnessAppItem) { + xHarnessAppItem.TryGetMetadata(MetadataName.CustomCommands, out string customCommands); + if (string.IsNullOrEmpty(customCommands)) + { + customCommands = null; + } + // Optional timeout for the actual test execution in the TimeSpan format TimeSpan testTimeout = s_defaultTestTimeout; - if (xHarnessAppItem.TryGetMetadata(TestTimeoutPropName, out string testTimeoutProp)) + if (xHarnessAppItem.TryGetMetadata(MetadataName.TestTimeout, out string testTimeoutProp)) { if (!TimeSpan.TryParse(testTimeoutProp, out testTimeout) || testTimeout.Ticks < 0) { - Log.LogError($"Invalid value \"{testTimeoutProp}\" provided in <{TestTimeoutPropName}>"); + Log.LogError($"Invalid value \"{testTimeoutProp}\" provided in <{MetadataName.TestTimeout}>"); } } // Optional timeout for the whole Helix work item run (includes SDK and tool installation) TimeSpan workItemTimeout = s_defaultWorkItemTimeout; - if (xHarnessAppItem.TryGetMetadata(WorkItemTimeoutPropName, out string workItemTimeoutProp)) + if (xHarnessAppItem.TryGetMetadata(MetadataName.WorkItemTimeout, out string workItemTimeoutProp)) { if (!TimeSpan.TryParse(workItemTimeoutProp, out workItemTimeout) || workItemTimeout.Ticks < 0) { - Log.LogError($"Invalid value \"{workItemTimeoutProp}\" provided in <{WorkItemTimeoutPropName}>"); + Log.LogError($"Invalid value \"{workItemTimeoutProp}\" provided in <{MetadataName.WorkItemTimeout}>"); } } else if (!string.IsNullOrEmpty(testTimeoutProp)) @@ -67,7 +78,7 @@ public abstract class XHarnessTaskBase : MSBuildTaskBase workItemTimeout = testTimeout + s_defaultWorkItemTimeout - s_defaultTestTimeout; } - if (workItemTimeout <= testTimeout) + if (customCommands == null && workItemTimeout <= testTimeout) { Log.LogWarning( $"Work item timeout ({workItemTimeout}) should be larger than test timeout ({testTimeout}) " + @@ -75,15 +86,12 @@ public abstract class XHarnessTaskBase : MSBuildTaskBase } int expectedExitCode = 0; - if (xHarnessAppItem.TryGetMetadata(ExpectedExitCodePropName, out string expectedExitCodeProp)) + if (xHarnessAppItem.TryGetMetadata(MetadataName.ExpectedExitCode, out string expectedExitCodeProp)) { int.TryParse(expectedExitCodeProp, out expectedExitCode); } - return ( - TestTimeout: testTimeout, - WorkItemTimeout: workItemTimeout, - ExpectedExitCode: expectedExitCode); + return (testTimeout, workItemTimeout, expectedExitCode, customCommands); } } } diff --git a/src/Microsoft.DotNet.Helix/Sdk/tools/xharness-runner/Readme.md b/src/Microsoft.DotNet.Helix/Sdk/tools/xharness-runner/Readme.md index 38ed2786dc4..e9b6459f8c9 100644 --- a/src/Microsoft.DotNet.Helix/Sdk/tools/xharness-runner/Readme.md +++ b/src/Microsoft.DotNet.Helix/Sdk/tools/xharness-runner/Readme.md @@ -20,11 +20,12 @@ This is automatically included as a Helix Correlation Payload for the job when X ## How to use -There are three main ways how to use XHarness through the Helix SDK: -- Specify the apks/app bundles using the `XHarnessApkToTest` and `XHarnessAppBundleToTest` items as described below and everything will be taken care of from there. You no longer specify the `HelixCommand` to be executed. Each apk/app bundle will be processed as a separate Helix work item. +There are two main ways how to use XHarness through the Helix SDK: +- Specify the apks/app bundles using the `XHarnessApkToTest` and `XHarnessAppBundleToTest` items as described below and everything will be taken care of from there. + You no longer specify the `HelixCommand` to be executed even though you can specify your own custom commands to be executed. + Each apk/app bundle will be processed as a separate Helix work item. - Specify the `XHarnessAndroidProject` or `XHarnessAppleProject` task items which will point to projects that produce apks/app bundles from their `Build` target. - Examples - [iOS](https://github.com/dotnet/arcade/blob/master/tests/XHarness/XHarness.TestAppBundle.proj) and [Android](https://github.com/dotnet/arcade/blob/master/tests/XHarness/XHarness.TestApk.proj) -- Only request the XHarness dotnet tool to be pre-installed for the Helix job for you and then call the XHarness tool yourself as shown below. There are some required configuration properties that need to be set for XHarness to work and some optional to customize the run further: @@ -36,7 +37,7 @@ There are some required configuration properties that need to be set for XHarnes 1.0.0-prerelease.20322.1 - + test/product/ https://helix.int-dot.net $(BUILD_SOURCEVERSIONAUTHOR) @@ -50,20 +51,6 @@ There are some required configuration properties that need to be set for XHarnes ``` -### Calling the XHarness tool directly - -In case you decide to request the SDK to pre-install the XHarness tool only without any specific payload, you just don't specify `XHarnessApkToTest` or `XHarnessAppBundleToTest` items and you specify the Helix command directly. -There will be an environmental variable called `XHARNESS_CLI_PATH` set that will point to the XHarness CLI DLL that needs to be run using `dotnet exec` like so: - -```xml - - - dotnet exec $XHARNESS_CLI_PATH wasm test --engine ... - dotnet exec %XHARNESS_CLI_PATH% wasm test --engine ... - - -``` - ### iOS/tvOS/WatchOS .app bundle payloads To execute .app bundles, declare one or more `XHarnessAppBundleToTest` items: @@ -162,3 +149,41 @@ You can also specify some metadata that will help you configure the run better: ### WASM payloads We currently do not support execution of WASM workloads directly, please call the `xharness wasm test` command manually. + +### Calling the XHarness tool directly via custom commands + +In case you want to run your own custom set of commands on the application, you can specify the `CustomCommands` property. However, be mindful that you need to perform a clean up (read "uninstall the app") in case of some problems. + +Example: + +```xml + + + ios-simulator-64 + 00:12:00 + + set -e + deviceId=`xharness apple device $targets` + xharness apple install -t $targets --device "$deviceId" -o "$output_directory" --app=$app + set +e + result=0 + xharness apple just-test -t $targets --device "$deviceId" -o "$output_directory" --app net.dot.Some.iOS --timeout 00:08:00 + ((result|=$?)) + xharness apple uninstall -t $targets --device "$deviceId" -o "$output_directory" --app net.dot.Some.iOS + ((result|=$?)) + exit $result + + + +``` + +When using `CustomCommands`, several variables will be defined for you for easier run. + +#### Variables defined for Apple scenarios +- `$app` - path to the application +- `$output_directory` - path under which all files will be uploaded to Helix at the end of the job + - If a file named `testResults.xml` is found containing xUnit results, it will be uploaded back to Azure DevOps +- `$targets`, `$timeout`, `$launch_timeout`, `$expected_exit_code`, `$includes_test_runner` - parsed metadata defined on the original `XHarnessAppBundleToTest` MSBuild item + +#### Variables defined for Android scenarios +Android is currently not supported - coming soon! diff --git a/src/Microsoft.DotNet.Helix/Sdk/tools/xharness-runner/XHarnessRunner.targets b/src/Microsoft.DotNet.Helix/Sdk/tools/xharness-runner/XHarnessRunner.targets index d96450e5f79..113944554d5 100644 --- a/src/Microsoft.DotNet.Helix/Sdk/tools/xharness-runner/XHarnessRunner.targets +++ b/src/Microsoft.DotNet.Helix/Sdk/tools/xharness-runner/XHarnessRunner.targets @@ -61,6 +61,7 @@ $(HelixPreCommands);set XHARNESS_DISABLE_COLORED_OUTPUT=true $(HelixPreCommands);set XHARNESS_LOG_WITH_TIMESTAMPS=true $(HelixPreCommands);set XHARNESS_CLI_PATH=%HELIX_CORRELATION_PAYLOAD%\microsoft.dotnet.xharness.cli\$(_XHarnessPackageVersion)\tools\net6.0\any\Microsoft.DotNet.XHarness.CLI.dll + $(HelixPreCommands);doskey xharness="dotnet exec %XHARNESS_CLI_PATH%" @@ -93,8 +94,7 @@ - + diff --git a/src/Microsoft.DotNet.Helix/Sdk/tools/xharness-runner/xharness-helix-job.apple.sh b/src/Microsoft.DotNet.Helix/Sdk/tools/xharness-runner/xharness-helix-job.apple.sh index 3a654aeb38f..cb69815cee1 100644 --- a/src/Microsoft.DotNet.Helix/Sdk/tools/xharness-runner/xharness-helix-job.apple.sh +++ b/src/Microsoft.DotNet.Helix/Sdk/tools/xharness-runner/xharness-helix-job.apple.sh @@ -6,11 +6,35 @@ ### GUI rendering capabilities. ### +app='' +forwarded_args='' + +while [[ $# -gt 0 ]]; do + opt="$(echo "$1" | tr "[:upper:]" "[:lower:]")" + case "$opt" in + --app) + app="$2" + shift + ;; + *) + forwarded_args="$forwarded_args $1" + ;; + esac + shift +done + set -x +# It is important we call the script via `launchctl asuser` in order to be able to spawn +# the simulator which needs to run in a user session with GUI rendering capabilities. chmod +x xharness-runner.apple.sh helix_runner_uid=$(id -u) -sudo launchctl asuser "$helix_runner_uid" sh ./xharness-runner.apple.sh "$@" +sudo launchctl asuser "$helix_runner_uid" sh ./xharness-runner.apple.sh \ + $forwarded_args \ + --app "$HELIX_WORKITEM_ROOT/$app" \ + --xharness-cli-path "$XHARNESS_CLI_PATH" \ + --output-directory "$HELIX_WORKITEM_UPLOAD_ROOT" \ + exit_code=$? # This handles an issue where Simulators get reeaally slow and they start failing to install apps diff --git a/src/Microsoft.DotNet.Helix/Sdk/tools/xharness-runner/xharness-runner.apple.sh b/src/Microsoft.DotNet.Helix/Sdk/tools/xharness-runner/xharness-runner.apple.sh index 3fd2803a57f..c8795fb47bd 100644 --- a/src/Microsoft.DotNet.Helix/Sdk/tools/xharness-runner/xharness-runner.apple.sh +++ b/src/Microsoft.DotNet.Helix/Sdk/tools/xharness-runner/xharness-runner.apple.sh @@ -15,7 +15,7 @@ xharness_cli_path='' xcode_version='' app_arguments='' expected_exit_code=0 -command='test' +includes_test_runner=false while [[ $# -gt 0 ]]; do opt="$(echo "$1" | tr "[:upper:]" "[:lower:]")" @@ -56,13 +56,8 @@ while [[ $# -gt 0 ]]; do expected_exit_code="$2" shift ;; - --command) - command="$2" - shift - ;; - *) - echo "Invalid argument: $1" - exit 1 + --includes-test-runner) + includes_test_runner=true ;; esac shift @@ -74,23 +69,16 @@ function die () exit 1 } -if [ -z "$timeout" ]; then - die "Test timeout wasn't provided"; +if [ -z "$app" ]; then + die "App bundle path wasn't provided"; fi -if [ -z "$xharness_cli_path" ]; then - die "XHarness path wasn't provided"; +if [ -z "$targets" ]; then + die "No targets were provided"; fi -if [ -n "$app_arguments" ]; then - app_arguments="-- $app_arguments"; -fi - -if [ "$command" == "run" ]; then - app_arguments="--expected-exit-code=$expected_exit_code $app_arguments" -elif [ -n "$launch_timeout" ]; then - # shellcheck disable=SC2089 - app_arguments="--launch-timeout=$launch_timeout $app_arguments" +if [ -z "$xharness_cli_path" ]; then + die "XHarness path wasn't provided"; fi if [ -z "$xcode_version" ]; then @@ -143,19 +131,10 @@ fi export XHARNESS_DISABLE_COLORED_OUTPUT=true export XHARNESS_LOG_WITH_TIMESTAMPS=true +alias xharness="dotnet exec $xharness_cli_path" -# We include $app_arguments non-escaped and not arrayed because it might contain several extra arguments -# which come from outside and are appeneded behind "--" and forwarded to the iOS application from XHarness. -# shellcheck disable=SC2086,SC2090 -dotnet exec "$xharness_cli_path" apple $command \ - --app="$app" \ - --output-directory="$output_directory" \ - --targets="$targets" \ - --timeout="$timeout" \ - --xcode="$xcode_path" \ - -v \ - $app_arguments - +# Act out the actual commands +source command.sh exit_code=$? # Kill the simulator just in case when we fail to launch the app diff --git a/tests/UnitTests.XHarness.iOS.Device.proj b/tests/UnitTests.XHarness.iOS.Device.proj index 40172f0e4eb..917fc076e2a 100644 --- a/tests/UnitTests.XHarness.iOS.Device.proj +++ b/tests/UnitTests.XHarness.iOS.Device.proj @@ -24,7 +24,7 @@ - + diff --git a/tests/UnitTests.XHarness.iOS.IncludeCliOnly.proj b/tests/UnitTests.XHarness.iOS.IncludeCliOnly.proj deleted file mode 100644 index 70357b5d596..00000000000 --- a/tests/UnitTests.XHarness.iOS.IncludeCliOnly.proj +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - $(MSBuildThisFileDirectory)../artifacts/bin/Microsoft.DotNet.Helix.Sdk/$(Configuration)/netcoreapp3.1/publish/Microsoft.DotNet.Helix.Sdk.dll - $(MSBuildThisFileDirectory)../artifacts/bin/Microsoft.DotNet.Helix.Sdk/$(Configuration)/net472/publish/Microsoft.DotNet.Helix.Sdk.dll - - - - test/product/ - $(AGENT_JOBNAME) - true - https://helix.dot.net - true - - - - - - dotnet exec "$XHARNESS_CLI_PATH" apple state - 00:03:00 - - - - - - - - - - - - - true - $(BUILD_SOURCEVERSIONAUTHOR) - anon - - - - - msbuild - - - - - - - diff --git a/tests/UnitTests.XHarness.iOS.proj b/tests/UnitTests.XHarness.iOS.Simulator.proj similarity index 92% rename from tests/UnitTests.XHarness.iOS.proj rename to tests/UnitTests.XHarness.iOS.Simulator.proj index f3232f46ad1..7c07d677773 100644 --- a/tests/UnitTests.XHarness.iOS.proj +++ b/tests/UnitTests.XHarness.iOS.Simulator.proj @@ -24,8 +24,9 @@ - - + + + diff --git a/tests/XHarness/XHarness.RunAppBundle.Device.proj b/tests/XHarness/XHarness.Device.AppleRun.proj similarity index 100% rename from tests/XHarness/XHarness.RunAppBundle.Device.proj rename to tests/XHarness/XHarness.Device.AppleRun.proj diff --git a/tests/XHarness/XHarness.RunAppBundle.proj b/tests/XHarness/XHarness.Simulator.AppleRun.proj similarity index 100% rename from tests/XHarness/XHarness.RunAppBundle.proj rename to tests/XHarness/XHarness.Simulator.AppleRun.proj diff --git a/tests/XHarness/XHarness.TestAppBundle.proj b/tests/XHarness/XHarness.Simulator.AppleTest.proj similarity index 84% rename from tests/XHarness/XHarness.TestAppBundle.proj rename to tests/XHarness/XHarness.Simulator.AppleTest.proj index 6225cbc254a..c176f023bfe 100644 --- a/tests/XHarness/XHarness.TestAppBundle.proj +++ b/tests/XHarness/XHarness.Simulator.AppleTest.proj @@ -3,7 +3,7 @@ - System.Numerics.Vectors.Tests.app + Microsoft.Extensions.Configuration.CommandLine.Tests.app https://netcorenativeassets.blob.core.windows.net/resource-packages/external/ios/test-app/ios-simulator-64/$(XHarnessTestAppBundleName).zip @@ -21,9 +21,9 @@ ios-simulator-64 - 00:30:00 - 00:35:00 - 00:20:00 + 00:05:00 + 00:12:00 + 00:06:00 diff --git a/tests/XHarness/XHarness.Simulator.CustomCommands.proj b/tests/XHarness/XHarness.Simulator.CustomCommands.proj new file mode 100644 index 00000000000..372ac9b06cb --- /dev/null +++ b/tests/XHarness/XHarness.Simulator.CustomCommands.proj @@ -0,0 +1,43 @@ + + + + + + System.Numerics.Vectors.Tests + https://netcorenativeassets.blob.core.windows.net/resource-packages/external/ios/test-app/ios-simulator-64/$(XHarnessAppBundleName).app.zip + + + + + + + + + + + + + + + + ios-simulator-64 + 00:12:00 + 00:05:00 + 00:05:00 + + set -ex + deviceId=`xharness apple device $targets` + xharness apple install -t=$targets --device="$deviceId" -o="$output_directory" --app="$app" -v + set +e + result=0 + xharness apple just-test -t=$targets --device="$deviceId" -o="$output_directory" --app="net.dot.$(XHarnessAppBundleName)" --launch-timeout=$launch_timeout --timeout=$timeout -v + ((result|=$?)) + xharness apple uninstall -t=$targets --device="$deviceId" -o="$output_directory" --app="net.dot.$(XHarnessAppBundleName)" -v + ((result|=$?)) + exit $result + + + + + +