Skip to content

Commit b6a6c82

Browse files
[codex] Fix Cloud Functions emulator runtime drift (#113)
* Fix Cloud Functions emulator runtime drift * Link Cloud Functions runtime drift PR * Persist Cloud Functions emulator origin on native instance * Keep Cloud Functions emulator fix as a thin binding * Revert incidental Cloud Functions version cleanup * Drop Cloud Functions whitespace-only churn
1 parent 3077a53 commit b6a6c82

5 files changed

Lines changed: 111 additions & 12 deletions

File tree

docs/firebase-runtime-failure-backlog.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ 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-
| `cloudfirestore-getquerynamed` | `Firebase.CloudFirestore.Firestore.GetQueryNamed` | `output/firebase-binding-audit/report.json` -> `CloudFirestore` -> `GetQueryNamed` (`signature-drift`) | The binding currently exports `getQueryNamed:completion:` as `NSInputStream` instead of `NSString`, so native Firestore receives `__NSCFInputStream` and throws an Objective-C exception when it treats the argument like a string. | Proved locally on the unfixed binding. Evidence: `ObjCRuntime.ObjCException`, `NSInvalidArgumentException`, selector `-[__NSCFInputStream length]`, artifact `tests/E2E/Firebase.Foundation/artifacts/firebase-foundation-result-cloudfirestore-getquerynamed-failure.json`, log `tests/E2E/Firebase.Foundation/artifacts/firebase-foundation-sim-cloudfirestore-getquerynamed-failure.log`. | Pending | - |
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) | - |
1010

1111
## Investigating
1212

1313
No additional runtime-failure candidates are currently promoted.
1414

1515
## Done
1616

17-
None yet.
17+
| Case id | Target/member | Audit finding reference | Runtime failure mechanism | Proof status | Fix PR | Merged commit |
18+
| --- | --- | --- | --- | --- | --- | --- |
19+
| `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/CloudFunctions/ApiDefinition.cs

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,6 @@ interface CloudFunctions
4747
[Export("HTTPSCallableWithName:")]
4848
HttpsCallable HttpsCallable(string name);
4949

50-
// @property(nonatomic, readonly, nullable) NSString *emulatorOrigin;
51-
[NullAllowed]
52-
[Export("emulatorOrigin")]
53-
string EmulatorOrigin { get; }
54-
55-
//- (void)useFunctionsEmulatorOrigin:(NSString *)origin
56-
[Export("useFunctionsEmulatorOrigin:")]
57-
void UseFunctionsEmulatorOrigin(string origin);
58-
5950
//- (void)useEmulatorWithHost:(NSString *)host port:(NSInteger) port;
6051
[Export ("useEmulatorWithHost:port:")]
6152
void UseEmulatorOriginWithHost (string host, uint port);

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

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@
66
using ObjCRuntime;
77
#endif
88

9+
#if ENABLE_RUNTIME_DRIFT_CASE_CLOUDFUNCTIONS_USEFUNCTIONSEMULATORORIGIN
10+
using Firebase.CloudFunctions;
11+
using Foundation;
12+
using ObjCRuntime;
13+
#endif
14+
915
namespace FirebaseFoundationE2E;
1016

1117
static class FirebaseRuntimeDriftCases
@@ -136,6 +142,95 @@ void OnMarshalObjectiveCException(object? sender, MarshalObjectiveCExceptionEven
136142
}
137143
#endif
138144

145+
#if ENABLE_RUNTIME_DRIFT_CASE_CLOUDFUNCTIONS_USEFUNCTIONSEMULATORORIGIN
146+
static Task<string> VerifyCloudFunctionsUseFunctionsEmulatorOriginAsync()
147+
{
148+
const string staleSelector = "useFunctionsEmulatorOrigin:";
149+
const string liveSelector = "useEmulatorWithHost:port:";
150+
151+
var functions = CloudFunctions.DefaultInstance;
152+
if (functions is null)
153+
{
154+
throw new InvalidOperationException("Firebase.CloudFunctions.CloudFunctions.DefaultInstance returned null after App.Configure().");
155+
}
156+
157+
var staleMethod = typeof(CloudFunctions).GetMethod(
158+
"UseFunctionsEmulatorOrigin",
159+
BindingFlags.Instance | BindingFlags.Public,
160+
binder: null,
161+
types: new[] { typeof(string) },
162+
modifiers: null);
163+
if (staleMethod is not null)
164+
{
165+
throw new InvalidOperationException(
166+
$"CloudFunctions still exposes stale managed API '{staleMethod.Name}', so callers can still reach selector '{staleSelector}'.");
167+
}
168+
169+
var staleProperty = typeof(CloudFunctions).GetProperty("EmulatorOrigin", BindingFlags.Instance | BindingFlags.Public);
170+
if (staleProperty is not null)
171+
{
172+
throw new InvalidOperationException(
173+
$"CloudFunctions still exposes stale managed property '{staleProperty.Name}', which no longer maps to a real ObjC surface.");
174+
}
175+
176+
if (functions.RespondsToSelector(new Selector(staleSelector)))
177+
{
178+
throw new InvalidOperationException(
179+
$"Native FIRFunctions still responds to stale selector '{staleSelector}', so the runtime drift would still be reachable.");
180+
}
181+
182+
if (!functions.RespondsToSelector(new Selector(liveSelector)))
183+
{
184+
throw new InvalidOperationException(
185+
$"Native FIRFunctions does not respond to expected live selector '{liveSelector}'.");
186+
}
187+
188+
NSException? marshaledException = null;
189+
MarshalObjectiveCExceptionMode? marshaledExceptionMode = null;
190+
191+
void OnMarshalObjectiveCException(object? sender, MarshalObjectiveCExceptionEventArgs args)
192+
{
193+
marshaledException ??= args.Exception;
194+
marshaledExceptionMode ??= args.ExceptionMode;
195+
}
196+
197+
Runtime.MarshalObjectiveCException += OnMarshalObjectiveCException;
198+
try
199+
{
200+
try
201+
{
202+
functions.UseEmulatorOriginWithHost("127.0.0.1", 5002);
203+
}
204+
catch (ObjCException ex)
205+
{
206+
throw new InvalidOperationException(
207+
$"Selector '{liveSelector}' should not throw after the binding fix, but observed {ex.GetType().FullName}. " +
208+
$"Runtime host argument type: {typeof(string).FullName}. Runtime port argument type: {typeof(uint).FullName}. " +
209+
$"NSException.Name: {FormatDetail(marshaledException?.Name?.ToString())}. " +
210+
$"NSException.Reason: {FormatDetail(marshaledException?.Reason)}. " +
211+
$"Marshal mode: {FormatDetail(marshaledExceptionMode?.ToString())}.",
212+
ex);
213+
}
214+
215+
if (marshaledException is not null)
216+
{
217+
throw new InvalidOperationException(
218+
$"Cloud Functions emulator API completed, but Runtime.MarshalObjectiveCException captured unexpected NSException.Name '{marshaledException.Name}'. " +
219+
$"Reason: {FormatDetail(marshaledException.Reason)}. Marshal mode: {FormatDetail(marshaledExceptionMode?.ToString())}.");
220+
}
221+
222+
return Task.FromResult(
223+
$"Removed stale managed selector '{staleSelector}' and property 'EmulatorOrigin'. " +
224+
$"Live selector '{liveSelector}' completed without ObjC exception after the binding fix. " +
225+
$"Runtime host argument type: {typeof(string).FullName}. Runtime port argument type: {typeof(uint).FullName}.");
226+
}
227+
finally
228+
{
229+
Runtime.MarshalObjectiveCException -= OnMarshalObjectiveCException;
230+
}
231+
}
232+
#endif
233+
139234
static string FormatDetail(string? value)
140235
{
141236
return string.IsNullOrWhiteSpace(value) ? "<empty>" : value;

tests/E2E/Firebase.Foundation/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ The runner writes logs and JSON results under:
6666

6767
## Optional nullability validation mode
6868

69-
This project also supports an opt-in nullability validation lane that exercises the shipped consumer APIs for the current Firebase nullability fixes and verifies those APIs still work with the broadened contracts. The lane is binding-focused: configuration-dependent Firebase errors are acceptable as long as the managed API crosses into native correctly and does not fail with a binding-layer exception. It currently covers the fixes that are safely exercisable in the E2E app; Cloud Functions is intentionally out of scope here because `emulatorOrigin` and `useFunctionsEmulatorOrigin:` are a separate selector-binding issue.
69+
This project also supports an opt-in nullability validation lane that exercises the shipped consumer APIs for the current Firebase nullability fixes and verifies those APIs still work with the broadened contracts. The lane is binding-focused: configuration-dependent Firebase errors are acceptable as long as the managed API crosses into native correctly and does not fail with a binding-layer exception. It currently covers the fixes that are safely exercisable in the E2E app; Cloud Functions remains out of scope for the nullability lane, and selector-oriented runtime cases are handled through the targeted runtime drift mode instead.
7070

7171
1. Pack the Firebase slices used by the validation lane and their dependencies:
7272

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,17 @@
1010
"version": "12.6.0"
1111
}
1212
]
13+
},
14+
{
15+
"id": "cloudfunctions-usefunctionsemulatororigin",
16+
"method": "VerifyCloudFunctionsUseFunctionsEmulatorOriginAsync",
17+
"bindingPackage": "AdamE.Firebase.iOS.CloudFunctions",
18+
"packages": [
19+
{
20+
"id": "AdamE.Firebase.iOS.CloudFunctions",
21+
"version": "12.6.0"
22+
}
23+
]
1324
}
1425
]
1526
}

0 commit comments

Comments
 (0)