// // 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 'package:test/test.dart'; import 'package:build_test/build_test.dart'; import 'package:build/build.dart'; import 'package:cherrypick_generator/module_generator.dart'; import 'package:source_gen/source_gen.dart'; void main() { group('ModuleGenerator Tests', () { setUp(() { // ModuleGenerator setup if needed }); group('Simple Module Generation', () { test('should generate basic module with instance binding', () async { const input = ''' import 'package:cherrypick_annotations/cherrypick_annotations.dart'; import 'package:cherrypick/cherrypick.dart'; part 'test_module.module.cherrypick.g.dart'; @module() abstract class TestModule extends Module { @instance() String testString() => "Hello World"; } '''; const expectedOutput = ''' // dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND part of 'test_module.dart'; // ************************************************************************** // ModuleGenerator // ************************************************************************** final class \$TestModule extends TestModule { @override void builder(Scope currentScope) { bind().toInstance(testString()); } } '''; await _testGeneration(input, expectedOutput); }); test('should generate basic module with provide binding', () async { const input = ''' import 'package:cherrypick_annotations/cherrypick_annotations.dart'; import 'package:cherrypick/cherrypick.dart'; part 'test_module.module.cherrypick.g.dart'; @module() abstract class TestModule extends Module { @provide() String testString() => "Hello World"; } '''; const expectedOutput = ''' // dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND part of 'test_module.dart'; // ************************************************************************** // ModuleGenerator // ************************************************************************** final class \$TestModule extends TestModule { @override void builder(Scope currentScope) { bind().toProvide(() => testString()); } } '''; await _testGeneration(input, expectedOutput); }); }); group('Singleton Bindings', () { test('should generate singleton instance binding', () async { const input = ''' import 'package:cherrypick_annotations/cherrypick_annotations.dart'; import 'package:cherrypick/cherrypick.dart'; part 'test_module.module.cherrypick.g.dart'; @module() abstract class TestModule extends Module { @instance() @singleton() String testString() => "Hello World"; } '''; const expectedOutput = ''' // dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND part of 'test_module.dart'; // ************************************************************************** // ModuleGenerator // ************************************************************************** final class \$TestModule extends TestModule { @override void builder(Scope currentScope) { bind().toInstance(testString()).singleton(); } } '''; await _testGeneration(input, expectedOutput); }); test('should generate singleton provide binding', () async { const input = ''' import 'package:cherrypick_annotations/cherrypick_annotations.dart'; import 'package:cherrypick/cherrypick.dart'; part 'test_module.module.cherrypick.g.dart'; @module() abstract class TestModule extends Module { @provide() @singleton() String testString() => "Hello World"; } '''; const expectedOutput = ''' // dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND part of 'test_module.dart'; // ************************************************************************** // ModuleGenerator // ************************************************************************** final class \$TestModule extends TestModule { @override void builder(Scope currentScope) { bind().toProvide(() => testString()).singleton(); } } '''; await _testGeneration(input, expectedOutput); }); }); group('Named Bindings', () { test('should generate named instance binding', () async { const input = ''' import 'package:cherrypick_annotations/cherrypick_annotations.dart'; import 'package:cherrypick/cherrypick.dart'; part 'test_module.module.cherrypick.g.dart'; @module() abstract class TestModule extends Module { @instance() @named('testName') String testString() => "Hello World"; } '''; const expectedOutput = ''' // dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND part of 'test_module.dart'; // ************************************************************************** // ModuleGenerator // ************************************************************************** final class \$TestModule extends TestModule { @override void builder(Scope currentScope) { bind().toInstance(testString()).withName('testName'); } } '''; await _testGeneration(input, expectedOutput); }); test('should generate named singleton binding', () async { const input = ''' import 'package:cherrypick_annotations/cherrypick_annotations.dart'; import 'package:cherrypick/cherrypick.dart'; part 'test_module.module.cherrypick.g.dart'; @module() abstract class TestModule extends Module { @provide() @singleton() @named('testName') String testString() => "Hello World"; } '''; const expectedOutput = ''' // dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND part of 'test_module.dart'; // ************************************************************************** // ModuleGenerator // ************************************************************************** final class \$TestModule extends TestModule { @override void builder(Scope currentScope) { bind() .toProvide(() => testString()) .withName('testName') .singleton(); } } '''; await _testGeneration(input, expectedOutput); }); }); group('Async Bindings', () { test('should generate async instance binding', () async { const input = ''' import 'package:cherrypick_annotations/cherrypick_annotations.dart'; import 'package:cherrypick/cherrypick.dart'; part 'test_module.module.cherrypick.g.dart'; @module() abstract class TestModule extends Module { @instance() Future testString() async => "Hello World"; } '''; const expectedOutput = ''' // dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND part of 'test_module.dart'; // ************************************************************************** // ModuleGenerator // ************************************************************************** final class \$TestModule extends TestModule { @override void builder(Scope currentScope) { bind().toInstanceAsync(testString()); } } '''; await _testGeneration(input, expectedOutput); }); test('should generate async provide binding', () async { const input = ''' import 'package:cherrypick_annotations/cherrypick_annotations.dart'; import 'package:cherrypick/cherrypick.dart'; part 'test_module.module.cherrypick.g.dart'; @module() abstract class TestModule extends Module { @provide() Future testString() async => "Hello World"; } '''; const expectedOutput = ''' // dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND part of 'test_module.dart'; // ************************************************************************** // ModuleGenerator // ************************************************************************** final class \$TestModule extends TestModule { @override void builder(Scope currentScope) { bind().toProvideAsync(() => testString()); } } '''; await _testGeneration(input, expectedOutput); }); test('should generate async binding with params', () async { const input = ''' import 'package:cherrypick_annotations/cherrypick_annotations.dart'; import 'package:cherrypick/cherrypick.dart'; part 'test_module.module.cherrypick.g.dart'; @module() abstract class TestModule extends Module { @provide() Future testString(@params() dynamic params) async => "Hello \$params"; } '''; const expectedOutput = ''' // dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND part of 'test_module.dart'; // ************************************************************************** // ModuleGenerator // ************************************************************************** final class \$TestModule extends TestModule { @override void builder(Scope currentScope) { bind().toProvideAsyncWithParams((args) => testString(args)); } } '''; await _testGeneration(input, expectedOutput); }); }); group('Dependencies Injection', () { test('should generate binding with injected dependencies', () async { const input = ''' import 'package:cherrypick_annotations/cherrypick_annotations.dart'; import 'package:cherrypick/cherrypick.dart'; part 'test_module.module.cherrypick.g.dart'; class ApiClient {} class Repository {} @module() abstract class TestModule extends Module { @provide() Repository repository(ApiClient client) => Repository(); } '''; const expectedOutput = ''' // dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND part of 'test_module.dart'; // ************************************************************************** // ModuleGenerator // ************************************************************************** final class \$TestModule extends TestModule { @override void builder(Scope currentScope) { bind().toProvide( () => repository(currentScope.resolve()), ); } } '''; await _testGeneration(input, expectedOutput); }); test('should generate binding with named dependencies', () async { const input = ''' import 'package:cherrypick_annotations/cherrypick_annotations.dart'; import 'package:cherrypick/cherrypick.dart'; part 'test_module.module.cherrypick.g.dart'; class ApiClient {} class Repository {} @module() abstract class TestModule extends Module { @provide() Repository repository(@named('api') ApiClient client) => Repository(); } '''; const expectedOutput = ''' // dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND part of 'test_module.dart'; // ************************************************************************** // ModuleGenerator // ************************************************************************** final class \$TestModule extends TestModule { @override void builder(Scope currentScope) { bind().toProvide( () => repository(currentScope.resolve(named: 'api')), ); } } '''; await _testGeneration(input, expectedOutput); }); }); group('Runtime Parameters', () { test('should generate binding with params', () async { const input = ''' import 'package:cherrypick_annotations/cherrypick_annotations.dart'; import 'package:cherrypick/cherrypick.dart'; part 'test_module.module.cherrypick.g.dart'; @module() abstract class TestModule extends Module { @provide() String testString(@params() dynamic params) => "Hello \$params"; } '''; const expectedOutput = ''' // dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND part of 'test_module.dart'; // ************************************************************************** // ModuleGenerator // ************************************************************************** final class \$TestModule extends TestModule { @override void builder(Scope currentScope) { bind().toProvideWithParams((args) => testString(args)); } } '''; await _testGeneration(input, expectedOutput); }); test('should generate async binding with params', () async { const input = ''' import 'package:cherrypick_annotations/cherrypick_annotations.dart'; import 'package:cherrypick/cherrypick.dart'; part 'test_module.module.cherrypick.g.dart'; @module() abstract class TestModule extends Module { @provide() Future testString(@params() dynamic params) async => "Hello \$params"; } '''; const expectedOutput = ''' // dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND part of 'test_module.dart'; // ************************************************************************** // ModuleGenerator // ************************************************************************** final class \$TestModule extends TestModule { @override void builder(Scope currentScope) { bind().toProvideAsyncWithParams((args) => testString(args)); } } '''; await _testGeneration(input, expectedOutput); }); }); group('Complex Scenarios', () { test('should generate module with multiple bindings', () async { const input = ''' import 'package:cherrypick_annotations/cherrypick_annotations.dart'; import 'package:cherrypick/cherrypick.dart'; part 'test_module.module.cherrypick.g.dart'; class ApiClient {} class Repository {} @module() abstract class TestModule extends Module { @instance() @singleton() @named('baseUrl') String baseUrl() => "https://api.example.com"; @provide() @singleton() ApiClient apiClient(@named('baseUrl') String url) => ApiClient(); @provide() Repository repository(ApiClient client) => Repository(); @provide() @named('greeting') String greeting(@params() dynamic name) => "Hello \$name"; } '''; const expectedOutput = ''' // dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND part of 'test_module.dart'; // ************************************************************************** // ModuleGenerator // ************************************************************************** final class \$TestModule extends TestModule { @override void builder(Scope currentScope) { bind().toInstance(baseUrl()).withName('baseUrl').singleton(); bind() .toProvide( () => apiClient(currentScope.resolve(named: 'baseUrl')), ) .singleton(); bind().toProvide( () => repository(currentScope.resolve()), ); bind() .toProvideWithParams((args) => greeting(args)) .withName('greeting'); } } '''; 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_module.module.cherrypick.g.dart'; @module() void notAClass() {} '''; await expectLater( () => _testGeneration(input, ''), throwsA(isA()), ); }); test( 'should throw error for method without @instance or @provide', () async { const input = ''' import 'package:cherrypick_annotations/cherrypick_annotations.dart'; import 'package:cherrypick/cherrypick.dart'; part 'test_module.module.cherrypick.g.dart'; @module() abstract class TestModule extends Module { String testString() => "Hello World"; } '''; await expectLater( () => _testGeneration(input, ''), throwsA(isA()), ); }, ); test('should throw error for @params with @instance', () async { const input = ''' import 'package:cherrypick_annotations/cherrypick_annotations.dart'; import 'package:cherrypick/cherrypick.dart'; part 'test_module.module.cherrypick.g.dart'; @module() abstract class TestModule extends Module { @instance() String testString(@params() dynamic params) => "Hello \$params"; } '''; await expectLater( () => _testGeneration(input, ''), throwsA(isA()), ); }); }); }); } /// Helper function to test code generation Future _testGeneration(String input, String expectedOutput) async { await testBuilder( moduleBuilder(BuilderOptions.empty), {'a|lib/test_module.dart': input}, outputs: {'a|lib/test_module.module.cherrypick.g.dart': expectedOutput}, readerWriter: TestReaderWriter(), ); }