build_system_test.dart 24.3 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 15
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';

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

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

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

36
    /// Create various test targets.
37
    fooTarget = TestTarget((Environment environment) async {
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
      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>[];
53
    barTarget = TestTarget((Environment environment) async {
54 55 56 57 58 59 60 61 62 63 64 65 66 67
      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>[];
68
    fizzTarget = TestTarget((Environment environment) async {
69 70 71 72 73 74 75 76 77 78
      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];
79
    sharedTarget = TestTarget((Environment environment) async {
80 81 82 83 84 85
      shared += 1;
    })
      ..name = 'shared'
      ..inputs = const <Source>[
        Source.pattern('{PROJECT_DIR}/foo.dart'),
      ];
86
    final Artifacts artifacts = Artifacts.test();
87 88
    environment = Environment.test(
      fileSystem.currentDirectory,
89
      artifacts: artifacts,
90 91 92
      processManager: FakeProcessManager.any(),
      fileSystem: fileSystem,
      logger: BufferLogger.test(),
93
    );
94 95 96 97
    fileSystem.file('foo.dart')
      ..createSync(recursive: true)
      ..writeAsStringSync('');
    fileSystem.file('pubspec.yaml').createSync();
98 99
  });

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

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

107
    expect(buildResult.hasException, false);
108
  });
109

110 111 112 113 114
  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.
115
    final Target badTarget = TestTarget((Environment environment) async {})
116 117 118 119
      ..inputs = const <Source>[
        Source.pattern('{PROJECT_DIR}/foo.dart'),
      ]
      ..outputs = const <Source>[
120
        Source.pattern('{BUILD_DIR}/out'),
121 122
      ];
    final BuildResult result = await buildSystem.build(badTarget, environment);
123

124
    expect(result.hasException, false);
125
  });
126

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

    expect(stampFile, exists);
134

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

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

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

145 146 147 148 149 150
    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);
151

152 153
    await buildSystem.build(fooTarget, environment);
    await buildSystem.build(fooTarget, environment);
154

155
    expect(fooInvocations, 1);
156
  });
157

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

162
    fileSystem.file('foo.dart').writeAsStringSync('new contents');
163

164
    await buildSystem.build(fooTarget, environment);
165

166
    expect(fooInvocations, 2);
167
  });
168

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

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

177
    expect(fooInvocations, 1);
178
  });
179

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

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

189
    expect(fooInvocations, 1);
190
  });
191 192


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

197
    environment.buildDir.childFile('out').writeAsStringSync('Something different');
198

199
    await buildSystem.build(fooTarget, environment);
200

201
    expect(fooInvocations, 2);
202
  });
203

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

208
    await buildSystem.build(barTarget, environment);
209

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

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

221
    await buildSystem.build(barTarget, environment);
222

223
    expect(shared, 1);
224
  });
225

226 227
  testWithoutContext('Automatically cleans old outputs when build graph changes', () async {
    final BuildSystem buildSystem = setUpBuildSystem(fileSystem);
228 229 230 231 232
    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')];
233
    fileSystem.file('foo.dart').createSync();
234 235 236

    await buildSystem.build(testTarget, environment);

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

    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);

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

251 252 253
  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 {
254 255 256 257
      environment.buildDir.childFile('foo.out').createSync();
    })
      ..inputs = const <Source>[Source.pattern('{PROJECT_DIR}/foo.dart')]
      ..outputs = const <Source>[Source.pattern('{BUILD_DIR}/foo.out')];
258
    fileSystem.file('foo.dart').createSync();
259

260
    await buildSystem.build(testWithoutContextTarget, environment);
261

262
    expect(environment.buildDir.childFile('foo.out'), exists);
263 264
    environment.buildDir.childFile('foo.out').deleteSync();

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

271
    await buildSystem.build(testWithoutContextTarget2, environment);
272

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

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

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

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

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

300

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

305
    expect(result.hasException, true);
306
  });
307

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

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

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

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

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

    await buildSystem.build(target, environment);

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

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

354
    expect(called, 1);
355
  });
356

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
  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);
  });

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

    expect(environmentA.buildDir.path, isNot(environmentB.buildDir.path));
402
  });
403

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

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

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

    await buildSystem.build(target, environment);

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

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

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

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

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

476 477 478 479
  testWithoutContext('trackSharedBuildDirectory handles a missing output dir', () {
    final Environment environment = Environment.test(
      fileSystem.currentDirectory,
      outputDir: fileSystem.directory('a/b/c/d'),
480
      artifacts: Artifacts.test(),
481 482 483 484 485 486 487 488 489 490 491 492 493 494 495
      processManager: FakeProcessManager.any(),
      fileSystem: fileSystem,
      logger: BufferLogger.test(),
    );
    FlutterBuildSystem(
      fileSystem: fileSystem,
      logger: BufferLogger.test(),
      platform: FakePlatform(),
    ).trackSharedBuildDirectory(environment, fileSystem, <String, File>{});

    expect(environment.outputDir.childFile('.last_build_id'), exists);
    expect(environment.outputDir.childFile('.last_build_id').readAsStringSync(),
      '5954e2278dd01e1c4e747578776eeb94');
  });

496 497 498 499
  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));
500 501 502 503 504
    FlutterBuildSystem(
      fileSystem: fileSystem,
      logger: BufferLogger.test(),
      platform: FakePlatform(),
    ).trackSharedBuildDirectory(environment, fileSystem, <String, File>{});
505 506 507 508 509 510 511 512 513 514 515 516 517 518 519

    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();
520 521 522 523 524
    FlutterBuildSystem(
      fileSystem: fileSystem,
      logger: BufferLogger.test(),
      platform: FakePlatform(),
    ).trackSharedBuildDirectory(environment, fileSystem, <String, File>{});
525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542

    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]));
543 544 545 546 547
    FlutterBuildSystem(
      fileSystem: fileSystem,
      logger: BufferLogger.test(),
      platform: FakePlatform(),
    ).trackSharedBuildDirectory(environment, fileSystem, <String, File>{});
548 549 550 551 552 553 554 555 556 557 558 559 560 561

    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',
      },
562
      artifacts: Artifacts.test(),
563 564 565 566 567 568 569 570 571 572
      processManager: FakeProcessManager.any(),
      logger: BufferLogger.test(),
      fileSystem: fileSystem,
    );
    final Environment testEnvironmentProfle = Environment.test(
      fileSystem.currentDirectory,
      outputDir: fileSystem.directory('output'),
      defines: <String, String>{
        'config': 'profile',
      },
573
      artifacts: Artifacts.test(),
574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601
      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);
  });
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 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645

  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);
  });

646 647
}

648
BuildSystem setUpBuildSystem(FileSystem fileSystem) {
649
  return FlutterBuildSystem(
650 651 652 653 654
    fileSystem: fileSystem,
    logger: BufferLogger.test(),
    platform: FakePlatform(operatingSystem: 'linux'),
  );
}
655

656
class TestTarget extends Target {
657
  TestTarget([this._build, this._canSkip]);
658

659
  final Future<void> Function(Environment environment) _build;
660

661 662 663 664 665 666 667 668 669 670
  final bool Function(Environment environment) _canSkip;

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

671
  @override
672
  Future<void> build(Environment environment) => _build(environment);
673 674 675 676 677 678 679

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

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

680 681 682
  @override
  List<String> depfiles = <String>[];

683
  @override
684
  String name = 'test';
685 686

  @override
687
  List<Source> outputs = <Source>[];
688
}