|
66 | 66 | using ObjCRuntime; |
67 | 67 | #endif |
68 | 68 |
|
| 69 | +#if ENABLE_RUNTIME_DRIFT_CASE_CLOUDFIRESTORE_SNAPSHOT_LISTEN_OPTIONS |
| 70 | +using Firebase.CloudFirestore; |
| 71 | +using Foundation; |
| 72 | +using ObjCRuntime; |
| 73 | +#endif |
| 74 | + |
69 | 75 | #if ENABLE_RUNTIME_DRIFT_CASE_CLOUDFUNCTIONS_USEFUNCTIONSEMULATORORIGIN |
70 | 76 | using Firebase.CloudFunctions; |
71 | 77 | using Foundation; |
@@ -1299,6 +1305,271 @@ static void RequireCount(int actual, int expected, string label) |
1299 | 1305 | } |
1300 | 1306 | #endif |
1301 | 1307 |
|
| 1308 | +#if ENABLE_RUNTIME_DRIFT_CASE_CLOUDFIRESTORE_SNAPSHOT_LISTEN_OPTIONS |
| 1309 | + static async Task<string> VerifyCloudFirestoreSnapshotListenOptionsAsync() |
| 1310 | + { |
| 1311 | + const string addSnapshotListenerWithOptionsSelector = "addSnapshotListenerWithOptions:listener:"; |
| 1312 | + const string optionsWithIncludeMetadataChangesSelector = "optionsWithIncludeMetadataChanges:"; |
| 1313 | + const string optionsWithSourceSelector = "optionsWithSource:"; |
| 1314 | + |
| 1315 | + var constructor = typeof(SnapshotListenOptions).GetConstructor(Type.EmptyTypes); |
| 1316 | + if (constructor is null) |
| 1317 | + { |
| 1318 | + throw new InvalidOperationException( |
| 1319 | + $"Expected managed API '{typeof(SnapshotListenOptions).FullName}()' was not found."); |
| 1320 | + } |
| 1321 | + |
| 1322 | + var includeMetadataChangesSignature = typeof(SnapshotListenOptions).GetMethod( |
| 1323 | + nameof(SnapshotListenOptions.OptionsWithIncludeMetadataChanges), |
| 1324 | + BindingFlags.Instance | BindingFlags.Public, |
| 1325 | + binder: null, |
| 1326 | + types: new[] { typeof(bool) }, |
| 1327 | + modifiers: null); |
| 1328 | + if (includeMetadataChangesSignature?.ReturnType != typeof(SnapshotListenOptions)) |
| 1329 | + { |
| 1330 | + throw new InvalidOperationException( |
| 1331 | + $"Expected managed API '{nameof(SnapshotListenOptions.OptionsWithIncludeMetadataChanges)}({typeof(bool).FullName})' " + |
| 1332 | + $"to return '{typeof(SnapshotListenOptions).FullName}' for selector '{optionsWithIncludeMetadataChangesSelector}', " + |
| 1333 | + $"observed '{includeMetadataChangesSignature?.ReturnType.FullName ?? "<missing>"}'."); |
| 1334 | + } |
| 1335 | + |
| 1336 | + var sourceSignature = typeof(SnapshotListenOptions).GetMethod( |
| 1337 | + nameof(SnapshotListenOptions.OptionsWithSource), |
| 1338 | + BindingFlags.Instance | BindingFlags.Public, |
| 1339 | + binder: null, |
| 1340 | + types: new[] { typeof(ListenSource) }, |
| 1341 | + modifiers: null); |
| 1342 | + if (sourceSignature?.ReturnType != typeof(SnapshotListenOptions)) |
| 1343 | + { |
| 1344 | + throw new InvalidOperationException( |
| 1345 | + $"Expected managed API '{nameof(SnapshotListenOptions.OptionsWithSource)}({typeof(ListenSource).FullName})' " + |
| 1346 | + $"to return '{typeof(SnapshotListenOptions).FullName}' for selector '{optionsWithSourceSelector}', " + |
| 1347 | + $"observed '{sourceSignature?.ReturnType.FullName ?? "<missing>"}'."); |
| 1348 | + } |
| 1349 | + |
| 1350 | + var documentListenerSignature = typeof(DocumentReference).GetMethod( |
| 1351 | + nameof(DocumentReference.AddSnapshotListener), |
| 1352 | + BindingFlags.Instance | BindingFlags.Public, |
| 1353 | + binder: null, |
| 1354 | + types: new[] { typeof(SnapshotListenOptions), typeof(DocumentSnapshotHandler) }, |
| 1355 | + modifiers: null); |
| 1356 | + if (documentListenerSignature?.ReturnType != typeof(IListenerRegistration)) |
| 1357 | + { |
| 1358 | + throw new InvalidOperationException( |
| 1359 | + $"Expected managed API '{typeof(DocumentReference).FullName}.{nameof(DocumentReference.AddSnapshotListener)}" + |
| 1360 | + $"({typeof(SnapshotListenOptions).FullName}, {typeof(DocumentSnapshotHandler).FullName})' to return " + |
| 1361 | + $"'{typeof(IListenerRegistration).FullName}' for selector '{addSnapshotListenerWithOptionsSelector}', " + |
| 1362 | + $"observed '{documentListenerSignature?.ReturnType.FullName ?? "<missing>"}'."); |
| 1363 | + } |
| 1364 | + |
| 1365 | + var queryListenerSignature = typeof(Query).GetMethod( |
| 1366 | + nameof(Query.AddSnapshotListener), |
| 1367 | + BindingFlags.Instance | BindingFlags.Public, |
| 1368 | + binder: null, |
| 1369 | + types: new[] { typeof(SnapshotListenOptions), typeof(QuerySnapshotHandler) }, |
| 1370 | + modifiers: null); |
| 1371 | + if (queryListenerSignature?.ReturnType != typeof(IListenerRegistration)) |
| 1372 | + { |
| 1373 | + throw new InvalidOperationException( |
| 1374 | + $"Expected managed API '{typeof(Query).FullName}.{nameof(Query.AddSnapshotListener)}" + |
| 1375 | + $"({typeof(SnapshotListenOptions).FullName}, {typeof(QuerySnapshotHandler).FullName})' to return " + |
| 1376 | + $"'{typeof(IListenerRegistration).FullName}' for selector '{addSnapshotListenerWithOptionsSelector}', " + |
| 1377 | + $"observed '{queryListenerSignature?.ReturnType.FullName ?? "<missing>"}'."); |
| 1378 | + } |
| 1379 | + |
| 1380 | + var firestore = Firestore.SharedInstance; |
| 1381 | + if (firestore is null) |
| 1382 | + { |
| 1383 | + throw new InvalidOperationException("Firebase.CloudFirestore.Firestore.SharedInstance returned null after App.Configure()."); |
| 1384 | + } |
| 1385 | + |
| 1386 | + var collectionName = $"codex-snapshot-options-e2e-{Guid.NewGuid():N}"; |
| 1387 | + var collection = firestore.GetCollection(collectionName); |
| 1388 | + if (collection is null) |
| 1389 | + { |
| 1390 | + throw new InvalidOperationException("Firebase.CloudFirestore.Firestore.GetCollection returned null."); |
| 1391 | + } |
| 1392 | + |
| 1393 | + var document = collection.GetDocument("listener-target"); |
| 1394 | + if (document is null) |
| 1395 | + { |
| 1396 | + throw new InvalidOperationException("Firebase.CloudFirestore.CollectionReference.GetDocument returned null."); |
| 1397 | + } |
| 1398 | + |
| 1399 | + using var defaultOptions = new SnapshotListenOptions(); |
| 1400 | + if (defaultOptions.Source != ListenSource.Default) |
| 1401 | + { |
| 1402 | + throw new InvalidOperationException( |
| 1403 | + $"New SnapshotListenOptions.Source returned '{defaultOptions.Source}', expected '{ListenSource.Default}'."); |
| 1404 | + } |
| 1405 | + |
| 1406 | + if (defaultOptions.IncludeMetadataChanges) |
| 1407 | + { |
| 1408 | + throw new InvalidOperationException("New SnapshotListenOptions.IncludeMetadataChanges returned true, expected false."); |
| 1409 | + } |
| 1410 | + |
| 1411 | + if (!defaultOptions.RespondsToSelector(new Selector(optionsWithIncludeMetadataChangesSelector))) |
| 1412 | + { |
| 1413 | + throw new InvalidOperationException( |
| 1414 | + $"Native FIRSnapshotListenOptions does not respond to expected selector '{optionsWithIncludeMetadataChangesSelector}'."); |
| 1415 | + } |
| 1416 | + |
| 1417 | + if (!defaultOptions.RespondsToSelector(new Selector(optionsWithSourceSelector))) |
| 1418 | + { |
| 1419 | + throw new InvalidOperationException( |
| 1420 | + $"Native FIRSnapshotListenOptions does not respond to expected selector '{optionsWithSourceSelector}'."); |
| 1421 | + } |
| 1422 | + |
| 1423 | + if (!document.RespondsToSelector(new Selector(addSnapshotListenerWithOptionsSelector))) |
| 1424 | + { |
| 1425 | + throw new InvalidOperationException( |
| 1426 | + $"Native FIRDocumentReference does not respond to expected selector '{addSnapshotListenerWithOptionsSelector}'."); |
| 1427 | + } |
| 1428 | + |
| 1429 | + if (!collection.RespondsToSelector(new Selector(addSnapshotListenerWithOptionsSelector))) |
| 1430 | + { |
| 1431 | + throw new InvalidOperationException( |
| 1432 | + $"Native FIRQuery does not respond to expected selector '{addSnapshotListenerWithOptionsSelector}'."); |
| 1433 | + } |
| 1434 | + |
| 1435 | + NSException? marshaledException = null; |
| 1436 | + MarshalObjectiveCExceptionMode? marshaledExceptionMode = null; |
| 1437 | + |
| 1438 | + void OnMarshalObjectiveCException(object? sender, MarshalObjectiveCExceptionEventArgs args) |
| 1439 | + { |
| 1440 | + marshaledException ??= args.Exception; |
| 1441 | + marshaledExceptionMode ??= args.ExceptionMode; |
| 1442 | + } |
| 1443 | + |
| 1444 | + Runtime.MarshalObjectiveCException += OnMarshalObjectiveCException; |
| 1445 | + IListenerRegistration? documentRegistration = null; |
| 1446 | + IListenerRegistration? queryRegistration = null; |
| 1447 | + var documentCallbackSource = new TaskCompletionSource<(DocumentSnapshot? Snapshot, NSError? Error)>(TaskCreationOptions.RunContinuationsAsynchronously); |
| 1448 | + var queryCallbackSource = new TaskCompletionSource<(QuerySnapshot? Snapshot, NSError? Error)>(TaskCreationOptions.RunContinuationsAsynchronously); |
| 1449 | + |
| 1450 | + try |
| 1451 | + { |
| 1452 | + SnapshotListenOptions metadataOptions; |
| 1453 | + SnapshotListenOptions cacheOptions; |
| 1454 | + try |
| 1455 | + { |
| 1456 | + metadataOptions = defaultOptions.OptionsWithIncludeMetadataChanges(true); |
| 1457 | + cacheOptions = metadataOptions.OptionsWithSource(ListenSource.Cache); |
| 1458 | + |
| 1459 | + if (metadataOptions is null) |
| 1460 | + { |
| 1461 | + throw new InvalidOperationException($"Selector '{optionsWithIncludeMetadataChangesSelector}' returned null."); |
| 1462 | + } |
| 1463 | + |
| 1464 | + if (cacheOptions is null) |
| 1465 | + { |
| 1466 | + throw new InvalidOperationException($"Selector '{optionsWithSourceSelector}' returned null."); |
| 1467 | + } |
| 1468 | + |
| 1469 | + if (!metadataOptions.IncludeMetadataChanges) |
| 1470 | + { |
| 1471 | + throw new InvalidOperationException( |
| 1472 | + $"Selector '{optionsWithIncludeMetadataChangesSelector}' returned options with IncludeMetadataChanges=false."); |
| 1473 | + } |
| 1474 | + |
| 1475 | + if (cacheOptions.Source != ListenSource.Cache) |
| 1476 | + { |
| 1477 | + throw new InvalidOperationException( |
| 1478 | + $"Selector '{optionsWithSourceSelector}' returned options with Source='{cacheOptions.Source}', expected '{ListenSource.Cache}'."); |
| 1479 | + } |
| 1480 | + |
| 1481 | + if (!cacheOptions.IncludeMetadataChanges) |
| 1482 | + { |
| 1483 | + throw new InvalidOperationException( |
| 1484 | + $"Selector '{optionsWithSourceSelector}' did not preserve IncludeMetadataChanges=true."); |
| 1485 | + } |
| 1486 | + |
| 1487 | + documentRegistration = document.AddSnapshotListener(cacheOptions, (snapshot, error) => |
| 1488 | + { |
| 1489 | + documentCallbackSource.TrySetResult((snapshot, error)); |
| 1490 | + }); |
| 1491 | + queryRegistration = collection.AddSnapshotListener(cacheOptions, (snapshot, error) => |
| 1492 | + { |
| 1493 | + queryCallbackSource.TrySetResult((snapshot, error)); |
| 1494 | + }); |
| 1495 | + } |
| 1496 | + catch (ObjCException ex) |
| 1497 | + { |
| 1498 | + throw new InvalidOperationException( |
| 1499 | + $"Firestore snapshot listen option selectors should not throw after the missing bindings are added, but observed {ex.GetType().FullName}. " + |
| 1500 | + $"Selectors exercised: '{optionsWithIncludeMetadataChangesSelector}', '{optionsWithSourceSelector}', '{addSnapshotListenerWithOptionsSelector}'. " + |
| 1501 | + $"NSException.Name: {FormatDetail(marshaledException?.Name?.ToString())}. " + |
| 1502 | + $"NSException.Reason: {FormatDetail(marshaledException?.Reason)}. " + |
| 1503 | + $"Marshal mode: {FormatDetail(marshaledExceptionMode?.ToString())}.", |
| 1504 | + ex); |
| 1505 | + } |
| 1506 | + |
| 1507 | + if (documentRegistration is null) |
| 1508 | + { |
| 1509 | + throw new InvalidOperationException( |
| 1510 | + $"DocumentReference selector '{addSnapshotListenerWithOptionsSelector}' returned null listener registration."); |
| 1511 | + } |
| 1512 | + |
| 1513 | + if (queryRegistration is null) |
| 1514 | + { |
| 1515 | + throw new InvalidOperationException( |
| 1516 | + $"Query selector '{addSnapshotListenerWithOptionsSelector}' returned null listener registration."); |
| 1517 | + } |
| 1518 | + |
| 1519 | + await Task.WhenAny( |
| 1520 | + Task.WhenAll(documentCallbackSource.Task, queryCallbackSource.Task), |
| 1521 | + Task.Delay(TimeSpan.FromMilliseconds(500))); |
| 1522 | + |
| 1523 | + if (marshaledException is not null) |
| 1524 | + { |
| 1525 | + throw new InvalidOperationException( |
| 1526 | + $"Firestore snapshot listen option selectors completed, but Runtime.MarshalObjectiveCException captured unexpected NSException.Name '{marshaledException.Name}'. " + |
| 1527 | + $"Reason: {FormatDetail(marshaledException.Reason)}. Marshal mode: {FormatDetail(marshaledExceptionMode?.ToString())}."); |
| 1528 | + } |
| 1529 | + |
| 1530 | + var documentCallbackDetail = documentCallbackSource.Task.IsCompletedSuccessfully |
| 1531 | + ? FormatDocumentCallback(documentCallbackSource.Task.Result) |
| 1532 | + : "not observed before listener removal"; |
| 1533 | + var queryCallbackDetail = queryCallbackSource.Task.IsCompletedSuccessfully |
| 1534 | + ? FormatQueryCallback(queryCallbackSource.Task.Result) |
| 1535 | + : "not observed before listener removal"; |
| 1536 | + |
| 1537 | + return |
| 1538 | + $"Firestore snapshot listen option APIs crossed the native selector boundary. " + |
| 1539 | + $"Options: Source={ListenSource.Cache}, IncludeMetadataChanges=true. " + |
| 1540 | + $"Document registration type: {documentRegistration.GetType().FullName}. " + |
| 1541 | + $"Query registration type: {queryRegistration.GetType().FullName}. " + |
| 1542 | + $"Document callback: {documentCallbackDetail}. Query callback: {queryCallbackDetail}."; |
| 1543 | + } |
| 1544 | + finally |
| 1545 | + { |
| 1546 | + try |
| 1547 | + { |
| 1548 | + documentRegistration?.Remove(); |
| 1549 | + } |
| 1550 | + finally |
| 1551 | + { |
| 1552 | + queryRegistration?.Remove(); |
| 1553 | + Runtime.MarshalObjectiveCException -= OnMarshalObjectiveCException; |
| 1554 | + } |
| 1555 | + } |
| 1556 | + |
| 1557 | + static string FormatDocumentCallback((DocumentSnapshot? Snapshot, NSError? Error) callback) |
| 1558 | + { |
| 1559 | + return callback.Error is null |
| 1560 | + ? $"snapshot type {callback.Snapshot?.GetType().FullName ?? "<null>"}" |
| 1561 | + : $"Firebase error {FormatNSError(callback.Error)}"; |
| 1562 | + } |
| 1563 | + |
| 1564 | + static string FormatQueryCallback((QuerySnapshot? Snapshot, NSError? Error) callback) |
| 1565 | + { |
| 1566 | + return callback.Error is null |
| 1567 | + ? $"snapshot type {callback.Snapshot?.GetType().FullName ?? "<null>"}" |
| 1568 | + : $"Firebase error {FormatNSError(callback.Error)}"; |
| 1569 | + } |
| 1570 | + } |
| 1571 | +#endif |
| 1572 | + |
1302 | 1573 | #if ENABLE_RUNTIME_DRIFT_CASE_CLOUDFIRESTORE_AGGREGATE_QUERY |
1303 | 1574 | static async Task<string> VerifyCloudFirestoreAggregateQueryAsync() |
1304 | 1575 | { |
|
0 commit comments