| // 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 '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/validate_command.dart'; | |
| import 'package:flutter_plugin_tools/src/validators/version_and_changelog_validator.dart'; | |
| import 'package:git/git.dart'; | |
| import 'package:platform/platform.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() { | |
| group('VersionCheckCommand', () { | |
| late NativePlatform mockPlatform; | |
| late Directory packagesDir; | |
| late CommandRunner<void> runner; | |
| late RecordingProcessRunner gitProcessRunner; | |
| setUp(() { | |
| mockPlatform = createMockPlatform(); | |
| final RecordingProcessRunner processRunner; | |
| final GitDir gitDir; | |
| (:packagesDir, :processRunner, :gitProcessRunner, :gitDir) = configureBaseCommandMocks( | |
| platform: mockPlatform, | |
| ); | |
| final command = ValidateCommand( | |
| packagesDir, | |
| processRunner: processRunner, | |
| platform: mockPlatform, | |
| gitDir: gitDir, | |
| targetedValidators: <Validator>{Validator.version}, | |
| ); | |
| runner = CommandRunner<void>( | |
| 'validate_version_test', | |
| 'Test for version and changelog validation', | |
| ); | |
| 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>[ | |
| 'validate', | |
| '--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>['validate', '--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>['validate']); | |
| 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>['validate']); | |
| 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>[ | |
| 'validate', | |
| '--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>['validate', '--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>[ | |
| 'validate', | |
| '--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>['validate', '--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>[ | |
| 'validate', | |
| '--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('(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>[ | |
| 'validate', | |
| '--base-sha=main', | |
| '--ignore-platform-interface-breaks', | |
| ]); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[ | |
| contains( | |
| 'Ignoring breaking change to plugin_platform_interface due ' | |
| 'to command configuration', | |
| ), | |
| contains('(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>[ | |
| 'validate', | |
| '--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>['validate', '--base-sha=main'], | |
| 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>[ | |
| 'validate', | |
| '--base-sha=main', | |
| ]); | |
| expect(output, containsAllInOrder(<Matcher>[contains('Running for plugin')])); | |
| }); | |
| test('Fail if CHANGELOG list items have a blank line', () async { | |
| const version = '1.0.1'; | |
| final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: version); | |
| // Blank line breaks the list items. | |
| const changelog = | |
| ''' | |
| ## $version | |
| * First item. | |
| * Second item. | |
| * Third item. | |
| '''; | |
| 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>['validate', '--base-sha=main'], | |
| errorHandler: (Error e) { | |
| commandError = e; | |
| }, | |
| ); | |
| expect(commandError, isA<ToolExit>()); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[ | |
| contains('Blank lines found between list items in CHANGELOG.'), | |
| contains('CHANGELOG.md failed validation.'), | |
| ]), | |
| ); | |
| }); | |
| test('Fail if CHANGELOG list items have a blank line with nested items', () async { | |
| const version = '1.0.1'; | |
| final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: version); | |
| // Blank line in nested list items. | |
| const changelog = | |
| ''' | |
| ## $version | |
| * Top level item. | |
| * Nested item A. | |
| * Nested item B. | |
| * Another top level item. | |
| '''; | |
| 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>['validate', '--base-sha=main'], | |
| errorHandler: (Error e) { | |
| commandError = e; | |
| }, | |
| ); | |
| expect(commandError, isA<ToolExit>()); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[ | |
| contains('Blank lines found between list items in CHANGELOG.'), | |
| contains('CHANGELOG.md failed validation.'), | |
| ]), | |
| ); | |
| }); | |
| 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>['validate', '--base-sha=main'], | |
| 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>[ | |
| 'validate', | |
| '--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>['validate', '--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('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>['validate', '--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>['validate', '--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>[ | |
| 'validate', | |
| '--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>[ | |
| 'validate', | |
| '--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>['validate', '--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>['validate', '--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>['validate', '--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>[ | |
| 'validate', | |
| '--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/test_apps/a_test_app/lib/main.dart | |
| packages/plugin/analysis_options.yaml | |
| packages/plugin/AGENTS.md | |
| 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', | |
| ), | |
| ]), | |
| ); | |
| }); | |
| }); | |
| 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>[ | |
| 'validate', | |
| '--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>[ | |
| 'validate', | |
| '--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>[ | |
| 'validate', | |
| '--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>[ | |
| 'validate', | |
| '--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>['validate', '--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>['validate', '--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>['validate', '--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>['validate', '--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>[ | |
| 'validate', | |
| '--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>['validate', '--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 override: batch-<package> 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>[ | |
| 'validate', | |
| '--base-sha=main', | |
| '--pr-labels=override: batch-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>['validate', '--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>['validate', '--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>[ | |
| 'validate', | |
| '--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>['validate', '--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>[ | |
| 'validate', | |
| '--base-sha=main', | |
| '--check-for-missing-changes', | |
| ]); | |
| expect(output, containsAllInOrder(<Matcher>[contains('No issues found!')])); | |
| }); | |
| test('fails for batch release package with version promote on post-1.0 package', () async { | |
| final RepositoryPackage package = createFakePackage( | |
| 'package', | |
| packagesDir, | |
| version: '1.0.0', | |
| ); | |
| package.ciConfigFile.writeAsStringSync(''' | |
| release: | |
| batch: true | |
| '''); | |
| package.libDirectory.childFile('foo.dart').writeAsStringSync('void foo() {}'); | |
| final Directory pendingChangelogs = package.directory.childDirectory('pending_changelogs'); | |
| pendingChangelogs.createSync(); | |
| pendingChangelogs.childFile('some_change.yaml').writeAsStringSync(''' | |
| changelog: "Promoting" | |
| version: promote | |
| '''); | |
| 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')), | |
| ]; | |
| Error? commandError; | |
| final List<String> output = await runCapturingPrint( | |
| runner, | |
| <String>['validate', '--base-sha=main', '--check-for-missing-changes'], | |
| errorHandler: (Error e) { | |
| commandError = e; | |
| }, | |
| ); | |
| expect(commandError, isA<ToolExit>()); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[contains('"promote" is only valid for pre-1.0 packages.')]), | |
| ); | |
| }); | |
| test('passes for batch release package with version promote on pre-1.0 package', () async { | |
| final RepositoryPackage package = createFakePackage( | |
| 'package', | |
| packagesDir, | |
| version: '0.9.0', | |
| ); | |
| package.ciConfigFile.writeAsStringSync(''' | |
| release: | |
| batch: true | |
| '''); | |
| package.libDirectory.childFile('foo.dart').writeAsStringSync('void foo() {}'); | |
| final Directory pendingChangelogs = package.directory.childDirectory('pending_changelogs'); | |
| pendingChangelogs.createSync(); | |
| pendingChangelogs.childFile('some_change.yaml').writeAsStringSync(''' | |
| changelog: "Promoting" | |
| version: promote | |
| '''); | |
| 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: 0.9.0')), | |
| ]; | |
| final List<String> output = await runCapturingPrint(runner, <String>[ | |
| 'validate', | |
| '--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); | |
| }); | |
| }); | |
| } |