Skip to content

Support testing builder factories. #4102

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 8 additions & 22 deletions build_runner_core/test/generate/build_configuration_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,20 @@

import 'package:_test_common/common.dart';
import 'package:build/build.dart';
import 'package:build_runner_core/build_runner_core.dart';
import 'package:test/test.dart';

void main() {
test('uses builder options', () async {
Builder copyBuilder(BuilderOptions options) => TestBuilder(
buildExtensions: replaceExtension(
options.config['inputExtension'] as String,
options.config['inputExtension'] as String? ?? '',
'.copy',
),
name: 'a:optioned_builder',
);

await testPhases(
[
apply('a:optioned_builder', [copyBuilder], toRoot(), hideOutput: false),
],
await testBuilderFactories(
[copyBuilder],
{
'a|lib/file.nomatch': 'a',
'a|lib/file.matches': 'b',
Expand All @@ -32,33 +30,21 @@ targets:
inputExtension: .matches
''',
},
testingBuilderConfig: false,
outputs: {'a|lib/file.copy': 'b'},
);
});

test('isRoot is applied correctly', () async {
Builder copyBuilder(BuilderOptions options) => TestBuilder(
buildExtensions: replaceExtension(
'.txt',
options.isRoot ? '.root.copy' : '.dep.copy',
),
);
var packageGraph = buildPackageGraph({
rootPackage('a'): ['b'],
package('b'): [],
});
await testPhases(
[
apply(
'a:optioned_builder',
[copyBuilder],
toAllPackages(),
hideOutput: true,
),
],
await testBuilderFactories(
[copyBuilder],
{'a|lib/a.txt': 'a', 'b|lib/b.txt': 'b'},
outputs: {r'$$a|lib/a.root.copy': 'a', r'$$b|lib/b.dep.copy': 'b'},
packageGraph: packageGraph,
outputs: {r'a|lib/a.root.copy': 'a', r'b|lib/b.dep.copy': 'b'},
);
});
}
43 changes: 18 additions & 25 deletions build_runner_core/test/generate/build_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -243,22 +243,19 @@ void main() {
});

test('with placeholder as input', () async {
await testPhases(
[
applyToRoot(
PlaceholderBuilder(
{'lib.txt': 'libText'}.build(),
inputPlaceholder: r'$lib$',
),
),
applyToRoot(
PlaceholderBuilder(
{'root.txt': 'rootText'}.build(),
inputPlaceholder: r'$package$',
),
),
],
final builder1 = PlaceholderBuilder(
{'lib.txt': 'libText'}.build(),
inputPlaceholder: r'$lib$',
);
final builder2 = PlaceholderBuilder(
{'root.txt': 'rootText'}.build(),
inputPlaceholder: r'$package$',
);
await testBuilders(
[builder1, builder2],
{},
visibleOutputBuilders: {builder1, builder2},
rootPackage: 'a',
outputs: {'a|lib/lib.txt': 'libText', 'a|root.txt': 'rootText'},
);
});
Expand Down Expand Up @@ -1030,16 +1027,12 @@ targets:
});

test('can\'t read files in .dart_tool', () async {
await testPhases(
[
apply('', [
(_) => TestBuilder(
build: copyFrom(makeAssetId('a|.dart_tool/any_file')),
),
], toRoot()),
],
{'a|lib/a.txt': 'a', 'a|.dart_tool/any_file': 'content'},
status: BuildStatus.failure,
expect(
(await testBuilders(
[TestBuilder(build: copyFrom(makeAssetId('a|.dart_tool/any_file')))],
{'a|lib/a.txt': 'a', 'a|.dart_tool/any_file': 'content'},
)).buildResult.status,
BuildStatus.failure,
);
});

Expand Down
12 changes: 12 additions & 0 deletions build_test/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
## 3.4.0-wip

- Add `testBuilderFactories`: like `testBuilders`, but provide the builder
factories instead of the builders. Use this to allow config read from
`build.yaml` to be passed in to the factory.
- `TestBuilder` now accepts a `name`: this is the name that will be shown
in logging and can be used to refer to the builder in `build.yaml`.
- More realistic test builds: in `resolveSources` and `testBuilders`, stop
builders reading from `.dart_tool`.
- Bug fix: in `testBuilders`, configure the root package correctly when it
has no sources.

## 3.3.0

- Read build configs using `AssetReader` so they're easier to test: you can now
Expand Down
16 changes: 15 additions & 1 deletion build_test/lib/src/builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ class TestBuilder implements Builder {
@override
final Map<String, List<String>> buildExtensions;

final String name;
final BuildBehavior _build;
final BuildBehavior? _extraWork;

Expand All @@ -109,14 +110,24 @@ class TestBuilder implements Builder {
final _buildsCompletedController = StreamController<AssetId>.broadcast();
Stream<AssetId> get buildsCompleted => _buildsCompletedController.stream;

/// A test [Builder].
///
/// Runs for all inputs and write outputs with `.copy` appended. Or, pass
/// [buildExtensions] to specify input and output extensions.
///
/// Copy its input to all possible outputs. Pass [build] to replace this
/// behavior, and/or [extraWork] to add additional behavior.
///
/// Default name for logging and for `build.yaml` is `TestBuilder`. Pass
/// [name] to set the name.
TestBuilder({
Map<String, List<String>>? buildExtensions,
BuildBehavior? build,
BuildBehavior? extraWork,
this.name = 'TestBuilder',
}) : buildExtensions = buildExtensions ?? appendExtension('.copy'),
_build = build ?? _defaultBehavior,
_extraWork = extraWork;

@override
Future build(BuildStep buildStep) async {
if (!await buildStep.canRead(buildStep.inputId)) return;
Expand All @@ -125,4 +136,7 @@ class TestBuilder implements Builder {
await _extraWork?.call(buildStep, buildExtensions);
_buildsCompletedController.add(buildStep.inputId);
}

@override
String toString() => name;
}
123 changes: 105 additions & 18 deletions build_test/lib/src/test_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,62 @@ Future<TestBuilderResult> testBuilder(

/// Runs [builders] in a test environment.
///
/// Calls [testBuilderFactories] with factories that each return a member of
/// [builders], see that method for details.
///
/// Because build config is passed via factories, this method does not read
/// build config. To test with build config, use [testBuilderFactories].
Future<TestBuilderResult> testBuilders(
Iterable<Builder> builders,
Map<String, /*String|List<int>*/ Object> sourceAssets, {
Set<String>? generateFor,
bool Function(String assetId)? isInput,
String? rootPackage,
Map<String, /*String|List<int>|Matcher<List<int>>*/ Object>? outputs,
void Function(LogRecord log)? onLog,
void Function(AssetId, Iterable<AssetId>)? reportUnusedAssetsForInput,
PackageConfig? packageConfig,
Resolvers? resolvers,
Set<Builder> optionalBuilders = const {},
Set<Builder> visibleOutputBuilders = const {},
bool testingBuilderConfig = true,
TestReaderWriter? readerWriter,
bool enableLowResourceMode = false,
}) {
final builderFactories = <BuilderFactory>[];
final optionalBuilderFactories = Set<BuilderFactory>.identity();
final visibleOutputBuilderFactories = Set<BuilderFactory>.identity();
for (final builder in builders) {
Builder builderFactory(_) => builder;
builderFactories.add(builderFactory);
if (optionalBuilders.contains(builder)) {
optionalBuilderFactories.add(builderFactory);
}
if (visibleOutputBuilders.contains(builder)) {
visibleOutputBuilderFactories.add(builderFactory);
}
}
return testBuilderFactories(
builderFactories,
sourceAssets,
generateFor: generateFor,
isInput: isInput,
rootPackage: rootPackage,
outputs: outputs,
onLog: onLog,
reportUnusedAssetsForInput: reportUnusedAssetsForInput,
packageConfig: packageConfig,
resolvers: resolvers,
optionalBuilderFactories: optionalBuilderFactories,
visibleOutputBuilderFactories: visibleOutputBuilderFactories,
testingBuilderConfig: testingBuilderConfig,
readerWriter: readerWriter,
enableLowResourceMode: enableLowResourceMode,
);
}

/// Runs [builderFactories] in a test environment.
///
/// The test environment supplies in-memory build [sourceAssets] to the builders
/// under test.
///
Expand Down Expand Up @@ -197,12 +253,13 @@ Future<TestBuilderResult> testBuilder(
/// Enabling of language experiments is supported through the
/// `withEnabledExperiments` method from package:build.
///
/// To mark a builder as optional, add it to [optionalBuilders]. Optional
/// builders only run if their output is used by a non-optional builder.
/// To mark a builder as optional, add its builder to
/// [optionalBuilderFactories]. Optional builders only run if their output is
/// used by a non-optional builder.
///
/// To mark a builder's output as visible, add it to [visibleOutputBuilders].
/// The builder then writes its outputs next to its input, instead of hidden
/// under `.dart_tool`.
/// To mark a builder's output as visible, add its factory to
/// [visibleOutputBuilderFactories]. The builder then writes its outputs next to
/// its input, instead of hidden under `.dart_tool`.
///
/// The default builder config will be overwritten with one that causes the
/// builder to run for all inputs. To use the default builder config instead,
Expand All @@ -217,8 +274,8 @@ Future<TestBuilderResult> testBuilder(
/// Returns a [TestBuilderResult] with the [BuildResult] and the
/// [TestReaderWriter] used for the build, which can be used for further
/// checks.
Future<TestBuilderResult> testBuilders(
Iterable<Builder> builders,
Future<TestBuilderResult> testBuilderFactories(
Iterable<BuilderFactory> builderFactories,
Map<String, /*String|List<int>*/ Object> sourceAssets, {
Set<String>? generateFor,
bool Function(String assetId)? isInput,
Expand All @@ -228,8 +285,8 @@ Future<TestBuilderResult> testBuilders(
void Function(AssetId, Iterable<AssetId>)? reportUnusedAssetsForInput,
PackageConfig? packageConfig,
Resolvers? resolvers,
Set<Builder> optionalBuilders = const {},
Set<Builder> visibleOutputBuilders = const {},
Set<BuilderFactory> optionalBuilderFactories = const {},
Set<BuilderFactory> visibleOutputBuilderFactories = const {},
bool testingBuilderConfig = true,
TestReaderWriter? readerWriter,
bool enableLowResourceMode = false,
Expand All @@ -240,10 +297,18 @@ Future<TestBuilderResult> testBuilders(
for (var descriptor in sourceAssets.keys) makeAssetId(descriptor),
};

if (inputIds.isEmpty && rootPackage == null) {
throw ArgumentError(
'`sourceAssets` is empty so `rootPackage` must be specified, '
'but `rootPackage` is null.',
);
}

// Differentiate input packages and all packages. Builders run on input
// packages; they can read/resolve all packages. Additional packages are
// supplied by passing a `readerWriter`.
var inputPackages = {for (var id in inputIds) id.package};
var inputPackages =
inputIds.isEmpty ? {rootPackage!} : {for (var id in inputIds) id.package};
final allPackages = inputPackages.toSet();
if (readerWriter != null) {
for (final asset in readerWriter.testing.assets) {
Expand Down Expand Up @@ -329,7 +394,11 @@ Future<TestBuilderResult> testBuilders(
if (package != rootPackage)
...defaultNonRootVisibleAssets,
...inputIds
.where((id) => id.package == package)
.where(
(id) =>
id.package == package &&
!id.path.startsWith('.dart_tool/'),
)
.map((id) => Glob.quote(id.path)),
],
},
Expand All @@ -346,16 +415,34 @@ Future<TestBuilderResult> testBuilders(
deleteFilesByDefault: true,
);

final buildSeries = await BuildSeries.create(buildOptions, environment, [
for (final builder in builders)
final builderApplications = <BuilderApplication>[];
for (final builderFactory in builderFactories) {
// The real build gets the name from the `build.yaml` where the builder is
// For tests, use the builder class name, or fall back if the test makes the
// builder factory throw.
String name;
try {
name = builderName(builderFactory(const BuilderOptions({})));
} catch (e) {
name = e.toString();
}
builderApplications.add(
apply(
builderName(builder),
[(_) => builder],
name,
[builderFactory],
(p) => inputPackages.contains(p.name),
isOptional: optionalBuilders.contains(builder),
hideOutput: !visibleOutputBuilders.contains(builder),
isOptional: optionalBuilderFactories.contains(builderFactory),
hideOutput: !visibleOutputBuilderFactories.contains(builderFactory),
),
], {});
);
}

final buildSeries = await BuildSeries.create(
buildOptions,
environment,
builderApplications,
{},
);

// Run the build.
final buildResult = await buildSeries.run({});
Expand Down
2 changes: 1 addition & 1 deletion build_test/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: build_test
description: Utilities for writing unit tests of Builders.
version: 3.3.0
version: 3.4.0-wip
repository: https://github.com/dart-lang/build/tree/master/build_test
resolution: workspace

Expand Down