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
5 changes: 5 additions & 0 deletions source/Firebase/Analytics/ApiDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ interface Analytics
[Export ("setSessionTimeoutInterval:")]
void SetSessionTimeoutInterval (double sessionTimeoutInterval);

// + (void)sessionIDWithCompletion:(void (^)(int64_t sessionID, NSError *_Nullable error))completion;
[Static]
[Export ("sessionIDWithCompletion:")]
void SessionIdWithCompletion (Action<long, NSError> completion);

// + (nullable NSString *)appInstanceID;
[Static]
[NullAllowed]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
using System.Reflection;

#if ENABLE_RUNTIME_DRIFT_CASE_ANALYTICS_SESSIONIDWITHCOMPLETION
using Firebase.Analytics;
using Foundation;
using ObjCRuntime;
#endif

#if ENABLE_RUNTIME_DRIFT_CASE_DATABASE_SERVERVALUE_INCREMENT
using Firebase.Database;
using Foundation;
Expand Down Expand Up @@ -84,6 +90,100 @@ public static async Task<string> ExecuteConfiguredCaseAsync()
?.Value;
}

#if ENABLE_RUNTIME_DRIFT_CASE_ANALYTICS_SESSIONIDWITHCOMPLETION
static async Task<string> VerifyAnalyticsSessionIdWithCompletionAsync()
{
const string selector = "sessionIDWithCompletion:";

var signature = typeof(Analytics).GetMethod(
nameof(Analytics.SessionIdWithCompletion),
BindingFlags.Static | BindingFlags.Public,
binder: null,
types: new[] { typeof(Action<long, NSError>) },
modifiers: null);
if (signature is null)
{
throw new InvalidOperationException(
$"Expected managed API '{nameof(Analytics.SessionIdWithCompletion)}({typeof(Action<long, NSError>).FullName})' was not found.");
}

Analytics.SetAnalyticsCollectionEnabled(true);
Analytics.SetConsent(new Dictionary<ConsentType, ConsentStatus>
{
[ConsentType.AnalyticsStorage] = ConsentStatus.Granted,
[ConsentType.AdStorage] = ConsentStatus.Denied,
});

var callbackInvoked = false;
long callbackSessionId = 0;
NSError? callbackError = null;
NSException? marshaledException = null;
MarshalObjectiveCExceptionMode? marshaledExceptionMode = null;
var completionSource = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);

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

Runtime.MarshalObjectiveCException += OnMarshalObjectiveCException;
try
{
try
{
Analytics.SessionIdWithCompletion((sessionId, error) =>
{
callbackInvoked = true;
callbackSessionId = sessionId;
callbackError = error;
completionSource.TrySetResult(true);
});
}
catch (ObjCException ex)
{
throw new InvalidOperationException(
$"Selector '{selector}' should not throw after the missing binding is added, but observed {ex.GetType().FullName}. " +
$"Completion delegate type: {typeof(Action<long, NSError>).FullName}. " +
$"NSException.Name: {FormatDetail(marshaledException?.Name?.ToString())}. " +
$"NSException.Reason: {FormatDetail(marshaledException?.Reason)}. " +
$"Marshal mode: {FormatDetail(marshaledExceptionMode?.ToString())}.",
ex);
}

var completedTask = await Task.WhenAny(completionSource.Task, Task.Delay(AsyncTimeout));
if (completedTask != completionSource.Task)
{
throw new TimeoutException(
$"Selector '{selector}' did not invoke its completion callback within {AsyncTimeout.TotalSeconds} seconds.");
}

if (!callbackInvoked)
{
throw new InvalidOperationException(
$"Selector '{selector}' completed without throwing, but the completion callback was never marked as invoked.");
}

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
$"Selector '{selector}' invoked its completion callback without ObjC exception. " +
$"Completion delegate type: {typeof(Action<long, NSError>).FullName}. " +
$"SessionId: {callbackSessionId}. " +
$"NSError: {callbackError?.LocalizedDescription ?? "<null>"}.";
}
finally
{
Runtime.MarshalObjectiveCException -= OnMarshalObjectiveCException;
}
}
#endif

#if ENABLE_RUNTIME_DRIFT_CASE_DATABASE_SERVERVALUE_INCREMENT
static Task<string> VerifyDatabaseServerValueIncrementAsync()
{
Expand Down
6 changes: 6 additions & 0 deletions tests/E2E/Firebase.Foundation/runtime-drift-cases.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
{
"cases": [
{
"id": "analytics-sessionidwithcompletion",
"method": "VerifyAnalyticsSessionIdWithCompletionAsync",
"bindingPackage": "AdamE.Firebase.iOS.Analytics",
"packages": []
},
{
"id": "database-servervalue-increment",
"method": "VerifyDatabaseServerValueIncrementAsync",
Expand Down
3 changes: 2 additions & 1 deletion tools/e2e/run-firebase-foundation.sh
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ method = case.get("method")
binding_package = case.get("bindingPackage")
packages = case.get("packages", [])

if not method or not binding_package or not packages:
if not method or not binding_package or packages is None:
raise SystemExit(f"Runtime drift case '{case_id}' is missing required manifest fields.")

symbol = "ENABLE_RUNTIME_DRIFT_CASE_" + re.sub(r"[^A-Za-z0-9]+", "_", case_id).strip("_").upper()
Expand Down Expand Up @@ -153,6 +153,7 @@ PY
runtime_drift_details=("${(@f)$(<"$runtime_drift_info")}")
runtime_drift_method="${runtime_drift_details[1]}"
runtime_drift_binding_package="${runtime_drift_details[2]}"
required_packages+=("$runtime_drift_binding_package")
for (( i = 3; i <= ${#runtime_drift_details[@]}; i++ )); do
required_packages+=("${runtime_drift_details[$i]}")
done
Expand Down
Loading