blob: 1451d42502f2c54977f60439292d8587aed1d89c [file] [log] [blame]
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. 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:convert';
import 'dart:io';
import 'package:path/path.dart' as p;
import 'package:pub/src/io.dart';
import 'package:pub_semver/pub_semver.dart';
import 'package:shelf/shelf.dart' as shelf;
import 'package:test/test.dart';
import 'package:yaml_edit/yaml_edit.dart';
import '../descriptor.dart' as d;
import '../descriptor.dart';
import '../golden_file.dart';
import '../test_pub.dart';
void manifestAndLockfile(GoldenTestContext context) {
String catFile(String filename) {
final path = p.join(d.sandbox, appPath, filename);
if (File(path).existsSync()) {
final contents = File(path).readAsLinesSync().map(filterUnstableText);
return '''
\$ cat $filename
${contents.join('\n')}''';
} else {
return '''
\$ cat $filename
No such file $filename.''';
}
}
context.expectNextSection('''
${catFile('pubspec.yaml')}
${catFile('pubspec.lock')}
''');
}
late final String snapshot;
extension on GoldenTestContext {
/// Returns the stdout.
Future<String> runDependencyServices(List<String> args,
{String? stdin}) async {
final buffer = StringBuffer();
buffer.writeln('## Section ${args.join(' ')}');
final process = await Process.start(
Platform.resolvedExecutable,
[
snapshot,
'--verbose',
...args,
],
environment: getPubTestEnvironment(),
workingDirectory: p.join(d.sandbox, appPath),
);
if (stdin != null) {
process.stdin.write(stdin);
await process.stdin.flush();
await process.stdin.close();
}
final outLines = outputLines(process.stdout);
final errLines = outputLines(process.stderr);
final exitCode = await process.exitCode;
final pipe = stdin == null ? '' : ' echo ${escapeShellArgument(stdin)} |';
buffer.writeln([
'\$$pipe dependency_services ${args.map(escapeShellArgument).join(' ')}',
...await outLines,
...(await errLines).map((e) => '[STDERR] $e'),
if (exitCode != 0) '[EXIT CODE] $exitCode',
].join('\n'));
expectNextSection(buffer.toString());
return (await outLines).join('\n');
}
}
Future<Iterable<String>> outputLines(Stream<List<int>> stream) async {
final s = await utf8.decodeStream(stream);
if (s.isEmpty) return [];
return s.split('\n').map(filterUnstableText);
}
Future<void> _listReportApply(
GoldenTestContext context,
List<_PackageVersion> upgrades, {
void Function(Map)? reportAssertions,
}) async {
manifestAndLockfile(context);
await context.runDependencyServices(['list']);
final report = await context.runDependencyServices(['report']);
if (reportAssertions != null) {
reportAssertions(json.decode(report));
}
final input = json.encode({
'dependencyChanges': upgrades,
});
await context.runDependencyServices(['apply'], stdin: input);
manifestAndLockfile(context);
}
Future<void> main() async {
setUpAll(() async {
final tempDir = Directory.systemTemp.createTempSync();
snapshot = p.join(tempDir.path, 'dependency_services.dart.snapshot');
final r = Process.runSync(Platform.resolvedExecutable, [
'--snapshot=$snapshot',
p.join('bin', 'dependency_services.dart'),
]);
expect(r.exitCode, 0, reason: r.stderr);
});
tearDownAll(() {
File(snapshot).parent.deleteSync(recursive: true);
});
testWithGolden('Removing transitive', (context) async {
final server = (await servePackages())
..serve('foo', '1.2.3', deps: {'transitive': '^1.0.0'})
..serve('foo', '2.2.3')
..serve('transitive', '1.0.0')
..serveContentHashes = true;
await d.dir(appPath, [
d.pubspec({
'name': 'app',
'dependencies': {
'foo': '^1.0.0',
},
})
]).create();
await pubGet();
server.dontAllowDownloads();
await _listReportApply(context, [
_PackageVersion('foo', '2.2.3'),
_PackageVersion('transitive', null)
], reportAssertions: (report) {
expect(
findChangeVersion(report, 'singleBreaking', 'foo'),
'2.2.3',
);
expect(
findChangeVersion(report, 'singleBreaking', 'transitive'),
null,
);
});
});
testWithGolden('No pubspec.lock', (context) async {
final server = (await servePackages())
..serve('foo', '1.2.3', deps: {'transitive': '^1.0.0'})
..serve('foo', '2.2.3')
..serve('transitive', '1.0.0')
..serveContentHashes = true;
await d.git('bar.git', [d.libPubspec('bar', '1.0.0')]).create();
await d.dir(appPath, [
d.pubspec({
'name': 'app',
'dependencies': {
'foo': '^1.0.0',
'bar': {
'git': {'url': '../bar.git'},
},
},
})
]).create();
server.dontAllowDownloads();
await _listReportApply(
context,
[
_PackageVersion('foo', '2.2.3'),
_PackageVersion('transitive', null),
],
);
});
testWithGolden('Compatible', (context) async {
final server = (await servePackages())
..serve('foo', '1.2.3')
..serve('foo', '2.2.3')
..serve('bar', '1.2.3')
..serve('bar', '2.2.3')
..serve('boo', '1.2.3')
..serveContentHashes = true;
await d.dir(appPath, [
d.pubspec({
'name': 'app',
'dependencies': {
'foo': '^1.0.0',
'bar': '^1.0.0',
'boo': '^1.0.0',
},
})
]).create();
await pubGet();
server.serve('foo', '1.2.4');
server.serve('boo', '1.2.4');
server.dontAllowDownloads();
await _listReportApply(context, [
_PackageVersion('foo', '1.2.4'),
], reportAssertions: (report) {
expect(
findChangeVersion(report, 'compatible', 'foo'),
'1.2.4',
);
});
});
testWithGolden('Preserves no content-hashes', (context) async {
final server = (await servePackages())
..serve('foo', '1.2.3')
..serve('foo', '2.2.3')
..serve('bar', '1.2.3')
..serve('bar', '2.2.3')
..serve('boo', '1.2.3')
..serveContentHashes = true;
await d.dir(appPath, [
d.pubspec({
'name': 'app',
'dependencies': {
'foo': '^1.0.0',
'bar': '^1.0.0',
'boo': '^1.0.0',
},
})
]).create();
await pubGet();
final lockFile = File(path(p.join(appPath, 'pubspec.lock')));
final lockFileYaml = YamlEditor(
lockFile.readAsStringSync(),
);
for (final p in lockFileYaml.parseAt(['packages']).value.entries) {
lockFileYaml.remove(['packages', p.key, 'description', 'sha256']);
}
lockFile.writeAsStringSync(lockFileYaml.toString());
server.serve('foo', '1.2.4');
server.serve('boo', '1.2.4');
server.dontAllowDownloads();
await _listReportApply(context, [
_PackageVersion('foo', '1.2.4'),
]);
});
testWithGolden('Adding transitive', (context) async {
final server = (await servePackages())
..serve('foo', '1.2.3')
..serve('foo', '2.2.3', deps: {'transitive': '^1.0.0'})
..serve('transitive', '1.0.0')
..serveContentHashes = true;
await d.dir(appPath, [
d.pubspec({
'name': 'app',
'dependencies': {
'foo': '^1.0.0',
},
})
]).create();
await pubGet();
server.dontAllowDownloads();
await _listReportApply(context, [
_PackageVersion('foo', '2.2.3'),
_PackageVersion('transitive', '1.0.0')
], reportAssertions: (report) {
expect(
findChangeVersion(report, 'singleBreaking', 'foo'),
'2.2.3',
);
expect(
findChangeVersion(report, 'singleBreaking', 'transitive'),
'1.0.0',
);
});
});
testWithGolden('multibreaking', (context) async {
final server = (await servePackages())
..serve('foo', '1.0.0')
..serve('bar', '1.0.0')
..serve('baz', '1.0.0')
..serveContentHashes = true;
await d.dir(appPath, [
d.pubspec({
'name': 'app',
'dependencies': {
'foo': '^1.0.0',
'bar': '^1.0.0',
// Pinned version. See that the widened constraint is correct.
'baz': '1.0.0',
},
})
]).create();
await pubGet();
server
..serve('foo', '1.5.0') // compatible
..serve('foo', '2.0.0') // single breaking
..serve('foo', '3.0.0', deps: {'bar': '^2.0.0'}) // multi breaking
..serve('foo', '3.0.1', deps: {'bar': '^2.0.0'})
..serve('bar', '2.0.0', deps: {'foo': '^3.0.0'})
..serve('baz', '1.1.0');
server.dontAllowDownloads();
await _listReportApply(context, [
_PackageVersion('foo', '3.0.1',
constraint: VersionConstraint.parse('^3.0.0')),
_PackageVersion('bar', '2.0.0')
], reportAssertions: (report) {
expect(
findChangeVersion(report, 'multiBreaking', 'foo'),
'3.0.1',
);
expect(
findChangeVersion(report, 'multiBreaking', 'bar'),
'2.0.0',
);
});
});
testWithGolden('Relative paths are allowed', (context) async {
// We cannot update path-dependencies, but they should be allowed.
final server = await servePackages();
server.serve('foo', '1.0.0');
await d.dir('bar', [d.libPubspec('bar', '1.0.0')]).create();
await d.appDir({
'foo': '^1.0.0',
'bar': {'path': '../bar'}
}).create();
await pubGet();
server.serve('foo', '2.0.0');
await _listReportApply(context, [
_PackageVersion('foo', '2.0.0',
constraint: VersionConstraint.parse('^2.0.0')),
], reportAssertions: (report) {
expect(
findChangeVersion(report, 'multiBreaking', 'foo'),
'2.0.0',
);
});
});
testWithGolden('Can update a git package', (context) async {
await d.git('foo.git', [d.libPubspec('foo', '1.0.0')]).create();
await d.git('bar.git', [d.libPubspec('bar', '1.0.0')]).create();
await d.appDir({
'foo': {
'git': {'url': '../foo.git'}
},
'bar': {
// A git dependency with a version constraint.
'git': {'url': '../bar.git'},
'version': '^1.0.0',
}
}).create();
await pubGet();
final secondVersion = d.git('foo.git', [d.libPubspec('foo', '2.0.0')]);
await secondVersion.commit();
final newRef = await secondVersion.revParse('HEAD');
final barSecondVersion = d.git('bar.git', [d.libPubspec('bar', '2.0.0')]);
await barSecondVersion.commit();
await _listReportApply(context, [
_PackageVersion('foo', newRef),
], reportAssertions: (report) {
expect(
findChangeVersion(report, 'multiBreaking', 'foo'),
newRef,
);
});
});
}
dynamic findChangeVersion(dynamic json, String updateType, String name) {
final dep = json['dependencies'].firstWhere((p) => p['name'] == 'foo');
if (dep == null) return null;
return dep[updateType].firstWhere((p) => p['name'] == name)['version'];
}
class _PackageVersion {
String name;
String? version;
VersionConstraint? constraint;
_PackageVersion(this.name, this.version, {this.constraint});
Map<String, Object?> toJson() => {
'name': name,
'version': version,
if (constraint != null) 'constraint': constraint.toString()
};
}
extension on PackageServer {
///Check that nothing is downloaded.
void dontAllowDownloads() {
// This testing logic is a bit fragile, if we change the pattern for pattern
// for the download URL then this will pass silently. There isn't much we
// can / should do about it. Just accept the limitations, and remove it if
// the test becomes useless.
handle(RegExp(r'/.+\.tar\.gz'), (request) {
return shelf.Response.notFound(
'This test should not download archives! Requested ${request.url}');
});
}
}