Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions src/Common/Microsoft.Arcade.Common/ZipArchiveManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
Expand All @@ -22,20 +23,32 @@ public void ArchiveDirectory(string directoryPath, string archivePath, bool incl

public void ArchiveFile(string filePath, string archivePath)
{
using (FileStream fs = File.OpenWrite(archivePath))
using (var zip = new ZipArchive(fs, ZipArchiveMode.Create, false))
bool archiveExists = File.Exists(archivePath);
string entryName = Path.GetFileName(filePath);

using FileStream fs = File.OpenWrite(archivePath);
using ZipArchive zip = new(fs, archiveExists ? ZipArchiveMode.Update : ZipArchiveMode.Create, false);

// Overwrite previous file, if any (otherwise dual records were created)
if (archiveExists)
{
zip.CreateEntryFromFile(filePath, Path.GetFileName(filePath));
zip.Entries.FirstOrDefault(e => e.FullName == entryName)?.Delete();
}

zip.CreateEntryFromFile(filePath, entryName);
}

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);
using FileStream archiveStream = new(archivePath, FileMode.Open);
using ZipArchive archive = new(archiveStream, ZipArchiveMode.Update);

// Overwrite previous file, if any (otherwise dual records were created)
archive.Entries.FirstOrDefault(e => e.FullName == targetFilename)?.Delete();

ZipArchiveEntry entry = archive.CreateEntry(targetFilename);
using Stream targetStream = entry.Open();
await content.CopyToAsync(targetStream);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ public void AndroidXHarnessWorkItemIsCreated()
var command = workItem.GetMetadata("Command");
command.Should().Contain("-timeout \"00:08:55\"");

_zipArchiveManager
.Verify(x => x.ArchiveFile("/apks/System.Foo.apk", payloadArchive), Times.Once);
_zipArchiveManager
.Verify(x => x.AddResourceFileToArchive<CreateXHarnessAndroidWorkItems>(payloadArchive, It.Is<string>(s => s.Contains("xharness-helix-job.android.sh")), "xharness-helix-job.android.sh"), Times.AtLeastOnce);
_zipArchiveManager
Expand Down Expand Up @@ -157,6 +159,42 @@ public void ApkIsReused()
_fileSystem.FileExists(payloadArchive).Should().BeTrue();
}

[Fact]
public void ZippedApkIsProvided()
{
var collection = CreateMockServiceCollection();
_task.ConfigureServices(collection);
_task.Apks = new[]
{
CreateApk("/apks/System.Foo.zip", "System.Foo", "00:15:42", "00:08:55")
};

// 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");
workItem.GetMetadata("Timeout").Should().Be("00:15:42");

var payloadArchive = workItem.GetMetadata("PayloadArchive");
payloadArchive.Should().NotBeNullOrEmpty();
_fileSystem.FileExists(payloadArchive).Should().BeTrue();

var command = workItem.GetMetadata("Command");
command.Should().Contain("-timeout \"00:08:55\"");

_zipArchiveManager
.Verify(x => x.ArchiveFile(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
_zipArchiveManager
.Verify(x => x.AddResourceFileToArchive<CreateXHarnessAndroidWorkItems>(payloadArchive, It.Is<string>(s => s.Contains("xharness-helix-job.android.sh")), "xharness-helix-job.android.sh"), Times.AtLeastOnce);
_zipArchiveManager
.Verify(x => x.AddResourceFileToArchive<CreateXHarnessAndroidWorkItems>(payloadArchive, It.Is<string>(s => s.Contains("xharness-helix-job.android.ps1")), "xharness-helix-job.android.ps1"), Times.AtLeastOnce);
}

[Fact]
public void AreDependenciesRegistered()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ public void AppleXHarnessWorkItemIsCreated()

_profileProvider
.Verify(x => x.AddProfilesToBundles(It.Is<ITaskItem[]>(bundles => bundles.Any(b => b.ItemSpec == "/apps/System.Foo.app"))), Times.Once);
_zipArchiveManager
.Verify(x => x.ArchiveDirectory("/apps/System.Foo.app", payloadArchive, true), Times.Once);
_zipArchiveManager
.Verify(x => x.AddResourceFileToArchive<CreateXHarnessAppleWorkItems>(payloadArchive, It.Is<string>(s => s.Contains("xharness-helix-job.apple.sh")), "xharness-helix-job.apple.sh"), Times.Once);
_zipArchiveManager
Expand Down Expand Up @@ -201,6 +203,52 @@ public void AppBundleIsReused()
command.Should().Contain("--target \"ios-simulator-64_13.5\"");
}

[Fact]
public void ZippedAppIsProvided()
{
var collection = CreateMockServiceCollection();
_task.ConfigureServices(collection);
_task.AppBundles = new[]
{
CreateAppBundle("/apps/System.Foo.zip", "ios-device_13.5", "00:15:42", "00:08:55", "00:02:33")
};
_task.TmpDir = "/tmp";
_fileSystem.Files.Add("/apps/System.Foo.zip", "zipped payload");
_fileSystem.Directories.Remove("/apps/System.Foo.zip");

// 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");
workItem.GetMetadata("Timeout").Should().Be("00:15:42");

var payloadArchive = workItem.GetMetadata("PayloadArchive");
payloadArchive.Should().NotBeNullOrEmpty();
_fileSystem.FileExists(payloadArchive).Should().BeTrue();

var command = workItem.GetMetadata("Command");
command.Should().Contain("System.Foo.app");
command.Should().Contain("--target \"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<ITaskItem[]>(bundles => bundles.Any(b => b.ItemSpec == "/apps/System.Foo.zip"))), Times.Once);
_zipArchiveManager
.Verify(x => x.ArchiveDirectory("/apps/System.Foo.app", payloadArchive, true), Times.Never);
_zipArchiveManager
.Verify(x => x.AddResourceFileToArchive<CreateXHarnessAppleWorkItems>(payloadArchive, It.Is<string>(s => s.Contains("xharness-helix-job.apple.sh")), "xharness-helix-job.apple.sh"), Times.Once);
_zipArchiveManager
.Verify(x => x.AddResourceFileToArchive<CreateXHarnessAppleWorkItems>(payloadArchive, It.Is<string>(s => s.Contains("xharness-runner.apple.sh")), "xharness-runner.apple.sh"), Times.Once);
_zipArchiveManager
.Verify(x => x.AddContentToArchive(payloadArchive, "command.sh", It.Is<string>(s => s.Contains("xharness apple test"))), Times.Once);
}

[Fact]
public void AreDependenciesRegistered()
{
Expand Down
51 changes: 37 additions & 14 deletions src/Microsoft.DotNet.Helix/Sdk/CreateXHarnessAndroidWorkItems.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,32 +88,45 @@ private async Task<ITaskItem> PrepareWorkItem(IZipArchiveManager zipArchiveManag
return null;
}

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

// Validation of any metadata specific to Android stuff goes here
if (!appPackage.GetRequiredMetadata(Log, MetadataNames.AndroidPackageName, out string androidPackageName))
if (!isAlreadyArchived && extension != ".apk")
{
Log.LogError($"{MetadataNames.AndroidPackageName} metadata must be specified; this may match, but can vary from file name");
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 command = GetHelixCommand(appPackage, apkPath, androidPackageName, testTimeout, expectedExitCode);
string apkName = Path.GetFileName(apkPath);
if (isAlreadyArchived) {
apkName = apkName.Replace(".zip", ".apk");
}

Log.LogMessage($"Creating work item with properties Identity: {workItemName}, Payload: {apkPath}, Command: {command}");
string command = GetHelixCommand(appPackage, apkName, androidPackageName, testTimeout, expectedExitCode);

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

return CreateTaskItem(workItemName, workItemZip, command, workItemTimeout);
}
Expand Down Expand Up @@ -143,7 +156,7 @@ private string GetDefaultCommand(ITaskItem appPackage, int expectedExitCode)
$"{ devOutArg } { instrumentationArg } { exitCodeArg } { extraArguments } { passthroughArgs }";
}

private string GetHelixCommand(ITaskItem appPackage, string apkPath, string androidPackageName, TimeSpan xHarnessTimeout, int expectedExitCode)
private string GetHelixCommand(ITaskItem appPackage, string apkName, string androidPackageName, TimeSpan xHarnessTimeout, int expectedExitCode)
{
appPackage.TryGetMetadata(MetadataNames.AndroidInstrumentationName, out string androidInstrumentationName);
appPackage.TryGetMetadata(MetadataNames.DeviceOutputPath, out string deviceOutputPath);
Expand All @@ -155,7 +168,7 @@ private string GetHelixCommand(ITaskItem appPackage, string apkPath, string andr
// We either call .ps1 or .sh so we need to format the arguments well (PS has -argument, bash has --argument)
string dash = IsPosixShell ? "--" : "-";
string xharnessRunCommand = $"{xharnessHelixWrapperScript} " +
$"{dash}app \"{Path.GetFileName(apkPath)}\" " +
$"{dash}app \"{apkName}\" " +
$"{dash}timeout \"{xHarnessTimeout}\" " +
$"{dash}package_name \"{androidPackageName}\" " +
(expectedExitCode != 0 ? $" {dash}expected_exit_code \"{expectedExitCode}\" " : string.Empty) +
Expand All @@ -171,6 +184,7 @@ private async Task<string> CreateZipArchiveOfPackageAsync(
IZipArchiveManager zipArchiveManager,
IFileSystem fileSystem,
string workItemName,
bool isAlreadyArchived,
string fileToZip,
string injectedCommands)
{
Expand All @@ -183,10 +197,19 @@ private async Task<string> CreateZipArchiveOfPackageAsync(
fileSystem.DeleteFile(outputZipPath);
}

zipArchiveManager.ArchiveFile(fileToZip, outputZipPath);
if (!isAlreadyArchived)
{
zipArchiveManager.ArchiveFile(fileToZip, outputZipPath);
}
else
{
Log.LogMessage($"App payload '{workItemName}` has already been zipped. Copying to '{outputZipPath}` instead");
fileSystem.FileCopy(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)
Log.LogMessage($"Adding the XHarness job scripts into the payload archive");
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);
Expand Down
10 changes: 7 additions & 3 deletions src/Microsoft.DotNet.Helix/Sdk/CreateXHarnessAppleWorkItems.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,17 @@ private async Task<ITaskItem> PrepareWorkItem(

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

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

if (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}");
Expand Down Expand Up @@ -176,8 +182,6 @@ private async Task<ITaskItem> PrepareWorkItem(
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);
}

Expand Down
19 changes: 18 additions & 1 deletion src/Microsoft.DotNet.Helix/Sdk/ProvisioningProfileProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,30 @@ public void AddProfilesToBundles(ITaskItem[] appBundles)

foreach (var appBundle in appBundles)
{
string appBundlePath;
if (appBundle.TryGetMetadata(CreateXHarnessAppleWorkItems.MetadataNames.AppBundlePath, out string pathMetadata)
&& !string.IsNullOrEmpty(pathMetadata))
{
appBundlePath = pathMetadata;
}
else
{
appBundlePath = appBundle.ItemSpec;
}

if (!appBundle.TryGetMetadata(CreateXHarnessAppleWorkItems.MetadataNames.Target, 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)");
continue;
}

if (appBundlePath.EndsWith(".zip"))
{
// TODO: We need to be able to add provisioning profiles into a zipped payload too
continue;
}

foreach (var pair in s_targetNames)
{
var platform = pair.Key;
Expand All @@ -80,7 +97,7 @@ public void AddProfilesToBundles(ITaskItem[] appBundles)
}

// App comes with a profile already
var provisioningProfileDestPath = _fileSystem.PathCombine(appBundle.ItemSpec, "embedded.mobileprovision");
var provisioningProfileDestPath = _fileSystem.PathCombine(appBundlePath, "embedded.mobileprovision");
if (_fileSystem.FileExists(provisioningProfileDestPath))
{
_log.LogMessage($"Bundle already contains a provisioning profile at `{provisioningProfileDestPath}`");
Expand Down
8 changes: 6 additions & 2 deletions src/Microsoft.DotNet.Helix/Sdk/XharnessTaskBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,17 @@ public class MetadataName
return (testTimeout, workItemTimeout, expectedExitCode, customCommands);
}

protected Build.Utilities.TaskItem CreateTaskItem(string workItemName, string payloadArchivePath, string command, TimeSpan timeout) =>
new (workItemName, new Dictionary<string, string>()
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}");

return new(workItemName, new Dictionary<string, string>()
{
{ "Identity", workItemName },
{ "PayloadArchive", payloadArchivePath },
{ "Command", command },
{ "Timeout", timeout.ToString() },
});
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,6 @@ if [ -z "$timeout" ]; then
die "No timeout was provided";
fi

if [ -z "$package_name" ]; then
die "Package name path wasn't provided";
fi

if [ -z "$output_directory" ]; then
die "No output directory provided";
fi
Expand Down