| // Copyright 2013 The Flutter Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| import 'dart:convert'; | |
| import 'package:args/command_runner.dart'; | |
| import 'package:file/file.dart'; | |
| import 'package:flutter_plugin_tools/src/common/core.dart'; | |
| import 'package:flutter_plugin_tools/src/update_dependency_command.dart'; | |
| import 'package:git/git.dart'; | |
| import 'package:http/http.dart' as http; | |
| import 'package:http/testing.dart'; | |
| import 'package:pubspec_parse/pubspec_parse.dart'; | |
| import 'package:test/test.dart'; | |
| import 'mocks.dart'; | |
| import 'util.dart'; | |
| void main() { | |
| late Directory packagesDir; | |
| late RecordingProcessRunner processRunner; | |
| late CommandRunner<void> runner; | |
| Future<http.Response> Function(http.Request request)? mockHttpResponse; | |
| setUp(() { | |
| final GitDir gitDir; | |
| (:packagesDir, :processRunner, gitProcessRunner: _, :gitDir) = configureBaseCommandMocks(); | |
| final command = UpdateDependencyCommand( | |
| packagesDir, | |
| processRunner: processRunner, | |
| gitDir: gitDir, | |
| httpClient: MockClient((http.Request request) => mockHttpResponse!(request)), | |
| ); | |
| runner = CommandRunner<void>( | |
| 'update_dependency_command', | |
| 'Test for update-dependency command.', | |
| ); | |
| runner.addCommand(command); | |
| }); | |
| /// Adds a dummy 'dependencies:' entries for [dependency] to [package]. | |
| void addDependency(RepositoryPackage package, String dependency, {String version = '^1.0.0'}) { | |
| final List<String> lines = package.pubspecFile.readAsLinesSync(); | |
| final int dependenciesStartIndex = lines.indexOf('dependencies:'); | |
| assert(dependenciesStartIndex != -1); | |
| lines.insert(dependenciesStartIndex + 1, ' $dependency: $version'); | |
| package.pubspecFile.writeAsStringSync(lines.join('\n')); | |
| } | |
| /// Adds a 'dev_dependencies:' section with an entry for [dependency] to | |
| /// [package]. | |
| void addDevDependency(RepositoryPackage package, String dependency, {String version = '^1.0.0'}) { | |
| final String originalContent = package.pubspecFile.readAsStringSync(); | |
| package.pubspecFile.writeAsStringSync(''' | |
| $originalContent | |
| dev_dependencies: | |
| $dependency: $version | |
| '''); | |
| } | |
| test('throws if no target is provided', () async { | |
| Error? commandError; | |
| final List<String> output = await runCapturingPrint( | |
| runner, | |
| <String>['update-dependency'], | |
| errorHandler: (Error e) { | |
| commandError = e; | |
| }, | |
| ); | |
| expect(commandError, isA<ToolExit>()); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[contains('Exactly one of the target flags must be provided:')]), | |
| ); | |
| }); | |
| test('throws if multiple dependencies specified', () async { | |
| Error? commandError; | |
| final List<String> output = await runCapturingPrint( | |
| runner, | |
| <String>[ | |
| 'update-dependency', | |
| '--pub-package', | |
| 'target_package', | |
| '--android-dependency', | |
| 'gradle', | |
| ], | |
| errorHandler: (Error e) { | |
| commandError = e; | |
| }, | |
| ); | |
| expect(commandError, isA<ToolExit>()); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[contains('Exactly one of the target flags must be provided:')]), | |
| ); | |
| }); | |
| group('pub dependencies', () { | |
| test('throws if no version is given for an unpublished target', () async { | |
| mockHttpResponse = (http.Request request) async { | |
| return http.Response('', 404); | |
| }; | |
| Error? commandError; | |
| final List<String> output = await runCapturingPrint( | |
| runner, | |
| <String>['update-dependency', '--pub-package', 'target_package'], | |
| errorHandler: (Error e) { | |
| commandError = e; | |
| }, | |
| ); | |
| expect(commandError, isA<ToolExit>()); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[contains('target_package does not exist on pub')]), | |
| ); | |
| }); | |
| test('skips if there is no dependency', () async { | |
| createFakePackage('a_package', packagesDir, examples: <String>[]); | |
| final List<String> output = await runCapturingPrint(runner, <String>[ | |
| 'update-dependency', | |
| '--pub-package', | |
| 'target_package', | |
| '--version', | |
| '1.5.0', | |
| ]); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[contains('SKIPPING: Does not depend on target_package')]), | |
| ); | |
| }); | |
| test('skips if the dependency is already the target version', () async { | |
| final RepositoryPackage package = createFakePackage( | |
| 'a_package', | |
| packagesDir, | |
| examples: <String>[], | |
| ); | |
| addDependency(package, 'target_package', version: '^1.5.0'); | |
| final List<String> output = await runCapturingPrint(runner, <String>[ | |
| 'update-dependency', | |
| '--pub-package', | |
| 'target_package', | |
| '--version', | |
| '1.5.0', | |
| ]); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[contains('SKIPPING: Already depends on ^1.5.0')]), | |
| ); | |
| }); | |
| test('logs updates', () async { | |
| final RepositoryPackage package = createFakePackage( | |
| 'a_package', | |
| packagesDir, | |
| examples: <String>[], | |
| ); | |
| addDependency(package, 'target_package'); | |
| final List<String> output = await runCapturingPrint(runner, <String>[ | |
| 'update-dependency', | |
| '--pub-package', | |
| 'target_package', | |
| '--version', | |
| '1.5.0', | |
| ]); | |
| expect(output, containsAllInOrder(<Matcher>[contains('Updating to "^1.5.0"')])); | |
| }); | |
| test('updates normal dependency', () async { | |
| final RepositoryPackage package = createFakePackage( | |
| 'a_package', | |
| packagesDir, | |
| examples: <String>[], | |
| ); | |
| addDependency(package, 'target_package'); | |
| await runCapturingPrint(runner, <String>[ | |
| 'update-dependency', | |
| '--pub-package', | |
| 'target_package', | |
| '--version', | |
| '1.5.0', | |
| ]); | |
| final Dependency? dep = package.parsePubspec().dependencies['target_package']; | |
| expect(dep, isA<HostedDependency>()); | |
| expect((dep! as HostedDependency).version.toString(), '^1.5.0'); | |
| }); | |
| test('updates dev dependency', () async { | |
| final RepositoryPackage package = createFakePackage( | |
| 'a_package', | |
| packagesDir, | |
| examples: <String>[], | |
| ); | |
| addDevDependency(package, 'target_package'); | |
| await runCapturingPrint(runner, <String>[ | |
| 'update-dependency', | |
| '--pub-package', | |
| 'target_package', | |
| '--version', | |
| '1.5.0', | |
| ]); | |
| final Dependency? dep = package.parsePubspec().devDependencies['target_package']; | |
| expect(dep, isA<HostedDependency>()); | |
| expect((dep! as HostedDependency).version.toString(), '^1.5.0'); | |
| }); | |
| test('updates dependency in example', () async { | |
| final RepositoryPackage package = createFakePackage('a_package', packagesDir); | |
| final RepositoryPackage example = package.getExamples().first; | |
| addDevDependency(example, 'target_package'); | |
| await runCapturingPrint(runner, <String>[ | |
| 'update-dependency', | |
| '--pub-package', | |
| 'target_package', | |
| '--version', | |
| '1.5.0', | |
| ]); | |
| final Dependency? dep = example.parsePubspec().devDependencies['target_package']; | |
| expect(dep, isA<HostedDependency>()); | |
| expect((dep! as HostedDependency).version.toString(), '^1.5.0'); | |
| }); | |
| test('uses provided constraint as-is', () async { | |
| final RepositoryPackage package = createFakePackage( | |
| 'a_package', | |
| packagesDir, | |
| examples: <String>[], | |
| ); | |
| addDependency(package, 'target_package'); | |
| const providedConstraint = '>=1.6.0 <3.0.0'; | |
| await runCapturingPrint(runner, <String>[ | |
| 'update-dependency', | |
| '--pub-package', | |
| 'target_package', | |
| '--version', | |
| providedConstraint, | |
| ]); | |
| final Dependency? dep = package.parsePubspec().dependencies['target_package']; | |
| expect(dep, isA<HostedDependency>()); | |
| expect((dep! as HostedDependency).version.toString(), providedConstraint); | |
| }); | |
| test('uses provided version as lower bound for unpinned', () async { | |
| final RepositoryPackage package = createFakePackage( | |
| 'a_package', | |
| packagesDir, | |
| examples: <String>[], | |
| ); | |
| addDependency(package, 'target_package'); | |
| await runCapturingPrint(runner, <String>[ | |
| 'update-dependency', | |
| '--pub-package', | |
| 'target_package', | |
| '--version', | |
| '1.5.0', | |
| ]); | |
| final Dependency? dep = package.parsePubspec().dependencies['target_package']; | |
| expect(dep, isA<HostedDependency>()); | |
| expect((dep! as HostedDependency).version.toString(), '^1.5.0'); | |
| }); | |
| test('uses provided version as exact version for pinned', () async { | |
| final RepositoryPackage package = createFakePackage( | |
| 'a_package', | |
| packagesDir, | |
| examples: <String>[], | |
| ); | |
| addDependency(package, 'target_package', version: '1.0.0'); | |
| await runCapturingPrint(runner, <String>[ | |
| 'update-dependency', | |
| '--pub-package', | |
| 'target_package', | |
| '--version', | |
| '1.5.0', | |
| ]); | |
| final Dependency? dep = package.parsePubspec().dependencies['target_package']; | |
| expect(dep, isA<HostedDependency>()); | |
| expect((dep! as HostedDependency).version.toString(), '1.5.0'); | |
| }); | |
| test('uses latest pub version as lower bound for unpinned', () async { | |
| final RepositoryPackage package = createFakePackage( | |
| 'a_package', | |
| packagesDir, | |
| examples: <String>[], | |
| ); | |
| addDependency(package, 'target_package'); | |
| const targetPackagePubResponse = <String, dynamic>{ | |
| 'name': 'a', | |
| 'versions': <String>['0.0.1', '1.0.0', '1.5.0'], | |
| }; | |
| mockHttpResponse = (http.Request request) async { | |
| if (request.url.pathSegments.last == 'target_package.json') { | |
| return http.Response(json.encode(targetPackagePubResponse), 200); | |
| } | |
| return http.Response('', 500); | |
| }; | |
| await runCapturingPrint(runner, <String>[ | |
| 'update-dependency', | |
| '--pub-package', | |
| 'target_package', | |
| ]); | |
| final Dependency? dep = package.parsePubspec().dependencies['target_package']; | |
| expect(dep, isA<HostedDependency>()); | |
| expect((dep! as HostedDependency).version.toString(), '^1.5.0'); | |
| }); | |
| test('uses latest pub version as exact version for pinned', () async { | |
| final RepositoryPackage package = createFakePackage( | |
| 'a_package', | |
| packagesDir, | |
| examples: <String>[], | |
| ); | |
| addDependency(package, 'target_package', version: '1.0.0'); | |
| const targetPackagePubResponse = <String, dynamic>{ | |
| 'name': 'a', | |
| 'versions': <String>['0.0.1', '1.0.0', '1.5.0'], | |
| }; | |
| mockHttpResponse = (http.Request request) async { | |
| if (request.url.pathSegments.last == 'target_package.json') { | |
| return http.Response(json.encode(targetPackagePubResponse), 200); | |
| } | |
| return http.Response('', 500); | |
| }; | |
| await runCapturingPrint(runner, <String>[ | |
| 'update-dependency', | |
| '--pub-package', | |
| 'target_package', | |
| ]); | |
| final Dependency? dep = package.parsePubspec().dependencies['target_package']; | |
| expect(dep, isA<HostedDependency>()); | |
| expect((dep! as HostedDependency).version.toString(), '1.5.0'); | |
| }); | |
| test('regenerates all pigeon files when updating pigeon', () async { | |
| final RepositoryPackage package = createFakePackage( | |
| 'a_package', | |
| packagesDir, | |
| extraFiles: <String>['pigeons/foo.dart', 'pigeons/bar.dart'], | |
| ); | |
| addDependency(package, 'pigeon', version: '1.0.0'); | |
| await runCapturingPrint(runner, <String>[ | |
| 'update-dependency', | |
| '--pub-package', | |
| 'pigeon', | |
| '--version', | |
| '1.5.0', | |
| ]); | |
| expect( | |
| processRunner.recordedCalls, | |
| orderedEquals(<ProcessCall>[ | |
| ProcessCall('dart', const <String>['pub', 'get'], package.path), | |
| ProcessCall('dart', const <String>[ | |
| 'run', | |
| 'pigeon', | |
| '--input', | |
| 'pigeons/foo.dart', | |
| ], package.path), | |
| ProcessCall('dart', const <String>[ | |
| 'run', | |
| 'pigeon', | |
| '--input', | |
| 'pigeons/bar.dart', | |
| ], package.path), | |
| ]), | |
| ); | |
| }); | |
| test('warns when regenerating pigeon if there are no pigeon files', () async { | |
| final RepositoryPackage package = createFakePackage('a_package', packagesDir); | |
| addDependency(package, 'pigeon', version: '1.0.0'); | |
| final List<String> output = await runCapturingPrint(runner, <String>[ | |
| 'update-dependency', | |
| '--pub-package', | |
| 'pigeon', | |
| '--version', | |
| '1.5.0', | |
| ]); | |
| expect(output, containsAllInOrder(<Matcher>[contains('No pigeon input files found')])); | |
| }); | |
| test('updating pigeon fails if pub get fails', () async { | |
| final RepositoryPackage package = createFakePackage( | |
| 'a_package', | |
| packagesDir, | |
| extraFiles: <String>['pigeons/foo.dart'], | |
| ); | |
| addDependency(package, 'pigeon', version: '1.0.0'); | |
| processRunner.mockProcessesForExecutable['dart'] = <FakeProcessInfo>[ | |
| FakeProcessInfo(MockProcess(exitCode: 1), <String>['pub', 'get']), | |
| ]; | |
| Error? commandError; | |
| final List<String> output = await runCapturingPrint( | |
| runner, | |
| <String>['update-dependency', '--pub-package', 'pigeon', '--version', '1.5.0'], | |
| errorHandler: (Error e) { | |
| commandError = e; | |
| }, | |
| ); | |
| expect(commandError, isA<ToolExit>()); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[ | |
| contains('Fetching dependencies failed'), | |
| contains('Failed to update pigeon files'), | |
| ]), | |
| ); | |
| }); | |
| test('updating pigeon fails if running pigeon fails', () async { | |
| final RepositoryPackage package = createFakePackage( | |
| 'a_package', | |
| packagesDir, | |
| extraFiles: <String>['pigeons/foo.dart'], | |
| ); | |
| addDependency(package, 'pigeon', version: '1.0.0'); | |
| processRunner.mockProcessesForExecutable['dart'] = <FakeProcessInfo>[ | |
| FakeProcessInfo(MockProcess(), <String>['pub', 'get']), | |
| FakeProcessInfo(MockProcess(exitCode: 1), <String>['run', 'pigeon']), | |
| ]; | |
| Error? commandError; | |
| final List<String> output = await runCapturingPrint( | |
| runner, | |
| <String>['update-dependency', '--pub-package', 'pigeon', '--version', '1.5.0'], | |
| errorHandler: (Error e) { | |
| commandError = e; | |
| }, | |
| ); | |
| expect(commandError, isA<ToolExit>()); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[ | |
| contains('dart run pigeon failed'), | |
| contains('Failed to update pigeon files'), | |
| ]), | |
| ); | |
| }); | |
| test('regenerates mocks when updating mockito if necessary', () async { | |
| final RepositoryPackage package = createFakePackage('a_package', packagesDir); | |
| addDependency(package, 'mockito', version: '1.0.0'); | |
| addDevDependency(package, 'build_runner'); | |
| await runCapturingPrint(runner, <String>[ | |
| 'update-dependency', | |
| '--pub-package', | |
| 'mockito', | |
| '--version', | |
| '1.5.0', | |
| ]); | |
| expect( | |
| processRunner.recordedCalls, | |
| orderedEquals(<ProcessCall>[ | |
| ProcessCall('dart', const <String>['pub', 'get'], package.path), | |
| ProcessCall('dart', const <String>[ | |
| 'run', | |
| 'build_runner', | |
| 'build', | |
| '--delete-conflicting-outputs', | |
| ], package.path), | |
| ]), | |
| ); | |
| }); | |
| test('skips regenerating mocks when there is no build_runner dependency', () async { | |
| final RepositoryPackage package = createFakePackage('a_package', packagesDir); | |
| addDependency(package, 'mockito', version: '1.0.0'); | |
| await runCapturingPrint(runner, <String>[ | |
| 'update-dependency', | |
| '--pub-package', | |
| 'mockito', | |
| '--version', | |
| '1.5.0', | |
| ]); | |
| expect(processRunner.recordedCalls.isEmpty, true); | |
| }); | |
| test('updating mockito fails if pub get fails', () async { | |
| final RepositoryPackage package = createFakePackage('a_package', packagesDir); | |
| addDependency(package, 'mockito', version: '1.0.0'); | |
| addDevDependency(package, 'build_runner'); | |
| processRunner.mockProcessesForExecutable['dart'] = <FakeProcessInfo>[ | |
| FakeProcessInfo(MockProcess(exitCode: 1), <String>['pub', 'get']), | |
| ]; | |
| Error? commandError; | |
| final List<String> output = await runCapturingPrint( | |
| runner, | |
| <String>['update-dependency', '--pub-package', 'mockito', '--version', '1.5.0'], | |
| errorHandler: (Error e) { | |
| commandError = e; | |
| }, | |
| ); | |
| expect(commandError, isA<ToolExit>()); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[ | |
| contains('Fetching dependencies failed'), | |
| contains('Failed to update mocks'), | |
| ]), | |
| ); | |
| }); | |
| test('updating mockito fails if running build_runner fails', () async { | |
| final RepositoryPackage package = createFakePackage('a_package', packagesDir); | |
| addDependency(package, 'mockito', version: '1.0.0'); | |
| addDevDependency(package, 'build_runner'); | |
| processRunner.mockProcessesForExecutable['dart'] = <FakeProcessInfo>[ | |
| FakeProcessInfo(MockProcess(), <String>['pub', 'get']), | |
| FakeProcessInfo(MockProcess(exitCode: 1), <String>['run', 'build_runner']), | |
| ]; | |
| Error? commandError; | |
| final List<String> output = await runCapturingPrint( | |
| runner, | |
| <String>['update-dependency', '--pub-package', 'mockito', '--version', '1.5.0'], | |
| errorHandler: (Error e) { | |
| commandError = e; | |
| }, | |
| ); | |
| expect(commandError, isA<ToolExit>()); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[ | |
| contains('"dart run build_runner build" failed'), | |
| contains('Failed to update mocks'), | |
| ]), | |
| ); | |
| }); | |
| }); | |
| group('Android dependencies', () { | |
| final invalidGradleAgpVersionsFormat = <String>['81', '811.1', '8.123', '8.12.12']; | |
| const invalidGradleAgpVersionError = ''' | |
| A version with a valid format (maximum 2-3 numbers separated by 1-2 periods) must be provided. | |
| 1. The first number must have one or two digits | |
| 2. The second number must have one or two digits | |
| 3. If present, the third number must have a single digit'''; | |
| const invalidKgpVersionError = ''' | |
| A version with a valid format (3 numbers separated by 2 periods) must be provided. | |
| 1. The first number must have one digit | |
| 2. The second number must have one digit | |
| 3. The third number must have one or two digits'''; | |
| group('gradle', () { | |
| for (final gradleVersion in invalidGradleAgpVersionsFormat) { | |
| test('throws because gradleVersion: $gradleVersion is invalid', () async { | |
| Error? commandError; | |
| final List<String> output = await runCapturingPrint( | |
| runner, | |
| <String>[ | |
| 'update-dependency', | |
| '--android-dependency', | |
| 'gradle', | |
| '--version', | |
| gradleVersion, | |
| ], | |
| errorHandler: (Error e) { | |
| commandError = e; | |
| }, | |
| ); | |
| expect(commandError, isA<ToolExit>()); | |
| expect(output, containsAllInOrder(<Matcher>[contains(invalidGradleAgpVersionError)])); | |
| }); | |
| } | |
| test('skips if example app does not run on Android', () async { | |
| final RepositoryPackage package = createFakePlugin('fake_plugin', packagesDir); | |
| final List<String> output = await runCapturingPrint(runner, <String>[ | |
| 'update-dependency', | |
| '--packages', | |
| package.displayName, | |
| '--android-dependency', | |
| 'gradle', | |
| '--version', | |
| '8.8.8', | |
| ]); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[contains('SKIPPING: No example apps run on Android.')]), | |
| ); | |
| }); | |
| test('throws if wrapper does not have distribution URL with expected format', () async { | |
| final RepositoryPackage package = createFakePlugin( | |
| 'fake_plugin', | |
| packagesDir, | |
| extraFiles: <String>['example/android/app/gradle/wrapper/gradle-wrapper.properties'], | |
| ); | |
| final File gradleWrapperPropertiesFile = package.directory | |
| .childDirectory('example') | |
| .childDirectory('android') | |
| .childDirectory('app') | |
| .childDirectory('gradle') | |
| .childDirectory('wrapper') | |
| .childFile('gradle-wrapper.properties'); | |
| gradleWrapperPropertiesFile.writeAsStringSync(''' | |
| How is it even possible that I didn't specify a Gradle distribution? | |
| '''); | |
| Error? commandError; | |
| final List<String> output = await runCapturingPrint( | |
| runner, | |
| <String>[ | |
| 'update-dependency', | |
| '--packages', | |
| package.displayName, | |
| '--android-dependency', | |
| 'gradle', | |
| '--version', | |
| '8.8.8', | |
| ], | |
| errorHandler: (Error e) { | |
| commandError = e; | |
| }, | |
| ); | |
| expect(commandError, isA<ToolExit>()); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[ | |
| contains( | |
| 'Unable to find a gradle version entry to update for ${package.displayName}/example.', | |
| ), | |
| ]), | |
| ); | |
| }); | |
| test('succeeds if example app has android/app/gradle directory structure', () async { | |
| final RepositoryPackage package = createFakePlugin( | |
| 'fake_plugin', | |
| packagesDir, | |
| extraFiles: <String>['example/android/app/gradle/wrapper/gradle-wrapper.properties'], | |
| ); | |
| const newGradleVersion = '8.8.8'; | |
| final File gradleWrapperPropertiesFile = package.directory | |
| .childDirectory('example') | |
| .childDirectory('android') | |
| .childDirectory('app') | |
| .childDirectory('gradle') | |
| .childDirectory('wrapper') | |
| .childFile('gradle-wrapper.properties'); | |
| gradleWrapperPropertiesFile.writeAsStringSync(r''' | |
| distributionBase=GRADLE_USER_HOME | |
| distributionPath=wrapper/dists | |
| zipStoreBase=GRADLE_USER_HOME | |
| zipStorePath=wrapper/dists | |
| distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip | |
| '''); | |
| await runCapturingPrint(runner, <String>[ | |
| 'update-dependency', | |
| '--packages', | |
| package.displayName, | |
| '--android-dependency', | |
| 'gradle', | |
| '--version', | |
| newGradleVersion, | |
| ]); | |
| final String updatedGradleWrapperPropertiesContents = gradleWrapperPropertiesFile | |
| .readAsStringSync(); | |
| expect( | |
| updatedGradleWrapperPropertiesContents, | |
| contains( | |
| r'distributionUrl=https\://services.gradle.org/distributions/' | |
| 'gradle-$newGradleVersion-all.zip', | |
| ), | |
| ); | |
| }); | |
| test('succeeds if example app has android/gradle directory structure', () async { | |
| final RepositoryPackage package = createFakePlugin( | |
| 'fake_plugin', | |
| packagesDir, | |
| extraFiles: <String>['example/android/gradle/wrapper/gradle-wrapper.properties'], | |
| ); | |
| const newGradleVersion = '9.9'; | |
| final File gradleWrapperPropertiesFile = package.directory | |
| .childDirectory('example') | |
| .childDirectory('android') | |
| .childDirectory('gradle') | |
| .childDirectory('wrapper') | |
| .childFile('gradle-wrapper.properties'); | |
| gradleWrapperPropertiesFile.writeAsStringSync(r''' | |
| distributionBase=GRADLE_USER_HOME | |
| distributionPath=wrapper/dists | |
| zipStoreBase=GRADLE_USER_HOME | |
| zipStorePath=wrapper/dists | |
| distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip | |
| '''); | |
| await runCapturingPrint(runner, <String>[ | |
| 'update-dependency', | |
| '--packages', | |
| package.displayName, | |
| '--android-dependency', | |
| 'gradle', | |
| '--version', | |
| newGradleVersion, | |
| ]); | |
| final String updatedGradleWrapperPropertiesContents = gradleWrapperPropertiesFile | |
| .readAsStringSync(); | |
| expect( | |
| updatedGradleWrapperPropertiesContents, | |
| contains( | |
| r'distributionUrl=https\://services.gradle.org/distributions/' | |
| 'gradle-$newGradleVersion-all.zip', | |
| ), | |
| ); | |
| }); | |
| test( | |
| 'succeeds if example app has android/gradle and android/app/gradle directory structure', | |
| () async { | |
| final RepositoryPackage package = createFakePlugin( | |
| 'fake_plugin', | |
| packagesDir, | |
| extraFiles: <String>[ | |
| 'example/android/gradle/wrapper/gradle-wrapper.properties', | |
| 'example/android/app/gradle/wrapper/gradle-wrapper.properties', | |
| ], | |
| ); | |
| const newGradleVersion = '9.9'; | |
| final File gradleWrapperPropertiesFile = package.directory | |
| .childDirectory('example') | |
| .childDirectory('android') | |
| .childDirectory('gradle') | |
| .childDirectory('wrapper') | |
| .childFile('gradle-wrapper.properties'); | |
| final File gradleAppWrapperPropertiesFile = package.directory | |
| .childDirectory('example') | |
| .childDirectory('android') | |
| .childDirectory('app') | |
| .childDirectory('gradle') | |
| .childDirectory('wrapper') | |
| .childFile('gradle-wrapper.properties'); | |
| gradleWrapperPropertiesFile.writeAsStringSync(r''' | |
| distributionBase=GRADLE_USER_HOME | |
| distributionPath=wrapper/dists | |
| zipStoreBase=GRADLE_USER_HOME | |
| zipStorePath=wrapper/dists | |
| distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip | |
| '''); | |
| gradleAppWrapperPropertiesFile.writeAsStringSync(r''' | |
| distributionBase=GRADLE_USER_HOME | |
| distributionPath=wrapper/dists | |
| zipStoreBase=GRADLE_USER_HOME | |
| zipStorePath=wrapper/dists | |
| distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip | |
| '''); | |
| await runCapturingPrint(runner, <String>[ | |
| 'update-dependency', | |
| '--packages', | |
| package.displayName, | |
| '--android-dependency', | |
| 'gradle', | |
| '--version', | |
| newGradleVersion, | |
| ]); | |
| final String updatedGradleWrapperPropertiesContents = gradleWrapperPropertiesFile | |
| .readAsStringSync(); | |
| final String updatedGradleAppWrapperPropertiesContents = gradleAppWrapperPropertiesFile | |
| .readAsStringSync(); | |
| expect( | |
| updatedGradleWrapperPropertiesContents, | |
| contains( | |
| r'distributionUrl=https\://services.gradle.org/distributions/' | |
| 'gradle-$newGradleVersion-all.zip', | |
| ), | |
| ); | |
| expect( | |
| updatedGradleAppWrapperPropertiesContents, | |
| contains( | |
| r'distributionUrl=https\://services.gradle.org/distributions/' | |
| 'gradle-$newGradleVersion-all.zip', | |
| ), | |
| ); | |
| }, | |
| ); | |
| test('succeeds if one example app runs on Android and another does not', () async { | |
| final RepositoryPackage package = createFakePlugin( | |
| 'fake_plugin', | |
| packagesDir, | |
| examples: <String>['example_1', 'example_2'], | |
| extraFiles: <String>[ | |
| 'example/example_2/android/app/gradle/wrapper/gradle-wrapper.properties', | |
| ], | |
| ); | |
| const newGradleVersion = '8.8.8'; | |
| final File gradleWrapperPropertiesFile = package.directory | |
| .childDirectory('example') | |
| .childDirectory('example_2') | |
| .childDirectory('android') | |
| .childDirectory('app') | |
| .childDirectory('gradle') | |
| .childDirectory('wrapper') | |
| .childFile('gradle-wrapper.properties'); | |
| gradleWrapperPropertiesFile.writeAsStringSync(r''' | |
| distributionBase=GRADLE_USER_HOME | |
| distributionPath=wrapper/dists | |
| zipStoreBase=GRADLE_USER_HOME | |
| zipStorePath=wrapper/dists | |
| distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip | |
| '''); | |
| await runCapturingPrint(runner, <String>[ | |
| 'update-dependency', | |
| '--packages', | |
| package.displayName, | |
| '--android-dependency', | |
| 'gradle', | |
| '--version', | |
| newGradleVersion, | |
| ]); | |
| final String updatedGradleWrapperPropertiesContents = gradleWrapperPropertiesFile | |
| .readAsStringSync(); | |
| expect( | |
| updatedGradleWrapperPropertiesContents, | |
| contains( | |
| r'distributionUrl=https\://services.gradle.org/distributions/' | |
| 'gradle-$newGradleVersion-all.zip', | |
| ), | |
| ); | |
| }); | |
| }); | |
| group('agp', () { | |
| for (final agpVersion in invalidGradleAgpVersionsFormat) { | |
| test('throws because agpVersion: $agpVersion is invalid', () async { | |
| Error? commandError; | |
| final List<String> output = await runCapturingPrint( | |
| runner, | |
| <String>[ | |
| 'update-dependency', | |
| '--android-dependency', | |
| 'androidGradlePlugin', | |
| '--version', | |
| agpVersion, | |
| ], | |
| errorHandler: (Error e) { | |
| commandError = e; | |
| }, | |
| ); | |
| expect(commandError, isA<ToolExit>()); | |
| expect(output, containsAllInOrder(<Matcher>[contains(invalidGradleAgpVersionError)])); | |
| }); | |
| } | |
| test('skips if example app does not run on Android', () async { | |
| final RepositoryPackage package = createFakePlugin('fake_plugin', packagesDir); | |
| final List<String> output = await runCapturingPrint(runner, <String>[ | |
| 'update-dependency', | |
| '--packages', | |
| package.displayName, | |
| '--android-dependency', | |
| 'androidGradlePlugin', | |
| '--version', | |
| '8.11.1', | |
| ]); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[contains('SKIPPING: No example apps run on Android.')]), | |
| ); | |
| }); | |
| test('succeeds if example app has android/settings.gradle structure', () async { | |
| final RepositoryPackage package = createFakePlugin( | |
| 'fake_plugin', | |
| packagesDir, | |
| extraFiles: <String>['example/android/settings.gradle'], | |
| ); | |
| const newAgpVersion = '9.9'; | |
| final File gradleSettingsFile = package.directory | |
| .childDirectory('example') | |
| .childDirectory('android') | |
| .childFile('settings.gradle'); | |
| gradleSettingsFile.writeAsStringSync(r''' | |
| ... | |
| plugins { | |
| id "dev.flutter.flutter-plugin-loader" version "1.0.0" | |
| id "com.android.application" version "8.11.1" apply false | |
| ... | |
| } | |
| ... | |
| '''); | |
| await runCapturingPrint(runner, <String>[ | |
| 'update-dependency', | |
| '--packages', | |
| package.displayName, | |
| '--android-dependency', | |
| 'androidGradlePlugin', | |
| '--version', | |
| newAgpVersion, | |
| ]); | |
| final String updatedGradleSettingsContents = gradleSettingsFile.readAsStringSync(); | |
| expect( | |
| updatedGradleSettingsContents, | |
| contains( | |
| r'id "com.android.application" version ' | |
| '"$newAgpVersion" apply false', | |
| ), | |
| ); | |
| }); | |
| test('succeeds if one example app runs on Android and another does not', () async { | |
| final RepositoryPackage package = createFakePlugin( | |
| 'fake_plugin', | |
| packagesDir, | |
| examples: <String>['example_1', 'example_2'], | |
| extraFiles: <String>['example/example_2/android/settings.gradle'], | |
| ); | |
| const newAgpVersion = '9.9'; | |
| final File gradleSettingsFile = package.directory | |
| .childDirectory('example') | |
| .childDirectory('example_2') | |
| .childDirectory('android') | |
| .childFile('settings.gradle'); | |
| gradleSettingsFile.writeAsStringSync(r''' | |
| ... | |
| plugins { | |
| id "dev.flutter.flutter-plugin-loader" version "1.0.0" | |
| id "com.android.application" version "8.11.1" apply false | |
| ... | |
| } | |
| ... | |
| '''); | |
| await runCapturingPrint(runner, <String>[ | |
| 'update-dependency', | |
| '--packages', | |
| package.displayName, | |
| '--android-dependency', | |
| 'androidGradlePlugin', | |
| '--version', | |
| newAgpVersion, | |
| ]); | |
| final String updatedGradleSettingsContents = gradleSettingsFile.readAsStringSync(); | |
| expect( | |
| updatedGradleSettingsContents, | |
| contains( | |
| r'id "com.android.application" version ' | |
| '"$newAgpVersion" apply false', | |
| ), | |
| ); | |
| }); | |
| }); | |
| group('kgp', () { | |
| final invalidKgpVersionsFormat = <String>['81', '81.1', '8.123', '8.12.12', '8.12.1']; | |
| for (final kgpVersion in invalidKgpVersionsFormat) { | |
| test('throws because kgpVersion: $kgpVersion is invalid', () async { | |
| Error? commandError; | |
| final List<String> output = await runCapturingPrint( | |
| runner, | |
| <String>[ | |
| 'update-dependency', | |
| '--android-dependency', | |
| 'kotlinGradlePlugin', | |
| '--version', | |
| kgpVersion, | |
| ], | |
| errorHandler: (Error e) { | |
| commandError = e; | |
| }, | |
| ); | |
| expect(commandError, isA<ToolExit>()); | |
| expect(output, containsAllInOrder(<Matcher>[contains(invalidKgpVersionError)])); | |
| }); | |
| } | |
| test('skips if example app does not run on Android', () async { | |
| final RepositoryPackage package = createFakePlugin('fake_plugin', packagesDir); | |
| final List<String> output = await runCapturingPrint( | |
| runner, | |
| <String>[ | |
| 'update-dependency', | |
| '--packages', | |
| package.displayName, | |
| '--android-dependency', | |
| 'kotlinGradlePlugin', | |
| '--version', | |
| '2.2.20', | |
| ], | |
| errorHandler: (Error e) { | |
| print((e as ToolExit).stackTrace); | |
| }, | |
| ); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[contains('SKIPPING: No example apps run on Android.')]), | |
| ); | |
| }); | |
| test('succeeds if example app has android/settings.gradle structure', () async { | |
| final RepositoryPackage package = createFakePlugin( | |
| 'fake_plugin', | |
| packagesDir, | |
| extraFiles: <String>['example/android/settings.gradle'], | |
| ); | |
| const newKgpVersion = '2.2.20'; | |
| final File gradleSettingsFile = package.directory | |
| .childDirectory('example') | |
| .childDirectory('android') | |
| .childFile('settings.gradle'); | |
| gradleSettingsFile.writeAsStringSync(r''' | |
| ... | |
| plugins { | |
| id "dev.flutter.flutter-plugin-loader" version "1.0.0" | |
| ... | |
| id "org.jetbrains.kotlin.android" version "2.2.20" apply false | |
| ... | |
| } | |
| ... | |
| '''); | |
| await runCapturingPrint(runner, <String>[ | |
| 'update-dependency', | |
| '--packages', | |
| package.displayName, | |
| '--android-dependency', | |
| 'kotlinGradlePlugin', | |
| '--version', | |
| newKgpVersion, | |
| ]); | |
| final String updatedGradleSettingsContents = gradleSettingsFile.readAsStringSync(); | |
| expect( | |
| updatedGradleSettingsContents, | |
| contains( | |
| r' id "org.jetbrains.kotlin.android" version ' | |
| '"$newKgpVersion" apply false', | |
| ), | |
| ); | |
| }); | |
| test('succeeds if one example app runs on Android and another does not', () async { | |
| final RepositoryPackage package = createFakePlugin( | |
| 'fake_plugin', | |
| packagesDir, | |
| examples: <String>['example_1', 'example_2'], | |
| extraFiles: <String>['example/example_2/android/settings.gradle'], | |
| ); | |
| const newKgpVersion = '2.2.20'; | |
| final File gradleSettingsFile = package.directory | |
| .childDirectory('example') | |
| .childDirectory('example_2') | |
| .childDirectory('android') | |
| .childFile('settings.gradle'); | |
| gradleSettingsFile.writeAsStringSync(r''' | |
| ... | |
| plugins { | |
| id "dev.flutter.flutter-plugin-loader" version "1.0.0" | |
| ... | |
| id "org.jetbrains.kotlin.android" version "2.2.20" apply false | |
| ... | |
| } | |
| ... | |
| '''); | |
| await runCapturingPrint(runner, <String>[ | |
| 'update-dependency', | |
| '--packages', | |
| package.displayName, | |
| '--android-dependency', | |
| 'kotlinGradlePlugin', | |
| '--version', | |
| newKgpVersion, | |
| ]); | |
| final String updatedGradleSettingsContents = gradleSettingsFile.readAsStringSync(); | |
| expect( | |
| updatedGradleSettingsContents, | |
| contains( | |
| r' id "org.jetbrains.kotlin.android" version ' | |
| '"$newKgpVersion" apply false', | |
| ), | |
| ); | |
| }); | |
| }); | |
| group('compileSdk/compileSdkForExamples', () { | |
| // Tests if the compileSdk version is updated for the provided | |
| // build.gradle file and new compileSdk version to update to. | |
| Future<void> testCompileSdkVersionUpdated({ | |
| required RepositoryPackage package, | |
| required File buildGradleFile, | |
| required String oldCompileSdkVersion, | |
| required String newCompileSdkVersion, | |
| bool runForExamples = false, | |
| bool checkForDeprecatedCompileSdkVersion = false, | |
| }) async { | |
| buildGradleFile.writeAsStringSync(''' | |
| android { | |
| // Conditional for compatibility with AGP <4.2. | |
| if (project.android.hasProperty("namespace")) { | |
| namespace 'io.flutter.plugins.pathprovider' | |
| } | |
| ${checkForDeprecatedCompileSdkVersion ? 'compileSdkVersion' : 'compileSdk'} $oldCompileSdkVersion | |
| '''); | |
| await runCapturingPrint(runner, <String>[ | |
| 'update-dependency', | |
| '--packages', | |
| package.displayName, | |
| '--android-dependency', | |
| if (runForExamples) 'compileSdkForExamples' else 'compileSdk', | |
| '--version', | |
| newCompileSdkVersion, | |
| ]); | |
| final String updatedBuildGradleContents = buildGradleFile.readAsStringSync(); | |
| // compileSdkVersion is now deprecated, so if the tool finds any | |
| // instances of compileSdk OR compileSdkVersion, it should change it | |
| // to compileSdk. See https://developer.android.com/reference/tools/gradle-api/7.2/com/android/build/api/dsl/CommonExtension#compileSdkVersion(kotlin.Int). | |
| expect(updatedBuildGradleContents, contains('compileSdk $newCompileSdkVersion')); | |
| } | |
| test('throws if version format is invalid for compileSdk', () async { | |
| Error? commandError; | |
| final List<String> output = await runCapturingPrint( | |
| runner, | |
| <String>['update-dependency', '--android-dependency', 'compileSdk', '--version', '834'], | |
| errorHandler: (Error e) { | |
| commandError = e; | |
| }, | |
| ); | |
| expect(commandError, isA<ToolExit>()); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[ | |
| contains('A valid Android SDK version number (1-2 digit numbers) must be provided.'), | |
| ]), | |
| ); | |
| }); | |
| test('throws if version format is invalid for compileSdkForExamples', () async { | |
| Error? commandError; | |
| final List<String> output = await runCapturingPrint( | |
| runner, | |
| <String>[ | |
| 'update-dependency', | |
| '--android-dependency', | |
| 'compileSdkForExamples', | |
| '--version', | |
| '438', | |
| ], | |
| errorHandler: (Error e) { | |
| commandError = e; | |
| }, | |
| ); | |
| expect(commandError, isA<ToolExit>()); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[ | |
| contains('A valid Android SDK version number (1-2 digit numbers) must be provided.'), | |
| ]), | |
| ); | |
| }); | |
| test('skips if plugin does not run on Android', () async { | |
| final RepositoryPackage package = createFakePlugin('fake_plugin', packagesDir); | |
| final List<String> output = await runCapturingPrint(runner, <String>[ | |
| 'update-dependency', | |
| '--packages', | |
| package.displayName, | |
| '--android-dependency', | |
| 'compileSdk', | |
| '--version', | |
| '34', | |
| ]); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[ | |
| contains('SKIPPING: Package ${package.displayName} does not run on Android.'), | |
| ]), | |
| ); | |
| }); | |
| test('skips if plugin example does not run on Android', () async { | |
| final RepositoryPackage package = createFakePlugin('fake_plugin', packagesDir); | |
| final List<String> output = await runCapturingPrint(runner, <String>[ | |
| 'update-dependency', | |
| '--packages', | |
| package.displayName, | |
| '--android-dependency', | |
| 'compileSdkForExamples', | |
| '--version', | |
| '34', | |
| ]); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[contains('SKIPPING: No example apps run on Android.')]), | |
| ); | |
| }); | |
| test( | |
| 'throws if build configuration file does not have compileSdk version with expected format for compileSdk', | |
| () async { | |
| final RepositoryPackage package = createFakePlugin( | |
| 'fake_plugin', | |
| packagesDir, | |
| extraFiles: <String>['android/build.gradle'], | |
| ); | |
| final File buildGradleFile = package.directory | |
| .childDirectory('android') | |
| .childFile('build.gradle'); | |
| buildGradleFile.writeAsStringSync(''' | |
| How is it even possible that I didn't specify a compileSdk version? | |
| '''); | |
| Error? commandError; | |
| final List<String> output = await runCapturingPrint( | |
| runner, | |
| <String>[ | |
| 'update-dependency', | |
| '--packages', | |
| package.displayName, | |
| '--android-dependency', | |
| 'compileSdk', | |
| '--version', | |
| '34', | |
| ], | |
| errorHandler: (Error e) { | |
| commandError = e; | |
| }, | |
| ); | |
| expect(commandError, isA<ToolExit>()); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[ | |
| contains( | |
| 'Unable to find a compileSdk version entry to update for ${package.displayName}.', | |
| ), | |
| ]), | |
| ); | |
| }, | |
| ); | |
| test( | |
| 'throws if build configuration file does not have compileSdk version with expected format for compileSdkForExamples', | |
| () async { | |
| final RepositoryPackage package = createFakePlugin( | |
| 'fake_plugin', | |
| packagesDir, | |
| extraFiles: <String>['example/android/app/build.gradle'], | |
| ); | |
| final File buildGradleFile = package.directory | |
| .childDirectory('example') | |
| .childDirectory('android') | |
| .childDirectory('app') | |
| .childFile('build.gradle'); | |
| buildGradleFile.writeAsStringSync(''' | |
| How is it even possible that I didn't specify a compileSdk version? | |
| '''); | |
| Error? commandError; | |
| final List<String> output = await runCapturingPrint( | |
| runner, | |
| <String>[ | |
| 'update-dependency', | |
| '--packages', | |
| package.displayName, | |
| '--android-dependency', | |
| 'compileSdkForExamples', | |
| '--version', | |
| '34', | |
| ], | |
| errorHandler: (Error e) { | |
| commandError = e; | |
| }, | |
| ); | |
| expect(commandError, isA<ToolExit>()); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[ | |
| contains( | |
| 'Unable to find a compileSdkForExamples version entry to update for ${package.displayName}/example.', | |
| ), | |
| ]), | |
| ); | |
| }, | |
| ); | |
| test( | |
| 'succeeds if plugin runs on Android and valid version is supplied for compileSdkVersion entry', | |
| () async { | |
| final RepositoryPackage package = createFakePlugin( | |
| 'fake_plugin', | |
| packagesDir, | |
| extraFiles: <String>['android/build.gradle', 'example/android/app/build.gradle'], | |
| ); | |
| final File buildGradleFile = package.directory | |
| .childDirectory('android') | |
| .childFile('build.gradle'); | |
| await testCompileSdkVersionUpdated( | |
| package: package, | |
| buildGradleFile: buildGradleFile, | |
| oldCompileSdkVersion: '8', | |
| newCompileSdkVersion: '16', | |
| checkForDeprecatedCompileSdkVersion: true, | |
| ); | |
| }, | |
| ); | |
| test( | |
| 'succeeds if plugin example runs on Android and valid version is supplied for compileSdkVersion entry', | |
| () async { | |
| final RepositoryPackage package = createFakePlugin( | |
| 'fake_plugin', | |
| packagesDir, | |
| extraFiles: <String>['android/build.gradle', 'example/android/app/build.gradle'], | |
| ); | |
| final File exampleBuildGradleFile = package.directory | |
| .childDirectory('example') | |
| .childDirectory('android') | |
| .childDirectory('app') | |
| .childFile('build.gradle'); | |
| await testCompileSdkVersionUpdated( | |
| package: package, | |
| buildGradleFile: exampleBuildGradleFile, | |
| oldCompileSdkVersion: '8', | |
| newCompileSdkVersion: '16', | |
| runForExamples: true, | |
| checkForDeprecatedCompileSdkVersion: true, | |
| ); | |
| }, | |
| ); | |
| test( | |
| 'succeeds if plugin runs on Android and valid version is supplied for compileSdk entry', | |
| () async { | |
| final RepositoryPackage package = createFakePlugin( | |
| 'fake_plugin', | |
| packagesDir, | |
| extraFiles: <String>['android/build.gradle', 'example/android/app/build.gradle'], | |
| ); | |
| final File buildGradleFile = package.directory | |
| .childDirectory('android') | |
| .childFile('build.gradle'); | |
| await testCompileSdkVersionUpdated( | |
| package: package, | |
| buildGradleFile: buildGradleFile, | |
| oldCompileSdkVersion: '8', | |
| newCompileSdkVersion: '16', | |
| ); | |
| }, | |
| ); | |
| test( | |
| 'succeeds if plugin example runs on Android and valid version is supplied for compileSdk entry', | |
| () async { | |
| final RepositoryPackage package = createFakePlugin( | |
| 'fake_plugin', | |
| packagesDir, | |
| extraFiles: <String>['android/build.gradle', 'example/android/app/build.gradle'], | |
| ); | |
| final File exampleBuildGradleFile = package.directory | |
| .childDirectory('example') | |
| .childDirectory('android') | |
| .childDirectory('app') | |
| .childFile('build.gradle'); | |
| await testCompileSdkVersionUpdated( | |
| package: package, | |
| buildGradleFile: exampleBuildGradleFile, | |
| oldCompileSdkVersion: '33', | |
| newCompileSdkVersion: '34', | |
| runForExamples: true, | |
| ); | |
| }, | |
| ); | |
| }); | |
| }); | |
| } |