diff --git a/build_runner/pubspec.yaml b/build_runner/pubspec.yaml index c29f0853fa..e3bbb25dd3 100644 --- a/build_runner/pubspec.yaml +++ b/build_runner/pubspec.yaml @@ -53,7 +53,6 @@ dev_dependencies: stream_channel: ^2.0.0 test: ^1.25.5 test_descriptor: ^2.0.0 - test_process: ^2.0.0 topics: - build-runner diff --git a/build_runner/test/commands/watch/watcher_test.dart b/build_runner/test/commands/watch/watcher_test.dart deleted file mode 100644 index cdd1d45495..0000000000 --- a/build_runner/test/commands/watch/watcher_test.dart +++ /dev/null @@ -1,1206 +0,0 @@ -// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:async/async.dart'; -import 'package:build/build.dart'; -import 'package:build_config/build_config.dart'; -import 'package:build_runner/src/build/asset_graph/graph.dart'; -import 'package:build_runner/src/build/asset_graph/node.dart'; -import 'package:build_runner/src/build/build_result.dart'; -import 'package:build_runner/src/build_plan/apply_builders.dart'; -import 'package:build_runner/src/build_plan/build_options.dart'; -import 'package:build_runner/src/build_plan/build_phases.dart'; -import 'package:build_runner/src/build_plan/builder_application.dart'; -import 'package:build_runner/src/build_plan/builder_factories.dart'; -import 'package:build_runner/src/build_plan/package_graph.dart'; -import 'package:build_runner/src/build_plan/testing_overrides.dart'; -import 'package:build_runner/src/commands/watch_command.dart'; -import 'package:build_runner/src/constants.dart'; -import 'package:built_collection/built_collection.dart'; -import 'package:logging/logging.dart'; -import 'package:path/path.dart' as path; -import 'package:test/test.dart'; -import 'package:watcher/watcher.dart'; - -import '../../common/common.dart'; - -void main() { - /// Basic phases/phase groups which get used in many tests - final copyABuildApplication = applyToRoot( - TestBuilder(buildExtensions: appendExtension('.copy', from: '.txt')), - ); - final packageConfigId = makeAssetId('a|.dart_tool/package_config.json'); - final packageGraph = buildPackageGraph({ - rootPackage('a', path: path.absolute('a')): [], - }); - late InternalTestReaderWriter readerWriter; - - setUp(() async { - readerWriter = InternalTestReaderWriter( - rootPackage: packageGraph.root.name, - ); - await readerWriter.writeAsString( - packageConfigId, - jsonEncode(_packageConfig), - ); - }); - - group('watch', () { - setUp(() { - _terminateWatchController = StreamController(); - }); - - tearDown(() { - FakeWatcher.watchers.clear(); - return terminateWatch(); - }); - - group('simple', () { - test('rebuilds once on file updates', () async { - final buildState = await startWatch( - [copyABuildApplication], - {'a|web/a.txt': 'a'}, - readerWriter, - packageGraph: packageGraph, - ); - final results = StreamQueue(buildState.buildResults); - - var result = await results.next; - checkBuild( - result, - outputs: {'a|web/a.txt.copy': 'a'}, - readerWriter: readerWriter, - ); - - await readerWriter.writeAsString(makeAssetId('a|web/a.txt'), 'b'); - - result = await results.next; - checkBuild( - result, - outputs: {'a|web/a.txt.copy': 'b'}, - readerWriter: readerWriter, - ); - - // Wait for the `_debounceDelay` before terminating. - await Future.delayed(_debounceDelay); - - await terminateWatch(); - expect(await results.hasNext, isFalse); - }); - - test('emits a warning when no builders are specified', () async { - final logs = []; - final buildState = await startWatch( - [], - {'a|web/a.txt.copy': 'a'}, - readerWriter, - packageGraph: packageGraph, - onLog: (record) { - if (record.level == Level.WARNING) logs.add(record); - }, - ); - final result = await buildState.buildResults.first; - expect(result.status, BuildStatus.success); - expect( - logs, - contains( - predicate( - (LogRecord record) => - record.message.contains('Nothing to build.'), - ), - ), - ); - }); - - test('rebuilds on file updates outside hardcoded sources', () async { - final buildState = await startWatch( - [copyABuildApplication], - { - 'a|test_files/a.txt': 'a', - 'a|build.yaml': ''' -targets: - a: - sources: - - test_files/** -''', - }, - readerWriter, - packageGraph: packageGraph, - ); - final results = StreamQueue(buildState.buildResults); - - var result = await results.next; - checkBuild( - result, - outputs: {'a|test_files/a.txt.copy': 'a'}, - readerWriter: readerWriter, - ); - - await readerWriter.writeAsString( - makeAssetId('a|test_files/a.txt'), - 'b', - ); - - result = await results.next; - checkBuild( - result, - outputs: {'a|test_files/a.txt.copy': 'b'}, - readerWriter: readerWriter, - ); - }); - - test('rebuilds on new files', () async { - final buildState = await startWatch( - [copyABuildApplication], - {'a|web/a.txt': 'a'}, - readerWriter, - packageGraph: packageGraph, - ); - final results = StreamQueue(buildState.buildResults); - - var result = await results.next; - checkBuild( - result, - outputs: {'a|web/a.txt.copy': 'a'}, - readerWriter: readerWriter, - ); - - await readerWriter.writeAsString(makeAssetId('a|web/b.txt'), 'b'); - - result = await results.next; - checkBuild( - result, - outputs: {'a|web/b.txt.copy': 'b'}, - readerWriter: readerWriter, - ); - // Previous outputs should still exist. - expect( - readerWriter.testing.readString(makeAssetId('a|web/a.txt.copy')), - 'a', - ); - }); - - test('rebuilds on new files outside hardcoded sources', () async { - final buildState = await startWatch( - [copyABuildApplication], - { - 'a|test_files/a.txt': 'a', - 'a|build.yaml': ''' -targets: - a: - sources: - - test_files/** -''', - }, - readerWriter, - packageGraph: packageGraph, - ); - final results = StreamQueue(buildState.buildResults); - - var result = await results.next; - checkBuild( - result, - outputs: {'a|test_files/a.txt.copy': 'a'}, - readerWriter: readerWriter, - ); - - await readerWriter.writeAsString( - makeAssetId('a|test_files/b.txt'), - 'b', - ); - - result = await results.next; - checkBuild( - result, - outputs: {'a|test_files/b.txt.copy': 'b'}, - readerWriter: readerWriter, - ); - // Previous outputs should still exist. - expect( - readerWriter.testing.readString( - makeAssetId('a|test_files/a.txt.copy'), - ), - 'a', - ); - }); - - test('rebuilds on deleted files', () async { - final buildState = await startWatch( - [copyABuildApplication], - {'a|web/a.txt': 'a', 'a|web/b.txt': 'b'}, - readerWriter, - packageGraph: packageGraph, - ); - final results = StreamQueue(buildState.buildResults); - - var result = await results.next; - checkBuild( - result, - outputs: {'a|web/a.txt.copy': 'a', 'a|web/b.txt.copy': 'b'}, - readerWriter: readerWriter, - ); - - // Don't call writer.delete, that has side effects. - readerWriter.testing.delete(makeAssetId('a|web/a.txt')); - FakeWatcher.notifyWatchers( - WatchEvent(ChangeType.REMOVE, path.absolute('a', 'web', 'a.txt')), - ); - - result = await results.next; - - // Shouldn't rebuild anything, no outputs. - checkBuild(result, outputs: {}, readerWriter: readerWriter); - - // The old output file should no longer exist either. - expect( - readerWriter.testing.exists(makeAssetId('a|web/a.txt.copy')), - isFalse, - ); - // Previous outputs should still exist. - expect( - readerWriter.testing.readString(makeAssetId('a|web/b.txt.copy')), - 'b', - ); - }); - - test('rebuilds on created missing source files', () async { - final application = applyToRoot( - TestBuilder( - buildExtensions: appendExtension('.copy', from: '.txt'), - extraWork: (buildStep, _) async { - await buildStep.canRead(makeAssetId('a|web/b.other')); - }, - ), - ); - - final buildState = await startWatch( - [application], - {'a|web/a.txt': 'a'}, - readerWriter, - packageGraph: packageGraph, - ); - final results = StreamQueue(buildState.buildResults); - - var result = await results.next; - checkBuild( - result, - outputs: {'a|web/a.txt.copy': 'a'}, - readerWriter: readerWriter, - ); - - readerWriter.testing.writeString(makeAssetId('a|web/b.other'), 'b'); - FakeWatcher.notifyWatchers( - WatchEvent(ChangeType.ADD, path.absolute('a', 'web', 'b.other')), - ); - - // Should rebuild due to the previously-missing input appearing. - result = await results.next; - checkBuild( - result, - outputs: {'a|web/a.txt.copy': 'a'}, - readerWriter: readerWriter, - ); - }); - - test('rebuilds on deleted files outside hardcoded sources', () async { - final buildState = await startWatch( - [copyABuildApplication], - { - 'a|test_files/a.txt': 'a', - 'a|test_files/b.txt': 'b', - 'a|build.yaml': ''' -targets: - a: - sources: - - test_files/** -''', - }, - readerWriter, - packageGraph: packageGraph, - ); - final results = StreamQueue(buildState.buildResults); - - var result = await results.next; - checkBuild( - result, - outputs: { - 'a|test_files/a.txt.copy': 'a', - 'a|test_files/b.txt.copy': 'b', - }, - readerWriter: readerWriter, - ); - - // Don't call writer.delete, that has side effects. - readerWriter.testing.delete(makeAssetId('a|test_files/a.txt')); - FakeWatcher.notifyWatchers( - WatchEvent( - ChangeType.REMOVE, - path.absolute('a', 'test_files', 'a.txt'), - ), - ); - - result = await results.next; - - // Shouldn't rebuild anything, no outputs. - checkBuild(result, outputs: {}, readerWriter: readerWriter); - - // The old output file should no longer exist either. - expect( - readerWriter.testing.exists(makeAssetId('a|test_files/a.txt.copy')), - isFalse, - ); - // Previous outputs should still exist. - expect( - readerWriter.testing.readString( - makeAssetId('a|test_files/b.txt.copy'), - ), - 'b', - ); - }); - - test('rebuilds properly update asset_graph.json', () async { - final buildState = await startWatch( - [copyABuildApplication], - {'a|web/a.txt': 'a', 'a|web/b.txt': 'b'}, - readerWriter, - packageGraph: packageGraph, - ); - final results = StreamQueue(buildState.buildResults); - - var result = await results.next; - checkBuild( - result, - outputs: {'a|web/a.txt.copy': 'a', 'a|web/b.txt.copy': 'b'}, - readerWriter: readerWriter, - ); - - await readerWriter.writeAsString(makeAssetId('a|web/c.txt'), 'c'); - - await readerWriter.writeAsString(makeAssetId('a|web/b.txt'), 'b2'); - - // Don't call writer.delete, that has side effects. - readerWriter.testing.delete(makeAssetId('a|web/a.txt')); - FakeWatcher.notifyWatchers( - WatchEvent(ChangeType.REMOVE, path.absolute('a', 'web', 'a.txt')), - ); - - result = await results.next; - checkBuild( - result, - outputs: {'a|web/b.txt.copy': 'b2', 'a|web/c.txt.copy': 'c'}, - readerWriter: readerWriter, - ); - - final cachedGraph = - AssetGraph.deserialize( - readerWriter.testing.readBytes(makeAssetId('a|$assetGraphPath')), - )!; - - final expectedGraph = await AssetGraph.build( - BuildPhases([]), - {}, - {packageConfigId}, - buildPackageGraph({rootPackage('a'): []}), - readerWriter, - ); - - final aTxtId = makeAssetId('a|web/a.txt'); - final aTxtNode = AssetNode.missingSource(aTxtId); - final aTxtCopyId = makeAssetId('a|web/a.txt.copy'); - final aTxtCopyNode = AssetNode.missingSource(aTxtCopyId); - final bCopyId = makeAssetId('a|web/b.txt.copy'); - final bTxtId = makeAssetId('a|web/b.txt'); - final bCopyNode = AssetNode.generated( - bCopyId, - phaseNumber: 0, - primaryInput: makeAssetId('a|web/b.txt'), - result: true, - digest: computeDigest(bCopyId, 'b2'), - inputs: [makeAssetId('a|web/b.txt')], - isHidden: false, - ); - - expectedGraph - ..add(aTxtNode) - ..add(aTxtCopyNode) - ..add(bCopyNode) - ..add( - AssetNode.source( - AssetId.parse('a|web/b.txt'), - outputs: [bCopyNode.id], - primaryOutputs: [bCopyNode.id], - digest: computeDigest(bTxtId, 'b2'), - ), - ); - - final cCopyId = makeAssetId('a|web/c.txt.copy'); - final cTxtId = makeAssetId('a|web/c.txt'); - final cCopyNode = AssetNode.generated( - cCopyId, - phaseNumber: 0, - primaryInput: cTxtId, - result: true, - digest: computeDigest(cCopyId, 'c'), - inputs: [makeAssetId('a|web/c.txt')], - isHidden: false, - ); - expectedGraph - ..add(cCopyNode) - ..add( - AssetNode.source( - AssetId.parse('a|web/c.txt'), - outputs: [cCopyNode.id], - primaryOutputs: [cCopyNode.id], - digest: computeDigest(cTxtId, 'c'), - ), - ); - - expect(cachedGraph, equalsAssetGraph(expectedGraph)); - expect( - cachedGraph.allPostProcessBuildStepOutputs, - expectedGraph.allPostProcessBuildStepOutputs, - ); - }); - - test('ignores events from nested packages', () async { - final packageGraph = buildPackageGraph({ - rootPackage('a', path: path.absolute('a')): ['b'], - package('b', path: path.absolute('a', 'b')): [], - }); - - final buildState = await startWatch( - [copyABuildApplication], - {'a|web/a.txt': 'a', 'b|web/b.txt': 'b'}, - readerWriter, - packageGraph: packageGraph, - ); - final results = StreamQueue(buildState.buildResults); - - var result = await results.next; - // Should ignore the files under the `b` package, even though they - // match the input set. - checkBuild( - result, - outputs: {'a|web/a.txt.copy': 'a'}, - readerWriter: readerWriter, - ); - - await readerWriter.writeAsString(makeAssetId('a|web/a.txt'), 'b'); - await readerWriter.writeAsString(makeAssetId('b|web/b.txt'), 'c'); - // Have to manually notify here since the path isn't standard. - FakeWatcher.notifyWatchers( - WatchEvent( - ChangeType.MODIFY, - path.absolute('a', 'b', 'web', 'a.txt'), - ), - ); - - result = await results.next; - // Ignores the modification under the `b` package, even though it - // matches the input set. - checkBuild( - result, - outputs: {'a|web/a.txt.copy': 'b'}, - readerWriter: readerWriter, - ); - }); - - test('rebuilds on file updates during first build', () async { - final blocker = Completer(); - final buildAction = applyToRoot( - TestBuilder(extraWork: (_, _) => blocker.future), - ); - final buildState = await startWatch( - [buildAction], - {'a|web/a.txt': 'a'}, - readerWriter, - packageGraph: packageGraph, - ); - final results = StreamQueue(buildState.buildResults); - - FakeWatcher.notifyWatchers( - WatchEvent(ChangeType.MODIFY, path.absolute('a', 'web', 'a.txt')), - ); - blocker.complete(); - - var result = await results.next; - // TODO: Move this up above the call to notifyWatchers once - // https://github.com/dart-lang/build/issues/526 is fixed. - await readerWriter.writeAsString(makeAssetId('a|web/a.txt'), 'b'); - - checkBuild( - result, - outputs: {'a|web/a.txt.copy': 'a'}, - readerWriter: readerWriter, - ); - - result = await results.next; - checkBuild( - result, - outputs: {'a|web/a.txt.copy': 'b'}, - readerWriter: readerWriter, - ); - }); - - test('edits to .dart_tool/package_config.json prevent future builds ' - 'and ask you to restart', () async { - final logs = []; - final buildState = await startWatch( - [copyABuildApplication], - {'a|web/a.txt': 'a'}, - readerWriter, - packageGraph: packageGraph, - onLog: (record) { - if (record.level == Level.SEVERE) logs.add(record); - }, - ); - final results = StreamQueue(buildState.buildResults); - - final result = await results.next; - checkBuild( - result, - outputs: {'a|web/a.txt.copy': 'a'}, - readerWriter: readerWriter, - ); - - final newConfig = Map.of(_packageConfig); - newConfig['extra'] = 'stuff'; - await readerWriter.writeAsString( - packageConfigId, - jsonEncode(newConfig), - ); - - expect(await results.hasNext, isFalse); - expect(logs, hasLength(1)); - expect( - logs.first.message, - contains('Terminating builds due to package graph update.'), - ); - }); - - test( - 'Gives the package config a chance to be re-written before failing', - () async { - final logs = []; - final buildState = await startWatch( - [copyABuildApplication], - {'a|web/a.txt': 'a'}, - readerWriter, - packageGraph: packageGraph, - onLog: (record) { - if (record.level == Level.SEVERE) logs.add(record); - }, - ); - buildState.buildResults.handleError( - (Object e, StackTrace s) => print('$e\n$s'), - ); - buildState.buildResults.listen(print); - final results = StreamQueue(buildState.buildResults); - - final result = await results.next; - checkBuild( - result, - outputs: {'a|web/a.txt.copy': 'a'}, - readerWriter: readerWriter, - ); - - await readerWriter.delete(packageConfigId); - - // Wait for it to try reading the file twice to ensure it will retry. - await (_readerForState[buildState] as InternalTestReaderWriter) - .onCanRead - .where((id) => id == packageConfigId) - .take(2) - .drain(); - - final newConfig = Map.of(_packageConfig); - newConfig['extra'] = 'stuff'; - await readerWriter.writeAsString( - packageConfigId, - jsonEncode(newConfig), - ); - - expect(await results.hasNext, isFalse); - expect(logs, hasLength(1)); - expect( - logs.first.message, - contains('Terminating builds due to package graph update.'), - ); - }, - ); - - group('build.yaml', () { - final packageGraph = buildPackageGraph({ - rootPackage('a', path: path.absolute('a')): ['b'], - package('b', path: path.absolute('b'), type: DependencyType.path): [], - }); - late List logs; - late StreamQueue results; - - group('is added', () { - setUp(() async { - logs = []; - final buildState = await startWatch( - [copyABuildApplication], - {}, - readerWriter, - onLog: (record) { - if (record.level == Level.SEVERE) logs.add(record); - }, - packageGraph: packageGraph, - ); - results = StreamQueue(buildState.buildResults); - await results.next; - }); - - test('to the root package', () async { - await readerWriter.writeAsString( - AssetId('a', 'build.yaml'), - '# New build.yaml file', - ); - expect(await results.hasNext, isTrue); - final next = await results.next; - expect(next.status, BuildStatus.failure); - expect(next.failureType, FailureType.buildConfigChanged); - expect(logs, hasLength(1)); - expect( - logs.first.message, - contains('Terminating builds due to a:build.yaml update'), - ); - }); - - test('to a dependency', () async { - await readerWriter.writeAsString( - AssetId('b', 'build.yaml'), - '# New build.yaml file', - ); - - expect(await results.hasNext, isTrue); - final next = await results.next; - expect(next.status, BuildStatus.failure); - expect(next.failureType, FailureType.buildConfigChanged); - expect(logs, hasLength(1)); - expect( - logs.first.message, - contains('Terminating builds due to b:build.yaml update'), - ); - }); - - test('.build.yaml', () async { - await readerWriter.writeAsString( - AssetId('a', 'b.build.yaml'), - '# New b.build.yaml file', - ); - expect(await results.hasNext, isTrue); - final next = await results.next; - expect(next.status, BuildStatus.failure); - expect(next.failureType, FailureType.buildConfigChanged); - expect(logs, hasLength(1)); - expect( - logs.first.message, - contains('Terminating builds due to a:b.build.yaml update'), - ); - }); - }); - - group('is edited', () { - setUp(() async { - logs = []; - final buildState = await startWatch( - [copyABuildApplication], - {'a|build.yaml': '', 'b|build.yaml': ''}, - readerWriter, - onLog: (record) { - if (record.level == Level.SEVERE) logs.add(record); - }, - packageGraph: packageGraph, - ); - results = StreamQueue(buildState.buildResults); - await results.next; - }); - - test('in the root package', () async { - await readerWriter.writeAsString( - AssetId('a', 'build.yaml'), - '# Edited build.yaml file', - ); - - expect(await results.hasNext, isTrue); - final next = await results.next; - expect(next.status, BuildStatus.failure); - expect(next.failureType, FailureType.buildConfigChanged); - expect(logs, hasLength(1)); - expect( - logs.first.message, - contains('Terminating builds due to a:build.yaml update'), - ); - }); - - test('in a dependency', () async { - await readerWriter.writeAsString( - AssetId('b', 'build.yaml'), - '# Edited build.yaml file', - ); - - expect(await results.hasNext, isTrue); - final next = await results.next; - expect(next.status, BuildStatus.failure); - expect(next.failureType, FailureType.buildConfigChanged); - expect(logs, hasLength(1)); - expect( - logs.first.message, - contains('Terminating builds due to b:build.yaml update'), - ); - }); - }); - - group('with --config', () { - setUp(() async { - logs = []; - final buildState = await startWatch( - [copyABuildApplication], - {'a|build.yaml': '', 'a|build.cool.yaml': ''}, - readerWriter, - configKey: 'cool', - onLog: (record) { - if (record.level == Level.SEVERE) logs.add(record); - }, - overrideBuildConfig: { - 'a': BuildConfig.useDefault('a', ['b']), - }, - packageGraph: packageGraph, - ); - results = StreamQueue(buildState.buildResults); - await results.next; - }); - - test('original is edited', () async { - await readerWriter.writeAsString( - AssetId('a', 'build.yaml'), - '# Edited build.yaml file', - ); - - expect(await results.hasNext, isTrue); - final next = await results.next; - expect(next.status, BuildStatus.failure); - expect(next.failureType, FailureType.buildConfigChanged); - expect(logs, hasLength(1)); - expect( - logs.first.message, - contains('Terminating builds due to a:build.yaml update'), - ); - }); - - test('build..yaml in dependencies are ignored', () async { - await readerWriter.writeAsString( - AssetId('b', 'build.cool.yaml'), - '# New build.yaml file', - ); - - await Future.delayed(_debounceDelay); - expect(logs, isEmpty); - - await terminateWatch(); - }); - - test('build..yaml is edited', () async { - await readerWriter.writeAsString( - AssetId('a', 'build.cool.yaml'), - '# Edited build.cool.yaml file', - ); - - expect(await results.hasNext, isTrue); - final next = await results.next; - expect(next.status, BuildStatus.failure); - expect(next.failureType, FailureType.buildConfigChanged); - expect(logs, hasLength(1)); - expect( - logs.first.message, - contains('Terminating builds due to a:build.cool.yaml update'), - ); - }); - }); - }); - }); - - group('file updates to same contents', () { - test('does not rebuild', () async { - var runCount = 0; - final buildState = await startWatch( - [ - applyToRoot( - TestBuilder( - buildExtensions: appendExtension('.copy', from: '.txt'), - build: (buildStep, _) { - runCount++; - buildStep.writeAsString( - buildStep.inputId.addExtension('.copy'), - buildStep.readAsString(buildStep.inputId), - ); - throw StateError('Fail'); - }, - ), - ), - ], - {'a|web/a.txt': 'a'}, - readerWriter, - packageGraph: packageGraph, - ); - final results = StreamQueue(buildState.buildResults); - - final result = await results.next; - expect(runCount, 1); - checkBuild( - result, - status: BuildStatus.failure, - readerWriter: readerWriter, - ); - - await readerWriter.writeAsString(makeAssetId('a|web/a.txt'), 'a'); - - // Wait for the `_debounceDelay * 4` before terminating to - // give it a chance to pick up the change. - await Future.delayed(_debounceDelay * 4); - - await terminateWatch(); - expect(await results.hasNext, isFalse); - }); - }); - - group('multiple phases', () { - test('edits propagate through all phases', () async { - final buildActions = [ - copyABuildApplication, - applyToRoot( - TestBuilder( - buildExtensions: appendExtension('.copy', from: '.copy'), - ), - ), - ]; - - final buildState = await startWatch( - buildActions, - {'a|web/a.txt': 'a'}, - readerWriter, - packageGraph: packageGraph, - ); - final results = StreamQueue(buildState.buildResults); - - var result = await results.next; - checkBuild( - result, - outputs: {'a|web/a.txt.copy': 'a', 'a|web/a.txt.copy.copy': 'a'}, - readerWriter: readerWriter, - ); - - await readerWriter.writeAsString(makeAssetId('a|web/a.txt'), 'b'); - - result = await results.next; - checkBuild( - result, - outputs: {'a|web/a.txt.copy': 'b', 'a|web/a.txt.copy.copy': 'b'}, - readerWriter: readerWriter, - ); - }); - - test('adds propagate through all phases', () async { - final buildActions = [ - copyABuildApplication, - applyToRoot( - TestBuilder( - buildExtensions: appendExtension('.copy', from: '.copy'), - ), - ), - ]; - - final buildState = await startWatch( - buildActions, - {'a|web/a.txt': 'a'}, - readerWriter, - packageGraph: packageGraph, - ); - final results = StreamQueue(buildState.buildResults); - - var result = await results.next; - checkBuild( - result, - outputs: {'a|web/a.txt.copy': 'a', 'a|web/a.txt.copy.copy': 'a'}, - readerWriter: readerWriter, - ); - - await readerWriter.writeAsString(makeAssetId('a|web/b.txt'), 'b'); - - result = await results.next; - checkBuild( - result, - outputs: {'a|web/b.txt.copy': 'b', 'a|web/b.txt.copy.copy': 'b'}, - readerWriter: readerWriter, - ); - // Previous outputs should still exist. - expect( - readerWriter.testing.readString(makeAssetId('a|web/a.txt.copy')), - 'a', - ); - expect( - readerWriter.testing.readString(makeAssetId('a|web/a.txt.copy.copy')), - 'a', - ); - }); - - test('deletes propagate through all phases', () async { - final buildActions = [ - copyABuildApplication, - applyToRoot( - TestBuilder( - buildExtensions: appendExtension('.copy', from: '.copy'), - ), - ), - ]; - - final buildState = await startWatch( - buildActions, - {'a|web/a.txt': 'a', 'a|web/b.txt': 'b'}, - readerWriter, - packageGraph: packageGraph, - ); - final results = StreamQueue(buildState.buildResults); - - var result = await results.next; - checkBuild( - result, - outputs: { - 'a|web/a.txt.copy': 'a', - 'a|web/a.txt.copy.copy': 'a', - 'a|web/b.txt.copy': 'b', - 'a|web/b.txt.copy.copy': 'b', - }, - readerWriter: readerWriter, - ); - - // Don't call writer.delete, that has side effects. - readerWriter.testing.delete(makeAssetId('a|web/a.txt')); - - FakeWatcher.notifyWatchers( - WatchEvent(ChangeType.REMOVE, path.absolute('a', 'web', 'a.txt')), - ); - - result = await results.next; - // Shouldn't rebuild anything, no outputs. - checkBuild(result, outputs: {}, readerWriter: readerWriter); - - // Derived outputs should no longer exist. - expect( - readerWriter.testing.exists(makeAssetId('a|web/a.txt.copy')), - isFalse, - ); - expect( - readerWriter.testing.exists(makeAssetId('a|web/a.txt.copy.copy')), - isFalse, - ); - // Other outputs should still exist. - expect( - readerWriter.testing.readString(makeAssetId('a|web/b.txt.copy')), - 'b', - ); - expect( - readerWriter.testing.readString(makeAssetId('a|web/b.txt.copy.copy')), - 'b', - ); - }); - - test('deleted generated outputs are regenerated', () async { - final buildActions = [ - copyABuildApplication, - applyToRoot( - TestBuilder( - buildExtensions: appendExtension('.copy', from: '.copy'), - ), - ), - ]; - - final buildState = await startWatch( - buildActions, - {'a|web/a.txt': 'a'}, - readerWriter, - packageGraph: packageGraph, - ); - final results = StreamQueue(buildState.buildResults); - - var result = await results.next; - checkBuild( - result, - outputs: {'a|web/a.txt.copy': 'a', 'a|web/a.txt.copy.copy': 'a'}, - readerWriter: readerWriter, - ); - - // Don't call writer.delete, that has side effects. - readerWriter.testing.delete(makeAssetId('a|web/a.txt.copy')); - FakeWatcher.notifyWatchers( - WatchEvent( - ChangeType.REMOVE, - path.absolute('a', 'web', 'a.txt.copy'), - ), - ); - - result = await results.next; - // Should rebuild the generated asset, but not its outputs because its - // content didn't change. - checkBuild( - result, - outputs: {'a|web/a.txt.copy': 'a'}, - readerWriter: readerWriter, - ); - }); - }); - - /// Tests for updates - group('secondary dependency', () { - test('of an output file is edited', () async { - final buildActions = [ - applyToRoot( - TestBuilder( - buildExtensions: appendExtension('.copy', from: '.a'), - build: copyFrom(makeAssetId('a|web/file.b')), - ), - ), - ]; - - final buildState = await startWatch( - buildActions, - {'a|web/file.a': 'a', 'a|web/file.b': 'b'}, - readerWriter, - packageGraph: packageGraph, - ); - final results = StreamQueue(buildState.buildResults); - - var result = await results.next; - checkBuild( - result, - outputs: {'a|web/file.a.copy': 'b'}, - readerWriter: readerWriter, - ); - - await readerWriter.writeAsString(makeAssetId('a|web/file.b'), 'c'); - - result = await results.next; - checkBuild( - result, - outputs: {'a|web/file.a.copy': 'c'}, - readerWriter: readerWriter, - ); - }); - - test( - 'of an output which is derived from another generated file is edited', - () async { - final buildActions = [ - applyToRoot( - TestBuilder( - buildExtensions: appendExtension('.copy', from: '.a'), - ), - ), - applyToRoot( - TestBuilder( - buildExtensions: appendExtension('.copy', from: '.a.copy'), - build: copyFrom(makeAssetId('a|web/file.b')), - ), - ), - ]; - - final buildState = await startWatch( - buildActions, - {'a|web/file.a': 'a', 'a|web/file.b': 'b'}, - readerWriter, - packageGraph: packageGraph, - ); - final results = StreamQueue(buildState.buildResults); - - var result = await results.next; - checkBuild( - result, - outputs: {'a|web/file.a.copy': 'a', 'a|web/file.a.copy.copy': 'b'}, - readerWriter: readerWriter, - ); - - await readerWriter.writeAsString(makeAssetId('a|web/file.b'), 'c'); - - result = await results.next; - checkBuild( - result, - outputs: {'a|web/file.a.copy.copy': 'c'}, - readerWriter: readerWriter, - ); - }, - ); - }); - }); -} - -final _debounceDelay = const Duration(milliseconds: 10); -StreamController? _terminateWatchController; - -/// Start watching files and running builds. -Future startWatch( - List builders, - Map inputs, - InternalTestReaderWriter readerWriter, { - required PackageGraph packageGraph, - Map overrideBuildConfig = const {}, - void Function(LogRecord)? onLog, - String? configKey, -}) async { - onLog ??= (_) {}; - inputs.forEach((serializedId, contents) { - readerWriter.writeAsString(makeAssetId(serializedId), contents); - }); - FakeWatcher watcherFactory(String path) => FakeWatcher(path); - - final state = - (await WatchCommand( - builderFactories: BuilderFactories(), - buildOptions: BuildOptions.forTests( - configKey: configKey, - skipBuildScriptCheck: true, - ), - testingOverrides: TestingOverrides( - builderApplications: builders.toBuiltList(), - buildConfig: overrideBuildConfig.build(), - directoryWatcherFactory: watcherFactory, - debounceDelay: _debounceDelay, - onLog: onLog, - packageGraph: packageGraph, - readerWriter: readerWriter, - terminateEventStream: _terminateWatchController!.stream, - ), - ).watch())!; - - // Some tests need access to `reader` so we expose it through an expando. - _readerForState[state] = readerWriter; - return state; -} - -/// Tells the program to stop watching files and terminate. -Future terminateWatch() async { - final terminateWatchController = _terminateWatchController; - if (terminateWatchController == null) return; - - /// Can add any type of event. - terminateWatchController.add(ProcessSignal.sigabrt); - await terminateWatchController.close(); - _terminateWatchController = null; -} - -const _packageConfig = { - 'configVersion': 2, - 'packages': [ - {'name': 'a', 'rootUri': 'file://fake/pkg/path', 'packageUri': 'lib/'}, - ], -}; - -/// Store the private in memory asset reader for a given [BuildState] object -/// here so we can get access to it. -final _readerForState = Expando(); diff --git a/build_runner/test/common/build_runner_tester.dart b/build_runner/test/common/build_runner_tester.dart index 697ea798c1..7ce3bfc5d5 100644 --- a/build_runner/test/common/build_runner_tester.dart +++ b/build_runner/test/common/build_runner_tester.dart @@ -2,6 +2,7 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:isolate'; @@ -13,7 +14,6 @@ import 'package:package_config/package_config.dart'; import 'package:path/path.dart' as p; import 'package:test/test.dart' as test; import 'package:test/test.dart'; -import 'package:test_process/test_process.dart'; import 'fixture_packages.dart'; @@ -171,7 +171,7 @@ ${result.stdout}${result.stderr}=== Future start(String directory, String commandLine) async { final args = commandLine.split(' '); final command = args.removeAt(0); - final process = await TestProcess.start( + final process = await Process.start( command, args, workingDirectory: p.join(tempDirectory.path, directory), @@ -183,16 +183,36 @@ ${result.stdout}${result.stderr}=== /// A running `build_runner` process. class BuildRunnerProcess { - final TestProcess process; + final Process process; final StreamQueue _outputs; late final HttpClient _client = HttpClient(); int? _port; BuildRunnerProcess(this.process) : _outputs = StreamQueue( - StreamGroup.merge([process.stdoutStream(), process.stderrStream()]), + StreamGroup.merge([ + process.stdout + .transform(utf8.decoder) + .transform(const LineSplitter()), + process.stderr + .transform(utf8.decoder) + .transform(const LineSplitter()), + ]), ); + /// Expects nothing new on stdout or stderr for [duration]. + Future expectNoOutput(Duration duration) async { + printOnFailure('--- $_testLine expects no output'); + try { + final line = await _outputs.next.timeout(duration); + fail('While expecting no output, got `$line`.'); + } on TimeoutException catch (_) { + // Expected. + } catch (_) { + fail('While expecting no output, process exited.'); + } + } + /// Expects [pattern] to appear in the process's stdout or stderr. /// /// If [failOn] is encountered instead, the test fails immediately. It @@ -202,12 +222,21 @@ class BuildRunnerProcess { /// If the process exits instead, the test fails immediately. /// /// Otherwise, waits until [pattern] appears, returns the matching line. + /// + /// Throws if the process appears to be stuck or done: if it outputs nothing + /// for 30s. Future expect(Pattern pattern, {Pattern? failOn}) async { + printOnFailure( + '--- $_testLine expects `$pattern`' + '${failOn == null ? '' : ', failOn: `$failOn`'}', + ); failOn ??= BuildLog.failurePattern; while (true) { String? line; try { - line = await _outputs.next; + line = await _outputs.next.timeout(const Duration(seconds: 30)); + } on TimeoutException catch (_) { + throw fail('While expecting `$pattern`, timed out after 30s.'); } catch (_) { throw fail('While expecting `$pattern`, process exited.'); } @@ -219,8 +248,23 @@ class BuildRunnerProcess { } } + String get _testLine { + var result = + StackTrace.current + .toString() + .split('\n') + .where((l) => l.contains('_test.dart')) + .first; + result = result.substring(result.lastIndexOf('/') + 1); + result = result.substring(0, result.lastIndexOf(':')); + return result; + } + /// Kills the process. - Future kill() => process.kill(); + Future kill() async { + process.kill(); + await process.exitCode; + } // Expects the server to log that it is serving, records the port. Future expectServing() async { diff --git a/build_runner/test/integration_tests/watch_command_invalidation_test.dart b/build_runner/test/integration_tests/watch_command_invalidation_test.dart deleted file mode 100644 index 73181e16c1..0000000000 --- a/build_runner/test/integration_tests/watch_command_invalidation_test.dart +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -@Tags(['integration3']) -library; - -import 'package:build_runner/src/logging/build_log.dart'; -import 'package:test/test.dart'; - -import '../common/common.dart'; - -void main() async { - test('watch command invalidation', () async { - final pubspecs = await Pubspecs.load(); - final tester = BuildRunnerTester(pubspecs); - - tester.writeFixturePackage(FixturePackages.copyBuilder()); - tester.writePackage( - name: 'root_pkg', - dependencies: ['build_runner'], - pathDependencies: ['builder_pkg'], - files: {'web/a.txt': 'a'}, - ); - - // Watch and initial build. - var watch = await tester.start('root_pkg', 'dart run build_runner watch'); - await watch.expect(BuildLog.successPattern); - expect(tester.read('root_pkg/web/a.txt.copy'), 'a'); - - // Builder change. - tester.update('builder_pkg/lib/builder.dart', (script) => '$script\n'); - await watch.expect('Terminating builds due to build script update'); - await watch.expect('Compiling the build script'); - await watch.expect('Creating the asset graph'); - await watch.expect(BuildLog.successPattern); - expect(tester.read('root_pkg/web/a.txt.copy'), 'a'); - - // Builder config change. - tester.write('root_pkg/build.yaml', '# new file, nothing here'); - await watch.expect('Terminating builds due to root_pkg:build.yaml update'); - await watch.expect(BuildLog.successPattern); - expect(tester.read('root_pkg/web/a.txt.copy'), 'a'); - - // Now with --output. - await watch.kill(); - watch = await tester.start( - 'root_pkg', - 'dart run build_runner watch --output web:build', - ); - await watch.expect(BuildLog.successPattern); - expect(tester.read('root_pkg/build/a.txt'), 'a'); - expect(tester.read('root_pkg/build/a.txt.copy'), 'a'); - - // Changed inputs and outputs are written to output directory. - tester.write('root_pkg/lib/a.txt', 'updated'); - await watch.expect(BuildLog.successPattern); - expect(tester.read('root_pkg/build/a.txt'), 'a'); - expect(tester.read('root_pkg/build/a.txt.copy'), 'a'); - }); -} diff --git a/build_runner/test/integration_tests/watch_command_test.dart b/build_runner/test/integration_tests/watch_command_test.dart new file mode 100644 index 0000000000..6a0e47d285 --- /dev/null +++ b/build_runner/test/integration_tests/watch_command_test.dart @@ -0,0 +1,143 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +@Tags(['integration3']) +library; + +import 'package:build_runner/src/logging/build_log.dart'; +import 'package:test/test.dart'; + +import '../common/common.dart'; + +void main() async { + test('watch command invalidation', () async { + final pubspecs = await Pubspecs.load(); + final tester = BuildRunnerTester(pubspecs); + + tester.writeFixturePackage(FixturePackages.copyBuilder()); + tester.writePackage( + name: 'root_pkg', + dependencies: ['build_runner'], + pathDependencies: ['builder_pkg', 'other_pkg'], + files: {'web/a.txt': 'a'}, + ); + tester.writePackage( + name: 'other_pkg', + dependencies: ['build_runner'], + pathDependencies: ['builder_pkg'], + files: {}, + ); + + // Watch and initial build. + var watch = await tester.start('root_pkg', 'dart run build_runner watch'); + await watch.expect(BuildLog.successPattern); + expect(tester.read('root_pkg/web/a.txt.copy'), 'a'); + + // File change. + tester.write('root_pkg/web/a.txt', 'updated'); + await watch.expect(BuildLog.successPattern); + expect(tester.read('root_pkg/web/a.txt.copy'), 'updated'); + + // File rewrite without change. + tester.write('root_pkg/web/a.txt', 'updated'); + await watch.expectNoOutput(const Duration(seconds: 1)); + + // State on disk is updated so `build` knows to do nothing. + var output = await tester.run('root_pkg', 'dart run build_runner build'); + expect(output, contains('wrote 0 outputs')); + + // New file. + tester.write('root_pkg/web/b.txt', 'b'); + await watch.expect(BuildLog.successPattern); + expect(tester.read('root_pkg/web/b.txt.copy'), 'b'); + + // State on disk is updated so `build` knows to do nothing. + output = await tester.run('root_pkg', 'dart run build_runner build'); + expect(output, contains('wrote 0 outputs')); + + // Deleted file. + tester.delete('root_pkg/web/b.txt'); + await watch.expect(BuildLog.successPattern); + expect(tester.read('root_pkg/web/b.txt.copy'), null); + + // Deleted output. + tester.delete('root_pkg/web/a.txt.copy'); + await watch.expect(BuildLog.successPattern); + expect(tester.read('root_pkg/web/a.txt.copy'), 'updated'); + + // Builder change. + tester.update('builder_pkg/lib/builder.dart', (script) => '$script\n'); + await watch.expect('Terminating builds due to build script update'); + await watch.expect('Compiling the build script'); + await watch.expect('Creating the asset graph'); + await watch.expect(BuildLog.successPattern); + expect(tester.read('root_pkg/web/a.txt.copy'), 'updated'); + + // State on disk is updated so `build` knows to do nothing. + output = await tester.run('root_pkg', 'dart run build_runner build'); + expect(output, contains('wrote 0 outputs')); + + // Builder config change, add a file. + tester.write('root_pkg/build.yaml', '# new file, nothing here'); + await watch.expect('Terminating builds due to root_pkg:build.yaml update'); + await watch.expect(BuildLog.successPattern); + expect(tester.read('root_pkg/web/a.txt.copy'), 'updated'); + + // Builder config change, update a file. + tester.update('root_pkg/build.yaml', (yaml) => '$yaml\n'); + await watch.expect('Terminating builds due to root_pkg:build.yaml update'); + await watch.expect(BuildLog.successPattern); + expect(tester.read('root_pkg/web/a.txt.copy'), 'updated'); + + // Builder config change in dependency. + tester.write('other_pkg/build.yaml', '# new file, nothing here'); + await watch.expect('Terminating builds due to other_pkg:build.yaml update'); + await watch.expect(BuildLog.successPattern); + expect(tester.read('root_pkg/web/a.txt.copy'), 'updated'); + + // Builder config change in root overriding dependency. + tester.write('root_pkg/other_pkg.build.yaml', '# new file, nothing here'); + await watch.expect( + 'Terminating builds due to root_pkg:other_pkg.build.yaml update', + ); + await watch.expect(BuildLog.successPattern); + expect(tester.read('root_pkg/web/a.txt.copy'), 'updated'); + + // State on disk is updated so `build` knows to do nothing. + output = await tester.run('root_pkg', 'dart run build_runner build'); + expect(output, contains('wrote 0 outputs')); + + // File change during build. + tester.write('root_pkg/web/a.txt', 'a'); + await watch.expect('Building'); + tester.write('root_pkg/web/a.txt', 'updated'); + await watch.expect(BuildLog.successPattern); + expect(tester.read('root_pkg/web/a.txt.copy'), 'a'); + await watch.expect(BuildLog.successPattern); + expect(tester.read('root_pkg/web/a.txt.copy'), 'updated'); + + // Change to `package_config.json` causes the watcher to exit. + tester.update( + 'root_pkg/.dart_tool/package_config.json', + (script) => '$script\n', + ); + await watch.expect('Terminating builds due to package graph update.'); + await watch.kill(); + + // Now with --output. + watch = await tester.start( + 'root_pkg', + 'dart run build_runner watch --output web:build', + ); + await watch.expect(BuildLog.successPattern); + expect(tester.read('root_pkg/build/a.txt'), 'updated'); + expect(tester.read('root_pkg/build/a.txt.copy'), 'updated'); + + // Changed inputs and outputs are written to output directory. + tester.write('root_pkg/web/a.txt', 'a'); + await watch.expect(BuildLog.successPattern); + expect(tester.read('root_pkg/build/a.txt'), 'a'); + expect(tester.read('root_pkg/build/a.txt.copy'), 'a'); + }); +}