// // Copyright 2021 Sergey Penkovsky (sergey.penkovsky@gmail.com) // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // https://www.apache.org/licenses/LICENSE-2.0 // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // import 'dart:isolate'; import 'package:build/build.dart'; import 'package:build_test/build_test.dart'; import 'package:build_runner_core/build_runner_core.dart'; import 'package:cherrypick_generator/inject_generator.dart'; import 'package:package_config/package_config.dart'; import 'package:source_gen/source_gen.dart'; import 'package:test/test.dart'; void main() { group('InjectGenerator Tests', () { setUp(() { // InjectGenerator setup if needed }); group('Basic Injection', () { test('should generate mixin for simple injection', () async { const input = ''' import 'package:cherrypick_annotations/cherrypick_annotations.dart'; part 'test_widget.inject.cherrypick.g.dart'; class MyService {} @injectable() class TestWidget { @inject() late final MyService service; } '''; const expectedOutput = ''' // GENERATED CODE - DO NOT MODIFY BY HAND // dart format width=80 part of 'test_widget.dart'; // ************************************************************************** // InjectGenerator // ************************************************************************** mixin _\$TestWidget { void _inject(TestWidget instance) { instance.service = CherryPick.openRootScope().resolve(); } } '''; await _testGeneration(input, expectedOutput); }); test('should generate mixin for nullable injection', () async { const input = ''' import 'package:cherrypick_annotations/cherrypick_annotations.dart'; part 'test_widget.inject.cherrypick.g.dart'; class MyService {} @injectable() class TestWidget { @inject() late final MyService? service; } '''; const expectedOutput = ''' // GENERATED CODE - DO NOT MODIFY BY HAND // dart format width=80 part of 'test_widget.dart'; // ************************************************************************** // InjectGenerator // ************************************************************************** mixin _\$TestWidget { void _inject(TestWidget instance) { instance.service = CherryPick.openRootScope().tryResolve(); } } '''; await _testGeneration(input, expectedOutput); }); }); group('Named Injection', () { test('should generate mixin for named injection', () async { const input = ''' import 'package:cherrypick_annotations/cherrypick_annotations.dart'; part 'test_widget.inject.cherrypick.g.dart'; class MyService {} @injectable() class TestWidget { @inject() @named('myService') late final MyService service; } '''; const expectedOutput = ''' // GENERATED CODE - DO NOT MODIFY BY HAND // dart format width=80 part of 'test_widget.dart'; // ************************************************************************** // InjectGenerator // ************************************************************************** mixin _\$TestWidget { void _inject(TestWidget instance) { instance.service = CherryPick.openRootScope().resolve(named: 'myService'); } } '''; await _testGeneration(input, expectedOutput); }); test('should generate mixin for named nullable injection', () async { const input = ''' import 'package:cherrypick_annotations/cherrypick_annotations.dart'; part 'test_widget.inject.cherrypick.g.dart'; class MyService {} @injectable() class TestWidget { @inject() @named('myService') late final MyService? service; } '''; const expectedOutput = ''' // GENERATED CODE - DO NOT MODIFY BY HAND // dart format width=80 part of 'test_widget.dart'; // ************************************************************************** // InjectGenerator // ************************************************************************** mixin _\$TestWidget { void _inject(TestWidget instance) { instance.service = CherryPick.openRootScope().tryResolve(named: 'myService'); } } '''; await _testGeneration(input, expectedOutput); }); }); group('Scoped Injection', () { test('should generate mixin for scoped injection', () async { const input = ''' import 'package:cherrypick_annotations/cherrypick_annotations.dart'; part 'test_widget.inject.cherrypick.g.dart'; class MyService {} @injectable() class TestWidget { @inject() @scope('userScope') late final MyService service; } '''; const expectedOutput = ''' // GENERATED CODE - DO NOT MODIFY BY HAND // dart format width=80 part of 'test_widget.dart'; // ************************************************************************** // InjectGenerator // ************************************************************************** mixin _\$TestWidget { void _inject(TestWidget instance) { instance.service = CherryPick.openScope(scopeName: 'userScope').resolve(); } } '''; await _testGeneration(input, expectedOutput); }); test('should generate mixin for scoped named injection', () async { const input = ''' import 'package:cherrypick_annotations/cherrypick_annotations.dart'; part 'test_widget.inject.cherrypick.g.dart'; class MyService {} @injectable() class TestWidget { @inject() @scope('userScope') @named('myService') late final MyService service; } '''; const expectedOutput = ''' // GENERATED CODE - DO NOT MODIFY BY HAND // dart format width=80 part of 'test_widget.dart'; // ************************************************************************** // InjectGenerator // ************************************************************************** mixin _\$TestWidget { void _inject(TestWidget instance) { instance.service = CherryPick.openScope(scopeName: 'userScope') .resolve(named: 'myService'); } } '''; await _testGeneration(input, expectedOutput); }); }); group('Async Injection', () { test('should generate mixin for Future injection', () async { const input = ''' import 'package:cherrypick_annotations/cherrypick_annotations.dart'; part 'test_widget.inject.cherrypick.g.dart'; class MyService {} @injectable() class TestWidget { @inject() late final Future service; } '''; const expectedOutput = ''' // GENERATED CODE - DO NOT MODIFY BY HAND // dart format width=80 part of 'test_widget.dart'; // ************************************************************************** // InjectGenerator // ************************************************************************** mixin _\$TestWidget { void _inject(TestWidget instance) { instance.service = CherryPick.openRootScope().resolveAsync(); } } '''; await _testGeneration(input, expectedOutput); }); test('should generate mixin for nullable Future injection', () async { const input = ''' import 'package:cherrypick_annotations/cherrypick_annotations.dart'; part 'test_widget.inject.cherrypick.g.dart'; class MyService {} @injectable() class TestWidget { @inject() late final Future service; } '''; const expectedOutput = ''' // GENERATED CODE - DO NOT MODIFY BY HAND // dart format width=80 part of 'test_widget.dart'; // ************************************************************************** // InjectGenerator // ************************************************************************** mixin _\$TestWidget { void _inject(TestWidget instance) { instance.service = CherryPick.openRootScope().tryResolveAsync(); } } '''; await _testGeneration(input, expectedOutput); }); test('should generate mixin for named Future injection', () async { const input = ''' import 'package:cherrypick_annotations/cherrypick_annotations.dart'; part 'test_widget.inject.cherrypick.g.dart'; class MyService {} @injectable() class TestWidget { @inject() @named('myService') late final Future service; } '''; const expectedOutput = ''' // GENERATED CODE - DO NOT MODIFY BY HAND // dart format width=80 part of 'test_widget.dart'; // ************************************************************************** // InjectGenerator // ************************************************************************** mixin _\$TestWidget { void _inject(TestWidget instance) { instance.service = CherryPick.openRootScope().resolveAsync(named: 'myService'); } } '''; await _testGeneration(input, expectedOutput); }); }); group('Multiple Fields', () { test('should generate mixin for multiple injected fields', () async { const input = ''' import 'package:cherrypick_annotations/cherrypick_annotations.dart'; part 'test_widget.inject.cherrypick.g.dart'; class ApiService {} class DatabaseService {} class CacheService {} @injectable() class TestWidget { @inject() late final ApiService apiService; @inject() @named('cache') late final CacheService? cacheService; @inject() @scope('dbScope') late final Future dbService; // Non-injected field should be ignored String nonInjectedField = "test"; } '''; const expectedOutput = ''' // GENERATED CODE - DO NOT MODIFY BY HAND // dart format width=80 part of 'test_widget.dart'; // ************************************************************************** // InjectGenerator // ************************************************************************** mixin _\$TestWidget { void _inject(TestWidget instance) { instance.apiService = CherryPick.openRootScope().resolve(); instance.cacheService = CherryPick.openRootScope().tryResolve(named: 'cache'); instance.dbService = CherryPick.openScope(scopeName: 'dbScope') .resolveAsync(); } } '''; await _testGeneration(input, expectedOutput); }); }); group('Complex Types', () { test('should handle generic types', () async { const input = ''' import 'package:cherrypick_annotations/cherrypick_annotations.dart'; part 'test_widget.inject.cherrypick.g.dart'; @injectable() class TestWidget { @inject() late final List stringList; @inject() late final Map stringIntMap; @inject() late final Future> futureStringList; } '''; const expectedOutput = ''' // GENERATED CODE - DO NOT MODIFY BY HAND // dart format width=80 part of 'test_widget.dart'; // ************************************************************************** // InjectGenerator // ************************************************************************** mixin _\$TestWidget { void _inject(TestWidget instance) { instance.stringList = CherryPick.openRootScope().resolve>(); instance.stringIntMap = CherryPick.openRootScope().resolve>(); instance.futureStringList = CherryPick.openRootScope().resolveAsync>(); } } '''; await _testGeneration(input, expectedOutput); }); }); group('Error Cases', () { test('should throw error for non-class element', () async { const input = ''' import 'package:cherrypick_annotations/cherrypick_annotations.dart'; part 'test_widget.inject.cherrypick.g.dart'; @injectable() void notAClass() {} '''; await expectLater( () => _testGeneration(input, ''), throwsA(isA()), ); }); test( 'should generate empty mixin for class without @inject fields', () async { const input = ''' import 'package:cherrypick_annotations/cherrypick_annotations.dart'; part 'test_widget.inject.cherrypick.g.dart'; @injectable() class TestWidget { String normalField = "test"; int anotherField = 42; } '''; const expectedOutput = ''' // GENERATED CODE - DO NOT MODIFY BY HAND // dart format width=80 part of 'test_widget.dart'; // ************************************************************************** // InjectGenerator // ************************************************************************** mixin _\$TestWidget { void _inject(TestWidget instance) {} } '''; await _testGeneration(input, expectedOutput); }, ); }); group('Edge Cases', () { test('should handle empty scope name', () async { const input = ''' import 'package:cherrypick_annotations/cherrypick_annotations.dart'; part 'test_widget.inject.cherrypick.g.dart'; class MyService {} @injectable() class TestWidget { @inject() @scope('') late final MyService service; } '''; const expectedOutput = ''' // GENERATED CODE - DO NOT MODIFY BY HAND // dart format width=80 part of 'test_widget.dart'; // ************************************************************************** // InjectGenerator // ************************************************************************** mixin _\$TestWidget { void _inject(TestWidget instance) { instance.service = CherryPick.openRootScope().resolve(); } } '''; await _testGeneration(input, expectedOutput); }); test('should handle empty named value', () async { const input = ''' import 'package:cherrypick_annotations/cherrypick_annotations.dart'; part 'test_widget.inject.cherrypick.g.dart'; class MyService {} @injectable() class TestWidget { @inject() @named('') late final MyService service; } '''; const expectedOutput = ''' // GENERATED CODE - DO NOT MODIFY BY HAND // dart format width=80 part of 'test_widget.dart'; // ************************************************************************** // InjectGenerator // ************************************************************************** mixin _\$TestWidget { void _inject(TestWidget instance) { instance.service = CherryPick.openRootScope().resolve(); } } '''; await _testGeneration(input, expectedOutput); }); }); }); } /// Helper function to test code generation Future _testGeneration(String input, String expectedOutput) async { final readerWriter = TestReaderWriter(rootPackage: 'a'); await readerWriter.testing.loadIsolateSources(); final packageConfig = await loadPackageConfigUri( (await Isolate.packageConfig)!, ); final outputs = expectedOutput.isEmpty ? null : { 'a|lib/test_widget.inject.cherrypick.g.dart': decodedMatches(_normalizedEquals(expectedOutput)), }; final result = await testBuilder( injectBuilder(BuilderOptions.empty), {'a|lib/test_widget.dart': input}, outputs: outputs, readerWriter: readerWriter, rootPackage: 'a', packageConfig: packageConfig, ); if (expectedOutput.isEmpty && result.buildResult.status == BuildStatus.failure) { throw InvalidGenerationSourceError('Build failed'); } } Matcher _normalizedEquals(String expected) { return predicate( (actual) => _normalize(actual) == _normalize(expected), 'matches after normalization', ); } String _normalize(String input) { return input .replaceAll(RegExp(r'\s+'), '') .replaceAll(RegExp(r',\)'), ')') .replaceAll(RegExp(r',\]'), ']') .replaceAll(RegExp(r',\}'), '}'); }