Skip to content

Commit b1e5b39

Browse files
authored
Add test for invalidation with resolved inputs. (#3993)
1 parent 17f64cb commit b1e5b39

File tree

3 files changed

+162
-6
lines changed

3 files changed

+162
-6
lines changed

build_runner_core/lib/src/generate/single_step_reader_writer.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,7 @@ class SingleStepReaderWriter extends AssetReader
456456
node = _runningBuild.assetGraph.get(id)!;
457457
}
458458
return PhasedValue.generated(
459-
atPhase: phase,
459+
atPhase: nodePhase,
460460
before: '',
461461
(node.wasOutput && node.generatedNodeState!.result == true)
462462
? await _delegate.readAsString(id)

build_runner_core/test/invalidation/invalidation_tester.dart

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ class InvalidationTester {
2121
/// The source assets on disk before the first build.
2222
final Set<AssetId> _sourceAssets = {};
2323

24+
/// Import statements.
25+
///
26+
/// If an asset has an entry here then sources and generated sources will
27+
/// start with the import statements specified.
28+
final Map<String, List<String>> _importGraph = {};
29+
2430
/// The builders that will run.
2531
final List<TestBuilder> _builders = [];
2632

@@ -43,6 +49,9 @@ class InvalidationTester {
4349
/// The build number for "printOnFailure" output.
4450
int _buildNumber = 0;
4551

52+
/// Output number, for writing outputs that are different.
53+
int _outputNumber = 0;
54+
4655
/// Sets the assets that will be on disk before the first build.
4756
///
4857
/// See the note on "names" in the class dartdoc.
@@ -53,6 +62,12 @@ class InvalidationTester {
5362
}
5463
}
5564

65+
// Sets the import graph for source files and generated files.
66+
void importGraph(Map<String, List<String>> importGraph) {
67+
_importGraph.clear();
68+
_importGraph.addAll(importGraph);
69+
}
70+
5671
/// Adds a builder to the test.
5772
///
5873
/// [from] and [to] are the input and output extension of the builder,
@@ -106,6 +121,13 @@ class InvalidationTester {
106121
_failureStrategies[name.assetId] = FailureStrategy.succeed;
107122
}
108123

124+
String _imports(AssetId id) {
125+
final imports = _importGraph[_assetIdToName(id)];
126+
return imports == null
127+
? ''
128+
: imports.map((i) => "import '${i.pathForImport}';").join('\n');
129+
}
130+
109131
/// Does a build.
110132
///
111133
/// For the initial build, do not pass [change] [delete] or [create].
@@ -120,7 +142,7 @@ class InvalidationTester {
120142
throw StateError('Do a build without change, delete or create first.');
121143
}
122144
for (final id in _sourceAssets) {
123-
assets[id] = '// initial source';
145+
assets[id] = '${_imports(id)}// initial source';
124146
}
125147
} else {
126148
// Create the new filesystem from the previous build state.
@@ -131,7 +153,8 @@ class InvalidationTester {
131153

132154
// Make the requested updates.
133155
if (change != null) {
134-
assets[change.assetId] = '${assets[change.assetId]}\n// changed';
156+
assets[change.assetId] =
157+
'${_imports(change.assetId)}}\n// ${++_outputNumber}';
135158
}
136159
if (delete != null) {
137160
if (assets.containsKey(delete.assetId)) {
@@ -148,7 +171,7 @@ class InvalidationTester {
148171
if (assets.containsKey(create.assetId)) {
149172
throw StateError('Asset $create to create already exists in: $assets');
150173
}
151-
assets[create.assetId] = '// initial source';
174+
assets[create.assetId] = '${_imports(create.assetId)}// initial source';
152175
}
153176

154177
// Build and check what changed.
@@ -259,6 +282,11 @@ class TestBuilderBuilder {
259282
_builder.otherReads.add(name);
260283
}
261284

285+
/// Test setup: the builder will parse the Dart source asset with [name].
286+
void resolvesOther(String name) {
287+
_builder.otherResolves.add(name);
288+
}
289+
262290
/// Test setup: the builder will write the asset that is [extension] applied
263291
/// to the primary input.
264292
///
@@ -287,6 +315,9 @@ class TestBuilder implements Builder {
287315
/// Names of assets that the builder will read.
288316
List<String> otherReads = [];
289317

318+
/// Names of assets that the builder will resolve.
319+
List<String> otherResolves = [];
320+
290321
/// Extensions of assets that the builder will write.
291322
///
292323
/// The extensions are applied to the primary input asset ID with
@@ -312,14 +343,21 @@ class TestBuilder implements Builder {
312343
content.add(await buildStep.readAsString(read.assetId));
313344
}
314345
}
346+
for (final resolve in otherResolves) {
347+
content.add(resolve.assetId.toString());
348+
await buildStep.resolver.libraryFor(resolve.assetId);
349+
}
315350
for (final write in writes) {
316351
final writeId = buildStep.inputId.replaceAllPathExtensions(write);
317352
final outputStrategy =
318353
_tester._outputStrategies[writeId] ?? OutputStrategy.inputDigest;
354+
final inputHash = base64.encode(
355+
md5.convert(utf8.encode(content.join('\n\n'))).bytes,
356+
);
319357
final output = switch (outputStrategy) {
320-
OutputStrategy.fixed => '',
358+
OutputStrategy.fixed => _tester._imports(writeId),
321359
OutputStrategy.inputDigest =>
322-
'// ${base64.encode(md5.convert(utf8.encode(content.join('\n\n'))).bytes)}',
360+
'${_tester._imports(writeId)}\n// $inputHash',
323361
OutputStrategy.none => null,
324362
};
325363
if (output != null) {
@@ -354,6 +392,9 @@ extension StringExtension on String {
354392
AssetId get generatedAssetId =>
355393
AssetId('pkg', '.dart_tool/build/generated/pkg/lib/$this.dart');
356394

395+
/// Maps "names" to relative import path.
396+
String get pathForImport => '$this.dart';
397+
357398
/// Displays trimmed and with two space indent.
358399
String get trimAndIndent => ' ${toString().trim().replaceAll('\n', '\n ')}';
359400
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:test/test.dart';
6+
7+
import 'invalidation_tester.dart';
8+
9+
/// In the test names:
10+
///
11+
/// - `a1 <== a2` means a1 is the primary input of a2 _and_ is an input:
12+
/// the builder _does_ read a1
13+
/// - dart source import graphs are introduced with "resolves", so
14+
/// `a1 resolves: a2 --> a3 --> a4` means that the generation of a1
15+
/// resolves a2, which imports a3, which imports a4
16+
void main() {
17+
late InvalidationTester tester;
18+
19+
setUp(() {
20+
tester = InvalidationTester();
21+
});
22+
23+
group('a.1 <== a.2, a.2 resolves: a.1 --> za --> zb', () {
24+
setUp(() {
25+
tester.sources(['a.1', 'za', 'zb']);
26+
tester.importGraph({
27+
'a.1': ['za'],
28+
'za': ['zb'],
29+
});
30+
tester.builder(from: '.1', to: '.2')
31+
..reads('.1')
32+
..resolvesOther('a.1')
33+
..writes('.2');
34+
});
35+
36+
test('a.2 is built', () async {
37+
expect(await tester.build(), Result(written: ['a.2']));
38+
});
39+
40+
test('change za, a.2 is rebuilt', () async {
41+
await tester.build();
42+
expect(await tester.build(change: 'za'), Result(written: ['a.2']));
43+
});
44+
45+
test('change zb, a.2 is rebuilt', () async {
46+
await tester.build();
47+
expect(await tester.build(change: 'zb'), Result(written: ['a.2']));
48+
});
49+
});
50+
51+
// Transitive dependencies via files generated in earlier phases.
52+
group('a.1 <== a.2, a.3 <== a.4, a.5 <== a.6, '
53+
'a.6 resolves: z -> a.4 --> a.2', () {
54+
setUp(() {
55+
tester.sources(['a.1', 'a.3', 'a.5', 'z']);
56+
tester.importGraph({
57+
'z': ['a.4'],
58+
'a.4': ['a.2'],
59+
});
60+
tester.builder(from: '.1', to: '.2')
61+
..reads('.1')
62+
..writes('.2');
63+
tester.builder(from: '.3', to: '.4')
64+
..reads('.3')
65+
..writes('.4');
66+
tester.builder(from: '.5', to: '.6')
67+
..reads('.5')
68+
..resolvesOther('a.4')
69+
..writes('.6');
70+
});
71+
72+
test('a.6 is built', () async {
73+
expect(await tester.build(), Result(written: ['a.2', 'a.4', 'a.6']));
74+
});
75+
76+
test('change a.1, a.6 is rebuilt', () async {
77+
expect(await tester.build(), Result(written: ['a.2', 'a.4', 'a.6']));
78+
expect(
79+
await tester.build(change: 'a.1'),
80+
Result(written: ['a.2', 'a.6']),
81+
);
82+
});
83+
});
84+
85+
// As previous group, but with builders in reverse order so the transitive
86+
// import is not available and is not an input.
87+
group('a.5 <== a.6, a.3 <== a.4, a.1 <== a.2, a.6 resolves: z -> a.4', () {
88+
setUp(() {
89+
tester.sources(['a.1', 'a.3', 'a.5', 'z']);
90+
tester.importGraph({
91+
'z': ['a.4'],
92+
'a.4': ['a.2'],
93+
});
94+
tester.builder(from: '.5', to: '.6')
95+
..reads('.5')
96+
..resolvesOther('z')
97+
..writes('.6');
98+
tester.builder(from: '.3', to: '.4')
99+
..reads('.3')
100+
..writes('.4');
101+
tester.builder(from: '.1', to: '.2')
102+
..reads('.1')
103+
..writes('.2');
104+
});
105+
106+
test('a.6 is built', () async {
107+
expect(await tester.build(), Result(written: ['a.2', 'a.4', 'a.6']));
108+
});
109+
110+
test('change a.1, a.6 is not rebuilt', () async {
111+
expect(await tester.build(), Result(written: ['a.2', 'a.4', 'a.6']));
112+
expect(await tester.build(change: 'a.1'), Result(written: ['a.2']));
113+
});
114+
});
115+
}

0 commit comments

Comments
 (0)