blob: 269cb64ed58b9f2a5d701038f2b69cbe3cea18ca [file] [log] [blame]
// Copyright (c) 2021, 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:async';
import 'dart:io';
import 'package:analysis_server/src/services/pub/pub_api.dart';
import 'package:analysis_server/src/services/pub/pub_command.dart';
import 'package:analysis_server/src/services/pub/pub_package_service.dart';
import 'package:analyzer/instrumentation/service.dart';
import 'package:analyzer/src/test_utilities/resource_provider_mixin.dart';
import 'package:collection/collection.dart';
import 'package:http/http.dart';
import 'package:path/path.dart' as path;
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import '../mocks.dart';
import 'server_abstract.dart';
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(PubApiTest);
defineReflectiveTests(PubCommandTest);
defineReflectiveTests(PubPackageServiceTest);
});
}
@reflectiveTest
class PubApiTest {
static const pubDefaultUrl = 'https://pub.dartlang.org';
Uri? lastCalledUrl;
late MockHttpClient httpClient;
Future<void> check_pubHostedUrl(String? envValue, String expectedBase) async {
final api =
PubApi(InstrumentationService.NULL_SERVICE, httpClient, envValue);
await api.allPackages();
expect(lastCalledUrl.toString(),
equals('$expectedBase/api/package-name-completion-data'));
}
void setUp() {
httpClient = MockHttpClient();
lastCalledUrl = null;
httpClient.sendHandler = (BaseRequest request) async {
lastCalledUrl = request.url;
return Response('{}', 200);
};
}
Future<void> test_envPubHostedUrl_emptyString() =>
check_pubHostedUrl('', pubDefaultUrl);
Future<void> test_envPubHostedUrl_invalidUrl() =>
check_pubHostedUrl('test', pubDefaultUrl);
Future<void> test_envPubHostedUrl_missingScheme() =>
// It's hard to tell that this is intended to be a valid URL minus the scheme
// so it will fail validation and fall back to the default.
check_pubHostedUrl('pub.example.org', pubDefaultUrl);
Future<void> test_envPubHostedUrl_null() =>
check_pubHostedUrl(null, pubDefaultUrl);
Future<void> test_envPubHostedUrl_valid() =>
check_pubHostedUrl('https://pub.example.org', 'https://pub.example.org');
Future<void> test_envPubHostedUrl_validTrailingSlash() =>
check_pubHostedUrl('https://pub.example.org/', 'https://pub.example.org');
Future<void> test_httpClient_closesOwn() async {
final api = PubApi(InstrumentationService.NULL_SERVICE, null, null);
api.close();
expect(() => api.httpClient.get(Uri.parse('https://www.google.co.uk/')),
throwsA(anything));
}
Future<void> test_httpClient_doesNotCloseProvided() async {
final api = PubApi(InstrumentationService.NULL_SERVICE, httpClient, null);
api.close();
expect(httpClient.wasClosed, isFalse);
}
}
@reflectiveTest
class PubCommandTest with ResourceProviderMixin {
late MockProcessRunner processRunner;
late PubCommand pubCommand;
late String pubspecPath, pubspec2Path;
void setUp() {
pubspecPath = convertPath('/home/project/pubspec.yaml');
pubspec2Path = convertPath('/home/project2/pubspec.yaml');
processRunner = MockProcessRunner();
pubCommand = PubCommand(InstrumentationService.NULL_SERVICE, processRunner);
}
Future<void> test_doesNotRunConcurrently() async {
var isRunning = false;
processRunner.startHandler = (executable, args, {dir, env}) async {
expect(isRunning, isFalse,
reason: 'pub commands should not run concurrently');
isRunning = true;
await pumpEventQueue(times: 500);
isRunning = false;
return MockProcess(0, 0, 'a', 'a');
};
await Future.wait([
pubCommand.outdatedVersions(pubspecPath),
pubCommand.outdatedVersions(pubspecPath),
]);
}
Future<void> test_killsCommandOnShutdown() async {
// Create a process that won't complete (unless it's killed, where
// the MockProcess will complete it with a non-zero exit code).
final process = MockProcess(0, Completer<int>().future, '', '');
processRunner.startHandler = (executable, args, {dir, env}) => process;
// Start running the (endless) command.
final commandFuture = pubCommand.outdatedVersions(pubspecPath);
await pumpEventQueue(times: 500);
// Trigger shutdown, which should kill any in-process commands.
pubCommand.shutdown();
// Expect the process we created above terminates with non-zero exit code.
expect(await process.exitCode, equals(MockProcess.killedExitCode));
expect(commandFuture, completes);
}
Future<void> test_outdated_args() async {
processRunner.startHandler = (executable, args, {dir, env}) {
expect(executable, Platform.resolvedExecutable);
expect(
args,
equals([
'pub',
'outdated',
'--show-all',
'--json',
]));
expect(dir, equals(convertPath('/home/project')));
expect(
env!['PUB_ENVIRONMENT'],
anyOf(equals('analysis_server.pub_api'),
endsWith(':analysis_server.pub_api')));
return MockProcess(0, 0, '', '');
};
await pubCommand.outdatedVersions(pubspecPath);
}
Future<void> test_outdated_invalidJson() async {
processRunner.startHandler = (String executable, List<String> args,
{dir, env}) =>
MockProcess(1, 0, 'NOT VALID JSON', '');
final result = await pubCommand.outdatedVersions(pubspecPath);
expect(result, isEmpty);
}
Future<void> test_outdated_missingFields() async {
final validJson = r'''
{
"packages": [
{
"package": "foo",
"current": { "version": "1.0.0" },
"upgradable": { "version": "2.0.0" },
"resolvable": { }
}
]
}
''';
processRunner.startHandler =
(executable, args, {dir, env}) => MockProcess(1, 0, validJson, '');
final result = await pubCommand.outdatedVersions(pubspecPath);
expect(result, hasLength(1));
final package = result.first;
expect(package.packageName, equals('foo'));
expect(package.currentVersion, equals('1.0.0'));
expect(package.upgradableVersion, equals('2.0.0'));
expect(package.resolvableVersion, isNull);
expect(package.latestVersion, isNull);
}
Future<void> test_outdated_multiplePubspecs() async {
final pubspecJson1 = r'''
{
"packages": [
{
"package": "foo",
"resolvable": { "version": "1.1.1" }
}
]
}
''';
final pubspecJson2 = r'''
{
"packages": [
{
"package": "foo",
"resolvable": { "version": "2.2.2" }
}
]
}
''';
processRunner.startHandler = (executable, args, {dir, env}) {
// Return different json based on the directory we were invoked in.
final json =
dir == path.dirname(pubspecPath) ? pubspecJson1 : pubspecJson2;
return MockProcess(1, 0, json, '');
};
final result1 = await pubCommand.outdatedVersions(pubspecPath);
final result2 = await pubCommand.outdatedVersions(pubspec2Path);
expect(result1.first.resolvableVersion, equals('1.1.1'));
expect(result2.first.resolvableVersion, equals('2.2.2'));
}
Future<void> test_outdated_nonZeroExitCode() async {
processRunner.startHandler =
(executable, args, {dir, env}) => MockProcess(1, 123, '{}', '');
final result = await pubCommand.outdatedVersions(pubspecPath);
expect(result, isEmpty);
}
Future<void> test_validJson() async {
final validJson = r'''
{
"packages": [
{
"package": "foo",
"current": { "version": "1.0.0" },
"upgradable": { "version": "2.0.0" },
"resolvable": { "version": "3.0.0" },
"latest": { "version": "4.0.0" }
},
{
"package": "bar",
"current": { "version": "1.0.0" },
"upgradable": { "version": "2.0.0" },
"resolvable": { "version": "3.0.0" },
"latest": { "version": "4.0.0" }
}
]
}
''';
processRunner.startHandler =
(executable, args, {dir, env}) => MockProcess(1, 0, validJson, '');
final result = await pubCommand.outdatedVersions(pubspecPath);
expect(result, hasLength(2));
result.forEachIndexed((index, package) {
expect(package.packageName, equals(index == 0 ? 'foo' : 'bar'));
expect(package.currentVersion, equals('1.0.0'));
expect(package.upgradableVersion, equals('2.0.0'));
expect(package.resolvableVersion, equals('3.0.0'));
expect(package.latestVersion, equals('4.0.0'));
});
}
}
@reflectiveTest
class PubPackageServiceTest extends AbstractLspAnalysisServerTest {
/// A sample API response for package names. This should match the JSON served
/// at https://pub.dev/api/package-name-completion-data.
static const samplePackageList = '''
{ "packages": ["one", "two", "three"] }
''';
void expectPackages(List<String> packageNames) => expect(
server.pubPackageService.cachedPackages.map((p) => p.packageName),
equals(packageNames));
void providePubApiPackageList(String jsonResponse) {
httpClient.sendHandler = (BaseRequest request) async {
if (request.url.toString().endsWith(PubApi.packageNameListPath)) {
return Response(jsonResponse, 200);
} else {
throw UnimplementedError();
}
};
}
@override
void setUp() {
super.setUp();
// Cause retries to run immediately.
PubApi.failedRetryInitialDelaySeconds = 0;
}
Future<void> test_packageCache_diskCacheReplacedIfStale() async {
// This test should mirror test_packageCache_diskCacheUsedIfFresh
// besides the lastUpdated timestamp + expectations.
providePubApiPackageList(samplePackageList);
// Write a cache that should be considered stale.
server.pubPackageService.writeDiskCache(
PackageDetailsCache.fromApiResults([PubApiPackage('stalePackage1')])
..lastUpdatedUtc = DateTime.utc(1990),
);
await initialize();
await openFile(pubspecFileUri, '');
await pumpEventQueue();
// Expect the sample list to have overwritten the stale cache.
expectPackages(['one', 'two', 'three']);
}
Future<void> test_packageCache_diskCacheUsedIfFresh() async {
// This test should mirror test_packageCache_diskCacheReplacedIfStale
// besides the lastUpdated timestamp + expectations.
providePubApiPackageList(samplePackageList);
// Write a cache that is not stale.
server.pubPackageService.writeDiskCache(
PackageDetailsCache.fromApiResults([PubApiPackage('freshPackage1')]));
await initialize();
await openFile(pubspecFileUri, '');
await pumpEventQueue();
// Expect the fresh cache to still be used.
expectPackages(['freshPackage1']);
}
Future<void> test_packageCache_doesNotRetryOn400() async {
var requestNum = 1;
httpClient.sendHandler = (BaseRequest request) async {
if (request.url.toString().endsWith(PubApi.packageNameListPath)) {
return requestNum++ == 1
? Response('ERROR', 400)
: Response(samplePackageList, 200);
} else {
throw UnimplementedError();
}
};
await initialize();
expectPackages([]);
await openFile(pubspecFileUri, '');
await pumpEventQueue();
expectPackages([]);
expect(httpClient.sendHandlerCalls, equals(1));
}
Future<void> test_packageCache_doesNotRetryUnknownException() async {
httpClient.sendHandler =
(BaseRequest request) async => throw UnimplementedError();
await initialize();
expectPackages([]);
await openFile(pubspecFileUri, '');
await pumpEventQueue();
expectPackages([]);
expect(httpClient.sendHandlerCalls, equals(1));
}
Future<void> test_packageCache_fetchesFromServer() async {
// Provide the sample packages in the web request.
providePubApiPackageList(samplePackageList);
await initialize();
expectPackages([]);
await openFile(pubspecFileUri, '');
await pumpEventQueue();
expectPackages(['one', 'two', 'three']);
}
Future<void> test_packageCache_initializesOnPubspecOpen() async {
await initialize();
expect(server.pubPackageService.isPackageNamesTimerRunning, isFalse);
expect(server.pubPackageService.packageCache, isNull);
expectPackages([]);
await openFile(pubspecFileUri, '');
await pumpEventQueue();
expect(server.pubPackageService.isPackageNamesTimerRunning, isTrue);
expect(server.pubPackageService.packageCache, isNotNull);
expectPackages([]);
}
Future<void> test_packageCache_readsDiskCache() async {
server.pubPackageService.writeDiskCache(
PackageDetailsCache.fromApiResults([PubApiPackage('package1')]));
await initialize();
expectPackages([]);
await openFile(pubspecFileUri, '');
await pumpEventQueue();
expectPackages(['package1']);
}
Future<void> test_packageCache_retriesOn500() async {
var requestNum = 1;
httpClient.sendHandler = (BaseRequest request) async {
if (request.url.toString().endsWith(PubApi.packageNameListPath)) {
return requestNum++ == 1
? Response('ERROR', 500)
: Response(samplePackageList, 200);
} else {
throw UnimplementedError();
}
};
await initialize();
expectPackages([]);
await openFile(pubspecFileUri, '');
await pumpEventQueue();
expectPackages(['one', 'two', 'three']);
expect(httpClient.sendHandlerCalls, equals(2));
}
Future<void> test_packageCache_retriesOnInvalidJson() async {
var requestNum = 1;
httpClient.sendHandler = (BaseRequest request) async {
if (request.url.toString().endsWith(PubApi.packageNameListPath)) {
return requestNum++ == 1
? Response('$samplePackageList{{{{{{{', 200)
: Response(samplePackageList, 200);
} else {
throw UnimplementedError();
}
};
await initialize();
expectPackages([]);
await openFile(pubspecFileUri, '');
await pumpEventQueue();
expectPackages(['one', 'two', 'three']);
expect(httpClient.sendHandlerCalls, equals(2));
}
Future<void> test_packageCache_timeRemaining() async {
void expectHoursRemaining(DateTime cacheTime, int expectedHoursRemaining) {
final cache = PackageDetailsCache.empty();
cache.lastUpdatedUtc = cacheTime.toUtc();
final remainingHours = cache.cacheTimeRemaining.inHours;
expect(remainingHours, isNonNegative);
expect(remainingHours, closeTo(expectedHoursRemaining, 1));
}
final maxHours = PackageDetailsCache.maxCacheAge.inHours;
// Very old cache should have no time remaining.
expectHoursRemaining(DateTime(2020, 12, 1), 0);
// Cache from 1 hour ago should max-1 hours remaining.
expectHoursRemaining(DateTime.now().add(Duration(hours: -1)), maxHours - 1);
// Cache from 10 hours ago should max-10 hours remaining.
expectHoursRemaining(
DateTime.now().add(Duration(hours: -10)), maxHours - 10);
// Cache from maxAge ago should have no hours remaining.
expectHoursRemaining(
DateTime.now().add(-PackageDetailsCache.maxCacheAge), 0);
}
Future<void> test_packageCache_writesDiskCache() async {
// Provide the sample packages in the web request.
providePubApiPackageList(samplePackageList);
await initialize();
expect(server.pubPackageService.readDiskCache(), isNull);
await openFile(pubspecFileUri, '');
await pumpEventQueue();
final cache = server.pubPackageService.readDiskCache()!;
final packages = cache.packages.values.toList();
expect(packages.map((p) => p.packageName), equals(['one', 'two', 'three']));
}
}