blob: 5948bda228dfe067d49ea4198606af4f4568241c [file] [log] [blame]
// Copyright 2014 The Flutter 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:file/memory.dart';
import 'package:file_testing/file_testing.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:mockito/mockito.dart';
import 'package:platform/platform.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/utils.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/convert.dart';
import '../../src/common.dart';
import '../../src/context.dart';
void main() {
FileSystem fileSystem;
Environment environment;
Target fooTarget;
Target barTarget;
Target fizzTarget;
Target sharedTarget;
int fooInvocations;
int barInvocations;
int shared;
setUp(() {
fileSystem = MemoryFileSystem.test();
fooInvocations = 0;
barInvocations = 0;
shared = 0;
/// Create various test targets.
fooTarget = TestTarget((Environment environment) async {
environment
.buildDir
.childFile('out')
..createSync(recursive: true)
..writeAsStringSync('hey');
fooInvocations++;
})
..name = 'foo'
..inputs = const <Source>[
Source.pattern('{PROJECT_DIR}/foo.dart'),
]
..outputs = const <Source>[
Source.pattern('{BUILD_DIR}/out'),
]
..dependencies = <Target>[];
barTarget = TestTarget((Environment environment) async {
environment.buildDir
.childFile('bar')
..createSync(recursive: true)
..writeAsStringSync('there');
barInvocations++;
})
..name = 'bar'
..inputs = const <Source>[
Source.pattern('{BUILD_DIR}/out'),
]
..outputs = const <Source>[
Source.pattern('{BUILD_DIR}/bar'),
]
..dependencies = <Target>[];
fizzTarget = TestTarget((Environment environment) async {
throw Exception('something bad happens');
})
..name = 'fizz'
..inputs = const <Source>[
Source.pattern('{BUILD_DIR}/out'),
]
..outputs = const <Source>[
Source.pattern('{BUILD_DIR}/fizz'),
]
..dependencies = <Target>[fooTarget];
sharedTarget = TestTarget((Environment environment) async {
shared += 1;
})
..name = 'shared'
..inputs = const <Source>[
Source.pattern('{PROJECT_DIR}/foo.dart'),
];
environment = Environment.test(
fileSystem.currentDirectory,
artifacts: MockArtifacts(),
processManager: FakeProcessManager.any(),
fileSystem: fileSystem,
logger: BufferLogger.test(),
);
fileSystem.file('foo.dart')
..createSync(recursive: true)
..writeAsStringSync('');
fileSystem.file('pubspec.yaml').createSync();
});
testWithoutContext('Does not throw exception if asked to build with missing inputs', () async {
final BuildSystem buildSystem = setUpBuildSystem(fileSystem);
// Delete required input file.
fileSystem.file('foo.dart').deleteSync();
final BuildResult buildResult = await buildSystem.build(fooTarget, environment);
expect(buildResult.hasException, false);
});
testWithoutContext('Does not throw exception if it does not produce a specified output', () async {
final BuildSystem buildSystem = setUpBuildSystem(fileSystem);
// This target is document as producing foo.dart but does not actually
// output this value.
final Target badTarget = TestTarget((Environment environment) async {})
..inputs = const <Source>[
Source.pattern('{PROJECT_DIR}/foo.dart'),
]
..outputs = const <Source>[
Source.pattern('{BUILD_DIR}/out'),
];
final BuildResult result = await buildSystem.build(badTarget, environment);
expect(result.hasException, false);
});
testWithoutContext('Saves a stamp file with inputs and outputs', () async {
final BuildSystem buildSystem = setUpBuildSystem(fileSystem);
await buildSystem.build(fooTarget, environment);
final File stampFile = fileSystem.file(
'${environment.buildDir.path}/foo.stamp');
expect(stampFile, exists);
final Map<String, dynamic> stampContents = castStringKeyedMap(
json.decode(stampFile.readAsStringSync()));
expect(stampContents, containsPair('inputs', <Object>['/foo.dart']));
});
testWithoutContext('Creates a BuildResult with inputs and outputs', () async {
final BuildSystem buildSystem = setUpBuildSystem(fileSystem);
final BuildResult result = await buildSystem.build(fooTarget, environment);
expect(result.inputFiles.single.path, '/foo.dart');
expect(result.outputFiles.single.path, '${environment.buildDir.path}/out');
});
testWithoutContext('Does not re-invoke build if stamp is valid', () async {
final BuildSystem buildSystem = setUpBuildSystem(fileSystem);
await buildSystem.build(fooTarget, environment);
await buildSystem.build(fooTarget, environment);
expect(fooInvocations, 1);
});
testWithoutContext('Re-invoke build if input is modified', () async {
final BuildSystem buildSystem = setUpBuildSystem(fileSystem);
await buildSystem.build(fooTarget, environment);
fileSystem.file('foo.dart').writeAsStringSync('new contents');
await buildSystem.build(fooTarget, environment);
expect(fooInvocations, 2);
});
testWithoutContext('does not re-invoke build if input timestamp changes', () async {
final BuildSystem buildSystem = setUpBuildSystem(fileSystem);
await buildSystem.build(fooTarget, environment);
// The file was previously empty so this does not modify it.
fileSystem.file('foo.dart').writeAsStringSync('');
await buildSystem.build(fooTarget, environment);
expect(fooInvocations, 1);
});
testWithoutContext('does not re-invoke build if output timestamp changes', () async {
final BuildSystem buildSystem = setUpBuildSystem(fileSystem);
await buildSystem.build(fooTarget, environment);
// This is the same content that the output file previously
// contained.
environment.buildDir.childFile('out').writeAsStringSync('hey');
await buildSystem.build(fooTarget, environment);
expect(fooInvocations, 1);
});
testWithoutContext('Re-invoke build if output is modified', () async {
final BuildSystem buildSystem = setUpBuildSystem(fileSystem);
await buildSystem.build(fooTarget, environment);
environment.buildDir.childFile('out').writeAsStringSync('Something different');
await buildSystem.build(fooTarget, environment);
expect(fooInvocations, 2);
});
testWithoutContext('Runs dependencies of targets', () async {
final BuildSystem buildSystem = setUpBuildSystem(fileSystem);
barTarget.dependencies.add(fooTarget);
await buildSystem.build(barTarget, environment);
expect(fileSystem.file('${environment.buildDir.path}/bar'), exists);
expect(fooInvocations, 1);
expect(barInvocations, 1);
});
testWithoutContext('Only invokes shared dependencies once', () async {
final BuildSystem buildSystem = setUpBuildSystem(fileSystem);
fooTarget.dependencies.add(sharedTarget);
barTarget.dependencies.add(sharedTarget);
barTarget.dependencies.add(fooTarget);
await buildSystem.build(barTarget, environment);
expect(shared, 1);
});
testWithoutContext('Automatically cleans old outputs when build graph changes', () async {
final BuildSystem buildSystem = setUpBuildSystem(fileSystem);
final TestTarget testTarget = TestTarget((Environment envionment) async {
environment.buildDir.childFile('foo.out').createSync();
})
..inputs = const <Source>[Source.pattern('{PROJECT_DIR}/foo.dart')]
..outputs = const <Source>[Source.pattern('{BUILD_DIR}/foo.out')];
fileSystem.file('foo.dart').createSync();
await buildSystem.build(testTarget, environment);
expect(environment.buildDir.childFile('foo.out'), exists);
final TestTarget testTarget2 = TestTarget((Environment envionment) async {
environment.buildDir.childFile('bar.out').createSync();
})
..inputs = const <Source>[Source.pattern('{PROJECT_DIR}/foo.dart')]
..outputs = const <Source>[Source.pattern('{BUILD_DIR}/bar.out')];
await buildSystem.build(testTarget2, environment);
expect(environment.buildDir.childFile('bar.out'), exists);
expect(environment.buildDir.childFile('foo.out'), isNot(exists));
});
testWithoutContext('Does not crash when filesytem and cache are out of sync', () async {
final BuildSystem buildSystem = setUpBuildSystem(fileSystem);
final TestTarget testWithoutContextTarget = TestTarget((Environment environment) async {
environment.buildDir.childFile('foo.out').createSync();
})
..inputs = const <Source>[Source.pattern('{PROJECT_DIR}/foo.dart')]
..outputs = const <Source>[Source.pattern('{BUILD_DIR}/foo.out')];
fileSystem.file('foo.dart').createSync();
await buildSystem.build(testWithoutContextTarget, environment);
expect(environment.buildDir.childFile('foo.out'), exists);
environment.buildDir.childFile('foo.out').deleteSync();
final TestTarget testWithoutContextTarget2 = TestTarget((Environment environment) async {
environment.buildDir.childFile('bar.out').createSync();
})
..inputs = const <Source>[Source.pattern('{PROJECT_DIR}/foo.dart')]
..outputs = const <Source>[Source.pattern('{BUILD_DIR}/bar.out')];
await buildSystem.build(testWithoutContextTarget2, environment);
expect(environment.buildDir.childFile('bar.out'), exists);
expect(environment.buildDir.childFile('foo.out'), isNot(exists));
});
testWithoutContext('Reruns build if stamp is corrupted', () async {
final BuildSystem buildSystem = setUpBuildSystem(fileSystem);
final TestTarget testWithoutContextTarget = TestTarget((Environment envionment) async {
environment.buildDir.childFile('foo.out').createSync();
})
..inputs = const <Source>[Source.pattern('{PROJECT_DIR}/foo.dart')]
..outputs = const <Source>[Source.pattern('{BUILD_DIR}/foo.out')];
fileSystem.file('foo.dart').createSync();
await buildSystem.build(testWithoutContextTarget, environment);
// invalid JSON
environment.buildDir.childFile('testWithoutContext.stamp').writeAsStringSync('{X');
await buildSystem.build(testWithoutContextTarget, environment);
// empty file
environment.buildDir.childFile('testWithoutContext.stamp').writeAsStringSync('');
await buildSystem.build(testWithoutContextTarget, environment);
// invalid format
environment.buildDir.childFile('testWithoutContext.stamp').writeAsStringSync('{"inputs": 2, "outputs": 3}');
await buildSystem.build(testWithoutContextTarget, environment);
});
testWithoutContext('handles a throwing build action without crashing', () async {
final BuildSystem buildSystem = setUpBuildSystem(fileSystem);
final BuildResult result = await buildSystem.build(fizzTarget, environment);
expect(result.hasException, true);
});
testWithoutContext('Can describe itself with JSON output', () {
environment.buildDir.createSync(recursive: true);
expect(fooTarget.toJson(environment), <String, dynamic>{
'inputs': <Object>[
'/foo.dart',
],
'outputs': <Object>[
fileSystem.path.join(environment.buildDir.path, 'out'),
],
'dependencies': <Object>[],
'name': 'foo',
'stamp': fileSystem.path.join(environment.buildDir.path, 'foo.stamp'),
});
});
testWithoutContext('Can find dependency cycles', () {
final Target barTarget = TestTarget()..name = 'bar';
final Target fooTarget = TestTarget()..name = 'foo';
barTarget.dependencies.add(fooTarget);
fooTarget.dependencies.add(barTarget);
expect(() => checkCycles(barTarget), throwsA(isA<CycleException>()));
});
testWithoutContext('Target with depfile dependency will not run twice without invalidation', () async {
final BuildSystem buildSystem = setUpBuildSystem(fileSystem);
int called = 0;
final TestTarget target = TestTarget((Environment environment) async {
environment.buildDir
.childFile('example.d')
.writeAsStringSync('a.txt: b.txt');
fileSystem.file('a.txt').writeAsStringSync('a');
called += 1;
})
..depfiles = <String>['example.d'];
fileSystem.file('b.txt').writeAsStringSync('b');
await buildSystem.build(target, environment);
expect(fileSystem.file('a.txt'), exists);
expect(called, 1);
// Second build is up to date due to depfile parse.
await buildSystem.build(target, environment);
expect(called, 1);
});
testWithoutContext('output directory is an input to the build', () async {
final Environment environmentA = Environment.test(
fileSystem.currentDirectory,
outputDir: fileSystem.directory('a'),
artifacts: MockArtifacts(),
processManager: FakeProcessManager.any(),
fileSystem: fileSystem,
logger: BufferLogger.test(),
);
final Environment environmentB = Environment.test(
fileSystem.currentDirectory,
outputDir: fileSystem.directory('b'),
artifacts: MockArtifacts(),
processManager: FakeProcessManager.any(),
fileSystem: fileSystem,
logger: BufferLogger.test(),
);
expect(environmentA.buildDir.path, isNot(environmentB.buildDir.path));
});
testWithoutContext('A target with depfile dependencies can delete stale outputs on the first run', () async {
final BuildSystem buildSystem = setUpBuildSystem(fileSystem);
int called = 0;
final TestTarget target = TestTarget((Environment environment) async {
if (called == 0) {
environment.buildDir.childFile('example.d')
.writeAsStringSync('a.txt c.txt: b.txt');
fileSystem.file('a.txt').writeAsStringSync('a');
fileSystem.file('c.txt').writeAsStringSync('a');
} else {
// On second run, we no longer claim c.txt as an output.
environment.buildDir.childFile('example.d')
.writeAsStringSync('a.txt: b.txt');
fileSystem.file('a.txt').writeAsStringSync('a');
}
called += 1;
})
..depfiles = const <String>['example.d'];
fileSystem.file('b.txt').writeAsStringSync('b');
await buildSystem.build(target, environment);
expect(fileSystem.file('a.txt'), exists);
expect(fileSystem.file('c.txt'), exists);
expect(called, 1);
// rewrite an input to force a rerun, expect that the old c.txt is deleted.
fileSystem.file('b.txt').writeAsStringSync('ba');
await buildSystem.build(target, environment);
expect(fileSystem.file('a.txt'), exists);
expect(fileSystem.file('c.txt'), isNot(exists));
expect(called, 2);
});
}
BuildSystem setUpBuildSystem(FileSystem fileSystem) {
return BuildSystem(
fileSystem: fileSystem,
logger: BufferLogger.test(),
platform: FakePlatform(operatingSystem: 'linux'),
);
}
class TestTarget extends Target {
TestTarget([this._build]);
final Future<void> Function(Environment environment) _build;
@override
Future<void> build(Environment environment) => _build(environment);
@override
List<Target> dependencies = <Target>[];
@override
List<Source> inputs = <Source>[];
@override
List<String> depfiles = <String>[];
@override
String name = 'test';
@override
List<Source> outputs = <Source>[];
}
class MockArtifacts extends Mock implements Artifacts {}