From 7737a020aa4b2346ac38e6d8048ba82d4746b8a6 Mon Sep 17 00:00:00 2001 From: Muhammad Mohiuddin Date: Sun, 10 Mar 2024 01:42:54 +0100 Subject: [PATCH 01/11] better default value for exec concurrency --- packages/melos/lib/src/command_runner/exec.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/melos/lib/src/command_runner/exec.dart b/packages/melos/lib/src/command_runner/exec.dart index 29401e6f1..5f7ca604d 100644 --- a/packages/melos/lib/src/command_runner/exec.dart +++ b/packages/melos/lib/src/command_runner/exec.dart @@ -23,7 +23,7 @@ import 'base.dart'; class ExecCommand extends MelosCommand { ExecCommand(super.config) { setupPackageFilterParser(); - argParser.addOption('concurrency', defaultsTo: '5', abbr: 'c'); + argParser.addOption('concurrency', defaultsTo: Platform.numberOfProcessors.toString(), abbr: 'c'); argParser.addFlag( 'fail-fast', abbr: 'f', From 3ebb41d12ec237f96d5c0f1c67b7c99e9af3340a Mon Sep 17 00:00:00 2001 From: Muhammad Mohiuddin Date: Sun, 10 Mar 2024 15:46:25 +0100 Subject: [PATCH 02/11] reformat file --- packages/melos/lib/src/command_runner/exec.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/melos/lib/src/command_runner/exec.dart b/packages/melos/lib/src/command_runner/exec.dart index 5f7ca604d..5c09edf56 100644 --- a/packages/melos/lib/src/command_runner/exec.dart +++ b/packages/melos/lib/src/command_runner/exec.dart @@ -23,7 +23,11 @@ import 'base.dart'; class ExecCommand extends MelosCommand { ExecCommand(super.config) { setupPackageFilterParser(); - argParser.addOption('concurrency', defaultsTo: Platform.numberOfProcessors.toString(), abbr: 'c'); + argParser.addOption( + 'concurrency', + defaultsTo: Platform.numberOfProcessors.toString(), + abbr: 'c', + ); argParser.addFlag( 'fail-fast', abbr: 'f', From 211d8d95ce80d61433142c54b7a6cac675408f61 Mon Sep 17 00:00:00 2001 From: Muhammad Mohiuddin Date: Sun, 10 Mar 2024 16:48:34 +0100 Subject: [PATCH 03/11] change default value inside function --- packages/melos/lib/src/commands/exec.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/melos/lib/src/commands/exec.dart b/packages/melos/lib/src/commands/exec.dart index 66cd45e0e..1b694c5ca 100644 --- a/packages/melos/lib/src/commands/exec.dart +++ b/packages/melos/lib/src/commands/exec.dart @@ -5,10 +5,11 @@ mixin _ExecMixin on _Melos { List execArgs, { GlobalOptions? global, PackageFilters? packageFilters, - int concurrency = 5, + int? concurrency, bool failFast = false, bool orderDependents = false, }) async { + concurrency ??= Platform.numberOfProcessors; final workspace = await createWorkspace(global: global, packageFilters: packageFilters); final packages = workspace.filteredPackages.values; From dc305c381fcaebfb8e72ac1b1c2e21332bddfcc1 Mon Sep 17 00:00:00 2001 From: Muhammad Mohiuddin Date: Sat, 16 Mar 2024 06:42:59 +0100 Subject: [PATCH 04/11] add immediate fail fast --- packages/melos/lib/src/commands/exec.dart | 102 ++++++++++++-------- packages/melos/lib/src/commands/runner.dart | 3 +- packages/melos/pubspec.yaml | 1 + 3 files changed, 63 insertions(+), 43 deletions(-) diff --git a/packages/melos/lib/src/commands/exec.dart b/packages/melos/lib/src/commands/exec.dart index 1b8c17de8..fcebd2503 100644 --- a/packages/melos/lib/src/commands/exec.dart +++ b/packages/melos/lib/src/commands/exec.dart @@ -11,8 +11,10 @@ mixin _ExecMixin on _Melos { Map extraEnvironment = const {}, }) async { concurrency ??= Platform.numberOfProcessors; - final workspace = - await createWorkspace(global: global, packageFilters: packageFilters); + final workspace = await createWorkspace( + global: global, + packageFilters: packageFilters, + ); final packages = workspace.filteredPackages.values; await _execForAllPackages( @@ -117,52 +119,65 @@ mixin _ExecMixin on _Melos { packages.map((package) => MapEntry(package.name, Completer())), ); - await pool.forEach(sortedPackages, (package) async { - if (failFast && failures.isNotEmpty) { - return; - } + CancelableOperation? operation; + + operation = CancelableOperation.fromFuture( + pool.forEach(sortedPackages, (package) async { + if (orderDependents) { + final dependenciesResults = await Future.wait( + package.allDependenciesInWorkspace.values + .map((package) => packageResults[package.name]?.future) + .whereNotNull(), + ); + + final dependencyFailed = dependenciesResults + .any((exitCode) => exitCode == null || exitCode > 0); + if (dependencyFailed) { + packageResults[package.name]?.complete(); + failures[package.name] = null; + + if (failFast) { + await operation?.cancel(); + operation = null; + } + + return; + } + } - if (orderDependents) { - final dependenciesResults = await Future.wait( - package.allDependenciesInWorkspace.values - .map((package) => packageResults[package.name]?.future) - .whereNotNull(), + if (!prefixLogs) { + logger + ..horizontalLine() + ..log(AnsiStyles.bgBlack.bold.italic('${package.name}:')); + } + + final packageExitCode = await _execForPackage( + workspace, + package, + execArgs, + prefixLogs: prefixLogs, + extraEnvironment: additionalEnvironment, ); - final dependencyFailed = dependenciesResults - .any((exitCode) => exitCode == null || exitCode > 0); - if (dependencyFailed) { - packageResults[package.name]?.complete(); - failures[package.name] = null; - return; + packageResults[package.name]?.complete(packageExitCode); + + if (packageExitCode > 0) { + failures[package.name] = packageExitCode; + } else if (!prefixLogs) { + logger.log( + AnsiStyles.bgBlack.bold.italic('${package.name}: ') + + AnsiStyles.bgBlack(successLabel), + ); } - } - if (!prefixLogs) { - logger - ..horizontalLine() - ..log(AnsiStyles.bgBlack.bold.italic('${package.name}:')); - } + if (packageExitCode > 0 && failFast) { + await operation?.cancel(); + operation = null; + } + }).drain(), + ); - final packageExitCode = await _execForPackage( - workspace, - package, - execArgs, - prefixLogs: prefixLogs, - extraEnvironment: additionalEnvironment, - ); - - packageResults[package.name]?.complete(packageExitCode); - - if (packageExitCode > 0) { - failures[package.name] = packageExitCode; - } else if (!prefixLogs) { - logger.log( - AnsiStyles.bgBlack.bold.italic('${package.name}: ') + - AnsiStyles.bgBlack(successLabel), - ); - } - }).drain(); + await operation?.valueOrCancellation(); logger ..horizontalLine() @@ -181,6 +196,9 @@ mixin _ExecMixin on _Melos { 'with exit code ${failures[packageName]})'}', ); } + if (failFast) { + exit(1); + } exitCode = 1; } else { resultLogger.child(successLabel); diff --git a/packages/melos/lib/src/commands/runner.dart b/packages/melos/lib/src/commands/runner.dart index 70da77c04..6f7f7e5c4 100644 --- a/packages/melos/lib/src/commands/runner.dart +++ b/packages/melos/lib/src/commands/runner.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'dart:math'; import 'package:ansi_styles/ansi_styles.dart'; +import 'package:async/async.dart'; import 'package:cli_util/cli_logging.dart'; import 'package:collection/collection.dart'; import 'package:file/local.dart'; @@ -31,8 +32,8 @@ import '../common/pending_package_update.dart'; import '../common/platform.dart'; import '../common/utils.dart' as utils; import '../common/utils.dart'; -import '../common/versioning.dart'; import '../common/versioning.dart' as versioning; +import '../common/versioning.dart'; import '../global_options.dart'; import '../lifecycle_hooks/lifecycle_hooks.dart'; import '../logging.dart'; diff --git a/packages/melos/pubspec.yaml b/packages/melos/pubspec.yaml index bbe9bcf9c..5767a783f 100644 --- a/packages/melos/pubspec.yaml +++ b/packages/melos/pubspec.yaml @@ -17,6 +17,7 @@ executables: dependencies: ansi_styles: ^0.3.2+1 args: ^2.4.2 + async: ^2.11.0 cli_launcher: ^0.3.1 cli_util: ^0.4.1 collection: ^1.18.0 From b6e249644a802cc805b078aba5b1eefec250cc22 Mon Sep 17 00:00:00 2001 From: Muhammad Mohiuddin Date: Sun, 17 Mar 2024 20:40:37 +0100 Subject: [PATCH 05/11] refactor fail fast - add tests --- packages/melos/lib/src/commands/exec.dart | 53 +++++++-- packages/melos/lib/src/common/utils.dart | 8 ++ packages/melos/lib/src/logging.dart | 1 + packages/melos/test/commands/exec_test.dart | 120 +++++++++++++++++++- 4 files changed, 172 insertions(+), 10 deletions(-) diff --git a/packages/melos/lib/src/commands/exec.dart b/packages/melos/lib/src/commands/exec.dart index fcebd2503..e936c8df2 100644 --- a/packages/melos/lib/src/commands/exec.dart +++ b/packages/melos/lib/src/commands/exec.dart @@ -119,10 +119,12 @@ mixin _ExecMixin on _Melos { packages.map((package) => MapEntry(package.name, Completer())), ); - CancelableOperation? operation; + late final CancelableOperation operation; operation = CancelableOperation.fromFuture( pool.forEach(sortedPackages, (package) async { + assert(!(failFast && failures.isNotEmpty)); + if (orderDependents) { final dependenciesResults = await Future.wait( package.allDependenciesInWorkspace.values @@ -135,11 +137,13 @@ mixin _ExecMixin on _Melos { if (dependencyFailed) { packageResults[package.name]?.complete(); failures[package.name] = null; + package.allDependentsInWorkspace.forEach((_, dependent) { + failures[dependent.name] = null; + }); - if (failFast) { - await operation?.cancel(); - operation = null; - } + // cancel in case of dependency failure, irrespective of failFast + // or not + await operation.cancel(); return; } @@ -171,13 +175,12 @@ mixin _ExecMixin on _Melos { } if (packageExitCode > 0 && failFast) { - await operation?.cancel(); - operation = null; + await operation.cancel(); } }).drain(), ); - await operation?.valueOrCancellation(); + await operation.valueOrCancellation(); logger ..horizontalLine() @@ -196,8 +199,40 @@ mixin _ExecMixin on _Melos { 'with exit code ${failures[packageName]})'}', ); } + + final canceled = []; + for (final package in packages) { + if (failures.containsKey(package.name)) { + continue; + } + + if (packageResults.containsKey(package.name)) { + final packageResult = packageResults[package.name]!; + + if (packageResult.isCompleted) { + final exitCode = await packageResult.future; + + if (exitCode == 0) { + continue; + } + } + } + + canceled.add(package.name); + } + + if (canceled.isNotEmpty) { + final canceledLogger = resultLogger + .child('$canceledLabel (in ${canceled.length} packages)'); + for (final packageName in canceled) { + canceledLogger.child( + '${errorPackageNameStyle(packageName)} (due to failFast)', + ); + } + } + if (failFast) { - exit(1); + runningPids.forEach(Process.killPid); } exitCode = 1; } else { diff --git a/packages/melos/lib/src/common/utils.dart b/packages/melos/lib/src/common/utils.dart index 217c0342e..5f78bc571 100644 --- a/packages/melos/lib/src/common/utils.dart +++ b/packages/melos/lib/src/common/utils.dart @@ -457,6 +457,10 @@ Future startCommandRaw( ); } +final _runningPids = []; + +List get runningPids => UnmodifiableListView(_runningPids); + Future startCommand( List command, { String? prefix, @@ -479,6 +483,8 @@ Future startCommand( includeParentEnvironment: includeParentEnvironment, ); + _runningPids.add(process.pid); + var stdoutStream = process.stdout; var stderrStream = process.stderr; @@ -534,6 +540,8 @@ Future startCommand( await processStderrCompleter.future; final exitCode = await process.exitCode; + _runningPids.remove(process.pid); + if (onlyOutputOnError && exitCode > 0) { logger.stdout(utf8.decode(processStdout, allowMalformed: true)); logger.stderr(utf8.decode(processStderr, allowMalformed: true)); diff --git a/packages/melos/lib/src/logging.dart b/packages/melos/lib/src/logging.dart index 853e09b7e..6a28eabc6 100644 --- a/packages/melos/lib/src/logging.dart +++ b/packages/melos/lib/src/logging.dart @@ -24,6 +24,7 @@ final successLabel = successLableColor(labelStyle('SUCCESS')); final warningLabel = warningLabelColor(labelStyle('WARNING')); final errorLabel = errorLabelColor(labelStyle('ERROR')); final failedLabel = errorLabelColor(labelStyle('FAILED')); +final canceledLabel = errorLabelColor(labelStyle('CANCELED')); final hintLabel = hintLabelColor(labelStyle('HINT')); final runningLabel = commandLabelColor(labelStyle('RUNNING')); final checkLabel = AnsiStyles.greenBright('✓'); diff --git a/packages/melos/test/commands/exec_test.dart b/packages/melos/test/commands/exec_test.dart index 2e345cabb..e5e1db068 100644 --- a/packages/melos/test/commands/exec_test.dart +++ b/packages/melos/test/commands/exec_test.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:melos/melos.dart'; import 'package:melos/src/common/io.dart'; import 'package:melos/src/common/utils.dart'; @@ -133,6 +135,122 @@ ${'-' * terminalWidth} ); }); + test('fails fast cancels all running processes on first fail', () async { + final workspaceDir = await createTemporaryWorkspace(); + + await createProject( + workspaceDir, + const PubSpec(name: 'a'), + ); + + await createProject( + workspaceDir, + const PubSpec(name: 'b'), + ); + + await createProject( + workspaceDir, + const PubSpec(name: 'c'), + ); + + final logger = TestLogger(); + final config = + await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); + final melos = Melos( + logger: logger, + config: config, + ); + + await melos.exec( + ['exit', '1'], + concurrency: 2, + orderDependents: true, + failFast: true, + ); + + expect( + logger.output.normalizeNewLines(), + ignoringAnsii( + ''' +\$ melos exec + └> exit 1 + └> RUNNING (in 3 packages) + +${'-' * terminalWidth} +${'-' * terminalWidth} + +\$ melos exec + └> exit 1 + └> FAILED (in 1 packages) + └> c (with exit code 1) + └> CANCELED (in 2 packages) + └> a (due to failFast) + └> b (due to failFast) +''', + ), + ); + }); + + test('all processes fail fail fast is not enabled', () async { + final workspaceDir = await createTemporaryWorkspace(); + + await createProject( + workspaceDir, + PubSpec( + name: 'a', + dependencies: {'c': HostedReference(VersionConstraint.any)}, + ), + ); + + await createProject( + workspaceDir, + const PubSpec(name: 'b'), + ); + + await createProject( + workspaceDir, + PubSpec( + name: 'c', + dependencies: {'b': HostedReference(VersionConstraint.any)}, + ), + ); + + final logger = TestLogger(); + final config = + await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); + final melos = Melos( + logger: logger, + config: config, + ); + + await melos.exec( + ['exit', '1'], + concurrency: 3, + orderDependents: true, + ); + + expect( + logger.output.normalizeNewLines(), + ignoringAnsii( + ''' +\$ melos exec + └> exit 1 + └> RUNNING (in 3 packages) + +${'-' * terminalWidth} +${'-' * terminalWidth} + +\$ melos exec + └> exit 1 + └> FAILED (in 3 packages) + └> b (with exit code 1) + └> c (dependency failed) + └> a (dependency failed) +''', + ), + ); + }); + test('fails fast if dependencies fail', () async { final workspaceDir = await createTemporaryWorkspace(); @@ -167,7 +285,7 @@ ${'-' * terminalWidth} await melos.exec( ['exit', '1'], - concurrency: 2, + concurrency: 3, orderDependents: true, ); From 8c496bf75a7a50d672b3c8de2ddb2164cc8bfe0f Mon Sep 17 00:00:00 2001 From: Muhammad Mohiuddin Date: Sun, 17 Mar 2024 20:40:56 +0100 Subject: [PATCH 06/11] remove unused import --- packages/melos/test/commands/exec_test.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/melos/test/commands/exec_test.dart b/packages/melos/test/commands/exec_test.dart index e5e1db068..fcf117177 100644 --- a/packages/melos/test/commands/exec_test.dart +++ b/packages/melos/test/commands/exec_test.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:melos/melos.dart'; import 'package:melos/src/common/io.dart'; import 'package:melos/src/common/utils.dart'; From 9c12e98cca4a311388b33665787112bc5fe34905 Mon Sep 17 00:00:00 2001 From: Muhammad Mohiuddin Date: Sun, 17 Mar 2024 20:43:21 +0100 Subject: [PATCH 07/11] format files --- packages/melos/test/commands/exec_test.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/melos/test/commands/exec_test.dart b/packages/melos/test/commands/exec_test.dart index fcf117177..e001bfae4 100644 --- a/packages/melos/test/commands/exec_test.dart +++ b/packages/melos/test/commands/exec_test.dart @@ -214,8 +214,9 @@ ${'-' * terminalWidth} ); final logger = TestLogger(); - final config = - await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); + final config = await MelosWorkspaceConfig.fromWorkspaceRoot( + workspaceDir, + ); final melos = Melos( logger: logger, config: config, From 979891b6bd5cefb11fcc813f01853c8df37e785f Mon Sep 17 00:00:00 2001 From: Muhammad Mohiuddin Date: Sun, 17 Mar 2024 21:01:45 +0100 Subject: [PATCH 08/11] fix typo --- packages/melos/test/commands/exec_test.dart | 86 +++++++++++---------- 1 file changed, 44 insertions(+), 42 deletions(-) diff --git a/packages/melos/test/commands/exec_test.dart b/packages/melos/test/commands/exec_test.dart index e001bfae4..b2d40c61f 100644 --- a/packages/melos/test/commands/exec_test.dart +++ b/packages/melos/test/commands/exec_test.dart @@ -72,16 +72,13 @@ ${'-' * terminalWidth} ); }); - group('order dependents', () { - test('sorts execution order topologically', () async { + group('concurrent processes', () { + test('get cancel on first fail when fail fast is enabled', () async { final workspaceDir = await createTemporaryWorkspace(); await createProject( workspaceDir, - PubSpec( - name: 'a', - dependencies: {'c': HostedReference(VersionConstraint.any)}, - ), + const PubSpec(name: 'a'), ); await createProject( @@ -91,24 +88,22 @@ ${'-' * terminalWidth} await createProject( workspaceDir, - PubSpec( - name: 'c', - dependencies: {'b': HostedReference(VersionConstraint.any)}, - ), + const PubSpec(name: 'c'), ); final logger = TestLogger(); final config = - await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); + await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final melos = Melos( logger: logger, config: config, ); await melos.exec( - ['echo', 'hello', 'world'], + ['exit', '1'], concurrency: 2, orderDependents: true, + failFast: true, ); expect( @@ -116,29 +111,33 @@ ${'-' * terminalWidth} ignoringAnsii( ''' \$ melos exec - └> echo hello world + └> exit 1 └> RUNNING (in 3 packages) ${'-' * terminalWidth} -[b]: hello world -[c]: hello world -[a]: hello world ${'-' * terminalWidth} \$ melos exec - └> echo hello world - └> SUCCESS + └> exit 1 + └> FAILED (in 1 packages) + └> c (with exit code 1) + └> CANCELED (in 2 packages) + └> a (due to failFast) + └> b (due to failFast) ''', ), ); }); - test('fails fast cancels all running processes on first fail', () async { + test('keep running when fail fast is not enabled', () async { final workspaceDir = await createTemporaryWorkspace(); await createProject( workspaceDir, - const PubSpec(name: 'a'), + PubSpec( + name: 'a', + dependencies: {'c': HostedReference(VersionConstraint.any)}, + ), ); await createProject( @@ -148,12 +147,16 @@ ${'-' * terminalWidth} await createProject( workspaceDir, - const PubSpec(name: 'c'), + PubSpec( + name: 'c', + dependencies: {'b': HostedReference(VersionConstraint.any)}, + ), ); final logger = TestLogger(); - final config = - await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); + final config = await MelosWorkspaceConfig.fromWorkspaceRoot( + workspaceDir, + ); final melos = Melos( logger: logger, config: config, @@ -161,9 +164,8 @@ ${'-' * terminalWidth} await melos.exec( ['exit', '1'], - concurrency: 2, + concurrency: 3, orderDependents: true, - failFast: true, ); expect( @@ -179,17 +181,18 @@ ${'-' * terminalWidth} \$ melos exec └> exit 1 - └> FAILED (in 1 packages) - └> c (with exit code 1) - └> CANCELED (in 2 packages) - └> a (due to failFast) - └> b (due to failFast) + └> FAILED (in 3 packages) + └> b (with exit code 1) + └> c (dependency failed) + └> a (dependency failed) ''', ), ); }); + }); - test('all processes fail fail fast is not enabled', () async { + group('order dependents', () { + test('sorts execution order topologically', () async { final workspaceDir = await createTemporaryWorkspace(); await createProject( @@ -214,17 +217,16 @@ ${'-' * terminalWidth} ); final logger = TestLogger(); - final config = await MelosWorkspaceConfig.fromWorkspaceRoot( - workspaceDir, - ); + final config = + await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final melos = Melos( logger: logger, config: config, ); await melos.exec( - ['exit', '1'], - concurrency: 3, + ['echo', 'hello', 'world'], + concurrency: 2, orderDependents: true, ); @@ -233,18 +235,18 @@ ${'-' * terminalWidth} ignoringAnsii( ''' \$ melos exec - └> exit 1 + └> echo hello world └> RUNNING (in 3 packages) ${'-' * terminalWidth} +[b]: hello world +[c]: hello world +[a]: hello world ${'-' * terminalWidth} \$ melos exec - └> exit 1 - └> FAILED (in 3 packages) - └> b (with exit code 1) - └> c (dependency failed) - └> a (dependency failed) + └> echo hello world + └> SUCCESS ''', ), ); From 1ab3177051b704a199f7db8b1e86e91acc0c9c08 Mon Sep 17 00:00:00 2001 From: Muhammad Mohiuddin Date: Sun, 17 Mar 2024 21:43:34 +0100 Subject: [PATCH 09/11] add delayed exit to make failure order predictable --- packages/melos/test/commands/exec_test.dart | 73 ++++++++++++++------- 1 file changed, 48 insertions(+), 25 deletions(-) diff --git a/packages/melos/test/commands/exec_test.dart b/packages/melos/test/commands/exec_test.dart index b2d40c61f..34c288bdf 100644 --- a/packages/melos/test/commands/exec_test.dart +++ b/packages/melos/test/commands/exec_test.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:melos/melos.dart'; import 'package:melos/src/common/io.dart'; import 'package:melos/src/common/utils.dart'; @@ -73,35 +75,57 @@ ${'-' * terminalWidth} }); group('concurrent processes', () { + /// Use this file instead of running "exit 1" so the failure + /// order is more predictable + void createDelayedExitFile( + Directory dir, { + int delay = 0, + int exitCode = 1, + }) { + File('${dir.path}/delayed_exit.dart').writeAsStringSync(''' + import 'dart:io'; + Future main() async { + await Future.delayed(Duration(milliseconds: $delay)); + exit($exitCode); + } + '''); + } + test('get cancel on first fail when fail fast is enabled', () async { final workspaceDir = await createTemporaryWorkspace(); - await createProject( + final a = await createProject( workspaceDir, const PubSpec(name: 'a'), ); - await createProject( + createDelayedExitFile(a, delay: 1000); + + final b = await createProject( workspaceDir, const PubSpec(name: 'b'), ); - await createProject( + createDelayedExitFile(b, delay: 500); + + final c = await createProject( workspaceDir, const PubSpec(name: 'c'), ); + createDelayedExitFile(c); final logger = TestLogger(); - final config = - await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); + final config = await MelosWorkspaceConfig.fromWorkspaceRoot( + workspaceDir, + ); final melos = Melos( logger: logger, config: config, ); await melos.exec( - ['exit', '1'], - concurrency: 2, + ['dart', 'delayed_exit.dart'], + concurrency: 3, orderDependents: true, failFast: true, ); @@ -111,14 +135,14 @@ ${'-' * terminalWidth} ignoringAnsii( ''' \$ melos exec - └> exit 1 + └> dart delayed_exit.dart └> RUNNING (in 3 packages) ${'-' * terminalWidth} ${'-' * terminalWidth} \$ melos exec - └> exit 1 + └> dart delayed_exit.dart └> FAILED (in 1 packages) └> c (with exit code 1) └> CANCELED (in 2 packages) @@ -132,26 +156,25 @@ ${'-' * terminalWidth} test('keep running when fail fast is not enabled', () async { final workspaceDir = await createTemporaryWorkspace(); - await createProject( + final a = await createProject( workspaceDir, - PubSpec( - name: 'a', - dependencies: {'c': HostedReference(VersionConstraint.any)}, - ), + const PubSpec(name: 'a'), ); - await createProject( + createDelayedExitFile(a, delay: 1000); + + final b = await createProject( workspaceDir, const PubSpec(name: 'b'), ); - await createProject( + createDelayedExitFile(b, delay: 500); + + final c = await createProject( workspaceDir, - PubSpec( - name: 'c', - dependencies: {'b': HostedReference(VersionConstraint.any)}, - ), + const PubSpec(name: 'c'), ); + createDelayedExitFile(c); final logger = TestLogger(); final config = await MelosWorkspaceConfig.fromWorkspaceRoot( @@ -163,7 +186,7 @@ ${'-' * terminalWidth} ); await melos.exec( - ['exit', '1'], + ['dart', 'delayed_exit.dart'], concurrency: 3, orderDependents: true, ); @@ -173,18 +196,18 @@ ${'-' * terminalWidth} ignoringAnsii( ''' \$ melos exec - └> exit 1 + └> dart delayed_exit.dart └> RUNNING (in 3 packages) ${'-' * terminalWidth} ${'-' * terminalWidth} \$ melos exec - └> exit 1 + └> dart delayed_exit.dart └> FAILED (in 3 packages) + └> c (with exit code 1) └> b (with exit code 1) - └> c (dependency failed) - └> a (dependency failed) + └> a (with exit code 1) ''', ), ); From 65c6a48b1dd590cbf7f9fd2d243dc15d3355c15c Mon Sep 17 00:00:00 2001 From: Muhammad Mohiuddin Date: Sun, 17 Mar 2024 21:43:48 +0100 Subject: [PATCH 10/11] refactor --- packages/melos/lib/src/commands/exec.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/melos/lib/src/commands/exec.dart b/packages/melos/lib/src/commands/exec.dart index e936c8df2..0dd0baaa5 100644 --- a/packages/melos/lib/src/commands/exec.dart +++ b/packages/melos/lib/src/commands/exec.dart @@ -182,6 +182,10 @@ mixin _ExecMixin on _Melos { await operation.valueOrCancellation(); + if (failFast) { + runningPids.forEach(Process.killPid); + } + logger ..horizontalLine() ..newLine() @@ -231,9 +235,6 @@ mixin _ExecMixin on _Melos { } } - if (failFast) { - runningPids.forEach(Process.killPid); - } exitCode = 1; } else { resultLogger.child(successLabel); From b3d39ed0fe5181c3024d401183ee9f0e717faf0a Mon Sep 17 00:00:00 2001 From: Muhammad Mohiuddin Date: Mon, 18 Mar 2024 04:04:54 +0100 Subject: [PATCH 11/11] remove useless code --- packages/melos/lib/src/commands/exec.dart | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/melos/lib/src/commands/exec.dart b/packages/melos/lib/src/commands/exec.dart index 0dd0baaa5..3a19e84a4 100644 --- a/packages/melos/lib/src/commands/exec.dart +++ b/packages/melos/lib/src/commands/exec.dart @@ -132,18 +132,12 @@ mixin _ExecMixin on _Melos { .whereNotNull(), ); - final dependencyFailed = dependenciesResults - .any((exitCode) => exitCode == null || exitCode > 0); + final dependencyFailed = dependenciesResults.any( + (exitCode) => exitCode == null || exitCode > 0, + ); if (dependencyFailed) { packageResults[package.name]?.complete(); failures[package.name] = null; - package.allDependentsInWorkspace.forEach((_, dependent) { - failures[dependent.name] = null; - }); - - // cancel in case of dependency failure, irrespective of failFast - // or not - await operation.cancel(); return; }