Skip to content

Commit 090809c

Browse files
Fix ABTesting UpdateExperiments overflow policy binding (#116)
1 parent 6973085 commit 090809c

5 files changed

Lines changed: 156 additions & 4 deletions

File tree

source/Firebase/ABTesting/ApiDefinition.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ interface ExperimentController {
1919

2020
// -(void)updateExperimentsWithServiceOrigin:(NSString * _Nonnull)origin events:(FIRLifecycleEvents * _Nonnull)events policy:(ABTExperimentPayloadExperimentOverflowPolicy)policy lastStartTime:(NSTimeInterval)lastStartTime payloads:(NSArray<NSData *> * _Nonnull)payloads completionHandler:(void (^ _Nullable)(NSError * _Nullable))completionHandler;
2121
[Export ("updateExperimentsWithServiceOrigin:events:policy:lastStartTime:payloads:completionHandler:")]
22-
void UpdateExperiments (string origin, LifecycleEvents events, NSObject policy, double lastStartTime, NSData [] payloads, [NullAllowed] Action<NSError> completionHandler);
22+
void UpdateExperiments (string origin, LifecycleEvents events, ExperimentPayloadExperimentOverflowPolicy policy, double lastStartTime, NSData [] payloads, [NullAllowed] Action<NSError> completionHandler);
2323

2424
// -(NSTimeInterval)latestExperimentStartTimestampBetweenTimestamp:(NSTimeInterval)timestamp andPayloads:(NSArray<NSData *> * _Nonnull)payloads;
2525
[Export ("latestExperimentStartTimestampBetweenTimestamp:andPayloads:")]

source/Firebase/ABTesting/Enums.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
using System;
2-
31
namespace Firebase.ABTesting {
2+
public enum ExperimentPayloadExperimentOverflowPolicy : int {
3+
UnrecognizedValue = 999,
4+
Unspecified = 0,
5+
DiscardOldest = 1,
6+
IgnoreNewest = 2
7+
}
48
}

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

Lines changed: 123 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_UPDATEEXPERIMENTS
4+
using Firebase.ABTesting;
5+
using Foundation;
6+
using ObjCRuntime;
7+
#endif
8+
39
#if ENABLE_RUNTIME_DRIFT_CASE_ABTESTING_ACTIVATEEXPERIMENT
410
using Firebase.ABTesting;
511
using Foundation;
@@ -72,6 +78,123 @@ public static async Task<string> ExecuteConfiguredCaseAsync()
7278
?.Value;
7379
}
7480

81+
#if ENABLE_RUNTIME_DRIFT_CASE_ABTESTING_UPDATEEXPERIMENTS
82+
static async Task<string> VerifyABTestingUpdateExperimentsAsync()
83+
{
84+
const string selector = "updateExperimentsWithServiceOrigin:events:policy:lastStartTime:payloads:completionHandler:";
85+
86+
var signature = typeof(ExperimentController).GetMethod(
87+
nameof(ExperimentController.UpdateExperiments),
88+
BindingFlags.Instance | BindingFlags.Public,
89+
binder: null,
90+
types: new[]
91+
{
92+
typeof(string),
93+
typeof(LifecycleEvents),
94+
typeof(ExperimentPayloadExperimentOverflowPolicy),
95+
typeof(double),
96+
typeof(NSData[]),
97+
typeof(Action<NSError>)
98+
},
99+
modifiers: null);
100+
if (signature is null)
101+
{
102+
throw new InvalidOperationException(
103+
$"Expected managed API '{nameof(ExperimentController.UpdateExperiments)}(string, {typeof(LifecycleEvents).FullName}, {typeof(ExperimentPayloadExperimentOverflowPolicy).FullName}, double, {typeof(NSData[]).FullName}, {typeof(Action<NSError>).FullName})' was not found.");
104+
}
105+
106+
var parameters = signature.GetParameters();
107+
if (parameters.Length != 6 || parameters[2].ParameterType != typeof(ExperimentPayloadExperimentOverflowPolicy))
108+
{
109+
throw new InvalidOperationException(
110+
$"Managed signature regression: expected policy parameter type '{typeof(ExperimentPayloadExperimentOverflowPolicy).FullName}', observed '{parameters.ElementAtOrDefault(2)?.ParameterType.FullName ?? "<missing>"}'.");
111+
}
112+
113+
var controller = ExperimentController.SharedInstance;
114+
if (controller is null)
115+
{
116+
throw new InvalidOperationException("Firebase.ABTesting.ExperimentController.SharedInstance returned null after App.Configure().");
117+
}
118+
119+
var events = new LifecycleEvents
120+
{
121+
SetExperimentEventName = new NSString("codex_set_experiment"),
122+
ActivateExperimentEventName = new NSString("codex_activate_experiment"),
123+
ClearExperimentEventName = new NSString("codex_clear_experiment"),
124+
TimeoutExperimentEventName = new NSString("codex_timeout_experiment"),
125+
ExpireExperimentEventName = new NSString("codex_expire_experiment"),
126+
};
127+
var policy = ExperimentPayloadExperimentOverflowPolicy.DiscardOldest;
128+
var payloads = Array.Empty<NSData>();
129+
var completionInvoked = false;
130+
NSError? completionError = null;
131+
NSException? marshaledException = null;
132+
MarshalObjectiveCExceptionMode? marshaledExceptionMode = null;
133+
var completionSource = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
134+
135+
void OnMarshalObjectiveCException(object? sender, MarshalObjectiveCExceptionEventArgs args)
136+
{
137+
marshaledException ??= args.Exception;
138+
marshaledExceptionMode ??= args.ExceptionMode;
139+
}
140+
141+
Runtime.MarshalObjectiveCException += OnMarshalObjectiveCException;
142+
try
143+
{
144+
try
145+
{
146+
controller.UpdateExperiments("codex", events, policy, -1, payloads, error =>
147+
{
148+
completionInvoked = true;
149+
completionError = error;
150+
completionSource.TrySetResult(true);
151+
});
152+
}
153+
catch (ObjCException ex)
154+
{
155+
throw new InvalidOperationException(
156+
$"Selector '{selector}' should not throw with the corrected enum binding, but observed {ex.GetType().FullName}. " +
157+
$"Managed policy argument type: {policy.GetType().FullName}. Policy value: {(int)policy}. " +
158+
$"Payload array type: {payloads.GetType().FullName}. Payload count: {payloads.Length}. " +
159+
$"NSException.Name: {FormatDetail(marshaledException?.Name?.ToString())}. " +
160+
$"NSException.Reason: {FormatDetail(marshaledException?.Reason)}. " +
161+
$"Marshal mode: {FormatDetail(marshaledExceptionMode?.ToString())}.",
162+
ex);
163+
}
164+
165+
var completedTask = await Task.WhenAny(completionSource.Task, Task.Delay(AsyncTimeout));
166+
if (completedTask != completionSource.Task)
167+
{
168+
throw new TimeoutException(
169+
$"Selector '{selector}' did not invoke its completion callback within {AsyncTimeout.TotalSeconds} seconds.");
170+
}
171+
172+
if (!completionInvoked)
173+
{
174+
throw new InvalidOperationException(
175+
$"Selector '{selector}' completed without throwing, but the completion callback was never marked as invoked.");
176+
}
177+
178+
if (marshaledException is not null)
179+
{
180+
throw new InvalidOperationException(
181+
$"Selector '{selector}' completed, but Runtime.MarshalObjectiveCException captured unexpected NSException.Name '{marshaledException.Name}'. " +
182+
$"Reason: {FormatDetail(marshaledException.Reason)}. Marshal mode: {FormatDetail(marshaledExceptionMode?.ToString())}.");
183+
}
184+
185+
return
186+
$"Selector '{selector}' completed without ObjC exception. " +
187+
$"Managed policy argument type: {parameters[2].ParameterType.FullName}. Policy value: {(int)policy}. " +
188+
$"Payload array type: {payloads.GetType().FullName}. Payload count: {payloads.Length}. " +
189+
$"CompletionInvoked: {completionInvoked}. CompletionError: {FormatDetail(completionError?.LocalizedDescription)}.";
190+
}
191+
finally
192+
{
193+
Runtime.MarshalObjectiveCException -= OnMarshalObjectiveCException;
194+
}
195+
}
196+
#endif
197+
75198
#if ENABLE_RUNTIME_DRIFT_CASE_ABTESTING_ACTIVATEEXPERIMENT
76199
static Task<string> VerifyABTestingActivateExperimentAsync()
77200
{

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-updateexperiments",
5+
"method": "VerifyABTestingUpdateExperimentsAsync",
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": "abtesting-activateexperiment",
516
"method": "VerifyABTestingActivateExperimentAsync",

tools/e2e/run-firebase-foundation.sh

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ log_file="$artifacts_dir/firebase-foundation-sim.log"
2020
result_file="$artifacts_dir/firebase-foundation-result.json"
2121
restore_config="$artifacts_dir/NuGet.generated.config"
2222
repo_restore_config="$repo_root/tests/E2E/Firebase.Foundation/NuGet.config"
23+
packages_cache_dir="$artifacts_dir/packages"
2324
runtime_drift_manifest="$repo_root/tests/E2E/Firebase.Foundation/runtime-drift-cases.json"
2425
runtime_drift_props="$artifacts_dir/runtime-drift-case.generated.props"
2526
runtime_drift_info="$artifacts_dir/runtime-drift-case.info"
@@ -63,6 +64,9 @@ fi
6364
mkdir -p "$artifacts_dir"
6465
: > "$log_file"
6566

67+
rm -rf "$packages_cache_dir"
68+
mkdir -p "$packages_cache_dir"
69+
6670
if [[ "$enable_nullability_validation" == "true" && -n "$runtime_drift_case" ]]; then
6771
echo "--enable-nullability-validation and --runtime-drift-case cannot be used together." >&2
6872
exit 1
@@ -80,6 +84,7 @@ required_packages=(
8084
)
8185

8286
msbuild_args=()
87+
restore_args=()
8388
if [[ "$enable_nullability_validation" == "true" ]]; then
8489
required_packages+=(
8590
"AdamE.Firebase.iOS.AppCheck"
@@ -157,6 +162,7 @@ PY
157162
"-p:RuntimeDriftCaseMethod=$runtime_drift_method"
158163
"-p:RuntimeDriftCasePropsPath=$runtime_drift_props"
159164
)
165+
restore_args+=("--force-evaluate")
160166

161167
echo "Runtime drift case: $runtime_drift_case ($runtime_drift_binding_package)"
162168
fi
@@ -198,7 +204,15 @@ if [[ ! -f "$config_file" ]]; then
198204
fi
199205

200206
echo "Restoring FirebaseFoundationE2E from $package_dir"
201-
dotnet restore "$project_file" --configfile "$restore_config" "${msbuild_args[@]}"
207+
dotnet restore "$project_file" --configfile "$restore_config" --packages "$packages_cache_dir" "${restore_args[@]}" "${msbuild_args[@]}"
208+
209+
echo "Cleaning FirebaseFoundationE2E for iOS simulator"
210+
dotnet clean "$project_file" \
211+
--configuration "$configuration" \
212+
--framework net9.0-ios \
213+
-p:Platform=iPhoneSimulator \
214+
-p:RuntimeIdentifier=iossimulator-arm64 \
215+
"${msbuild_args[@]}"
202216

203217
echo "Building FirebaseFoundationE2E for iOS simulator"
204218
dotnet build "$project_file" \

0 commit comments

Comments
 (0)