// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:args/args.dart'; import 'package:dev_tools/roll_dev.dart'; import 'package:mockito/mockito.dart'; import './common.dart'; void main() { group('run()', () { const String usage = 'usage info...'; const String level = 'z'; const String commit = 'abcde012345'; const String origin = 'upstream'; const String lastVersion = '1.2.0-0.0.pre'; const String nextVersion = '1.2.0-1.0.pre'; FakeArgResults fakeArgResults; MockGit mockGit; setUp(() { mockGit = MockGit(); }); test('returns false if help requested', () { fakeArgResults = FakeArgResults( level: level, commit: commit, origin: origin, help: true, ); expect( run( usage: usage, argResults: fakeArgResults, git: mockGit, ), false, ); }); test('returns false if level not provided', () { fakeArgResults = FakeArgResults( level: null, commit: commit, origin: origin, ); expect( run( usage: usage, argResults: fakeArgResults, git: mockGit, ), false, ); }); test('returns false if commit not provided', () { fakeArgResults = FakeArgResults( level: level, commit: null, origin: origin, ); expect( run( usage: usage, argResults: fakeArgResults, git: mockGit, ), false, ); }); test('throws exception if upstream remote wrong', () { const String remote = 'wrong-remote'; when(mockGit.getOutput('remote get-url $origin', any)).thenReturn(remote); fakeArgResults = FakeArgResults( level: level, commit: commit, origin: origin, ); const String errorMessage = 'The remote named $origin is set to $remote, when $kUpstreamRemote was expected.'; expect( () => run( usage: usage, argResults: fakeArgResults, git: mockGit, ), throwsExceptionWith(errorMessage), ); }); test('throws exception if git checkout not clean', () { when(mockGit.getOutput('remote get-url $origin', any)).thenReturn(kUpstreamRemote); when(mockGit.getOutput('status --porcelain', any)).thenReturn( ' M dev/tools/test/roll_dev_test.dart', ); fakeArgResults = FakeArgResults( level: level, commit: commit, origin: origin, ); Exception exception; try { run( usage: usage, argResults: fakeArgResults, git: mockGit, ); } on Exception catch (e) { exception = e; } const String pattern = r'Your git repository is not clean. Try running ' '"git clean -fd". Warning, this will delete files! Run with -n to find ' 'out which ones.'; expect(exception?.toString(), contains(pattern)); }); test('does not reset or tag if --just-print is specified', () { when(mockGit.getOutput('remote get-url $origin', any)).thenReturn(kUpstreamRemote); when(mockGit.getOutput('status --porcelain', any)).thenReturn(''); when(mockGit.getOutput( 'describe --match *.*.*-*.*.pre --exact-match --tags refs/remotes/$origin/dev', any, )).thenReturn(lastVersion); when(mockGit.getOutput( 'rev-parse $lastVersion', any, )).thenReturn('zxy321'); fakeArgResults = FakeArgResults( level: level, commit: commit, origin: origin, justPrint: true, ); expect(run( usage: usage, argResults: fakeArgResults, git: mockGit, ), false); verify(mockGit.run('fetch $origin', any)); verifyNever(mockGit.run('reset $commit --hard', any)); verifyNever(mockGit.getOutput('rev-parse HEAD', any)); }); test('exits with exception if --skip-tagging is provided but commit isn\'t ' 'already tagged', () { when(mockGit.getOutput('remote get-url $origin', any)).thenReturn(kUpstreamRemote); when(mockGit.getOutput('status --porcelain', any)).thenReturn(''); when(mockGit.getOutput( 'describe --match *.*.*-*.*.pre --exact-match --tags refs/remotes/$origin/dev', any, )).thenReturn(lastVersion); when(mockGit.getOutput( 'rev-parse $lastVersion', any, )).thenReturn('zxy321'); const String exceptionMessage = 'Failed to verify $commit is already ' 'tagged. You can only use the flag `$kSkipTagging` if the commit has ' 'already been tagged.'; when(mockGit.run( 'describe --exact-match --tags $commit', any, )).thenThrow(Exception(exceptionMessage)); fakeArgResults = FakeArgResults( level: level, commit: commit, origin: origin, skipTagging: true, ); expect( () => run( usage: usage, argResults: fakeArgResults, git: mockGit, ), throwsExceptionWith(exceptionMessage), ); verify(mockGit.run('fetch $origin', any)); verifyNever(mockGit.run('reset $commit --hard', any)); verifyNever(mockGit.getOutput('rev-parse HEAD', any)); }); test('throws exception if desired commit is already tip of dev branch', () { when(mockGit.getOutput('remote get-url $origin', any)).thenReturn(kUpstreamRemote); when(mockGit.getOutput('status --porcelain', any)).thenReturn(''); when(mockGit.getOutput( 'describe --match *.*.*-*.*.pre --exact-match --tags refs/remotes/$origin/dev', any, )).thenReturn(lastVersion); when(mockGit.getOutput( 'rev-parse $lastVersion', any, )).thenReturn(commit); fakeArgResults = FakeArgResults( level: level, commit: commit, origin: origin, justPrint: true, ); expect( () => run( usage: usage, argResults: fakeArgResults, git: mockGit, ), throwsExceptionWith('is already on the dev branch as'), ); verify(mockGit.run('fetch $origin', any)); verifyNever(mockGit.run('reset $commit --hard', any)); verifyNever(mockGit.getOutput('rev-parse HEAD', any)); }); test('does not tag if last release is not direct ancestor of desired ' 'commit and --force not supplied', () { when(mockGit.getOutput('remote get-url $origin', any)) .thenReturn(kUpstreamRemote); when(mockGit.getOutput('status --porcelain', any)) .thenReturn(''); when(mockGit.getOutput( 'describe --match *.*.*-*.*.pre --exact-match --tags refs/remotes/$origin/dev', any, )).thenReturn(lastVersion); when(mockGit.getOutput( 'rev-parse $lastVersion', any, )).thenReturn('zxy321'); when(mockGit.run('merge-base --is-ancestor $lastVersion $commit', any)) .thenThrow(Exception( 'Failed to verify $lastVersion is a direct ancestor of $commit. The ' 'flag `--force` is required to force push a new release past a ' 'cherry-pick', )); fakeArgResults = FakeArgResults( level: level, commit: commit, origin: origin, ); const String errorMessage = 'Failed to verify $lastVersion is a direct ' 'ancestor of $commit. The flag `--force` is required to force push a ' 'new release past a cherry-pick'; expect( () => run( argResults: fakeArgResults, git: mockGit, usage: usage, ), throwsExceptionWith(errorMessage), ); verify(mockGit.run('fetch $origin', any)); verifyNever(mockGit.run('reset $commit --hard', any)); verifyNever(mockGit.run('push $origin HEAD:dev', any)); verifyNever(mockGit.run('tag $nextVersion', any)); }); test('does not tag but updates branch if --skip-tagging provided', () { when(mockGit.getOutput('remote get-url $origin', any)) .thenReturn(kUpstreamRemote); when(mockGit.getOutput('status --porcelain', any)) .thenReturn(''); when(mockGit.getOutput( 'describe --match *.*.*-*.*.pre --exact-match --tags refs/remotes/$origin/dev', any, )).thenReturn(lastVersion); when(mockGit.getOutput( 'rev-parse $lastVersion', any, )).thenReturn('zxy321'); when(mockGit.getOutput('rev-parse HEAD', any)).thenReturn(commit); fakeArgResults = FakeArgResults( level: level, commit: commit, origin: origin, skipTagging: true, ); expect(run( usage: usage, argResults: fakeArgResults, git: mockGit, ), true); verify(mockGit.run('fetch $origin', any)); verify(mockGit.run('reset $commit --hard', any)); verifyNever(mockGit.run('tag $nextVersion', any)); verifyNever(mockGit.run('push $origin $nextVersion', any)); verify(mockGit.run('push $origin HEAD:dev', any)); }); test('successfully tags and publishes release', () { when(mockGit.getOutput('remote get-url $origin', any)) .thenReturn(kUpstreamRemote); when(mockGit.getOutput('status --porcelain', any)) .thenReturn(''); when(mockGit.getOutput( 'describe --match *.*.*-*.*.pre --exact-match --tags refs/remotes/$origin/dev', any, )).thenReturn('1.2.0-0.0.pre'); when(mockGit.getOutput( 'rev-parse $lastVersion', any, )).thenReturn('zxy321'); when(mockGit.getOutput('rev-parse HEAD', any)).thenReturn(commit); fakeArgResults = FakeArgResults( level: level, commit: commit, origin: origin, ); expect(run( usage: usage, argResults: fakeArgResults, git: mockGit, ), true); verify(mockGit.run('fetch $origin', any)); verify(mockGit.run('reset $commit --hard', any)); verify(mockGit.run('tag $nextVersion', any)); verify(mockGit.run('push $origin $nextVersion', any)); verify(mockGit.run('push $origin HEAD:dev', any)); }); test('successfully publishes release with --force', () { when(mockGit.getOutput('remote get-url $origin', any)).thenReturn(kUpstreamRemote); when(mockGit.getOutput('status --porcelain', any)).thenReturn(''); when(mockGit.getOutput( 'describe --match *.*.*-*.*.pre --exact-match --tags refs/remotes/$origin/dev', any, )).thenReturn(lastVersion); when(mockGit.getOutput( 'rev-parse $lastVersion', any, )).thenReturn('zxy321'); when(mockGit.getOutput('rev-parse HEAD', any)).thenReturn(commit); fakeArgResults = FakeArgResults( level: level, commit: commit, origin: origin, force: true, ); expect(run( usage: usage, argResults: fakeArgResults, git: mockGit, ), true); verify(mockGit.run('fetch $origin', any)); verify(mockGit.run('reset $commit --hard', any)); verify(mockGit.run('tag $nextVersion', any)); verify(mockGit.run('push --force $origin HEAD:dev', any)); }); }); group('parseFullTag', () { test('returns match on valid version input', () { final List<String> validTags = <String>[ '1.2.3-1.2.pre', '10.2.30-12.22.pre', '1.18.0-0.0.pre', '2.0.0-1.99.pre', '12.34.56-78.90.pre', '0.0.1-0.0.pre', '958.80.144-6.224.pre', ]; for (final String validTag in validTags) { final Match match = parseFullTag(validTag); expect(match, isNotNull, reason: 'Expected $validTag to be parsed'); } }); test('returns null on invalid version input', () { final List<String> invalidTags = <String>[ '1.2.3-1.2.pre-3-gabc123', '1.2.3-1.2.3.pre', '1.2.3.1.2.pre', '1.2.3-dev.1.2', '1.2.3-1.2-3', 'v1.2.3', '2.0.0', 'v1.2.3-1.2.pre', '1.2.3-1.2.pre_', ]; for (final String invalidTag in invalidTags) { final Match match = parseFullTag(invalidTag); expect(match, null, reason: 'Expected $invalidTag to not be parsed'); } }); }); group('getVersionFromParts', () { test('returns correct string from valid parts', () { List<int> parts = <int>[1, 2, 3, 4, 5]; expect(getVersionFromParts(parts), '1.2.3-4.5.pre'); parts = <int>[11, 2, 33, 1, 0]; expect(getVersionFromParts(parts), '11.2.33-1.0.pre'); }); }); group('incrementLevel()', () { const String hash = 'abc123'; test('throws exception if hash is not valid release candidate', () { String level = 'z'; String version = '1.0.0-0.0.pre-1-g$hash'; expect( () => incrementLevel(version, level), throwsExceptionWith('Git reported the latest version as "$version"'), reason: 'should throw because $version should be an exact tag', ); version = '1.2.3'; expect( () => incrementLevel(version, level), throwsExceptionWith('Git reported the latest version as "$version"'), reason: 'should throw because $version should be a dev tag, not stable.' ); version = '1.0.0-0.0.pre-1-g$hash'; level = 'q'; expect( () => incrementLevel(version, level), throwsExceptionWith('Git reported the latest version as "$version"'), reason: 'should throw because $level is unsupported', ); }); test('successfully increments x', () { const String level = 'x'; String version = '1.0.0-0.0.pre'; expect(incrementLevel(version, level), '2.0.0-0.0.pre'); version = '10.20.0-40.50.pre'; expect(incrementLevel(version, level), '11.0.0-0.0.pre'); version = '1.18.0-3.0.pre'; expect(incrementLevel(version, level), '2.0.0-0.0.pre'); }); test('successfully increments y', () { const String level = 'y'; String version = '1.0.0-0.0.pre'; expect(incrementLevel(version, level), '1.1.0-0.0.pre'); version = '10.20.0-40.50.pre'; expect(incrementLevel(version, level), '10.21.0-0.0.pre'); version = '1.18.0-3.0.pre'; expect(incrementLevel(version, level), '1.19.0-0.0.pre'); }); test('successfully increments z', () { const String level = 'z'; String version = '1.0.0-0.0.pre'; expect(incrementLevel(version, level), '1.0.0-1.0.pre'); version = '10.20.0-40.50.pre'; expect(incrementLevel(version, level), '10.20.0-41.0.pre'); version = '1.18.0-3.0.pre'; expect(incrementLevel(version, level), '1.18.0-4.0.pre'); }); }); } Matcher throwsExceptionWith(String messageSubString) { return throwsA( isA<Exception>().having( (Exception e) => e.toString(), 'description', contains(messageSubString), ), ); } class FakeArgResults implements ArgResults { FakeArgResults({ String level, String commit, String origin, bool justPrint = false, bool autoApprove = true, // so we don't have to mock stdin bool help = false, bool force = false, bool skipTagging = false, }) : _parsedArgs = <String, dynamic>{ 'increment': level, 'commit': commit, 'origin': origin, 'just-print': justPrint, 'yes': autoApprove, 'help': help, 'force': force, 'skip-tagging': skipTagging, }; @override String name; @override ArgResults command; @override final List<String> rest = <String>[]; @override List<String> arguments; final Map<String, dynamic> _parsedArgs; @override Iterable<String> get options { return null; } @override dynamic operator [](String name) { return _parsedArgs[name]; } @override bool wasParsed(String name) { return null; } } class MockGit extends Mock implements Git {}