File: XharnessTaskBase.cs
Web Access
Project: src\src\Microsoft.DotNet.Helix\Sdk\Microsoft.DotNet.Helix.Sdk.csproj (Microsoft.DotNet.Helix.Sdk)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Arcade.Common;
using Microsoft.Build.Framework;
 
namespace Microsoft.DotNet.Helix.Sdk
{
    /// <summary>
    /// MSBuild custom task to create HelixWorkItems for provided Android application packages.
    /// </summary>
    public abstract class XHarnessTaskBase : MSBuildTaskBase
    {
        private static readonly TimeSpan s_defaultWorkItemTimeout = TimeSpan.FromMinutes(20);
        private static readonly TimeSpan s_defaultTestTimeout = TimeSpan.FromMinutes(12);
        private static readonly TimeSpan s_telemetryBuffer = TimeSpan.FromMinutes(2); // extra time to send the XHarness telemetry
 
        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.";
        private const string CustomCommandsScript = "command";
        private const string DiagnosticsScript = "xharness-event-processor.py";
 
        /// <summary>
        /// Extra arguments that will be passed to the iOS/Android/... app that is being run
        /// </summary>
        public string AppArguments { get; set; }
 
        /// <summary>
        /// An array of ITaskItems of type HelixWorkItem
        /// </summary>
        [Output]
        public ITaskItem[] WorkItems { get; set; }
 
        /// <summary>
        /// Parses metadata of the task item pointing to the app (app bundle/apk) we want to turn into an XHarness job.
        /// </summary>
        /// <param name="xHarnessAppItem">MSBuild task item</param>
        /// <returns>
        /// Parsed data:
        ///   - TestTimeout - Optional timeout for the actual test execution
        ///   - 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
        /// </returns>
        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(MetadataName.TestTimeout, out string testTimeoutProp))
            {
                if (!TimeSpan.TryParse(testTimeoutProp, out testTimeout) || testTimeout.Ticks < 0)
                {
                    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(MetadataName.WorkItemTimeout, out string workItemTimeoutProp))
            {
                if (!TimeSpan.TryParse(workItemTimeoutProp, out workItemTimeout) || workItemTimeout.Ticks < 0)
                {
                    Log.LogError($"Invalid value \"{workItemTimeoutProp}\" provided in <{MetadataName.WorkItemTimeout}>");
                }
            }
            else if (!string.IsNullOrEmpty(testTimeoutProp))
            {
                // When test timeout was set and work item timeout has not,
                // we adjust the work item timeout to give enough space for things to work
                workItemTimeout = testTimeout + s_defaultWorkItemTimeout - s_defaultTestTimeout;
            }
 
            if (customCommands == null && workItemTimeout <= testTimeout)
            {
                Log.LogWarning(
                    $"Work item timeout ({workItemTimeout}) should be larger than test timeout ({testTimeout}) " +
                    $"to allow the XHarness tool to be initialized properly.");
            }
 
            int expectedExitCode = 0;
            if (xHarnessAppItem.TryGetMetadata(MetadataName.ExpectedExitCode, out string expectedExitCodeProp))
            {
                int.TryParse(expectedExitCodeProp, out expectedExitCode);
            }
 
            return (testTimeout, workItemTimeout, expectedExitCode, customCommands);
        }
 
        protected Build.Utilities.TaskItem CreateTaskItem(string workItemName, string payloadArchivePath, string command, TimeSpan timeout)
        {
            Log.LogMessage($"Creating work item with properties Identity: {workItemName}, Payload: {payloadArchivePath}, Command: {command}");
 
            // Leave some time at the end of the work item to send the telemetry (in case it times out)
            timeout += s_telemetryBuffer;
 
            return new(workItemName, new Dictionary<string, string>()
            {
                { "Identity", workItemName },
                { "PayloadArchive", payloadArchivePath },
                { "Command", command },
                { "Timeout", timeout.ToString() },
            });
        }
 
        protected async Task<string> CreatePayloadArchive(
            IZipArchiveManager zipArchiveManager,
            IFileSystem fileSystem,
            string workItemName,
            bool isAlreadyArchived,
            bool isPosix,
            string pathToZip,
            string injectedCommands,
            string[] payloadScripts)
        {
            string outputZipPath;
            if (!isAlreadyArchived)
            {
                string appFolderDirectory = fileSystem.GetDirectoryName(pathToZip);
                string fileName = $"xharness-payload-{workItemName.ToLowerInvariant()}.zip";
                outputZipPath = fileSystem.PathCombine(appFolderDirectory, fileName);
 
                if (fileSystem.FileExists(outputZipPath))
                {
                    Log.LogMessage($"Zip archive '{outputZipPath}' already exists, overwriting..");
                    fileSystem.DeleteFile(outputZipPath);
                }
 
                if (fileSystem.GetAttributes(pathToZip).HasFlag(FileAttributes.Directory))
                {
                    zipArchiveManager.ArchiveDirectory(pathToZip, outputZipPath, true);
                }
                else
                {
                    zipArchiveManager.ArchiveFile(pathToZip, outputZipPath);
                }
            }
            else
            {
                Log.LogMessage($"App payload '{workItemName}` has already been zipped");
                outputZipPath = pathToZip;
            }
 
            Log.LogMessage($"Adding the XHarness job scripts into the payload archive");
 
            foreach (var payloadScript in payloadScripts)
            {
                await zipArchiveManager.AddResourceFileToArchive<XHarnessTaskBase>(
                    outputZipPath,
                    ScriptNamespace + payloadScript,
                    payloadScript);
            }
 
            await zipArchiveManager.AddResourceFileToArchive<XHarnessTaskBase>(
                outputZipPath,
                ScriptNamespace + DiagnosticsScript,
                DiagnosticsScript);
 
            await zipArchiveManager.AddContentToArchive(
                outputZipPath,
                CustomCommandsScript + (isPosix ? ".sh" : ".ps1"),
                injectedCommands);
 
            return outputZipPath;
        }
 
        /// <summary>
        /// This method parses the name for the Helix work item and path of the app from the item's metadata.
        /// 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.
        /// </summary>
        public static (string WorkItemName, string AppPath) GetNameAndPath(ITaskItem item, string pathMetadataName, IFileSystem fileSystem)
        {
            if (item.TryGetMetadata(pathMetadataName, out string appPathMetadata) && !string.IsNullOrEmpty(appPathMetadata))
            {
                return (item.ItemSpec, appPathMetadata);
            }
            else
            {
                return (fileSystem.GetFileNameWithoutExtension(item.ItemSpec), item.ItemSpec);
            }
        }
    }
}