mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
555 lines
19 KiB
Dart
555 lines
19 KiB
Dart
// Copyright 2019 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 'package:flutter_tools/src/artifacts.dart';
|
|
import 'package:flutter_tools/src/base/file_system.dart';
|
|
import 'package:flutter_tools/src/base/platform.dart';
|
|
import 'package:flutter_tools/src/build_info.dart';
|
|
import 'package:flutter_tools/src/build_system/build_system.dart';
|
|
import 'package:flutter_tools/src/build_system/exceptions.dart';
|
|
import 'package:flutter_tools/src/build_system/file_hash_store.dart';
|
|
import 'package:flutter_tools/src/build_system/filecache.pb.dart' as pb;
|
|
import 'package:flutter_tools/src/cache.dart';
|
|
import 'package:flutter_tools/src/convert.dart';
|
|
import 'package:mockito/mockito.dart';
|
|
|
|
import '../src/common.dart';
|
|
import '../src/context.dart';
|
|
import '../src/testbed.dart';
|
|
|
|
void main() {
|
|
setUpAll(() {
|
|
Cache.disableLocking();
|
|
});
|
|
|
|
group(Target, () {
|
|
Testbed testbed;
|
|
MockPlatform mockPlatform;
|
|
Environment environment;
|
|
Target fooTarget;
|
|
Target barTarget;
|
|
Target fizzTarget;
|
|
BuildSystem buildSystem;
|
|
int fooInvocations;
|
|
int barInvocations;
|
|
|
|
setUp(() {
|
|
fooInvocations = 0;
|
|
barInvocations = 0;
|
|
mockPlatform = MockPlatform();
|
|
// Keep file paths the same.
|
|
when(mockPlatform.isWindows).thenReturn(false);
|
|
testbed = Testbed(
|
|
setup: () {
|
|
environment = Environment(
|
|
projectDir: fs.currentDirectory,
|
|
);
|
|
fs.file('foo.dart').createSync(recursive: true);
|
|
fs.file('pubspec.yaml').createSync();
|
|
fooTarget = Target(
|
|
name: 'foo',
|
|
inputs: const <Source>[
|
|
Source.pattern('{PROJECT_DIR}/foo.dart'),
|
|
],
|
|
outputs: const <Source>[
|
|
Source.pattern('{BUILD_DIR}/out'),
|
|
],
|
|
dependencies: <Target>[],
|
|
buildAction: (Map<String, ChangeType> updates, Environment environment) {
|
|
environment
|
|
.buildDir
|
|
.childFile('out')
|
|
..createSync(recursive: true)
|
|
..writeAsStringSync('hey');
|
|
fooInvocations++;
|
|
}
|
|
);
|
|
barTarget = Target(
|
|
name: 'bar',
|
|
inputs: const <Source>[
|
|
Source.pattern('{BUILD_DIR}/out'),
|
|
],
|
|
outputs: const <Source>[
|
|
Source.pattern('{BUILD_DIR}/bar'),
|
|
],
|
|
dependencies: <Target>[fooTarget],
|
|
buildAction: (Map<String, ChangeType> updates, Environment environment) {
|
|
environment.buildDir
|
|
.childFile('bar')
|
|
..createSync(recursive: true)
|
|
..writeAsStringSync('there');
|
|
barInvocations++;
|
|
}
|
|
);
|
|
fizzTarget = Target(
|
|
name: 'fizz',
|
|
inputs: const <Source>[
|
|
Source.pattern('{BUILD_DIR}/out'),
|
|
],
|
|
outputs: const <Source>[
|
|
Source.pattern('{BUILD_DIR}/fizz'),
|
|
],
|
|
dependencies: <Target>[fooTarget],
|
|
buildAction: (Map<String, ChangeType> updates, Environment environment) {
|
|
throw Exception('something bad happens');
|
|
}
|
|
);
|
|
buildSystem = BuildSystem(<String, Target>{
|
|
fooTarget.name: fooTarget,
|
|
barTarget.name: barTarget,
|
|
fizzTarget.name: fizzTarget,
|
|
});
|
|
},
|
|
overrides: <Type, Generator>{
|
|
Platform: () => mockPlatform,
|
|
}
|
|
);
|
|
});
|
|
|
|
test('can describe build rules', () => testbed.run(() {
|
|
expect(buildSystem.describe('foo', environment), <Object>[
|
|
<String, Object>{
|
|
'name': 'foo',
|
|
'dependencies': <String>[],
|
|
'inputs': <String>['/foo.dart'],
|
|
'outputs': <String>[fs.path.join(environment.buildDir.path, 'out')],
|
|
'stamp': fs.path.join(environment.buildDir.path, 'foo.stamp'),
|
|
}
|
|
]);
|
|
}));
|
|
|
|
test('Throws exception if asked to build non-existent target', () => testbed.run(() {
|
|
expect(buildSystem.build('not_real', environment, const BuildSystemConfig()), throwsA(isInstanceOf<Exception>()));
|
|
}));
|
|
|
|
test('Throws exception if asked to build with missing inputs', () => testbed.run(() async {
|
|
// Delete required input file.
|
|
fs.file('foo.dart').deleteSync();
|
|
final BuildResult buildResult = await buildSystem.build('foo', environment, const BuildSystemConfig());
|
|
|
|
expect(buildResult.hasException, true);
|
|
expect(buildResult.exceptions.values.single.exception, isInstanceOf<MissingInputException>());
|
|
}));
|
|
|
|
test('Throws exception if it does not produce a specified output', () => testbed.run(() async {
|
|
final Target badTarget = Target
|
|
(buildAction: (Map<String, ChangeType> inputs, Environment environment) {},
|
|
inputs: const <Source>[
|
|
Source.pattern('{PROJECT_DIR}/foo.dart'),
|
|
],
|
|
outputs: const <Source>[
|
|
Source.pattern('{BUILD_DIR}/out')
|
|
],
|
|
name: 'bad'
|
|
);
|
|
buildSystem = BuildSystem(<String, Target>{
|
|
badTarget.name: badTarget,
|
|
});
|
|
final BuildResult result = await buildSystem.build('bad', environment, const BuildSystemConfig());
|
|
|
|
expect(result.hasException, true);
|
|
expect(result.exceptions.values.single.exception, isInstanceOf<MissingOutputException>());
|
|
}));
|
|
|
|
test('Saves a stamp file with inputs and outputs', () => testbed.run(() async {
|
|
await buildSystem.build('foo', environment, const BuildSystemConfig());
|
|
|
|
final File stampFile = fs.file(fs.path.join(environment.buildDir.path, 'foo.stamp'));
|
|
expect(stampFile.existsSync(), true);
|
|
|
|
final Map<String, Object> stampContents = json.decode(stampFile.readAsStringSync());
|
|
expect(stampContents['inputs'], <Object>['/foo.dart']);
|
|
}));
|
|
|
|
test('Does not re-invoke build if stamp is valid', () => testbed.run(() async {
|
|
await buildSystem.build('foo', environment, const BuildSystemConfig());
|
|
await buildSystem.build('foo', environment, const BuildSystemConfig());
|
|
|
|
expect(fooInvocations, 1);
|
|
}));
|
|
|
|
test('Re-invoke build if input is modified', () => testbed.run(() async {
|
|
await buildSystem.build('foo', environment, const BuildSystemConfig());
|
|
|
|
fs.file('foo.dart').writeAsStringSync('new contents');
|
|
|
|
await buildSystem.build('foo', environment, const BuildSystemConfig());
|
|
expect(fooInvocations, 2);
|
|
}));
|
|
|
|
test('does not re-invoke build if input timestamp changes', () => testbed.run(() async {
|
|
await buildSystem.build('foo', environment, const BuildSystemConfig());
|
|
|
|
fs.file('foo.dart').writeAsStringSync('');
|
|
|
|
await buildSystem.build('foo', environment, const BuildSystemConfig());
|
|
expect(fooInvocations, 1);
|
|
}));
|
|
|
|
test('does not re-invoke build if output timestamp changes', () => testbed.run(() async {
|
|
await buildSystem.build('foo', environment, const BuildSystemConfig());
|
|
|
|
environment.buildDir.childFile('out').writeAsStringSync('hey');
|
|
|
|
await buildSystem.build('foo', environment, const BuildSystemConfig());
|
|
expect(fooInvocations, 1);
|
|
}));
|
|
|
|
|
|
test('Re-invoke build if output is modified', () => testbed.run(() async {
|
|
await buildSystem.build('foo', environment, const BuildSystemConfig());
|
|
|
|
environment.buildDir.childFile('out').writeAsStringSync('Something different');
|
|
|
|
await buildSystem.build('foo', environment, const BuildSystemConfig());
|
|
expect(fooInvocations, 2);
|
|
}));
|
|
|
|
test('Runs dependencies of targets', () => testbed.run(() async {
|
|
await buildSystem.build('bar', environment, const BuildSystemConfig());
|
|
|
|
expect(fs.file(fs.path.join(environment.buildDir.path, 'bar')).existsSync(), true);
|
|
expect(fooInvocations, 1);
|
|
expect(barInvocations, 1);
|
|
}));
|
|
|
|
test('handles a throwing build action', () => testbed.run(() async {
|
|
final BuildResult result = await buildSystem.build('fizz', environment, const BuildSystemConfig());
|
|
|
|
expect(result.hasException, true);
|
|
}));
|
|
|
|
test('Can describe itself with JSON output', () => testbed.run(() {
|
|
environment.buildDir.createSync(recursive: true);
|
|
expect(fooTarget.toJson(environment), <String, dynamic>{
|
|
'inputs': <Object>[
|
|
'/foo.dart'
|
|
],
|
|
'outputs': <Object>[
|
|
fs.path.join(environment.buildDir.path, 'out'),
|
|
],
|
|
'dependencies': <Object>[],
|
|
'name': 'foo',
|
|
'stamp': fs.path.join(environment.buildDir.path, 'foo.stamp'),
|
|
});
|
|
}));
|
|
|
|
test('Compute update recognizes added files', () => testbed.run(() async {
|
|
fs.directory('build').createSync();
|
|
final FileHashStore fileCache = FileHashStore(environment);
|
|
fileCache.initialize();
|
|
final List<File> inputs = fooTarget.resolveInputs(environment);
|
|
final Map<String, ChangeType> changes = await fooTarget.computeChanges(inputs, environment, fileCache);
|
|
fileCache.persist();
|
|
|
|
expect(changes, <String, ChangeType>{
|
|
'/foo.dart': ChangeType.Added
|
|
});
|
|
|
|
await buildSystem.build('foo', environment, const BuildSystemConfig());
|
|
final Map<String, ChangeType> secondChanges = await fooTarget.computeChanges(inputs, environment, fileCache);
|
|
|
|
expect(secondChanges, <String, ChangeType>{});
|
|
}));
|
|
});
|
|
|
|
group('FileCache', () {
|
|
Testbed testbed;
|
|
Environment environment;
|
|
|
|
setUp(() {
|
|
testbed = Testbed(setup: () {
|
|
fs.directory('build').createSync();
|
|
environment = Environment(
|
|
projectDir: fs.currentDirectory,
|
|
);
|
|
});
|
|
});
|
|
|
|
test('Initializes file cache', () => testbed.run(() {
|
|
final FileHashStore fileCache = FileHashStore(environment);
|
|
fileCache.initialize();
|
|
fileCache.persist();
|
|
|
|
expect(fs.file(fs.path.join('build', '.filecache')).existsSync(), true);
|
|
|
|
final List<int> buffer = fs.file(fs.path.join('build', '.filecache')).readAsBytesSync();
|
|
final pb.FileStorage fileStorage = pb.FileStorage.fromBuffer(buffer);
|
|
|
|
expect(fileStorage.files, isEmpty);
|
|
expect(fileStorage.version, 1);
|
|
}));
|
|
|
|
test('saves and restores to file cache', () => testbed.run(() {
|
|
final File file = fs.file('foo.dart')
|
|
..createSync()
|
|
..writeAsStringSync('hello');
|
|
final FileHashStore fileCache = FileHashStore(environment);
|
|
fileCache.initialize();
|
|
fileCache.hashFiles(<File>[file]);
|
|
fileCache.persist();
|
|
final String currentHash = fileCache.currentHashes[file.resolveSymbolicLinksSync()];
|
|
final List<int> buffer = fs.file(fs.path.join('build', '.filecache')).readAsBytesSync();
|
|
pb.FileStorage fileStorage = pb.FileStorage.fromBuffer(buffer);
|
|
|
|
expect(fileStorage.files.single.hash, currentHash);
|
|
expect(fileStorage.files.single.path, file.resolveSymbolicLinksSync());
|
|
|
|
|
|
final FileHashStore newFileCache = FileHashStore(environment);
|
|
newFileCache.initialize();
|
|
expect(newFileCache.currentHashes, isEmpty);
|
|
expect(newFileCache.previousHashes[fs.path.absolute('foo.dart')], currentHash);
|
|
newFileCache.persist();
|
|
|
|
// Still persisted correctly.
|
|
fileStorage = pb.FileStorage.fromBuffer(buffer);
|
|
|
|
expect(fileStorage.files.single.hash, currentHash);
|
|
expect(fileStorage.files.single.path, file.resolveSymbolicLinksSync());
|
|
}));
|
|
});
|
|
|
|
group('Target', () {
|
|
Testbed testbed;
|
|
MockPlatform mockPlatform;
|
|
Environment environment;
|
|
Target sharedTarget;
|
|
BuildSystem buildSystem;
|
|
int shared;
|
|
|
|
setUp(() {
|
|
shared = 0;
|
|
Cache.flutterRoot = '';
|
|
mockPlatform = MockPlatform();
|
|
// Keep file paths the same.
|
|
when(mockPlatform.isWindows).thenReturn(false);
|
|
when(mockPlatform.isLinux).thenReturn(true);
|
|
when(mockPlatform.isMacOS).thenReturn(false);
|
|
testbed = Testbed(
|
|
setup: () {
|
|
environment = Environment(
|
|
projectDir: fs.currentDirectory,
|
|
);
|
|
fs.file('foo.dart').createSync(recursive: true);
|
|
fs.file('pubspec.yaml').createSync();
|
|
sharedTarget = Target(
|
|
name: 'shared',
|
|
inputs: const <Source>[
|
|
Source.pattern('{PROJECT_DIR}/foo.dart'),
|
|
],
|
|
outputs: const <Source>[],
|
|
dependencies: <Target>[],
|
|
buildAction: (Map<String, ChangeType> updates, Environment environment) {
|
|
shared += 1;
|
|
}
|
|
);
|
|
final Target fooTarget = Target(
|
|
name: 'foo',
|
|
inputs: const <Source>[
|
|
Source.pattern('{PROJECT_DIR}/foo.dart'),
|
|
],
|
|
outputs: const <Source>[
|
|
Source.pattern('{BUILD_DIR}/out'),
|
|
],
|
|
dependencies: <Target>[sharedTarget],
|
|
buildAction: (Map<String, ChangeType> updates, Environment environment) {
|
|
environment
|
|
.buildDir
|
|
.childFile('out')
|
|
..createSync(recursive: true)
|
|
..writeAsStringSync('hey');
|
|
}
|
|
);
|
|
final Target barTarget = Target(
|
|
name: 'bar',
|
|
inputs: const <Source>[
|
|
Source.pattern('{BUILD_DIR}/out'),
|
|
],
|
|
outputs: const <Source>[
|
|
Source.pattern('{BUILD_DIR}/bar'),
|
|
],
|
|
dependencies: <Target>[fooTarget, sharedTarget],
|
|
buildAction: (Map<String, ChangeType> updates, Environment environment) {
|
|
environment
|
|
.buildDir
|
|
.childFile('bar')
|
|
..createSync(recursive: true)
|
|
..writeAsStringSync('there');
|
|
}
|
|
);
|
|
buildSystem = BuildSystem(<String, Target>{
|
|
fooTarget.name: fooTarget,
|
|
barTarget.name: barTarget,
|
|
sharedTarget.name: sharedTarget,
|
|
});
|
|
},
|
|
overrides: <Type, Generator>{
|
|
Platform: () => mockPlatform,
|
|
}
|
|
);
|
|
});
|
|
|
|
test('Only invokes shared target once', () => testbed.run(() async {
|
|
await buildSystem.build('bar', environment, const BuildSystemConfig());
|
|
|
|
expect(shared, 1);
|
|
}));
|
|
});
|
|
|
|
group('Source', () {
|
|
Testbed testbed;
|
|
SourceVisitor visitor;
|
|
Environment environment;
|
|
|
|
setUp(() {
|
|
testbed = Testbed(setup: () {
|
|
fs.directory('cache').createSync();
|
|
environment = Environment(
|
|
projectDir: fs.currentDirectory,
|
|
buildDir: fs.directory('build'),
|
|
);
|
|
visitor = SourceVisitor(environment);
|
|
environment.buildDir.createSync(recursive: true);
|
|
});
|
|
});
|
|
|
|
test('configures implicit vs explict correctly', () => testbed.run(() {
|
|
expect(const Source.pattern('{PROJECT_DIR}/foo').implicit, false);
|
|
expect(const Source.pattern('{PROJECT_DIR}/*foo').implicit, true);
|
|
expect(Source.function((Environment environment) => <File>[]).implicit, true);
|
|
expect(Source.behavior(TestBehavior()).implicit, true);
|
|
}));
|
|
|
|
test('can substitute {PROJECT_DIR}/foo', () => testbed.run(() {
|
|
fs.file('foo').createSync();
|
|
const Source fooSource = Source.pattern('{PROJECT_DIR}/foo');
|
|
fooSource.accept(visitor);
|
|
|
|
expect(visitor.sources.single.path, fs.path.absolute('foo'));
|
|
}));
|
|
|
|
test('can substitute {BUILD_DIR}/bar', () => testbed.run(() {
|
|
final String path = fs.path.join(environment.buildDir.path, 'bar');
|
|
fs.file(path).createSync();
|
|
const Source barSource = Source.pattern('{BUILD_DIR}/bar');
|
|
barSource.accept(visitor);
|
|
|
|
expect(visitor.sources.single.path, fs.path.absolute(path));
|
|
}));
|
|
|
|
test('can substitute Artifact', () => testbed.run(() {
|
|
final String path = fs.path.join(
|
|
Cache.instance.getArtifactDirectory('engine').path,
|
|
'windows-x64',
|
|
'foo',
|
|
);
|
|
fs.file(path).createSync(recursive: true);
|
|
const Source fizzSource = Source.artifact(Artifact.windowsDesktopPath, platform: TargetPlatform.windows_x64);
|
|
fizzSource.accept(visitor);
|
|
|
|
expect(visitor.sources.single.resolveSymbolicLinksSync(), fs.path.absolute(path));
|
|
}));
|
|
|
|
test('can substitute {PROJECT_DIR}/*.fizz', () => testbed.run(() {
|
|
const Source fizzSource = Source.pattern('{PROJECT_DIR}/*.fizz');
|
|
fizzSource.accept(visitor);
|
|
|
|
expect(visitor.sources, isEmpty);
|
|
|
|
fs.file('foo.fizz').createSync();
|
|
fs.file('foofizz').createSync();
|
|
|
|
|
|
fizzSource.accept(visitor);
|
|
|
|
expect(visitor.sources.single.path, fs.path.absolute('foo.fizz'));
|
|
}));
|
|
|
|
test('can substitute {PROJECT_DIR}/fizz.*', () => testbed.run(() {
|
|
const Source fizzSource = Source.pattern('{PROJECT_DIR}/fizz.*');
|
|
fizzSource.accept(visitor);
|
|
|
|
expect(visitor.sources, isEmpty);
|
|
|
|
fs.file('fizz.foo').createSync();
|
|
fs.file('fizz').createSync();
|
|
|
|
fizzSource.accept(visitor);
|
|
|
|
expect(visitor.sources.single.path, fs.path.absolute('fizz.foo'));
|
|
}));
|
|
|
|
|
|
test('can substitute {PROJECT_DIR}/a*bc', () => testbed.run(() {
|
|
const Source fizzSource = Source.pattern('{PROJECT_DIR}/bc*bc');
|
|
fizzSource.accept(visitor);
|
|
|
|
expect(visitor.sources, isEmpty);
|
|
|
|
fs.file('bcbc').createSync();
|
|
fs.file('bc').createSync();
|
|
|
|
fizzSource.accept(visitor);
|
|
|
|
expect(visitor.sources.single.path, fs.path.absolute('bcbc'));
|
|
}));
|
|
|
|
|
|
test('crashes on bad substitute of two **', () => testbed.run(() {
|
|
const Source fizzSource = Source.pattern('{PROJECT_DIR}/*.*bar');
|
|
|
|
fs.file('abcd.bar').createSync();
|
|
|
|
expect(() => fizzSource.accept(visitor), throwsA(isInstanceOf<InvalidPatternException>()));
|
|
}));
|
|
|
|
|
|
test('can\'t substitute foo', () => testbed.run(() {
|
|
const Source invalidBase = Source.pattern('foo');
|
|
|
|
expect(() => invalidBase.accept(visitor), throwsA(isInstanceOf<InvalidPatternException>()));
|
|
}));
|
|
});
|
|
|
|
|
|
|
|
test('Can find dependency cycles', () {
|
|
final Target barTarget = Target(
|
|
name: 'bar',
|
|
inputs: <Source>[],
|
|
outputs: <Source>[],
|
|
buildAction: null,
|
|
dependencies: nonconst(<Target>[])
|
|
);
|
|
final Target fooTarget = Target(
|
|
name: 'foo',
|
|
inputs: <Source>[],
|
|
outputs: <Source>[],
|
|
buildAction: null,
|
|
dependencies: nonconst(<Target>[])
|
|
);
|
|
barTarget.dependencies.add(fooTarget);
|
|
fooTarget.dependencies.add(barTarget);
|
|
expect(() => checkCycles(barTarget), throwsA(isInstanceOf<CycleException>()));
|
|
});
|
|
}
|
|
|
|
class MockPlatform extends Mock implements Platform {}
|
|
|
|
// Work-around for silly lint check.
|
|
T nonconst<T>(T input) => input;
|
|
|
|
class TestBehavior extends SourceBehavior {
|
|
@override
|
|
List<File> inputs(Environment environment) {
|
|
return null;
|
|
}
|
|
|
|
@override
|
|
List<File> outputs(Environment environment) {
|
|
return null;
|
|
}
|
|
}
|