Skip to content

Commit f77b1c5

Browse files
Fix ABTesting ActivateExperiment runtime drift (#114)
1 parent b6a6c82 commit f77b1c5

4 files changed

Lines changed: 85 additions & 2 deletions

File tree

docs/firebase-runtime-failure-backlog.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ This backlog tracks binding drifts that are concrete candidates for the runtime-
66

77
| Case id | Target/member | Audit finding reference | Expected runtime failure mechanism | Proof status | Fix PR | Merged commit |
88
| --- | --- | --- | --- | --- | --- | --- |
9-
| `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) | - |
9+
| `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`. | - | - |
1010

1111
## Investigating
1212

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

1717
| Case id | Target/member | Audit finding reference | Runtime failure mechanism | Proof status | Fix PR | Merged commit |
1818
| --- | --- | --- | --- | --- | --- | --- |
19+
| `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` |
1920
| `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` |

source/Firebase/ABTesting/ApiDefinition.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
using System;
44

55
namespace Firebase.ABTesting {
6+
// @class ABTExperimentPayload;
7+
[BaseType (typeof (NSObject), Name = "ABTExperimentPayload")]
8+
interface ExperimentPayload {
9+
}
10+
611
// @interface FIRExperimentController : NSObject
712
[DisableDefaultCtor]
813
[BaseType (typeof (NSObject), Name = "FIRExperimentController")]
@@ -26,7 +31,7 @@ interface ExperimentController {
2631

2732
// -(void)activateExperiment:(ABTExperimentPayload * _Nonnull)experimentPayload forServiceOrigin:(NSString * _Nonnull)origin;
2833
[Export ("activateExperiment:forServiceOrigin:")]
29-
void ActivateExperiment (NSObject experimentPayload, string origin);
34+
void ActivateExperiment (ExperimentPayload experimentPayload, string origin);
3035
}
3136

3237
[Static]

tests/E2E/Firebase.Foundation/FirebaseFoundationE2E/FirebaseRuntimeDriftCases.cs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
using System.Reflection;
22

3+
#if ENABLE_RUNTIME_DRIFT_CASE_ABTESTING_ACTIVATEEXPERIMENT
4+
using Firebase.ABTesting;
5+
using Foundation;
6+
using ObjCRuntime;
7+
#endif
8+
39
#if ENABLE_RUNTIME_DRIFT_CASE_CLOUDFIRESTORE_GETQUERYNAMED
410
using Firebase.CloudFirestore;
511
using Foundation;
@@ -60,6 +66,66 @@ public static async Task<string> ExecuteConfiguredCaseAsync()
6066
?.Value;
6167
}
6268

69+
#if ENABLE_RUNTIME_DRIFT_CASE_ABTESTING_ACTIVATEEXPERIMENT
70+
static Task<string> VerifyABTestingActivateExperimentAsync()
71+
{
72+
const string selector = "activateExperiment:forServiceOrigin:";
73+
74+
var controller = ExperimentController.SharedInstance;
75+
if (controller is null)
76+
{
77+
throw new InvalidOperationException("Firebase.ABTesting.ExperimentController.SharedInstance returned null after App.Configure().");
78+
}
79+
80+
var payload = new ExperimentPayload();
81+
var origin = "codex";
82+
NSException? marshaledException = null;
83+
MarshalObjectiveCExceptionMode? marshaledExceptionMode = null;
84+
85+
void OnMarshalObjectiveCException(object? sender, MarshalObjectiveCExceptionEventArgs args)
86+
{
87+
marshaledException ??= args.Exception;
88+
marshaledExceptionMode ??= args.ExceptionMode;
89+
}
90+
91+
Runtime.MarshalObjectiveCException += OnMarshalObjectiveCException;
92+
try
93+
{
94+
try
95+
{
96+
controller.ActivateExperiment(payload, origin);
97+
}
98+
catch (ObjCException ex)
99+
{
100+
throw new InvalidOperationException(
101+
$"Selector '{selector}' should not throw after the binding fix, but observed {ex.GetType().FullName}. " +
102+
$"Managed payload type: {payload.GetType().FullName}. " +
103+
$"Origin argument type: {origin.GetType().FullName}. " +
104+
$"NSException.Name: {FormatDetail(marshaledException?.Name?.ToString())}. " +
105+
$"NSException.Reason: {FormatDetail(marshaledException?.Reason)}. " +
106+
$"Marshal mode: {FormatDetail(marshaledExceptionMode?.ToString())}.",
107+
ex);
108+
}
109+
110+
if (marshaledException is not null)
111+
{
112+
throw new InvalidOperationException(
113+
$"Selector '{selector}' completed, but Runtime.MarshalObjectiveCException captured unexpected NSException.Name '{marshaledException.Name}'. " +
114+
$"Reason: {FormatDetail(marshaledException.Reason)}. Marshal mode: {FormatDetail(marshaledExceptionMode?.ToString())}.");
115+
}
116+
117+
return Task.FromResult(
118+
$"Selector '{selector}' completed without ObjC exception after the binding fix. " +
119+
$"Managed payload type: {payload.GetType().FullName}. " +
120+
$"Origin argument type: {origin.GetType().FullName}.");
121+
}
122+
finally
123+
{
124+
Runtime.MarshalObjectiveCException -= OnMarshalObjectiveCException;
125+
}
126+
}
127+
#endif
128+
63129
#if ENABLE_RUNTIME_DRIFT_CASE_CLOUDFIRESTORE_GETQUERYNAMED
64130
static async Task<string> VerifyCloudFirestoreGetQueryNamedAsync()
65131
{

tests/E2E/Firebase.Foundation/runtime-drift-cases.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
{
22
"cases": [
3+
{
4+
"id": "abtesting-activateexperiment",
5+
"method": "VerifyABTestingActivateExperimentAsync",
6+
"bindingPackage": "AdamE.Firebase.iOS.ABTesting",
7+
"packages": [
8+
{
9+
"id": "AdamE.Firebase.iOS.ABTesting",
10+
"version": "12.6.0"
11+
}
12+
]
13+
},
314
{
415
"id": "cloudfirestore-getquerynamed",
516
"method": "VerifyCloudFirestoreGetQueryNamedAsync",

0 commit comments

Comments
 (0)