// Copyright (c) 2015, 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. @TestOn('vm') import 'dart:async'; import 'package:analyzer/dart/element/type.dart'; import 'package:build/build.dart'; import 'package:dart_style/dart_style.dart' as dart_style; import 'package:json_annotation/json_annotation.dart'; import 'package:json_serializable/json_serializable.dart'; import 'package:json_serializable/src/constants.dart'; import 'package:json_serializable/src/type_helper.dart'; import 'package:source_gen/source_gen.dart'; import 'package:test/test.dart'; import 'analysis_utils.dart'; import 'shared_config.dart'; import 'test_file_utils.dart'; Matcher _matcherFromShouldGenerateAnnotation(ConstantReader reader, {bool wrapped = false}) { String expectedOutput; if (wrapped) { final expectedWrappedOutput = reader.read('expectedWrappedOutput'); if (expectedWrappedOutput.isNull) { return null; } expectedOutput = expectedWrappedOutput.stringValue; } else { expectedOutput = reader.read('expectedOutput').stringValue; } final isContains = reader.read('contains').boolValue; if (isContains) { return contains(expectedOutput); } return equals(expectedOutput); } Matcher _throwsInvalidGenerationSourceError(messageMatcher, todoMatcher) => throwsA(const TypeMatcher() .having((e) => e.message, 'message', messageMatcher) .having((e) => e.todo, 'todo', todoMatcher) .having((e) => e.element, 'element', isNotNull)); Matcher _throwsUnsupportedError(matcher) => throwsA(const TypeMatcher() .having((e) => e.message, 'message', matcher)); final _formatter = dart_style.DartFormatter(); LibraryReader _library; final _buildLogItems = []; const _expectedAnnotatedTests = { '_json_serializable_test_input.dart': [ 'theAnswer', 'annotatedMethod', 'Person', 'Order', 'FinalFields', 'FinalFieldsNotSetInCtor', 'SetSupport', 'IncludeIfNullOverride', 'KeyDupesField', 'DupeKeys', 'IgnoredFieldClass', 'IgnoredFieldCtorClass', 'PrivateFieldCtorClass', 'IncludeIfNullDisallowNullClass', 'JsonValueWithBool', 'JsonValueValid' ], 'checked_test_input.dart': [ 'WithANonCtorGetterChecked', 'WithANonCtorGetter' ], 'default_value_input.dart': [ 'DefaultWithSymbol', 'DefaultWithFunction', 'DefaultWithType', 'DefaultWithConstObject', 'DefaultWithNestedEnum', 'DefaultWithNonNullableField', 'DefaultWithNonNullableClass' ], 'field_namer_input.dart': [ 'FieldNamerNone', 'FieldNamerKebab', 'FieldNamerSnake' ], 'generic_test_input.dart': ['GenericClass'], 'inheritance_test_input.dart': [ 'SubType', 'SubTypeWithAnnotatedFieldOverrideExtends', 'SubTypeWithAnnotatedFieldOverrideExtendsWithOverrides', 'SubTypeWithAnnotatedFieldOverrideImplements' ], 'json_converter_test_input.dart': [ 'JsonConverterNamedCtor', 'JsonConvertOnField', 'JsonConverterWithBadTypeArg', 'JsonConverterDuplicateAnnotations', 'JsonConverterCtorParams', ], 'setter_test_input.dart': [ 'JustSetter', 'JustSetterNoToJson', 'JustSetterNoFromJson' ], 'to_from_json_test_input.dart': [ 'BadFromFuncReturnType', 'InvalidFromFunc2Args', 'ValidToFromFuncClassStatic', 'BadToFuncReturnType', 'InvalidToFunc2Args', 'ObjectConvertMethods', 'DynamicConvertMethods', 'TypedConvertMethods', 'FromDynamicCollection', 'BadNoArgs', 'BadTwoRequiredPositional', 'BadOneNamed', 'OkayOneNormalOptionalPositional', 'OkayOneNormalOptionalNamed', 'OkayOnlyOptionalPositional' ], }; void main() async { final path = testFilePath('test', 'src'); _library = await resolveCompilationUnit(path); StreamSubscription logSubscription; setUp(() { assert(_buildLogItems.isEmpty); assert(logSubscription == null); logSubscription = log.onRecord.listen((r) => _buildLogItems.add(r.message)); }); tearDown(() async { if (logSubscription != null) { await logSubscription.cancel(); logSubscription = null; } final remainingItems = _buildLogItems.toList(); _buildLogItems.clear(); expect(remainingItems, isEmpty, reason: 'Tests should validate entries and clear this before `tearDown`.'); _buildLogItems.clear(); }); // Only need to run this check once! test('[all expected files]', () { expect(_annotatedElements.keys, _expectedAnnotatedTests.keys); }); for (final entry in _annotatedElements.entries) { group(entry.key, () { test('[all expected classes]', () { expect(_expectedAnnotatedTests, containsPair(entry.key, entry.value.map((ae) => ae.element.name))); }); for (final annotatedElement in entry.value) { _testAnnotatedClass(annotatedElement); } }); } group('without wrappers', () { _registerTests(JsonSerializable.defaults); }); group( 'with wrapper', () => _registerTests( const JsonSerializable(useWrappers: true).withDefaults())); group('configuration', () { void runWithConfigAndLogger(JsonSerializable config, String className) { _runForElementNamedWithGenerator( JsonSerializableGenerator( config: config, typeHelpers: const [_ConfigLogger()]), className); } setUp(_ConfigLogger.configurations.clear); group('defaults', () { for (var className in [ 'ConfigurationImplicitDefaults', 'ConfigurationExplicitDefaults', ]) { for (var nullConfig in [true, false]) { final testDescription = '$className with ${nullConfig ? 'null' : 'default'} config'; test(testDescription, () { runWithConfigAndLogger( nullConfig ? null : const JsonSerializable(), className); expect(_ConfigLogger.configurations, hasLength(2)); expect(_ConfigLogger.configurations.first, same(_ConfigLogger.configurations.last)); expect(_ConfigLogger.configurations.first.toJson(), generatorConfigDefaultJson); }); } } }); test( 'values in config override unconfigured (default) values in annotation', () { runWithConfigAndLogger( JsonSerializable.fromJson(generatorConfigNonDefaultJson), 'ConfigurationImplicitDefaults'); expect(_ConfigLogger.configurations, isEmpty, reason: 'all generation is disabled'); // Create a configuration with just `create_to_json` set to true so we // can validate the configuration that is run with final configMap = Map.from(generatorConfigNonDefaultJson); configMap['create_to_json'] = true; runWithConfigAndLogger(JsonSerializable.fromJson(configMap), 'ConfigurationImplicitDefaults'); }); test( 'explicit values in annotation override corresponding settings in config', () { runWithConfigAndLogger( JsonSerializable.fromJson(generatorConfigNonDefaultJson), 'ConfigurationExplicitDefaults'); expect(_ConfigLogger.configurations, hasLength(2)); expect(_ConfigLogger.configurations.first, same(_ConfigLogger.configurations.last)); // The effective configuration should be non-Default configuration, but // with all fields set from JsonSerializable as the defaults final expected = Map.from(generatorConfigNonDefaultJson); for (var jsonSerialKey in jsonSerializableFields) { expected[jsonSerialKey] = generatorConfigDefaultJson[jsonSerialKey]; } expect(_ConfigLogger.configurations.first.toJson(), expected); }); }); } void _testAnnotatedClass(AnnotatedElement annotatedElement) { final annotationName = annotatedElement.annotation.objectValue.type.name; switch (annotationName) { case 'ShouldThrow': _testShouldThrow(annotatedElement); break; case 'ShouldGenerate': _testShouldGenerate(annotatedElement); break; default: throw UnsupportedError("We don't support $annotationName"); } } void _testShouldThrow(AnnotatedElement annotatedElement) { final element = annotatedElement.element; final constReader = annotatedElement.annotation; final messageMatcher = constReader.read('errorMessage').stringValue; var todoMatcher = constReader.read('todo').literalValue; test(element.name, () { todoMatcher ??= isEmpty; expect( () => _runForElementNamed( const JsonSerializable(useWrappers: false), element.name), _throwsInvalidGenerationSourceError(messageMatcher, todoMatcher), reason: 'Should fail without wrappers.'); expect( () => _runForElementNamed( const JsonSerializable(useWrappers: true), element.name), _throwsInvalidGenerationSourceError(messageMatcher, todoMatcher), reason: 'Should fail with wrappers.'); }); } void _testShouldGenerate(AnnotatedElement annotatedElement) { final element = annotatedElement.element; final matcher = _matcherFromShouldGenerateAnnotation(annotatedElement.annotation); final expectedLogItems = annotatedElement.annotation .read('expectedLogItems') .listValue .map((obj) => obj.toStringValue()) .toList(); final checked = annotatedElement.annotation.read('checked').boolValue; test(element.name, () { final output = _runForElementNamed(JsonSerializable(checked: checked), element.name); expect(output, matcher); expect(_buildLogItems, expectedLogItems); _buildLogItems.clear(); }); final wrappedMatcher = _matcherFromShouldGenerateAnnotation( annotatedElement.annotation, wrapped: true); if (wrappedMatcher != null) { test('${element.name} - (wrapped)', () { final output = _runForElementNamed( JsonSerializable(checked: checked, useWrappers: true), element.name); expect(output, wrappedMatcher); expect(_buildLogItems, expectedLogItems); _buildLogItems.clear(); }); } } String _runForElementNamed(JsonSerializable config, String name) { final generator = JsonSerializableGenerator(config: config); return _runForElementNamedWithGenerator(generator, name); } String _runForElementNamedWithGenerator( JsonSerializableGenerator generator, String name) { final element = _library.allElements.singleWhere((e) => e.name == name); final annotation = generator.typeChecker.firstAnnotationOf(element); final generated = generator .generateForAnnotatedElement(element, ConstantReader(annotation), null) .map((e) => e.trim()) .where((e) => e.isNotEmpty) .map((e) => '$e\n\n') .join(); final output = _formatter.format(generated); printOnFailure("r'''\n$output'''"); return output; } final _annotatedElements = _library.allElements .map((e) { for (final md in e.metadata) { final reader = ConstantReader(md.constantValue); if (const ['ShouldGenerate', 'ShouldThrow'] .contains(reader.objectValue.type.name)) { return AnnotatedElement(reader, e); } } return null; }) .where((ae) => ae != null) .fold>>( >{}, (map, annotatedElement) { final list = map.putIfAbsent( annotatedElement.element.source.uri.pathSegments.last, () => []); list.add(annotatedElement); return map; }); void _registerTests(JsonSerializable generator) { String runForElementNamed(String name) => _runForElementNamed(generator, name); void expectThrows(String elementName, messageMatcher, [todoMatcher]) { todoMatcher ??= isEmpty; expect(() => runForElementNamed(elementName), _throwsInvalidGenerationSourceError(messageMatcher, todoMatcher)); } group('explicit toJson', () { test('nullable', () { final output = _runForElementNamed( JsonSerializable( explicitToJson: true, useWrappers: generator.useWrappers), 'TrivialNestedNullable'); final expected = generator.useWrappers ? r''' Map _$TrivialNestedNullableToJson( TrivialNestedNullable instance) => _$TrivialNestedNullableJsonMapWrapper(instance); class _$TrivialNestedNullableJsonMapWrapper extends $JsonMapWrapper { final TrivialNestedNullable _v; _$TrivialNestedNullableJsonMapWrapper(this._v); @override Iterable get keys => const ['child', 'otherField']; @override dynamic operator [](Object key) { if (key is String) { switch (key) { case 'child': return _v.child?.toJson(); case 'otherField': return _v.otherField; } } return null; } } ''' : r''' Map _$TrivialNestedNullableToJson( TrivialNestedNullable instance) => { 'child': instance.child?.toJson(), 'otherField': instance.otherField }; '''; expect(output, expected); }); test('non-nullable', () { final output = _runForElementNamed( JsonSerializable( explicitToJson: true, useWrappers: generator.useWrappers), 'TrivialNestedNonNullable'); final expected = generator.useWrappers ? r''' Map _$TrivialNestedNonNullableToJson( TrivialNestedNonNullable instance) => _$TrivialNestedNonNullableJsonMapWrapper(instance); class _$TrivialNestedNonNullableJsonMapWrapper extends $JsonMapWrapper { final TrivialNestedNonNullable _v; _$TrivialNestedNonNullableJsonMapWrapper(this._v); @override Iterable get keys => const ['child', 'otherField']; @override dynamic operator [](Object key) { if (key is String) { switch (key) { case 'child': return _v.child.toJson(); case 'otherField': return _v.otherField; } } return null; } } ''' : r''' Map _$TrivialNestedNonNullableToJson( TrivialNestedNonNullable instance) => { 'child': instance.child.toJson(), 'otherField': instance.otherField }; '''; expect(output, expected); }); }); group('unknown types', () { tearDown(() { expect(_buildLogItems, hasLength(1)); expect(_buildLogItems.first, startsWith('This element has an undefined type.')); _buildLogItems.clear(); }); String flavorMessage(String flavor) => 'Could not generate `$flavor` code for `number` ' 'because the type is undefined.'; String flavorTodo(String flavor) => 'Check your imports. If you\'re trying to generate code for a ' 'Platform-provided type, you may have to specify a custom `$flavor` ' 'in the associated `@JsonKey` annotation.'; group('fromJson', () { final msg = flavorMessage('fromJson'); final todo = flavorTodo('fromJson'); test('in constructor arguments', () { expectThrows('UnknownCtorParamType', msg, todo); }); test('in fields', () { expectThrows('UnknownFieldType', msg, todo); }); }); group('toJson', () { test('in fields', () { expectThrows('UnknownFieldTypeToJsonOnly', flavorMessage('toJson'), flavorTodo('toJson')); }); }); test('with proper convert methods', () { final output = runForElementNamed('UnknownFieldTypeWithConvert'); expect(output, contains("_everythingIs42(json['number'])")); if (generator.useWrappers) { expect(output, contains('_everythingIs42(_v.number)')); } else { expect(output, contains('_everythingIs42(instance.number)')); } }); }); group('unserializable types', () { final noSupportHelperFyi = 'Could not generate `toJson` code for `watch`.\n' 'None of the provided `TypeHelper` instances support the defined type.'; test('for toJson', () { expectThrows('NoSerializeFieldType', noSupportHelperFyi, 'Make sure all of the types are serializable.'); }); test('for fromJson', () { expectThrows( 'NoDeserializeFieldType', noSupportHelperFyi.replaceFirst('toJson', 'fromJson'), 'Make sure all of the types are serializable.'); }); final mapKeyFyi = 'Could not generate `toJson` code for `intDateTimeMap` ' 'because of type `int`.\nMap keys must be of type ' '`String`, enum, `Object` or `dynamic`.'; test('for toJson in Map key', () { expectThrows('NoSerializeBadKey', mapKeyFyi, 'Make sure all of the types are serializable.'); }); test('for fromJson', () { expectThrows( 'NoDeserializeBadKey', mapKeyFyi.replaceFirst('toJson', 'fromJson'), 'Make sure all of the types are serializable.'); }); }); test('class with final fields', () { final generateResult = runForElementNamed('FinalFields'); expect( generateResult, contains( r'Map _$FinalFieldsToJson(FinalFields instance)')); }); group('valid inputs', () { test('class with fromJson() constructor with optional parameters', () { final output = runForElementNamed('FromJsonOptionalParameters'); expect(output, contains('ChildWithFromJson.fromJson')); }); test('class with child json-able object', () { final output = runForElementNamed('ParentObject'); expect( output, contains("ChildObject.fromJson(json['child'] " 'as Map)')); }); test('class with child json-able object - anyMap', () { final output = _runForElementNamed( JsonSerializable(anyMap: true, useWrappers: generator.useWrappers), 'ParentObject'); expect(output, contains("ChildObject.fromJson(json['child'] as Map)")); }); test('class with child list of json-able objects', () { final output = runForElementNamed('ParentObjectWithChildren'); expect(output, contains('.toList()')); expect(output, contains('ChildObject.fromJson')); }); test('class with child list of dynamic objects is left alone', () { final output = runForElementNamed('ParentObjectWithDynamicChildren'); expect(output, contains('children = json[\'children\'] as List;')); }); test('class with list of int is cast for strong mode', () { final output = runForElementNamed('Person'); expect(output, contains("json['listOfInts'] as List)?.map((e) => e as int)")); }); }); group('includeIfNull', () { test('some', () { final output = runForElementNamed('IncludeIfNullAll'); expect(output, isNot(contains(generatedLocalVarName))); expect(output, isNot(contains(toJsonMapHelperName))); }); }); test('missing default ctor with a factory', () { expect( () => runForElementNamed('NoCtorClass'), _throwsUnsupportedError( 'The class `NoCtorClass` has no default constructor.')); }); } class _ConfigLogger implements TypeHelper { static final configurations = []; const _ConfigLogger(); @override Object deserialize(DartType targetType, String expression, TypeHelperContextWithConfig context) { configurations.add(context.config); return null; } @override Object serialize(DartType targetType, String expression, TypeHelperContextWithConfig context) { configurations.add(context.config); return null; } }