blob: ed953e0b7fa418627c8ab91242be88afc64f8c1a [file] [log] [blame]
// Copyright 2013 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 'dart:async';
import 'dart:io' as io;
import 'package:args/command_runner.dart';
import 'package:file/file.dart';
import 'package:flutter_plugin_tools/src/common.dart';
import 'package:git/git.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
import 'package:flutter_plugin_tools/src/version_check_command.dart';
import 'package:pub_semver/pub_semver.dart';
import 'util.dart';
void testAllowedVersion(
String masterVersion,
String headVersion, {
bool allowed = true,
NextVersionType nextVersionType,
}) {
final Version master = Version.parse(masterVersion);
final Version head = Version.parse(headVersion);
final Map<Version, NextVersionType> allowedVersions =
getAllowedNextVersions(master, head);
if (allowed) {
expect(allowedVersions, contains(head));
if (nextVersionType != null) {
expect(allowedVersions[head], equals(nextVersionType));
}
} else {
expect(allowedVersions, isNot(contains(head)));
}
}
class MockGitDir extends Mock implements GitDir {}
class MockProcessResult extends Mock implements io.ProcessResult {}
void main() {
group('$VersionCheckCommand', () {
CommandRunner<void> runner;
RecordingProcessRunner processRunner;
List<List<String>> gitDirCommands;
String gitDiffResponse;
Map<String, String> gitShowResponses;
setUp(() {
gitDirCommands = <List<String>>[];
gitDiffResponse = '';
gitShowResponses = <String, String>{};
final MockGitDir gitDir = MockGitDir();
when(gitDir.runCommand(any)).thenAnswer((Invocation invocation) {
gitDirCommands.add(invocation.positionalArguments[0] as List<String>);
final MockProcessResult mockProcessResult = MockProcessResult();
if (invocation.positionalArguments[0][0] == 'diff') {
when<String>(mockProcessResult.stdout as String)
.thenReturn(gitDiffResponse);
} else if (invocation.positionalArguments[0][0] == 'show') {
final String response =
gitShowResponses[invocation.positionalArguments[0][1]];
when<String>(mockProcessResult.stdout as String).thenReturn(response);
} else if (invocation.positionalArguments[0][0] == 'merge-base') {
when<String>(mockProcessResult.stdout as String).thenReturn('abc123');
}
return Future<io.ProcessResult>.value(mockProcessResult);
});
initializeFakePackages();
processRunner = RecordingProcessRunner();
final VersionCheckCommand command = VersionCheckCommand(
mockPackagesDir, mockFileSystem,
processRunner: processRunner, gitDir: gitDir);
runner = CommandRunner<void>(
'version_check_command', 'Test for $VersionCheckCommand');
runner.addCommand(command);
});
tearDown(() {
cleanupPackages();
});
test('allows valid version', () async {
createFakePlugin('plugin', includeChangeLog: true, includeVersion: true);
gitDiffResponse = 'packages/plugin/pubspec.yaml';
gitShowResponses = <String, String>{
'master:packages/plugin/pubspec.yaml': 'version: 1.0.0',
'HEAD:packages/plugin/pubspec.yaml': 'version: 2.0.0',
};
final List<String> output = await runCapturingPrint(
runner, <String>['version-check', '--base-sha=master']);
expect(
output,
containsAllInOrder(<String>[
'No version check errors found!',
]),
);
expect(gitDirCommands.length, equals(3));
expect(
gitDirCommands,
containsAll(<Matcher>[
equals(<String>['diff', '--name-only', 'master', 'HEAD']),
equals(<String>['show', 'master:packages/plugin/pubspec.yaml']),
equals(<String>['show', 'HEAD:packages/plugin/pubspec.yaml']),
]));
});
test('denies invalid version', () async {
createFakePlugin('plugin', includeChangeLog: true, includeVersion: true);
gitDiffResponse = 'packages/plugin/pubspec.yaml';
gitShowResponses = <String, String>{
'master:packages/plugin/pubspec.yaml': 'version: 0.0.1',
'HEAD:packages/plugin/pubspec.yaml': 'version: 0.2.0',
};
final Future<List<String>> result = runCapturingPrint(
runner, <String>['version-check', '--base-sha=master']);
await expectLater(
result,
throwsA(const TypeMatcher<ToolExit>()),
);
expect(gitDirCommands.length, equals(3));
expect(
gitDirCommands,
containsAll(<Matcher>[
equals(<String>['diff', '--name-only', 'master', 'HEAD']),
equals(<String>['show', 'master:packages/plugin/pubspec.yaml']),
equals(<String>['show', 'HEAD:packages/plugin/pubspec.yaml']),
]));
});
test('allows valid version without explicit base-sha', () async {
createFakePlugin('plugin', includeChangeLog: true, includeVersion: true);
gitDiffResponse = 'packages/plugin/pubspec.yaml';
gitShowResponses = <String, String>{
'abc123:packages/plugin/pubspec.yaml': 'version: 1.0.0',
'HEAD:packages/plugin/pubspec.yaml': 'version: 2.0.0',
};
final List<String> output =
await runCapturingPrint(runner, <String>['version-check']);
expect(
output,
containsAllInOrder(<String>[
'No version check errors found!',
]),
);
});
test('denies invalid version without explicit base-sha', () async {
createFakePlugin('plugin', includeChangeLog: true, includeVersion: true);
gitDiffResponse = 'packages/plugin/pubspec.yaml';
gitShowResponses = <String, String>{
'abc123:packages/plugin/pubspec.yaml': 'version: 0.0.1',
'HEAD:packages/plugin/pubspec.yaml': 'version: 0.2.0',
};
final Future<List<String>> result =
runCapturingPrint(runner, <String>['version-check']);
await expectLater(
result,
throwsA(const TypeMatcher<ToolExit>()),
);
});
test('gracefully handles missing pubspec.yaml', () async {
createFakePlugin('plugin', includeChangeLog: true, includeVersion: true);
gitDiffResponse = 'packages/plugin/pubspec.yaml';
mockFileSystem.currentDirectory
.childDirectory('packages')
.childDirectory('plugin')
.childFile('pubspec.yaml')
.deleteSync();
final List<String> output = await runCapturingPrint(
runner, <String>['version-check', '--base-sha=master']);
expect(
output,
orderedEquals(<String>[
'Determine diff with base sha: master',
'Checking versions for packages/plugin/pubspec.yaml...',
' Deleted; skipping.',
'No version check errors found!',
]),
);
expect(gitDirCommands.length, equals(1));
expect(gitDirCommands.first.join(' '),
equals('diff --name-only master HEAD'));
});
test('allows minor changes to platform interfaces', () async {
createFakePlugin('plugin_platform_interface',
includeChangeLog: true, includeVersion: true);
gitDiffResponse = 'packages/plugin_platform_interface/pubspec.yaml';
gitShowResponses = <String, String>{
'master:packages/plugin_platform_interface/pubspec.yaml':
'version: 1.0.0',
'HEAD:packages/plugin_platform_interface/pubspec.yaml':
'version: 1.1.0',
};
final List<String> output = await runCapturingPrint(
runner, <String>['version-check', '--base-sha=master']);
expect(
output,
containsAllInOrder(<String>[
'No version check errors found!',
]),
);
expect(gitDirCommands.length, equals(3));
expect(
gitDirCommands,
containsAll(<Matcher>[
equals(<String>['diff', '--name-only', 'master', 'HEAD']),
equals(<String>[
'show',
'master:packages/plugin_platform_interface/pubspec.yaml'
]),
equals(<String>[
'show',
'HEAD:packages/plugin_platform_interface/pubspec.yaml'
]),
]));
});
test('disallows breaking changes to platform interfaces', () async {
createFakePlugin('plugin_platform_interface',
includeChangeLog: true, includeVersion: true);
gitDiffResponse = 'packages/plugin_platform_interface/pubspec.yaml';
gitShowResponses = <String, String>{
'master:packages/plugin_platform_interface/pubspec.yaml':
'version: 1.0.0',
'HEAD:packages/plugin_platform_interface/pubspec.yaml':
'version: 2.0.0',
};
final Future<List<String>> output = runCapturingPrint(
runner, <String>['version-check', '--base-sha=master']);
await expectLater(
output,
throwsA(const TypeMatcher<ToolExit>()),
);
expect(gitDirCommands.length, equals(3));
expect(
gitDirCommands,
containsAll(<Matcher>[
equals(<String>['diff', '--name-only', 'master', 'HEAD']),
equals(<String>[
'show',
'master:packages/plugin_platform_interface/pubspec.yaml'
]),
equals(<String>[
'show',
'HEAD:packages/plugin_platform_interface/pubspec.yaml'
]),
]));
});
test('Allow empty lines in front of the first version in CHANGELOG',
() async {
createFakePlugin('plugin', includeChangeLog: true, includeVersion: true);
final Directory pluginDirectory =
mockPackagesDir.childDirectory('plugin');
createFakePubspec(pluginDirectory,
isFlutter: true, includeVersion: true, version: '1.0.1');
const String changelog = '''
## 1.0.1
* Some changes.
''';
createFakeCHANGELOG(pluginDirectory, changelog);
final List<String> output = await runCapturingPrint(
runner, <String>['version-check', '--base-sha=master']);
expect(
output,
containsAllInOrder(<String>[
'Checking the first version listed in CHANGELOG.md matches the version in pubspec.yaml for plugin.',
'plugin passed version check',
'No version check errors found!'
]),
);
});
test('Throws if versions in changelog and pubspec do not match', () async {
createFakePlugin('plugin', includeChangeLog: true, includeVersion: true);
final Directory pluginDirectory =
mockPackagesDir.childDirectory('plugin');
createFakePubspec(pluginDirectory,
isFlutter: true, includeVersion: true, version: '1.0.1');
const String changelog = '''
## 1.0.2
* Some changes.
''';
createFakeCHANGELOG(pluginDirectory, changelog);
final Future<List<String>> output = runCapturingPrint(
runner, <String>['version-check', '--base-sha=master']);
await expectLater(
output,
throwsA(const TypeMatcher<ToolExit>()),
);
try {
final List<String> outputValue = await output;
await expectLater(
outputValue,
containsAllInOrder(<String>[
'''
versions for plugin in CHANGELOG.md and pubspec.yaml do not match.
The version in pubspec.yaml is 1.0.1.
The first version listed in CHANGELOG.md is 1.0.2.
''',
]),
);
} on ToolExit catch (_) {}
});
test('Success if CHANGELOG and pubspec versions match', () async {
createFakePlugin('plugin', includeChangeLog: true, includeVersion: true);
final Directory pluginDirectory =
mockPackagesDir.childDirectory('plugin');
createFakePubspec(pluginDirectory,
isFlutter: true, includeVersion: true, version: '1.0.1');
const String changelog = '''
## 1.0.1
* Some changes.
''';
createFakeCHANGELOG(pluginDirectory, changelog);
final List<String> output = await runCapturingPrint(
runner, <String>['version-check', '--base-sha=master']);
expect(
output,
containsAllInOrder(<String>[
'Checking the first version listed in CHANGELOG.md matches the version in pubspec.yaml for plugin.',
'plugin passed version check',
'No version check errors found!'
]),
);
});
test(
'Fail if pubspec version only matches an older version listed in CHANGELOG',
() async {
createFakePlugin('plugin', includeChangeLog: true, includeVersion: true);
final Directory pluginDirectory =
mockPackagesDir.childDirectory('plugin');
createFakePubspec(pluginDirectory,
isFlutter: true, includeVersion: true, version: '1.0.0');
const String changelog = '''
## 1.0.1
* Some changes.
## 1.0.0
* Some other changes.
''';
createFakeCHANGELOG(pluginDirectory, changelog);
final Future<List<String>> output = runCapturingPrint(
runner, <String>['version-check', '--base-sha=master']);
await expectLater(
output,
throwsA(const TypeMatcher<ToolExit>()),
);
try {
final List<String> outputValue = await output;
await expectLater(
outputValue,
containsAllInOrder(<String>[
'''
versions for plugin in CHANGELOG.md and pubspec.yaml do not match.
The version in pubspec.yaml is 1.0.0.
The first version listed in CHANGELOG.md is 1.0.1.
''',
]),
);
} on ToolExit catch (_) {}
});
test('Allow NEXT as a placeholder for gathering CHANGELOG entries',
() async {
createFakePlugin('plugin', includeChangeLog: true, includeVersion: true);
final Directory pluginDirectory =
mockPackagesDir.childDirectory('plugin');
createFakePubspec(pluginDirectory,
isFlutter: true, includeVersion: true, version: '1.0.0');
const String changelog = '''
## NEXT
* Some changes that won't be published until the next time there's a release.
## 1.0.0
* Some other changes.
''';
createFakeCHANGELOG(pluginDirectory, changelog);
final List<String> output = await runCapturingPrint(
runner, <String>['version-check', '--base-sha=master']);
await expectLater(
output,
containsAllInOrder(<String>[
'Found NEXT; validating next version in the CHANGELOG.',
'plugin passed version check',
'No version check errors found!',
]),
);
});
test('Fail if NEXT is left in the CHANGELOG when adding a version bump',
() async {
createFakePlugin('plugin', includeChangeLog: true, includeVersion: true);
final Directory pluginDirectory =
mockPackagesDir.childDirectory('plugin');
createFakePubspec(pluginDirectory,
isFlutter: true, includeVersion: true, version: '1.0.1');
const String changelog = '''
## 1.0.1
* Some changes.
## NEXT
* Some changes that should have been folded in 1.0.1.
## 1.0.0
* Some other changes.
''';
createFakeCHANGELOG(pluginDirectory, changelog);
final Future<List<String>> output = runCapturingPrint(
runner, <String>['version-check', '--base-sha=master']);
await expectLater(
output,
throwsA(const TypeMatcher<ToolExit>()),
);
try {
final List<String> outputValue = await output;
await expectLater(
outputValue,
containsAllInOrder(<String>[
'''
versions for plugin in CHANGELOG.md and pubspec.yaml do not match.
The version in pubspec.yaml is 1.0.0.
The first version listed in CHANGELOG.md is 1.0.1.
''',
]),
);
} on ToolExit catch (_) {}
});
test('Fail if the version changes without replacing NEXT', () async {
createFakePlugin('plugin', includeChangeLog: true, includeVersion: true);
final Directory pluginDirectory =
mockPackagesDir.childDirectory('plugin');
createFakePubspec(pluginDirectory,
isFlutter: true, includeVersion: true, version: '1.0.1');
const String changelog = '''
## NEXT
* Some changes that should be listed as part of 1.0.1.
## 1.0.0
* Some other changes.
''';
createFakeCHANGELOG(pluginDirectory, changelog);
final Future<List<String>> output = runCapturingPrint(
runner, <String>['version-check', '--base-sha=master']);
await expectLater(
output,
throwsA(const TypeMatcher<ToolExit>()),
);
try {
final List<String> outputValue = await output;
await expectLater(
outputValue,
containsAllInOrder(<String>[
'''
versions for plugin in CHANGELOG.md and pubspec.yaml do not match.
The version in pubspec.yaml is 1.0.0.
The first version listed in CHANGELOG.md is 1.0.1.
''',
]),
);
} on ToolExit catch (_) {}
});
});
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);
});
});
}