build_system_test.dart 23.7 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6 7
import 'package:file/memory.dart';
import 'package:file_testing/file_testing.dart';
import 'package:flutter_tools/src/artifacts.dart';
8
import 'package:flutter_tools/src/base/file_system.dart';
9 10
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
11
import 'package:flutter_tools/src/base/utils.dart';
12 13 14
import 'package:flutter_tools/src/build_system/build_system.dart';
import 'package:flutter_tools/src/build_system/exceptions.dart';
import 'package:flutter_tools/src/convert.dart';
15
import 'package:mockito/mockito.dart';
16

17 18
import '../../src/common.dart';
import '../../src/context.dart';
19 20

void main() {
21
  FileSystem fileSystem;
22 23 24 25 26 27 28 29 30 31
  Environment environment;
  Target fooTarget;
  Target barTarget;
  Target fizzTarget;
  Target sharedTarget;
  int fooInvocations;
  int barInvocations;
  int shared;

  setUp(() {
32
    fileSystem = MemoryFileSystem.test();
33 34 35 36
    fooInvocations = 0;
    barInvocations = 0;
    shared = 0;

37
    /// Create various test targets.
38
    fooTarget = TestTarget((Environment environment) async {
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
      environment
        .buildDir
        .childFile('out')
        ..createSync(recursive: true)
        ..writeAsStringSync('hey');
      fooInvocations++;
    })
      ..name = 'foo'
      ..inputs = const <Source>[
        Source.pattern('{PROJECT_DIR}/foo.dart'),
      ]
      ..outputs = const <Source>[
        Source.pattern('{BUILD_DIR}/out'),
      ]
      ..dependencies = <Target>[];
54
    barTarget = TestTarget((Environment environment) async {
55 56 57 58 59 60 61 62 63 64 65 66 67 68
      environment.buildDir
        .childFile('bar')
        ..createSync(recursive: true)
        ..writeAsStringSync('there');
      barInvocations++;
    })
      ..name = 'bar'
      ..inputs = const <Source>[
        Source.pattern('{BUILD_DIR}/out'),
      ]
      ..outputs = const <Source>[
        Source.pattern('{BUILD_DIR}/bar'),
      ]
      ..dependencies = <Target>[];
69
    fizzTarget = TestTarget((Environment environment) async {
70 71 72 73 74 75 76 77 78 79
      throw Exception('something bad happens');
    })
      ..name = 'fizz'
      ..inputs = const <Source>[
        Source.pattern('{BUILD_DIR}/out'),
      ]
      ..outputs = const <Source>[
        Source.pattern('{BUILD_DIR}/fizz'),
      ]
      ..dependencies = <Target>[fooTarget];
80
    sharedTarget = TestTarget((Environment environment) async {
81 82 83 84 85 86
      shared += 1;
    })
      ..name = 'shared'
      ..inputs = const <Source>[
        Source.pattern('{PROJECT_DIR}/foo.dart'),
      ];
87 88
    final MockArtifacts artifacts = MockArtifacts();
    when(artifacts.isLocalEngine).thenReturn(false);
89 90
    environment = Environment.test(
      fileSystem.currentDirectory,
91
      artifacts: artifacts,
92 93 94
      processManager: FakeProcessManager.any(),
      fileSystem: fileSystem,
      logger: BufferLogger.test(),
95
    );
96 97 98 99
    fileSystem.file('foo.dart')
      ..createSync(recursive: true)
      ..writeAsStringSync('');
    fileSystem.file('pubspec.yaml').createSync();
100 101
  });

102 103 104
  testWithoutContext('Does not throw exception if asked to build with missing inputs', () async {
    final BuildSystem buildSystem = setUpBuildSystem(fileSystem);

105
    // Delete required input file.
106
    fileSystem.file('foo.dart').deleteSync();
107
    final BuildResult buildResult = await buildSystem.build(fooTarget, environment);
108

109
    expect(buildResult.hasException, false);
110
  });
111

112 113 114 115 116
  testWithoutContext('Does not throw exception if it does not produce a specified output', () async {
    final BuildSystem buildSystem = setUpBuildSystem(fileSystem);

    // This target is document as producing foo.dart but does not actually
    // output this value.
117
    final Target badTarget = TestTarget((Environment environment) async {})
118 119 120 121
      ..inputs = const <Source>[
        Source.pattern('{PROJECT_DIR}/foo.dart'),
      ]
      ..outputs = const <Source>[
122
        Source.pattern('{BUILD_DIR}/out'),
123 124
      ];
    final BuildResult result = await buildSystem.build(badTarget, environment);
125

126
    expect(result.hasException, false);
127
  });
128

129 130
  testWithoutContext('Saves a stamp file with inputs and outputs', () async {
    final BuildSystem buildSystem = setUpBuildSystem(fileSystem);
131
    await buildSystem.build(fooTarget, environment);
132 133 134 135
    final File stampFile = fileSystem.file(
      '${environment.buildDir.path}/foo.stamp');

    expect(stampFile, exists);
136

137 138
    final Map<String, dynamic> stampContents = castStringKeyedMap(
      json.decode(stampFile.readAsStringSync()));
139

140 141
    expect(stampContents, containsPair('inputs', <Object>['/foo.dart']));
  });
142

143 144
  testWithoutContext('Creates a BuildResult with inputs and outputs', () async {
    final BuildSystem buildSystem = setUpBuildSystem(fileSystem);
145 146
    final BuildResult result = await buildSystem.build(fooTarget, environment);

147 148 149 150 151 152
    expect(result.inputFiles.single.path, '/foo.dart');
    expect(result.outputFiles.single.path, '${environment.buildDir.path}/out');
  });

  testWithoutContext('Does not re-invoke build if stamp is valid', () async {
    final BuildSystem buildSystem = setUpBuildSystem(fileSystem);
153

154 155
    await buildSystem.build(fooTarget, environment);
    await buildSystem.build(fooTarget, environment);
156

157
    expect(fooInvocations, 1);
158
  });
159

160 161
  testWithoutContext('Re-invoke build if input is modified', () async {
    final BuildSystem buildSystem = setUpBuildSystem(fileSystem);
162
    await buildSystem.build(fooTarget, environment);
163

164
    fileSystem.file('foo.dart').writeAsStringSync('new contents');
165

166
    await buildSystem.build(fooTarget, environment);
167

168
    expect(fooInvocations, 2);
169
  });
170

171 172
  testWithoutContext('does not re-invoke build if input timestamp changes', () async {
    final BuildSystem buildSystem = setUpBuildSystem(fileSystem);
173
    await buildSystem.build(fooTarget, environment);
174

175 176
    // The file was previously empty so this does not modify it.
    fileSystem.file('foo.dart').writeAsStringSync('');
177
    await buildSystem.build(fooTarget, environment);
178

179
    expect(fooInvocations, 1);
180
  });
181

182 183
  testWithoutContext('does not re-invoke build if output timestamp changes', () async {
    final BuildSystem buildSystem = setUpBuildSystem(fileSystem);
184
    await buildSystem.build(fooTarget, environment);
185

186 187
    // This is the same content that the output file previously
    // contained.
188 189
    environment.buildDir.childFile('out').writeAsStringSync('hey');
    await buildSystem.build(fooTarget, environment);
190

191
    expect(fooInvocations, 1);
192
  });
193 194


195 196
  testWithoutContext('Re-invoke build if output is modified', () async {
    final BuildSystem buildSystem = setUpBuildSystem(fileSystem);
197
    await buildSystem.build(fooTarget, environment);
198

199
    environment.buildDir.childFile('out').writeAsStringSync('Something different');
200

201
    await buildSystem.build(fooTarget, environment);
202

203
    expect(fooInvocations, 2);
204
  });
205

206 207
  testWithoutContext('Runs dependencies of targets', () async {
    final BuildSystem buildSystem = setUpBuildSystem(fileSystem);
208
    barTarget.dependencies.add(fooTarget);
209

210
    await buildSystem.build(barTarget, environment);
211

212
    expect(fileSystem.file('${environment.buildDir.path}/bar'), exists);
213 214
    expect(fooInvocations, 1);
    expect(barInvocations, 1);
215
  });
216

217 218
  testWithoutContext('Only invokes shared dependencies once', () async {
    final BuildSystem buildSystem = setUpBuildSystem(fileSystem);
219 220 221
    fooTarget.dependencies.add(sharedTarget);
    barTarget.dependencies.add(sharedTarget);
    barTarget.dependencies.add(fooTarget);
222

223
    await buildSystem.build(barTarget, environment);
224

225
    expect(shared, 1);
226
  });
227

228 229
  testWithoutContext('Automatically cleans old outputs when build graph changes', () async {
    final BuildSystem buildSystem = setUpBuildSystem(fileSystem);
230 231 232 233 234
    final TestTarget testTarget = TestTarget((Environment envionment) async {
      environment.buildDir.childFile('foo.out').createSync();
    })
      ..inputs = const <Source>[Source.pattern('{PROJECT_DIR}/foo.dart')]
      ..outputs = const <Source>[Source.pattern('{BUILD_DIR}/foo.out')];
235
    fileSystem.file('foo.dart').createSync();
236 237 238

    await buildSystem.build(testTarget, environment);

239
    expect(environment.buildDir.childFile('foo.out'), exists);
240 241 242 243 244 245 246 247 248

    final TestTarget testTarget2 = TestTarget((Environment envionment) async {
      environment.buildDir.childFile('bar.out').createSync();
    })
      ..inputs = const <Source>[Source.pattern('{PROJECT_DIR}/foo.dart')]
      ..outputs = const <Source>[Source.pattern('{BUILD_DIR}/bar.out')];

    await buildSystem.build(testTarget2, environment);

249 250 251
    expect(environment.buildDir.childFile('bar.out'), exists);
    expect(environment.buildDir.childFile('foo.out'), isNot(exists));
  });
252

253 254 255
  testWithoutContext('Does not crash when filesytem and cache are out of sync', () async {
    final BuildSystem buildSystem = setUpBuildSystem(fileSystem);
    final TestTarget testWithoutContextTarget = TestTarget((Environment environment) async {
256 257 258 259
      environment.buildDir.childFile('foo.out').createSync();
    })
      ..inputs = const <Source>[Source.pattern('{PROJECT_DIR}/foo.dart')]
      ..outputs = const <Source>[Source.pattern('{BUILD_DIR}/foo.out')];
260
    fileSystem.file('foo.dart').createSync();
261

262
    await buildSystem.build(testWithoutContextTarget, environment);
263

264
    expect(environment.buildDir.childFile('foo.out'), exists);
265 266
    environment.buildDir.childFile('foo.out').deleteSync();

267
    final TestTarget testWithoutContextTarget2 = TestTarget((Environment environment) async {
268 269 270 271 272
      environment.buildDir.childFile('bar.out').createSync();
    })
      ..inputs = const <Source>[Source.pattern('{PROJECT_DIR}/foo.dart')]
      ..outputs = const <Source>[Source.pattern('{BUILD_DIR}/bar.out')];

273
    await buildSystem.build(testWithoutContextTarget2, environment);
274

275 276 277
    expect(environment.buildDir.childFile('bar.out'), exists);
    expect(environment.buildDir.childFile('foo.out'), isNot(exists));
  });
278

279 280 281
  testWithoutContext('Reruns build if stamp is corrupted', () async {
    final BuildSystem buildSystem = setUpBuildSystem(fileSystem);
    final TestTarget testWithoutContextTarget = TestTarget((Environment envionment) async {
282 283 284 285
      environment.buildDir.childFile('foo.out').createSync();
    })
      ..inputs = const <Source>[Source.pattern('{PROJECT_DIR}/foo.dart')]
      ..outputs = const <Source>[Source.pattern('{BUILD_DIR}/foo.out')];
286 287
    fileSystem.file('foo.dart').createSync();
    await buildSystem.build(testWithoutContextTarget, environment);
288 289

    // invalid JSON
290 291
    environment.buildDir.childFile('testWithoutContext.stamp').writeAsStringSync('{X');
    await buildSystem.build(testWithoutContextTarget, environment);
292 293

    // empty file
294 295
    environment.buildDir.childFile('testWithoutContext.stamp').writeAsStringSync('');
    await buildSystem.build(testWithoutContextTarget, environment);
296 297

    // invalid format
298 299 300
    environment.buildDir.childFile('testWithoutContext.stamp').writeAsStringSync('{"inputs": 2, "outputs": 3}');
    await buildSystem.build(testWithoutContextTarget, environment);
  });
301

302

303 304
  testWithoutContext('handles a throwing build action without crashing', () async {
    final BuildSystem buildSystem = setUpBuildSystem(fileSystem);
305
    final BuildResult result = await buildSystem.build(fizzTarget, environment);
306

307
    expect(result.hasException, true);
308
  });
309

310
  testWithoutContext('Can describe itself with JSON output', () {
311
    environment.buildDir.createSync(recursive: true);
312

313 314
    expect(fooTarget.toJson(environment), <String, dynamic>{
      'inputs':  <Object>[
315
        '/foo.dart',
316 317
      ],
      'outputs': <Object>[
318
        fileSystem.path.join(environment.buildDir.path, 'out'),
319 320 321
      ],
      'dependencies': <Object>[],
      'name':  'foo',
322
      'stamp': fileSystem.path.join(environment.buildDir.path, 'foo.stamp'),
323
    });
324
  });
325

326
  testWithoutContext('Can find dependency cycles', () {
327 328
    final Target barTarget = TestTarget()..name = 'bar';
    final Target fooTarget = TestTarget()..name = 'foo';
329 330
    barTarget.dependencies.add(fooTarget);
    fooTarget.dependencies.add(barTarget);
331

Dan Field's avatar
Dan Field committed
332
    expect(() => checkCycles(barTarget), throwsA(isA<CycleException>()));
333
  });
334

335 336
  testWithoutContext('Target with depfile dependency will not run twice without invalidation', () async {
    final BuildSystem buildSystem = setUpBuildSystem(fileSystem);
337 338
    int called = 0;
    final TestTarget target = TestTarget((Environment environment) async {
339 340
      environment.buildDir
        .childFile('example.d')
341
        .writeAsStringSync('a.txt: b.txt');
342
      fileSystem.file('a.txt').writeAsStringSync('a');
343 344
      called += 1;
    })
345
      ..depfiles = <String>['example.d'];
346
    fileSystem.file('b.txt').writeAsStringSync('b');
347 348 349

    await buildSystem.build(target, environment);

350
    expect(fileSystem.file('a.txt'), exists);
351 352
    expect(called, 1);

353
    // Second build is up to date due to depfile parse.
354
    await buildSystem.build(target, environment);
355

356
    expect(called, 1);
357
  });
358

359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384
  testWithoutContext('Target with depfile dependency will not run twice without '
    'invalidation in incremental builds', () async {
    final BuildSystem buildSystem = setUpBuildSystem(fileSystem);
    int called = 0;
    final TestTarget target = TestTarget((Environment environment) async {
      environment.buildDir
        .childFile('example.d')
        .writeAsStringSync('a.txt: b.txt');
      fileSystem.file('a.txt').writeAsStringSync('a');
      called += 1;
    })
      ..depfiles = <String>['example.d'];
    fileSystem.file('b.txt').writeAsStringSync('b');

    final BuildResult result = await buildSystem
      .buildIncremental(target, environment, null);

    expect(fileSystem.file('a.txt'), exists);
    expect(called, 1);

    // Second build is up to date due to depfile parse.
    await buildSystem.buildIncremental(target, environment, result);

    expect(called, 1);
  });

385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401
  testWithoutContext('output directory is an input to the build',  () async {
    final Environment environmentA = Environment.test(
      fileSystem.currentDirectory,
      outputDir: fileSystem.directory('a'),
      artifacts: MockArtifacts(),
      processManager: FakeProcessManager.any(),
      fileSystem: fileSystem,
      logger: BufferLogger.test(),
    );
    final Environment environmentB = Environment.test(
      fileSystem.currentDirectory,
      outputDir: fileSystem.directory('b'),
      artifacts: MockArtifacts(),
      processManager: FakeProcessManager.any(),
      fileSystem: fileSystem,
      logger: BufferLogger.test(),
    );
402 403

    expect(environmentA.buildDir.path, isNot(environmentB.buildDir.path));
404
  });
405

406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430
  testWithoutContext('Additional inputs do not change the build configuration',  () async {
    final Environment environmentA = Environment.test(
      fileSystem.currentDirectory,
      artifacts: MockArtifacts(),
      processManager: FakeProcessManager.any(),
      fileSystem: fileSystem,
      logger: BufferLogger.test(),
      inputs: <String, String>{
        'C': 'D',
      }
    );
    final Environment environmentB = Environment.test(
      fileSystem.currentDirectory,
      artifacts: MockArtifacts(),
      processManager: FakeProcessManager.any(),
      fileSystem: fileSystem,
      logger: BufferLogger.test(),
      inputs: <String, String>{
        'A': 'B',
      }
    );

    expect(environmentA.buildDir.path, equals(environmentB.buildDir.path));
  });

431 432
  testWithoutContext('A target with depfile dependencies can delete stale outputs on the first run',  () async {
    final BuildSystem buildSystem = setUpBuildSystem(fileSystem);
433 434 435 436 437
    int called = 0;
    final TestTarget target = TestTarget((Environment environment) async {
      if (called == 0) {
        environment.buildDir.childFile('example.d')
          .writeAsStringSync('a.txt c.txt: b.txt');
438 439
        fileSystem.file('a.txt').writeAsStringSync('a');
        fileSystem.file('c.txt').writeAsStringSync('a');
440 441 442 443
      } else {
        // On second run, we no longer claim c.txt as an output.
        environment.buildDir.childFile('example.d')
          .writeAsStringSync('a.txt: b.txt');
444
        fileSystem.file('a.txt').writeAsStringSync('a');
445 446 447
      }
      called += 1;
    })
448
      ..depfiles = const <String>['example.d'];
449
    fileSystem.file('b.txt').writeAsStringSync('b');
450 451 452

    await buildSystem.build(target, environment);

453 454
    expect(fileSystem.file('a.txt'), exists);
    expect(fileSystem.file('c.txt'), exists);
455 456
    expect(called, 1);

457 458
    // rewrite an input to force a rerun, expect that the old c.txt is deleted.
    fileSystem.file('b.txt').writeAsStringSync('ba');
459 460
    await buildSystem.build(target, environment);

461 462
    expect(fileSystem.file('a.txt'), exists);
    expect(fileSystem.file('c.txt'), isNot(exists));
463
    expect(called, 2);
464
  });
465 466

  testWithoutContext('trackSharedBuildDirectory handles a missing .last_build_id', () {
467 468 469 470 471
    FlutterBuildSystem(
      fileSystem: fileSystem,
      logger: BufferLogger.test(),
      platform: FakePlatform(),
    ).trackSharedBuildDirectory(environment, fileSystem, <String, File>{});
472 473 474 475 476 477 478 479 480 481

    expect(environment.outputDir.childFile('.last_build_id'), exists);
    expect(environment.outputDir.childFile('.last_build_id').readAsStringSync(),
      '6666cd76f96956469e7be39d750cc7d9');
  });

  testWithoutContext('trackSharedBuildDirectory does not modify .last_build_id when config is identical', () {
    environment.outputDir.childFile('.last_build_id')
      ..writeAsStringSync('6666cd76f96956469e7be39d750cc7d9')
      ..setLastModifiedSync(DateTime(1991, 8, 23));
482 483 484 485 486
    FlutterBuildSystem(
      fileSystem: fileSystem,
      logger: BufferLogger.test(),
      platform: FakePlatform(),
    ).trackSharedBuildDirectory(environment, fileSystem, <String, File>{});
487 488 489 490 491 492 493 494 495 496 497 498 499 500 501

    expect(environment.outputDir.childFile('.last_build_id').lastModifiedSync(),
      DateTime(1991, 8, 23));
  });

  testWithoutContext('trackSharedBuildDirectory does not delete files when outputs.json is missing', () {
    environment.outputDir
      .childFile('.last_build_id')
      .writeAsStringSync('foo');
    environment.buildDir.parent
      .childDirectory('foo')
      .createSync(recursive: true);
    environment.outputDir
      .childFile('stale')
      .createSync();
502 503 504 505 506
    FlutterBuildSystem(
      fileSystem: fileSystem,
      logger: BufferLogger.test(),
      platform: FakePlatform(),
    ).trackSharedBuildDirectory(environment, fileSystem, <String, File>{});
507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524

    expect(environment.outputDir.childFile('.last_build_id').readAsStringSync(),
      '6666cd76f96956469e7be39d750cc7d9');
    expect(environment.outputDir.childFile('stale'), exists);
  });

  testWithoutContext('trackSharedBuildDirectory deletes files in outputs.json but not in current outputs', () {
    environment.outputDir
      .childFile('.last_build_id')
      .writeAsStringSync('foo');
    final Directory otherBuildDir = environment.buildDir.parent
      .childDirectory('foo')
      ..createSync(recursive: true);
    final File staleFile = environment.outputDir
      .childFile('stale')
      ..createSync();
    otherBuildDir.childFile('outputs.json')
      .writeAsStringSync(json.encode(<String>[staleFile.absolute.path]));
525 526 527 528 529
    FlutterBuildSystem(
      fileSystem: fileSystem,
      logger: BufferLogger.test(),
      platform: FakePlatform(),
    ).trackSharedBuildDirectory(environment, fileSystem, <String, File>{});
530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583

    expect(environment.outputDir.childFile('.last_build_id').readAsStringSync(),
      '6666cd76f96956469e7be39d750cc7d9');
    expect(environment.outputDir.childFile('stale'), isNot(exists));
  });

  testWithoutContext('multiple builds to the same output directory do no leave stale artifacts', () async {
    final BuildSystem buildSystem = setUpBuildSystem(fileSystem);
    final Environment testEnvironmentDebug = Environment.test(
      fileSystem.currentDirectory,
      outputDir: fileSystem.directory('output'),
      defines: <String, String>{
        'config': 'debug',
      },
      artifacts: MockArtifacts(),
      processManager: FakeProcessManager.any(),
      logger: BufferLogger.test(),
      fileSystem: fileSystem,
    );
    final Environment testEnvironmentProfle = Environment.test(
      fileSystem.currentDirectory,
      outputDir: fileSystem.directory('output'),
      defines: <String, String>{
        'config': 'profile',
      },
      artifacts: MockArtifacts(),
      processManager: FakeProcessManager.any(),
      logger: BufferLogger.test(),
      fileSystem: fileSystem,
    );

    final TestTarget debugTarget = TestTarget((Environment environment) async {
      environment.outputDir.childFile('debug').createSync();
    })..outputs = const <Source>[Source.pattern('{OUTPUT_DIR}/debug')];
    final TestTarget releaseTarget = TestTarget((Environment environment) async {
      environment.outputDir.childFile('release').createSync();
    })..outputs = const <Source>[Source.pattern('{OUTPUT_DIR}/release')];

    await buildSystem.build(debugTarget, testEnvironmentDebug);

    // Verify debug output was created
    expect(fileSystem.file('output/debug'), exists);

    await buildSystem.build(releaseTarget, testEnvironmentProfle);

    // Last build config is updated properly
    expect(testEnvironmentProfle.outputDir.childFile('.last_build_id'), exists);
    expect(testEnvironmentProfle.outputDir.childFile('.last_build_id').readAsStringSync(),
      'c20b3747fb2aa148cc4fd39bfbbd894f');

    // Verify debug output removeds
    expect(fileSystem.file('output/debug'), isNot(exists));
    expect(fileSystem.file('output/release'), exists);
  });
584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627

  testWithoutContext('A target using canSkip can create a conditional output',  () async {
    final BuildSystem buildSystem = setUpBuildSystem(fileSystem);
    final File bar = environment.buildDir.childFile('bar');
    final File foo = environment.buildDir.childFile('foo');

    // The target will write a file `foo`, but only if `bar` already exists.
    final TestTarget target = TestTarget(
      (Environment environment) async {
        foo.writeAsStringSync(bar.readAsStringSync());
        environment.buildDir
          .childFile('example.d')
          .writeAsStringSync('${foo.path}: ${bar.path}');
      },
      (Environment environment) {
        return !environment.buildDir.childFile('bar').existsSync();
      }
    )
      ..depfiles = const <String>['example.d'];

    // bar does not exist, there should be no inputs/outputs.
    final BuildResult firstResult = await buildSystem.build(target, environment);

    expect(foo, isNot(exists));
    expect(firstResult.inputFiles, isEmpty);
    expect(firstResult.outputFiles, isEmpty);

    // bar is created, the target should be able to run.
    bar.writeAsStringSync('content-1');
    final BuildResult secondResult = await buildSystem.build(target, environment);

    expect(foo, exists);
    expect(secondResult.inputFiles.map((File file) => file.path), <String>[bar.path]);
    expect(secondResult.outputFiles.map((File file) => file.path), <String>[foo.path]);

    // bar is destroyed, foo is also deleted.
    bar.deleteSync();
    final BuildResult thirdResult = await buildSystem.build(target, environment);

    expect(foo, isNot(exists));
    expect(thirdResult.inputFiles, isEmpty);
    expect(thirdResult.outputFiles, isEmpty);
  });

628 629
}

630
BuildSystem setUpBuildSystem(FileSystem fileSystem) {
631
  return FlutterBuildSystem(
632 633 634 635 636
    fileSystem: fileSystem,
    logger: BufferLogger.test(),
    platform: FakePlatform(operatingSystem: 'linux'),
  );
}
637

638
class TestTarget extends Target {
639
  TestTarget([this._build, this._canSkip]);
640

641
  final Future<void> Function(Environment environment) _build;
642

643 644 645 646 647 648 649 650 651 652
  final bool Function(Environment environment) _canSkip;

  @override
  bool canSkip(Environment environment) {
    if (_canSkip != null) {
      return _canSkip(environment);
    }
    return super.canSkip(environment);
  }

653
  @override
654
  Future<void> build(Environment environment) => _build(environment);
655 656 657 658 659 660 661

  @override
  List<Target> dependencies = <Target>[];

  @override
  List<Source> inputs = <Source>[];

662 663 664
  @override
  List<String> depfiles = <String>[];

665
  @override
666
  String name = 'test';
667 668

  @override
669
  List<Source> outputs = <Source>[];
670
}
671 672

class MockArtifacts extends Mock implements Artifacts {}