Skip to content

Commit 0a20e64

Browse files
authored
Add invalidation test coverage for failing generators. (#3986)
1 parent 49821e9 commit 0a20e64

File tree

2 files changed

+111
-11
lines changed

2 files changed

+111
-11
lines changed

build_runner_core/test/invalidation/invalidation_tester.dart

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,12 @@ class InvalidationTester {
2727
/// The [OutputStrategy] for generated assets.
2828
///
2929
/// [OutputStrategy.inputDigest] is the default if there is no entry.
30-
final Map<AssetId, OutputStrategy> _outputs = {};
30+
final Map<AssetId, OutputStrategy> _outputStrategies = {};
31+
32+
/// The [FailureStrategy] for generated assets.
33+
///
34+
/// [FailureStrategy.succeed] is the default if there is no entry.
35+
final Map<AssetId, FailureStrategy> _failureStrategies = {};
3136

3237
/// Assets written by generators.
3338
final Set<AssetId> _generatedOutputsWritten = {};
@@ -73,14 +78,32 @@ class InvalidationTester {
7378
/// By default, output will be a digest of all read files. This changes it to
7479
/// fixed: it won't change when inputs change.
7580
void fixOutput(String name) {
76-
_outputs[name.assetId] = OutputStrategy.fixed;
81+
_outputStrategies[name.assetId] = OutputStrategy.fixed;
82+
}
83+
84+
/// Sets the output strategy for [name] back to the default,
85+
/// [OutputStrategy.inputDigest].
86+
void digestOutput(String name) {
87+
_outputStrategies[name.assetId] = OutputStrategy.inputDigest;
7788
}
7889

7990
/// Sets the output strategy for [name] to [OutputStrategy.none].
8091
///
8192
/// The generator will not output the file.
8293
void skipOutput(String name) {
83-
_outputs[name.assetId] = OutputStrategy.none;
94+
_outputStrategies[name.assetId] = OutputStrategy.none;
95+
}
96+
97+
/// Sets the failure strategy for [name] to [FailureStrategy.fail].
98+
///
99+
/// The generator will write any outputs it is configured to write, then fail.
100+
void fail(String name) {
101+
_failureStrategies[name.assetId] = FailureStrategy.fail;
102+
}
103+
104+
/// Sets the failure strategy for [name] to [FailureStrategy.succeed].
105+
void succeed(String name) {
106+
_failureStrategies[name.assetId] = FailureStrategy.succeed;
84107
}
85108

86109
/// Does a build.
@@ -140,9 +163,10 @@ class InvalidationTester {
140163
optionalBuilders: _builders.where((b) => b.isOptional).toSet(),
141164
testingBuilderConfig: false,
142165
);
166+
final logString = log.toString();
143167
printOnFailure(
144168
'=== build log #${++_buildNumber} ===\n\n'
145-
'${log.toString().trimAndIndent}',
169+
'${logString.trimAndIndent}',
146170
);
147171
_readerWriter = testBuildResult.readerWriter;
148172

@@ -152,7 +176,9 @@ class InvalidationTester {
152176
);
153177
final deleted = deletedAssets.map(_assetIdToName);
154178

155-
return Result(written: written, deleted: deleted);
179+
return logString.contains('Succeeded after')
180+
? Result(written: written, deleted: deleted)
181+
: Result.failure(written: written, deleted: deleted);
156182
}
157183
}
158184

@@ -168,6 +194,12 @@ enum OutputStrategy {
168194
inputDigest,
169195
}
170196

197+
/// Whether a generator succeeds or fails.
198+
///
199+
/// Writing files is independent from success or failure: if a generator is
200+
/// configured to write files, it does so before failing.
201+
enum FailureStrategy { fail, succeed }
202+
171203
/// The changes on disk caused by the build.
172204
class Result {
173205
/// The "names" of the assets that were written.
@@ -176,22 +208,34 @@ class Result {
176208
/// The "names" of the assets that were deleted.
177209
BuiltSet<String> deleted;
178210

211+
/// Whether the build succeeded.
212+
bool succeeded;
213+
179214
Result({Iterable<String>? written, Iterable<String>? deleted})
180215
: written = (written ?? {}).toBuiltSet(),
181-
deleted = (deleted ?? {}).toBuiltSet();
216+
deleted = (deleted ?? {}).toBuiltSet(),
217+
succeeded = true;
218+
219+
Result.failure({Iterable<String>? written, Iterable<String>? deleted})
220+
: written = (written ?? {}).toBuiltSet(),
221+
deleted = (deleted ?? {}).toBuiltSet(),
222+
succeeded = false;
182223

183224
@override
184225
bool operator ==(Object other) {
185226
if (other is! Result) return false;
186-
return written == other.written && deleted == other.deleted;
227+
return succeeded == other.succeeded &&
228+
written == other.written &&
229+
deleted == other.deleted;
187230
}
188231

189232
@override
190-
int get hashCode => Object.hash(written, deleted);
233+
int get hashCode => Object.hash(succeeded, written, deleted);
191234

192235
@override
193236
String toString() => [
194237
'Result(',
238+
if (!succeeded) 'failed',
195239
if (written.isNotEmpty) 'written: ${written.join(', ')}',
196240
if (deleted.isNotEmpty) 'deleted: ${deleted.join(', ')}',
197241
')',
@@ -249,7 +293,7 @@ class TestBuilder implements Builder {
249293
for (final write in writes) {
250294
final writeId = buildStep.inputId.replaceAllPathExtensions(write);
251295
final outputStrategy =
252-
_tester._outputs[writeId] ?? OutputStrategy.inputDigest;
296+
_tester._outputStrategies[writeId] ?? OutputStrategy.inputDigest;
253297
final output = switch (outputStrategy) {
254298
OutputStrategy.fixed => '',
255299
OutputStrategy.inputDigest =>
@@ -261,14 +305,20 @@ class TestBuilder implements Builder {
261305
_tester._generatedOutputsWritten.add(writeId);
262306
}
263307
}
308+
for (final write in writes) {
309+
final writeId = buildStep.inputId.replaceAllPathExtensions(write);
310+
if (_tester._failureStrategies[writeId] == FailureStrategy.fail) {
311+
throw StateError('Failing as requested by test setup.');
312+
}
313+
}
264314
}
265315
}
266316

267317
extension LogRecordExtension on LogRecord {
268-
/// Displays [message] with error and stack trace if present.
318+
/// Displays [toString] plus error and stack trace if present.
269319
String get display {
270320
if (error == null && stackTrace == null) return message;
271-
return '$message\n$error\n$stackTrace';
321+
return '${toString()}\n$error\n$stackTrace';
272322
}
273323
}
274324

build_runner_core/test/invalidation/primary_input_invalidation_test.dart

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ void main() {
3232
expect(await tester.build(), Result(written: ['a.1']));
3333
});
3434

35+
test('a.1 can be output by failing generator', () async {
36+
tester.fail('a.1');
37+
expect(await tester.build(), Result.failure(written: ['a.1']));
38+
});
39+
3540
test('change a, nothing is rebuilt', () async {
3641
await tester.build();
3742
expect(await tester.build(change: 'a'), Result());
@@ -79,6 +84,19 @@ void main() {
7984
await tester.build();
8085
expect(await tester.build(change: 'a'), Result());
8186
});
87+
88+
test('change a, failed a.1 is not built', () async {
89+
// "a" is not an input of "a.1", so changing "a" does not retry the
90+
// failed "a.1" build: it would fail exactly the same.
91+
tester
92+
..fail('a.1')
93+
..skipOutput('a.1');
94+
await tester.build();
95+
tester
96+
..succeed('a.1')
97+
..digestOutput('a.1');
98+
expect(await tester.build(change: 'a'), Result());
99+
});
82100
});
83101

84102
group('a <-- a.1 <-- a.2', () {
@@ -92,6 +110,14 @@ void main() {
92110
expect(await tester.build(), Result());
93111
});
94112

113+
test('a.1 is output but fails, a.2 is not built', () async {
114+
tester
115+
..fail('a.1')
116+
..skipOutput('a.1');
117+
118+
expect(await tester.build(), Result.failure());
119+
});
120+
95121
test('a.1+a.2 are built', () async {
96122
expect(await tester.build(), Result(written: ['a.1', 'a.2']));
97123
});
@@ -130,6 +156,17 @@ void main() {
130156
expect(await tester.build(change: 'a'), Result(written: ['a.1']));
131157
});
132158

159+
test('a.1 is built after fix, a.2 is built', () async {
160+
tester
161+
..fail('a.1')
162+
..skipOutput('a.1');
163+
expect(await tester.build(), Result.failure());
164+
tester
165+
..succeed('a.1')
166+
..digestOutput('a.1');
167+
expect(await tester.build(change: 'a'), Result(written: ['a.1', 'a.2']));
168+
});
169+
133170
test('change a, on rebuild a.1 is not output, a.2 is deleted', () async {
134171
expect(await tester.build(), Result(written: ['a.1', 'a.2']));
135172
tester.skipOutput('a.1');
@@ -297,6 +334,19 @@ void main() {
297334
expect(await tester.build(change: 'a'), Result(written: ['a.1', 'a.2']));
298335
});
299336

337+
test(
338+
'change a to recover from failure, a.1+a.2+a.3+a.4 are built',
339+
() async {
340+
tester.fail('a.1');
341+
expect(await tester.build(), Result.failure(written: ['a.1']));
342+
tester.succeed('a.1');
343+
expect(
344+
await tester.build(change: 'a'),
345+
Result(written: ['a.1', 'a.2', 'a.3', 'a.4']),
346+
);
347+
},
348+
);
349+
300350
test('change a, on rebuild a.2 is not output', () async {
301351
expect(
302352
await tester.build(),

0 commit comments

Comments
 (0)