Exemple #1
0
        /// <summary>
        /// Prepares HelixWorkItem that can run on a device (currently Android or iOS) using XHarness
        /// </summary>
        /// <param name="appPackage">Path to application package</param>
        /// <returns>An ITaskItem instance representing the prepared HelixWorkItem.</returns>
        private async Task <ITaskItem> PrepareWorkItem(IZipArchiveManager zipArchiveManager, IFileSystem fileSystem, ITaskItem appPackage)
        {
            string workItemName = fileSystem.GetFileNameWithoutExtension(appPackage.ItemSpec);

            var(testTimeout, workItemTimeout, expectedExitCode, customCommands) = ParseMetadata(appPackage);

            string command = ValidateMetadataAndGetXHarnessAndroidCommand(appPackage, testTimeout, expectedExitCode);

            if (!fileSystem.GetExtension(appPackage.ItemSpec).Equals(".apk", StringComparison.OrdinalIgnoreCase))
            {
                Log.LogError($"Unsupported app package type: {fileSystem.GetFileName(appPackage.ItemSpec)}");
                return(null);
            }

            Log.LogMessage($"Creating work item with properties Identity: {workItemName}, Payload: {appPackage.ItemSpec}, Command: {command}");

            string workItemZip = await CreateZipArchiveOfPackageAsync(zipArchiveManager, fileSystem, appPackage.ItemSpec);

            return(new Build.Utilities.TaskItem(workItemName, new Dictionary <string, string>()
            {
                { "Identity", workItemName },
                { "PayloadArchive", workItemZip },
                { "Command", command },
                { "Timeout", workItemTimeout.ToString() },
            }));
        }
        private async Task <string> CreateZipArchiveOfFolder(
            IZipArchiveManager zipArchiveManager,
            IFileSystem fileSystem,
            string folderToZip,
            string injectedCommands)
        {
            if (!fileSystem.DirectoryExists(folderToZip))
            {
                Log.LogError($"Cannot find path containing app: '{folderToZip}'");
                return(string.Empty);
            }

            string appFolderDirectory = fileSystem.GetDirectoryName(folderToZip);
            string fileName           = $"xharness-app-payload-{fileSystem.GetFileName(folderToZip).ToLowerInvariant()}.zip";
            string outputZipPath      = fileSystem.PathCombine(appFolderDirectory, fileName);

            if (fileSystem.FileExists(outputZipPath))
            {
                Log.LogMessage($"Zip archive '{outputZipPath}' already exists, overwriting..");
                fileSystem.DeleteFile(outputZipPath);
            }

            zipArchiveManager.ArchiveDirectory(folderToZip, outputZipPath, true);

            Log.LogMessage($"Adding the XHarness job scripts into the payload archive");
            await zipArchiveManager.AddResourceFileToArchive <CreateXHarnessAppleWorkItems>(outputZipPath, ScriptNamespace + EntryPointScript, EntryPointScript);

            await zipArchiveManager.AddResourceFileToArchive <CreateXHarnessAppleWorkItems>(outputZipPath, ScriptNamespace + RunnerScript, RunnerScript);

            await zipArchiveManager.AddContentToArchive(outputZipPath, CustomCommandsScript + ".sh", injectedCommands);

            return(outputZipPath);
        }
        private async Task <string> CreateZipArchiveOfPackageAsync(
            IZipArchiveManager zipArchiveManager,
            IFileSystem fileSystem,
            string workItemName,
            string fileToZip,
            string injectedCommands)
        {
            string fileName      = $"xharness-apk-payload-{workItemName.ToLowerInvariant()}.zip";
            string outputZipPath = fileSystem.PathCombine(fileSystem.GetDirectoryName(fileToZip), fileName);

            if (fileSystem.FileExists(outputZipPath))
            {
                Log.LogMessage($"Zip archive '{outputZipPath}' already exists, overwriting..");
                fileSystem.DeleteFile(outputZipPath);
            }

            zipArchiveManager.ArchiveFile(fileToZip, outputZipPath);

            // WorkItem payloads of APKs can be reused if sent to multiple queues at once,
            // so we'll always include both scripts (very small)
            await zipArchiveManager.AddResourceFileToArchive <CreateXHarnessAndroidWorkItems>(outputZipPath, ScriptNamespace + PosixAndroidWrapperScript, PosixAndroidWrapperScript);

            await zipArchiveManager.AddResourceFileToArchive <CreateXHarnessAndroidWorkItems>(outputZipPath, ScriptNamespace + NonPosixAndroidWrapperScript, NonPosixAndroidWrapperScript);

            await zipArchiveManager.AddContentToArchive(outputZipPath, CustomCommandsScript + (IsPosixShell ? ".sh" : ".ps1"), injectedCommands);

            return(outputZipPath);
        }
Exemple #4
0
        /// <summary>
        /// The main method of this MSBuild task which calls the asynchronous execution method and
        /// collates logged errors in order to determine the success of HelixWorkItems
        /// </summary>
        /// <returns>A boolean value indicating the success of HelixWorkItem creation</returns>
        public bool ExecuteTask(IZipArchiveManager zipArchiveManager, IFileSystem fileSystem)
        {
            var tasks = Apks.Select(apk => PrepareWorkItem(zipArchiveManager, fileSystem, apk));

            WorkItems = Task.WhenAll(tasks).GetAwaiter().GetResult().Where(wi => wi != null).ToArray();

            return(!Log.HasLoggedErrors);
        }
Exemple #5
0
        /// <summary>
        /// The main method of this MSBuild task which calls the asynchronous execution method and
        /// collates logged errors in order to determine the success of HelixWorkItems
        /// </summary>
        /// <returns>A boolean value indicating the success of HelixWorkItem creation</returns>
        public bool ExecuteTask(
            IProvisioningProfileProvider provisioningProfileProvider,
            IZipArchiveManager zipArchiveManager,
            IFileSystem fileSystem)
        {
            var tasks = AppBundles.Select(bundle => PrepareWorkItem(zipArchiveManager, fileSystem, provisioningProfileProvider, bundle));

            WorkItems = Task.WhenAll(tasks).GetAwaiter().GetResult().Where(wi => wi != null).ToArray();

            return(!Log.HasLoggedErrors);
        }
        /// <summary>
        /// Prepares HelixWorkItem that can run on a device (currently Android or iOS) using XHarness
        /// </summary>
        /// <param name="appPackage">Path to application package</param>
        /// <returns>An ITaskItem instance representing the prepared HelixWorkItem.</returns>
        private async Task <ITaskItem> PrepareWorkItem(IZipArchiveManager zipArchiveManager, IFileSystem fileSystem, ITaskItem appPackage)
        {
            // The user can re-use the same .apk for 2 work items so the name of the work item will come from ItemSpec and path from metadata
            // This can be useful when we want to run the same app with different parameters or run the same app on different test targets, e.g. iOS 13 and 13.5
            string workItemName;
            string apkPath;

            if (appPackage.TryGetMetadata(MetadataNames.ApkPath, out string apkPathMetadata) && !string.IsNullOrEmpty(apkPathMetadata))
            {
                workItemName = appPackage.ItemSpec;
                apkPath      = apkPathMetadata;
            }
            else
            {
                workItemName = fileSystem.GetFileNameWithoutExtension(appPackage.ItemSpec);
                apkPath      = appPackage.ItemSpec;
            }

            if (!fileSystem.FileExists(apkPath))
            {
                Log.LogError($"App package not found in {apkPath}");
                return(null);
            }

            if (!fileSystem.GetExtension(apkPath).Equals(".apk", StringComparison.OrdinalIgnoreCase))
            {
                Log.LogError($"Unsupported app package type: {fileSystem.GetFileName(apkPath)}");
                return(null);
            }

            // Validation of any metadata specific to Android stuff goes here
            if (!appPackage.GetRequiredMetadata(Log, MetadataNames.AndroidPackageName, out string androidPackageName))
            {
                Log.LogError($"{MetadataNames.AndroidPackageName} metadata must be specified; this may match, but can vary from file name");
                return(null);
            }

            var(testTimeout, workItemTimeout, expectedExitCode, customCommands) = ParseMetadata(appPackage);

            if (customCommands == null)
            {
                // When no user commands are specified, we add the default `android test ...` command
                customCommands = GetDefaultCommand(appPackage, expectedExitCode);
            }

            string command = GetHelixCommand(appPackage, apkPath, androidPackageName, testTimeout, expectedExitCode);

            Log.LogMessage($"Creating work item with properties Identity: {workItemName}, Payload: {apkPath}, Command: {command}");

            string workItemZip = await CreateZipArchiveOfPackageAsync(zipArchiveManager, fileSystem, workItemName, apkPath, customCommands);

            return(CreateTaskItem(workItemName, workItemZip, command, workItemTimeout));
        }
Exemple #7
0
        private async Task <string> CreateZipArchiveOfFolder(
            IZipArchiveManager zipArchiveManager,
            IFileSystem fileSystem,
            string workItemName,
            bool isAlreadyArchived,
            string folderToZip,
            string injectedCommands)
        {
            string appFolderDirectory = fileSystem.GetDirectoryName(folderToZip);
            string fileName           = $"xharness-app-payload-{workItemName.ToLowerInvariant()}.zip";
            string outputZipPath      = fileSystem.PathCombine(appFolderDirectory, fileName);

            if (fileSystem.FileExists(outputZipPath))
            {
                Log.LogMessage($"Zip archive '{outputZipPath}' already exists, overwriting..");
                fileSystem.DeleteFile(outputZipPath);
            }

            if (!isAlreadyArchived)
            {
                zipArchiveManager.ArchiveDirectory(folderToZip, outputZipPath, true);
            }
            else
            {
                Log.LogMessage($"App payload '{workItemName}` has already been zipped. Copying to '{outputZipPath}` instead");
                fileSystem.FileCopy(folderToZip, outputZipPath);
            }

            Log.LogMessage($"Adding the XHarness job scripts into the payload archive");
            await zipArchiveManager.AddResourceFileToArchive <CreateXHarnessAppleWorkItems>(outputZipPath, ScriptNamespace + EntryPointScript, EntryPointScript);

            await zipArchiveManager.AddResourceFileToArchive <CreateXHarnessAppleWorkItems>(outputZipPath, ScriptNamespace + RunnerScript, RunnerScript);

            await zipArchiveManager.AddContentToArchive(outputZipPath, CustomCommandsScript + ".sh", injectedCommands);

            return(outputZipPath);
        }
        /// <summary>
        /// Prepares HelixWorkItem that can run on an iOS device using XHarness
        /// </summary>
        /// <param name="appFolderPath">Path to application package</param>
        /// <returns>An ITaskItem instance representing the prepared HelixWorkItem.</returns>
        private async Task <ITaskItem> PrepareWorkItem(
            IZipArchiveManager zipArchiveManager,
            IFileSystem fileSystem,
            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, customCommands) = ParseMetadata(appBundleItem);

            // Validation of any metadata specific to iOS stuff goes here
            if (!appBundleItem.TryGetMetadata(MetadataNames.Target, out string target))
            {
                Log.LogError($"'{MetadataNames.Target}' 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();

            // 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(MetadataNames.LaunchTimeout, out string launchTimeoutProp))
            {
                if (!TimeSpan.TryParse(launchTimeoutProp, out launchTimeout) || launchTimeout.Ticks < 0)
                {
                    Log.LogError($"Invalid value \"{launchTimeoutProp}\" provided in <{MetadataNames.LaunchTimeout}>");
                    return(null);
                }
            }

            bool includesTestRunner = true;

            if (appBundleItem.TryGetMetadata(MetadataNames.IncludesTestRunner, out string includesTestRunnerProp))
            {
                if (includesTestRunnerProp.ToLowerInvariant() == "false")
                {
                    includesTestRunner = false;
                }
            }

            if (includesTestRunner && expectedExitCode != 0 && customCommands != null)
            {
                Log.LogWarning("The ExpectedExitCode property is ignored in the `apple test` scenario");
            }

            bool resetSimulator = false;

            if (appBundleItem.TryGetMetadata(MetadataNames.ResetSimulator, out string resetSimulatorRunnerProp))
            {
                if (resetSimulatorRunnerProp.ToLowerInvariant() == "true")
                {
                    resetSimulator = true;
                }
            }

            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\" " +
                                 "--target \"$target\" " +
                                 "--timeout \"$timeout\" " +
                                 (includesTestRunner
                        ? $"--launch-timeout \"$launch_timeout\" "
                        : $"--expected-exit-code $expected_exit_code ") +
                                 (resetSimulator ? $"--reset-simulator " : string.Empty) +
                                 "--xcode \"$xcode_path\" " +
                                 "-v " +
                                 (!string.IsNullOrEmpty(AppArguments) ? "-- " + AppArguments : string.Empty);
            }

            string appName            = fileSystem.GetFileName(appBundleItem.ItemSpec);
            string helixCommand       = GetHelixCommand(appName, target, testTimeout, launchTimeout, includesTestRunner, expectedExitCode, resetSimulator);
            string payloadArchivePath = await CreateZipArchiveOfFolder(zipArchiveManager, fileSystem, appFolderPath, customCommands);

            Log.LogMessage($"Creating work item with properties Identity: {workItemName}, Payload: {appFolderPath}, Command: {helixCommand}");

            return(new Build.Utilities.TaskItem(workItemName, new Dictionary <string, string>()
            {
                { "Identity", workItemName },
                { "PayloadArchive", payloadArchivePath },
                { "Command", helixCommand },
                { "Timeout", workItemTimeout.ToString() },
            }));
        }
Exemple #9
0
        /// <summary>
        /// Prepares HelixWorkItem that can run on a device (currently Android or iOS) using XHarness
        /// </summary>
        /// <param name="appPackage">Path to application package</param>
        /// <returns>An ITaskItem instance representing the prepared HelixWorkItem.</returns>
        private async Task <ITaskItem> PrepareWorkItem(IZipArchiveManager zipArchiveManager, IFileSystem fileSystem, ITaskItem appPackage)
        {
            var(workItemName, apkPath) = GetNameAndPath(appPackage, MetadataNames.ApkPath, fileSystem);

            if (!fileSystem.FileExists(apkPath))
            {
                Log.LogError($"App package not found in {apkPath}");
                return(null);
            }

            string extension         = fileSystem.GetExtension(apkPath).ToLowerInvariant();
            bool   isAlreadyArchived = (extension == ".zip");

            if (!isAlreadyArchived && extension != ".apk")
            {
                Log.LogError($"Unsupported payload file `{fileSystem.GetFileName(apkPath)}`; expecting .apk or .zip");
                return(null);
            }

            var(testTimeout, workItemTimeout, expectedExitCode, customCommands) = ParseMetadata(appPackage);
            appPackage.TryGetMetadata(MetadataNames.AndroidPackageName, out string androidPackageName);

            if (customCommands == null)
            {
                // When no user commands are specified, we add the default `android test ...` command
                customCommands = GetDefaultCommand(appPackage, expectedExitCode);

                // Validation of any metadata specific to Android stuff goes here
                if (string.IsNullOrEmpty(androidPackageName))
                {
                    Log.LogError($"{MetadataNames.AndroidPackageName} metadata must be specified when not supplying custom commands");
                    return(null);
                }
            }

            string apkName = Path.GetFileName(apkPath);

            if (isAlreadyArchived)
            {
                apkName = apkName.Replace(".zip", ".apk");
            }

            string command = GetHelixCommand(appPackage, apkName, androidPackageName, workItemTimeout, testTimeout, expectedExitCode);

            if (!IsPosixShell)
            {
                // For windows, we need to add a .ps1 header to turn the script into a cmdlet
                customCommands = WrapCustomCommands(customCommands);
            }

            string workItemZip = await CreatePayloadArchive(
                zipArchiveManager,
                fileSystem,
                workItemName,
                isAlreadyArchived,
                IsPosixShell,
                apkPath,
                customCommands,
                new[]
            {
                // WorkItem payloads of APKs can be reused if sent to multiple queues at once,
                // so we'll always include both scripts (very small)
                PosixAndroidScript, NonPosixAndroidScript
            });

            return(CreateTaskItem(workItemName, workItemZip, command, workItemTimeout));
        }
Exemple #10
0
        /// <summary>
        /// Prepares HelixWorkItem that can run on an iOS device using XHarness
        /// </summary>
        /// <param name="appFolderPath">Path to application package</param>
        /// <returns>An ITaskItem instance representing the prepared HelixWorkItem.</returns>
        private async Task <ITaskItem> PrepareWorkItem(
            IZipArchiveManager zipArchiveManager,
            IFileSystem fileSystem,
            IProvisioningProfileProvider provisioningProfileProvider,
            ITaskItem appBundleItem)
        {
            var(workItemName, appFolderPath) = GetNameAndPath(appBundleItem, MetadataNames.AppBundlePath, fileSystem);

            appFolderPath = appFolderPath.TrimEnd(Path.DirectorySeparatorChar);

            bool isAlreadyArchived = appFolderPath.EndsWith(".zip");

            if (isAlreadyArchived && workItemName.EndsWith(".app"))
            {
                // If someone named the zip something.app.zip, we want both gone
                workItemName = workItemName.Substring(0, workItemName.Length - 4);
            }

            if (!ValidateAppBundlePath(fileSystem, appFolderPath, isAlreadyArchived))
            {
                Log.LogError($"App bundle not found in {appFolderPath}");
                return(null);
            }

            // If we are re-using one .zip for multiple work items, we need to copy it to a new location
            // because we will be changing the contents (we assume we don't mind otherwise)
            if (isAlreadyArchived && appBundleItem.TryGetMetadata(MetadataNames.AppBundlePath, out string metadata) && !string.IsNullOrEmpty(metadata))
            {
                string appFolderDirectory = fileSystem.GetDirectoryName(appFolderPath);
                string fileName           = $"xharness-payload-{workItemName.ToLowerInvariant()}.zip";
                string archiveCopyPath    = fileSystem.PathCombine(appFolderDirectory, fileName);
                fileSystem.CopyFile(appFolderPath, archiveCopyPath, overwrite: true);
                appFolderPath = archiveCopyPath;
            }

            var(testTimeout, workItemTimeout, expectedExitCode, customCommands) = ParseMetadata(appBundleItem);

            // Validation of any metadata specific to iOS stuff goes here
            if (!appBundleItem.TryGetMetadata(MetadataNames.Target, out string target))
            {
                Log.LogError($"'{MetadataNames.Target}' 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();

            // Optional timeout for the how long it takes for the app to be installed, booted and tests start executing
            TimeSpan launchTimeout = target.Contains("device") ? s_defaultDeviceLaunchTimeout : s_defaultSimulatorLaunchTimeout;

            if (appBundleItem.TryGetMetadata(MetadataNames.LaunchTimeout, out string launchTimeoutProp))
            {
                if (!TimeSpan.TryParse(launchTimeoutProp, out launchTimeout) || launchTimeout.Ticks < 0)
                {
                    Log.LogError($"Invalid value \"{launchTimeoutProp}\" provided in <{MetadataNames.LaunchTimeout}>");
                    return(null);
                }
            }

            bool includesTestRunner = true;

            if (appBundleItem.TryGetMetadata(MetadataNames.IncludesTestRunner, out string includesTestRunnerProp))
            {
                if (includesTestRunnerProp.ToLowerInvariant() == "false")
                {
                    includesTestRunner = false;
                }
            }

            if (includesTestRunner && expectedExitCode != 0 && customCommands != null)
            {
                Log.LogWarning($"The {MetadataName.ExpectedExitCode} property is ignored in the `apple test` scenario");
            }

            bool resetSimulator = false;

            if (appBundleItem.TryGetMetadata(MetadataNames.ResetSimulator, out string resetSimulatorRunnerProp))
            {
                if (resetSimulatorRunnerProp.ToLowerInvariant() == "true")
                {
                    resetSimulator = true;
                }
            }

            if (customCommands == null)
            {
                // When no user commands are specified, we add the default `apple test ...` command
                customCommands = GetDefaultCommand(includesTestRunner, resetSimulator);
            }

            string appName            = isAlreadyArchived ? $"{fileSystem.GetFileNameWithoutExtension(appFolderPath)}.app" : fileSystem.GetFileName(appFolderPath);
            string helixCommand       = GetHelixCommand(appName, target, workItemTimeout, testTimeout, launchTimeout, includesTestRunner, expectedExitCode, resetSimulator);
            string payloadArchivePath = await CreatePayloadArchive(
                zipArchiveManager,
                fileSystem,
                workItemName,
                isAlreadyArchived,
                isPosix : true,
                appFolderPath,
                customCommands,
                new[] { EntryPointScript, RunnerScript });

            provisioningProfileProvider.AddProfileToPayload(payloadArchivePath, target);

            return(CreateTaskItem(workItemName, payloadArchivePath, helixCommand, workItemTimeout));
        }
Exemple #11
0
        /// <summary>
        /// Prepares HelixWorkItem that can run on an iOS device using XHarness
        /// </summary>
        /// <param name="appFolderPath">Path to application package</param>
        /// <returns>An ITaskItem instance representing the prepared HelixWorkItem.</returns>
        private async Task <ITaskItem> PrepareWorkItem(
            IZipArchiveManager zipArchiveManager,
            IFileSystem fileSystem,
            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);

            // Validation of any metadata specific to iOS stuff goes here
            if (!appBundleItem.TryGetMetadata(TargetPropName, out string target))
            {
                Log.LogError($"'{TargetPropName}' 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();

            // 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 (!TimeSpan.TryParse(launchTimeoutProp, out launchTimeout) || launchTimeout.Ticks < 0)
                {
                    Log.LogError($"Invalid value \"{launchTimeoutProp}\" provided in <{LaunchTimeoutPropName}>");
                    return(null);
                }
            }

            bool includesTestRunner = true;

            if (appBundleItem.TryGetMetadata(IncludesTestRunnerPropName, out string includesTestRunnerProp))
            {
                if (includesTestRunnerProp.ToLowerInvariant() == "false")
                {
                    includesTestRunner = false;
                }
            }

            if (includesTestRunner && expectedExitCode != 0)
            {
                Log.LogWarning("The ExpectedExitCode property is ignored in the `apple test` scenario");
            }

            string appName            = fileSystem.GetFileName(appBundleItem.ItemSpec);
            string command            = GetHelixCommand(appName, target, testTimeout, launchTimeout, includesTestRunner, expectedExitCode);
            string payloadArchivePath = await CreateZipArchiveOfFolder(zipArchiveManager, fileSystem, appFolderPath);

            Log.LogMessage($"Creating work item with properties Identity: {workItemName}, Payload: {appFolderPath}, Command: {command}");

            return(new Build.Utilities.TaskItem(workItemName, new Dictionary <string, string>()
            {
                { "Identity", workItemName },
                { "PayloadArchive", payloadArchivePath },
                { "Command", command },
                { "Timeout", workItemTimeout.ToString() },
            }));
        }
Exemple #12
0
        /// <summary>
        /// Prepares HelixWorkItem that can run on an iOS device using XHarness
        /// </summary>
        /// <param name="appFolderPath">Path to application package</param>
        /// <returns>An ITaskItem instance representing the prepared HelixWorkItem.</returns>
        private async Task <ITaskItem> PrepareWorkItem(
            IZipArchiveManager zipArchiveManager,
            IFileSystem fileSystem,
            ITaskItem appBundleItem)
        {
            // The user can re-use the same .apk for 2 work items so the name of the work item will come from ItemSpec and path from metadata
            string workItemName;
            string appFolderPath;

            if (appBundleItem.TryGetMetadata(MetadataNames.AppBundlePath, out string appPathMetadata) && !string.IsNullOrEmpty(appPathMetadata))
            {
                workItemName  = appBundleItem.ItemSpec;
                appFolderPath = appPathMetadata;
            }
            else
            {
                workItemName  = fileSystem.GetFileName(appBundleItem.ItemSpec);
                appFolderPath = appBundleItem.ItemSpec;
            }

            appFolderPath = appFolderPath.TrimEnd(Path.DirectorySeparatorChar);

            bool isAlreadyArchived = workItemName.EndsWith(".zip");

            if (isAlreadyArchived || workItemName.EndsWith(".app"))
            {
                workItemName = workItemName.Substring(0, workItemName.Length - 4);
            }

            if (!ValidateAppBundlePath(fileSystem, appFolderPath, isAlreadyArchived))
            {
                Log.LogError($"App bundle not found in {appFolderPath}");
                return(null);
            }

            var(testTimeout, workItemTimeout, expectedExitCode, customCommands) = ParseMetadata(appBundleItem);

            // Validation of any metadata specific to iOS stuff goes here
            if (!appBundleItem.TryGetMetadata(MetadataNames.Target, out string target))
            {
                Log.LogError($"'{MetadataNames.Target}' 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();

            // 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(MetadataNames.LaunchTimeout, out string launchTimeoutProp))
            {
                if (!TimeSpan.TryParse(launchTimeoutProp, out launchTimeout) || launchTimeout.Ticks < 0)
                {
                    Log.LogError($"Invalid value \"{launchTimeoutProp}\" provided in <{MetadataNames.LaunchTimeout}>");
                    return(null);
                }
            }

            bool includesTestRunner = true;

            if (appBundleItem.TryGetMetadata(MetadataNames.IncludesTestRunner, out string includesTestRunnerProp))
            {
                if (includesTestRunnerProp.ToLowerInvariant() == "false")
                {
                    includesTestRunner = false;
                }
            }

            if (includesTestRunner && expectedExitCode != 0 && customCommands != null)
            {
                Log.LogWarning("The ExpectedExitCode property is ignored in the `apple test` scenario");
            }

            bool resetSimulator = false;

            if (appBundleItem.TryGetMetadata(MetadataNames.ResetSimulator, out string resetSimulatorRunnerProp))
            {
                if (resetSimulatorRunnerProp.ToLowerInvariant() == "true")
                {
                    resetSimulator = true;
                }
            }

            if (customCommands == null)
            {
                // When no user commands are specified, we add the default `apple test ...` command
                customCommands = GetDefaultCommand(target, includesTestRunner, resetSimulator);
            }

            string appName            = isAlreadyArchived ? $"{fileSystem.GetFileNameWithoutExtension(appFolderPath)}.app" : fileSystem.GetFileName(appFolderPath);
            string helixCommand       = GetHelixCommand(appName, target, testTimeout, launchTimeout, includesTestRunner, expectedExitCode, resetSimulator);
            string payloadArchivePath = await CreateZipArchiveOfFolder(zipArchiveManager, fileSystem, workItemName, isAlreadyArchived, appFolderPath, customCommands);

            Log.LogMessage($"Creating work item with properties Identity: {workItemName}, Payload: {appFolderPath}, Command: {helixCommand}");

            return(CreateTaskItem(workItemName, payloadArchivePath, helixCommand, workItemTimeout));
        }