blob: 4b78803a75b77665868c0a34f424cd0090558294 [file] [log] [blame]
// Copyright (c) 2015, 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.
library test.source.caching_pub_package_map_provider;
import 'dart:convert';
import 'dart:core';
import 'dart:io' as io;
import 'package:analysis_server/src/source/caching_pub_package_map_provider.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/file_system/memory_file_system.dart';
import 'package:analyzer/source/package_map_provider.dart';
import 'package:analyzer/src/dart/sdk/sdk.dart';
import 'package:analyzer/src/generated/engine.dart';
import 'package:unittest/unittest.dart';
import '../utils.dart';
main() {
initializeTestEnvironment();
group('CachingPubPackageMapProvider', () {
MemoryResourceProvider resProvider;
_MockPubListRunner mockRunner;
bool writeFileException;
Map result1 = {
'packages': {'foo': '/tmp/proj1/packages/foo'},
'input_files': ['/tmp/proj1/pubspec.yaml']
};
Map result1error = {
'input_files': ['/tmp/proj1/pubspec.lock']
};
Map result2 = {
'packages': {'bar': '/tmp/proj2/packages/bar'},
'input_files': ['/tmp/proj2/pubspec.yaml']
};
Folder newProj(Map result) {
Map packages = result['packages'];
packages.forEach((String name, String path) {
resProvider.newFolder(path);
});
List<String> inputFiles = result['input_files'] as List<String>;
for (String path in inputFiles) {
resProvider.newFile(path, '');
}
Folder projectFolder = resProvider.getResource(inputFiles[0]).parent;
resProvider.newFile(projectFolder.path + '/pubspec.lock', '');
return projectFolder;
}
int mockWriteFile(File cacheFile, String content) {
if (writeFileException) {
throw 'simulated write failure: $cacheFile';
}
if (!cacheFile.exists) {
resProvider.newFolder(cacheFile.parent.path);
resProvider.newFile(cacheFile.path, content);
} else {
resProvider.modifyFile(cacheFile.path, content);
}
Resource res = resProvider.getResource(cacheFile.path);
if (res is File) {
return res.createSource().modificationStamp;
}
throw 'expected file, but found $res';
}
CachingPubPackageMapProvider newPkgProvider() {
return new CachingPubPackageMapProvider(
resProvider,
new FolderBasedDartSdk(
resProvider, FolderBasedDartSdk.defaultSdkDirectory(resProvider)),
mockRunner.runPubList,
mockWriteFile);
}
setUp(() {
resProvider = new MemoryResourceProvider();
resProvider.newFolder('/tmp/proj/packages/foo');
mockRunner = new _MockPubListRunner();
writeFileException = false;
});
group('computePackageMap', () {
// Assert pub list called once and results are cached in memory
test('cache memory', () {
expect(mockRunner.runCount, 0);
Folder folder1 = newProj(result1);
CachingPubPackageMapProvider pkgProvider = newPkgProvider();
mockRunner.nextResult = JSON.encode(result1);
PackageMapInfo info = pkgProvider.computePackageMap(folder1);
expect(mockRunner.runCount, 1);
_assertInfo(info, result1);
info = pkgProvider.computePackageMap(folder1);
expect(mockRunner.runCount, 1);
_assertInfo(info, result1);
});
// Assert pub list called once and results are cached on disk
test('cache disk', () {
expect(mockRunner.runCount, 0);
Folder folder1 = newProj(result1);
CachingPubPackageMapProvider pkgProvider1 = newPkgProvider();
mockRunner.nextResult = JSON.encode(result1);
PackageMapInfo info = pkgProvider1.computePackageMap(folder1);
expect(mockRunner.runCount, 1);
_assertInfo(info, result1);
CachingPubPackageMapProvider pkgProvider2 = newPkgProvider();
info = pkgProvider2.computePackageMap(folder1);
expect(mockRunner.runCount, 1);
_assertInfo(info, result1);
});
// Assert pub list called even if cache file is corrupted
test('corrupt cache file', () {
expect(mockRunner.runCount, 0);
Folder folder1 = newProj(result1);
CachingPubPackageMapProvider pkgProvider1 = newPkgProvider();
resProvider.newFile(pkgProvider1.cacheFile.path, 'corrupt content');
mockRunner.nextResult = JSON.encode(result1);
PackageMapInfo info = pkgProvider1.computePackageMap(folder1);
expect(mockRunner.runCount, 1);
_assertInfo(info, result1);
CachingPubPackageMapProvider pkgProvider2 = newPkgProvider();
info = pkgProvider2.computePackageMap(folder1);
expect(mockRunner.runCount, 1);
_assertInfo(info, result1);
});
// Assert gracefully continue even if write to file fails
test('failed write to cache file', () {
expect(mockRunner.runCount, 0);
Folder folder1 = newProj(result1);
CachingPubPackageMapProvider pkgProvider = newPkgProvider();
mockRunner.nextResult = JSON.encode(result1);
writeFileException = true;
PackageMapInfo info = pkgProvider.computePackageMap(folder1);
expect(mockRunner.runCount, 1);
_assertInfo(info, result1);
info = pkgProvider.computePackageMap(folder1);
expect(mockRunner.runCount, 1);
_assertInfo(info, result1);
});
// Assert modification in one shows up in the other
test('shared disk cache', () {
expect(mockRunner.runCount, 0);
Folder folder1 = newProj(result1);
CachingPubPackageMapProvider pkgProvider1 = newPkgProvider();
mockRunner.nextResult = JSON.encode(result1);
PackageMapInfo info = pkgProvider1.computePackageMap(folder1);
expect(mockRunner.runCount, 1);
_assertInfo(info, result1);
Folder folder2 = newProj(result2);
CachingPubPackageMapProvider pkgProvider2 = newPkgProvider();
mockRunner.nextResult = JSON.encode(result2);
info = pkgProvider2.computePackageMap(folder2);
expect(mockRunner.runCount, 2);
_assertInfo(info, result2);
info = pkgProvider1.computePackageMap(folder2);
expect(mockRunner.runCount, 2);
_assertInfo(info, result2);
});
// Assert pub list called again if input file modified
test('input file changed', () {
expect(mockRunner.runCount, 0);
Folder folder1 = newProj(result1);
CachingPubPackageMapProvider pkgProvider = newPkgProvider();
mockRunner.nextResult = JSON.encode(result1);
PackageMapInfo info = pkgProvider.computePackageMap(folder1);
expect(mockRunner.runCount, 1);
_assertInfo(info, result1);
resProvider.modifyFile(info.dependencies.first, 'new content');
mockRunner.nextResult = JSON.encode(result1);
info = pkgProvider.computePackageMap(folder1);
expect(mockRunner.runCount, 2);
_assertInfo(info, result1);
});
// Assert pub list called again if input file modified
// after reloading package provider cache from disk
test('input file changed 2', () {
expect(mockRunner.runCount, 0);
Folder folder1 = newProj(result1);
CachingPubPackageMapProvider pkgProvider1 = newPkgProvider();
mockRunner.nextResult = JSON.encode(result1);
PackageMapInfo info = pkgProvider1.computePackageMap(folder1);
expect(mockRunner.runCount, 1);
_assertInfo(info, result1);
resProvider.modifyFile(info.dependencies.first, 'new content');
mockRunner.nextResult = JSON.encode(result1);
CachingPubPackageMapProvider pkgProvider2 = newPkgProvider();
info = pkgProvider2.computePackageMap(folder1);
expect(mockRunner.runCount, 2);
_assertInfo(info, result1);
});
// Assert pub list called again if input file deleted
test('input file deleted', () {
expect(mockRunner.runCount, 0);
Folder folder1 = newProj(result1);
CachingPubPackageMapProvider pkgProvider = newPkgProvider();
mockRunner.nextResult = JSON.encode(result1);
PackageMapInfo info = pkgProvider.computePackageMap(folder1);
expect(mockRunner.runCount, 1);
_assertInfo(info, result1);
resProvider.deleteFile(info.dependencies.first);
mockRunner.nextResult = JSON.encode(result1);
info = pkgProvider.computePackageMap(folder1);
expect(mockRunner.runCount, 2);
_assertInfo(info, result1);
});
// Assert pub list not called if folder does not exist
// and returns same cached result if folder restored as before
test('project removed then restored', () {
expect(mockRunner.runCount, 0);
Folder folder1 = newProj(result1);
CachingPubPackageMapProvider pkgProvider = newPkgProvider();
mockRunner.nextResult = JSON.encode(result1);
PackageMapInfo info = pkgProvider.computePackageMap(folder1);
expect(mockRunner.runCount, 1);
_assertInfo(info, result1);
_RestorePoint restorePoint = new _RestorePoint(resProvider, folder1);
resProvider.deleteFolder(folder1.path);
info = pkgProvider.computePackageMap(folder1);
expect(mockRunner.runCount, 1);
_assertError(info, result1error);
restorePoint.restore();
info = pkgProvider.computePackageMap(folder1);
expect(mockRunner.runCount, 1);
_assertInfo(info, result1);
});
// Assert pub list *is* run again
// if dependency has changed during execution
test('dependency changed during execution', () {
expect(mockRunner.runCount, 0);
Folder folder1 = newProj(result1);
Resource pubspecFile = folder1.getChild('pubspec.yaml');
expect(pubspecFile.exists, isTrue);
CachingPubPackageMapProvider pkgProvider = newPkgProvider();
mockRunner.nextResultFunction = () {
resProvider.modifyFile(pubspecFile.path, 'new content');
return JSON.encode(result1);
};
mockRunner.nextResult = JSON.encode(result1);
PackageMapInfo info = pkgProvider.computePackageMap(folder1);
expect(mockRunner.runCount, 2);
_assertInfo(info, result1);
});
});
});
}
_assertError(PackageMapInfo info, Map expected) {
expect(info.packageMap, isNull);
List<String> expectedFiles = expected['input_files'] as List<String>;
expect(info.dependencies, hasLength(expectedFiles.length));
for (String path in expectedFiles) {
expect(info.dependencies, contains(path));
}
}
_assertInfo(PackageMapInfo info, Map expected) {
Map<String, String> expectedPackages =
expected['packages'] as Map<String, String>;
expect(info.packageMap, hasLength(expectedPackages.length));
for (String key in expectedPackages.keys) {
List<Folder> packageList = info.packageMap[key];
expect(packageList, hasLength(1));
expect(packageList[0].path, expectedPackages[key]);
}
List<String> expectedFiles = expected['input_files'] as List<String>;
expect(info.dependencies, hasLength(expectedFiles.length));
for (String path in expectedFiles) {
expect(info.dependencies, contains(path));
}
}
typedef String MockResultFunction();
/**
* Mock for simulating and tracking execution of pub list
*/
class _MockPubListRunner {
int runCount = 0;
List nextResults = [];
void set nextResult(String result) {
nextResults.add(result);
}
void set nextResultFunction(MockResultFunction resultFunction) {
nextResults.add(resultFunction);
}
io.ProcessResult runPubList(Folder folder) {
if (nextResults.isEmpty) {
throw 'missing nextResult';
}
var result = nextResults.removeAt(0);
if (result is MockResultFunction) {
result = result();
}
++runCount;
return new _MockResult(result);
}
}
class _MockResult implements io.ProcessResult {
String result;
_MockResult(this.result);
@override
int get exitCode => 0;
// TODO: implement stdout
@override
get stdout => result;
noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
/**
* An object containing information to restore the state of a deleted
* folder and its content.
*/
class _RestorePoint {
final MemoryResourceProvider provider;
final List<String> _folderPaths = <String>[];
final List<String> _filePaths = <String>[];
final List<TimestampedData> _fileContents = <TimestampedData>[];
/**
* Construct a new instance that captures the current state of the folder
* and all of its contained files and folders.
*/
_RestorePoint(this.provider, Folder folder) {
record(folder);
}
/**
* Capture the current state of the folder
* and all of its contained files and folders.
*/
void record(Folder folder) {
_folderPaths.add(folder.path);
for (Resource child in folder.getChildren()) {
if (child is Folder) {
record(child);
} else if (child is File) {
_filePaths.add(child.path);
_fileContents.add(child.createSource().contents);
} else {
throw 'unknown resource: $child';
}
}
}
/**
* Restore the original files and folders.
*/
void restore() {
for (String path in _folderPaths) {
provider.newFolder(path);
}
int fileCount = _filePaths.length;
for (int fileIndex = 0; fileIndex < fileCount; ++fileIndex) {
String path = _filePaths[fileIndex];
TimestampedData content = _fileContents[fileIndex];
provider.newFile(path, content.data, content.modificationTime);
}
}
}