From 365950a2716b430121d8d468a64aa9a4bb19e65d Mon Sep 17 00:00:00 2001 From: Greg Price Date: Tue, 8 Aug 2023 16:54:16 -0700 Subject: [PATCH] test: Write a generic jsonEquals check and deepToJson function --- test/api/model/events_test.dart | 1 + test/api/model/model_checks.dart | 20 --------- test/api/route/messages_test.dart | 1 - test/model/message_list_test.dart | 1 + test/stdlib_checks.dart | 67 +++++++++++++++++++++++++++++++ 5 files changed, 69 insertions(+), 21 deletions(-) diff --git a/test/api/model/events_test.dart b/test/api/model/events_test.dart index 542bd59b412..b05771fd1e5 100644 --- a/test/api/model/events_test.dart +++ b/test/api/model/events_test.dart @@ -3,6 +3,7 @@ import 'package:test/scaffolding.dart'; import 'package:zulip/api/model/events.dart'; import '../../example_data.dart' as eg; +import '../../stdlib_checks.dart'; import 'events_checks.dart'; import 'model_checks.dart'; diff --git a/test/api/model/model_checks.dart b/test/api/model/model_checks.dart index a1c5c82a3ae..7067e53d848 100644 --- a/test/api/model/model_checks.dart +++ b/test/api/model/model_checks.dart @@ -2,14 +2,6 @@ import 'package:checks/checks.dart'; import 'package:zulip/api/model/model.dart'; extension MessageChecks on Subject { - Subject> get toJson => has((e) => e.toJson(), 'toJson'); - - void jsonEquals(Message expected) { - final expectedJson = expected.toJson(); - expectedJson['reactions'] = it()..isA>().jsonEquals(expected.reactions); - toJson.deepEquals(expectedJson); - } - Subject get content => has((e) => e.content, 'content'); Subject get isMeMessage => has((e) => e.isMeMessage, 'isMeMessage'); Subject get lastEditTimestamp => has((e) => e.lastEditTimestamp, 'lastEditTimestamp'); @@ -23,21 +15,9 @@ extension ReactionsChecks on Subject> { void deepEquals(_) { throw UnimplementedError('Tried to call [Subject>.deepEquals]. Use jsonEquals instead.'); } - - void jsonEquals(List expected) { - // (cast, to bypass this extension's deepEquals implementation, which throws) - // ignore: unnecessary_cast - (this as Subject).deepEquals(expected.map((r) => it()..isA().jsonEquals(r))); - } } extension ReactionChecks on Subject { - Subject> get toJson => has((r) => r.toJson(), 'toJson'); - - void jsonEquals(Reaction expected) { - toJson.deepEquals(expected.toJson()); - } - Subject get emojiName => has((r) => r.emojiName, 'emojiName'); Subject get emojiCode => has((r) => r.emojiCode, 'emojiCode'); Subject get reactionType => has((r) => r.reactionType, 'reactionType'); diff --git a/test/api/route/messages_test.dart b/test/api/route/messages_test.dart index 962d0d9ccea..d9853bb79be 100644 --- a/test/api/route/messages_test.dart +++ b/test/api/route/messages_test.dart @@ -11,7 +11,6 @@ import 'package:zulip/model/narrow.dart'; import '../../example_data.dart' as eg; import '../../stdlib_checks.dart'; import '../fake_api.dart'; -import '../model/model_checks.dart'; import 'route_checks.dart'; void main() { diff --git a/test/model/message_list_test.dart b/test/model/message_list_test.dart index 463860b4e87..e1ac2431b61 100644 --- a/test/model/message_list_test.dart +++ b/test/model/message_list_test.dart @@ -11,6 +11,7 @@ import '../api/model/model_checks.dart'; import '../example_data.dart' as eg; import '../model/binding.dart'; import '../model/test_store.dart'; +import '../stdlib_checks.dart'; const int userId = 1; diff --git a/test/stdlib_checks.dart b/test/stdlib_checks.dart index acb6b8cabdb..e2d05657e1d 100644 --- a/test/stdlib_checks.dart +++ b/test/stdlib_checks.dart @@ -19,6 +19,73 @@ extension NullableMapChecks on Subject?> { } } +/// Convert [object] to a pure JSON-like value. +/// +/// The result is similar to `jsonDecode(jsonEncode(object))`, but without +/// passing through a serialized form. +/// +/// All JSON atoms (numbers, booleans, null, and strings) are used directly. +/// All JSON containers (lists, and maps with string keys) are copied +/// as their elements are converted recursively. +/// For any other value, a dynamic call `.toJson()` is made and +/// should return either a JSON atom or a JSON container. +Object? deepToJson(Object? object) { + // Implementation is based on the recursion underlying [jsonEncode], + // at [_JsonStringifier.writeObject] in the stdlib's convert/json.dart . + // (We leave out the cycle-checking, for simplicity / out of laziness.) + + var (result, success) = _deeplyConvertShallowJsonValue(object); + if (success) return result; + + final Object? shallowlyConverted; + try { + shallowlyConverted = (object as dynamic).toJson(); + } catch (e) { + throw JsonUnsupportedObjectError(object, cause: e); + } + + (result, success) = _deeplyConvertShallowJsonValue(shallowlyConverted); + if (success) return result; + throw JsonUnsupportedObjectError(object); +} + +(Object? result, bool success) _deeplyConvertShallowJsonValue(Object? object) { + final Object? result; + switch (object) { + case null || bool() || String() || num(): + result = object; + case List(): + result = object.map((x) => deepToJson(x)).toList(); + case Map() when object.keys.every((k) => k is String): + result = object.map((k, v) => MapEntry(k, deepToJson(v))); + default: + return (null, false); + } + return (result, true); +} + +extension JsonChecks on Subject { + /// Expects that the value is deeply equal to [expected], + /// after calling [deepToJson] on both. + /// + /// Deep equality is computed by [MapChecks.deepEquals] + /// or [IterableChecks.deepEquals]. + void jsonEquals(Object? expected) { + final expectedJson = deepToJson(expected); + final actualJson = has((e) => deepToJson(e), 'deepToJson'); + switch (expectedJson) { + case null || bool() || String() || num(): + return actualJson.equals(expectedJson); + case List(): + return actualJson.isA().deepEquals(expectedJson); + case Map(): + return actualJson.isA().deepEquals(expectedJson); + case _: + assert(false); + } + } +} + extension UriChecks on Subject { Subject get asString => has((u) => u.toString(), 'toString'); // TODO(checks): what's a good convention for this?