Unverified Commit 66c7b6a9 authored by Chris Bracken's avatar Chris Bracken Committed by GitHub

Add Fingerprinter class (#17255)

Adds a Fingerprinter utility class that can be used to compute unique
fingerprints for a set of input paths and build options, compare to the
output of a previous run, and skip the build action if no inputs or
options have changed. The existing Fingerprint class still does all the
heavy lifting. Fingerprinter adds common operations such as
reading/writing/comparing fingerprints and parsing depfiles.

This migrates existing uses of Fingerprint over to Fingerprinter.

This also adds better fingerprinting to AOT snapshotting, which
previously failed to include several options in its fingerprint
(--preview-dart-2, --prefer-shared-library).
parent 322eb81a
......@@ -3,11 +3,8 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:convert' show json;
import 'package:crypto/crypto.dart' show md5;
import 'package:meta/meta.dart';
import 'package:quiver/core.dart' show hash2;
import '../android/android_sdk.dart';
import '../artifacts.dart';
......@@ -16,9 +13,9 @@ import '../compile.dart';
import '../dart/package_map.dart';
import '../globals.dart';
import '../ios/mac.dart';
import '../version.dart';
import 'context.dart';
import 'file_system.dart';
import 'fingerprint.dart';
import 'process.dart';
GenSnapshot get genSnapshot => context[GenSnapshot];
......@@ -66,97 +63,6 @@ class GenSnapshot {
}
}
/// A fingerprint for a set of build input files and properties.
///
/// This class can be used during build actions to compute a fingerprint of the
/// build action inputs, and if unchanged from the previous build, skip the
/// build step. This assumes that build outputs are strictly a product of the
/// fingerprint inputs.
class Fingerprint {
Fingerprint.fromBuildInputs(Map<String, String> properties, Iterable<String> inputPaths) {
final Iterable<File> files = inputPaths.map(fs.file);
final Iterable<File> missingInputs = files.where((File file) => !file.existsSync());
if (missingInputs.isNotEmpty)
throw new ArgumentError('Missing input files:\n' + missingInputs.join('\n'));
_checksums = <String, String>{};
for (File file in files) {
final List<int> bytes = file.readAsBytesSync();
_checksums[file.path] = md5.convert(bytes).toString();
}
_properties = <String, String>{}..addAll(properties);
}
/// Creates a Fingerprint from serialized JSON.
///
/// Throws [ArgumentError], if there is a version mismatch between the
/// serializing framework and this framework.
Fingerprint.fromJson(String jsonData) {
final Map<String, dynamic> content = json.decode(jsonData);
final String version = content['version'];
if (version != FlutterVersion.instance.frameworkRevision)
throw new ArgumentError('Incompatible fingerprint version: $version');
_checksums = content['files'] ?? <String, String>{};
_properties = content['properties'] ?? <String, String>{};
}
Map<String, String> _checksums;
Map<String, String> _properties;
String toJson() => json.encode(<String, dynamic>{
'version': FlutterVersion.instance.frameworkRevision,
'properties': _properties,
'files': _checksums,
});
@override
bool operator==(dynamic other) {
if (identical(other, this))
return true;
if (other.runtimeType != runtimeType)
return false;
final Fingerprint typedOther = other;
return _equalMaps(typedOther._checksums, _checksums)
&& _equalMaps(typedOther._properties, _properties);
}
bool _equalMaps(Map<String, String> a, Map<String, String> b) {
return a.length == b.length
&& a.keys.every((String key) => a[key] == b[key]);
}
@override
// Ignore map entries here to avoid becoming inconsistent with equals
// due to differences in map entry order.
int get hashCode => hash2(_properties.length, _checksums.length);
}
final RegExp _separatorExpr = new RegExp(r'([^\\]) ');
final RegExp _escapeExpr = new RegExp(r'\\(.)');
/// Parses a VM snapshot dependency file.
///
/// Snapshot dependency files are a single line mapping the output snapshot to a
/// space-separated list of input files used to generate that output. Spaces and
/// backslashes are escaped with a backslash. e.g,
///
/// outfile : file1.dart fil\\e2.dart fil\ e3.dart
///
/// will return a set containing: 'file1.dart', 'fil\e2.dart', 'fil e3.dart'.
Future<Set<String>> readDepfile(String depfilePath) async {
// Depfile format:
// outfile1 outfile2 : file1.dart file2.dart file3.dart
final String contents = await fs.file(depfilePath).readAsString();
final String dependencies = contents.split(': ')[1];
return dependencies
.replaceAllMapped(_separatorExpr, (Match match) => '${match.group(1)}\n')
.split('\n')
.map((String path) => path.replaceAllMapped(_escapeExpr, (Match match) => match.group(1)).trim())
.where((String path) => path.isNotEmpty)
.toSet();
}
/// Dart snapshot builder.
///
/// Builds Dart snapshots in one of three modes:
......@@ -186,24 +92,37 @@ class ScriptSnapshotter {
mainPath,
];
final String fingerprintPath = '$depfilePath.fingerprint';
final Set<String> outputPaths = <String>[snapshotPath].toSet();
if (!await _isBuildRequired(snapshotType, outputPaths, depfilePath, mainPath, fingerprintPath)) {
final Fingerprinter fingerprinter = new Fingerprinter(
fingerprintPath: '$depfilePath.fingerprint',
paths: <String>[
mainPath,
snapshotPath,
vmSnapshotData,
isolateSnapshotData,
],
properties: <String, String>{
'buildMode': snapshotType.mode.toString(),
'targetPlatform': snapshotType.platform?.toString() ?? '',
'entryPoint': mainPath,
},
depfilePaths: <String>[depfilePath],
);
if (await fingerprinter.doesFingerprintMatch()) {
printTrace('Skipping script snapshot build. Fingerprints match.');
return 0;
}
// Build the snapshot.
final int exitCode = await genSnapshot.run(
snapshotType: snapshotType,
packagesPath: packagesPath,
depfilePath: depfilePath,
additionalArgs: args,
snapshotType: snapshotType,
packagesPath: packagesPath,
depfilePath: depfilePath,
additionalArgs: args,
);
if (exitCode != 0)
return exitCode;
await _writeFingerprint(snapshotType, outputPaths, depfilePath, mainPath, fingerprintPath);
await fingerprinter.writeFingerprint();
return exitCode;
}
}
......@@ -311,9 +230,20 @@ class AOTSnapshotter {
}
// If inputs and outputs have not changed since last run, skip the build.
final String fingerprintPath = '$depfilePath.fingerprint';
final SnapshotType snapshotType = new SnapshotType(platform, buildMode);
if (!await _isBuildRequired(snapshotType, outputPaths, depfilePath, mainPath, fingerprintPath)) {
final Fingerprinter fingerprinter = new Fingerprinter(
fingerprintPath: '$depfilePath.fingerprint',
paths: <String>[mainPath]..addAll(inputPaths)..addAll(outputPaths),
properties: <String, String>{
'buildMode': buildMode.toString(),
'targetPlatform': platform.toString(),
'entryPoint': mainPath,
'dart2': previewDart2.toString(),
'sharedLib': compileToSharedLibrary.toString(),
'extraGenSnapshotOptions': extraGenSnapshotOptions.join(' '),
},
depfilePaths: <String>[depfilePath],
);
if (await fingerprinter.doesFingerprintMatch()) {
printTrace('Skipping AOT snapshot build. Fingerprint match.');
return 0;
}
......@@ -346,7 +276,7 @@ class AOTSnapshotter {
}
// Compute and record build fingerprint.
await _writeFingerprint(snapshotType, outputPaths, depfilePath, mainPath, fingerprintPath);
await fingerprinter.writeFingerprint();
return 0;
}
......@@ -454,48 +384,3 @@ class AOTSnapshotter {
return fs.path.dirname(fs.path.fromUri(packageMap.map[package]));
}
}
Future<bool> _isBuildRequired(SnapshotType type, Set<String> outputPaths, String depfilePath, String mainPath, String fingerprintPath) async {
final File fingerprintFile = fs.file(fingerprintPath);
final List<String> requiredFiles = <String>[fingerprintPath, depfilePath]..addAll(outputPaths);
if (!requiredFiles.every(fs.isFileSync))
return true;
try {
if (fingerprintFile.existsSync()) {
final Fingerprint oldFingerprint = new Fingerprint.fromJson(await fingerprintFile.readAsString());
final Set<String> inputFilePaths = await readDepfile(depfilePath)..add(mainPath)..addAll(outputPaths);
final Fingerprint newFingerprint = createFingerprint(type, mainPath, inputFilePaths);
return oldFingerprint != newFingerprint;
}
} catch (e) {
// Log exception and continue, this step is a performance improvement only.
printTrace('Rebuilding snapshot due to fingerprint check error: $e');
}
return true;
}
Future<Null> _writeFingerprint(SnapshotType type, Set<String> outputPaths, String depfilePath, String mainPath, String fingerprintPath) async {
try {
final Set<String> inputFilePaths = await readDepfile(depfilePath)
..add(mainPath)
..addAll(outputPaths);
final Fingerprint fingerprint = createFingerprint(type, mainPath, inputFilePaths);
await fs.file(fingerprintPath).writeAsString(fingerprint.toJson());
} catch (e, s) {
// Log exception and continue, this step is a performance improvement only.
printStatus('Error during snapshot fingerprinting: $e\n$s');
}
}
Fingerprint createFingerprint(SnapshotType type, String mainPath, Iterable<String> inputFilePaths) {
final Map<String, String> properties = <String, String>{
'buildMode': type.mode.toString(),
'targetPlatform': type.platform?.toString() ?? '',
'entryPoint': mainPath,
};
final List<String> pathsWithSnapshotData = inputFilePaths.toList()
..add(artifacts.getArtifactPath(Artifact.vmSnapshotData))
..add(artifacts.getArtifactPath(Artifact.isolateSnapshotData));
return new Fingerprint.fromBuildInputs(properties, pathsWithSnapshotData);
}
// Copyright 2018 The Chromium 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:convert' show json;
import 'package:crypto/crypto.dart' show md5;
import 'package:meta/meta.dart';
import 'package:quiver/core.dart' show hash2;
import '../globals.dart';
import '../version.dart';
import 'file_system.dart';
/// A tool that can be used to compute, compare, and write [Fingerprint]s for a
/// set of input files and associated build settings.
///
/// This class can be used during build actions to compute a fingerprint of the
/// build action inputs and options, and if unchanged from the previous build,
/// skip the build step. This assumes that build outputs are strictly a product
/// of the fingerprint inputs.
class Fingerprinter {
Fingerprinter({
@required this.fingerprintPath,
@required Iterable<String> paths,
@required Map<String, String> properties,
Iterable<String> depfilePaths: const <String>[]
}) : _paths = paths.toList(),
_properties = new Map<String, String>.from(properties),
_depfilePaths = depfilePaths.toList(),
assert(fingerprintPath != null),
assert(paths != null && paths.every((String path) => path != null)),
assert(properties != null),
assert(depfilePaths != null && depfilePaths.every((String path) => path != null));
final String fingerprintPath;
final List<String> _paths;
final Map<String, String> _properties;
final List<String> _depfilePaths;
Future<Fingerprint> buildFingerprint() async {
final List<String> paths = await _getPaths();
return new Fingerprint.fromBuildInputs(_properties, paths);
}
Future<bool> doesFingerprintMatch() async {
final File fingerprintFile = fs.file(fingerprintPath);
if (!fingerprintFile.existsSync())
return false;
if (!_depfilePaths.every(fs.isFileSync))
return false;
final List<String> paths = await _getPaths();
if (!paths.every(fs.isFileSync))
return false;
try {
final Fingerprint oldFingerprint = new Fingerprint.fromJson(await fingerprintFile.readAsString());
final Fingerprint newFingerprint = await buildFingerprint();
return oldFingerprint == newFingerprint;
} catch (e) {
// Log exception and continue, fingerprinting is only a performance improvement.
printTrace('Fingerprint check error: $e');
}
return false;
}
Future<void> writeFingerprint() async {
try {
final Fingerprint fingerprint = await buildFingerprint();
return fs.file(fingerprintPath).writeAsStringSync(fingerprint.toJson());
} catch (e) {
// Log exception and continue, fingerprinting is only a performance improvement.
printTrace('Fingerprint write error: $e');
}
}
Future<List<String>> _getPaths() async {
final Set<String> paths = _paths.toSet();
for (String depfilePath in _depfilePaths)
paths.addAll(await readDepfile(depfilePath));
return paths.toList()..sort();
}
}
/// A fingerprint that uniquely identifies a set of build input files and
/// properties.
///
/// See [Fingerprinter].
class Fingerprint {
Fingerprint.fromBuildInputs(Map<String, String> properties, Iterable<String> inputPaths) {
final Iterable<File> files = inputPaths.map(fs.file);
final Iterable<File> missingInputs = files.where((File file) => !file.existsSync());
if (missingInputs.isNotEmpty)
throw new ArgumentError('Missing input files:\n' + missingInputs.join('\n'));
_checksums = <String, String>{};
for (File file in files) {
final List<int> bytes = file.readAsBytesSync();
_checksums[file.path] = md5.convert(bytes).toString();
}
_properties = <String, String>{}..addAll(properties);
}
/// Creates a Fingerprint from serialized JSON.
///
/// Throws [ArgumentError], if there is a version mismatch between the
/// serializing framework and this framework.
Fingerprint.fromJson(String jsonData) {
final Map<String, dynamic> content = json.decode(jsonData);
final String version = content['version'];
if (version != FlutterVersion.instance.frameworkRevision)
throw new ArgumentError('Incompatible fingerprint version: $version');
_checksums = content['files'] ?? <String, String>{};
_properties = content['properties'] ?? <String, String>{};
}
Map<String, String> _checksums;
Map<String, String> _properties;
String toJson() => json.encode(<String, dynamic>{
'version': FlutterVersion.instance.frameworkRevision,
'properties': _properties,
'files': _checksums,
});
@override
bool operator==(dynamic other) {
if (identical(other, this))
return true;
if (other.runtimeType != runtimeType)
return false;
final Fingerprint typedOther = other;
return _equalMaps(typedOther._checksums, _checksums)
&& _equalMaps(typedOther._properties, _properties);
}
bool _equalMaps(Map<String, String> a, Map<String, String> b) {
return a.length == b.length
&& a.keys.every((String key) => a[key] == b[key]);
}
@override
// Ignore map entries here to avoid becoming inconsistent with equals
// due to differences in map entry order.
int get hashCode => hash2(_properties.length, _checksums.length);
}
final RegExp _separatorExpr = new RegExp(r'([^\\]) ');
final RegExp _escapeExpr = new RegExp(r'\\(.)');
/// Parses a VM snapshot dependency file.
///
/// Snapshot dependency files are a single line mapping the output snapshot to a
/// space-separated list of input files used to generate that output. Spaces and
/// backslashes are escaped with a backslash. e.g,
///
/// outfile : file1.dart fil\\e2.dart fil\ e3.dart
///
/// will return a set containing: 'file1.dart', 'fil\e2.dart', 'fil e3.dart'.
Future<Set<String>> readDepfile(String depfilePath) async {
// Depfile format:
// outfile1 outfile2 : file1.dart file2.dart file3.dart
final String contents = await fs.file(depfilePath).readAsString();
final String dependencies = contents.split(': ')[1];
return dependencies
.replaceAllMapped(_separatorExpr, (Match match) => '${match.group(1)}\n')
.split('\n')
.map((String path) => path.replaceAllMapped(_escapeExpr, (Match match) => match.group(1)).trim())
.where((String path) => path.isNotEmpty)
.toSet();
}
......@@ -9,6 +9,7 @@ import 'asset.dart';
import 'base/build.dart';
import 'base/common.dart';
import 'base/file_system.dart';
import 'base/fingerprint.dart';
import 'build_info.dart';
import 'compile.dart';
import 'dart/package_map.dart';
......@@ -70,36 +71,20 @@ Future<void> build({
DevFSContent kernelContent;
if (!precompiledSnapshot && previewDart2) {
final File fingerprintFile = fs.file('$depfilePath.fingerprint');
final List<String> inputPaths = <String>[
mainPath,
];
bool needBuild = true;
final List<File> fingerprintFiles = <File>[fingerprintFile, fs.file(depfilePath)]
..addAll(inputPaths.map(fs.file));
Future<Fingerprint> makeFingerprint() async {
final Set<String> compilerInputPaths = await readDepfile(depfilePath)
..add(mainPath);
final Map<String, String> properties = <String, String>{
final Fingerprinter fingerprinter = new Fingerprinter(
fingerprintPath: '$depfilePath.fingerprint',
paths: <String>[mainPath],
properties: <String, String>{
'entryPoint': mainPath,
'trackWidgetCreation': trackWidgetCreation.toString(),
};
return new Fingerprint.fromBuildInputs(properties, compilerInputPaths);
}
},
depfilePaths: <String>[depfilePath],
);
if (fingerprintFiles.every((File file) => file.existsSync())) {
try {
final String json = await fingerprintFile.readAsString();
final Fingerprint oldFingerprint = new Fingerprint.fromJson(json);
if (oldFingerprint == await makeFingerprint()) {
needBuild = false;
printStatus('Skipping compilation. Fingerprint match.');
}
} catch (e) {
printTrace('Rebuilding kernel file due to fingerprint check error: $e');
}
if (await fingerprinter.doesFingerprintMatch()) {
needBuild = false;
printStatus('Skipping compilation. Fingerprint match.');
}
String kernelBinaryFilename;
......@@ -121,13 +106,7 @@ Future<void> build({
throwToolExit('Compiler failed on $mainPath');
}
// Compute and record build fingerprint.
try {
final Fingerprint fingerprint = await makeFingerprint();
await fingerprintFile.writeAsString(fingerprint.toJson());
} catch (e, s) {
// Log exception and continue, this step is a performance improvement only.
printStatus('Error during compilation output fingerprinting: $e\n$s');
}
await fingerprinter.writeFingerprint();
} else {
kernelBinaryFilename = applicationKernelFilePath;
}
......
......@@ -3,7 +3,6 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'dart:convert' show json;
import 'package:file/memory.dart';
......@@ -83,252 +82,6 @@ void main() {
});
});
group('Fingerprint', () {
MockFlutterVersion mockVersion;
const String kVersion = '123456abcdef';
setUp(() {
mockVersion = new MockFlutterVersion();
when(mockVersion.frameworkRevision).thenReturn(kVersion);
});
group('fromBuildInputs', () {
MemoryFileSystem fs;
setUp(() {
fs = new MemoryFileSystem();
});
testUsingContext('throws if any input file does not exist', () async {
await fs.file('a.dart').create();
expect(
() => new Fingerprint.fromBuildInputs(<String, String>{}, <String>['a.dart', 'b.dart']),
throwsArgumentError,
);
}, overrides: <Type, Generator>{ FileSystem: () => fs });
testUsingContext('populates checksums for valid files', () async {
await fs.file('a.dart').writeAsString('This is a');
await fs.file('b.dart').writeAsString('This is b');
final Fingerprint fingerprint = new Fingerprint.fromBuildInputs(<String, String>{}, <String>['a.dart', 'b.dart']);
final Map<String, dynamic> jsonObject = json.decode(fingerprint.toJson());
expect(jsonObject['files'], hasLength(2));
expect(jsonObject['files']['a.dart'], '8a21a15fad560b799f6731d436c1b698');
expect(jsonObject['files']['b.dart'], '6f144e08b58cd0925328610fad7ac07c');
}, overrides: <Type, Generator>{ FileSystem: () => fs });
testUsingContext('includes framework version', () {
final Fingerprint fingerprint = new Fingerprint.fromBuildInputs(<String, String>{}, <String>[]);
final Map<String, dynamic> jsonObject = json.decode(fingerprint.toJson());
expect(jsonObject['version'], mockVersion.frameworkRevision);
}, overrides: <Type, Generator>{ FlutterVersion: () => mockVersion });
testUsingContext('includes provided properties', () {
final Fingerprint fingerprint = new Fingerprint.fromBuildInputs(<String, String>{'a': 'A', 'b': 'B'}, <String>[]);
final Map<String, dynamic> jsonObject = json.decode(fingerprint.toJson());
expect(jsonObject['properties'], hasLength(2));
expect(jsonObject['properties']['a'], 'A');
expect(jsonObject['properties']['b'], 'B');
}, overrides: <Type, Generator>{ FlutterVersion: () => mockVersion });
});
group('fromJson', () {
testUsingContext('throws if JSON is invalid', () async {
expect(() => new Fingerprint.fromJson('<xml></xml>'), throwsA(anything));
}, overrides: <Type, Generator>{
FlutterVersion: () => mockVersion,
});
testUsingContext('creates fingerprint from valid JSON', () async {
final String jsonString = json.encode(<String, dynamic>{
'version': kVersion,
'properties': <String, String>{
'buildMode': BuildMode.release.toString(),
'targetPlatform': TargetPlatform.ios.toString(),
'entryPoint': 'a.dart',
},
'files': <String, dynamic>{
'a.dart': '8a21a15fad560b799f6731d436c1b698',
'b.dart': '6f144e08b58cd0925328610fad7ac07c',
},
});
final Fingerprint fingerprint = new Fingerprint.fromJson(jsonString);
final Map<String, dynamic> content = json.decode(fingerprint.toJson());
expect(content, hasLength(3));
expect(content['version'], mockVersion.frameworkRevision);
expect(content['properties'], hasLength(3));
expect(content['properties']['buildMode'], BuildMode.release.toString());
expect(content['properties']['targetPlatform'], TargetPlatform.ios.toString());
expect(content['properties']['entryPoint'], 'a.dart');
expect(content['files'], hasLength(2));
expect(content['files']['a.dart'], '8a21a15fad560b799f6731d436c1b698');
expect(content['files']['b.dart'], '6f144e08b58cd0925328610fad7ac07c');
}, overrides: <Type, Generator>{
FlutterVersion: () => mockVersion,
});
testUsingContext('throws ArgumentError for unknown versions', () async {
final String jsonString = json.encode(<String, dynamic>{
'version': 'bad',
'properties':<String, String>{},
'files':<String, String>{},
});
expect(() => new Fingerprint.fromJson(jsonString), throwsArgumentError);
}, overrides: <Type, Generator>{
FlutterVersion: () => mockVersion,
});
testUsingContext('throws ArgumentError if version is not present', () async {
final String jsonString = json.encode(<String, dynamic>{
'properties':<String, String>{},
'files':<String, String>{},
});
expect(() => new Fingerprint.fromJson(jsonString), throwsArgumentError);
}, overrides: <Type, Generator>{
FlutterVersion: () => mockVersion,
});
testUsingContext('treats missing properties and files entries as if empty', () async {
final String jsonString = json.encode(<String, dynamic>{
'version': kVersion,
});
expect(new Fingerprint.fromJson(jsonString), new Fingerprint.fromBuildInputs(<String, String>{}, <String>[]));
}, overrides: <Type, Generator>{
FlutterVersion: () => mockVersion,
});
});
group('operator ==', () {
testUsingContext('reports not equal if properties do not match', () async {
final Map<String, dynamic> a = <String, dynamic>{
'version': kVersion,
'properties': <String, String>{
'buildMode': BuildMode.debug.toString(),
},
'files': <String, dynamic>{},
};
final Map<String, dynamic> b = new Map<String, dynamic>.from(a);
b['properties'] = <String, String>{
'buildMode': BuildMode.release.toString(),
};
expect(new Fingerprint.fromJson(json.encode(a)) == new Fingerprint.fromJson(json.encode(b)), isFalse);
}, overrides: <Type, Generator>{
FlutterVersion: () => mockVersion,
});
testUsingContext('reports not equal if file checksums do not match', () async {
final Map<String, dynamic> a = <String, dynamic>{
'version': kVersion,
'properties': <String, String>{},
'files': <String, dynamic>{
'a.dart': '8a21a15fad560b799f6731d436c1b698',
'b.dart': '6f144e08b58cd0925328610fad7ac07c',
},
};
final Map<String, dynamic> b = new Map<String, dynamic>.from(a);
b['files'] = <String, dynamic>{
'a.dart': '8a21a15fad560b799f6731d436c1b698',
'b.dart': '6f144e08b58cd0925328610fad7ac07d',
};
expect(new Fingerprint.fromJson(json.encode(a)) == new Fingerprint.fromJson(json.encode(b)), isFalse);
}, overrides: <Type, Generator>{
FlutterVersion: () => mockVersion,
});
testUsingContext('reports not equal if file paths do not match', () async {
final Map<String, dynamic> a = <String, dynamic>{
'version': kVersion,
'properties': <String, String>{},
'files': <String, dynamic>{
'a.dart': '8a21a15fad560b799f6731d436c1b698',
'b.dart': '6f144e08b58cd0925328610fad7ac07c',
},
};
final Map<String, dynamic> b = new Map<String, dynamic>.from(a);
b['files'] = <String, dynamic>{
'a.dart': '8a21a15fad560b799f6731d436c1b698',
'c.dart': '6f144e08b58cd0925328610fad7ac07d',
};
expect(new Fingerprint.fromJson(json.encode(a)) == new Fingerprint.fromJson(json.encode(b)), isFalse);
}, overrides: <Type, Generator>{
FlutterVersion: () => mockVersion,
});
testUsingContext('reports equal if properties and file checksums match', () async {
final Map<String, dynamic> a = <String, dynamic>{
'version': kVersion,
'properties': <String, String>{
'buildMode': BuildMode.debug.toString(),
'targetPlatform': TargetPlatform.ios.toString(),
'entryPoint': 'a.dart',
},
'files': <String, dynamic>{
'a.dart': '8a21a15fad560b799f6731d436c1b698',
'b.dart': '6f144e08b58cd0925328610fad7ac07c',
},
};
expect(new Fingerprint.fromJson(json.encode(a)) == new Fingerprint.fromJson(json.encode(a)), isTrue);
}, overrides: <Type, Generator>{
FlutterVersion: () => mockVersion,
});
});
group('hashCode', () {
testUsingContext('is consistent with equals, even if map entries are reordered', () async {
final Fingerprint a = new Fingerprint.fromJson('{"version":"$kVersion","properties":{"a":"A","b":"B"},"files":{}}');
final Fingerprint b = new Fingerprint.fromJson('{"version":"$kVersion","properties":{"b":"B","a":"A"},"files":{}}');
expect(a, b);
expect(a.hashCode, b.hashCode);
}, overrides: <Type, Generator>{
FlutterVersion: () => mockVersion,
});
});
});
group('readDepfile', () {
MemoryFileSystem fs;
setUp(() {
fs = new MemoryFileSystem();
});
final Map<Type, Generator> contextOverrides = <Type, Generator>{ FileSystem: () => fs };
testUsingContext('returns one file if only one is listed', () async {
await fs.file('a.d').writeAsString('snapshot.d: /foo/a.dart');
expect(await readDepfile('a.d'), unorderedEquals(<String>['/foo/a.dart']));
}, overrides: contextOverrides);
testUsingContext('returns multiple files', () async {
await fs.file('a.d').writeAsString('snapshot.d: /foo/a.dart /foo/b.dart');
expect(await readDepfile('a.d'), unorderedEquals(<String>[
'/foo/a.dart',
'/foo/b.dart',
]));
}, overrides: contextOverrides);
testUsingContext('trims extra spaces between files', () async {
await fs.file('a.d').writeAsString('snapshot.d: /foo/a.dart /foo/b.dart /foo/c.dart');
expect(await readDepfile('a.d'), unorderedEquals(<String>[
'/foo/a.dart',
'/foo/b.dart',
'/foo/c.dart',
]));
}, overrides: contextOverrides);
testUsingContext('returns files with spaces and backslashes', () async {
await fs.file('a.d').writeAsString(r'snapshot.d: /foo/a\ a.dart /foo/b\\b.dart /foo/c\\ c.dart');
expect(await readDepfile('a.d'), unorderedEquals(<String>[
r'/foo/a a.dart',
r'/foo/b\b.dart',
r'/foo/c\ c.dart',
]));
}, overrides: contextOverrides);
});
group('Snapshotter - Script Snapshots', () {
const String kVersion = '123456abcdef';
const String kIsolateSnapshotData = 'isolate_snapshot.bin';
......@@ -520,58 +273,6 @@ void main() {
'output.snapshot': 'd41d8cd98f00b204e9800998ecf8427e',
});
}, overrides: contextOverrides);
group('createFingerprint', () {
final Map<Type, Generator> contextOverrides = <Type, Generator>{
FileSystem: () => fs,
Artifacts: () => mockArtifacts,
};
final List<String> artifactPaths = <String>[
kVmSnapshotData,
kIsolateSnapshotData,
];
testUsingContext('creates fingerprint with target platform', () {
final Fingerprint fingerprint = createFingerprint(
new SnapshotType(TargetPlatform.android_x64, BuildMode.release),
'a.dart',
<String>[],
);
expect(fingerprint, new Fingerprint.fromBuildInputs(<String, String>{
'buildMode': 'BuildMode.release',
'targetPlatform': 'TargetPlatform.android_x64',
'entryPoint': 'a.dart',
}, artifactPaths));
}, overrides: contextOverrides);
testUsingContext('creates fingerprint without target platform', () {
final Fingerprint fingerprint = createFingerprint(
new SnapshotType(null, BuildMode.release),
'a.dart',
<String>[],
);
expect(fingerprint, new Fingerprint.fromBuildInputs(<String, String>{
'buildMode': 'BuildMode.release',
'targetPlatform': '',
'entryPoint': 'a.dart',
}, artifactPaths));
}, overrides: contextOverrides);
testUsingContext('creates fingerprint with file checksums', () async {
await fs.file('a.dart').create();
await fs.file('b.dart').create();
final Fingerprint fingerprint = createFingerprint(
new SnapshotType(TargetPlatform.android_x64, BuildMode.release),
'a.dart',
<String>['a.dart', 'b.dart'],
);
expect(fingerprint, new Fingerprint.fromBuildInputs(<String, String>{
'buildMode': 'BuildMode.release',
'targetPlatform': 'TargetPlatform.android_x64',
'entryPoint': 'a.dart',
}, <String>[
'a.dart',
'b.dart',
]..addAll(artifactPaths)));
}, overrides: contextOverrides);
});
});
group('Snapshotter - iOS AOT', () {
......@@ -645,7 +346,7 @@ void main() {
genSnapshot.outputs = <String, String>{
fs.path.join(outputPath, 'snapshot_assembly.S'): '',
fs.path.join(outputPath, 'snapshot.d'): '',
fs.path.join(outputPath, 'snapshot.d'): '${fs.path.join(outputPath, 'snapshot_assembly.S')} : ',
};
final RunResult successResult = new RunResult(new ProcessResult(1, 0, '', ''));
......@@ -691,7 +392,7 @@ void main() {
genSnapshot.outputs = <String, String>{
fs.path.join(outputPath, 'snapshot_assembly.S'): '',
fs.path.join(outputPath, 'snapshot.d'): '',
fs.path.join(outputPath, 'snapshot.d'): '${fs.path.join(outputPath, 'snapshot_assembly.S')} : ',
};
final RunResult successResult = new RunResult(new ProcessResult(1, 0, '', ''));
......
// Copyright 2018 The Chromium 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:convert' show json;
import 'package:file/memory.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/fingerprint.dart';
import 'package:flutter_tools/src/version.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
import '../src/context.dart';
void main() {
group('Fingerprinter', () {
const String kVersion = '123456abcdef';
MemoryFileSystem fs;
MockFlutterVersion mockVersion;
setUp(() {
fs = new MemoryFileSystem();
mockVersion = new MockFlutterVersion();
when(mockVersion.frameworkRevision).thenReturn(kVersion);
});
final Map<Type, Generator> contextOverrides = <Type, Generator>{
FileSystem: () => fs,
};
testUsingContext('creates fingerprint with specified properties and files', () async {
await fs.file('a.dart').create();
final Fingerprinter fingerprinter = new Fingerprinter(
fingerprintPath: 'out.fingerprint',
paths: <String>['a.dart'],
properties: <String, String>{
'foo': 'bar',
'wibble': 'wobble',
},
);
final Fingerprint fingerprint = await fingerprinter.buildFingerprint();
expect(fingerprint, new Fingerprint.fromBuildInputs(<String, String>{
'foo': 'bar',
'wibble': 'wobble',
}, <String>['a.dart']));
}, overrides: contextOverrides);
testUsingContext('creates fingerprint with file checksums', () async {
await fs.file('a.dart').create();
await fs.file('b.dart').create();
final Fingerprinter fingerprinter = new Fingerprinter(
fingerprintPath: 'out.fingerprint',
paths: <String>['a.dart', 'b.dart'],
properties: <String, String>{
'bar': 'baz',
'wobble': 'womble',
},
);
final Fingerprint fingerprint = await fingerprinter.buildFingerprint();
expect(fingerprint, new Fingerprint.fromBuildInputs(<String, String>{
'bar': 'baz',
'wobble': 'womble',
}, <String>['a.dart', 'b.dart']));
}, overrides: contextOverrides);
testUsingContext('fingerprint does not match if not present', () async {
await fs.file('a.dart').create();
await fs.file('b.dart').create();
final Fingerprinter fingerprinter = new Fingerprinter(
fingerprintPath: 'out.fingerprint',
paths: <String>['a.dart', 'b.dart'],
properties: <String, String>{
'bar': 'baz',
'wobble': 'womble',
},
);
expect(await fingerprinter.doesFingerprintMatch(), isFalse);
}, overrides: contextOverrides);
testUsingContext('fingerprint does match if different', () async {
await fs.file('a.dart').create();
await fs.file('b.dart').create();
final Fingerprinter fingerprinter1 = new Fingerprinter(
fingerprintPath: 'out.fingerprint',
paths: <String>['a.dart', 'b.dart'],
properties: <String, String>{
'bar': 'baz',
'wobble': 'womble',
},
);
await fingerprinter1.writeFingerprint();
final Fingerprinter fingerprinter2 = new Fingerprinter(
fingerprintPath: 'out.fingerprint',
paths: <String>['a.dart', 'b.dart'],
properties: <String, String>{
'bar': 'baz',
'wobble': 'elbmow',
},
);
expect(await fingerprinter2.doesFingerprintMatch(), isFalse);
}, overrides: contextOverrides);
testUsingContext('fingerprint does match if identical', () async {
await fs.file('a.dart').create();
await fs.file('b.dart').create();
final Fingerprinter fingerprinter = new Fingerprinter(
fingerprintPath: 'out.fingerprint',
paths: <String>['a.dart', 'b.dart'],
properties: <String, String>{
'bar': 'baz',
'wobble': 'womble',
},
);
await fingerprinter.writeFingerprint();
expect(await fingerprinter.doesFingerprintMatch(), isTrue);
}, overrides: contextOverrides);
});
group('Fingerprint', () {
MockFlutterVersion mockVersion;
const String kVersion = '123456abcdef';
setUp(() {
mockVersion = new MockFlutterVersion();
when(mockVersion.frameworkRevision).thenReturn(kVersion);
});
group('fromBuildInputs', () {
MemoryFileSystem fs;
setUp(() {
fs = new MemoryFileSystem();
});
testUsingContext('throws if any input file does not exist', () async {
await fs.file('a.dart').create();
expect(
() => new Fingerprint.fromBuildInputs(<String, String>{}, <String>['a.dart', 'b.dart']),
throwsArgumentError,
);
}, overrides: <Type, Generator>{ FileSystem: () => fs });
testUsingContext('populates checksums for valid files', () async {
await fs.file('a.dart').writeAsString('This is a');
await fs.file('b.dart').writeAsString('This is b');
final Fingerprint fingerprint = new Fingerprint.fromBuildInputs(<String, String>{}, <String>['a.dart', 'b.dart']);
final Map<String, dynamic> jsonObject = json.decode(fingerprint.toJson());
expect(jsonObject['files'], hasLength(2));
expect(jsonObject['files']['a.dart'], '8a21a15fad560b799f6731d436c1b698');
expect(jsonObject['files']['b.dart'], '6f144e08b58cd0925328610fad7ac07c');
}, overrides: <Type, Generator>{ FileSystem: () => fs });
testUsingContext('includes framework version', () {
final Fingerprint fingerprint = new Fingerprint.fromBuildInputs(<String, String>{}, <String>[]);
final Map<String, dynamic> jsonObject = json.decode(fingerprint.toJson());
expect(jsonObject['version'], mockVersion.frameworkRevision);
}, overrides: <Type, Generator>{ FlutterVersion: () => mockVersion });
testUsingContext('includes provided properties', () {
final Fingerprint fingerprint = new Fingerprint.fromBuildInputs(<String, String>{'a': 'A', 'b': 'B'}, <String>[]);
final Map<String, dynamic> jsonObject = json.decode(fingerprint.toJson());
expect(jsonObject['properties'], hasLength(2));
expect(jsonObject['properties']['a'], 'A');
expect(jsonObject['properties']['b'], 'B');
}, overrides: <Type, Generator>{ FlutterVersion: () => mockVersion });
});
group('fromJson', () {
testUsingContext('throws if JSON is invalid', () async {
expect(() => new Fingerprint.fromJson('<xml></xml>'), throwsA(anything));
}, overrides: <Type, Generator>{
FlutterVersion: () => mockVersion,
});
testUsingContext('creates fingerprint from valid JSON', () async {
final String jsonString = json.encode(<String, dynamic>{
'version': kVersion,
'properties': <String, String>{
'buildMode': BuildMode.release.toString(),
'targetPlatform': TargetPlatform.ios.toString(),
'entryPoint': 'a.dart',
},
'files': <String, dynamic>{
'a.dart': '8a21a15fad560b799f6731d436c1b698',
'b.dart': '6f144e08b58cd0925328610fad7ac07c',
},
});
final Fingerprint fingerprint = new Fingerprint.fromJson(jsonString);
final Map<String, dynamic> content = json.decode(fingerprint.toJson());
expect(content, hasLength(3));
expect(content['version'], mockVersion.frameworkRevision);
expect(content['properties'], hasLength(3));
expect(content['properties']['buildMode'], BuildMode.release.toString());
expect(content['properties']['targetPlatform'], TargetPlatform.ios.toString());
expect(content['properties']['entryPoint'], 'a.dart');
expect(content['files'], hasLength(2));
expect(content['files']['a.dart'], '8a21a15fad560b799f6731d436c1b698');
expect(content['files']['b.dart'], '6f144e08b58cd0925328610fad7ac07c');
}, overrides: <Type, Generator>{
FlutterVersion: () => mockVersion,
});
testUsingContext('throws ArgumentError for unknown versions', () async {
final String jsonString = json.encode(<String, dynamic>{
'version': 'bad',
'properties':<String, String>{},
'files':<String, String>{},
});
expect(() => new Fingerprint.fromJson(jsonString), throwsArgumentError);
}, overrides: <Type, Generator>{
FlutterVersion: () => mockVersion,
});
testUsingContext('throws ArgumentError if version is not present', () async {
final String jsonString = json.encode(<String, dynamic>{
'properties':<String, String>{},
'files':<String, String>{},
});
expect(() => new Fingerprint.fromJson(jsonString), throwsArgumentError);
}, overrides: <Type, Generator>{
FlutterVersion: () => mockVersion,
});
testUsingContext('treats missing properties and files entries as if empty', () async {
final String jsonString = json.encode(<String, dynamic>{
'version': kVersion,
});
expect(new Fingerprint.fromJson(jsonString), new Fingerprint.fromBuildInputs(<String, String>{}, <String>[]));
}, overrides: <Type, Generator>{
FlutterVersion: () => mockVersion,
});
});
group('operator ==', () {
testUsingContext('reports not equal if properties do not match', () async {
final Map<String, dynamic> a = <String, dynamic>{
'version': kVersion,
'properties': <String, String>{
'buildMode': BuildMode.debug.toString(),
},
'files': <String, dynamic>{},
};
final Map<String, dynamic> b = new Map<String, dynamic>.from(a);
b['properties'] = <String, String>{
'buildMode': BuildMode.release.toString(),
};
expect(new Fingerprint.fromJson(json.encode(a)) == new Fingerprint.fromJson(json.encode(b)), isFalse);
}, overrides: <Type, Generator>{
FlutterVersion: () => mockVersion,
});
testUsingContext('reports not equal if file checksums do not match', () async {
final Map<String, dynamic> a = <String, dynamic>{
'version': kVersion,
'properties': <String, String>{},
'files': <String, dynamic>{
'a.dart': '8a21a15fad560b799f6731d436c1b698',
'b.dart': '6f144e08b58cd0925328610fad7ac07c',
},
};
final Map<String, dynamic> b = new Map<String, dynamic>.from(a);
b['files'] = <String, dynamic>{
'a.dart': '8a21a15fad560b799f6731d436c1b698',
'b.dart': '6f144e08b58cd0925328610fad7ac07d',
};
expect(new Fingerprint.fromJson(json.encode(a)) == new Fingerprint.fromJson(json.encode(b)), isFalse);
}, overrides: <Type, Generator>{
FlutterVersion: () => mockVersion,
});
testUsingContext('reports not equal if file paths do not match', () async {
final Map<String, dynamic> a = <String, dynamic>{
'version': kVersion,
'properties': <String, String>{},
'files': <String, dynamic>{
'a.dart': '8a21a15fad560b799f6731d436c1b698',
'b.dart': '6f144e08b58cd0925328610fad7ac07c',
},
};
final Map<String, dynamic> b = new Map<String, dynamic>.from(a);
b['files'] = <String, dynamic>{
'a.dart': '8a21a15fad560b799f6731d436c1b698',
'c.dart': '6f144e08b58cd0925328610fad7ac07d',
};
expect(new Fingerprint.fromJson(json.encode(a)) == new Fingerprint.fromJson(json.encode(b)), isFalse);
}, overrides: <Type, Generator>{
FlutterVersion: () => mockVersion,
});
testUsingContext('reports equal if properties and file checksums match', () async {
final Map<String, dynamic> a = <String, dynamic>{
'version': kVersion,
'properties': <String, String>{
'buildMode': BuildMode.debug.toString(),
'targetPlatform': TargetPlatform.ios.toString(),
'entryPoint': 'a.dart',
},
'files': <String, dynamic>{
'a.dart': '8a21a15fad560b799f6731d436c1b698',
'b.dart': '6f144e08b58cd0925328610fad7ac07c',
},
};
expect(new Fingerprint.fromJson(json.encode(a)) == new Fingerprint.fromJson(json.encode(a)), isTrue);
}, overrides: <Type, Generator>{
FlutterVersion: () => mockVersion,
});
});
group('hashCode', () {
testUsingContext('is consistent with equals, even if map entries are reordered', () async {
final Fingerprint a = new Fingerprint.fromJson('{"version":"$kVersion","properties":{"a":"A","b":"B"},"files":{}}');
final Fingerprint b = new Fingerprint.fromJson('{"version":"$kVersion","properties":{"b":"B","a":"A"},"files":{}}');
expect(a, b);
expect(a.hashCode, b.hashCode);
}, overrides: <Type, Generator>{
FlutterVersion: () => mockVersion,
});
});
});
group('readDepfile', () {
MemoryFileSystem fs;
setUp(() {
fs = new MemoryFileSystem();
});
final Map<Type, Generator> contextOverrides = <Type, Generator>{ FileSystem: () => fs };
testUsingContext('returns one file if only one is listed', () async {
await fs.file('a.d').writeAsString('snapshot.d: /foo/a.dart');
expect(await readDepfile('a.d'), unorderedEquals(<String>['/foo/a.dart']));
}, overrides: contextOverrides);
testUsingContext('returns multiple files', () async {
await fs.file('a.d').writeAsString('snapshot.d: /foo/a.dart /foo/b.dart');
expect(await readDepfile('a.d'), unorderedEquals(<String>[
'/foo/a.dart',
'/foo/b.dart',
]));
}, overrides: contextOverrides);
testUsingContext('trims extra spaces between files', () async {
await fs.file('a.d').writeAsString('snapshot.d: /foo/a.dart /foo/b.dart /foo/c.dart');
expect(await readDepfile('a.d'), unorderedEquals(<String>[
'/foo/a.dart',
'/foo/b.dart',
'/foo/c.dart',
]));
}, overrides: contextOverrides);
testUsingContext('returns files with spaces and backslashes', () async {
await fs.file('a.d').writeAsString(r'snapshot.d: /foo/a\ a.dart /foo/b\\b.dart /foo/c\\ c.dart');
expect(await readDepfile('a.d'), unorderedEquals(<String>[
r'/foo/a a.dart',
r'/foo/b\b.dart',
r'/foo/c\ c.dart',
]));
}, overrides: contextOverrides);
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment