| // 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, |
| ); |
| }, |
| ); |
| }); |
| }); |
| } |