Files
packages/script/tool/test/update_dependency_command_test.dart
stuartmorgan-g cc3dca67f2 [all] Omit obvious local types (#10511)
Makes the analysis options changes described in
https://github.com/flutter/flutter/issues/178827:
- Adding
[omit_obvious_local_variable_types](https://dart.dev/tools/linter-rules/omit_obvious_local_variable_types)
- Adding
[specify_nonobvious_local_variable_types](https://dart.dev/tools/linter-rules/specify_nonobvious_local_variable_types)
- Adding
[specify_nonobvious_property_types](https://dart.dev/tools/linter-rules/specify_nonobvious_property_types)
- Adding
[type_annotate_public_apis](https://dart.dev/tools/linter-rules/type_annotate_public_apis)
- Removing
[always_specify_types](https://dart.dev/tools/linter-rules/always_specify_types)

After those changes, makes the following repo-wide changes:
- `dart fix --apply` in all packages and in script/tool/
- `dart format` in all packages and in script/tool/
- `update-excerpts` repo tooling command to update excerpts based on the
changes to their sources

Also updates the min Flutter/Dart SDK version to 3.35/3.9 for the
following packages, to avoid `analyze` failures in the `N-2` legacy
analysis run due to what appears to be a 3.9 change in what the Dart
analyzer continues to be an obvious local type in loop iterations:
- go_router
- google_fonts
- google_identity_services_web
- google_maps_flutter_web
- local_auth_platform_interface
- metrics_center
- multicast_dns
- pigeon
- rfw
- shared_preferences
- two_dimensional_scrollables
- vector_graphics_compiler
- mustache_template
- path_parsing

Because this is causing a significant amount of format churn already, I
took this opportunity to update the repository tooling to a min Dart SDK
of 3.8 (the N-2 stable version, so the earliest version we need the
tooling to support) to pick up the new format style, so the amount of
automated formatter change is higher in script/tool/ than in the
packages.

This does contain two manual changes (other than the repo tooling min
version):
-
d700b45c7d
changes `dynamic` to `Object?` in a few places where `dynamic` caused
analyzer warnings under the new rule set.
- Updates the repo tooling to ignore `.dart_tool/` when looking for
unexpected local `analysis_options.yaml` files, to fix issues running
the repo tool's `analyze` command locally based on recent changes in
`dart` behavior.

This does not include any CHANGELOG or version updates; even though we
normally version any changes to production code, mass automated changes
like this aren't worth the churn of releasing. This includes changes to
lib/example/main.dart and to README.md excerpts; while the style changes
will be user-visible on pub.dev, it's fine for those changes to wait for
the next release of each package.

Part of https://github.com/flutter/flutter/issues/178827
2025-11-24 18:26:06 -05:00

1694 lines
50 KiB
Dart

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