Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[release/9.0] Support downloading and installing simulator images with Xcode 16 #1279

Merged
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -360,3 +360,5 @@ MigrationBackup/

# some ide stuff
.idea

.vscode/
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<PackageVersion Include="Mono.Options" Version="6.12.0.148" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="Selenium.WebDriver" Version="4.0.0-alpha05" />
<PackageVersion Include="Microsoft.Tools.Mlaunch" Version="1.0.96" />
<PackageVersion Include="Microsoft.Tools.Mlaunch" Version="1.0.256" />
<PackageVersion Include="NUnit" Version="3.13.0" />
<PackageVersion Include="NUnit.Engine" Version="3.13.0" />
<PackageVersion Include="xunit.extensibility.execution" Version="$(XUnitVersion)" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,30 @@ protected override async Task<ExitCode> InvokeInternal(ILogger logger)

private async Task<bool> Install(Simulator simulator)
{
if (simulator.Source is null)
{
var xcodeVersion = await GetXcodeVersion();
if (!simulator.IsCryptexDiskImage || Version.Parse(xcodeVersion).Major < 16)
{
throw new Exception($"Cannot download simulator: {simulator.Name} from source nor through Xcode: {xcodeVersion}");
}
else
{
Logger.LogInformation($"Downloading and installing simulator: {simulator.Name} through xcodebuild with Xcode: {xcodeVersion}");
var (succeeded, stdout) = await ExecuteCommand("xcodebuild", TimeSpan.FromMinutes(15), "-downloadPlatform", simulator.Platform, "-verbose");
if (!succeeded)
{
Logger.LogError($"Download and installation failed through xcodebuild for simulator: {simulator.Name} with Xcode: {xcodeVersion}!" + Environment.NewLine + stdout);
return false;
}
else
{
Logger.LogDebug(stdout);
return true;
}
}
}

var filename = Path.GetFileName(simulator.Source);
var downloadPath = Path.Combine(TempDirectory, filename);
var download = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,25 @@ namespace Microsoft.DotNet.XHarness.CLI.Commands.Apple.Simulators;
internal class Simulator
{
public string Name { get; }
public string Platform { get; }
public string Identifier { get; }
public string Version { get; }
public string Source { get; }
public string? Source { get; }
public string InstallPrefix { get; }
public long FileSize { get; }
public bool IsDmgFormat { get; }
public bool IsCryptexDiskImage { get; }

public Simulator(string name, string identifier, string version, string source, string installPrefix, long fileSize)
public Simulator(string name, string platform, string identifier, string version, string? source, string installPrefix, long fileSize, bool isCryptexDiskImage)
{
Name = name;
Platform = platform;
Identifier = identifier;
Version = version;
Source = source;
InstallPrefix = installPrefix;
FileSize = fileSize;
IsDmgFormat = Identifier.StartsWith("com.apple.dmg.", StringComparison.OrdinalIgnoreCase);
IsCryptexDiskImage = isCryptexDiskImage;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,6 @@ protected async Task<IEnumerable<Simulator>> GetAvailableSimulators()
var versionNode = downloadable.SelectSingleNode("key[text()='version']/following-sibling::string") ?? throw new Exception("Version node not found");
var identifierNode = downloadable.SelectSingleNode("key[text()='identifier']/following-sibling::string") ?? throw new Exception("Identifier node not found");
var sourceNode = downloadable.SelectSingleNode("key[text()='source']/following-sibling::string");
if (sourceNode is null)
{
// It seems that Apple can list beta simulators in the index file, but they do not provide a source for downloading them (eg: iOS 18.0 beta Simulator Runtime).
// In such cases log a warning and skip trying to download such simulator as they are not publicly available.
Logger.LogWarning($"Simulator with name: '{nameNode.InnerText}' version: '{versionNode.InnerText}' identifier: '{identifierNode.InnerText}' has no source for download, skipping...");
continue;
}

var fileSizeNode = downloadable.SelectSingleNode("key[text()='fileSize']/following-sibling::integer|key[text()='fileSize']/following-sibling::real");
var installPrefixNode = downloadable.SelectSingleNode("key[text()='userInfo']/following-sibling::dict/key[text()='InstallPrefix']/following-sibling::string");
Expand Down Expand Up @@ -144,13 +137,51 @@ protected async Task<IEnumerable<Simulator>> GetAvailableSimulators()
installPrefix = $"/Library/Developer/CoreSimulator/Profiles/Runtimes/{simRuntimeName}";
}

var platform = name.Split(' ').FirstOrDefault();
if (platform is null)
{
Logger.LogWarning($"Platform name could not be parsed from simulator name: '{nameNode.InnerText}' version: '{versionNode.InnerText}' identifier: '{identifierNode.InnerText}' skipping...");
continue;
}

var source = ReplaceStringUsingKey(sourceNode?.InnerText, dict);
var isCryptexDiskImage = false;
if (source is null)
{
// We allow source to be missing for newer simulators (e.g., iOS 18+ available from Xcode 16) that use cryptographically-sealed archives.
// Eg.:
// <dict>
// <key>category</key>
// <string>simulator</string>
// <key>contentType</key>
// <string>cryptexDiskImage</string>
// ...
// These images are downloaded and installed through xcodebuild instead.
// https://developer.apple.com/documentation/xcode/installing-additional-simulator-runtimes#Install-and-manage-Simulator-runtimes-from-the-command-line
var contentTypeNode = downloadable.SelectSingleNode("key[text()='contentType']/following-sibling::string") ?? throw new Exception("ContentType node not found");
var contentType = contentTypeNode.InnerText;
if (contentType.Equals("cryptexDiskImage", StringComparison.OrdinalIgnoreCase))
{
isCryptexDiskImage = true;
Logger.LogInformation($"Simulator with name: '{nameNode.InnerText}' version: '{versionNode.InnerText}' identifier: '{identifierNode.InnerText}' has no source but it is a cryptex disk image which can be downloaded through xcodebuild.");
}
else
{
Logger.LogWarning($"Simulator with name: '{nameNode.InnerText}' version: '{versionNode.InnerText}' identifier: '{identifierNode.InnerText}' has no source for download nor it is a cryptex disk image, skipping...");
continue;
}
}

simulators.Add(new Simulator(
name: name,
platform: platform,
identifier: ReplaceStringUsingKey(identifierNode.InnerText, dict),
version: versionNode.InnerText,
source: ReplaceStringUsingKey(sourceNode.InnerText, dict),
source: source,
installPrefix: installPrefix,
fileSize: (long)parsedFileSize));
fileSize: (long)parsedFileSize,
isCryptexDiskImage: isCryptexDiskImage
));
}

return simulators;
Expand Down Expand Up @@ -182,6 +213,8 @@ protected async Task<IEnumerable<Simulator>> GetAvailableSimulators()
{
return null;
}
Logger.LogDebug($"Listing runtime disk images via returned: {json}");

string simulatorRuntime = "";
string simulatorVersion = "";

Expand Down Expand Up @@ -385,7 +418,7 @@ private async Task<bool> DownloadFile(string url, string destinationPath)
return false;
}

private async Task<string> GetXcodeVersion()
protected async Task<string> GetXcodeVersion()
{
if (_xcodeVersion is not null)
{
Expand Down
8 changes: 7 additions & 1 deletion tests/integration-tests/Apple/Simulator.Commands.Tests.proj
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
<HelixTargetQueue Include="osx.13.amd64.open"/>
</ItemGroup>

<PropertyGroup>
<iOSSimulatorVersionUnderTest>16.4</iOSSimulatorVersionUnderTest>
</PropertyGroup>

<PropertyGroup>
<TestAppBundleName>System.Numerics.Vectors.Tests</TestAppBundleName>
<XHarnessTestAppBundleUrl>$(AssetsBaseUri)/ios/test-app/ios-simulator-64/$(TestAppBundleName).app.zip</XHarnessTestAppBundleUrl>
Expand All @@ -21,13 +25,15 @@

<ItemGroup>
<XHarnessAppBundleToTest Include="$(TestAppDestinationDir)\$(TestAppBundleName).app">
<TestTarget>ios-simulator-64</TestTarget>
<TestTarget>ios-simulator-64_$(iOSSimulatorVersionUnderTest)</TestTarget>
<WorkItemTimeout>00:20:00</WorkItemTimeout>
<TestTimeout>00:07:00</TestTimeout>
<LaunchTimeout>00:03:30</LaunchTimeout>
<CustomCommands>
<![CDATA[
set -ex
xharness apple simulators install $target --verbosity=Debug
xharness apple simulators reset-simulator --output-directory="$output_directory" --target=$target --verbosity=Debug
deviceId=`xharness apple device $target`
xharness apple install -t=$target --device="$deviceId" -o="$output_directory" --app="$app" --timeout=$launch_timeout -v
set +e
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<Project Sdk="Microsoft.DotNet.Helix.Sdk">

<ItemGroup>
<!-- Adjust to the desired souting queue. -->
<HelixTargetQueue Include="osx.amd64.iphone.scouting.open"/>
</ItemGroup>

<PropertyGroup>
<!--
Change to specify the exact Xcode version for testing.
In the CustomCommands below we can probe installed Xcode versions on Helix with `ls -al /Applications` and then choosing the write path.
-->
<XcodeVersionUnderTest>Xcode_16_beta_6</XcodeVersionUnderTest>
<iOSSimulatorVersionUnderTest>18.0</iOSSimulatorVersionUnderTest>
</PropertyGroup>

<PropertyGroup>
<TestAppBundleName>System.Numerics.Vectors.Tests</TestAppBundleName>
<XHarnessTestAppBundleUrl>$(AssetsBaseUri)/ios/test-app/ios-simulator-64/$(TestAppBundleName).app.zip</XHarnessTestAppBundleUrl>
<TestAppDestinationDir>$(ArtifactsTmpDir)test-app\ios-simulator-64</TestAppDestinationDir>
</PropertyGroup>

<Target Name="TestApple" BeforeTargets="CoreTest">
<DownloadFile SourceUrl="$(XHarnessTestAppBundleUrl)" DestinationFolder="$(TestAppDestinationDir)" SkipUnchangedFiles="True" Retries="5">
<Output TaskParameter="DownloadedFile" ItemName="ZippedAppBundle" />
</DownloadFile>

<Message Text="Downloaded $(TestAppBundleName) from @(ZippedAppBundle). Extracting..." Importance="High" />
<Exec Command="tar -xzf @(ZippedAppBundle) -C $(TestAppDestinationDir)" />
<Message Text="Extracted to $(TestAppDestinationDir)" Importance="High" />

<ItemGroup>
<XHarnessAppBundleToTest Include="$(TestAppDestinationDir)\$(TestAppBundleName).app">
<TestTarget>ios-simulator-64_$(iOSSimulatorVersionUnderTest)</TestTarget>
<WorkItemTimeout>00:20:00</WorkItemTimeout>
<TestTimeout>00:07:00</TestTimeout>
<LaunchTimeout>00:03:30</LaunchTimeout>
<CustomCommands>
<![CDATA[
set -ex
xharness apple simulators install $target --verbosity=Debug --xcode /Applications/$(XcodeVersionUnderTest).app
xharness apple simulators reset-simulator --output-directory="$output_directory" --target=$target --verbosity=Debug --xcode /Applications/$(XcodeVersionUnderTest).app
deviceId=`xharness apple device $target`
xharness apple install -t=$target --device="$deviceId" -o="$output_directory" --app="$app" --timeout=$launch_timeout -v --xcode /Applications/$(XcodeVersionUnderTest).app
set +e
result=0
xharness apple just-test -t=$target --device="$deviceId" -o="$output_directory" --app="net.dot.$(TestAppBundleName)" --launch-timeout=$launch_timeout --timeout=$timeout -v --xcode /Applications/$(XcodeVersionUnderTest).app
((result|=$?))
xharness apple uninstall -t=$target --device="$deviceId" -o="$output_directory" --app="net.dot.$(TestAppBundleName)" -v --xcode /Applications/$(XcodeVersionUnderTest).app
((result|=$?))
exit $result
]]>
</CustomCommands>
</XHarnessAppBundleToTest>
</ItemGroup>
</Target>

</Project>
20 changes: 20 additions & 0 deletions tests/integration-tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Integration tests

This folder includes integration tests projects for different support platforms (iOS, Android, WASM).
They are used in end-to-end testing scenarios and are referenced from `azure-pipelines-public.yml` E2E templates.

In the relevant `*.proj` files one can configure various setting for execution on Helix like:
- configuring the Helix queue (e.g., `osx.13.amd64.iphone.open` via `HelixTargetQueue` item group)
- app bundle to download, send to Helix and test (e.g., `System.Buffers.Tests.app`)
- etc.

## Testing on scouting queue

NOTE: This is Apple-specific but can be applied to other platforms as well

There are two test projects which can be used on scouting queues which are not used by default:

- Apple/Simulator.Scouting.Tests.proj
- Apple/Simulator.Scouting.Commands.Tests.proj

When desired, these can be included in the `azure-pipelines-public.yml` so that the CI runs them on a desired scouting queue (check the `HelixTargetQueue` setting) with a particular version of Xcode.
Loading