| // 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( |
| dependencies: { |
| '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( |
| dependencies: { |
| '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}', |
| ); |
| }); |
| } |
| } |