From 3458a0d36547bf541aa115060543855bd093f6d2 Mon Sep 17 00:00:00 2001 From: David Morgan Date: Mon, 14 Jul 2025 11:59:19 +0200 Subject: [PATCH 01/18] Remove testPhases. --- .../generate/build_configuration_test.dart | 28 ++---- build_test/lib/src/builder.dart | 5 ++ build_test/lib/src/test_builder.dart | 86 ++++++++++++++++--- 3 files changed, 85 insertions(+), 34 deletions(-) diff --git a/build_runner_core/test/generate/build_configuration_test.dart b/build_runner_core/test/generate/build_configuration_test.dart index 564346f5a..cf8fa1037 100644 --- a/build_runner_core/test/generate/build_configuration_test.dart +++ b/build_runner_core/test/generate/build_configuration_test.dart @@ -11,15 +11,14 @@ 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', @@ -32,6 +31,7 @@ targets: inputExtension: .matches ''', }, + testingBuilderConfig: false, outputs: {'a|lib/file.copy': 'b'}, ); }); @@ -43,22 +43,10 @@ targets: 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'}, ); }); } diff --git a/build_test/lib/src/builder.dart b/build_test/lib/src/builder.dart index e0cffab60..fc4471fe0 100644 --- a/build_test/lib/src/builder.dart +++ b/build_test/lib/src/builder.dart @@ -94,6 +94,7 @@ class TestBuilder implements Builder { @override final Map> buildExtensions; + final String name; final BuildBehavior _build; final BuildBehavior? _extraWork; @@ -113,6 +114,7 @@ class TestBuilder implements Builder { Map>? buildExtensions, BuildBehavior? build, BuildBehavior? extraWork, + this.name = 'TestBuilder', }) : buildExtensions = buildExtensions ?? appendExtension('.copy'), _build = build ?? _defaultBehavior, _extraWork = extraWork; @@ -125,4 +127,7 @@ class TestBuilder implements Builder { await _extraWork?.call(buildStep, buildExtensions); _buildsCompletedController.add(buildStep.inputId); } + + @override + String toString() => name; } diff --git a/build_test/lib/src/test_builder.dart b/build_test/lib/src/test_builder.dart index f17d2fcd2..a1e3a7d68 100644 --- a/build_test/lib/src/test_builder.dart +++ b/build_test/lib/src/test_builder.dart @@ -3,6 +3,7 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:async'; import 'dart:convert'; +import 'dart:ffi'; import 'dart:io'; import 'package:build/build.dart'; @@ -160,6 +161,62 @@ Future 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 testBuilders( + Iterable builders, + Map*/ Object> sourceAssets, { + Set? generateFor, + bool Function(String assetId)? isInput, + String? rootPackage, + Map|Matcher>*/ Object>? outputs, + void Function(LogRecord log)? onLog, + void Function(AssetId, Iterable)? reportUnusedAssetsForInput, + PackageConfig? packageConfig, + Resolvers? resolvers, + Set optionalBuilders = const {}, + Set visibleOutputBuilders = const {}, + bool testingBuilderConfig = true, + TestReaderWriter? readerWriter, + bool enableLowResourceMode = false, +}) { + final builderFactories = []; + final optionalBuilderFactories = Set.identity(); + final visibleOutputBuilderFactories = Set.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. /// @@ -197,12 +254,13 @@ Future 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, @@ -217,8 +275,8 @@ Future testBuilder( /// Returns a [TestBuilderResult] with the [BuildResult] and the /// [TestReaderWriter] used for the build, which can be used for further /// checks. -Future testBuilders( - Iterable builders, +Future testBuilderFactories( + Iterable builderFactories, Map*/ Object> sourceAssets, { Set? generateFor, bool Function(String assetId)? isInput, @@ -228,8 +286,8 @@ Future testBuilders( void Function(AssetId, Iterable)? reportUnusedAssetsForInput, PackageConfig? packageConfig, Resolvers? resolvers, - Set optionalBuilders = const {}, - Set visibleOutputBuilders = const {}, + Set optionalBuilderFactories = const {}, + Set visibleOutputBuilderFactories = const {}, bool testingBuilderConfig = true, TestReaderWriter? readerWriter, bool enableLowResourceMode = false, @@ -347,13 +405,13 @@ Future testBuilders( ); final buildSeries = await BuildSeries.create(buildOptions, environment, [ - for (final builder in builders) + for (final builderFactory in builderFactories) apply( - builderName(builder), - [(_) => builder], + builderName(builderFactory(const BuilderOptions({}))), + [builderFactory], (p) => inputPackages.contains(p.name), - isOptional: optionalBuilders.contains(builder), - hideOutput: !visibleOutputBuilders.contains(builder), + isOptional: optionalBuilderFactories.contains(builderFactory), + hideOutput: !visibleOutputBuilderFactories.contains(builderFactory), ), ], {}); From da972ce3137bff84c50b7e97d52e6cd08cb548e5 Mon Sep 17 00:00:00 2001 From: David Morgan Date: Mon, 14 Jul 2025 12:15:21 +0200 Subject: [PATCH 02/18] More. --- .../generate/build_configuration_test.dart | 1 - .../test/generate/resolver_reuse_test.dart | 121 ++++++++---------- 2 files changed, 53 insertions(+), 69 deletions(-) diff --git a/build_runner_core/test/generate/build_configuration_test.dart b/build_runner_core/test/generate/build_configuration_test.dart index cf8fa1037..256a95729 100644 --- a/build_runner_core/test/generate/build_configuration_test.dart +++ b/build_runner_core/test/generate/build_configuration_test.dart @@ -4,7 +4,6 @@ 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() { diff --git a/build_runner_core/test/generate/resolver_reuse_test.dart b/build_runner_core/test/generate/resolver_reuse_test.dart index 62f33dcfc..80552550f 100644 --- a/build_runner_core/test/generate/resolver_reuse_test.dart +++ b/build_runner_core/test/generate/resolver_reuse_test.dart @@ -8,7 +8,6 @@ import 'dart:async'; import 'package:_test_common/common.dart'; import 'package:build/build.dart'; -import 'package:build_config/build_config.dart'; import 'package:build_runner_core/build_runner_core.dart'; import 'package:test/test.dart'; @@ -70,19 +69,14 @@ void main() { ); }, ); - await testPhases( + await testBuilders( [ - applyToRoot(optionalWithResolver, isOptional: true), - applyToRoot( - nonOptionalWritesImportedFile, - generateFor: const InputSet(include: ['lib/file.dart']), - ), - applyToRoot( - nonOptionalResolveImportedFile, - generateFor: const InputSet(include: ['lib/file.dart']), - ), + optionalWithResolver, + nonOptionalWritesImportedFile, + nonOptionalResolveImportedFile, ], {'a|lib/file.dart': 'import "file.imported.dart";'}, + optionalBuilders: {optionalWithResolver}, outputs: { 'a|lib/file.dart.bar': '[SomeClass]', 'a|lib/file.dart.foo': 'anything', @@ -95,68 +89,58 @@ void main() { test('A hidden generated file does not poison resolving', () async { final slowBuilderCompleter = Completer(); final builders = [ - applyToRoot( - TestBuilder( - buildExtensions: replaceExtension('.dart', '.g1.dart'), - build: (buildStep, _) async { - // Put the analysis driver into the bad state. - await buildStep.inputLibrary; - await buildStep.writeAsString( - buildStep.inputId.changeExtension('.g1.dart'), - 'class Annotation {const Annotation();}', - ); - }, - ), - generateFor: const InputSet(include: ['lib/a.dart']), + TestBuilder( + buildExtensions: replaceExtension('.dart', '.g1.dart'), + build: (buildStep, _) async { + if (buildStep.inputId.path != 'lib/a.dart') return; + // Put the analysis driver into the bad state. + await buildStep.inputLibrary; + await buildStep.writeAsString( + buildStep.inputId.changeExtension('.g1.dart'), + 'class Annotation {const Annotation();}', + ); + }, ), - applyToRoot( - TestBuilder( - buildExtensions: replaceExtension('.dart', '.g2.dart'), - build: (buildStep, _) async { - var library = await buildStep.inputLibrary; - var annotation = - library.topLevelFunctions.single.metadata2.annotations.single - .computeConstantValue(); - await buildStep.writeAsString( - buildStep.inputId.changeExtension('.g2.dart'), - '//$annotation', - ); - slowBuilderCompleter.complete(); - }, - ), - isOptional: true, - generateFor: const InputSet(include: ['lib/a.dart']), + TestBuilder( + buildExtensions: replaceExtension('.dart', '.g2.dart'), + build: (buildStep, _) async { + if (buildStep.inputId.path != 'lib/a.dart') return; + var library = await buildStep.inputLibrary; + var annotation = + library.topLevelFunctions.single.metadata2.annotations.single + .computeConstantValue(); + await buildStep.writeAsString( + buildStep.inputId.changeExtension('.g2.dart'), + '//$annotation', + ); + slowBuilderCompleter.complete(); + }, ), - applyToRoot( - TestBuilder( - buildExtensions: replaceExtension('.dart', '.slow.dart'), - build: (buildStep, _) async { - // The test relies on `g2` generation running so that - // `slowBuilderCompleter` is completed. It's in an earlier phase, - // so it always _can_ run earlier, but it's not guaranteed. Read - // it so that it actually does run earlier. - await buildStep.canRead(AssetId('a', 'lib/a.g2.dart')); - await slowBuilderCompleter.future; - await buildStep.writeAsString( - buildStep.inputId.changeExtension('.slow.dart'), - '', - ); - }, - ), - isOptional: true, - generateFor: const InputSet(include: ['lib/b.dart']), + TestBuilder( + buildExtensions: replaceExtension('.dart', '.slow.dart'), + build: (buildStep, _) async { + if (buildStep.inputId.path != 'lib/b.dart') return; + // The test relies on `g2` generation running so that + // `slowBuilderCompleter` is completed. It's in an earlier phase, + // so it always _can_ run earlier, but it's not guaranteed. Read + // it so that it actually does run earlier. + await buildStep.canRead(AssetId('a', 'lib/a.g2.dart')); + await slowBuilderCompleter.future; + await buildStep.writeAsString( + buildStep.inputId.changeExtension('.slow.dart'), + '', + ); + }, ), - applyToRoot( - TestBuilder( - buildExtensions: replaceExtension('.dart', '.root'), - build: (buildStep, _) async { - await buildStep.inputLibrary; - }, - ), - generateFor: const InputSet(include: ['lib/b.dart']), + TestBuilder( + buildExtensions: replaceExtension('.dart', '.root'), + build: (buildStep, _) async { + if (buildStep.inputId.path != 'lib/b.dart') return; + await buildStep.inputLibrary; + }, ), ]; - await testPhases( + await testBuilders( builders, { 'a|lib/a.dart': ''' @@ -172,6 +156,7 @@ import 'a.g2.dart'; import 'b.slow.dart'; ''', }, + optionalBuilders: {builders[1]}, outputs: { 'a|lib/a.g1.dart': 'class Annotation {const Annotation();}', 'a|lib/a.g2.dart': '//Annotation ()', From a0986e155ccff0e1ac701f674de0f25842ea08b4 Mon Sep 17 00:00:00 2001 From: David Morgan Date: Mon, 14 Jul 2025 12:34:36 +0200 Subject: [PATCH 03/18] More. --- .../test/generate/build_error_test.dart | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/build_runner_core/test/generate/build_error_test.dart b/build_runner_core/test/generate/build_error_test.dart index 34baf33cf..73026d213 100644 --- a/build_runner_core/test/generate/build_error_test.dart +++ b/build_runner_core/test/generate/build_error_test.dart @@ -26,37 +26,35 @@ void main() { }); test('should fail if a severe logged', () async { - await testPhases( - [applyToRoot(_LoggingBuilder(Level.SEVERE))], - {'a|lib/a.dart': ''}, - packageGraph: buildPackageGraph({rootPackage('a'): []}), - checkBuildStatus: true, - status: BuildStatus.failure, - outputs: {'a|lib/a.dart.empty': ''}, + expect( + (await testBuilders( + [_LoggingBuilder(Level.SEVERE)], + {'a|lib/a.dart': ''}, + outputs: {'a|lib/a.dart.empty': ''}, + )).buildResult.status, + BuildStatus.failure, ); }); test('should fail if a severe was logged on a previous build', () async { - var packageGraph = buildPackageGraph({rootPackage('a'): []}); - var builder = _LoggingBuilder(Level.SEVERE); - var builders = [applyToRoot(builder)]; - final result = await testPhases( - builders, + var result = await testBuilders( + [_LoggingBuilder(Level.SEVERE)], {'a|lib/a.dart': ''}, - packageGraph: packageGraph, - checkBuildStatus: true, - status: BuildStatus.failure, outputs: {'a|lib/a.dart.empty': ''}, ); - await testPhases( - builders, - {}, - resumeFrom: result, - packageGraph: packageGraph, - checkBuildStatus: true, - status: BuildStatus.failure, - outputs: {}, + expect(result.buildResult.status, BuildStatus.failure); + + final builder = _LoggingBuilder(Level.SEVERE); + result = await testBuilders( + [builder], + {'a|lib/a.dart': ''}, + // Pass the output from the previous build, so the build resumes. + readerWriter: result.readerWriter, + outputs: {'a|lib/a.dart.empty': ''}, ); + expect(result.buildResult.status, BuildStatus.failure); + // Should have failed without actually building again. + expect(builder.built, false); }); test( @@ -145,11 +143,13 @@ void main() { class _LoggingBuilder implements Builder { Level level; + bool built = false; _LoggingBuilder(this.level); @override Future build(BuildStep buildStep) async { + built = true; log.log(level, buildStep.inputId.toString()); await buildStep.canRead(buildStep.inputId); await buildStep.writeAsString(buildStep.inputId.addExtension('.empty'), ''); From 8518783f6334b6b0f2b82d72faf324f2d90f46e3 Mon Sep 17 00:00:00 2001 From: David Morgan Date: Tue, 15 Jul 2025 09:50:12 +0200 Subject: [PATCH 04/18] More. --- .../test/generate/build_error_test.dart | 98 ++++++++----------- 1 file changed, 42 insertions(+), 56 deletions(-) diff --git a/build_runner_core/test/generate/build_error_test.dart b/build_runner_core/test/generate/build_error_test.dart index 73026d213..7eb2c9a7a 100644 --- a/build_runner_core/test/generate/build_error_test.dart +++ b/build_runner_core/test/generate/build_error_test.dart @@ -48,7 +48,7 @@ void main() { result = await testBuilders( [builder], {'a|lib/a.dart': ''}, - // Pass the output from the previous build, so the build resumes. + // Resume from the previous builld. readerWriter: result.readerWriter, outputs: {'a|lib/a.dart.empty': ''}, ); @@ -60,83 +60,69 @@ void main() { test( 'should succeed if a severe log is fixed on a subsequent build', () async { - var packageGraph = buildPackageGraph({rootPackage('a'): []}); - var builder = _LoggingBuilder(Level.SEVERE); - var builders = [applyToRoot(builder)]; - final result = await testPhases( - builders, + var result = await testBuilders( + [_LoggingBuilder(Level.SEVERE)], {'a|lib/a.dart': ''}, - packageGraph: packageGraph, - checkBuildStatus: true, - status: BuildStatus.failure, outputs: {'a|lib/a.dart.empty': ''}, ); - builder.level = Level.WARNING; - await testPhases( - builders, + expect(result.buildResult.status, BuildStatus.failure); + + result = await testBuilders( + [_LoggingBuilder(Level.WARNING)], {'a|lib/a.dart': 'changed'}, - resumeFrom: result, - packageGraph: packageGraph, - checkBuildStatus: true, - status: BuildStatus.success, + // Resume from the previous builld. + readerWriter: result.readerWriter, outputs: {'a|lib/a.dart.empty': ''}, ); + expect(result.buildResult.status, BuildStatus.success); }, ); test('should fail if an exception is thrown', () async { - await testPhases( - [ - applyToRoot( - TestBuilder(build: (_, _) => throw Exception('Some build failure')), - ), - ], - {'a|lib/a.txt': ''}, - packageGraph: buildPackageGraph({rootPackage('a'): []}), - status: BuildStatus.failure, + expect( + (await testBuilders( + [TestBuilder(build: (_, _) => throw Exception('Some build failure'))], + {'a|lib/a.txt': ''}, + )).buildResult.status, + BuildStatus.failure, ); }); test( 'should throw an exception if a read is attempted on a failed file', () async { - await testPhases( + final result = await testBuilders( [ - applyToRoot( - TestBuilder( - buildExtensions: replaceExtension('.txt', '.failed'), - build: (buildStep, _) async { - await buildStep.writeAsString( - buildStep.inputId.changeExtension('.failed'), - 'failed', - ); - log.severe('Wrote an output then failed'); - }, - ), + TestBuilder( + buildExtensions: replaceExtension('.txt', '.failed'), + build: (buildStep, _) async { + await buildStep.writeAsString( + buildStep.inputId.changeExtension('.failed'), + 'failed', + ); + log.severe('Wrote an output then failed'); + }, ), - applyToRoot( - TestBuilder( - buildExtensions: replaceExtension('.txt', '.success'), - build: expectAsync2((buildStep, _) async { - // Attempts to read the file that came from a failing build step - // and hides the exception. - var failedFile = buildStep.inputId.changeExtension('.failed'); - await expectLater( - buildStep.readAsString(failedFile), - throwsA(anything), - ); - await buildStep.writeAsString( - buildStep.inputId.changeExtension('.success'), - 'success', - ); - }), - ), + TestBuilder( + buildExtensions: replaceExtension('.txt', '.success'), + build: expectAsync2((buildStep, _) async { + // Attempts to read the file that came from a failing build step + // and hides the exception. + var failedFile = buildStep.inputId.changeExtension('.failed'); + await expectLater( + buildStep.readAsString(failedFile), + throwsA(anything), + ); + await buildStep.writeAsString( + buildStep.inputId.changeExtension('.success'), + 'success', + ); + }), ), ], {'a|lib/a.txt': ''}, - packageGraph: buildPackageGraph({rootPackage('a'): []}), - status: BuildStatus.failure, ); + expect(result.buildResult.status, BuildStatus.failure); }, ); } From b893f575e162059a5e47265f48aa3f381f89b42a Mon Sep 17 00:00:00 2001 From: David Morgan Date: Tue, 15 Jul 2025 09:53:35 +0200 Subject: [PATCH 05/18] More. --- .../generate/custom_generated_dir_test.dart | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/build_runner_core/test/generate/custom_generated_dir_test.dart b/build_runner_core/test/generate/custom_generated_dir_test.dart index b555af826..633e9155e 100644 --- a/build_runner_core/test/generate/custom_generated_dir_test.dart +++ b/build_runner_core/test/generate/custom_generated_dir_test.dart @@ -11,24 +11,10 @@ void main() { final customGeneratedDir = 'my-custom-dir'; overrideGeneratedOutputDirectory(customGeneratedDir); - late PackageGraph packageGraph; - - setUp(() { - packageGraph = buildPackageGraph({rootPackage('a', path: 'a/'): []}); - }); - test('can output files to a custom generated dir', () async { - final result = await testPhases( - [ - applyToRoot( - TestBuilder(buildExtensions: appendExtension('.copy', from: '.txt')), - hideOutput: true, - ), - ], + final result = await testBuilders( + [TestBuilder(buildExtensions: appendExtension('.copy', from: '.txt'))], {'a|lib/a.txt': 'a'}, - packageGraph: packageGraph, - outputs: {r'$$a|lib/a.txt.copy': 'a'}, - expectedGeneratedDir: customGeneratedDir, ); expect( result.readerWriter.testing.exists( From 2d7598de9f2eb61a1eb14b5e1f211c34d9d948dc Mon Sep 17 00:00:00 2001 From: David Morgan Date: Tue, 15 Jul 2025 10:08:40 +0200 Subject: [PATCH 06/18] More. --- .../test/generate/build_test.dart | 63 +++++-------------- build_test/lib/src/test_builder.dart | 27 ++++++-- 2 files changed, 39 insertions(+), 51 deletions(-) diff --git a/build_runner_core/test/generate/build_test.dart b/build_runner_core/test/generate/build_test.dart index a620ea42b..e100fd8a5 100644 --- a/build_runner_core/test/generate/build_test.dart +++ b/build_runner_core/test/generate/build_test.dart @@ -47,20 +47,12 @@ void main() { group('build', () { test('can log within a buildFactory', () async { - await testPhases( + await testBuilderFactories( [ - apply( - '', - [ - (_) { - log.info('I can log!'); - return TestBuilder(buildExtensions: appendExtension('.1')); - }, - ], - toRoot(), - isOptional: true, - hideOutput: false, - ), + (_) { + log.info('I can log!'); + return TestBuilder(buildExtensions: appendExtension('.1')); + }, ], {'a|web/a.txt': 'a'}, ); @@ -68,27 +60,14 @@ void main() { test('Builder factories are only invoked once per application', () async { var invokedCount = 0; - final packageGraph = buildPackageGraph({ - rootPackage('a'): ['b'], - package('b'): [], - }); - await testPhases( - [ - apply( - '', - [ - (_) { - invokedCount += 1; - return TestBuilder(); - }, - ], - toAllPackages(), - isOptional: false, - hideOutput: true, - ), - ], - {}, - packageGraph: packageGraph, + Builder builderFactory(_) { + invokedCount += 1; + return TestBuilder(); + } + + await testBuilderFactories( + [builderFactory], + {'a|lib/a.dart': '', 'b|lib/b.dart': ''}, ); // Once per package, including the SDK. @@ -97,19 +76,11 @@ void main() { test('throws an error if the builderFactory fails', () async { expect( - () async => await testPhases( + () async => await testBuilderFactories( [ - apply( - '', - [ - (_) { - throw StateError('some error'); - }, - ], - toRoot(), - isOptional: true, - hideOutput: false, - ), + (_) { + throw StateError('some error'); + }, ], {'a|web/a.txt': 'a'}, ), diff --git a/build_test/lib/src/test_builder.dart b/build_test/lib/src/test_builder.dart index a1e3a7d68..b5539f79f 100644 --- a/build_test/lib/src/test_builder.dart +++ b/build_test/lib/src/test_builder.dart @@ -3,7 +3,6 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:async'; import 'dart:convert'; -import 'dart:ffi'; import 'dart:io'; import 'package:build/build.dart'; @@ -404,16 +403,34 @@ Future testBuilderFactories( deleteFilesByDefault: true, ); - final buildSeries = await BuildSeries.create(buildOptions, environment, [ - for (final builderFactory in builderFactories) + final builderApplications = []; + 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(builderFactory(const BuilderOptions({}))), + name, [builderFactory], (p) => inputPackages.contains(p.name), isOptional: optionalBuilderFactories.contains(builderFactory), hideOutput: !visibleOutputBuilderFactories.contains(builderFactory), ), - ], {}); + ); + } + + final buildSeries = await BuildSeries.create( + buildOptions, + environment, + builderApplications, + {}, + ); // Run the build. final buildResult = await buildSeries.run({}); From 10f65d7efbf7b45effcb97ea48d691188d2976e5 Mon Sep 17 00:00:00 2001 From: David Morgan Date: Tue, 15 Jul 2025 10:15:30 +0200 Subject: [PATCH 07/18] More. --- .../test/generate/build_test.dart | 116 ++++++++---------- 1 file changed, 48 insertions(+), 68 deletions(-) diff --git a/build_runner_core/test/generate/build_test.dart b/build_runner_core/test/generate/build_test.dart index e100fd8a5..f005d5042 100644 --- a/build_runner_core/test/generate/build_test.dart +++ b/build_runner_core/test/generate/build_test.dart @@ -88,45 +88,37 @@ void main() { ); }); - test('throws an error if any output extensions match input extensions', () { - expect( - testPhases( - [ - apply( - '', - [ - expectAsync1( - (_) => TestBuilder( - buildExtensions: { - '.dart': ['.g.dart', '.json'], - '.json': ['.dart'], - }, + test( + 'throws an error if any output extensions match input extensions', + () async { + await expectLater( + () async => await testBuilderFactories( + [ + (_) => TestBuilder( + buildExtensions: { + '.dart': ['.g.dart', '.json'], + '.json': ['.dart'], + }, + ), + ], + {'a|lib/a.dart': ''}, + ), + throwsA( + isA() + .having((e) => e.name, 'name', 'TestBuilder.buildExtensions') + .having( + (e) => e.message, + 'message', + allOf( + contains('.json'), + contains('.dart'), + isNot(contains('.g.dart')), ), ), - ], - toRoot(), - isOptional: false, - hideOutput: false, - ), - ], - {}, - status: BuildStatus.failure, - ), - throwsA( - isA() - .having((e) => e.name, 'name', 'TestBuilder.buildExtensions') - .having( - (e) => e.message, - 'message', - allOf( - contains('.json'), - contains('.dart'), - isNot(contains('.g.dart')), - ), - ), - ), - ); - }); + ), + ); + }, + ); test('runs a max of one concurrent action per phase', () async { var assets = {}; @@ -136,34 +128,22 @@ void main() { var concurrentCount = 0; var maxConcurrentCount = 0; var reachedMax = Completer(); - await testPhases( + await testBuilders( [ - apply( - '', - [ - (_) { - return TestBuilder( - build: (_, _) async { - concurrentCount += 1; - maxConcurrentCount = math.max( - concurrentCount, - maxConcurrentCount, - ); - if (concurrentCount >= 1 && !reachedMax.isCompleted) { - await Future.delayed( - const Duration(milliseconds: 100), - ); - if (!reachedMax.isCompleted) reachedMax.complete(null); - } - await reachedMax.future; - concurrentCount -= 1; - }, - ); - }, - ], - toRoot(), - isOptional: false, - hideOutput: false, + TestBuilder( + build: (_, _) async { + concurrentCount += 1; + maxConcurrentCount = math.max( + concurrentCount, + maxConcurrentCount, + ); + if (concurrentCount >= 1 && !reachedMax.isCompleted) { + await Future.delayed(const Duration(milliseconds: 100)); + if (!reachedMax.isCompleted) reachedMax.complete(null); + } + await reachedMax.future; + concurrentCount -= 1; + }, ), ], assets, @@ -174,8 +154,8 @@ void main() { group('with root package inputs', () { test('one phase, one builder, one-to-one outputs', () async { - await testPhases( - [copyABuilderApplication], + await testBuilders( + [testBuilder], {'a|web/a.txt': 'a', 'a|lib/b.txt': 'b'}, outputs: {'a|web/a.txt.copy': 'a', 'a|lib/b.txt.copy': 'b'}, ); @@ -192,8 +172,8 @@ void main() { }, ); - await testPhases( - [applyToRoot(testBuilder)], + await testBuilders( + [testBuilder], {'a|web/a.txt': ''}, outputs: {'a|web/a.txt.1': '', 'a|web/a.txt.2': ''}, ); From a53be9ed675f1058ce31171ce39a74e080a292ce Mon Sep 17 00:00:00 2001 From: David Morgan Date: Tue, 15 Jul 2025 10:32:00 +0200 Subject: [PATCH 08/18] Bug fix: empty sources. --- .../test/generate/build_test.dart | 25 ++++++++----------- build_test/CHANGELOG.md | 2 ++ build_test/lib/src/test_builder.dart | 10 +++++++- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/build_runner_core/test/generate/build_test.dart b/build_runner_core/test/generate/build_test.dart index f005d5042..59f94f473 100644 --- a/build_runner_core/test/generate/build_test.dart +++ b/build_runner_core/test/generate/build_test.dart @@ -194,22 +194,17 @@ 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', + }, inputExtension: r'$lib$'); + final builder2 = PlaceholderBuilder({ + 'root.txt': 'rootText', + }, inputExtension: r'$package$'); + await testBuilders( + [builder1, builder2], {}, + visibleOutputBuilders: {builder1, builder2}, + rootPackage: 'a', outputs: {'a|lib/lib.txt': 'libText', 'a|root.txt': 'rootText'}, ); }); diff --git a/build_test/CHANGELOG.md b/build_test/CHANGELOG.md index b4d94072d..2627cd277 100644 --- a/build_test/CHANGELOG.md +++ b/build_test/CHANGELOG.md @@ -19,6 +19,8 @@ pass in `build.yaml` like any other asset. - Bug fix: don't crash when a builder logs during a `testBuilder` or `resolveSource` call outside a test. +- Bug fix: in `testBuilder`, if no sources are passed, still treat `rootPackage` + as an input package. - Remove unused deps: `async`, `convert`. - Remove unused dev_deps: `collection`. diff --git a/build_test/lib/src/test_builder.dart b/build_test/lib/src/test_builder.dart index b5539f79f..f995984b1 100644 --- a/build_test/lib/src/test_builder.dart +++ b/build_test/lib/src/test_builder.dart @@ -297,10 +297,18 @@ Future testBuilderFactories( 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) { From 26fffb82a03a2306f20beb0ea8ab3ef226b14673 Mon Sep 17 00:00:00 2001 From: David Morgan Date: Tue, 15 Jul 2025 10:32:32 +0200 Subject: [PATCH 09/18] More. --- .../test/generate/build_test.dart | 140 ++++++------------ 1 file changed, 47 insertions(+), 93 deletions(-) diff --git a/build_runner_core/test/generate/build_test.dart b/build_runner_core/test/generate/build_test.dart index 59f94f473..4917fade2 100644 --- a/build_runner_core/test/generate/build_test.dart +++ b/build_runner_core/test/generate/build_test.dart @@ -210,12 +210,10 @@ void main() { }); test('one phase, one builder, one-to-many outputs', () async { - await testPhases( + await testBuilders( [ - applyToRoot( - TestBuilder( - buildExtensions: appendExtension('.copy', numCopies: 2), - ), + TestBuilder( + buildExtensions: appendExtension('.copy', numCopies: 2), ), ], {'a|web/a.txt': 'a', 'a|lib/b.txt': 'b'}, @@ -229,14 +227,12 @@ void main() { }); test('outputs with ^', () async { - await testPhases( + await testBuilders( [ - applyToRoot( - TestBuilder( - buildExtensions: { - '^pubspec.yaml': ['pubspec.yaml.copy'], - }, - ), + TestBuilder( + buildExtensions: { + '^pubspec.yaml': ['pubspec.yaml.copy'], + }, ), ], {'a|pubspec.yaml': 'a', 'a|lib/pubspec.yaml': 'a'}, @@ -245,14 +241,12 @@ void main() { }); test('outputs with a capture group', () async { - await testPhases( + await testBuilders( [ - applyToRoot( - TestBuilder( - buildExtensions: { - 'assets/{{}}.txt': ['lib/src/generated/{{}}.dart'], - }, - ), + TestBuilder( + buildExtensions: { + 'assets/{{}}.txt': ['lib/src/generated/{{}}.dart'], + }, ), ], {'a|assets/nested/input/file.txt': 'a'}, @@ -261,25 +255,20 @@ void main() { }); test('recognizes right optional builder with capture groups', () async { - await testPhases( - [ - applyToRoot( - TestBuilder( - buildExtensions: { - 'assets/{{}}.txt': ['lib/src/generated/{{}}.dart'], - }, - ), - isOptional: true, - ), - applyToRoot( - TestBuilder( - buildExtensions: { - '.dart': ['.copy.dart'], - }, - ), - ), - ], + final builder1 = TestBuilder( + buildExtensions: { + 'assets/{{}}.txt': ['lib/src/generated/{{}}.dart'], + }, + ); + final builder2 = TestBuilder( + buildExtensions: { + '.dart': ['.copy.dart'], + }, + ); + await testBuilders( + [builder1, builder2], {'a|assets/nested/input/file.txt': 'a'}, + optionalBuilders: {builder1}, outputs: { 'a|lib/src/generated/nested/input/file.dart': 'a', 'a|lib/src/generated/nested/input/file.copy.dart': 'a', @@ -290,25 +279,13 @@ void main() { test( 'optional build actions don\'t run if their outputs aren\'t read', () async { - await testPhases( - [ - apply( - '', - [(_) => TestBuilder(buildExtensions: appendExtension('.1'))], - toRoot(), - isOptional: true, - ), - apply( - 'a:only_on_1', - [ - (_) => TestBuilder( - buildExtensions: appendExtension('.copy', from: '.1'), - ), - ], - toRoot(), - isOptional: true, - ), - ], + final builder1 = TestBuilder(buildExtensions: appendExtension('.1')); + final builder2 = TestBuilder( + buildExtensions: appendExtension('.copy', from: '.1'), + ); + await testBuilders( + [builder1, builder2], + optionalBuilders: {builder1, builder2}, {'a|lib/a.txt': 'a'}, outputs: {}, ); @@ -316,36 +293,17 @@ void main() { ); test('optional build actions do run if their outputs are read', () async { - await testPhases( - [ - apply( - '', - [(_) => TestBuilder(buildExtensions: appendExtension('.1'))], - toRoot(), - isOptional: true, - hideOutput: false, - ), - apply( - '', - [ - (_) => - TestBuilder(buildExtensions: replaceExtension('.1', '.2')), - ], - toRoot(), - isOptional: true, - hideOutput: false, - ), - apply( - '', - [ - (_) => - TestBuilder(buildExtensions: replaceExtension('.2', '.3')), - ], - toRoot(), - hideOutput: false, - ), - ], + final builder1 = TestBuilder(buildExtensions: appendExtension('.1')); + final builder2 = TestBuilder( + buildExtensions: replaceExtension('.1', '.2'), + ); + final builder3 = TestBuilder( + buildExtensions: replaceExtension('.2', '.3'), + ); + await testBuilders( + [builder1, builder2, builder3], {'a|web/a.txt': 'a'}, + optionalBuilders: {builder1, builder2}, outputs: { 'a|web/a.txt.1': 'a', 'a|web/a.txt.2': 'a', @@ -412,16 +370,12 @@ targets: }); test('allows running on generated inputs that do not match target ' - 'source globx', () async { + 'source globs', () async { var builders = [ - applyToRoot( - TestBuilder(buildExtensions: appendExtension('.1', from: '.txt')), - ), - applyToRoot( - TestBuilder(buildExtensions: appendExtension('.2', from: '.1')), - ), + TestBuilder(buildExtensions: appendExtension('.1', from: '.txt')), + TestBuilder(buildExtensions: appendExtension('.2', from: '.1')), ]; - await testPhases( + await testBuilders( builders, { 'a|lib/a.txt': 'a', From bb4ac78564ad4066b57d1347b33dfd780a4f5e71 Mon Sep 17 00:00:00 2001 From: David Morgan Date: Tue, 15 Jul 2025 10:53:27 +0200 Subject: [PATCH 10/18] More. --- .../test/generate/build_test.dart | 61 ++++++++----------- 1 file changed, 24 insertions(+), 37 deletions(-) diff --git a/build_runner_core/test/generate/build_test.dart b/build_runner_core/test/generate/build_test.dart index 4917fade2..c7b78313b 100644 --- a/build_runner_core/test/generate/build_test.dart +++ b/build_runner_core/test/generate/build_test.dart @@ -393,23 +393,17 @@ targets: test('early step touches a not-yet-generated asset', () async { var copyId = AssetId('a', 'lib/file.a.copy'); var builders = [ - applyToRoot( - TestBuilder( - buildExtensions: appendExtension('.copy', from: '.b'), - extraWork: (buildStep, _) => buildStep.canRead(copyId), - ), - ), - applyToRoot( - TestBuilder(buildExtensions: appendExtension('.copy', from: '.a')), + TestBuilder( + buildExtensions: appendExtension('.copy', from: '.b'), + extraWork: (buildStep, _) => buildStep.canRead(copyId), ), - applyToRoot( - TestBuilder( - buildExtensions: appendExtension('.exists', from: '.a'), - build: writeCanRead(copyId), - ), + TestBuilder(buildExtensions: appendExtension('.copy', from: '.a')), + TestBuilder( + buildExtensions: appendExtension('.exists', from: '.a'), + build: writeCanRead(copyId), ), ]; - await testPhases( + await testBuilders( builders, {'a|lib/file.a': 'a', 'a|lib/file.b': 'b'}, outputs: { @@ -428,32 +422,29 @@ targets: build: writeCanRead(aTxtId), ); var builders = [ - applyToRoot(firstBuilder), - applyToRoot( - TestBuilder( - buildExtensions: appendExtension('.exists', from: '.b'), - build: (_, _) => ready.future, - extraWork: writeCanRead(aTxtId), - ), + firstBuilder, + TestBuilder( + buildExtensions: appendExtension('.exists', from: '.b'), + build: (_, _) => ready.future, + extraWork: writeCanRead(aTxtId), ), ]; - // Do an first build so a reader is created. - final result = await testPhases(builders, {'unused|lib/unused.a': ''}); - - // After the first builder runs, delete the asset from the reader and - // allow the 2nd builder to run. + // After the first builder runs, delete the asset from the in-memory + //filesystem and allow the 2nd builder to run. + final readerWriter = TestReaderWriter(rootPackage: 'a'); unawaited( firstBuilder.buildsCompleted.first.then((id) { - result.readerWriter.testing.delete(aTxtId); + readerWriter.testing.delete(aTxtId); ready.complete(); }), ); - await testPhases( + await testBuilders( builders, {'a|lib/file.a': '', 'a|lib/file.b': ''}, - resumeFrom: result, + readerWriter: readerWriter, + rootPackage: 'a', outputs: { 'a|lib/file.a.exists': 'true', 'a|lib/file.b.exists': 'true', @@ -462,18 +453,15 @@ targets: }); test('pre-existing outputs', () async { - final result = await testPhases( + final result = await testBuilders( [ - copyABuilderApplication, - applyToRoot( - TestBuilder( - buildExtensions: appendExtension('.clone', from: '.copy'), - ), + testBuilder, + TestBuilder( + buildExtensions: appendExtension('.clone', from: '.copy'), ), ], {'a|web/a.txt': 'a', 'a|web/a.txt.copy': 'a'}, outputs: {'a|web/a.txt.copy': 'a', 'a|web/a.txt.copy.clone': 'a'}, - deleteFilesByDefault: true, ); var graphId = makeAssetId('a|$assetGraphPath'); @@ -488,7 +476,6 @@ targets: makeAssetId('a|web/a.txt.copy'), makeAssetId('a|web/a.txt.copy.clone'), ...placeholders, - makeAssetId('a|.dart_tool/package_config.json'), ]), ); expect(cachedGraph.sources, [makeAssetId('a|web/a.txt')]); From fa6fc06e0811d54ff48ef2f65c1cb6b6145828f3 Mon Sep 17 00:00:00 2001 From: David Morgan Date: Tue, 15 Jul 2025 11:05:54 +0200 Subject: [PATCH 11/18] More. --- .../test/generate/build_test.dart | 200 +++++++----------- 1 file changed, 71 insertions(+), 129 deletions(-) diff --git a/build_runner_core/test/generate/build_test.dart b/build_runner_core/test/generate/build_test.dart index c7b78313b..66c2fad81 100644 --- a/build_runner_core/test/generate/build_test.dart +++ b/build_runner_core/test/generate/build_test.dart @@ -498,13 +498,16 @@ targets: }); test('previous outputs are cleaned up', () async { - final result = await testPhases( - [copyABuilderApplication], + final result = await testBuilders( + [testBuilder], {'a|web/a.txt': 'a'}, outputs: {'a|web/a.txt.copy': 'a'}, ); - var copyId = makeAssetId('a|web/a.txt.copy'); + final copyId = makeAssetId( + 'a|.dart_tool/build/generated/a/web/a.txt.copy', + ); + expect(result.readerWriter.testing.exists(copyId), isTrue); var canReadInBuild = Completer(); var blockingCompleter = Completer(); @@ -519,11 +522,10 @@ targets: await blockingCompleter.future; }, ); - var done = testPhases( - [applyToRoot(builder)], + var done = testBuilders( + [builder], {'a|web/a.txt': 'b'}, - resumeFrom: result, - outputs: {'a|web/a.txt.copy': 'b'}, + readerWriter: result.readerWriter, ); // Before the build starts we should still see the asset, we haven't @@ -584,19 +586,12 @@ additional_public_assets: group('reading assets outside of the root package', () { test('can read public non-lib assets', () async { - final packageGraph = buildPackageGraph({ - rootPackage('a', path: 'a/'): ['b'], - package('b', path: 'a/b'): [], - }); - final builder = TestBuilder( build: copyFrom(makeAssetId('b|test/foo.bar')), ); - await testPhases( - [ - apply('', [(_) => builder], toPackage('a')), - ], + await testBuilders( + [builder], { 'a|lib/a.foo': '', 'b|test/foo.bar': 'content', @@ -605,17 +600,14 @@ additional_public_assets: - test/** ''', }, - packageGraph: packageGraph, - outputs: {r'$$a|lib/a.foo.copy': 'content'}, + // Visible output so it only runs on the root package `a`. + visibleOutputBuilders: {builder}, + outputs: {r'a|lib/a.foo.copy': 'content'}, + testingBuilderConfig: false, ); }); test('reading private assets throws InvalidInputException', () { - final packageGraph = buildPackageGraph({ - rootPackage('a', path: 'a/'): ['b'], - package('b', path: 'a/b'): [], - }); - final builder = TestBuilder( buildExtensions: const { '.txt': ['.copy'], @@ -637,22 +629,14 @@ additional_public_assets: }, ); - return testPhases( - [ - apply('', [(_) => builder], toPackage('a')), - ], + return testBuilders( + [builder], {'a|lib/foo.txt': "doesn't matter"}, - packageGraph: packageGraph, outputs: {}, ); }); test('canRead doesn\'t throw for invalid inputs or missing packages', () { - final packageGraph = buildPackageGraph({ - rootPackage('a', path: 'a/'): ['b'], - package('b', path: 'a/b'): [], - }); - final builder = TestBuilder( buildExtensions: const { '.txt': ['.copy'], @@ -669,12 +653,9 @@ additional_public_assets: }, ); - return testPhases( - [ - apply('', [(_) => builder], toPackage('a')), - ], + return testBuilders( + [builder], {'a|lib/foo.txt': "doesn't matter"}, - packageGraph: packageGraph, outputs: {}, ); }); @@ -683,21 +664,12 @@ additional_public_assets: test( 'skips builders which would output files in non-root packages', () async { - final packageGraph = buildPackageGraph({ - rootPackage('a', path: 'a/'): ['b'], - package('b', path: 'a/b'): [], - }); - await testPhases( - [ - apply( - '', - [(_) => TestBuilder()], - toPackage('b'), - hideOutput: false, - ), - ], + await testBuilders( + [testBuilder], {'b|lib/b.txt': 'b'}, - packageGraph: packageGraph, + // Visible output so it only runs on the root package `a`. + visibleOutputBuilders: {testBuilder}, + rootPackage: 'a', outputs: {}, ); }, @@ -731,45 +703,43 @@ additional_public_assets: }); test('handles mixed hidden and non-hidden outputs', () async { - await testPhases( + final result = await testBuilders( [ - applyToRoot(TestBuilder()), - applyToRoot( - TestBuilder(buildExtensions: appendExtension('.hiddencopy')), - hideOutput: true, - ), + testBuilder, + TestBuilder(buildExtensions: appendExtension('.hiddencopy')), ], {'a|lib/a.txt': 'a'}, - packageGraph: packageGraph, + visibleOutputBuilders: {testBuilder}, outputs: { - r'$$a|lib/a.txt.hiddencopy': 'a', - r'$$a|lib/a.txt.copy.hiddencopy': 'a', + r'a|lib/a.txt.hiddencopy': 'a', + r'a|lib/a.txt.copy.hiddencopy': 'a', r'a|lib/a.txt.copy': 'a', }, ); + // Two of the outputs are under the generated output path. + expect( + result.readerWriter.testing.assets.where( + (a) => a.path.contains('.dart_tool/build/generated'), + ), + hasLength(2), + ); }); test('allows reading hidden outputs from another package to create ' 'a non-hidden output', () async { - await testPhases( - [ - apply( - 'hidden_on_b', - [(_) => TestBuilder()], - toPackage('b'), - hideOutput: true, - ), - applyToRoot( - TestBuilder( - buildExtensions: appendExtension('.check_can_read'), - build: writeCanRead(makeAssetId('b|lib/b.txt.copy')), - ), - ), - ], + final builder1 = TestBuilder(); + final builder2 = TestBuilder( + buildExtensions: appendExtension('.check_can_read'), + build: writeCanRead(makeAssetId('b|lib/b.txt.copy')), + ); + await testBuilders( + [builder1, builder2], {'a|lib/a.txt': 'a', 'b|lib/b.txt': 'b'}, - packageGraph: packageGraph, + visibleOutputBuilders: {builder2}, outputs: { - r'$$b|lib/b.txt.copy': 'b', + r'a|lib/a.txt.copy': 'a', + r'a|lib/a.txt.copy.check_can_read': 'true', + r'b|lib/b.txt.copy': 'b', r'a|lib/a.txt.check_can_read': 'true', }, ); @@ -777,20 +747,17 @@ additional_public_assets: test('allows reading hidden outputs from same package to create ' 'a non-hidden output', () async { - await testPhases( - [ - applyToRoot(TestBuilder(), hideOutput: true), - applyToRoot( - TestBuilder( - buildExtensions: appendExtension('.check_can_read'), - build: writeCanRead(makeAssetId('a|lib/a.txt.copy')), - ), - ), - ], + final builder1 = TestBuilder(); + final builder2 = TestBuilder( + buildExtensions: appendExtension('.check_can_read'), + build: writeCanRead(makeAssetId('a|lib/a.txt.copy')), + ); + await testBuilders( + [builder1, builder2], {'a|lib/a.txt': 'a'}, - packageGraph: packageGraph, + visibleOutputBuilders: {builder2}, outputs: { - r'$$a|lib/a.txt.copy': 'a', + r'a|lib/a.txt.copy': 'a', r'a|lib/a.txt.copy.check_can_read': 'true', r'a|lib/a.txt.check_can_read': 'true', }, @@ -821,46 +788,21 @@ additional_public_assets: }); test('can read files from external packages', () async { - var packageGraph = buildPackageGraph({ - rootPackage('a'): ['b'], - package('b'): [], - }); - - var builders = [ - apply( - '', - [ - (_) => TestBuilder( - extraWork: - (buildStep, _) => - buildStep.canRead(makeAssetId('b|lib/b.txt')), - ), - ], - toRoot(), - hideOutput: false, - ), - ]; - await testPhases( - builders, + var builder = TestBuilder( + extraWork: + (buildStep, _) => buildStep.canRead(makeAssetId('b|lib/b.txt')), + ); + await testBuilders( + [builder], + visibleOutputBuilders: {builder}, {'a|lib/a.txt': 'a', 'b|lib/b.txt': 'b'}, outputs: {'a|lib/a.txt.copy': 'a'}, - packageGraph: packageGraph, ); }); test('can glob files from packages', () async { - final packageGraph = buildPackageGraph({ - rootPackage('a', path: 'a/'): ['b'], - package('b', path: 'a/b/'): [], - }); - - var builders = [ - apply('', [(_) => globBuilder], toRoot(), hideOutput: true), - apply('', [(_) => globBuilder], toPackage('b'), hideOutput: true), - ]; - - await testPhases( - builders, + await testBuilders( + [globBuilder], { 'a|lib/a.globPlaceholder': '', 'a|lib/a.txt': '', @@ -872,16 +814,15 @@ additional_public_assets: 'b|web/b.txt': '', }, outputs: { - r'$$a|lib/a.matchingFiles': 'a|lib/a.txt\na|lib/b.txt\na|web/a.txt', - r'$$b|lib/b.matchingFiles': 'b|lib/c.txt\nb|lib/d.txt', + r'a|lib/a.matchingFiles': 'a|lib/a.txt\na|lib/b.txt\na|web/a.txt', + r'b|lib/b.matchingFiles': 'b|lib/c.txt\nb|lib/d.txt', }, - packageGraph: packageGraph, ); }); test('can glob files with excludes applied', () async { - await testPhases( - [applyToRoot(globBuilder)], + await testBuilders( + [globBuilder], { 'a|lib/a/1.txt': '', 'a|lib/a/2.txt': '', @@ -897,6 +838,7 @@ targets: ''', }, outputs: {'a|lib/test.matchingFiles': 'a|lib/b/1.txt\na|lib/b/2.txt'}, + testingBuilderConfig: false, ); }); From a01e821b345cc3e87188d60155b174f4e36bd7dd Mon Sep 17 00:00:00 2001 From: David Morgan Date: Tue, 15 Jul 2025 13:13:52 +0200 Subject: [PATCH 12/18] Fix merge. --- .../test/generate/build_test.dart | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/build_runner_core/test/generate/build_test.dart b/build_runner_core/test/generate/build_test.dart index 66c2fad81..e579ec401 100644 --- a/build_runner_core/test/generate/build_test.dart +++ b/build_runner_core/test/generate/build_test.dart @@ -194,12 +194,14 @@ void main() { }); test('with placeholder as input', () async { - final builder1 = PlaceholderBuilder({ - 'lib.txt': 'libText', - }, inputExtension: r'$lib$'); - final builder2 = PlaceholderBuilder({ - 'root.txt': 'rootText', - }, inputExtension: 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], {}, @@ -843,8 +845,14 @@ targets: }); test('can build on files outside the hardcoded sources', () async { - await testPhases( - [applyToRoot(TestBuilder())], + await testBuilders( + [ + TestBuilder( + buildExtensions: { + '.txt': ['.txt.copy'], + }, + ), + ], { 'a|test_files/a.txt': 'a', 'a|build.yaml': ''' From 218b2de3ef70f04a4599bb8113364f9da7f195ca Mon Sep 17 00:00:00 2001 From: David Morgan Date: Tue, 15 Jul 2025 17:56:19 +0200 Subject: [PATCH 13/18] Bug fix. --- build_runner_core/test/generate/build_test.dart | 16 ++++++---------- build_test/CHANGELOG.md | 2 ++ build_test/lib/src/test_builder.dart | 6 +++++- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/build_runner_core/test/generate/build_test.dart b/build_runner_core/test/generate/build_test.dart index e579ec401..00e0601e7 100644 --- a/build_runner_core/test/generate/build_test.dart +++ b/build_runner_core/test/generate/build_test.dart @@ -867,16 +867,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, ); }); diff --git a/build_test/CHANGELOG.md b/build_test/CHANGELOG.md index 2627cd277..ed6ab78b0 100644 --- a/build_test/CHANGELOG.md +++ b/build_test/CHANGELOG.md @@ -21,6 +21,8 @@ `resolveSource` call outside a test. - Bug fix: in `testBuilder`, if no sources are passed, still treat `rootPackage` as an input package. +- Bug fix: in `testBuilder`, if sources are passed under `.dart_tool`, don't mark + them as inputs. - Remove unused deps: `async`, `convert`. - Remove unused dev_deps: `collection`. diff --git a/build_test/lib/src/test_builder.dart b/build_test/lib/src/test_builder.dart index f995984b1..b7f02d14a 100644 --- a/build_test/lib/src/test_builder.dart +++ b/build_test/lib/src/test_builder.dart @@ -394,7 +394,11 @@ Future testBuilderFactories( 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)), ], }, From bb20627e84d74f9880b5b6b9859147de3f2bb6bc Mon Sep 17 00:00:00 2001 From: David Morgan Date: Tue, 15 Jul 2025 17:57:08 +0200 Subject: [PATCH 14/18] More. --- build_runner_core/test/generate/build_test.dart | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/build_runner_core/test/generate/build_test.dart b/build_runner_core/test/generate/build_test.dart index 00e0601e7..e344c9b97 100644 --- a/build_runner_core/test/generate/build_test.dart +++ b/build_runner_core/test/generate/build_test.dart @@ -880,18 +880,14 @@ targets: 'Overdeclared outputs are not treated as inputs to later steps', () async { var builders = [ - applyToRoot( - TestBuilder( - buildExtensions: appendExtension('.unexpected'), - build: (_, _) {}, - ), - ), - applyToRoot( - TestBuilder(buildExtensions: appendExtension('.expected')), + TestBuilder( + buildExtensions: appendExtension('.unexpected'), + build: (_, _) {}, ), - applyToRoot(TestBuilder()), + TestBuilder(buildExtensions: appendExtension('.expected')), + TestBuilder(), ]; - await testPhases( + await testBuilders( builders, {'a|lib/a.txt': 'a'}, outputs: { From 15709feef45de44ec993d7777b75087d26808493 Mon Sep 17 00:00:00 2001 From: David Morgan Date: Tue, 15 Jul 2025 17:59:26 +0200 Subject: [PATCH 15/18] More. --- .../test/generate/build_test.dart | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/build_runner_core/test/generate/build_test.dart b/build_runner_core/test/generate/build_test.dart index e344c9b97..9ffebf083 100644 --- a/build_runner_core/test/generate/build_test.dart +++ b/build_runner_core/test/generate/build_test.dart @@ -1209,25 +1209,15 @@ targets: test( "builders reading their output don't cause self-referential nodes", () async { - final result = await testPhases( + final result = await testBuilders( [ - apply( - '', - [ - (_) { - return TestBuilder( - build: (step, _) async { - final output = step.inputId.addExtension('.out'); - await step.writeAsString(output, 'a'); - await step.readAsString(output); - }, - buildExtensions: appendExtension('.out', from: '.txt'), - ); - }, - ], - toRoot(), - isOptional: false, - hideOutput: false, + TestBuilder( + build: (step, _) async { + final output = step.inputId.addExtension('.out'); + await step.writeAsString(output, 'a'); + await step.readAsString(output); + }, + buildExtensions: appendExtension('.out', from: '.txt'), ), ], {'a|lib/a.txt': 'a'}, From 9410a9d51481c64eea0953bd2b9b1a0fbdfcfece Mon Sep 17 00:00:00 2001 From: David Morgan Date: Tue, 15 Jul 2025 18:07:06 +0200 Subject: [PATCH 16/18] More. --- .../test/generate/build_test.dart | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/build_runner_core/test/generate/build_test.dart b/build_runner_core/test/generate/build_test.dart index 9ffebf083..07d27b3f9 100644 --- a/build_runner_core/test/generate/build_test.dart +++ b/build_runner_core/test/generate/build_test.dart @@ -1244,17 +1244,19 @@ targets: 'a|lib/b.txt.copy': 'b', }; // First run, nothing special. - final result = await testPhases( - [copyABuilderApplication], + var result = await testBuilders( + [TestBuilder()], inputs, outputs: outputs, ); - // Second run, should have no outputs. - await testPhases( - [copyABuilderApplication], - inputs, - outputs: {}, - resumeFrom: result, + // Second run, only output should be the asset graph. + for (final id in result.readerWriter.testing.assets) { + inputs[id.toString()] = await result.readerWriter.readAsString(id); + } + result = await testBuilders([TestBuilder()], inputs); + expect( + result.readerWriter.testing.assetsWritten.single.toString(), + contains('asset_graph.json'), ); }, ); @@ -1266,8 +1268,8 @@ targets: 'a|lib/b.txt.copy': 'b', }; // First run, nothing special. - final result = await testPhases( - [copyABuilderApplication], + final result = await testBuilders( + [TestBuilder()], inputs, outputs: outputs, ); @@ -1277,16 +1279,12 @@ targets: await result.readerWriter.delete(outputId); // Second run, should have no extra outputs. - var done = testPhases( - [copyABuilderApplication], + await testBuilders( + [TestBuilder()], inputs, outputs: outputs, - resumeFrom: result, + readerWriter: result.readerWriter, ); - // Should block on user input. - await Future.delayed(const Duration(seconds: 1)); - // Now it should complete! - await done; }); group('incremental builds with cached graph', () { From e368f1cf2a3d1762ffe0aa9114b82551286f66d9 Mon Sep 17 00:00:00 2001 From: David Morgan Date: Tue, 15 Jul 2025 18:31:07 +0200 Subject: [PATCH 17/18] Refactor. --- .../lib/src/asset_graph/graph_loader.dart | 1 + .../lib/src/generate/build_phases.dart | 1 + .../test/generate/build_test.dart | 60 ------------------- .../asset_input_invalidation_test.dart | 35 +++++++++++ 4 files changed, 37 insertions(+), 60 deletions(-) diff --git a/build_runner_core/lib/src/asset_graph/graph_loader.dart b/build_runner_core/lib/src/asset_graph/graph_loader.dart index 0a4b31943..a072f7266 100644 --- a/build_runner_core/lib/src/asset_graph/graph_loader.dart +++ b/build_runner_core/lib/src/asset_graph/graph_loader.dart @@ -74,6 +74,7 @@ class AssetGraphLoader { final enabledExperimentsChanged = cachedGraph.enabledExperiments != enabledExperiments.build(); if (buildPhasesChanged || pkgVersionsChanged || enabledExperimentsChanged) { + buildLog.debug('${buildPhases.digest} ${cachedGraph.buildPhasesDigest}'); buildLog.fullBuildBecause(FullBuildReason.incompatibleBuild); await Future.wait([ writer.delete(assetGraphId), diff --git a/build_runner_core/lib/src/generate/build_phases.dart b/build_runner_core/lib/src/generate/build_phases.dart index c423bc874..3308c8614 100644 --- a/build_runner_core/lib/src/generate/build_phases.dart +++ b/build_runner_core/lib/src/generate/build_phases.dart @@ -64,6 +64,7 @@ class BuildPhases { inBuildPhases.length + (postBuildPhase.builderActions.isEmpty ? 0 : 1); static Digest _computeDigest(Iterable phases) { + buildLog.debug(phases.toString()); final digestSink = AccumulatorSink(); md5.startChunkedConversion(digestSink) ..add(phases.map((phase) => phase.identity).toList()) diff --git a/build_runner_core/test/generate/build_test.dart b/build_runner_core/test/generate/build_test.dart index 07d27b3f9..1b73139f7 100644 --- a/build_runner_core/test/generate/build_test.dart +++ b/build_runner_core/test/generate/build_test.dart @@ -1288,66 +1288,6 @@ targets: }); group('incremental builds with cached graph', () { - // Using `resumeFrom: result` to pass the filesystem between `testBuilders` - // calls causes the serialized graph from the previous build to be loaded, - // exactly as in real builds. - - test('one new asset, one modified asset, one unchanged asset', () async { - var builders = [copyABuilderApplication]; - - // Initial build. - final result = await testPhases( - builders, - {'a|web/a.txt': 'a', 'a|lib/b.txt': 'b'}, - outputs: {'a|web/a.txt.copy': 'a', 'a|lib/b.txt.copy': 'b'}, - ); - - // Followup build with modified inputs. - await testPhases( - builders, - { - 'a|web/a.txt': 'a2', - 'a|web/a.txt.copy': 'a', - 'a|lib/b.txt': 'b', - 'a|lib/b.txt.copy': 'b', - 'a|lib/c.txt': 'c', - }, - outputs: {'a|web/a.txt.copy': 'a2', 'a|lib/c.txt.copy': 'c'}, - resumeFrom: result, - ); - }); - - test( - 'deleting only the second output of a builder causes it to rerun', - () async { - var builders = [ - applyToRoot( - TestBuilder( - buildExtensions: { - '.txt': ['.txt.1', '.txt.2'], - }, - ), - ), - ]; - - // Initial build. - final result = await testPhases( - builders, - {'a|lib/a.txt': 'a'}, - outputs: {'a|lib/a.txt.1': 'a', 'a|lib/a.txt.2': 'a'}, - ); - - // Followup build with the 2nd output missing. - result.readerWriter.testing.delete(AssetId('a', 'lib/a.txt.2')); - await testPhases( - builders, - {'a|lib/a.txt': 'a', 'a|lib/a.txt.1': 'a'}, - outputs: {'a|lib/a.txt.1': 'a', 'a|lib/a.txt.2': 'a'}, - resumeFrom: result, - ); - }, - ); - group('reportUnusedAssets', () { test('removes input dependencies', () async { final builder = TestBuilder( diff --git a/build_runner_core/test/invalidation/asset_input_invalidation_test.dart b/build_runner_core/test/invalidation/asset_input_invalidation_test.dart index f8ea0a2ac..c1dd94d42 100644 --- a/build_runner_core/test/invalidation/asset_input_invalidation_test.dart +++ b/build_runner_core/test/invalidation/asset_input_invalidation_test.dart @@ -57,6 +57,41 @@ void main() { }); }); + group('a.1+(y, z) <-- a.2', () { + setUp(() { + tester.sources(['a.1', 'y', 'z']); + tester.builder(from: '.1', to: '.2') + ..readsOther('y') + ..readsOther('z') + ..writes('.2'); + }); + + test('a.2 is built', () async { + expect(await tester.build(), Result(written: ['a.2'])); + }); + + test('change y, a.2 is rebuilt', () async { + await tester.build(); + expect(await tester.build(change: 'y'), Result(written: ['a.2'])); + }); + + test('change z, a.2 is rebuilt', () async { + await tester.build(); + expect(await tester.build(change: 'z'), Result(written: ['a.2'])); + }); + + test('delete z, a.2 is rebuilt', () async { + await tester.build(); + expect(await tester.build(delete: 'z'), Result(written: ['a.2'])); + }); + + test('create z, a.2 is rebuilt', () async { + tester.sources(['a.1']); + expect(await tester.build(), Result(written: ['a.2'])); + expect(await tester.build(create: 'z'), Result(written: ['a.2'])); + }); + }); + group('a.1 <== a.2, b.3+(a.2) <== b.4', () { setUp(() { tester.sources(['a.1', 'b.3']); From 2468841abccf54c16bb7b53777bc9a9d57ec009f Mon Sep 17 00:00:00 2001 From: David Morgan Date: Tue, 15 Jul 2025 18:34:02 +0200 Subject: [PATCH 18/18] More. --- .../test/generate/build_test.dart | 48 +++++++++---------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/build_runner_core/test/generate/build_test.dart b/build_runner_core/test/generate/build_test.dart index 1b73139f7..39147384f 100644 --- a/build_runner_core/test/generate/build_test.dart +++ b/build_runner_core/test/generate/build_test.dart @@ -1688,24 +1688,22 @@ targets: test('the entrypoint cannot be read by a builder', () async { var builders = [ - applyToRoot( - TestBuilder( - buildExtensions: replaceExtension('.txt', '.hasEntrypoint'), - build: (buildStep, _) async { - var hasEntrypoint = await buildStep - .findAssets(Glob('**')) - .contains( - makeAssetId('a|.dart_tool/build/entrypoint/build.dart'), - ); - await buildStep.writeAsString( - buildStep.inputId.changeExtension('.hasEntrypoint'), - '$hasEntrypoint', - ); - }, - ), + TestBuilder( + buildExtensions: replaceExtension('.txt', '.hasEntrypoint'), + build: (buildStep, _) async { + var hasEntrypoint = await buildStep + .findAssets(Glob('**')) + .contains( + makeAssetId('a|.dart_tool/build/entrypoint/build.dart'), + ); + await buildStep.writeAsString( + buildStep.inputId.changeExtension('.hasEntrypoint'), + '$hasEntrypoint', + ); + }, ), ]; - await testPhases( + await testBuilders( builders, { 'a|lib/a.txt': 'a', @@ -1820,18 +1818,16 @@ targets: test('can have assets ending in a dot', () async { var builders = [ - applyToRoot( - TestBuilder( - buildExtensions: { - '': ['copy'], - }, - build: (step, _) async { - await step.writeAsString(step.allowedOutputs.single, 'out'); - }, - ), + TestBuilder( + buildExtensions: { + '': ['copy'], + }, + build: (step, _) async { + await step.writeAsString(step.allowedOutputs.single, 'out'); + }, ), ]; - await testPhases( + await testBuilders( builders, {'a|lib/a.': 'a'}, outputs: {'a|lib/a.copy': 'out'},