Skip to content

Commit 42d283a

Browse files
Add DatabaseQuery getData binding (#135)
* Add DatabaseQuery getData binding * Fail DatabaseQuery getData test on callback timeout
1 parent f6830da commit 42d283a

3 files changed

Lines changed: 140 additions & 1 deletion

File tree

source/Firebase/Database/ApiDefinition.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ interface DatabaseQuery
112112
[Export ("observeEventType:andPreviousSiblingKeyWithBlock:withCancelBlock:")]
113113
nuint ObserveEvent (DataEventType eventType, DatabaseQueryPreviousSiblingKeyUpdateHandler completionHandler, [NullAllowed] DatabaseQueryCancelHandler cancelHandler);
114114

115+
// - (void)getDataWithCompletionBlock:(void (^_Nonnull)(NSError *__nullable error, FIRDataSnapshot *__nullable snapshot))block;
116+
[Export ("getDataWithCompletionBlock:")]
117+
void GetData (DataSnapshotCompletionHandler completionHandler);
118+
115119
// -(void)observeSingleEventOfType:(FIRDataEventType)eventType withBlock:(void (^ _Nonnull)(FIRDataSnapshot * _Nonnull))block;
116120
[Export ("observeSingleEventOfType:withBlock:")]
117121
void ObserveSingleEvent (DataEventType eventType, DatabaseQueryUpdateHandler completionHandler);

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

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@
2525
using ObjCRuntime;
2626
#endif
2727

28-
#if ENABLE_RUNTIME_DRIFT_CASE_DATABASE_SERVERVALUE_INCREMENT
28+
#if ENABLE_RUNTIME_DRIFT_CASE_DATABASE_SERVERVALUE_INCREMENT || ENABLE_RUNTIME_DRIFT_CASE_DATABASE_QUERY_GETDATA
2929
using Firebase.Database;
30+
using FirebaseCoreOptions = Firebase.Core.Options;
3031
using Foundation;
3132
using ObjCRuntime;
3233
#endif
@@ -736,6 +737,129 @@ void OnMarshalObjectiveCException(object? sender, MarshalObjectiveCExceptionEven
736737
}
737738
#endif
738739

740+
#if ENABLE_RUNTIME_DRIFT_CASE_DATABASE_QUERY_GETDATA
741+
static async Task<string> VerifyDatabaseQueryGetDataAsync()
742+
{
743+
const string selector = "getDataWithCompletionBlock:";
744+
745+
var signature = typeof(DatabaseQuery).GetMethod(
746+
nameof(DatabaseQuery.GetData),
747+
BindingFlags.Instance | BindingFlags.Public,
748+
binder: null,
749+
types: new[] { typeof(DataSnapshotCompletionHandler) },
750+
modifiers: null);
751+
if (signature?.ReturnType != typeof(void))
752+
{
753+
throw new InvalidOperationException(
754+
$"Expected managed API '{typeof(DatabaseQuery).FullName}.{nameof(DatabaseQuery.GetData)}({typeof(DataSnapshotCompletionHandler).FullName})' " +
755+
$"to return void for selector '{selector}', observed '{signature?.ReturnType.FullName ?? "<missing>"}'.");
756+
}
757+
758+
var projectId = FirebaseCoreOptions.DefaultInstance?.ProjectId
759+
?? throw new InvalidOperationException("Firebase.Core.Options.ProjectId returned null before Database query validation.");
760+
var database = Firebase.Database.Database.From($"https://{projectId}-default-rtdb.firebaseio.com");
761+
var root = database.GetRootReference();
762+
var query = root.GetQueryOrderedByKey();
763+
if (query is null)
764+
{
765+
throw new InvalidOperationException("Firebase.Database.DatabaseReference.GetQueryOrderedByKey returned null.");
766+
}
767+
768+
if (!query.RespondsToSelector(new Selector(selector)))
769+
{
770+
throw new InvalidOperationException($"Native FIRDatabaseQuery does not respond to expected selector '{selector}'.");
771+
}
772+
773+
var completionSource = new TaskCompletionSource<(NSError? Error, DataSnapshot? Snapshot)>(TaskCreationOptions.RunContinuationsAsynchronously);
774+
var completionInvoked = false;
775+
NSError? callbackError = null;
776+
DataSnapshot? callbackSnapshot = null;
777+
NSException? marshaledException = null;
778+
MarshalObjectiveCExceptionMode? marshaledExceptionMode = null;
779+
780+
void OnMarshalObjectiveCException(object? sender, MarshalObjectiveCExceptionEventArgs args)
781+
{
782+
marshaledException ??= args.Exception;
783+
marshaledExceptionMode ??= args.ExceptionMode;
784+
}
785+
786+
Runtime.MarshalObjectiveCException += OnMarshalObjectiveCException;
787+
try
788+
{
789+
try
790+
{
791+
query.GetData((error, snapshot) =>
792+
{
793+
completionInvoked = true;
794+
callbackError = error;
795+
callbackSnapshot = snapshot;
796+
completionSource.TrySetResult((error, snapshot));
797+
});
798+
}
799+
catch (ObjCException ex)
800+
{
801+
throw new InvalidOperationException(
802+
$"Selector '{selector}' should not throw after the missing DatabaseQuery binding is added, but observed {ex.GetType().FullName}. " +
803+
$"Managed query runtime type: {query.GetType().FullName}. " +
804+
$"NSException.Name: {FormatDetail(marshaledException?.Name?.ToString())}. " +
805+
$"NSException.Reason: {FormatDetail(marshaledException?.Reason)}. " +
806+
$"Marshal mode: {FormatDetail(marshaledExceptionMode?.ToString())}.",
807+
ex);
808+
}
809+
810+
if (marshaledException is not null)
811+
{
812+
throw new InvalidOperationException(
813+
$"Selector '{selector}' completed, but Runtime.MarshalObjectiveCException captured unexpected NSException.Name '{marshaledException.Name}'. " +
814+
$"Reason: {FormatDetail(marshaledException.Reason)}. Marshal mode: {FormatDetail(marshaledExceptionMode?.ToString())}.");
815+
}
816+
817+
var completedTask = await Task.WhenAny(completionSource.Task, Task.Delay(AsyncTimeout));
818+
if (completedTask != completionSource.Task)
819+
{
820+
throw new TimeoutException(
821+
$"Selector '{selector}' did not invoke its completion callback within {AsyncTimeout.TotalSeconds} seconds after crossing the native DatabaseQuery boundary.");
822+
}
823+
824+
string callbackDetail;
825+
var (completedError, completedSnapshot) = await completionSource.Task;
826+
if (!completionInvoked)
827+
{
828+
throw new InvalidOperationException(
829+
$"Selector '{selector}' completed without throwing, but the completion callback was never marked as invoked.");
830+
}
831+
832+
if (!ReferenceEquals(callbackError, completedError) || !ReferenceEquals(callbackSnapshot, completedSnapshot))
833+
{
834+
throw new InvalidOperationException("Database query getData callback state did not match the completed task payload.");
835+
}
836+
837+
callbackDetail = completedError is not null
838+
? $"completion callback returned Firebase error {FormatNSError(completedError)}"
839+
: completedSnapshot is not null
840+
? $"completion callback returned snapshot type {completedSnapshot.GetType().FullName} with key '{FormatDetail(completedSnapshot.Key)}'"
841+
: "completion callback returned neither snapshot nor Firebase error";
842+
843+
if (marshaledException is not null)
844+
{
845+
throw new InvalidOperationException(
846+
$"Selector '{selector}' completed, but Runtime.MarshalObjectiveCException captured unexpected NSException.Name '{marshaledException.Name}'. " +
847+
$"Reason: {FormatDetail(marshaledException.Reason)}. Marshal mode: {FormatDetail(marshaledExceptionMode?.ToString())}.");
848+
}
849+
850+
return
851+
$"Selector '{selector}' crossed the native DatabaseQuery boundary. " +
852+
$"Managed query runtime type: {query.GetType().FullName}. " +
853+
$"Query reference URL: {query.Reference.Url}. " +
854+
$"Callback detail: {callbackDetail}.";
855+
}
856+
finally
857+
{
858+
Runtime.MarshalObjectiveCException -= OnMarshalObjectiveCException;
859+
}
860+
}
861+
#endif
862+
739863
#if ENABLE_RUNTIME_DRIFT_CASE_ABTESTING_UPDATEEXPERIMENTS
740864
static async Task<string> VerifyABTestingUpdateExperimentsAsync()
741865
{

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,17 @@
4949
}
5050
]
5151
},
52+
{
53+
"id": "database-query-getdata",
54+
"method": "VerifyDatabaseQueryGetDataAsync",
55+
"bindingPackage": "AdamE.Firebase.iOS.Database",
56+
"packages": [
57+
{
58+
"id": "AdamE.Firebase.iOS.Database",
59+
"version": "12.6.0"
60+
}
61+
]
62+
},
5263
{
5364
"id": "abtesting-updateexperiments",
5465
"method": "VerifyABTestingUpdateExperimentsAsync",

0 commit comments

Comments
 (0)