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
97 changes: 97 additions & 0 deletions .github/workflows/google-foundation-validation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
name: Google Foundation Validation

on:
pull_request:
paths:
- ".github/workflows/google-foundation-validation.yml"
- ".config/dotnet-tools.json"
- ".gitignore"
- "Directory.Build.props"
- "global.json"
- "Xamarin.Google.sln"
- "*.cake"
- "icons/googleiosplaces_128x128.png"
- "source/AssemblyInfo.cs"
- "source/Google/Places/**"
- "tests/E2E/Google.Foundation/**"
- "tools/e2e/check-consumer-shapes.sh"
- "tools/e2e/run-google-foundation.sh"
push:
branches:
- main
paths:
- ".github/workflows/google-foundation-validation.yml"
- ".config/dotnet-tools.json"
- ".gitignore"
- "Directory.Build.props"
- "global.json"
- "Xamarin.Google.sln"
- "*.cake"
- "icons/googleiosplaces_128x128.png"
- "source/AssemblyInfo.cs"
- "source/Google/Places/**"
- "tests/E2E/Google.Foundation/**"
- "tools/e2e/check-consumer-shapes.sh"
- "tools/e2e/run-google-foundation.sh"
workflow_dispatch:

permissions:
contents: read

jobs:
places:
runs-on: macos-26
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Select Xcode 26.6
run: |
sudo xcode-select --switch /Applications/Xcode_26.6.app/Contents/Developer
xcodebuild -version
xcode-select -p

- name: Setup .NET SDK
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json

- name: Install .NET iOS workload
run: |
dotnet workload install ios
dotnet workload list

- name: Install CocoaPods
run: |
if ! command -v pod >/dev/null 2>&1; then
sudo gem install cocoapods
fi
pod --version

- name: Restore tools
run: dotnet tool restore

- name: Pack Google.Places
run: dotnet tool run dotnet-cake -- --target=nuget --names=Google.Places

- name: Direct and transitive consumer checks
run: >-
tools/e2e/check-consumer-shapes.sh
--target Places
--package-dir output

- name: Simulator E2E
run: >-
tools/e2e/run-google-foundation.sh
--target Places
--package-dir output
--configuration Debug

- name: Upload diagnostics
if: failure()
uses: actions/upload-artifact@v4
with:
name: google-foundation-validation-diagnostics
path: tests/E2E/Google.Foundation/artifacts/
if-no-files-found: ignore
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ tools/*
!tools/e2e/
tools/e2e/*
!tools/e2e/run-firebase-foundation.sh
!tools/e2e/run-google-foundation.sh
!tools/e2e/check-consumer-shapes.sh
tmp-nugets
*.userprefs
*.DS_Store
Expand Down
26 changes: 26 additions & 0 deletions tests/E2E/Google.Foundation/GoogleFoundationE2E/AppDelegate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Foundation;
using UIKit;

namespace GoogleFoundationE2E;

[Register("AppDelegate")]
public sealed class AppDelegate : UIApplicationDelegate
{
public override UIWindow? Window { get; set; }

public override bool FinishedLaunching(UIApplication application, NSDictionary? launchOptions)
{
#pragma warning disable CA1422
Window = new UIWindow(UIScreen.MainScreen.Bounds);
#pragma warning restore CA1422

var statusViewController = new StatusViewController();
Window.RootViewController = statusViewController;
Window.MakeKeyAndVisible();
statusViewController.LoadViewIfNeeded();

_ = GoogleSelfTestRunner.RunAsync(statusViewController);

return true;
}
}
23 changes: 23 additions & 0 deletions tests/E2E/Google.Foundation/GoogleFoundationE2E/E2EContracts.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace GoogleFoundationE2E;

public sealed class GoogleE2ERunResult
{
public string BundleId { get; set; } = string.Empty;
public string Target { get; set; } = string.Empty;
public string? PackageVersion { get; set; }
public bool Success { get; set; }
public DateTimeOffset StartedAtUtc { get; set; }
public DateTimeOffset CompletedAtUtc { get; set; }
public string? FatalError { get; set; }
public List<GoogleE2ETestCaseResult> Cases { get; } = new();
}

public sealed class GoogleE2ETestCaseResult
{
public string Name { get; set; } = string.Empty;
public bool Success { get; set; }
public long DurationMs { get; set; }
public string? Message { get; set; }
public string? ExceptionType { get; set; }
public string? Detail { get; set; }
}
9 changes: 9 additions & 0 deletions tests/E2E/Google.Foundation/GoogleFoundationE2E/E2ELogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace GoogleFoundationE2E;

public static class E2ELogger
{
public static void WriteLine(string message)
{
Console.WriteLine(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Matches the packages: the repo targets net10 only. See README for the Xcode pairing. -->
<TargetFramework>net10.0-ios</TargetFramework>
<RuntimeIdentifiers Condition="!Exists('$(ProjectAssetsFile)')">iossimulator-arm64</RuntimeIdentifiers>
<RuntimeIdentifiers Condition="Exists('$(ProjectAssetsFile)')"></RuntimeIdentifiers>
<OutputType>Exe</OutputType>
<Nullable>enable</Nullable>
<ImplicitUsings>true</ImplicitUsings>
<SupportedOSPlatformVersion>15.0</SupportedOSPlatformVersion>
<RootNamespace>GoogleFoundationE2E</RootNamespace>
<AssemblyName>GoogleFoundationE2E</AssemblyName>
<IPhoneResourcePrefix>Resources</IPhoneResourcePrefix>
<Platforms>AnyCPU;iPhoneSimulator</Platforms>
<Configurations>Debug;Release</Configurations>
<IsPackable>false</IsPackable>
</PropertyGroup>

<!--
Which binding package this run exercises. Places is the only implemented target today; the
remaining Google packages slot in by adding a target block below plus a case class.
-->
<PropertyGroup>
<GoogleE2ETarget Condition="'$(GoogleE2ETarget)' == ''">Places</GoogleE2ETarget>
<PlacesPackageVersion Condition="'$(PlacesPackageVersion)' == ''">7.4.0.2</PlacesPackageVersion>
</PropertyGroup>

<PropertyGroup Condition="'$(TargetFramework)' == 'net10.0-ios' and '$(Platform)' == ''">
<Platform>iPhoneSimulator</Platform>
</PropertyGroup>

<PropertyGroup Condition="'$(TargetFramework)' == 'net10.0-ios' and '$(RuntimeIdentifier)' == ''">
<RuntimeIdentifier>iossimulator-arm64</RuntimeIdentifier>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<MtouchLink>None</MtouchLink>
</PropertyGroup>

<PropertyGroup Condition="'$(GoogleE2ETarget)' == 'Places'">
<DefineConstants>$(DefineConstants);ENABLE_TARGET_PLACES</DefineConstants>
</PropertyGroup>

<ItemGroup Condition="'$(GoogleE2ETarget)' == 'Places'">
<PackageReference Include="AdamE.Google.iOS.Places" Version="$(PlacesPackageVersion)" />
</ItemGroup>

<ItemGroup>
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
<_Parameter1>GoogleE2ETarget</_Parameter1>
<_Parameter2>$(GoogleE2ETarget)</_Parameter2>
</AssemblyAttribute>
<!--
Record which package version the run was asked to restore. The binding's AssemblyVersion does
not track the NuGet version (the cake build only pokes FileVersion/PackageVersion), so reading
the assembly version would report the same value for two different packages and make an A/B
comparison look valid when it is not.
-->
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute" Condition="'$(GoogleE2ETarget)' == 'Places'">
<_Parameter1>TargetPackageVersion</_Parameter1>
<_Parameter2>$(PlacesPackageVersion)</_Parameter2>
</AssemblyAttribute>
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
using System.Diagnostics;
using System.Reflection;
using System.Text.Json;
using Foundation;

namespace GoogleFoundationE2E;

public static class GoogleSelfTestRunner
{
const string ResultFileName = "google-foundation-e2e-result.json";

public static async Task RunAsync(StatusViewController statusViewController)
{
var target = ReadAssemblyMetadata("GoogleE2ETarget") ?? "Places";

var result = new GoogleE2ERunResult
{
BundleId = NSBundle.MainBundle.BundleIdentifier ?? string.Empty,
Target = target,
PackageVersion = ReadAssemblyMetadata("TargetPackageVersion"),
StartedAtUtc = DateTimeOffset.UtcNow,
};

await statusViewController.AppendLineAsync("Target: " + target);
await statusViewController.AppendLineAsync("Package version: " + (result.PackageVersion ?? "unknown"));

try
{
// The trimmer warns (IL2045) that it may strip AssemblyMetadataAttribute. It currently
// does not, but if that ever changes the version would silently read as null and an A/B
// comparison between two package versions would look valid while comparing one package
// against itself. Refuse to run rather than produce a result that cannot be trusted.
if (string.IsNullOrWhiteSpace(result.PackageVersion))
{
throw new InvalidOperationException(
"TargetPackageVersion assembly metadata is missing, so this run cannot report " +
"which package version it exercised. Results would not be trustworthy.");
}

switch (target)
{
#if ENABLE_TARGET_PLACES
case "Places":
await PlacesCases.RunAsync(result, statusViewController, ExecuteCaseAsync);
break;
#endif
default:
throw new NotSupportedException(
$"No E2E cases are compiled in for target '{target}'. " +
"Build with -p:GoogleE2ETarget=<target>.");
}
}
catch (Exception ex)
{
result.FatalError = ex.ToString();
E2ELogger.WriteLine($"Unhandled E2E failure: {ex}");
await statusViewController.AppendLineAsync("Unhandled failure: " + ex.Message);
}

result.CompletedAtUtc = DateTimeOffset.UtcNow;
result.Success = string.IsNullOrWhiteSpace(result.FatalError)
&& result.Cases.Count > 0
&& result.Cases.All(c => c.Success);

var indentedJson = JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true });

Check warning on line 65 in tests/E2E/Google.Foundation/GoogleFoundationE2E/GoogleSelfTestRunner.cs

View workflow job for this annotation

GitHub Actions / places

Using member 'System.Text.Json.JsonSerializer.Serialize<TValue>(TValue, JsonSerializerOptions)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.

Check warning on line 65 in tests/E2E/Google.Foundation/GoogleFoundationE2E/GoogleSelfTestRunner.cs

View workflow job for this annotation

GitHub Actions / places

Using member 'System.Text.Json.JsonSerializer.Serialize<TValue>(TValue, JsonSerializerOptions)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.
var compactJson = JsonSerializer.Serialize(result);

Check warning on line 66 in tests/E2E/Google.Foundation/GoogleFoundationE2E/GoogleSelfTestRunner.cs

View workflow job for this annotation

GitHub Actions / places

Using member 'System.Text.Json.JsonSerializer.Serialize<TValue>(TValue, JsonSerializerOptions)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.

Check warning on line 66 in tests/E2E/Google.Foundation/GoogleFoundationE2E/GoogleSelfTestRunner.cs

View workflow job for this annotation

GitHub Actions / places

Using member 'System.Text.Json.JsonSerializer.Serialize<TValue>(TValue, JsonSerializerOptions)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.
var resultFilePath = GetResultFilePath();
Directory.CreateDirectory(Path.GetDirectoryName(resultFilePath)!);
File.WriteAllText(resultFilePath, indentedJson);

await statusViewController.AppendLineAsync(result.Success ? "All Google E2E checks passed." : "Google E2E checks failed.");
await statusViewController.AppendLineAsync("Result file: " + resultFilePath);

E2ELogger.WriteLine("E2E_RESULT:" + compactJson);
E2ELogger.WriteLine("E2E_STATUS:" + (result.Success ? "PASS" : "FAIL"));
}

public static async Task ExecuteCaseAsync(
GoogleE2ERunResult result,
StatusViewController statusViewController,
string name,
Func<Task<string>> testCase)
{
await statusViewController.AppendLineAsync(string.Empty);
await statusViewController.AppendLineAsync("Running " + name + "...");

var caseResult = new GoogleE2ETestCaseResult
{
Name = name,
};

var stopwatch = Stopwatch.StartNew();

try
{
caseResult.Detail = await testCase();
caseResult.Success = true;
caseResult.Message = "OK";
await statusViewController.AppendLineAsync("PASS " + name + ": " + caseResult.Detail);
}
catch (Exception ex)
{
caseResult.Success = false;
caseResult.Message = ex.Message;
caseResult.ExceptionType = ex.GetType().FullName;
caseResult.Detail = ex.ToString();
E2ELogger.WriteLine($"FAIL {name}: {ex}");
await statusViewController.AppendLineAsync("FAIL " + name + ": " + ex.Message);
}
finally
{
stopwatch.Stop();
caseResult.DurationMs = stopwatch.ElapsedMilliseconds;
result.Cases.Add(caseResult);
}
}

static string? ReadAssemblyMetadata(string key) =>
typeof(GoogleSelfTestRunner).Assembly
.GetCustomAttributes<AssemblyMetadataAttribute>()
.FirstOrDefault(a => a.Key == key)?.Value;

static string GetResultFilePath()
{
var cacheDirectory = NSSearchPath.GetDirectories(NSSearchPathDirectory.CachesDirectory, NSSearchPathDomain.User).FirstOrDefault();
if (string.IsNullOrWhiteSpace(cacheDirectory))
{
cacheDirectory = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
}

return Path.Combine(cacheDirectory, ResultFileName);
}
}
14 changes: 14 additions & 0 deletions tests/E2E/Google.Foundation/GoogleFoundationE2E/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDisplayName</key>
<string>Google E2E</string>
<key>CFBundleIdentifier</key>
<string>com.googleapisforioscomponents.tests.googlefoundatione2e</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>
4 changes: 4 additions & 0 deletions tests/E2E/Google.Foundation/GoogleFoundationE2E/Main.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
using UIKit;
using GoogleFoundationE2E;

UIApplication.Main(args, null, typeof(AppDelegate));
Loading
Loading