Skip to content

Commit 531b1c8

Browse files
AdamEssenmacherAdam Essenmacher
andauthored
Add Google package delivery and runtime validation (#201)
* Add Google package delivery validation * Fix Google validation workflow package selection * Install iOS workload for Google validation * Use pinned workload set in validation * Pin generated consumers to repository workload set --------- Co-authored-by: Adam Essenmacher <adam@Adams-MacBook-Pro.local>
1 parent 676e204 commit 531b1c8

15 files changed

Lines changed: 1405 additions & 0 deletions
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
name: Google Foundation Validation
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- ".github/workflows/google-foundation-validation.yml"
7+
- ".config/dotnet-tools.json"
8+
- ".gitignore"
9+
- "Directory.Build.props"
10+
- "global.json"
11+
- "Xamarin.Google.sln"
12+
- "*.cake"
13+
- "icons/googleiosplaces_128x128.png"
14+
- "source/AssemblyInfo.cs"
15+
- "source/Google/Places/**"
16+
- "tests/E2E/Google.Foundation/**"
17+
- "tools/e2e/check-consumer-shapes.sh"
18+
- "tools/e2e/run-google-foundation.sh"
19+
push:
20+
branches:
21+
- main
22+
paths:
23+
- ".github/workflows/google-foundation-validation.yml"
24+
- ".config/dotnet-tools.json"
25+
- ".gitignore"
26+
- "Directory.Build.props"
27+
- "global.json"
28+
- "Xamarin.Google.sln"
29+
- "*.cake"
30+
- "icons/googleiosplaces_128x128.png"
31+
- "source/AssemblyInfo.cs"
32+
- "source/Google/Places/**"
33+
- "tests/E2E/Google.Foundation/**"
34+
- "tools/e2e/check-consumer-shapes.sh"
35+
- "tools/e2e/run-google-foundation.sh"
36+
workflow_dispatch:
37+
38+
permissions:
39+
contents: read
40+
41+
jobs:
42+
places:
43+
runs-on: macos-26
44+
timeout-minutes: 60
45+
steps:
46+
- name: Checkout
47+
uses: actions/checkout@v4
48+
49+
- name: Select Xcode 26.6
50+
run: |
51+
sudo xcode-select --switch /Applications/Xcode_26.6.app/Contents/Developer
52+
xcodebuild -version
53+
xcode-select -p
54+
55+
- name: Setup .NET SDK
56+
uses: actions/setup-dotnet@v4
57+
with:
58+
global-json-file: global.json
59+
60+
- name: Install .NET iOS workload
61+
run: |
62+
dotnet workload install ios
63+
dotnet workload list
64+
65+
- name: Install CocoaPods
66+
run: |
67+
if ! command -v pod >/dev/null 2>&1; then
68+
sudo gem install cocoapods
69+
fi
70+
pod --version
71+
72+
- name: Restore tools
73+
run: dotnet tool restore
74+
75+
- name: Pack Google.Places
76+
run: dotnet tool run dotnet-cake -- --target=nuget --names=Google.Places
77+
78+
- name: Direct and transitive consumer checks
79+
run: >-
80+
tools/e2e/check-consumer-shapes.sh
81+
--target Places
82+
--package-dir output
83+
84+
- name: Simulator E2E
85+
run: >-
86+
tools/e2e/run-google-foundation.sh
87+
--target Places
88+
--package-dir output
89+
--configuration Debug
90+
91+
- name: Upload diagnostics
92+
if: failure()
93+
uses: actions/upload-artifact@v4
94+
with:
95+
name: google-foundation-validation-diagnostics
96+
path: tests/E2E/Google.Foundation/artifacts/
97+
if-no-files-found: ignore

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ tools/*
88
!tools/e2e/
99
tools/e2e/*
1010
!tools/e2e/run-firebase-foundation.sh
11+
!tools/e2e/run-google-foundation.sh
12+
!tools/e2e/check-consumer-shapes.sh
1113
tmp-nugets
1214
*.userprefs
1315
*.DS_Store
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using Foundation;
2+
using UIKit;
3+
4+
namespace GoogleFoundationE2E;
5+
6+
[Register("AppDelegate")]
7+
public sealed class AppDelegate : UIApplicationDelegate
8+
{
9+
public override UIWindow? Window { get; set; }
10+
11+
public override bool FinishedLaunching(UIApplication application, NSDictionary? launchOptions)
12+
{
13+
#pragma warning disable CA1422
14+
Window = new UIWindow(UIScreen.MainScreen.Bounds);
15+
#pragma warning restore CA1422
16+
17+
var statusViewController = new StatusViewController();
18+
Window.RootViewController = statusViewController;
19+
Window.MakeKeyAndVisible();
20+
statusViewController.LoadViewIfNeeded();
21+
22+
_ = GoogleSelfTestRunner.RunAsync(statusViewController);
23+
24+
return true;
25+
}
26+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
namespace GoogleFoundationE2E;
2+
3+
public sealed class GoogleE2ERunResult
4+
{
5+
public string BundleId { get; set; } = string.Empty;
6+
public string Target { get; set; } = string.Empty;
7+
public string? PackageVersion { get; set; }
8+
public bool Success { get; set; }
9+
public DateTimeOffset StartedAtUtc { get; set; }
10+
public DateTimeOffset CompletedAtUtc { get; set; }
11+
public string? FatalError { get; set; }
12+
public List<GoogleE2ETestCaseResult> Cases { get; } = new();
13+
}
14+
15+
public sealed class GoogleE2ETestCaseResult
16+
{
17+
public string Name { get; set; } = string.Empty;
18+
public bool Success { get; set; }
19+
public long DurationMs { get; set; }
20+
public string? Message { get; set; }
21+
public string? ExceptionType { get; set; }
22+
public string? Detail { get; set; }
23+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace GoogleFoundationE2E;
2+
3+
public static class E2ELogger
4+
{
5+
public static void WriteLine(string message)
6+
{
7+
Console.WriteLine(message);
8+
}
9+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<!-- Matches the packages: the repo targets net10 only. See README for the Xcode pairing. -->
4+
<TargetFramework>net10.0-ios</TargetFramework>
5+
<RuntimeIdentifiers Condition="!Exists('$(ProjectAssetsFile)')">iossimulator-arm64</RuntimeIdentifiers>
6+
<RuntimeIdentifiers Condition="Exists('$(ProjectAssetsFile)')"></RuntimeIdentifiers>
7+
<OutputType>Exe</OutputType>
8+
<Nullable>enable</Nullable>
9+
<ImplicitUsings>true</ImplicitUsings>
10+
<SupportedOSPlatformVersion>15.0</SupportedOSPlatformVersion>
11+
<RootNamespace>GoogleFoundationE2E</RootNamespace>
12+
<AssemblyName>GoogleFoundationE2E</AssemblyName>
13+
<IPhoneResourcePrefix>Resources</IPhoneResourcePrefix>
14+
<Platforms>AnyCPU;iPhoneSimulator</Platforms>
15+
<Configurations>Debug;Release</Configurations>
16+
<IsPackable>false</IsPackable>
17+
</PropertyGroup>
18+
19+
<!--
20+
Which binding package this run exercises. Places is the only implemented target today; the
21+
remaining Google packages slot in by adding a target block below plus a case class.
22+
-->
23+
<PropertyGroup>
24+
<GoogleE2ETarget Condition="'$(GoogleE2ETarget)' == ''">Places</GoogleE2ETarget>
25+
<PlacesPackageVersion Condition="'$(PlacesPackageVersion)' == ''">7.4.0.2</PlacesPackageVersion>
26+
</PropertyGroup>
27+
28+
<PropertyGroup Condition="'$(TargetFramework)' == 'net10.0-ios' and '$(Platform)' == ''">
29+
<Platform>iPhoneSimulator</Platform>
30+
</PropertyGroup>
31+
32+
<PropertyGroup Condition="'$(TargetFramework)' == 'net10.0-ios' and '$(RuntimeIdentifier)' == ''">
33+
<RuntimeIdentifier>iossimulator-arm64</RuntimeIdentifier>
34+
</PropertyGroup>
35+
36+
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
37+
<MtouchLink>None</MtouchLink>
38+
</PropertyGroup>
39+
40+
<PropertyGroup Condition="'$(GoogleE2ETarget)' == 'Places'">
41+
<DefineConstants>$(DefineConstants);ENABLE_TARGET_PLACES</DefineConstants>
42+
</PropertyGroup>
43+
44+
<ItemGroup Condition="'$(GoogleE2ETarget)' == 'Places'">
45+
<PackageReference Include="AdamE.Google.iOS.Places" Version="$(PlacesPackageVersion)" />
46+
</ItemGroup>
47+
48+
<ItemGroup>
49+
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
50+
<_Parameter1>GoogleE2ETarget</_Parameter1>
51+
<_Parameter2>$(GoogleE2ETarget)</_Parameter2>
52+
</AssemblyAttribute>
53+
<!--
54+
Record which package version the run was asked to restore. The binding's AssemblyVersion does
55+
not track the NuGet version (the cake build only pokes FileVersion/PackageVersion), so reading
56+
the assembly version would report the same value for two different packages and make an A/B
57+
comparison look valid when it is not.
58+
-->
59+
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute" Condition="'$(GoogleE2ETarget)' == 'Places'">
60+
<_Parameter1>TargetPackageVersion</_Parameter1>
61+
<_Parameter2>$(PlacesPackageVersion)</_Parameter2>
62+
</AssemblyAttribute>
63+
</ItemGroup>
64+
</Project>
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
using System.Diagnostics;
2+
using System.Reflection;
3+
using System.Text.Json;
4+
using Foundation;
5+
6+
namespace GoogleFoundationE2E;
7+
8+
public static class GoogleSelfTestRunner
9+
{
10+
const string ResultFileName = "google-foundation-e2e-result.json";
11+
12+
public static async Task RunAsync(StatusViewController statusViewController)
13+
{
14+
var target = ReadAssemblyMetadata("GoogleE2ETarget") ?? "Places";
15+
16+
var result = new GoogleE2ERunResult
17+
{
18+
BundleId = NSBundle.MainBundle.BundleIdentifier ?? string.Empty,
19+
Target = target,
20+
PackageVersion = ReadAssemblyMetadata("TargetPackageVersion"),
21+
StartedAtUtc = DateTimeOffset.UtcNow,
22+
};
23+
24+
await statusViewController.AppendLineAsync("Target: " + target);
25+
await statusViewController.AppendLineAsync("Package version: " + (result.PackageVersion ?? "unknown"));
26+
27+
try
28+
{
29+
// The trimmer warns (IL2045) that it may strip AssemblyMetadataAttribute. It currently
30+
// does not, but if that ever changes the version would silently read as null and an A/B
31+
// comparison between two package versions would look valid while comparing one package
32+
// against itself. Refuse to run rather than produce a result that cannot be trusted.
33+
if (string.IsNullOrWhiteSpace(result.PackageVersion))
34+
{
35+
throw new InvalidOperationException(
36+
"TargetPackageVersion assembly metadata is missing, so this run cannot report " +
37+
"which package version it exercised. Results would not be trustworthy.");
38+
}
39+
40+
switch (target)
41+
{
42+
#if ENABLE_TARGET_PLACES
43+
case "Places":
44+
await PlacesCases.RunAsync(result, statusViewController, ExecuteCaseAsync);
45+
break;
46+
#endif
47+
default:
48+
throw new NotSupportedException(
49+
$"No E2E cases are compiled in for target '{target}'. " +
50+
"Build with -p:GoogleE2ETarget=<target>.");
51+
}
52+
}
53+
catch (Exception ex)
54+
{
55+
result.FatalError = ex.ToString();
56+
E2ELogger.WriteLine($"Unhandled E2E failure: {ex}");
57+
await statusViewController.AppendLineAsync("Unhandled failure: " + ex.Message);
58+
}
59+
60+
result.CompletedAtUtc = DateTimeOffset.UtcNow;
61+
result.Success = string.IsNullOrWhiteSpace(result.FatalError)
62+
&& result.Cases.Count > 0
63+
&& result.Cases.All(c => c.Success);
64+
65+
var indentedJson = JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true });
66+
var compactJson = JsonSerializer.Serialize(result);
67+
var resultFilePath = GetResultFilePath();
68+
Directory.CreateDirectory(Path.GetDirectoryName(resultFilePath)!);
69+
File.WriteAllText(resultFilePath, indentedJson);
70+
71+
await statusViewController.AppendLineAsync(result.Success ? "All Google E2E checks passed." : "Google E2E checks failed.");
72+
await statusViewController.AppendLineAsync("Result file: " + resultFilePath);
73+
74+
E2ELogger.WriteLine("E2E_RESULT:" + compactJson);
75+
E2ELogger.WriteLine("E2E_STATUS:" + (result.Success ? "PASS" : "FAIL"));
76+
}
77+
78+
public static async Task ExecuteCaseAsync(
79+
GoogleE2ERunResult result,
80+
StatusViewController statusViewController,
81+
string name,
82+
Func<Task<string>> testCase)
83+
{
84+
await statusViewController.AppendLineAsync(string.Empty);
85+
await statusViewController.AppendLineAsync("Running " + name + "...");
86+
87+
var caseResult = new GoogleE2ETestCaseResult
88+
{
89+
Name = name,
90+
};
91+
92+
var stopwatch = Stopwatch.StartNew();
93+
94+
try
95+
{
96+
caseResult.Detail = await testCase();
97+
caseResult.Success = true;
98+
caseResult.Message = "OK";
99+
await statusViewController.AppendLineAsync("PASS " + name + ": " + caseResult.Detail);
100+
}
101+
catch (Exception ex)
102+
{
103+
caseResult.Success = false;
104+
caseResult.Message = ex.Message;
105+
caseResult.ExceptionType = ex.GetType().FullName;
106+
caseResult.Detail = ex.ToString();
107+
E2ELogger.WriteLine($"FAIL {name}: {ex}");
108+
await statusViewController.AppendLineAsync("FAIL " + name + ": " + ex.Message);
109+
}
110+
finally
111+
{
112+
stopwatch.Stop();
113+
caseResult.DurationMs = stopwatch.ElapsedMilliseconds;
114+
result.Cases.Add(caseResult);
115+
}
116+
}
117+
118+
static string? ReadAssemblyMetadata(string key) =>
119+
typeof(GoogleSelfTestRunner).Assembly
120+
.GetCustomAttributes<AssemblyMetadataAttribute>()
121+
.FirstOrDefault(a => a.Key == key)?.Value;
122+
123+
static string GetResultFilePath()
124+
{
125+
var cacheDirectory = NSSearchPath.GetDirectories(NSSearchPathDirectory.CachesDirectory, NSSearchPathDomain.User).FirstOrDefault();
126+
if (string.IsNullOrWhiteSpace(cacheDirectory))
127+
{
128+
cacheDirectory = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
129+
}
130+
131+
return Path.Combine(cacheDirectory, ResultFileName);
132+
}
133+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>CFBundleDisplayName</key>
6+
<string>Google E2E</string>
7+
<key>CFBundleIdentifier</key>
8+
<string>com.googleapisforioscomponents.tests.googlefoundatione2e</string>
9+
<key>CFBundleShortVersionString</key>
10+
<string>1.0</string>
11+
<key>CFBundleVersion</key>
12+
<string>1</string>
13+
</dict>
14+
</plist>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
using UIKit;
2+
using GoogleFoundationE2E;
3+
4+
UIApplication.Main(args, null, typeof(AppDelegate));

0 commit comments

Comments
 (0)