// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import 'dart:math'; import 'package:_macros/src/api.dart'; import 'package:_macros/src/executor.dart'; import 'package:_macros/src/executor/exception_impls.dart'; import 'package:_macros/src/executor/introspection_impls.dart'; import 'package:_macros/src/executor/remote_instance.dart'; import 'package:_macros/src/executor/serialization.dart'; import 'package:test/test.dart'; import '../util.dart'; void main() { // We randomize fields which should make the tests more likely to catch issues // related to serialization ordering. final seed = Random().nextInt(1000); print('Nondeterministic test ran with seed: $seed, change to this seed to ' 'repro.'); final rand = Random(seed); for (var mode in [SerializationMode.json, SerializationMode.byteData]) { test('$mode can serialize and deserialize basic data', () { withSerializationMode(mode, () { var serializer = serializerFactory(); serializer ..addInt(0) ..addInt(1) ..addInt(0xff) ..addInt(0xffff) ..addInt(0xffffffff) ..addInt(0xffffffffffffffff) ..addInt(-1) ..addInt(-0x80) ..addInt(-0x8000) ..addInt(-0x80000000) ..addInt(-0x8000000000000000) ..addNullableInt(null) ..addString('hello') ..addString('€') // Requires a two byte string ..addString('𐐷') // Requires two, 16 bit code units ..addNullableString(null) ..startList() ..addBool(true) ..startList() ..addNull() ..endList() ..addNullableBool(null) ..endList() ..addDouble(1.0) ..startList() ..endList(); var deserializer = deserializerFactory(serializer.result); expect(deserializer.moveNext(), true); expect(deserializer.expectInt(), 0); expect(deserializer.moveNext(), true); expect(deserializer.expectInt(), 1); expect(deserializer.moveNext(), true); expect(deserializer.expectInt(), 0xff); expect(deserializer.moveNext(), true); expect(deserializer.expectInt(), 0xffff); expect(deserializer.moveNext(), true); expect(deserializer.expectInt(), 0xffffffff); expect(deserializer.moveNext(), true); expect(deserializer.expectInt(), 0xffffffffffffffff); expect(deserializer.moveNext(), true); expect(deserializer.expectInt(), -1); expect(deserializer.moveNext(), true); expect(deserializer.expectInt(), -0x80); expect(deserializer.moveNext(), true); expect(deserializer.expectInt(), -0x8000); expect(deserializer.moveNext(), true); expect(deserializer.expectInt(), -0x80000000); expect(deserializer.moveNext(), true); expect(deserializer.expectInt(), -0x8000000000000000); expect(deserializer.moveNext(), true); expect(deserializer.expectNullableInt(), null); expect(deserializer.moveNext(), true); expect(deserializer.expectString(), 'hello'); expect(deserializer.moveNext(), true); expect(deserializer.expectString(), '€'); expect(deserializer.moveNext(), true); expect(deserializer.expectString(), '𐐷'); expect(deserializer.moveNext(), true); expect(deserializer.expectNullableString(), null); expect(deserializer.moveNext(), true); deserializer.expectList(); expect(deserializer.moveNext(), true); expect(deserializer.expectBool(), true); expect(deserializer.moveNext(), true); deserializer.expectList(); expect(deserializer.moveNext(), true); expect(deserializer.checkNull(), true); expect(deserializer.moveNext(), false); expect(deserializer.moveNext(), true); expect(deserializer.expectNullableBool(), null); expect(deserializer.moveNext(), false); // Have to move the parent again to advance it past the list entry. expect(deserializer.moveNext(), true); expect(deserializer.expectDouble(), 1.0); expect(deserializer.moveNext(), true); deserializer.expectList(); expect(deserializer.moveNext(), false); expect(deserializer.moveNext(), false); }); }); } for (var mode in [SerializationMode.byteData, SerializationMode.json]) { test('remote instances in $mode', () async { var string = NamedTypeAnnotationImpl( id: RemoteInstance.uniqueId, isNullable: false, identifier: IdentifierImpl(id: RemoteInstance.uniqueId, name: 'String'), typeArguments: const []); var foo = NamedTypeAnnotationImpl( id: RemoteInstance.uniqueId, isNullable: false, identifier: IdentifierImpl(id: RemoteInstance.uniqueId, name: 'Foo'), typeArguments: [string]); withSerializationMode(mode, () { final int zoneId = newRemoteInstanceZone(); withRemoteInstanceZone(zoneId, () { var serializer = serializerFactory(); foo.serialize(serializer); // This is a fake client, we don't want to actually share the cache, // so we negate the zone id and use that. var response = roundTrip(serializer.result, -zoneId); var deserializer = deserializerFactory(response); var instance = RemoteInstance.deserialize(deserializer); expect(instance, foo); }); }); }); } group('declarations', () { final barType = NamedTypeAnnotationImpl( id: RemoteInstance.uniqueId, isNullable: rand.nextBool(), identifier: IdentifierImpl(id: RemoteInstance.uniqueId, name: 'Bar'), typeArguments: []); final fooType = NamedTypeAnnotationImpl( id: RemoteInstance.uniqueId, isNullable: rand.nextBool(), identifier: IdentifierImpl(id: RemoteInstance.uniqueId, name: 'Foo'), typeArguments: [barType]); for (var mode in [SerializationMode.byteData, SerializationMode.json]) { group('with mode $mode', () { test('NamedTypeAnnotation', () { expectSerializationEquality( fooType, mode, RemoteInstance.deserialize); }); final fooNamedParam = FormalParameterDeclarationImpl( id: RemoteInstance.uniqueId, isNamed: rand.nextBool(), isRequired: rand.nextBool(), identifier: IdentifierImpl(id: RemoteInstance.uniqueId, name: 'foo'), library: Fixtures.library, metadata: [], style: ParameterStyle.values[rand.nextInt(3)], type: fooType); final fooNamedFunctionTypeParam = FormalParameterImpl( id: RemoteInstance.uniqueId, isNamed: rand.nextBool(), isRequired: rand.nextBool(), metadata: [], name: 'foo', type: fooType); final barPositionalParam = FormalParameterDeclarationImpl( id: RemoteInstance.uniqueId, isNamed: rand.nextBool(), isRequired: rand.nextBool(), identifier: IdentifierImpl(id: RemoteInstance.uniqueId, name: 'bar'), library: Fixtures.library, metadata: [], style: ParameterStyle.values[rand.nextInt(3)], type: barType); final barPositionalFunctionTypeParam = FormalParameterImpl( id: RemoteInstance.uniqueId, isNamed: rand.nextBool(), isRequired: rand.nextBool(), metadata: [], name: 'bar', type: fooType); final unnamedFunctionTypeParam = FormalParameterImpl( id: RemoteInstance.uniqueId, isNamed: rand.nextBool(), isRequired: rand.nextBool(), metadata: [], name: rand.nextBool() ? null : 'zip', type: fooType); final zapTypeParam = TypeParameterDeclarationImpl( id: RemoteInstance.uniqueId, identifier: IdentifierImpl(id: RemoteInstance.uniqueId, name: 'Zap'), library: Fixtures.library, metadata: [], bound: barType); // Transitively tests `TypeParameterDeclaration` and // `ParameterDeclaration`. test('FunctionTypeAnnotation', () { var functionType = FunctionTypeAnnotationImpl( id: RemoteInstance.uniqueId, isNullable: rand.nextBool(), namedParameters: [ fooNamedFunctionTypeParam, unnamedFunctionTypeParam ], positionalParameters: [barPositionalFunctionTypeParam], returnType: fooType, typeParameters: [ TypeParameterImpl( id: RemoteInstance.uniqueId, metadata: [], name: 'Zip', bound: barType) ], ); expectSerializationEquality( functionType, mode, RemoteInstance.deserialize); }); test('FunctionDeclaration', () { var function = FunctionDeclarationImpl( id: RemoteInstance.uniqueId, identifier: IdentifierImpl(id: RemoteInstance.uniqueId, name: 'name'), library: Fixtures.library, metadata: [], hasBody: rand.nextBool(), hasExternal: rand.nextBool(), isGetter: rand.nextBool(), isOperator: rand.nextBool(), isSetter: rand.nextBool(), namedParameters: [], positionalParameters: [], returnType: fooType, typeParameters: []); expectSerializationEquality( function, mode, RemoteInstance.deserialize); }); test('MethodDeclaration', () { var method = MethodDeclarationImpl( id: RemoteInstance.uniqueId, identifier: IdentifierImpl(id: RemoteInstance.uniqueId, name: 'zorp'), library: Fixtures.library, metadata: [], hasBody: rand.nextBool(), hasExternal: rand.nextBool(), isGetter: rand.nextBool(), isOperator: rand.nextBool(), isSetter: rand.nextBool(), namedParameters: [fooNamedParam], positionalParameters: [barPositionalParam], returnType: fooType, typeParameters: [zapTypeParam], definingType: fooType.identifier, hasStatic: rand.nextBool()); expectSerializationEquality( method, mode, RemoteInstance.deserialize); }); test('ConstructorDeclaration', () { var constructor = ConstructorDeclarationImpl( id: RemoteInstance.uniqueId, identifier: IdentifierImpl(id: RemoteInstance.uniqueId, name: 'new'), library: Fixtures.library, metadata: [], hasBody: rand.nextBool(), hasExternal: rand.nextBool(), namedParameters: [fooNamedParam], positionalParameters: [barPositionalParam], returnType: fooType, typeParameters: [zapTypeParam], definingType: fooType.identifier, isConst: rand.nextBool(), isFactory: rand.nextBool(), ); expectSerializationEquality( constructor, mode, RemoteInstance.deserialize); }); test('VariableDeclaration', () { var bar = VariableDeclarationImpl( id: RemoteInstance.uniqueId, identifier: IdentifierImpl(id: RemoteInstance.uniqueId, name: 'bar'), library: Fixtures.library, metadata: [], hasConst: rand.nextBool(), hasExternal: rand.nextBool(), hasFinal: rand.nextBool(), hasInitializer: rand.nextBool(), hasLate: rand.nextBool(), type: barType, ); expectSerializationEquality( bar, mode, RemoteInstance.deserialize); }); test('FieldDeclaration', () { var bar = FieldDeclarationImpl( id: RemoteInstance.uniqueId, identifier: IdentifierImpl(id: RemoteInstance.uniqueId, name: 'bar'), library: Fixtures.library, metadata: [], hasAbstract: rand.nextBool(), hasConst: rand.nextBool(), hasExternal: rand.nextBool(), hasFinal: rand.nextBool(), hasInitializer: rand.nextBool(), hasLate: rand.nextBool(), type: barType, definingType: fooType.identifier, hasStatic: rand.nextBool(), ); expectSerializationEquality( bar, mode, RemoteInstance.deserialize); }); var objectType = NamedTypeAnnotationImpl( id: RemoteInstance.uniqueId, identifier: IdentifierImpl(id: RemoteInstance.uniqueId, name: 'Object'), isNullable: false, typeArguments: [], ); var serializableType = NamedTypeAnnotationImpl( id: RemoteInstance.uniqueId, identifier: IdentifierImpl(id: RemoteInstance.uniqueId, name: 'Serializable'), isNullable: rand.nextBool(), typeArguments: [], ); test('ClassDeclaration', () { var fooClass = ClassDeclarationImpl( id: RemoteInstance.uniqueId, identifier: IdentifierImpl(id: RemoteInstance.uniqueId, name: 'Foo'), library: Fixtures.library, metadata: [], interfaces: [barType], hasAbstract: rand.nextBool(), hasBase: rand.nextBool(), hasExternal: rand.nextBool(), hasFinal: rand.nextBool(), hasInterface: rand.nextBool(), hasMixin: rand.nextBool(), hasSealed: rand.nextBool(), mixins: [serializableType], superclass: objectType, typeParameters: [zapTypeParam], ); expectSerializationEquality( fooClass, mode, RemoteInstance.deserialize); }); test('EnumDeclaration', () { var fooEnum = EnumDeclarationImpl( id: RemoteInstance.uniqueId, identifier: IdentifierImpl(id: RemoteInstance.uniqueId, name: 'MyEnum'), library: Fixtures.library, metadata: [], interfaces: [barType], mixins: [serializableType], typeParameters: [zapTypeParam], ); expectSerializationEquality( fooEnum, mode, RemoteInstance.deserialize); }); test('EnumValueDeclaration', () { var entry = EnumValueDeclarationImpl( id: RemoteInstance.uniqueId, identifier: IdentifierImpl(id: RemoteInstance.uniqueId, name: 'a'), library: Fixtures.library, metadata: [], definingEnum: IdentifierImpl(id: RemoteInstance.uniqueId, name: 'MyEnum'), ); expectSerializationEquality( entry, mode, RemoteInstance.deserialize); }); test('ExtensionDeclaration', () { var extension = ExtensionDeclarationImpl( id: RemoteInstance.uniqueId, identifier: IdentifierImpl( id: RemoteInstance.uniqueId, name: 'MyExtension'), library: Fixtures.library, metadata: [], typeParameters: [], onType: Fixtures.myClassType); expectSerializationEquality( extension, mode, RemoteInstance.deserialize); }); test('ExtensionTypeDeclaration', () { var extensionType = ExtensionTypeDeclarationImpl( id: RemoteInstance.uniqueId, identifier: IdentifierImpl( id: RemoteInstance.uniqueId, name: 'MyExtensionType'), library: Fixtures.library, metadata: [], typeParameters: [], representationType: Fixtures.myClassType); expectSerializationEquality( extensionType, mode, RemoteInstance.deserialize); }); test('MixinDeclaration', () { var mixin = MixinDeclarationImpl( id: RemoteInstance.uniqueId, identifier: IdentifierImpl(id: RemoteInstance.uniqueId, name: 'MyMixin'), library: Fixtures.library, metadata: [], hasBase: rand.nextBool(), interfaces: [barType], superclassConstraints: [serializableType], typeParameters: [zapTypeParam], ); expectSerializationEquality( mixin, mode, RemoteInstance.deserialize); }); test('TypeAliasDeclaration', () { var typeAlias = TypeAliasDeclarationImpl( id: RemoteInstance.uniqueId, identifier: IdentifierImpl(id: RemoteInstance.uniqueId, name: 'FooOfBar'), library: Fixtures.library, metadata: [], typeParameters: [zapTypeParam], aliasedType: NamedTypeAnnotationImpl( id: RemoteInstance.uniqueId, isNullable: rand.nextBool(), identifier: IdentifierImpl(id: RemoteInstance.uniqueId, name: 'Foo'), typeArguments: [barType]), ); expectSerializationEquality( typeAlias, mode, RemoteInstance.deserialize); }); /// Transitively tests [RecordField] test('RecordTypeAnnotation', () { var recordType = RecordTypeAnnotationImpl( id: RemoteInstance.uniqueId, isNullable: rand.nextBool(), namedFields: [ RecordFieldImpl( id: RemoteInstance.uniqueId, name: 'hello', type: barType, ), ], positionalFields: [ RecordFieldImpl( id: RemoteInstance.uniqueId, name: rand.nextBool() ? null : 'zoiks', type: fooType, ), ], ); expectSerializationEquality( recordType, mode, RemoteInstance.deserialize); }); }); } }); group('Arguments', () { test('can create properly typed collections', () { withSerializationMode(SerializationMode.json, () { final parsed = Arguments.deserialize(deserializerFactory([ // positional args [ // int ArgumentKind.int.index, 1, // List ArgumentKind.list.index, [ArgumentKind.int.index], [ ArgumentKind.int.index, 1, ArgumentKind.int.index, 2, ArgumentKind.int.index, 3, ], // List> ArgumentKind.list.index, [ArgumentKind.set.index, ArgumentKind.string.index], [ // Set ArgumentKind.set.index, [ArgumentKind.string.index], [ ArgumentKind.string.index, 'hello', ArgumentKind.string.index, 'world', ] ], // Map> ArgumentKind.map.index, [ ArgumentKind.int.index, ArgumentKind.nullable.index, ArgumentKind.list.index, ArgumentKind.string.index ], [ // key: int ArgumentKind.int.index, 4, // value: List ArgumentKind.list.index, [ArgumentKind.string.index], [ ArgumentKind.string.index, 'zip', ], ArgumentKind.int.index, 5, ArgumentKind.nil.index, ] ], // named args [], ])); expect(parsed.positional.length, 4); expect(parsed.positional.first.value, 1); expect(parsed.positional[1].value, [1, 2, 3]); expect(parsed.positional[1].value, isA>()); expect(parsed.positional[2].value, [ {'hello', 'world'} ]); expect(parsed.positional[2].value, isA>>()); expect( parsed.positional[3].value, { 4: ['zip'], 5: null, }, ); expect(parsed.positional[3].value, isA?>>()); }); }); group('can be serialized and deserialized', () { for (var mode in [SerializationMode.byteData, SerializationMode.json]) { test('with mode $mode', () { final arguments = Arguments([ MapArgument({ StringArgument('hello'): ListArgument( [BoolArgument(rand.nextBool()), NullArgument()], [ArgumentKind.nullable, ArgumentKind.bool]), }, [ ArgumentKind.string, ArgumentKind.list, ArgumentKind.nullable, ArgumentKind.bool ]), CodeArgument(ExpressionCode.fromParts([ '1 + ', IdentifierImpl(id: RemoteInstance.uniqueId, name: 'a') ])), ListArgument([ TypeAnnotationArgument(Fixtures.myClassType), TypeAnnotationArgument(Fixtures.myEnumType), TypeAnnotationArgument(NamedTypeAnnotationImpl( id: RemoteInstance.uniqueId, isNullable: false, identifier: IdentifierImpl(id: RemoteInstance.uniqueId, name: 'List'), typeArguments: [Fixtures.stringType])), ], [ ArgumentKind.typeAnnotation ]) ], { 'a': SetArgument([ MapArgument({ IntArgument(1): StringArgument('1'), }, [ ArgumentKind.int, ArgumentKind.string ]) ], [ ArgumentKind.map, ArgumentKind.int, ArgumentKind.string ]) }); expectSerializationEquality(arguments, mode, Arguments.deserialize); }); } }); }); group('Exceptions', () { group('can be serialized and deserialized', () { for (var mode in [SerializationMode.byteData, SerializationMode.json]) { test('with mode $mode', () { final exception = UnexpectedMacroExceptionImpl('something happened', stackTrace: 'here'); expectSerializationEquality( exception, mode, RemoteInstance.deserialize); }); } }); }); group('metadata annotations can be serialized and deserialized', () { for (var mode in [SerializationMode.byteData, SerializationMode.json]) { group('with mode $mode', () { test('identifiers', () { final identifierMetadata = IdentifierMetadataAnnotationImpl( id: RemoteInstance.uniqueId, identifier: IdentifierImpl( id: RemoteInstance.uniqueId, name: 'singleton')); expectSerializationEquality( identifierMetadata, mode, RemoteInstance.deserialize); }); test('constructor invocations', () { final constructorMetadata = ConstructorMetadataAnnotationImpl( id: RemoteInstance.uniqueId, type: NamedTypeAnnotationImpl( id: RemoteInstance.uniqueId, identifier: IdentifierImpl( id: RemoteInstance.uniqueId, name: 'Singleton'), isNullable: false, typeArguments: []), constructor: IdentifierImpl(id: RemoteInstance.uniqueId, name: 'someName'), positionalArguments: [ ExpressionCode.fromString("'foo'"), ExpressionCode.fromString('12'), ], namedArguments: { 'bar': ExpressionCode.fromString("'bar'"), 'foobar': ExpressionCode.fromString('13'), }); expectSerializationEquality( constructorMetadata, mode, RemoteInstance.deserialize); }); }); } }); group('static types can be serialized and deserialized', () { for (var mode in [SerializationMode.byteData, SerializationMode.json]) { group('with mode $mode', () { test('named static type', () async { final staticType = NamedStaticTypeImpl( RemoteInstance.uniqueId, declaration: ClassDeclarationImpl( id: RemoteInstance.uniqueId, identifier: IdentifierImpl(id: RemoteInstance.uniqueId, name: 'Foo'), library: Fixtures.library, metadata: [], interfaces: [], hasAbstract: false, hasBase: false, hasExternal: false, hasFinal: false, hasInterface: false, hasMixin: false, hasSealed: false, mixins: [], superclass: null, typeParameters: [ TypeParameterDeclarationImpl( id: RemoteInstance.uniqueId, identifier: IdentifierImpl(id: RemoteInstance.uniqueId, name: 'T'), library: Fixtures.library, metadata: const [], bound: null, ), ], ), typeArguments: [ NamedStaticTypeImpl( RemoteInstance.uniqueId, declaration: Fixtures.stringClass, typeArguments: const [], ), ], ); expectSerializationEquality( staticType, mode, RemoteInstance.deserialize); }); }); } }); } /// Serializes [serializable] in server mode, then deserializes it in client /// mode, and checks that all the fields are the same. void expectSerializationEquality(T serializable, SerializationMode mode, T Function(Deserializer deserializer) deserialize) { withSerializationMode(mode, () { late Object? serialized; final int zoneId = newRemoteInstanceZone(); withRemoteInstanceZone(zoneId, () { var serializer = serializerFactory(); serializable.serialize(serializer); serialized = serializer.result; }); // This is a fake client, we don't want to actually share the cache, // so we negate the zone id and use that. withRemoteInstanceZone(-zoneId, () { var deserializer = deserializerFactory(serialized); var deserialized = deserialize(deserializer); expect( serializable, switch (deserialized) { Declaration() => deepEqualsDeclaration(deserialized as Declaration), TypeAnnotation() => deepEqualsTypeAnnotation(deserialized as TypeAnnotation), Arguments() => deepEqualsArguments(deserialized), MacroExceptionImpl() => deepEqualsMacroException(deserialized), MetadataAnnotation() => deepEqualsMetadataAnnotation(deserialized as MetadataAnnotation), NamedStaticTypeImpl() => deepEqualsStaticType(deserialized as NamedStaticTypeImpl), _ => throw UnsupportedError('Unsupported object type $deserialized'), }); }, createIfMissing: true); }); } /// Deserializes [serialized] in its own remote instance cache and sends it /// back. Object? roundTrip(Object? serialized, int zoneId) { return withRemoteInstanceZone(zoneId, () { var deserializer = deserializerFactory(serialized); var instance = RemoteInstance.deserialize(deserializer) as Serializable; var serializer = serializerFactory(); instance.serialize(serializer); return serializer.result; }, createIfMissing: true); }