blob: 7d13648b89959f19bf93909299b3d5f71f1f1e21 [file] [log] [blame]
// 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 {}