// 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 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 runner; late RecordingProcessRunner gitProcessRunner; // Ignored if mockHttpResponse is set. int mockHttpStatus; Map? 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( '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(MockProcess(stdout: 'version: 1.0.0')), ]; final List output = await runCapturingPrint(runner, [ 'version-check', '--base-sha=main', ]); expect( output, containsAllInOrder([ contains('Running for plugin'), contains('1.0.0 -> 2.0.0'), ]), ); expect( gitProcessRunner.recordedCalls, containsAllInOrder(const [ ProcessCall('git-show', [ 'main:packages/plugin/pubspec.yaml', ], null), ]), ); }); test('denies invalid version', () async { createFakePlugin('plugin', packagesDir, version: '0.2.0'); gitProcessRunner.mockProcessesForExecutable['git-show'] = [ FakeProcessInfo(MockProcess(stdout: 'version: 0.0.1')), ]; Error? commandError; final List output = await runCapturingPrint( runner, ['version-check', '--base-sha=main'], errorHandler: (Error e) { commandError = e; }, ); expect(commandError, isA()); expect( output, containsAllInOrder([contains('Incorrectly updated version.')]), ); expect( gitProcessRunner.recordedCalls, containsAllInOrder(const [ ProcessCall('git-show', [ '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(MockProcess(stdout: 'abc123')), FakeProcessInfo(MockProcess(stdout: 'abc123')), ]; gitProcessRunner.mockProcessesForExecutable['git-show'] = [ FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), ]; final List output = await runCapturingPrint(runner, [ 'version-check', ]); expect( output, containsAllInOrder([ contains('Running for plugin'), contains('1.0.0 -> 2.0.0'), ]), ); expect( gitProcessRunner.recordedCalls, containsAllInOrder(const [ ProcessCall('git-merge-base', [ '--fork-point', 'main', 'HEAD', ], null), ProcessCall('git-show', [ 'abc123:packages/plugin/pubspec.yaml', ], null), ]), ); }); test('allows valid version for new package.', () async { createFakePlugin('plugin', packagesDir, version: '1.0.0'); final List output = await runCapturingPrint(runner, [ 'version-check', ]); expect( output, containsAllInOrder([ 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(MockProcess(stdout: 'version: 0.6.2')), ]; final List output = await runCapturingPrint(runner, [ 'version-check', '--base-sha=main', ]); expect( output, containsAllInOrder([ contains( 'New version is lower than previous version. ' 'This is assumed to be a revert.', ), ]), ); expect( gitProcessRunner.recordedCalls, containsAllInOrder(const [ ProcessCall('git-show', [ '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(MockProcess(stdout: 'version: 0.6.2')), ]; Error? commandError; final List output = await runCapturingPrint( runner, ['version-check', '--base-sha=main'], errorHandler: (Error e) { commandError = e; }, ); expect(commandError, isA()); expect( output, containsAllInOrder([contains('Incorrectly updated version.')]), ); expect( gitProcessRunner.recordedCalls, containsAllInOrder(const [ ProcessCall('git-show', [ '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(MockProcess(stdout: 'version: 1.0.0')), ]; final List output = await runCapturingPrint(runner, [ 'version-check', '--base-sha=main', ]); expect( output, containsAllInOrder([ contains('Running for plugin'), contains('1.0.0 -> 1.1.0'), ]), ); expect( gitProcessRunner.recordedCalls, containsAllInOrder(const [ ProcessCall('git-show', [ '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(MockProcess(stdout: 'version: 1.0.0')), ]; Error? commandError; final List output = await runCapturingPrint( runner, ['version-check', '--base-sha=main'], errorHandler: (Error e) { commandError = e; }, ); expect(commandError, isA()); expect( output, containsAllInOrder([ 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('git-show', [ '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(MockProcess(stdout: 'version: 1.0.0')), ]; final List output = await runCapturingPrint(runner, [ 'version-check', '--base-sha=main', '--pr-labels=some label,override: allow breaking change,another-label', ]); expect( output, containsAllInOrder([ 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('git-show', [ '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(MockProcess(stdout: 'version: 1.0.0')), ]; final List output = await runCapturingPrint(runner, [ 'version-check', '--base-sha=main', '--ignore-platform-interface-breaks', ]); expect( output, containsAllInOrder([ 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('git-show', [ '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 output = await runCapturingPrint(runner, [ 'version-check', '--base-sha=main', ]); expect( output, containsAllInOrder([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 output = await runCapturingPrint( runner, ['version-check', '--base-sha=main', '--against-pub'], errorHandler: (Error e) { commandError = e; }, ); expect(commandError, isA()); expect( output, containsAllInOrder([ 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 output = await runCapturingPrint(runner, [ 'version-check', '--base-sha=main', ]); expect( output, containsAllInOrder([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 output = await runCapturingPrint( runner, ['version-check', '--base-sha=main', '--against-pub'], errorHandler: (Error e) { expect(e, isA()); hasError = true; }, ); expect(hasError, isTrue); expect( output, containsAllInOrder([ 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(MockProcess(stdout: 'version: 1.0.0')), ]; final List output = await runCapturingPrint(runner, [ 'version-check', '--base-sha=main', ]); expect( output, containsAllInOrder([ 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 output = await runCapturingPrint( runner, ['version-check', '--base-sha=main', '--against-pub'], errorHandler: (Error e) { expect(e, isA()); hasError = true; }, ); expect(hasError, isTrue); expect( output, containsAllInOrder([ 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(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 output = await runCapturingPrint( runner, ['version-check', '--base-sha=main'], errorHandler: (Error e) { expect(e, isA()); hasError = true; }, ); expect(hasError, isTrue); expect( output, containsAllInOrder([ 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(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 output = await runCapturingPrint( runner, ['version-check', '--base-sha=main'], errorHandler: (Error e) { expect(e, isA()); hasError = true; }, ); expect(hasError, isTrue); expect( output, containsAllInOrder([ 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(MockProcess(stdout: 'version: 1.0.1')), ]; final List output = await runCapturingPrint(runner, [ 'version-check', '--base-sha=main', ]); expect( output, containsAllInOrder([ 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 output = await runCapturingPrint(runner, [ 'version-check', '--base-sha=main', ]); expect( output, containsAllInOrder([ 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(MockProcess(stdout: 'version: 1.0.0')), ]; Error? commandError; final List output = await runCapturingPrint( runner, ['version-check', '--base-sha=main'], errorHandler: (Error e) { commandError = e; }, ); expect(commandError, isA()); expect( output, containsAllInOrder([ 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(MockProcess(stdout: 'version: 1.0.0')), ]; Error? commandError; final List output = await runCapturingPrint( runner, ['version-check', '--base-sha=main'], errorHandler: (Error e) { commandError = e; }, ); expect(commandError, isA()); expect( output, containsAllInOrder([ 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(MockProcess(stdout: 'version: 1.0.0')), ]; Error? commandError; final List output = await runCapturingPrint( runner, ['version-check', '--base-sha=main'], errorHandler: (Error e) { commandError = e; }, ); expect(commandError, isA()); expect( output, containsAllInOrder([ contains('"Alpha" could not be parsed as a version.'), ]), ); }); group('missing change detection', () { Future> runWithMissingChangeDetection( List extraArgs, { void Function(Error error)? errorHandler, }) async { return runCapturingPrint(runner, [ '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(MockProcess(stdout: 'version: 1.0.0')), ]; gitProcessRunner.mockProcessesForExecutable['git-diff'] = [FakeProcessInfo(MockProcess(stdout: ''))]; final List output = await runWithMissingChangeDetection( [], ); expect( output, containsAllInOrder([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(MockProcess(stdout: 'version: 1.0.0')), ]; gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo( MockProcess( stdout: ''' packages/plugin/pending_changelogs/some_entry.yaml ''', ), ), ]; final List output = await runWithMissingChangeDetection( [], ); expect( output, containsAllInOrder([ 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(MockProcess(stdout: 'version: 1.0.0')), ]; gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo( MockProcess( stdout: ''' packages/plugin/lib/plugin.dart ''', ), ), ]; Error? commandError; final List output = await runWithMissingChangeDetection( [], errorHandler: (Error e) { commandError = e; }, ); expect(commandError, isA()); expect( output, containsAllInOrder([ 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(MockProcess(stdout: 'version: 1.0.0')), ]; gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo( MockProcess( stdout: ''' packages/plugin/lib/plugin.dart packages/plugin/CHANGELOG.md packages/plugin/pubspec.yaml ''', ), ), ]; final List output = await runWithMissingChangeDetection( [], ); expect( output, containsAllInOrder([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(MockProcess(stdout: 'version: 1.0.0')), ]; gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo( MockProcess( stdout: ''' packages/plugin_a/lib/plugin.dart tool/plugin/lib/plugin.dart ''', ), ), ]; final List output = await runWithMissingChangeDetection( [], ); expect( output, containsAllInOrder([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(MockProcess(stdout: 'version: 1.0.0')), ]; gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ 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 output = await runWithMissingChangeDetection( [], ); expect( output, containsAllInOrder([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(MockProcess(stdout: 'version: 1.0.0')), ]; gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo( MockProcess( stdout: ''' packages/plugin/lib/plugin.dart packages/plugin/CHANGELOG.md packages/plugin/pubspec.yaml ''', ), ), ]; final List output = await runWithMissingChangeDetection([ '--pr-labels=some label,override: no versioning needed,another-label', ]); expect( output, containsAllInOrder([ 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(MockProcess(stdout: 'version: 1.0.0')), ]; gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo( MockProcess( stdout: ''' packages/plugin/example/lib/foo.dart ''', ), ), ]; Error? commandError; final List output = await runWithMissingChangeDetection( [], errorHandler: (Error e) { commandError = e; }, ); expect(commandError, isA()); expect( output, containsAllInOrder([ 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(MockProcess(stdout: 'version: 1.0.0')), ]; gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo( MockProcess( stdout: ''' packages/plugin/example/lib/foo.dart packages/plugin/CHANGELOG.md ''', ), ), ]; final List output = await runWithMissingChangeDetection( [], ); expect( output, containsAllInOrder([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(MockProcess(stdout: 'version: 1.0.0')), ]; gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo( MockProcess( stdout: ''' packages/plugin/example/lib/foo.dart packages/another_plugin/CHANGELOG.md ''', ), ), ]; Error? commandError; final List output = await runWithMissingChangeDetection( [], errorHandler: (Error e) { commandError = e; }, ); expect(commandError, isA()); expect( output, containsAllInOrder([ 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(MockProcess(stdout: 'version: 1.0.0')), ]; gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo( MockProcess( stdout: ''' packages/plugin/example/lib/foo.dart ''', ), ), ]; final List output = await runWithMissingChangeDetection([ '--pr-labels=some label,override: no changelog needed,another-label', ]); expect( output, containsAllInOrder([ 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(MockProcess(stdout: 'version: 1.0.0')), ]; gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ // 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 output = await runWithMissingChangeDetection( [], ); expect( output, containsAllInOrder([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(MockProcess(stdout: 'version: 1.0.0')), ]; gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ // File list. FakeProcessInfo( MockProcess( stdout: ''' packages/plugin/tool/run_tests.dart packages/plugin/run_tests.sh ''', ), ), ]; final List output = await runWithMissingChangeDetection( [], ); expect( output, containsAllInOrder([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(MockProcess(stdout: 'version: 1.0.0')), ]; gitProcessRunner .mockProcessesForExecutable['git-diff'] = [ // File list. FakeProcessInfo( MockProcess( stdout: ''' packages/plugin/lib/plugin.dart ''', ), ), // Dart file diff. FakeProcessInfo( MockProcess( stdout: ''' + // TODO(someone): Fix this. + // ignore: some_lint ''', ), ['main', 'HEAD', '--', 'packages/plugin/lib/plugin.dart'], ), ]; final List output = await runWithMissingChangeDetection( [], ); expect( output, containsAllInOrder([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(MockProcess(stdout: 'version: 1.0.0')), ]; gitProcessRunner .mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo( MockProcess( stdout: ''' packages/plugin/lib/plugin.dart ''', ), ), // Dart file diff. FakeProcessInfo( MockProcess( stdout: ''' + /// Important new information for API clients! ''', ), ['main', 'HEAD', '--', 'packages/plugin/lib/plugin.dart'], ), ]; Error? commandError; final List output = await runWithMissingChangeDetection( [], errorHandler: (Error e) { commandError = e; }, ); expect(commandError, isA()); expect( output, containsAllInOrder([ 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 = { 'name': 'some_package', 'versions': ['0.0.1', '0.0.2', '1.0.0'], }; createFakePlugin('plugin', packagesDir, version: '2.0.0'); final List output = await runCapturingPrint(runner, [ 'version-check', '--base-sha=main', '--against-pub', ]); expect( output, containsAllInOrder([ contains('plugin: Current largest version on pub: 1.0.0'), ]), ); }); test('denies invalid against pub', () async { mockHttpResponse = { 'name': 'some_package', 'versions': ['0.0.1', '0.0.2'], }; createFakePlugin('plugin', packagesDir, version: '2.0.0'); var hasError = false; final List result = await runCapturingPrint( runner, ['version-check', '--base-sha=main', '--against-pub'], errorHandler: (Error e) { expect(e, isA()); hasError = true; }, ); expect(hasError, isTrue); expect( result, containsAllInOrder([ 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 result = await runCapturingPrint( runner, ['version-check', '--base-sha=main', '--against-pub'], errorHandler: (Error e) { expect(e, isA()); hasError = true; }, ); expect(hasError, isTrue); expect( result, containsAllInOrder([ 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(MockProcess(stdout: 'version: 1.0.0')), ]; final List result = await runCapturingPrint(runner, [ 'version-check', '--base-sha=main', '--against-pub', ]); expect( result, containsAllInOrder([ 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(MockProcess(stdout: 'version: 1.0.0')), ]; final List output = await runCapturingPrint(runner, [ 'version-check', '--base-sha=main', ]); expect( output, containsAllInOrder([ contains('Running for plugin'), contains('1.0.0 -> 2.0.0-dev'), ]), ); expect( gitProcessRunner.recordedCalls, containsAllInOrder(const [ ProcessCall('git-show', [ '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(MockProcess(stdout: 'version: 1.2.0-dev')), ]; final List output = await runCapturingPrint(runner, [ 'version-check', '--base-sha=main', ]); expect( output, containsAllInOrder([ contains('Running for plugin'), contains('1.2.0-dev -> 1.2.0'), ]), ); expect( gitProcessRunner.recordedCalls, containsAllInOrder(const [ ProcessCall('git-show', [ '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(MockProcess(stdout: 'version: 1.2.0-dev')), ]; final List output = await runCapturingPrint(runner, [ 'version-check', '--base-sha=main', ]); expect( output, containsAllInOrder([ contains('Running for plugin'), contains('1.2.0-dev -> 2.0.0'), ]), ); expect( gitProcessRunner.recordedCalls, containsAllInOrder(const [ ProcessCall('git-show', [ '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(MockProcess(stdout: 'version: 1.2.0-dev.1')), ]; final List output = await runCapturingPrint(runner, [ 'version-check', '--base-sha=main', ]); expect( output, containsAllInOrder([ contains('Running for plugin'), contains('1.2.0-dev.1 -> 1.2.0-dev.2'), ]), ); expect( gitProcessRunner.recordedCalls, containsAllInOrder(const [ ProcessCall('git-show', [ '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(MockProcess(stdout: 'version: 0.0.1')), ]; Error? commandError; final List output = await runCapturingPrint( runner, ['version-check', '--base-sha=main'], errorHandler: (Error e) { commandError = e; }, ); expect(commandError, isA()); expect( output, containsAllInOrder([ contains('Incorrectly updated version.'), ]), ); expect( gitProcessRunner.recordedCalls, containsAllInOrder(const [ ProcessCall('git-show', [ '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(MockProcess(stdout: 'version: 0.0.1-dev')), ]; Error? commandError; final List output = await runCapturingPrint( runner, ['version-check', '--base-sha=main'], errorHandler: (Error e) { commandError = e; }, ); expect(commandError, isA()); expect( output, containsAllInOrder([ contains('Incorrectly updated version.'), ]), ); expect( gitProcessRunner.recordedCalls, containsAllInOrder(const [ ProcessCall('git-show', [ '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(MockProcess(stdout: 'version: 0.0.1-dev')), ]; Error? commandError; final List output = await runCapturingPrint( runner, ['version-check', '--base-sha=main'], errorHandler: (Error e) { commandError = e; }, ); expect(commandError, isA()); expect( output, containsAllInOrder([ contains('Incorrectly updated version.'), ]), ); expect( gitProcessRunner.recordedCalls, containsAllInOrder(const [ ProcessCall('git-show', [ '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 output = await runCapturingPrint( runner, ['version-check', '--base-sha=main'], errorHandler: (Error e) { commandError = e; }, ); expect(commandError, isA()); expect( output, containsAllInOrder([ 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(MockProcess(stdout: 'version: 0.0.1')), ]; final List output = await runCapturingPrint(runner, [ 'version-check', '--base-sha=main', ]); expect( output, containsAllInOrder([ 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 output = await runCapturingPrint( runner, ['version-check', '--base-sha=main'], errorHandler: (Error e) { commandError = e; }, ); expect(commandError, isA()); expect( output, containsAllInOrder([ 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( MockProcess( stdout: ''' packages/package/CHANGELOG.md packages/package/pubspec.yaml ''', ), ), ]; gitProcessRunner.mockProcessesForExecutable['git-show'] = [ FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), ]; final List output = await runCapturingPrint(runner, [ 'version-check', '--base-sha=main', '--pr-labels=post-release-package', ]); expect( output, containsAllInOrder([ 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( MockProcess( stdout: ''' packages/package/CHANGELOG.md ''', ), ), ]; gitProcessRunner.mockProcessesForExecutable['git-show'] = [ FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), ]; Error? commandError; final List output = await runCapturingPrint( runner, ['version-check', '--base-sha=main'], errorHandler: (Error e) { commandError = e; }, ); expect(commandError, isA()); expect( output, containsAllInOrder([ 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( MockProcess( stdout: ''' packages/package/pubspec.yaml ''', ), ), ]; gitProcessRunner.mockProcessesForExecutable['git-show'] = [ FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), ]; Error? commandError; final List output = await runCapturingPrint( runner, ['version-check', '--base-sha=main'], errorHandler: (Error e) { commandError = e; }, ); expect(commandError, isA()); expect( output, containsAllInOrder([ 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( MockProcess( stdout: ''' packages/package/pubspec.yaml ''', ), ), ]; gitProcessRunner.mockProcessesForExecutable['git-show'] = [ FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), ]; final List output = await runCapturingPrint(runner, [ 'version-check', '--base-sha=main', ]); expect( output, containsAllInOrder([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(MockProcess(stdout: 'packages/package/lib/foo.dart')), ]; gitProcessRunner.mockProcessesForExecutable['git-show'] = [ FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), ]; Error? commandError; final List output = await runCapturingPrint( runner, [ 'version-check', '--base-sha=main', '--check-for-missing-changes', ], errorHandler: (Error e) { commandError = e; }, ); expect(commandError, isA()); expect( output, containsAllInOrder([ 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( MockProcess( stdout: ''' packages/package/lib/foo.dart packages/package/pending_changelogs/some_change.yaml ''', ), ), ]; gitProcessRunner.mockProcessesForExecutable['git-show'] = [ FakeProcessInfo(MockProcess(stdout: 'version: 1.0.0')), ]; final List output = await runCapturingPrint(runner, [ 'version-check', '--base-sha=main', '--check-for-missing-changes', ]); expect( output, containsAllInOrder([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); }); }); }