| // 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/version_check_command.dart'; |
| import 'package:git/git.dart'; |
| import 'package:http/http.dart' as http; |
| import 'package:http/testing.dart'; |
| import 'package:pub_semver/pub_semver.dart'; |
| import 'package:test/test.dart'; |
| |
| import 'mocks.dart'; |
| import 'util.dart'; |
| |
| void testAllowedVersion( |
| String mainVersion, |
| String headVersion, { |
| bool allowed = true, |
| NextVersionType? nextVersionType, |
| }) { |
| final main = Version.parse(mainVersion); |
| final head = Version.parse(headVersion); |
| final Map<Version, NextVersionType> allowedVersions = getAllowedNextVersions( |
| main, |
| newVersion: head, |
| ); |
| if (allowed) { |
| expect(allowedVersions, contains(head)); |
| if (nextVersionType != null) { |
| expect(allowedVersions[head], equals(nextVersionType)); |
| } |
| } else { |
| expect(allowedVersions, isNot(contains(head))); |
| } |
| } |
| |
| void main() { |
| const indentation = ' '; |
| group('VersionCheckCommand', () { |
| late MockPlatform mockPlatform; |
| late Directory packagesDir; |
| late CommandRunner<void> runner; |
| late RecordingProcessRunner gitProcessRunner; |
| // Ignored if mockHttpResponse is set. |
| int mockHttpStatus; |
| Map<String, dynamic>? mockHttpResponse; |
| |
| setUp(() { |
| mockPlatform = MockPlatform(); |
| final RecordingProcessRunner processRunner; |
| final GitDir gitDir; |
| (:packagesDir, :processRunner, :gitProcessRunner, :gitDir) = |
| configureBaseCommandMocks(platform: mockPlatform); |
| |
| // Default to simulating the plugin never having been published. |
| mockHttpStatus = 404; |
| mockHttpResponse = null; |
| final mockClient = MockClient((http.Request request) async { |
| return http.Response( |
| json.encode(mockHttpResponse), |
| mockHttpResponse == null ? mockHttpStatus : 200, |
| ); |
| }); |
| |
| final command = VersionCheckCommand( |
| packagesDir, |
| processRunner: processRunner, |
| platform: mockPlatform, |
| gitDir: gitDir, |
| httpClient: mockClient, |
| ); |
| |
| runner = CommandRunner<void>( |
| 'version_check_command', |
| 'Test for $VersionCheckCommand', |
| ); |
| runner.addCommand(command); |
| }); |
| |
| test('allows valid version', () async { |
| createFakePlugin('plugin', packagesDir, version: '2.0.0'); |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), |
| ]; |
| final List<String> output = await runCapturingPrint(runner, <String>[ |
| 'version-check', |
| '--base-sha=main', |
| ]); |
| |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('Running for plugin'), |
| contains('1.0.0 -> 2.0.0'), |
| ]), |
| ); |
| expect( |
| gitProcessRunner.recordedCalls, |
| containsAllInOrder(const <ProcessCall>[ |
| ProcessCall('git-show', <String>[ |
| 'main:packages/plugin/pubspec.yaml', |
| ], null), |
| ]), |
| ); |
| }); |
| |
| test('denies invalid version', () async { |
| createFakePlugin('plugin', packagesDir, version: '0.2.0'); |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 0.0.1')), |
| ]; |
| Error? commandError; |
| final List<String> output = await runCapturingPrint( |
| runner, |
| <String>['version-check', '--base-sha=main'], |
| errorHandler: (Error e) { |
| commandError = e; |
| }, |
| ); |
| |
| expect(commandError, isA<ToolExit>()); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[contains('Incorrectly updated version.')]), |
| ); |
| expect( |
| gitProcessRunner.recordedCalls, |
| containsAllInOrder(const <ProcessCall>[ |
| ProcessCall('git-show', <String>[ |
| 'main:packages/plugin/pubspec.yaml', |
| ], null), |
| ]), |
| ); |
| }); |
| |
| test('uses merge-base without explicit base-sha', () async { |
| createFakePlugin('plugin', packagesDir, version: '2.0.0'); |
| gitProcessRunner.mockProcessesForExecutable['git-merge-base'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'abc123')), |
| FakeProcessInfo(MockProcess(stdout: 'abc123')), |
| ]; |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), |
| ]; |
| final List<String> output = await runCapturingPrint(runner, <String>[ |
| 'version-check', |
| ]); |
| |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('Running for plugin'), |
| contains('1.0.0 -> 2.0.0'), |
| ]), |
| ); |
| expect( |
| gitProcessRunner.recordedCalls, |
| containsAllInOrder(const <ProcessCall>[ |
| ProcessCall('git-merge-base', <String>[ |
| '--fork-point', |
| 'main', |
| 'HEAD', |
| ], null), |
| ProcessCall('git-show', <String>[ |
| 'abc123:packages/plugin/pubspec.yaml', |
| ], null), |
| ]), |
| ); |
| }); |
| |
| test('allows valid version for new package.', () async { |
| createFakePlugin('plugin', packagesDir, version: '1.0.0'); |
| final List<String> output = await runCapturingPrint(runner, <String>[ |
| 'version-check', |
| ]); |
| |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('Running for plugin'), |
| contains('Unable to find previous version at git base.'), |
| ]), |
| ); |
| }); |
| |
| test('allows likely reverts.', () async { |
| createFakePlugin('plugin', packagesDir, version: '0.6.1'); |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 0.6.2')), |
| ]; |
| final List<String> output = await runCapturingPrint(runner, <String>[ |
| 'version-check', |
| '--base-sha=main', |
| ]); |
| |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains( |
| 'New version is lower than previous version. ' |
| 'This is assumed to be a revert.', |
| ), |
| ]), |
| ); |
| expect( |
| gitProcessRunner.recordedCalls, |
| containsAllInOrder(const <ProcessCall>[ |
| ProcessCall('git-show', <String>[ |
| 'main:packages/plugin/pubspec.yaml', |
| ], null), |
| ]), |
| ); |
| }); |
| |
| test('denies lower version that could not be a simple revert', () async { |
| createFakePlugin('plugin', packagesDir, version: '0.5.1'); |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 0.6.2')), |
| ]; |
| |
| Error? commandError; |
| final List<String> output = await runCapturingPrint( |
| runner, |
| <String>['version-check', '--base-sha=main'], |
| errorHandler: (Error e) { |
| commandError = e; |
| }, |
| ); |
| |
| expect(commandError, isA<ToolExit>()); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[contains('Incorrectly updated version.')]), |
| ); |
| expect( |
| gitProcessRunner.recordedCalls, |
| containsAllInOrder(const <ProcessCall>[ |
| ProcessCall('git-show', <String>[ |
| 'main:packages/plugin/pubspec.yaml', |
| ], null), |
| ]), |
| ); |
| }); |
| |
| test('allows minor changes to platform interfaces', () async { |
| createFakePlugin( |
| 'plugin_platform_interface', |
| packagesDir, |
| version: '1.1.0', |
| ); |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), |
| ]; |
| final List<String> output = await runCapturingPrint(runner, <String>[ |
| 'version-check', |
| '--base-sha=main', |
| ]); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('Running for plugin'), |
| contains('1.0.0 -> 1.1.0'), |
| ]), |
| ); |
| expect( |
| gitProcessRunner.recordedCalls, |
| containsAllInOrder(const <ProcessCall>[ |
| ProcessCall('git-show', <String>[ |
| 'main:packages/plugin_platform_interface/pubspec.yaml', |
| ], null), |
| ]), |
| ); |
| }); |
| |
| test( |
| 'disallows breaking changes to platform interfaces by default', |
| () async { |
| createFakePlugin( |
| 'plugin_platform_interface', |
| packagesDir, |
| version: '2.0.0', |
| ); |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), |
| ]; |
| Error? commandError; |
| final List<String> output = await runCapturingPrint( |
| runner, |
| <String>['version-check', '--base-sha=main'], |
| errorHandler: (Error e) { |
| commandError = e; |
| }, |
| ); |
| |
| expect(commandError, isA<ToolExit>()); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains( |
| ' Breaking changes to platform interfaces are not allowed ' |
| 'without explicit justification.\n' |
| ' See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md ' |
| 'for more information.', |
| ), |
| ]), |
| ); |
| expect( |
| gitProcessRunner.recordedCalls, |
| containsAllInOrder(const <ProcessCall>[ |
| ProcessCall('git-show', <String>[ |
| 'main:packages/plugin_platform_interface/pubspec.yaml', |
| ], null), |
| ]), |
| ); |
| }, |
| ); |
| |
| test( |
| 'allows breaking changes to platform interfaces with override label', |
| () async { |
| createFakePlugin( |
| 'plugin_platform_interface', |
| packagesDir, |
| version: '2.0.0', |
| ); |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), |
| ]; |
| |
| final List<String> output = await runCapturingPrint(runner, <String>[ |
| 'version-check', |
| '--base-sha=main', |
| '--pr-labels=some label,override: allow breaking change,another-label', |
| ]); |
| |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains( |
| 'Allowing breaking change to plugin_platform_interface ' |
| 'due to the "override: allow breaking change" label.', |
| ), |
| contains('Ran for 1 package(s) (1 with warnings)'), |
| ]), |
| ); |
| expect( |
| gitProcessRunner.recordedCalls, |
| containsAllInOrder(const <ProcessCall>[ |
| ProcessCall('git-show', <String>[ |
| 'main:packages/plugin_platform_interface/pubspec.yaml', |
| ], null), |
| ]), |
| ); |
| }, |
| ); |
| |
| test( |
| 'allows breaking changes to platform interfaces with bypass flag', |
| () async { |
| createFakePlugin( |
| 'plugin_platform_interface', |
| packagesDir, |
| version: '2.0.0', |
| ); |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), |
| ]; |
| final List<String> output = await runCapturingPrint(runner, <String>[ |
| 'version-check', |
| '--base-sha=main', |
| '--ignore-platform-interface-breaks', |
| ]); |
| |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains( |
| 'Allowing breaking change to plugin_platform_interface due ' |
| 'to --ignore-platform-interface-breaks', |
| ), |
| contains('Ran for 1 package(s) (1 with warnings)'), |
| ]), |
| ); |
| expect( |
| gitProcessRunner.recordedCalls, |
| containsAllInOrder(const <ProcessCall>[ |
| ProcessCall('git-show', <String>[ |
| 'main:packages/plugin_platform_interface/pubspec.yaml', |
| ], null), |
| ]), |
| ); |
| }, |
| ); |
| |
| test( |
| 'Allow empty lines in front of the first version in CHANGELOG', |
| () async { |
| const version = '1.0.1'; |
| final RepositoryPackage plugin = createFakePlugin( |
| 'plugin', |
| packagesDir, |
| version: version, |
| ); |
| const changelog = |
| ''' |
| |
| ## $version |
| * Some changes. |
| '''; |
| plugin.changelogFile.writeAsStringSync(changelog); |
| final List<String> output = await runCapturingPrint(runner, <String>[ |
| 'version-check', |
| '--base-sha=main', |
| ]); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[contains('Running for plugin')]), |
| ); |
| }, |
| ); |
| |
| test('Throws if versions in changelog and pubspec do not match', () async { |
| final RepositoryPackage plugin = createFakePlugin( |
| 'plugin', |
| packagesDir, |
| version: '1.0.1', |
| ); |
| const changelog = ''' |
| ## 1.0.2 |
| * Some changes. |
| '''; |
| plugin.changelogFile.writeAsStringSync(changelog); |
| Error? commandError; |
| final List<String> output = await runCapturingPrint( |
| runner, |
| <String>['version-check', '--base-sha=main', '--against-pub'], |
| errorHandler: (Error e) { |
| commandError = e; |
| }, |
| ); |
| |
| expect(commandError, isA<ToolExit>()); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('Versions in CHANGELOG.md and pubspec.yaml do not match.'), |
| ]), |
| ); |
| }); |
| |
| test('Success if CHANGELOG and pubspec versions match', () async { |
| const version = '1.0.1'; |
| final RepositoryPackage plugin = createFakePlugin( |
| 'plugin', |
| packagesDir, |
| version: version, |
| ); |
| |
| const changelog = |
| ''' |
| ## $version |
| * Some changes. |
| '''; |
| plugin.changelogFile.writeAsStringSync(changelog); |
| final List<String> output = await runCapturingPrint(runner, <String>[ |
| 'version-check', |
| '--base-sha=main', |
| ]); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[contains('Running for plugin')]), |
| ); |
| }); |
| |
| test( |
| 'Fail if pubspec version only matches an older version listed in CHANGELOG', |
| () async { |
| final RepositoryPackage plugin = createFakePlugin( |
| 'plugin', |
| packagesDir, |
| version: '1.0.0', |
| ); |
| |
| const changelog = ''' |
| ## 1.0.1 |
| * Some changes. |
| ## 1.0.0 |
| * Some other changes. |
| '''; |
| plugin.changelogFile.writeAsStringSync(changelog); |
| var hasError = false; |
| final List<String> output = await runCapturingPrint( |
| runner, |
| <String>['version-check', '--base-sha=main', '--against-pub'], |
| errorHandler: (Error e) { |
| expect(e, isA<ToolExit>()); |
| hasError = true; |
| }, |
| ); |
| expect(hasError, isTrue); |
| |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('Versions in CHANGELOG.md and pubspec.yaml do not match.'), |
| ]), |
| ); |
| }, |
| ); |
| |
| test( |
| 'Allow NEXT as a placeholder for gathering CHANGELOG entries', |
| () async { |
| const version = '1.0.0'; |
| final RepositoryPackage plugin = createFakePlugin( |
| 'plugin', |
| packagesDir, |
| version: version, |
| ); |
| |
| const changelog = |
| ''' |
| ## NEXT |
| * Some changes that won't be published until the next time there's a release. |
| ## $version |
| * Some other changes. |
| '''; |
| plugin.changelogFile.writeAsStringSync(changelog); |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), |
| ]; |
| |
| final List<String> output = await runCapturingPrint(runner, <String>[ |
| 'version-check', |
| '--base-sha=main', |
| ]); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('Running for plugin'), |
| contains('Found NEXT; validating next version in the CHANGELOG.'), |
| ]), |
| ); |
| }, |
| ); |
| |
| test('Fail if NEXT appears after a version', () async { |
| const version = '1.0.1'; |
| final RepositoryPackage plugin = createFakePlugin( |
| 'plugin', |
| packagesDir, |
| version: version, |
| ); |
| |
| const changelog = |
| ''' |
| ## $version |
| * Some changes. |
| ## NEXT |
| * Some changes that should have been folded in 1.0.1. |
| ## 1.0.0 |
| * Some other changes. |
| '''; |
| plugin.changelogFile.writeAsStringSync(changelog); |
| var hasError = false; |
| final List<String> output = await runCapturingPrint( |
| runner, |
| <String>['version-check', '--base-sha=main', '--against-pub'], |
| errorHandler: (Error e) { |
| expect(e, isA<ToolExit>()); |
| hasError = true; |
| }, |
| ); |
| expect(hasError, isTrue); |
| |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains( |
| 'When bumping the version for release, the NEXT section ' |
| "should be incorporated into the new version's release notes.", |
| ), |
| ]), |
| ); |
| }); |
| |
| test( |
| 'Fail if NEXT is left in the CHANGELOG when adding a version bump', |
| () async { |
| const version = '1.0.1'; |
| final RepositoryPackage plugin = createFakePlugin( |
| 'plugin', |
| packagesDir, |
| version: version, |
| ); |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), |
| ]; |
| |
| const changelog = |
| ''' |
| ## NEXT |
| * Some changes that should have been folded in 1.0.1. |
| ## $version |
| * Some changes. |
| ## 1.0.0 |
| * Some other changes. |
| '''; |
| plugin.changelogFile.writeAsStringSync(changelog); |
| |
| var hasError = false; |
| final List<String> output = await runCapturingPrint( |
| runner, |
| <String>['version-check', '--base-sha=main'], |
| errorHandler: (Error e) { |
| expect(e, isA<ToolExit>()); |
| hasError = true; |
| }, |
| ); |
| expect(hasError, isTrue); |
| |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains( |
| 'When bumping the version for release, the NEXT section ' |
| "should be incorporated into the new version's release notes.", |
| ), |
| contains( |
| 'plugin:\n' |
| ' CHANGELOG.md failed validation.', |
| ), |
| ]), |
| ); |
| }, |
| ); |
| |
| test('fails if the version increases without replacing NEXT', () async { |
| final RepositoryPackage plugin = createFakePlugin( |
| 'plugin', |
| packagesDir, |
| version: '1.0.1', |
| ); |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), |
| ]; |
| |
| const changelog = ''' |
| ## NEXT |
| * Some changes that should be listed as part of 1.0.1. |
| ## 1.0.0 |
| * Some other changes. |
| '''; |
| plugin.changelogFile.writeAsStringSync(changelog); |
| |
| var hasError = false; |
| final List<String> output = await runCapturingPrint( |
| runner, |
| <String>['version-check', '--base-sha=main'], |
| errorHandler: (Error e) { |
| expect(e, isA<ToolExit>()); |
| hasError = true; |
| }, |
| ); |
| expect(hasError, isTrue); |
| |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains( |
| 'When bumping the version for release, the NEXT section ' |
| "should be incorporated into the new version's release notes.", |
| ), |
| ]), |
| ); |
| }); |
| |
| test('allows NEXT for a revert', () async { |
| final RepositoryPackage plugin = createFakePlugin( |
| 'plugin', |
| packagesDir, |
| version: '1.0.0', |
| ); |
| |
| const changelog = ''' |
| ## NEXT |
| * Some changes that should be listed as part of 1.0.1. |
| ## 1.0.0 |
| * Some other changes. |
| '''; |
| plugin.changelogFile.writeAsStringSync(changelog); |
| plugin.changelogFile.writeAsStringSync(changelog); |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 1.0.1')), |
| ]; |
| |
| final List<String> output = await runCapturingPrint(runner, <String>[ |
| 'version-check', |
| '--base-sha=main', |
| ]); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains( |
| 'New version is lower than previous version. ' |
| 'This is assumed to be a revert.', |
| ), |
| ]), |
| ); |
| }); |
| |
| // This handles imports of a package with a NEXT section. |
| test('allows NEXT for a new package', () async { |
| final RepositoryPackage plugin = createFakePackage( |
| 'a_package', |
| packagesDir, |
| version: '1.0.0', |
| ); |
| |
| const changelog = ''' |
| ## NEXT |
| * Some changes that should be listed in the next release. |
| ## 1.0.0 |
| * Some other changes. |
| '''; |
| plugin.changelogFile.writeAsStringSync(changelog); |
| |
| final List<String> output = await runCapturingPrint(runner, <String>[ |
| 'version-check', |
| '--base-sha=main', |
| ]); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('Unable to find previous version at git base'), |
| contains('Found NEXT; validating next version in the CHANGELOG'), |
| ]), |
| ); |
| }); |
| |
| test('fails gracefully if the first entry uses the wrong style', () async { |
| final RepositoryPackage plugin = createFakePlugin( |
| 'plugin', |
| packagesDir, |
| version: '1.0.0', |
| ); |
| |
| const changelog = ''' |
| # 1.0.0 |
| * Some changes for a later release. |
| ## 0.9.0 |
| * Some earlier changes. |
| '''; |
| plugin.changelogFile.writeAsStringSync(changelog); |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), |
| ]; |
| |
| Error? commandError; |
| final List<String> output = await runCapturingPrint( |
| runner, |
| <String>['version-check', '--base-sha=main'], |
| errorHandler: (Error e) { |
| commandError = e; |
| }, |
| ); |
| |
| expect(commandError, isA<ToolExit>()); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('Unable to find a version in CHANGELOG.md'), |
| contains( |
| 'The current version should be on a line starting with ' |
| '"## ", either on the first non-empty line or after a "## NEXT" ' |
| 'section.', |
| ), |
| ]), |
| ); |
| }); |
| |
| test( |
| 'fails gracefully if the version headers are not found due to using the wrong style', |
| () async { |
| final RepositoryPackage plugin = createFakePlugin( |
| 'plugin', |
| packagesDir, |
| version: '1.0.0', |
| ); |
| |
| const changelog = ''' |
| ## NEXT |
| * Some changes for a later release. |
| # 1.0.0 |
| * Some other changes. |
| '''; |
| plugin.changelogFile.writeAsStringSync(changelog); |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), |
| ]; |
| |
| Error? commandError; |
| final List<String> output = await runCapturingPrint( |
| runner, |
| <String>['version-check', '--base-sha=main'], |
| errorHandler: (Error e) { |
| commandError = e; |
| }, |
| ); |
| |
| expect(commandError, isA<ToolExit>()); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('Unable to find a version in CHANGELOG.md'), |
| contains( |
| 'The current version should be on a line starting with ' |
| '"## ", either on the first non-empty line or after a "## NEXT" ' |
| 'section.', |
| ), |
| ]), |
| ); |
| }, |
| ); |
| |
| test('fails gracefully if the version is unparseable', () async { |
| final RepositoryPackage plugin = createFakePlugin( |
| 'plugin', |
| packagesDir, |
| version: '1.0.0', |
| ); |
| |
| const changelog = ''' |
| ## Alpha |
| * Some changes. |
| '''; |
| plugin.changelogFile.writeAsStringSync(changelog); |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), |
| ]; |
| |
| Error? commandError; |
| final List<String> output = await runCapturingPrint( |
| runner, |
| <String>['version-check', '--base-sha=main'], |
| errorHandler: (Error e) { |
| commandError = e; |
| }, |
| ); |
| |
| expect(commandError, isA<ToolExit>()); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('"Alpha" could not be parsed as a version.'), |
| ]), |
| ); |
| }); |
| |
| group('missing change detection', () { |
| Future<List<String>> runWithMissingChangeDetection( |
| List<String> extraArgs, { |
| void Function(Error error)? errorHandler, |
| }) async { |
| return runCapturingPrint(runner, <String>[ |
| 'version-check', |
| '--base-sha=main', |
| '--check-for-missing-changes', |
| ...extraArgs, |
| ], errorHandler: errorHandler); |
| } |
| |
| test('passes for unchanged packages', () async { |
| final RepositoryPackage plugin = createFakePlugin( |
| 'plugin', |
| packagesDir, |
| version: '1.0.0', |
| ); |
| |
| const changelog = ''' |
| ## 1.0.0 |
| * Some changes. |
| '''; |
| plugin.changelogFile.writeAsStringSync(changelog); |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), |
| ]; |
| gitProcessRunner.mockProcessesForExecutable['git-diff'] = |
| <FakeProcessInfo>[FakeProcessInfo(MockProcess(stdout: ''))]; |
| |
| final List<String> output = await runWithMissingChangeDetection( |
| <String>[], |
| ); |
| |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[contains('Running for plugin')]), |
| ); |
| }); |
| |
| test('passes if the only change is in pending_changelogs', () async { |
| final RepositoryPackage plugin = createFakePlugin( |
| 'plugin', |
| packagesDir, |
| version: '1.0.0', |
| ); |
| |
| const changelog = ''' |
| ## 1.0.0 |
| * Some changes. |
| '''; |
| plugin.changelogFile.writeAsStringSync(changelog); |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), |
| ]; |
| gitProcessRunner.mockProcessesForExecutable['git-diff'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo( |
| MockProcess( |
| stdout: ''' |
| packages/plugin/pending_changelogs/some_entry.yaml |
| ''', |
| ), |
| ), |
| ]; |
| |
| final List<String> output = await runWithMissingChangeDetection( |
| <String>[], |
| ); |
| |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('Running for plugin'), |
| contains('No issues found'), |
| ]), |
| ); |
| }); |
| |
| test('fails if a version and CHANGELOG change is missing from a change ' |
| 'that does not pass the exemption check', () async { |
| final RepositoryPackage plugin = createFakePlugin( |
| 'plugin', |
| packagesDir, |
| version: '1.0.0', |
| ); |
| |
| const changelog = ''' |
| ## 1.0.0 |
| * Some changes. |
| '''; |
| plugin.changelogFile.writeAsStringSync(changelog); |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), |
| ]; |
| gitProcessRunner.mockProcessesForExecutable['git-diff'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo( |
| MockProcess( |
| stdout: ''' |
| packages/plugin/lib/plugin.dart |
| ''', |
| ), |
| ), |
| ]; |
| |
| Error? commandError; |
| final List<String> output = await runWithMissingChangeDetection( |
| <String>[], |
| errorHandler: (Error e) { |
| commandError = e; |
| }, |
| ); |
| |
| expect(commandError, isA<ToolExit>()); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('No version change found'), |
| contains('No CHANGELOG change found.'), |
| contains('"update-release-info" command'), |
| contains( |
| 'plugin:\n' |
| ' Missing version change', |
| ), |
| ]), |
| ); |
| }); |
| |
| test('passes version change requirement when version changes', () async { |
| final RepositoryPackage plugin = createFakePlugin( |
| 'plugin', |
| packagesDir, |
| version: '1.0.1', |
| ); |
| |
| const changelog = ''' |
| ## 1.0.1 |
| * Some changes. |
| '''; |
| plugin.changelogFile.writeAsStringSync(changelog); |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), |
| ]; |
| gitProcessRunner.mockProcessesForExecutable['git-diff'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo( |
| MockProcess( |
| stdout: ''' |
| packages/plugin/lib/plugin.dart |
| packages/plugin/CHANGELOG.md |
| packages/plugin/pubspec.yaml |
| ''', |
| ), |
| ), |
| ]; |
| |
| final List<String> output = await runWithMissingChangeDetection( |
| <String>[], |
| ); |
| |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[contains('Running for plugin')]), |
| ); |
| }); |
| |
| test('version change check ignores files outside the package', () async { |
| final RepositoryPackage plugin = createFakePlugin( |
| 'plugin', |
| packagesDir, |
| version: '1.0.0', |
| ); |
| |
| const changelog = ''' |
| ## 1.0.0 |
| * Some changes. |
| '''; |
| plugin.changelogFile.writeAsStringSync(changelog); |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), |
| ]; |
| gitProcessRunner.mockProcessesForExecutable['git-diff'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo( |
| MockProcess( |
| stdout: ''' |
| packages/plugin_a/lib/plugin.dart |
| tool/plugin/lib/plugin.dart |
| ''', |
| ), |
| ), |
| ]; |
| |
| final List<String> output = await runWithMissingChangeDetection( |
| <String>[], |
| ); |
| |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[contains('Running for plugin')]), |
| ); |
| }); |
| |
| test('allows missing version change for exempt changes', () async { |
| final RepositoryPackage plugin = createFakePlugin( |
| 'plugin', |
| packagesDir, |
| version: '1.0.0', |
| ); |
| |
| const changelog = ''' |
| ## 1.0.0 |
| * Some changes. |
| '''; |
| plugin.changelogFile.writeAsStringSync(changelog); |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), |
| ]; |
| gitProcessRunner.mockProcessesForExecutable['git-diff'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo( |
| MockProcess( |
| stdout: ''' |
| packages/plugin/example/android/.pluginToolsConfig.yaml |
| packages/plugin/example/android/lint-baseline.xml |
| packages/plugin/example/android/src/androidTest/foo/bar/FooTest.java |
| packages/plugin/example/ios/RunnerTests/Foo.m |
| packages/plugin/example/ios/RunnerUITests/info.plist |
| packages/plugin/darwin/Tests/Foo.swift |
| packages/plugin/analysis_options.yaml |
| packages/plugin/CHANGELOG.md |
| ''', |
| ), |
| ), |
| ]; |
| |
| final List<String> output = await runWithMissingChangeDetection( |
| <String>[], |
| ); |
| |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[contains('Running for plugin')]), |
| ); |
| }); |
| |
| test('allows missing version change with override label', () async { |
| final RepositoryPackage plugin = createFakePlugin( |
| 'plugin', |
| packagesDir, |
| version: '1.0.0', |
| ); |
| |
| const changelog = ''' |
| ## 1.0.0 |
| * Some changes. |
| '''; |
| plugin.changelogFile.writeAsStringSync(changelog); |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), |
| ]; |
| gitProcessRunner.mockProcessesForExecutable['git-diff'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo( |
| MockProcess( |
| stdout: ''' |
| packages/plugin/lib/plugin.dart |
| packages/plugin/CHANGELOG.md |
| packages/plugin/pubspec.yaml |
| ''', |
| ), |
| ), |
| ]; |
| |
| final List<String> |
| output = await runWithMissingChangeDetection(<String>[ |
| '--pr-labels=some label,override: no versioning needed,another-label', |
| ]); |
| |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains( |
| 'Ignoring lack of version change due to the ' |
| '"override: no versioning needed" label.', |
| ), |
| ]), |
| ); |
| }); |
| |
| test('fails if a CHANGELOG change is missing', () async { |
| final RepositoryPackage plugin = createFakePlugin( |
| 'plugin', |
| packagesDir, |
| version: '1.0.0', |
| ); |
| |
| const changelog = ''' |
| ## 1.0.0 |
| * Some changes. |
| '''; |
| plugin.changelogFile.writeAsStringSync(changelog); |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), |
| ]; |
| gitProcessRunner.mockProcessesForExecutable['git-diff'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo( |
| MockProcess( |
| stdout: ''' |
| packages/plugin/example/lib/foo.dart |
| ''', |
| ), |
| ), |
| ]; |
| |
| Error? commandError; |
| final List<String> output = await runWithMissingChangeDetection( |
| <String>[], |
| errorHandler: (Error e) { |
| commandError = e; |
| }, |
| ); |
| |
| expect(commandError, isA<ToolExit>()); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('No CHANGELOG change found.\nIf'), |
| contains( |
| 'plugin:\n' |
| ' Missing CHANGELOG change', |
| ), |
| ]), |
| ); |
| }); |
| |
| test('passes CHANGELOG check when the CHANGELOG is changed', () async { |
| final RepositoryPackage plugin = createFakePlugin( |
| 'plugin', |
| packagesDir, |
| version: '1.0.0', |
| ); |
| |
| const changelog = ''' |
| ## 1.0.0 |
| * Some changes. |
| '''; |
| plugin.changelogFile.writeAsStringSync(changelog); |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), |
| ]; |
| gitProcessRunner.mockProcessesForExecutable['git-diff'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo( |
| MockProcess( |
| stdout: ''' |
| packages/plugin/example/lib/foo.dart |
| packages/plugin/CHANGELOG.md |
| ''', |
| ), |
| ), |
| ]; |
| |
| final List<String> output = await runWithMissingChangeDetection( |
| <String>[], |
| ); |
| |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[contains('Running for plugin')]), |
| ); |
| }); |
| |
| test( |
| 'fails CHANGELOG check if only another package CHANGELOG chages', |
| () async { |
| final RepositoryPackage plugin = createFakePlugin( |
| 'plugin', |
| packagesDir, |
| version: '1.0.0', |
| ); |
| |
| const changelog = ''' |
| ## 1.0.0 |
| * Some changes. |
| '''; |
| plugin.changelogFile.writeAsStringSync(changelog); |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), |
| ]; |
| gitProcessRunner.mockProcessesForExecutable['git-diff'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo( |
| MockProcess( |
| stdout: ''' |
| packages/plugin/example/lib/foo.dart |
| packages/another_plugin/CHANGELOG.md |
| ''', |
| ), |
| ), |
| ]; |
| |
| Error? commandError; |
| final List<String> output = await runWithMissingChangeDetection( |
| <String>[], |
| errorHandler: (Error e) { |
| commandError = e; |
| }, |
| ); |
| |
| expect(commandError, isA<ToolExit>()); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('No CHANGELOG change found'), |
| ]), |
| ); |
| }, |
| ); |
| |
| test('allows missing CHANGELOG change with justification', () async { |
| final RepositoryPackage plugin = createFakePlugin( |
| 'plugin', |
| packagesDir, |
| version: '1.0.0', |
| ); |
| |
| const changelog = ''' |
| ## 1.0.0 |
| * Some changes. |
| '''; |
| plugin.changelogFile.writeAsStringSync(changelog); |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), |
| ]; |
| gitProcessRunner.mockProcessesForExecutable['git-diff'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo( |
| MockProcess( |
| stdout: ''' |
| packages/plugin/example/lib/foo.dart |
| ''', |
| ), |
| ), |
| ]; |
| |
| final List<String> |
| output = await runWithMissingChangeDetection(<String>[ |
| '--pr-labels=some label,override: no changelog needed,another-label', |
| ]); |
| |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains( |
| 'Ignoring lack of CHANGELOG update due to the ' |
| '"override: no changelog needed" label.', |
| ), |
| ]), |
| ); |
| }); |
| |
| // This test ensures that Dependabot Gradle changes to test-only files |
| // aren't flagged by the version check. |
| test( |
| 'allows missing CHANGELOG and version change for test-only Gradle changes', |
| () async { |
| final RepositoryPackage plugin = createFakePlugin( |
| 'plugin', |
| packagesDir, |
| version: '1.0.0', |
| ); |
| |
| const changelog = ''' |
| ## 1.0.0 |
| * Some changes. |
| '''; |
| plugin.changelogFile.writeAsStringSync(changelog); |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), |
| ]; |
| gitProcessRunner.mockProcessesForExecutable['git-diff'] = |
| <FakeProcessInfo>[ |
| // File list. |
| FakeProcessInfo( |
| MockProcess( |
| stdout: ''' |
| packages/plugin/android/build.gradle |
| ''', |
| ), |
| ), |
| // build.gradle diff |
| FakeProcessInfo( |
| MockProcess( |
| stdout: ''' |
| - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' |
| - testImplementation 'junit:junit:4.10.0' |
| + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' |
| + testImplementation 'junit:junit:4.13.2' |
| ''', |
| ), |
| ), |
| ]; |
| |
| final List<String> output = await runWithMissingChangeDetection( |
| <String>[], |
| ); |
| |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[contains('Running for plugin')]), |
| ); |
| }, |
| ); |
| |
| test( |
| 'allows missing CHANGELOG and version change for dev-only-file changes', |
| () async { |
| final RepositoryPackage plugin = createFakePlugin( |
| 'plugin', |
| packagesDir, |
| version: '1.0.0', |
| ); |
| |
| const changelog = ''' |
| ## 1.0.0 |
| * Some changes. |
| '''; |
| plugin.changelogFile.writeAsStringSync(changelog); |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), |
| ]; |
| gitProcessRunner.mockProcessesForExecutable['git-diff'] = |
| <FakeProcessInfo>[ |
| // File list. |
| FakeProcessInfo( |
| MockProcess( |
| stdout: ''' |
| packages/plugin/tool/run_tests.dart |
| packages/plugin/run_tests.sh |
| ''', |
| ), |
| ), |
| ]; |
| |
| final List<String> output = await runWithMissingChangeDetection( |
| <String>[], |
| ); |
| |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[contains('Running for plugin')]), |
| ); |
| }, |
| ); |
| |
| test( |
| 'allows missing CHANGELOG and version change for dev-only line-level ' |
| 'changes in production files', |
| () async { |
| final RepositoryPackage plugin = createFakePlugin( |
| 'plugin', |
| packagesDir, |
| version: '1.0.0', |
| ); |
| |
| const changelog = ''' |
| ## 1.0.0 |
| * Some changes. |
| '''; |
| plugin.changelogFile.writeAsStringSync(changelog); |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), |
| ]; |
| gitProcessRunner |
| .mockProcessesForExecutable['git-diff'] = <FakeProcessInfo>[ |
| // File list. |
| FakeProcessInfo( |
| MockProcess( |
| stdout: ''' |
| packages/plugin/lib/plugin.dart |
| ''', |
| ), |
| ), |
| // Dart file diff. |
| FakeProcessInfo( |
| MockProcess( |
| stdout: ''' |
| + // TODO(someone): Fix this. |
| + // ignore: some_lint |
| ''', |
| ), |
| <String>['main', 'HEAD', '--', 'packages/plugin/lib/plugin.dart'], |
| ), |
| ]; |
| |
| final List<String> output = await runWithMissingChangeDetection( |
| <String>[], |
| ); |
| |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[contains('Running for plugin')]), |
| ); |
| }, |
| ); |
| |
| test('documentation comments are not exempt', () async { |
| final RepositoryPackage plugin = createFakePlugin( |
| 'plugin', |
| packagesDir, |
| version: '1.0.0', |
| ); |
| |
| const changelog = ''' |
| ## 1.0.0 |
| * Some changes. |
| '''; |
| plugin.changelogFile.writeAsStringSync(changelog); |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), |
| ]; |
| gitProcessRunner |
| .mockProcessesForExecutable['git-diff'] = <FakeProcessInfo>[ |
| FakeProcessInfo( |
| MockProcess( |
| stdout: ''' |
| packages/plugin/lib/plugin.dart |
| ''', |
| ), |
| ), |
| // Dart file diff. |
| FakeProcessInfo( |
| MockProcess( |
| stdout: ''' |
| + /// Important new information for API clients! |
| ''', |
| ), |
| <String>['main', 'HEAD', '--', 'packages/plugin/lib/plugin.dart'], |
| ), |
| ]; |
| |
| Error? commandError; |
| final List<String> output = await runWithMissingChangeDetection( |
| <String>[], |
| errorHandler: (Error e) { |
| commandError = e; |
| }, |
| ); |
| |
| expect(commandError, isA<ToolExit>()); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains( |
| 'No version change found, but the change to this package could ' |
| 'not be verified to be exempt\n', |
| ), |
| contains( |
| 'plugin:\n' |
| ' Missing version change', |
| ), |
| ]), |
| ); |
| }); |
| }); |
| |
| test('allows valid against pub', () async { |
| mockHttpResponse = <String, dynamic>{ |
| 'name': 'some_package', |
| 'versions': <String>['0.0.1', '0.0.2', '1.0.0'], |
| }; |
| |
| createFakePlugin('plugin', packagesDir, version: '2.0.0'); |
| final List<String> output = await runCapturingPrint(runner, <String>[ |
| 'version-check', |
| '--base-sha=main', |
| '--against-pub', |
| ]); |
| |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('plugin: Current largest version on pub: 1.0.0'), |
| ]), |
| ); |
| }); |
| |
| test('denies invalid against pub', () async { |
| mockHttpResponse = <String, dynamic>{ |
| 'name': 'some_package', |
| 'versions': <String>['0.0.1', '0.0.2'], |
| }; |
| |
| createFakePlugin('plugin', packagesDir, version: '2.0.0'); |
| |
| var hasError = false; |
| final List<String> result = await runCapturingPrint( |
| runner, |
| <String>['version-check', '--base-sha=main', '--against-pub'], |
| errorHandler: (Error e) { |
| expect(e, isA<ToolExit>()); |
| hasError = true; |
| }, |
| ); |
| expect(hasError, isTrue); |
| |
| expect( |
| result, |
| containsAllInOrder(<Matcher>[ |
| contains( |
| ''' |
| ${indentation}Incorrectly updated version. |
| ${indentation}HEAD: 2.0.0, pub: 0.0.2. |
| ${indentation}Allowed versions: {1.0.0: NextVersionType.BREAKING_MAJOR, 0.1.0: NextVersionType.MINOR, 0.0.3: NextVersionType.PATCH}''', |
| ), |
| ]), |
| ); |
| }); |
| |
| test( |
| 'throw and print error message if http request failed when checking against pub', |
| () async { |
| mockHttpStatus = 400; |
| |
| createFakePlugin('plugin', packagesDir, version: '2.0.0'); |
| var hasError = false; |
| final List<String> result = await runCapturingPrint( |
| runner, |
| <String>['version-check', '--base-sha=main', '--against-pub'], |
| errorHandler: (Error e) { |
| expect(e, isA<ToolExit>()); |
| hasError = true; |
| }, |
| ); |
| expect(hasError, isTrue); |
| |
| expect( |
| result, |
| containsAllInOrder(<Matcher>[ |
| contains(''' |
| ${indentation}Error fetching version on pub for plugin. |
| ${indentation}HTTP Status 400 |
| ${indentation}HTTP response: null |
| '''), |
| ]), |
| ); |
| }, |
| ); |
| |
| test( |
| 'when checking against pub, allow any version if http status is 404.', |
| () async { |
| mockHttpStatus = 404; |
| |
| createFakePlugin('plugin', packagesDir, version: '2.0.0'); |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), |
| ]; |
| final List<String> result = await runCapturingPrint(runner, <String>[ |
| 'version-check', |
| '--base-sha=main', |
| '--against-pub', |
| ]); |
| |
| expect( |
| result, |
| containsAllInOrder(<Matcher>[ |
| contains('Unable to find previous version on pub server.'), |
| ]), |
| ); |
| }, |
| ); |
| |
| group('prelease versions', () { |
| test( |
| 'allow an otherwise-valid transition that also adds a pre-release component', |
| () async { |
| createFakePlugin('plugin', packagesDir, version: '2.0.0-dev'); |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), |
| ]; |
| final List<String> output = await runCapturingPrint(runner, <String>[ |
| 'version-check', |
| '--base-sha=main', |
| ]); |
| |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('Running for plugin'), |
| contains('1.0.0 -> 2.0.0-dev'), |
| ]), |
| ); |
| expect( |
| gitProcessRunner.recordedCalls, |
| containsAllInOrder(const <ProcessCall>[ |
| ProcessCall('git-show', <String>[ |
| 'main:packages/plugin/pubspec.yaml', |
| ], null), |
| ]), |
| ); |
| }, |
| ); |
| |
| test('allow releasing a pre-release', () async { |
| createFakePlugin('plugin', packagesDir, version: '1.2.0'); |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 1.2.0-dev')), |
| ]; |
| final List<String> output = await runCapturingPrint(runner, <String>[ |
| 'version-check', |
| '--base-sha=main', |
| ]); |
| |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('Running for plugin'), |
| contains('1.2.0-dev -> 1.2.0'), |
| ]), |
| ); |
| expect( |
| gitProcessRunner.recordedCalls, |
| containsAllInOrder(const <ProcessCall>[ |
| ProcessCall('git-show', <String>[ |
| 'main:packages/plugin/pubspec.yaml', |
| ], null), |
| ]), |
| ); |
| }); |
| |
| // Allow abandoning a pre-release version in favor of a different version |
| // change type. |
| test( |
| 'allow an otherwise-valid transition that also removes a pre-release component', |
| () async { |
| createFakePlugin('plugin', packagesDir, version: '2.0.0'); |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 1.2.0-dev')), |
| ]; |
| final List<String> output = await runCapturingPrint(runner, <String>[ |
| 'version-check', |
| '--base-sha=main', |
| ]); |
| |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('Running for plugin'), |
| contains('1.2.0-dev -> 2.0.0'), |
| ]), |
| ); |
| expect( |
| gitProcessRunner.recordedCalls, |
| containsAllInOrder(const <ProcessCall>[ |
| ProcessCall('git-show', <String>[ |
| 'main:packages/plugin/pubspec.yaml', |
| ], null), |
| ]), |
| ); |
| }, |
| ); |
| |
| test('allow changing only the pre-release version', () async { |
| createFakePlugin('plugin', packagesDir, version: '1.2.0-dev.2'); |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 1.2.0-dev.1')), |
| ]; |
| final List<String> output = await runCapturingPrint(runner, <String>[ |
| 'version-check', |
| '--base-sha=main', |
| ]); |
| |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('Running for plugin'), |
| contains('1.2.0-dev.1 -> 1.2.0-dev.2'), |
| ]), |
| ); |
| expect( |
| gitProcessRunner.recordedCalls, |
| containsAllInOrder(const <ProcessCall>[ |
| ProcessCall('git-show', <String>[ |
| 'main:packages/plugin/pubspec.yaml', |
| ], null), |
| ]), |
| ); |
| }); |
| |
| test( |
| 'denies invalid version change that also adds a pre-release', |
| () async { |
| createFakePlugin('plugin', packagesDir, version: '0.2.0-dev'); |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 0.0.1')), |
| ]; |
| Error? commandError; |
| final List<String> output = await runCapturingPrint( |
| runner, |
| <String>['version-check', '--base-sha=main'], |
| errorHandler: (Error e) { |
| commandError = e; |
| }, |
| ); |
| |
| expect(commandError, isA<ToolExit>()); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('Incorrectly updated version.'), |
| ]), |
| ); |
| expect( |
| gitProcessRunner.recordedCalls, |
| containsAllInOrder(const <ProcessCall>[ |
| ProcessCall('git-show', <String>[ |
| 'main:packages/plugin/pubspec.yaml', |
| ], null), |
| ]), |
| ); |
| }, |
| ); |
| |
| test( |
| 'denies invalid version change that also removes a pre-release', |
| () async { |
| createFakePlugin('plugin', packagesDir, version: '0.2.0'); |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 0.0.1-dev')), |
| ]; |
| Error? commandError; |
| final List<String> output = await runCapturingPrint( |
| runner, |
| <String>['version-check', '--base-sha=main'], |
| errorHandler: (Error e) { |
| commandError = e; |
| }, |
| ); |
| |
| expect(commandError, isA<ToolExit>()); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('Incorrectly updated version.'), |
| ]), |
| ); |
| expect( |
| gitProcessRunner.recordedCalls, |
| containsAllInOrder(const <ProcessCall>[ |
| ProcessCall('git-show', <String>[ |
| 'main:packages/plugin/pubspec.yaml', |
| ], null), |
| ]), |
| ); |
| }, |
| ); |
| |
| test('denies invalid version change between pre-releases', () async { |
| createFakePlugin('plugin', packagesDir, version: '0.2.0-dev'); |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 0.0.1-dev')), |
| ]; |
| Error? commandError; |
| final List<String> output = await runCapturingPrint( |
| runner, |
| <String>['version-check', '--base-sha=main'], |
| errorHandler: (Error e) { |
| commandError = e; |
| }, |
| ); |
| |
| expect(commandError, isA<ToolExit>()); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('Incorrectly updated version.'), |
| ]), |
| ); |
| expect( |
| gitProcessRunner.recordedCalls, |
| containsAllInOrder(const <ProcessCall>[ |
| ProcessCall('git-show', <String>[ |
| 'main:packages/plugin/pubspec.yaml', |
| ], null), |
| ]), |
| ); |
| }); |
| }); |
| group('batch release', () { |
| test( |
| 'fails for batch release package missing pending_changelogs', |
| () async { |
| final RepositoryPackage package = createFakePlugin( |
| 'a_package', |
| packagesDir, |
| ); |
| package.ciConfigFile.writeAsStringSync(''' |
| release: |
| batch: true |
| '''); |
| |
| Error? commandError; |
| final List<String> output = await runCapturingPrint( |
| runner, |
| <String>['version-check', '--base-sha=main'], |
| errorHandler: (Error e) { |
| commandError = e; |
| }, |
| ); |
| |
| expect(commandError, isA<ToolExit>()); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('No pending_changelogs folder found for a_package.'), |
| ]), |
| ); |
| }, |
| ); |
| |
| test( |
| 'passes for batch release package with pending_changelogs', |
| () async { |
| final RepositoryPackage package = createFakePlugin( |
| 'a_package', |
| packagesDir, |
| ); |
| package.ciConfigFile.writeAsStringSync(''' |
| release: |
| batch: true |
| '''); |
| package.pendingChangelogsDirectory.createSync(); |
| |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 0.0.1')), |
| ]; |
| |
| final List<String> output = await runCapturingPrint(runner, <String>[ |
| 'version-check', |
| '--base-sha=main', |
| ]); |
| |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('Running for a_package'), |
| contains('No issues found!'), |
| ]), |
| ); |
| }, |
| ); |
| |
| test( |
| 'fails for non-batch release package with pending_changelogs', |
| () async { |
| final RepositoryPackage package = createFakePlugin( |
| 'a_package', |
| packagesDir, |
| ); |
| // No ci_config.yaml means batch release is false by default. |
| package.pendingChangelogsDirectory.createSync(); |
| |
| Error? commandError; |
| final List<String> output = await runCapturingPrint( |
| runner, |
| <String>['version-check', '--base-sha=main'], |
| errorHandler: (Error e) { |
| commandError = e; |
| }, |
| ); |
| |
| expect(commandError, isA<ToolExit>()); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains( |
| 'Package does not use batch release but has pending changelogs.', |
| ), |
| ]), |
| ); |
| }, |
| ); |
| test( |
| 'ignores changelog and pubspec yaml version modifications check with post-release label', |
| () async { |
| final RepositoryPackage package = createFakePackage( |
| 'package', |
| packagesDir, |
| version: '1.0.0', |
| ); |
| package.ciConfigFile.writeAsStringSync(''' |
| release: |
| batch: true |
| '''); |
| // Create the pending_changelogs directory so the test doesn't fail on that check. |
| package.directory.childDirectory('pending_changelogs').createSync(); |
| |
| gitProcessRunner.mockProcessesForExecutable['git-diff'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo( |
| MockProcess( |
| stdout: ''' |
| packages/package/CHANGELOG.md |
| packages/package/pubspec.yaml |
| ''', |
| ), |
| ), |
| ]; |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), |
| ]; |
| |
| final List<String> output = await runCapturingPrint(runner, <String>[ |
| 'version-check', |
| '--base-sha=main', |
| '--pr-labels=post-release-package', |
| ]); |
| |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('Running for package'), |
| contains('No issues found!'), |
| ]), |
| ); |
| }, |
| ); |
| |
| test('fails when there is changelog modifications', () async { |
| final RepositoryPackage package = createFakePackage( |
| 'package', |
| packagesDir, |
| version: '1.0.0', |
| ); |
| package.ciConfigFile.writeAsStringSync(''' |
| release: |
| batch: true |
| '''); |
| // Create the pending_changelogs directory so the test doesn't fail on that check. |
| package.directory.childDirectory('pending_changelogs').createSync(); |
| |
| gitProcessRunner.mockProcessesForExecutable['git-diff'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo( |
| MockProcess( |
| stdout: ''' |
| packages/package/CHANGELOG.md |
| ''', |
| ), |
| ), |
| ]; |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), |
| ]; |
| |
| Error? commandError; |
| final List<String> output = await runCapturingPrint( |
| runner, |
| <String>['version-check', '--base-sha=main'], |
| errorHandler: (Error e) { |
| commandError = e; |
| }, |
| ); |
| |
| expect(commandError, isA<ToolExit>()); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains( |
| 'This package uses batch release, so CHANGELOG.md should not be changed directly.', |
| ), |
| ]), |
| ); |
| }); |
| |
| test('fails when there is pubspec version modifications', () async { |
| final RepositoryPackage package = createFakePackage( |
| 'package', |
| packagesDir, |
| version: '1.0.1', |
| ); |
| package.ciConfigFile.writeAsStringSync(''' |
| release: |
| batch: true |
| '''); |
| // Create the pending_changelogs directory so the test doesn't fail on that check. |
| package.directory.childDirectory('pending_changelogs').createSync(); |
| |
| gitProcessRunner.mockProcessesForExecutable['git-diff'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo( |
| MockProcess( |
| stdout: ''' |
| packages/package/pubspec.yaml |
| ''', |
| ), |
| ), |
| ]; |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), |
| ]; |
| |
| Error? commandError; |
| final List<String> output = await runCapturingPrint( |
| runner, |
| <String>['version-check', '--base-sha=main'], |
| errorHandler: (Error e) { |
| commandError = e; |
| }, |
| ); |
| |
| expect(commandError, isA<ToolExit>()); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains( |
| 'This package uses batch release, so the version in pubspec.yaml should not be changed directly.', |
| ), |
| ]), |
| ); |
| }); |
| |
| test( |
| 'passes when there is pubspec modification but no version change', |
| () async { |
| final RepositoryPackage package = createFakePackage( |
| 'package', |
| packagesDir, |
| version: '1.0.0', |
| ); |
| package.ciConfigFile.writeAsStringSync(''' |
| release: |
| batch: true |
| '''); |
| // Create the pending_changelogs directory so the test doesn't fail on that check. |
| package.directory.childDirectory('pending_changelogs').createSync(); |
| |
| gitProcessRunner.mockProcessesForExecutable['git-diff'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo( |
| MockProcess( |
| stdout: ''' |
| packages/package/pubspec.yaml |
| ''', |
| ), |
| ), |
| ]; |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), |
| ]; |
| |
| final List<String> output = await runCapturingPrint(runner, <String>[ |
| 'version-check', |
| '--base-sha=main', |
| ]); |
| |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[contains('No issues found!')]), |
| ); |
| }, |
| ); |
| |
| test('fails for batch release package with no new changelog', () async { |
| final RepositoryPackage package = createFakePackage( |
| 'package', |
| packagesDir, |
| version: '1.0.0', |
| ); |
| package.ciConfigFile.writeAsStringSync(''' |
| release: |
| batch: true |
| '''); |
| // Simulate a code change |
| package.libDirectory |
| .childFile('foo.dart') |
| .writeAsStringSync('void foo() {}'); |
| // Create the pending_changelogs directory so the test doesn't fail on that check. |
| package.directory.childDirectory('pending_changelogs').createSync(); |
| |
| gitProcessRunner |
| .mockProcessesForExecutable['git-diff'] = <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'packages/package/lib/foo.dart')), |
| ]; |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), |
| ]; |
| |
| Error? commandError; |
| final List<String> output = await runCapturingPrint( |
| runner, |
| <String>[ |
| 'version-check', |
| '--base-sha=main', |
| '--check-for-missing-changes', |
| ], |
| errorHandler: (Error e) { |
| commandError = e; |
| }, |
| ); |
| |
| expect(commandError, isA<ToolExit>()); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains( |
| 'No new changelog files found in the pending_changelogs folder.', |
| ), |
| ]), |
| ); |
| }); |
| |
| test('passes for batch release package with new changelog', () async { |
| final RepositoryPackage package = createFakePackage( |
| 'package', |
| packagesDir, |
| version: '1.0.0', |
| ); |
| package.ciConfigFile.writeAsStringSync(''' |
| release: |
| batch: true |
| '''); |
| // Simulate a code change |
| package.libDirectory |
| .childFile('foo.dart') |
| .writeAsStringSync('void foo() {}'); |
| // Create the pending_changelogs directory so the test doesn't fail on that check. |
| final Directory pendingChangelogs = package.directory.childDirectory( |
| 'pending_changelogs', |
| ); |
| pendingChangelogs.createSync(); |
| pendingChangelogs.childFile('some_change.yaml').writeAsStringSync(''' |
| changelog: "Some change" |
| version: patch |
| '''); |
| |
| gitProcessRunner.mockProcessesForExecutable['git-diff'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo( |
| MockProcess( |
| stdout: ''' |
| packages/package/lib/foo.dart |
| packages/package/pending_changelogs/some_change.yaml |
| ''', |
| ), |
| ), |
| ]; |
| gitProcessRunner.mockProcessesForExecutable['git-show'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), |
| ]; |
| |
| final List<String> output = await runCapturingPrint(runner, <String>[ |
| 'version-check', |
| '--base-sha=main', |
| '--check-for-missing-changes', |
| ]); |
| |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[contains('No issues found!')]), |
| ); |
| }); |
| }); |
| }); |
| |
| group('Pre 1.0', () { |
| test('nextVersion allows patch version', () { |
| testAllowedVersion( |
| '0.12.0', |
| '0.12.0+1', |
| nextVersionType: NextVersionType.PATCH, |
| ); |
| testAllowedVersion( |
| '0.12.0+4', |
| '0.12.0+5', |
| nextVersionType: NextVersionType.PATCH, |
| ); |
| }); |
| |
| test('nextVersion does not allow jumping patch', () { |
| testAllowedVersion('0.12.0', '0.12.0+2', allowed: false); |
| testAllowedVersion('0.12.0+2', '0.12.0+4', allowed: false); |
| }); |
| |
| test('nextVersion does not allow going back', () { |
| testAllowedVersion('0.12.0', '0.11.0', allowed: false); |
| testAllowedVersion('0.12.0+2', '0.12.0+1', allowed: false); |
| testAllowedVersion('0.12.0+1', '0.12.0', allowed: false); |
| }); |
| |
| test('nextVersion allows minor version', () { |
| testAllowedVersion( |
| '0.12.0', |
| '0.12.1', |
| nextVersionType: NextVersionType.MINOR, |
| ); |
| testAllowedVersion( |
| '0.12.0+4', |
| '0.12.1', |
| nextVersionType: NextVersionType.MINOR, |
| ); |
| }); |
| |
| test('nextVersion does not allow jumping minor', () { |
| testAllowedVersion('0.12.0', '0.12.2', allowed: false); |
| testAllowedVersion('0.12.0+2', '0.12.3', allowed: false); |
| }); |
| }); |
| |
| group('Releasing 1.0', () { |
| test('nextVersion allows releasing 1.0', () { |
| testAllowedVersion( |
| '0.12.0', |
| '1.0.0', |
| nextVersionType: NextVersionType.BREAKING_MAJOR, |
| ); |
| testAllowedVersion( |
| '0.12.0+4', |
| '1.0.0', |
| nextVersionType: NextVersionType.BREAKING_MAJOR, |
| ); |
| }); |
| |
| test('nextVersion does not allow jumping major', () { |
| testAllowedVersion('0.12.0', '2.0.0', allowed: false); |
| testAllowedVersion('0.12.0+4', '2.0.0', allowed: false); |
| }); |
| |
| test('nextVersion does not allow un-releasing', () { |
| testAllowedVersion('1.0.0', '0.12.0+4', allowed: false); |
| testAllowedVersion('1.0.0', '0.12.0', allowed: false); |
| }); |
| }); |
| |
| group('Post 1.0', () { |
| test('nextVersion allows patch jumps', () { |
| testAllowedVersion( |
| '1.0.1', |
| '1.0.2', |
| nextVersionType: NextVersionType.PATCH, |
| ); |
| testAllowedVersion( |
| '1.0.0', |
| '1.0.1', |
| nextVersionType: NextVersionType.PATCH, |
| ); |
| }); |
| |
| test('nextVersion does not allow build jumps', () { |
| testAllowedVersion('1.0.1', '1.0.1+1', allowed: false); |
| testAllowedVersion('1.0.0+5', '1.0.0+6', allowed: false); |
| }); |
| |
| test('nextVersion does not allow skipping patches', () { |
| testAllowedVersion('1.0.1', '1.0.3', allowed: false); |
| testAllowedVersion('1.0.0', '1.0.6', allowed: false); |
| }); |
| |
| test('nextVersion allows minor version jumps', () { |
| testAllowedVersion( |
| '1.0.1', |
| '1.1.0', |
| nextVersionType: NextVersionType.MINOR, |
| ); |
| testAllowedVersion( |
| '1.0.0', |
| '1.1.0', |
| nextVersionType: NextVersionType.MINOR, |
| ); |
| }); |
| |
| test('nextVersion does not allow skipping minor versions', () { |
| testAllowedVersion('1.0.1', '1.2.0', allowed: false); |
| testAllowedVersion('1.1.0', '1.3.0', allowed: false); |
| }); |
| |
| test('nextVersion allows breaking changes', () { |
| testAllowedVersion( |
| '1.0.1', |
| '2.0.0', |
| nextVersionType: NextVersionType.BREAKING_MAJOR, |
| ); |
| testAllowedVersion( |
| '1.0.0', |
| '2.0.0', |
| nextVersionType: NextVersionType.BREAKING_MAJOR, |
| ); |
| }); |
| |
| test('nextVersion does not allow skipping major versions', () { |
| testAllowedVersion('1.0.1', '3.0.0', allowed: false); |
| testAllowedVersion('1.1.0', '2.3.0', allowed: false); |
| }); |
| }); |
| } |