// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/make_deps_path_based_command.dart'; import 'package:mockito/mockito.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; import 'package:test/test.dart'; import 'common/package_command_test.mocks.dart'; import 'mocks.dart'; import 'util.dart'; void main() { FileSystem fileSystem; late Directory packagesDir; late Directory thirdPartyPackagesDir; late CommandRunner runner; late RecordingProcessRunner processRunner; setUp(() { fileSystem = MemoryFileSystem(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); thirdPartyPackagesDir = packagesDir.parent .childDirectory('third_party') .childDirectory('packages'); final MockGitDir gitDir = MockGitDir(); when(gitDir.path).thenReturn(packagesDir.parent.path); when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) .thenAnswer((Invocation invocation) { final List arguments = invocation.positionalArguments[0]! as List; // Route git calls through the process runner, to make mock output // consistent with other processes. Attach the first argument to the // command to make targeting the mock results easier. final String gitCommand = arguments.removeAt(0); return processRunner.run('git-$gitCommand', arguments); }); processRunner = RecordingProcessRunner(); final MakeDepsPathBasedCommand command = MakeDepsPathBasedCommand(packagesDir, gitDir: gitDir); runner = CommandRunner( 'make-deps-path-based_command', 'Test for $MakeDepsPathBasedCommand'); runner.addCommand(command); }); /// Adds dummy 'dependencies:' entries for each package in [dependencies] /// to [package]. void addDependencies(RepositoryPackage package, Iterable dependencies, {String constraint = '<2.0.0'}) { final List lines = package.pubspecFile.readAsLinesSync(); final int dependenciesStartIndex = lines.indexOf('dependencies:'); assert(dependenciesStartIndex != -1); lines.insertAll(dependenciesStartIndex + 1, [ for (final String dependency in dependencies) ' $dependency: $constraint', ]); package.pubspecFile.writeAsStringSync(lines.join('\n')); } /// Adds a 'dev_dependencies:' section with entries for each package in /// [dependencies] to [package]. void addDevDependenciesSection( RepositoryPackage package, Iterable devDependencies, {String constraint = '<2.0.0'}) { final String originalContent = package.pubspecFile.readAsStringSync(); package.pubspecFile.writeAsStringSync(''' $originalContent dev_dependencies: ${devDependencies.map((String dep) => ' $dep: $constraint').join('\n')} '''); } Map getDependencyOverrides(RepositoryPackage package) { final Pubspec pubspec = package.parsePubspec(); return pubspec.dependencyOverrides.map((String name, Dependency dep) => MapEntry( name, (dep is PathDependency) ? dep.path : dep.toString())); } test('no-ops for no plugins', () async { createFakePackage('foo', packagesDir, isFlutter: true); final RepositoryPackage packageBar = createFakePackage('bar', packagesDir, isFlutter: true); addDependencies(packageBar, ['foo']); final String originalPubspecContents = packageBar.pubspecFile.readAsStringSync(); final List output = await runCapturingPrint(runner, ['make-deps-path-based']); expect( output, containsAllInOrder([ contains('No target dependencies'), ]), ); // The 'foo' reference should not have been modified. expect(packageBar.pubspecFile.readAsStringSync(), originalPubspecContents); }); test('includes explanatory comment', () async { final RepositoryPackage packageA = createFakePackage('package_a', packagesDir, isFlutter: true); createFakePackage('package_b', packagesDir, isFlutter: true); addDependencies(packageA, [ 'package_b', ]); await runCapturingPrint(runner, ['make-deps-path-based', '--target-dependencies=package_b']); expect( packageA.pubspecFile.readAsLinesSync(), containsAllInOrder([ '# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE.', '# See https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#changing-federated-plugins', 'dependency_overrides:', ])); }); test('rewrites "dependencies" references', () async { final RepositoryPackage simplePackage = createFakePackage('foo', packagesDir, isFlutter: true); final Directory pluginGroup = packagesDir.childDirectory('bar'); createFakePackage('bar_platform_interface', pluginGroup, isFlutter: true); final RepositoryPackage pluginImplementation = createFakePlugin('bar_android', pluginGroup); final RepositoryPackage pluginAppFacing = createFakePlugin('bar', pluginGroup); addDependencies(simplePackage, [ 'bar', 'bar_android', 'bar_platform_interface', ]); addDependencies(pluginAppFacing, [ 'bar_platform_interface', 'bar_android', ]); addDependencies(pluginImplementation, [ 'bar_platform_interface', ]); final List output = await runCapturingPrint(runner, [ 'make-deps-path-based', '--target-dependencies=bar,bar_platform_interface' ]); expect( output, containsAll([ 'Rewriting references to: bar, bar_platform_interface...', ' Modified packages/bar/bar/pubspec.yaml', ' Modified packages/bar/bar_android/pubspec.yaml', ' Modified packages/foo/pubspec.yaml', ])); expect( output, isNot(contains( ' Modified packages/bar/bar_platform_interface/pubspec.yaml'))); final Map simplePackageOverrides = getDependencyOverrides(simplePackage); expect(simplePackageOverrides.length, 2); expect(simplePackageOverrides['bar'], '../bar/bar'); expect(simplePackageOverrides['bar_platform_interface'], '../bar/bar_platform_interface'); final Map appFacingPackageOverrides = getDependencyOverrides(pluginAppFacing); expect(appFacingPackageOverrides.length, 1); expect(appFacingPackageOverrides['bar_platform_interface'], '../../bar/bar_platform_interface'); }); test('rewrites "dev_dependencies" references', () async { createFakePackage('foo', packagesDir); final RepositoryPackage builderPackage = createFakePackage('foo_builder', packagesDir); addDevDependenciesSection(builderPackage, [ 'foo', ]); final List output = await runCapturingPrint( runner, ['make-deps-path-based', '--target-dependencies=foo']); expect( output, containsAll([ 'Rewriting references to: foo...', ' Modified packages/foo_builder/pubspec.yaml', ])); final Map overrides = getDependencyOverrides(builderPackage); expect(overrides.length, 1); expect(overrides['foo'], '../foo'); }); test('rewrites examples when rewriting the main package', () async { final Directory pluginGroup = packagesDir.childDirectory('bar'); createFakePackage('bar_platform_interface', pluginGroup, isFlutter: true); final RepositoryPackage pluginImplementation = createFakePlugin('bar_android', pluginGroup); final RepositoryPackage pluginAppFacing = createFakePlugin('bar', pluginGroup); addDependencies(pluginAppFacing, [ 'bar_platform_interface', 'bar_android', ]); addDependencies(pluginImplementation, [ 'bar_platform_interface', ]); await runCapturingPrint(runner, ['make-deps-path-based', '--target-dependencies=bar_android']); final Map exampleOverrides = getDependencyOverrides(pluginAppFacing.getExamples().first); expect(exampleOverrides.length, 1); expect(exampleOverrides['bar_android'], '../../../bar/bar_android'); }); test('example overrides include both local and main-package dependencies', () async { final Directory pluginGroup = packagesDir.childDirectory('bar'); createFakePackage('bar_platform_interface', pluginGroup, isFlutter: true); createFakePlugin('bar_android', pluginGroup); final RepositoryPackage pluginAppFacing = createFakePlugin('bar', pluginGroup); createFakePackage('another_package', packagesDir); addDependencies(pluginAppFacing, [ 'bar_platform_interface', 'bar_android', ]); addDependencies(pluginAppFacing.getExamples().first, [ 'another_package', ]); await runCapturingPrint(runner, [ 'make-deps-path-based', '--target-dependencies=bar_android,another_package' ]); final Map exampleOverrides = getDependencyOverrides(pluginAppFacing.getExamples().first); expect(exampleOverrides.length, 2); expect(exampleOverrides['another_package'], '../../../another_package'); expect(exampleOverrides['bar_android'], '../../../bar/bar_android'); }); test( 'alphabetizes overrides from different sections to avoid lint warnings in analysis', () async { createFakePackage('a', packagesDir); createFakePackage('b', packagesDir); createFakePackage('c', packagesDir); final RepositoryPackage targetPackage = createFakePackage('target', packagesDir); addDependencies(targetPackage, ['a', 'c']); addDevDependenciesSection(targetPackage, ['b']); final List output = await runCapturingPrint(runner, ['make-deps-path-based', '--target-dependencies=c,a,b']); expect( output, containsAllInOrder([ 'Rewriting references to: c, a, b...', ' Modified packages/target/pubspec.yaml', ])); // This matches with a regex in order to all for either flow style or // expanded style output. expect( targetPackage.pubspecFile.readAsStringSync(), matches(RegExp(r'dependency_overrides:.*a:.*b:.*c:.*', multiLine: true, dotAll: true))); }); test('finds third_party packages', () async { createFakePackage('bar', thirdPartyPackagesDir, isFlutter: true); final RepositoryPackage firstPartyPackge = createFakePlugin('foo', packagesDir); addDependencies(firstPartyPackge, [ 'bar', ]); final List output = await runCapturingPrint( runner, ['make-deps-path-based', '--target-dependencies=bar']); expect( output, containsAll([ 'Rewriting references to: bar...', ' Modified packages/foo/pubspec.yaml', ])); final Map simplePackageOverrides = getDependencyOverrides(firstPartyPackge); expect(simplePackageOverrides.length, 1); expect(simplePackageOverrides['bar'], '../../third_party/packages/bar'); }); // This test case ensures that running CI using this command on an interim // PR that itself used this command won't fail on the rewrite step. test('running a second time no-ops without failing', () async { final RepositoryPackage simplePackage = createFakePackage('foo', packagesDir, isFlutter: true); final Directory pluginGroup = packagesDir.childDirectory('bar'); createFakePackage('bar_platform_interface', pluginGroup, isFlutter: true); final RepositoryPackage pluginImplementation = createFakePlugin('bar_android', pluginGroup); final RepositoryPackage pluginAppFacing = createFakePlugin('bar', pluginGroup); addDependencies(simplePackage, [ 'bar', 'bar_android', 'bar_platform_interface', ]); addDependencies(pluginAppFacing, [ 'bar_platform_interface', 'bar_android', ]); addDependencies(pluginImplementation, [ 'bar_platform_interface', ]); await runCapturingPrint(runner, [ 'make-deps-path-based', '--target-dependencies=bar,bar_platform_interface' ]); final String simplePackageUpdatedContent = simplePackage.pubspecFile.readAsStringSync(); final String appFacingPackageUpdatedContent = pluginAppFacing.pubspecFile.readAsStringSync(); final String implementationPackageUpdatedContent = pluginImplementation.pubspecFile.readAsStringSync(); final List output = await runCapturingPrint(runner, [ 'make-deps-path-based', '--target-dependencies=bar,bar_platform_interface' ]); expect( output, containsAll([ 'Rewriting references to: bar, bar_platform_interface...', ' Modified packages/bar/bar/pubspec.yaml', ' Modified packages/bar/bar_android/pubspec.yaml', ' Modified packages/foo/pubspec.yaml', ])); expect(simplePackageUpdatedContent, simplePackage.pubspecFile.readAsStringSync()); expect(appFacingPackageUpdatedContent, pluginAppFacing.pubspecFile.readAsStringSync()); expect(implementationPackageUpdatedContent, pluginImplementation.pubspecFile.readAsStringSync()); }); group('target-dependencies-with-non-breaking-updates', () { test('no-ops for no published changes', () async { final RepositoryPackage package = createFakePackage('foo', packagesDir); final String changedFileOutput = [ package.pubspecFile, ].map((File file) => file.path).join('\n'); processRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo(MockProcess(stdout: changedFileOutput)), ]; // Simulate no change to the version in the interface's pubspec.yaml. processRunner.mockProcessesForExecutable['git-show'] = [ FakeProcessInfo( MockProcess(stdout: package.pubspecFile.readAsStringSync())), ]; final List output = await runCapturingPrint(runner, [ 'make-deps-path-based', '--target-dependencies-with-non-breaking-updates' ]); expect( output, containsAllInOrder([ contains('No target dependencies'), ]), ); }); test('no-ops for no deleted packages', () async { final String changedFileOutput = [ // A change for a file that's not on disk simulates a deletion. packagesDir.childDirectory('foo').childFile('pubspec.yaml'), ].map((File file) => file.path).join('\n'); processRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo(MockProcess(stdout: changedFileOutput)), ]; final List output = await runCapturingPrint(runner, [ 'make-deps-path-based', '--target-dependencies-with-non-breaking-updates' ]); expect( output, containsAllInOrder([ contains('Skipping foo; deleted.'), contains('No target dependencies'), ]), ); }); test('includes bugfix version changes as targets', () async { const String newVersion = '1.0.1'; final RepositoryPackage package = createFakePackage('foo', packagesDir, version: newVersion); final File pubspecFile = package.pubspecFile; final String changedFileOutput = [ pubspecFile, ].map((File file) => file.path).join('\n'); processRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo(MockProcess(stdout: changedFileOutput)), ]; final String gitPubspecContents = pubspecFile.readAsStringSync().replaceAll(newVersion, '1.0.0'); // Simulate no change to the version in the interface's pubspec.yaml. processRunner.mockProcessesForExecutable['git-show'] = [ FakeProcessInfo(MockProcess(stdout: gitPubspecContents)), ]; final List output = await runCapturingPrint(runner, [ 'make-deps-path-based', '--target-dependencies-with-non-breaking-updates' ]); expect( output, containsAllInOrder([ contains('Rewriting references to: foo...'), ]), ); }); test('includes minor version changes to 1.0+ as targets', () async { const String newVersion = '1.1.0'; final RepositoryPackage package = createFakePackage('foo', packagesDir, version: newVersion); final File pubspecFile = package.pubspecFile; final String changedFileOutput = [ pubspecFile, ].map((File file) => file.path).join('\n'); processRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo(MockProcess(stdout: changedFileOutput)), ]; final String gitPubspecContents = pubspecFile.readAsStringSync().replaceAll(newVersion, '1.0.0'); // Simulate no change to the version in the interface's pubspec.yaml. processRunner.mockProcessesForExecutable['git-show'] = [ FakeProcessInfo(MockProcess(stdout: gitPubspecContents)), ]; final List output = await runCapturingPrint(runner, [ 'make-deps-path-based', '--target-dependencies-with-non-breaking-updates' ]); expect( output, containsAllInOrder([ contains('Rewriting references to: foo...'), ]), ); }); test('does not include major version changes as targets', () async { const String newVersion = '2.0.0'; final RepositoryPackage package = createFakePackage('foo', packagesDir, version: newVersion); final File pubspecFile = package.pubspecFile; final String changedFileOutput = [ pubspecFile, ].map((File file) => file.path).join('\n'); processRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo(MockProcess(stdout: changedFileOutput)), ]; final String gitPubspecContents = pubspecFile.readAsStringSync().replaceAll(newVersion, '1.0.0'); // Simulate no change to the version in the interface's pubspec.yaml. processRunner.mockProcessesForExecutable['git-show'] = [ FakeProcessInfo(MockProcess(stdout: gitPubspecContents)), ]; final List output = await runCapturingPrint(runner, [ 'make-deps-path-based', '--target-dependencies-with-non-breaking-updates' ]); expect( output, containsAllInOrder([ contains('No target dependencies'), ]), ); }); test('does not include minor version changes to 0.x as targets', () async { const String newVersion = '0.8.0'; final RepositoryPackage package = createFakePackage('foo', packagesDir, version: newVersion); final File pubspecFile = package.pubspecFile; final String changedFileOutput = [ pubspecFile, ].map((File file) => file.path).join('\n'); processRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo(MockProcess(stdout: changedFileOutput)), ]; final String gitPubspecContents = pubspecFile.readAsStringSync().replaceAll(newVersion, '0.7.0'); // Simulate no change to the version in the interface's pubspec.yaml. processRunner.mockProcessesForExecutable['git-show'] = [ FakeProcessInfo(MockProcess(stdout: gitPubspecContents)), ]; final List output = await runCapturingPrint(runner, [ 'make-deps-path-based', '--target-dependencies-with-non-breaking-updates' ]); expect( output, containsAllInOrder([ contains('No target dependencies'), ]), ); }); test('does not update references with an older major version', () async { const String newVersion = '2.0.1'; final RepositoryPackage targetPackage = createFakePackage('foo', packagesDir, version: newVersion); final RepositoryPackage referencingPackage = createFakePackage('bar', packagesDir); // For a dependency on ^1.0.0, the 2.0.0->2.0.1 update should not apply. addDependencies(referencingPackage, ['foo'], constraint: '^1.0.0'); final File pubspecFile = targetPackage.pubspecFile; final String changedFileOutput = [ pubspecFile, ].map((File file) => file.path).join('\n'); processRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo(MockProcess(stdout: changedFileOutput)), ]; final String gitPubspecContents = pubspecFile.readAsStringSync().replaceAll(newVersion, '2.0.0'); processRunner.mockProcessesForExecutable['git-show'] = [ FakeProcessInfo(MockProcess(stdout: gitPubspecContents)), ]; final List output = await runCapturingPrint(runner, [ 'make-deps-path-based', '--target-dependencies-with-non-breaking-updates' ]); final Pubspec referencingPubspec = referencingPackage.parsePubspec(); expect( output, containsAllInOrder([ contains('Rewriting references to: foo'), ]), ); expect(referencingPubspec.dependencyOverrides.isEmpty, true); }); test('does update references with a matching version range', () async { const String newVersion = '2.0.1'; final RepositoryPackage targetPackage = createFakePackage('foo', packagesDir, version: newVersion); final RepositoryPackage referencingPackage = createFakePackage('bar', packagesDir); // For a dependency on ^1.0.0, the 2.0.0->2.0.1 update should not apply. addDependencies(referencingPackage, ['foo'], constraint: '^2.0.0'); final File pubspecFile = targetPackage.pubspecFile; final String changedFileOutput = [ pubspecFile, ].map((File file) => file.path).join('\n'); processRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo(MockProcess(stdout: changedFileOutput)), ]; final String gitPubspecContents = pubspecFile.readAsStringSync().replaceAll(newVersion, '2.0.0'); processRunner.mockProcessesForExecutable['git-show'] = [ FakeProcessInfo(MockProcess(stdout: gitPubspecContents)), ]; final List output = await runCapturingPrint(runner, [ 'make-deps-path-based', '--target-dependencies-with-non-breaking-updates' ]); final Pubspec referencingPubspec = referencingPackage.parsePubspec(); expect( output, containsAllInOrder([ contains('Rewriting references to: foo'), ]), ); expect(referencingPubspec.dependencyOverrides['foo'] is PathDependency, true); }); test('skips anything outside of the packages directory', () async { final Directory toolDir = packagesDir.parent.childDirectory('tool'); const String newVersion = '1.1.0'; final RepositoryPackage package = createFakePackage( 'flutter_plugin_tools', toolDir, version: newVersion); // Simulate a minor version change so it would be a target. final File pubspecFile = package.pubspecFile; final String changedFileOutput = [ pubspecFile, ].map((File file) => file.path).join('\n'); processRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo(MockProcess(stdout: changedFileOutput)), ]; final String gitPubspecContents = pubspecFile.readAsStringSync().replaceAll(newVersion, '1.0.0'); processRunner.mockProcessesForExecutable['git-show'] = [ FakeProcessInfo(MockProcess(stdout: gitPubspecContents)), ]; final List output = await runCapturingPrint(runner, [ 'make-deps-path-based', '--target-dependencies-with-non-breaking-updates' ]); expect( output, containsAllInOrder([ contains( 'Skipping /tool/flutter_plugin_tools/pubspec.yaml; not in packages directory.'), contains('No target dependencies'), ]), ); }); }); }