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
3 changes: 2 additions & 1 deletion docs/firebase-runtime-failure-backlog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This backlog tracks binding drifts that are concrete candidates for the runtime-

| Case id | Target/member | Audit finding reference | Expected runtime failure mechanism | Proof status | Fix PR | Merged commit |
| --- | --- | --- | --- | --- | --- | --- |
| `cloudfunctions-usefunctionsemulatororigin` | `Firebase.CloudFunctions.CloudFunctions.UseFunctionsEmulatorOrigin` | Manual runtime candidate from the current binding surface and native `FirebaseFunctions` Swift header | The binding still exports `useFunctionsEmulatorOrigin:` even though the current framework only exposes `useEmulatorWithHost:port:`. Calling the stale selector raises `ObjCRuntime.ObjCException` with an unrecognized-selector native exception. | Proved locally on the unfixed binding. Evidence: `ObjCRuntime.ObjCException`, `NSInvalidArgumentException`, selector `-[FIRFunctions useFunctionsEmulatorOrigin:]`, artifact `tests/E2E/Firebase.Foundation/artifacts/firebase-foundation-result-cloudfunctions-usefunctionsemulatororigin-failure.json`, log `tests/E2E/Firebase.Foundation/artifacts/firebase-foundation-sim-cloudfunctions-usefunctionsemulatororigin-failure.log`. | [#113](https://github.com/AdamEssenmacher/GoogleApisForiOSComponents/pull/113) | - |
| `abtesting-activateexperiment` | `Firebase.ABTesting.ExperimentController.ActivateExperiment` | `output/firebase-binding-audit/report.json` -> `ABTesting` -> `ActivateExperiment` (`signature-drift`) | The old binding exports `activateExperiment:forServiceOrigin:` as `NSObject` instead of `ABTExperimentPayload`, so callers can send an arbitrary object and native ABTesting immediately raises `ObjCRuntime.ObjCException` when it invokes `ABTExperimentPayload` selectors on that object. | Proved locally on the unfixed binding. Evidence artifact: `tests/E2E/Firebase.Foundation/artifacts/firebase-foundation-result-abtesting-activateexperiment-failure.json`. Regression artifact: `tests/E2E/Firebase.Foundation/artifacts/firebase-foundation-result-abtesting-activateexperiment-success.json`. | - | - |

## Investigating

Expand All @@ -16,4 +16,5 @@ No additional runtime-failure candidates are currently promoted.

| Case id | Target/member | Audit finding reference | Runtime failure mechanism | Proof status | Fix PR | Merged commit |
| --- | --- | --- | --- | --- | --- | --- |
| `cloudfunctions-usefunctionsemulatororigin` | `Firebase.CloudFunctions.CloudFunctions.UseFunctionsEmulatorOrigin` | Manual runtime candidate from the current binding surface and native `FirebaseFunctions` Swift header | The binding exported `useFunctionsEmulatorOrigin:` even though the current framework only exposes `useEmulatorWithHost:port:`. Calling the stale selector raised `ObjCRuntime.ObjCException` with an unrecognized-selector native exception. | Proved and fixed. Evidence artifact: `tests/E2E/Firebase.Foundation/artifacts/firebase-foundation-result-cloudfunctions-usefunctionsemulatororigin-failure.json`. Regression artifact: `tests/E2E/Firebase.Foundation/artifacts/firebase-foundation-result-cloudfunctions-usefunctionsemulatororigin-success.json`. | [#113](https://github.com/AdamEssenmacher/GoogleApisForiOSComponents/pull/113) | `b6a6c82c74e0cf3645edb9dc5519d4a628f7ed36` |
| `cloudfirestore-getquerynamed` | `Firebase.CloudFirestore.Firestore.GetQueryNamed` | `output/firebase-binding-audit/report.json` -> `CloudFirestore` -> `GetQueryNamed` (`signature-drift`) | The old binding exported `getQueryNamed:completion:` as `NSInputStream` instead of `NSString`, so native Firestore received `__NSCFInputStream` and threw an Objective-C exception when it treated the argument like a string. | Proved and fixed. Evidence artifact: `tests/E2E/Firebase.Foundation/artifacts/firebase-foundation-result-cloudfirestore-getquerynamed-failure.json`. Regression artifact: `tests/E2E/Firebase.Foundation/artifacts/firebase-foundation-result-cloudfirestore-getquerynamed-success.json`. | [#112](https://github.com/AdamEssenmacher/GoogleApisForiOSComponents/pull/112) | `3077a538a892de0db3350f0a459bf8620729f4db` |
7 changes: 6 additions & 1 deletion source/Firebase/ABTesting/ApiDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
using System;

namespace Firebase.ABTesting {
// @class ABTExperimentPayload;
[BaseType (typeof (NSObject), Name = "ABTExperimentPayload")]
interface ExperimentPayload {
}

// @interface FIRExperimentController : NSObject
[DisableDefaultCtor]
[BaseType (typeof (NSObject), Name = "FIRExperimentController")]
Expand All @@ -26,7 +31,7 @@ interface ExperimentController {

// -(void)activateExperiment:(ABTExperimentPayload * _Nonnull)experimentPayload forServiceOrigin:(NSString * _Nonnull)origin;
[Export ("activateExperiment:forServiceOrigin:")]
void ActivateExperiment (NSObject experimentPayload, string origin);
void ActivateExperiment (ExperimentPayload experimentPayload, string origin);
}

[Static]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
using System.Reflection;

#if ENABLE_RUNTIME_DRIFT_CASE_ABTESTING_ACTIVATEEXPERIMENT
using Firebase.ABTesting;
using Foundation;
using ObjCRuntime;
#endif

#if ENABLE_RUNTIME_DRIFT_CASE_CLOUDFIRESTORE_GETQUERYNAMED
using Firebase.CloudFirestore;
using Foundation;
Expand Down Expand Up @@ -60,6 +66,66 @@ public static async Task<string> ExecuteConfiguredCaseAsync()
?.Value;
}

#if ENABLE_RUNTIME_DRIFT_CASE_ABTESTING_ACTIVATEEXPERIMENT
static Task<string> VerifyABTestingActivateExperimentAsync()
{
const string selector = "activateExperiment:forServiceOrigin:";

var controller = ExperimentController.SharedInstance;
if (controller is null)
{
throw new InvalidOperationException("Firebase.ABTesting.ExperimentController.SharedInstance returned null after App.Configure().");
}

var payload = new ExperimentPayload();
var origin = "codex";
NSException? marshaledException = null;
MarshalObjectiveCExceptionMode? marshaledExceptionMode = null;

void OnMarshalObjectiveCException(object? sender, MarshalObjectiveCExceptionEventArgs args)
{
marshaledException ??= args.Exception;
marshaledExceptionMode ??= args.ExceptionMode;
}

Runtime.MarshalObjectiveCException += OnMarshalObjectiveCException;
try
{
try
{
controller.ActivateExperiment(payload, origin);
}
catch (ObjCException ex)
{
throw new InvalidOperationException(
$"Selector '{selector}' should not throw after the binding fix, but observed {ex.GetType().FullName}. " +
$"Managed payload type: {payload.GetType().FullName}. " +
$"Origin argument type: {origin.GetType().FullName}. " +
$"NSException.Name: {FormatDetail(marshaledException?.Name?.ToString())}. " +
$"NSException.Reason: {FormatDetail(marshaledException?.Reason)}. " +
$"Marshal mode: {FormatDetail(marshaledExceptionMode?.ToString())}.",
ex);
}

if (marshaledException is not null)
{
throw new InvalidOperationException(
$"Selector '{selector}' completed, but Runtime.MarshalObjectiveCException captured unexpected NSException.Name '{marshaledException.Name}'. " +
$"Reason: {FormatDetail(marshaledException.Reason)}. Marshal mode: {FormatDetail(marshaledExceptionMode?.ToString())}.");
}

return Task.FromResult(
$"Selector '{selector}' completed without ObjC exception after the binding fix. " +
$"Managed payload type: {payload.GetType().FullName}. " +
$"Origin argument type: {origin.GetType().FullName}.");
}
finally
{
Runtime.MarshalObjectiveCException -= OnMarshalObjectiveCException;
}
}
#endif

#if ENABLE_RUNTIME_DRIFT_CASE_CLOUDFIRESTORE_GETQUERYNAMED
static async Task<string> VerifyCloudFirestoreGetQueryNamedAsync()
{
Expand Down
11 changes: 11 additions & 0 deletions tests/E2E/Firebase.Foundation/runtime-drift-cases.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
{
"cases": [
{
"id": "abtesting-activateexperiment",
"method": "VerifyABTestingActivateExperimentAsync",
"bindingPackage": "AdamE.Firebase.iOS.ABTesting",
"packages": [
{
"id": "AdamE.Firebase.iOS.ABTesting",
"version": "12.6.0"
}
]
},
{
"id": "cloudfirestore-getquerynamed",
"method": "VerifyCloudFirestoreGetQueryNamedAsync",
Expand Down
Loading