From b3f3a7bc2f8471dc75a1dbf5d3baacf90fc3c281 Mon Sep 17 00:00:00 2001 From: Konboi Date: Tue, 17 Jun 2025 17:17:54 +0900 Subject: [PATCH 01/29] add method to check enabled fail-fast mode --- launchable/utils/launchable_client.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/launchable/utils/launchable_client.py b/launchable/utils/launchable_client.py index e8774a80f..deb83e6cc 100644 --- a/launchable/utils/launchable_client.py +++ b/launchable/utils/launchable_client.py @@ -30,6 +30,7 @@ def __init__(self, tracking_client: Optional[TrackingClient] = None, base_url: s "Confirm that you set LAUNCHABLE_TOKEN " "(or LAUNCHABLE_ORGANIZATION and LAUNCHABLE_WORKSPACE) environment variable(s)\n" "See https://docs.launchableinc.com/getting-started#setting-your-api-key") + self._workspace_state_cache = {} def request( self, @@ -99,3 +100,17 @@ def print_exception_and_recover(self, e: Exception, warning: Optional[str] = Non if warning: click.echo(click.style(warning, fg=warning_color), err=True) + + def is_fail_fast_mode(self) -> bool: + if 'fail_fast_mode' in self._workspace_state_cache: + return self._workspace_state_cache['fail_fast_mode'] + # TODO: call api and set the result to cache + try: + res = self.request("get", "state") + if res.status_code == 200: + self._workspace_state_cache['fail_fast_mode'] = res.json().get('isFailFastMode', False) + return self._workspace_state_cache['fail_fast_mode'] + except Exception as e: + self.print_exception_and_recover(e, "Failed to check fail-fast mode status") + + return False From 3948b8d30feb349c9e6676c17890e2f96077c02d Mon Sep 17 00:00:00 2001 From: Konboi Date: Tue, 17 Jun 2025 17:40:04 +0900 Subject: [PATCH 02/29] exit when fail fast mode is enabled and configurations is invalid --- launchable/commands/record/build.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/launchable/commands/record/build.py b/launchable/commands/record/build.py index 8f5d6013d..082639b8d 100644 --- a/launchable/commands/record/build.py +++ b/launchable/commands/record/build.py @@ -113,6 +113,11 @@ def build( links: Sequence[Tuple[str, str]], branches: Sequence[str], lineage: str, timestamp: Optional[datetime.datetime]): + tracking_client = TrackingClient(Tracking.Command.RECORD_BUILD, app=ctx.obj) + client = LaunchableClient(app=ctx.obj, tracking_client=tracking_client) + + is_fail_fast_mode = client.is_fail_fast_mode() + if "/" in build_name or "%2f" in build_name.lower(): sys.exit("--name must not contain a slash and an encoded slash") if "%25" in build_name: @@ -266,12 +271,15 @@ def compute_hash_and_branch(ws: List[Workspace]): sys.exit(1) if not ws_by_name.get(kv[0]): - click.echo(click.style( - "Invalid repository name {} in a --branch option. ".format(kv[0]), - fg="yellow"), - err=True) - # TODO: is there any reason this is not an error? for now erring on caution - # sys.exit(1) + message = "Invalid repository name {repo} in a --branch option.\nThe repository “{repo}” is not specified via `--source` or `--commit` option.".format(repo=kv[0]) # noqa: E501 + fg_color = "red" if is_fail_fast_mode else "yellow" + click.echo(click.style(message, fg=fg_color), err=True) + if is_fail_fast_mode: + tracking_client.send_error_event( + event_name=Tracking.ErrorEvent.USER_ERROR, + stack_trace=message, + ) + sys.exit(1) branch_name_map[kv[0]] = kv[1] @@ -324,8 +332,6 @@ def compute_links(): }) return _links - tracking_client = TrackingClient(Tracking.Command.RECORD_BUILD, app=ctx.obj) - client = LaunchableClient(app=ctx.obj, tracking_client=tracking_client) try: payload = { "buildNumber": build_name, From 650afcea9e79881cdb131a6f9ab16331c3d6cf73 Mon Sep 17 00:00:00 2001 From: Konboi Date: Wed, 18 Jun 2025 11:38:43 +0900 Subject: [PATCH 03/29] require `--build` option when issues test session --- launchable/commands/record/session.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/launchable/commands/record/session.py b/launchable/commands/record/session.py index a2bf8f29f..f5e2c80fc 100644 --- a/launchable/commands/record/session.py +++ b/launchable/commands/record/session.py @@ -132,6 +132,22 @@ def session( you should set print_session = False because users don't expect to print session ID to the subset output. """ + tracking_client = TrackingClient(Tracking.Command.RECORD_SESSION, app=ctx.obj) + client = LaunchableClient(app=ctx.obj, tracking_client=tracking_client) + is_fail_fast_mode = client.is_fail_fast_mode() + + if is_fail_fast_mode: + if build_name is None: + click.echo(click.style( + "Your workspace requires the use of the `--build` option to issue a session.", fg='red'), err=True) + if is_no_build: + click.echo( + click.style( + "If you want to import historical data, running `record build` command with the `--timestamp` option.", + fg='red'), + err=True) + sys.exit(1) + if not is_no_build and not build_name: raise click.UsageError("Error: Missing option '--build'") @@ -143,9 +159,6 @@ def session( build_name = NO_BUILD_BUILD_NAME - tracking_client = TrackingClient(Tracking.Command.RECORD_SESSION, app=ctx.obj) - client = LaunchableClient(app=ctx.obj, tracking_client=tracking_client) - if session_name: sub_path = "builds/{}/test_session_names/{}".format(build_name, session_name) try: From 0815a0568e571b5f6642bc0dbc26d33fc7f72523 Mon Sep 17 00:00:00 2001 From: Konboi Date: Wed, 18 Jun 2025 11:56:49 +0900 Subject: [PATCH 04/29] fixed to check other options --- launchable/commands/record/session.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/launchable/commands/record/session.py b/launchable/commands/record/session.py index f5e2c80fc..02ee34a26 100644 --- a/launchable/commands/record/session.py +++ b/launchable/commands/record/session.py @@ -3,7 +3,7 @@ import re import sys from http import HTTPStatus -from typing import Optional, Sequence, Tuple +from typing import List, Optional, Sequence, Tuple import click @@ -137,15 +137,23 @@ def session( is_fail_fast_mode = client.is_fail_fast_mode() if is_fail_fast_mode: + errors: List[str] = [] if build_name is None: - click.echo(click.style( - "Your workspace requires the use of the `--build` option to issue a session.", fg='red'), err=True) + errors.append("Your workspace requires the use of the `--build` option to issue a session.") # noqa: E501 if is_no_build: - click.echo( - click.style( - "If you want to import historical data, running `record build` command with the `--timestamp` option.", - fg='red'), - err=True) + errors.append("If you want to import historical data, running `record build` command with the `--timestamp` option.") # noqa: E501 + + if test_suite is None: + errors.append( + "Your workspace requires the use of the `--test-suite` option to issue a session. Please specify a test suite such as \"unit-test\" or \"e2e\".") # noqa: E501 + + if len(errors) > 0: + msg = "\n".join(map(lambda x: click.style(x, fg='red'), errors)) + tracking_client.send_error_event( + event_name=Tracking.ErrorEvent.USER_ERROR, + stack_trace=msg, + ) + click.echo(msg, err=True) sys.exit(1) if not is_no_build and not build_name: From 277a413488cbba31cc75a2e3a5ea9b76a2975b75 Mon Sep 17 00:00:00 2001 From: Konboi Date: Wed, 18 Jun 2025 13:50:30 +0900 Subject: [PATCH 05/29] Move the command to a dedicated class want to use from fail fast mode validator --- launchable/utils/commands.py | 10 ++++++++++ launchable/utils/tracking.py | 12 +++--------- 2 files changed, 13 insertions(+), 9 deletions(-) create mode 100644 launchable/utils/commands.py diff --git a/launchable/utils/commands.py b/launchable/utils/commands.py new file mode 100644 index 000000000..9503d227c --- /dev/null +++ b/launchable/utils/commands.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class Command(Enum): + VERIFY = 'VERIFY' + RECORD_TESTS = 'RECORD_TESTS' + RECORD_BUILD = 'RECORD_BUILD' + RECORD_SESSION = 'RECORD_SESSION' + SUBSET = 'SUBSET' + COMMIT = 'COMMIT' diff --git a/launchable/utils/tracking.py b/launchable/utils/tracking.py index cc22c5258..8358b23eb 100644 --- a/launchable/utils/tracking.py +++ b/launchable/utils/tracking.py @@ -8,6 +8,8 @@ from launchable.utils.http_client import _HttpClient, _join_paths from launchable.version import __version__ +from .commands import Command + class Tracking: # General events @@ -27,17 +29,9 @@ class ErrorEvent(Enum): INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR' UNEXPECTED_HTTP_STATUS_ERROR = 'UNEXPECTED_HTTP_STATUS_ERROR' - class Command(Enum): - VERIFY = 'VERIFY' - RECORD_TESTS = 'RECORD_TESTS' - RECORD_BUILD = 'RECORD_BUILD' - RECORD_SESSION = 'RECORD_SESSION' - SUBSET = 'SUBSET' - COMMIT = 'COMMIT' - class TrackingClient: - def __init__(self, command: Tracking.Command, base_url: str = "", session: Optional[Session] = None, + def __init__(self, command: Command, base_url: str = "", session: Optional[Session] = None, test_runner: Optional[str] = "", app: Optional[Application] = None): self.http_client = _HttpClient( base_url=base_url, From e30fb7c43aa38f7fab5c5b301c63b69b638a3288 Mon Sep 17 00:00:00 2001 From: Konboi Date: Wed, 18 Jun 2025 14:56:25 +0900 Subject: [PATCH 06/29] introduce fail fast mode validator --- launchable/commands/record/session.py | 12 ++++- launchable/utils/fail_fast_mode_validator.py | 55 ++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 launchable/utils/fail_fast_mode_validator.py diff --git a/launchable/commands/record/session.py b/launchable/commands/record/session.py index 02ee34a26..a8b838f48 100644 --- a/launchable/commands/record/session.py +++ b/launchable/commands/record/session.py @@ -12,6 +12,8 @@ from launchable.utils.tracking import Tracking, TrackingClient from ...utils.click import KEY_VALUE +from ...utils.commands import Command +from ...utils.fail_fast_mode_validator import FailFastModeValidator from ...utils.launchable_client import LaunchableClient from ...utils.no_build import NO_BUILD_BUILD_NAME from ...utils.session import _session_file_path, read_build, write_session @@ -132,10 +134,18 @@ def session( you should set print_session = False because users don't expect to print session ID to the subset output. """ - tracking_client = TrackingClient(Tracking.Command.RECORD_SESSION, app=ctx.obj) + tracking_client = TrackingClient(Command.RECORD_SESSION, app=ctx.obj) client = LaunchableClient(app=ctx.obj, tracking_client=tracking_client) is_fail_fast_mode = client.is_fail_fast_mode() + FailFastModeValidator( + command=Command.RECORD_SESSION, + fail_fast_mode=is_fail_fast_mode, + build=build_name, + is_no_build=is_no_build, + test_suite=test_suite, + ).validate() + if is_fail_fast_mode: errors: List[str] = [] if build_name is None: diff --git a/launchable/utils/fail_fast_mode_validator.py b/launchable/utils/fail_fast_mode_validator.py new file mode 100644 index 000000000..233ec93dc --- /dev/null +++ b/launchable/utils/fail_fast_mode_validator.py @@ -0,0 +1,55 @@ +import sys +from typing import List, Optional + +import click + +from .commands import Command + + +class FailFastModeValidator: + def __init__( + self, + command: Command = None, + fail_fast_mode: bool = False, + build: Optional[str] = None, + is_no_build: bool = False, + test_suite: Optional[str] = None, + ): + self.command = command + self.fail_fast_mode = fail_fast_mode + self.build = build + self.is_no_build = is_no_build + self.test_suite = test_suite + self.errors: List[str] = [] + + # Validate the record session command options + self._validate_record_session() + + def validate(self): + if not self.fail_fast_mode: + return + + if self.command == Command.RECORD_SESSION: + self._validate_record_session() + + def _validate_record_session(self): + """ + Validate the record session command options + """ + if self.build is None: + self.errors.append("Your workspace requires the use of the `--build` option to issue a session.") + if self.is_no_build: + self.errors.append( + "If you want to import historical data, running `record build` command with the `--timestamp` option.") + + if self.test_suite is None: + self.errors.append( + "Your workspace requires the use of the `--test-suite` option to issue a session. Please specify a test suite such as \"unit-test\" or \"e2e\".") # noqa: E501 + + if self.command != "record_session": + return + + if len(self.errors) > 0: + msg = "\n".join(map(lambda x: click.style(x, fg='red'), self.errors)) + click.echo(msg, err=True) + sys.exit(1) From 66a8fa61ca9c662c0787dd3d2d065c6a5b7d5f26 Mon Sep 17 00:00:00 2001 From: Konboi Date: Wed, 18 Jun 2025 15:28:45 +0900 Subject: [PATCH 07/29] introduce fail fast mode validator to the subset command --- launchable/commands/subset.py | 27 +++++++++--- launchable/utils/fail_fast_mode_validator.py | 45 ++++++++++++++++++-- 2 files changed, 62 insertions(+), 10 deletions(-) diff --git a/launchable/commands/subset.py b/launchable/commands/subset.py index a62e5e686..3c94e3c8d 100644 --- a/launchable/commands/subset.py +++ b/launchable/commands/subset.py @@ -17,7 +17,9 @@ from ..app import Application from ..testpath import FilePathNormalizer, TestPath from ..utils.click import DURATION, KEY_VALUE, PERCENTAGE, DurationType, PercentageType, ignorable_error +from ..utils.commands import Command from ..utils.env_keys import REPORT_ERROR_KEY +from ..utils.fail_fast_mode_validator import FailFastModeValidator from ..utils.launchable_client import LaunchableClient from .helper import find_or_create_session from .test_path_writer import TestPathWriter @@ -225,7 +227,24 @@ def subset( test_suite: Optional[str] = None, ): app = context.obj - tracking_client = TrackingClient(Tracking.Command.SUBSET, app=app) + tracking_client = TrackingClient(Command.SUBSET, app=app) + client = LaunchableClient( + test_runner=context.invoked_subcommand, + app=app, + tracking_client=tracking_client) + + is_fail_fast_mode = client.is_fail_fast_mode() + FailFastModeValidator( + command=Command.SUBSET, + fail_fast_mode=is_fail_fast_mode, + session=session, + build=build_name, + flavor=flavor, + is_observation=is_observation, + links=links, + is_no_build=is_no_build, + test_suite=test_suite, + ).validate() if is_observation and is_output_exclusion_rules: msg = ( @@ -270,7 +289,7 @@ def subset( is_no_build = False session_id = None - tracking_client = TrackingClient(Tracking.Command.SUBSET, app=app) + try: if session_name: if not build_name: @@ -537,10 +556,6 @@ def run(self): else: try: test_runner = context.invoked_subcommand - client = LaunchableClient( - test_runner=test_runner, - app=app, - tracking_client=tracking_client) # temporarily extend the timeout because subset API response has become slow # TODO: remove this line when API response return respose diff --git a/launchable/utils/fail_fast_mode_validator.py b/launchable/utils/fail_fast_mode_validator.py index 233ec93dc..2ffdb8bf0 100644 --- a/launchable/utils/fail_fast_mode_validator.py +++ b/launchable/utils/fail_fast_mode_validator.py @@ -1,5 +1,5 @@ import sys -from typing import List, Optional +from typing import List, Optional, Sequence, Tuple import click @@ -14,23 +14,30 @@ def __init__( build: Optional[str] = None, is_no_build: bool = False, test_suite: Optional[str] = None, + session: Optional[str] = None, + links: Sequence[Tuple[str, str]] = (), + is_observation: bool = False, + flavor: Sequence[Tuple[str, str]] = (), ): self.command = command self.fail_fast_mode = fail_fast_mode self.build = build self.is_no_build = is_no_build self.test_suite = test_suite + self.session = session + self.links = links + self.is_observation = is_observation + self.flavor = flavor self.errors: List[str] = [] - # Validate the record session command options - self._validate_record_session() - def validate(self): if not self.fail_fast_mode: return if self.command == Command.RECORD_SESSION: self._validate_record_session() + if self.command == Command.SUBSET: + self._validate_subset() def _validate_record_session(self): """ @@ -49,6 +56,36 @@ def _validate_record_session(self): if self.command != "record_session": return + self._print_errors() + + def _validate_subset(self): + if self.is_no_build: + self.errors.append("Your workspace doesn't support the `--no-build` option in the subset command.") + self.errors.append( + "Please run `launchable record build` command to create a build first, then run `launchable record session` command to create a session.\n") # noqa: E501 + + if self.build: + self.errors.append("Your workspace doesn't support the `--build` option to execute the subset command.") + self.errors.append("Please run `launchable record sessions` command to create a session first.\n") + + if self.session is None: + self.errors.append("Your workspace requires the use of `--session` option to execute the subset command.") + self.errors.append("Please run `launchable record session` command to create a session first.\n") + + if self.test_suite: + self.errors.append("Your workspace doesn't support the `--test-suite` option in the subset command. Please set the option to the `record session` command instead.") # noqa: E501 + + if self.is_observation: + self.errors.append( + "Your workspace doesn't support the `--observation` option in the subset command. Please set the option to the `record session` command instead.") # noqa: E501 + + if len(self.links) > 0: + self.errors.append( + "Your workspace doesn't support the `--link` option in the subset command. Please set the option to the `record session` command instead.") # noqa: E501 + + self._print_errors() + + def _print_errors(self): if len(self.errors) > 0: msg = "\n".join(map(lambda x: click.style(x, fg='red'), self.errors)) click.echo(msg, err=True) From f315a37e5ef40f5193b950736e45d64ca035c1df Mon Sep 17 00:00:00 2001 From: Konboi Date: Wed, 18 Jun 2025 15:48:47 +0900 Subject: [PATCH 08/29] introduce fail fast mode validator to the record tests command --- launchable/commands/record/tests.py | 17 +++++++++++- launchable/utils/fail_fast_mode_validator.py | 27 +++++++++++++++----- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/launchable/commands/record/tests.py b/launchable/commands/record/tests.py index 39b0a396d..ab82bf44c 100644 --- a/launchable/commands/record/tests.py +++ b/launchable/commands/record/tests.py @@ -17,7 +17,9 @@ from ...testpath import FilePathNormalizer, TestPathComponent, unparse_test_path from ...utils.click import DATETIME_WITH_TZ, KEY_VALUE, validate_past_datetime +from ...utils.commands import Command from ...utils.exceptions import InvalidJUnitXMLException +from ...utils.fail_fast_mode_validator import FailFastModeValidator from ...utils.launchable_client import LaunchableClient from ...utils.logger import Logger from ...utils.no_build import NO_BUILD_BUILD_NAME, NO_BUILD_TEST_SESSION_ID @@ -193,9 +195,22 @@ def tests( test_runner = context.invoked_subcommand - tracking_client = TrackingClient(Tracking.Command.RECORD_TESTS, app=context.obj) + tracking_client = TrackingClient(Command.RECORD_TESTS, app=context.obj) client = LaunchableClient(test_runner=test_runner, app=context.obj, tracking_client=tracking_client) + is_fail_fast_mode = client.is_fail_fast_mode() + + FailFastModeValidator( + command=Command.RECORD_TESTS, + fail_fast_mode=is_fail_fast_mode, + session=session, + build=build_name, + flavor=flavor, + links=links, + is_no_build=is_no_build, + test_suite=test_suite, + ).validate() + file_path_normalizer = FilePathNormalizer(base_path, no_base_path_inference=no_base_path_inference) if is_no_build and (read_build() and read_build() != ""): diff --git a/launchable/utils/fail_fast_mode_validator.py b/launchable/utils/fail_fast_mode_validator.py index 2ffdb8bf0..ef991b411 100644 --- a/launchable/utils/fail_fast_mode_validator.py +++ b/launchable/utils/fail_fast_mode_validator.py @@ -38,6 +38,8 @@ def validate(self): self._validate_record_session() if self.command == Command.SUBSET: self._validate_subset() + if self.command == Command.RECORD_TESTS: + self._validate_record_tests() def _validate_record_session(self): """ @@ -58,31 +60,42 @@ def _validate_record_session(self): self._print_errors() - def _validate_subset(self): + def _validate_require_session_option(self, cmd_name: str): if self.is_no_build: - self.errors.append("Your workspace doesn't support the `--no-build` option in the subset command.") + self.errors.append("Your workspace doesn't support the `--no-build` option in the {} command.".format(cmd_name)) self.errors.append( "Please run `launchable record build` command to create a build first, then run `launchable record session` command to create a session.\n") # noqa: E501 if self.build: - self.errors.append("Your workspace doesn't support the `--build` option to execute the subset command.") + self.errors.append("Your workspace doesn't support the `--build` option to execute the {} command.".format(cmd_name)) self.errors.append("Please run `launchable record sessions` command to create a session first.\n") if self.session is None: - self.errors.append("Your workspace requires the use of `--session` option to execute the subset command.") + self.errors.append( + "Your workspace requires the use of `--session` option to execute the {} command.".format(cmd_name)) self.errors.append("Please run `launchable record session` command to create a session first.\n") if self.test_suite: - self.errors.append("Your workspace doesn't support the `--test-suite` option in the subset command. Please set the option to the `record session` command instead.") # noqa: E501 + self.errors.append("Your workspace doesn't support the `--test-suite` option in the {} command. Please set the option to the `record session` command instead.".format(cmd_name)) # noqa: E501 if self.is_observation: self.errors.append( - "Your workspace doesn't support the `--observation` option in the subset command. Please set the option to the `record session` command instead.") # noqa: E501 + "Your workspace doesn't support the `--observation` option in the {} command. Please set the option to the `record session` command instead.".format(cmd_name)) # noqa: E501 + + if len(self.flavor) > 0: + self.errors.append( + "Your workspace doesn't support the `--flavor` option in the {} command. Please set the option to the `record session` command instead.".format(cmd_name)) # noqa: E501 if len(self.links) > 0: self.errors.append( - "Your workspace doesn't support the `--link` option in the subset command. Please set the option to the `record session` command instead.") # noqa: E501 + "Your workspace doesn't support the `--link` option in the {} command. Please set the option to the `record session` command instead.".format(cmd_name)) # noqa: E501 + + def _validate_subset(self): + self._validate_require_session_option("subset") + self._print_errors() + def _validate_record_tests(self): + self._validate_require_session_option("record tests") self._print_errors() def _print_errors(self): From 39232221059f3af8c1ef33f2af3e266829b64fa8 Mon Sep 17 00:00:00 2001 From: Konboi Date: Wed, 18 Jun 2025 16:32:32 +0900 Subject: [PATCH 09/29] fix type error --- launchable/commands/record/build.py | 3 ++- launchable/commands/record/commit.py | 3 ++- launchable/commands/verify.py | 3 ++- launchable/utils/fail_fast_mode_validator.py | 2 +- launchable/utils/launchable_client.py | 2 +- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/launchable/commands/record/build.py b/launchable/commands/record/build.py index 082639b8d..1150f3bd9 100644 --- a/launchable/commands/record/build.py +++ b/launchable/commands/record/build.py @@ -13,6 +13,7 @@ from ...utils import subprocess from ...utils.authentication import get_org_workspace from ...utils.click import DATETIME_WITH_TZ, KEY_VALUE, validate_past_datetime +from ...utils.commands import Command from ...utils.launchable_client import LaunchableClient from ...utils.session import clean_session_files, write_build from .commit import commit @@ -113,7 +114,7 @@ def build( links: Sequence[Tuple[str, str]], branches: Sequence[str], lineage: str, timestamp: Optional[datetime.datetime]): - tracking_client = TrackingClient(Tracking.Command.RECORD_BUILD, app=ctx.obj) + tracking_client = TrackingClient(Command.RECORD_BUILD, app=ctx.obj) client = LaunchableClient(app=ctx.obj, tracking_client=tracking_client) is_fail_fast_mode = client.is_fail_fast_mode() diff --git a/launchable/commands/record/commit.py b/launchable/commands/record/commit.py index 36775a2e9..21554c258 100644 --- a/launchable/commands/record/commit.py +++ b/launchable/commands/record/commit.py @@ -10,6 +10,7 @@ from launchable.utils.tracking import Tracking, TrackingClient from ...app import Application +from ...utils.commands import Command from ...utils.commit_ingester import upload_commits from ...utils.env_keys import COMMIT_TIMEOUT, REPORT_ERROR_KEY from ...utils.git_log_parser import parse_git_log @@ -57,7 +58,7 @@ def commit(ctx, source: str, executable: bool, max_days: int, scrub_pii: bool, i _import_git_log(import_git_log_output, ctx.obj) return - tracking_client = TrackingClient(Tracking.Command.COMMIT, app=ctx.obj) + tracking_client = TrackingClient(Command.COMMIT, app=ctx.obj) client = LaunchableClient(tracking_client=tracking_client, app=ctx.obj) # Commit messages are not collected in the default. diff --git a/launchable/commands/verify.py b/launchable/commands/verify.py index 95db8c567..3af7e37f3 100644 --- a/launchable/commands/verify.py +++ b/launchable/commands/verify.py @@ -12,6 +12,7 @@ from ..utils.authentication import get_org_workspace from ..utils.click import emoji +from ..utils.commands import Command from ..utils.java import get_java_command from ..utils.launchable_client import LaunchableClient from ..version import __version__ as version @@ -61,7 +62,7 @@ def verify(context: click.core.Context): # Click gracefully. org, workspace = get_org_workspace() - tracking_client = TrackingClient(Tracking.Command.VERIFY, app=context.obj) + tracking_client = TrackingClient(Command.VERIFY, app=context.obj) client = LaunchableClient(tracking_client=tracking_client, app=context.obj) java = get_java_command() diff --git a/launchable/utils/fail_fast_mode_validator.py b/launchable/utils/fail_fast_mode_validator.py index ef991b411..917743330 100644 --- a/launchable/utils/fail_fast_mode_validator.py +++ b/launchable/utils/fail_fast_mode_validator.py @@ -9,7 +9,7 @@ class FailFastModeValidator: def __init__( self, - command: Command = None, + command: Command, fail_fast_mode: bool = False, build: Optional[str] = None, is_no_build: bool = False, diff --git a/launchable/utils/launchable_client.py b/launchable/utils/launchable_client.py index deb83e6cc..0f5347814 100644 --- a/launchable/utils/launchable_client.py +++ b/launchable/utils/launchable_client.py @@ -30,7 +30,7 @@ def __init__(self, tracking_client: Optional[TrackingClient] = None, base_url: s "Confirm that you set LAUNCHABLE_TOKEN " "(or LAUNCHABLE_ORGANIZATION and LAUNCHABLE_WORKSPACE) environment variable(s)\n" "See https://docs.launchableinc.com/getting-started#setting-your-api-key") - self._workspace_state_cache = {} + self._workspace_state_cache: dict = {} def request( self, From 513f3f62b581bc18e275d39a08dfd87fb9e311f2 Mon Sep 17 00:00:00 2001 From: Konboi Date: Wed, 18 Jun 2025 17:14:44 +0900 Subject: [PATCH 10/29] fix tests --- tests/cli_test_case.py | 8 ++++++++ tests/commands/record/test_build.py | 20 +++++++++++--------- tests/commands/record/test_session.py | 14 +++++++------- tests/commands/test_api_error.py | 18 +++++++++++++----- tests/commands/test_helper.py | 5 +++-- tests/commands/test_subset.py | 8 ++++---- 6 files changed, 46 insertions(+), 27 deletions(-) diff --git a/tests/cli_test_case.py b/tests/cli_test_case.py index 9fe24536a..e64e0f57d 100644 --- a/tests/cli_test_case.py +++ b/tests/cli_test_case.py @@ -186,6 +186,14 @@ def setUp(self): self.workspace), json={'commitMessage': True}, status=200) + responses.add( + responses.GET, + "{}/intake/organizations/{}/workspaces/{}/state".format( + get_base_url(), + self.organization, + self.workspace), + json={'isFailFastMode': False}, + status=200) def get_test_files_dir(self): file_name = Path(inspect.getfile(self.__class__)) # obtain the file of the concrete type diff --git a/tests/commands/record/test_build.py b/tests/commands/record/test_build.py index bf1a7a4d7..5d6940867 100644 --- a/tests/commands/record/test_build.py +++ b/tests/commands/record/test_build.py @@ -37,7 +37,7 @@ def test_submodule(self, mock_check_output): # Name & Path should both reflect the submodule path self.assertTrue("| ./bar-zot | ./bar-zot | 8bccab48338219e73c3118ad71c8c98fbd32a4be |" in result.stdout, result.stdout) - payload = json.loads(responses.calls[0].request.body.decode()) + payload = json.loads(responses.calls[1].request.body.decode()) self.assert_json_orderless_equal( { "buildNumber": "123", @@ -82,7 +82,7 @@ def test_no_submodule(self, mock_check_output): result = self.cli("record", "build", "--no-commit-collection", "--no-submodules", "--name", self.build_name) self.assert_success(result) - payload = json.loads(responses.calls[0].request.body.decode()) + payload = json.loads(responses.calls[1].request.body.decode()) self.assert_json_orderless_equal( { "buildNumber": "123", @@ -114,7 +114,7 @@ def test_no_git_directory(self): self.cli("record", "build", "--no-commit-collection", "--commit", ".=c50f5de0f06fe16afa4fd1dd615e4903e40b42a2", "--name", self.build_name) - payload = json.loads(responses.calls[0].request.body.decode()) + payload = json.loads(responses.calls[1].request.body.decode()) self.assert_json_orderless_equal( { "buildNumber": "123", @@ -144,7 +144,7 @@ def test_commit_option_and_build_option(self): result = self.cli("record", "build", "--no-commit-collection", "--commit", "A=abc12", "--name", self.build_name) self.assert_success(result) - payload = json.loads(responses.calls[0].request.body.decode()) + payload = json.loads(responses.calls[1].request.body.decode()) self.assert_json_orderless_equal( { "buildNumber": "123", @@ -174,7 +174,7 @@ def test_commit_option_and_build_option(self): self.build_name) self.assert_success(result) - payload = json.loads(responses.calls[0].request.body.decode()) + payload = json.loads(responses.calls[1].request.body.decode()) self.assert_json_orderless_equal( { "buildNumber": "123", @@ -204,7 +204,7 @@ def test_commit_option_and_build_option(self): self.build_name) self.assert_success(result) - payload = json.loads(responses.calls[0].request.body.decode()) + payload = json.loads(responses.calls[1].request.body.decode()) self.assert_json_orderless_equal( { "buildNumber": "123", @@ -220,7 +220,7 @@ def test_commit_option_and_build_option(self): "timestamp": None }, payload) responses.calls.reset() - self.assertIn("Invalid repository name B in a --branch option. ", result.output) + self.assertIn("Invalid repository name B in a --branch option.", result.output) # case multiple --commit options and multiple --branch options result = self.cli( @@ -239,7 +239,7 @@ def test_commit_option_and_build_option(self): self.build_name) self.assert_success(result) - payload = json.loads(responses.calls[0].request.body.decode()) + payload = json.loads(responses.calls[1].request.body.decode()) self.assert_json_orderless_equal( { "buildNumber": "123", @@ -261,6 +261,8 @@ def test_commit_option_and_build_option(self): }, payload) responses.calls.reset() + @responses.activate + @mock.patch.dict(os.environ, {"LAUNCHABLE_TOKEN": CliTestCase.launchable_token}) def test_build_name_validation(self): result = self.cli("record", "build", "--no-commit-collection", "--name", "foo/hoge") self.assert_exit_code(result, 1) @@ -289,7 +291,7 @@ def test_with_timestamp(self, mock_check_output): "2025-01-23 12:34:56Z") self.assert_success(result) - payload = json.loads(responses.calls[0].request.body.decode()) + payload = json.loads(responses.calls[1].request.body.decode()) self.assert_json_orderless_equal( { "buildNumber": "123", diff --git a/tests/commands/record/test_session.py b/tests/commands/record/test_session.py index 014e4a124..ddac44e15 100644 --- a/tests/commands/record/test_session.py +++ b/tests/commands/record/test_session.py @@ -26,7 +26,7 @@ def test_run_session_without_flavor(self): result = self.cli("record", "session", "--build", self.build_name) self.assert_success(result) - payload = json.loads(responses.calls[0].request.body.decode()) + payload = json.loads(responses.calls[1].request.body.decode()) self.assert_json_orderless_equal({ "flavors": {}, "isObservation": False, @@ -47,7 +47,7 @@ def test_run_session_with_flavor(self): "--flavor", "key=value", "--flavor", "k:v", "--flavor", "k e y = v a l u e") self.assert_success(result) - payload = json.loads(responses.calls[0].request.body.decode()) + payload = json.loads(responses.calls[1].request.body.decode()) self.assert_json_orderless_equal({ "flavors": { "key": "value", @@ -75,7 +75,7 @@ def test_run_session_with_observation(self): result = self.cli("record", "session", "--build", self.build_name, "--observation") self.assert_success(result) - payload = json.loads(responses.calls[0].request.body.decode()) + payload = json.loads(responses.calls[1].request.body.decode()) self.assert_json_orderless_equal({ "flavors": {}, @@ -115,7 +115,7 @@ def test_run_session_with_session_name(self): result = self.cli("record", "session", "--build", self.build_name, "--session-name", self.session_name) self.assert_success(result) - payload = json.loads(responses.calls[3].request.body.decode()) + payload = json.loads(responses.calls[5].request.body.decode()) self.assert_json_orderless_equal({ "flavors": {}, "isObservation": False, @@ -136,7 +136,7 @@ def test_run_session_with_lineage(self): "--lineage", "example-lineage") self.assert_success(result) - payload = json.loads(responses.calls[0].request.body.decode()) + payload = json.loads(responses.calls[1].request.body.decode()) self.assert_json_orderless_equal({ "flavors": {}, "isObservation": False, @@ -157,7 +157,7 @@ def test_run_session_with_test_suite(self): "--test-suite", "example-test-suite") self.assert_success(result) - payload = json.loads(responses.calls[0].request.body.decode()) + payload = json.loads(responses.calls[1].request.body.decode()) self.assert_json_orderless_equal({ "flavors": {}, "isObservation": False, @@ -178,7 +178,7 @@ def test_run_session_with_timestamp(self): "--timestamp", "2023-10-01T12:00:00Z") self.assert_success(result) - payload = json.loads(responses.calls[0].request.body.decode()) + payload = json.loads(responses.calls[1].request.body.decode()) self.assert_json_orderless_equal({ "flavors": {}, "isObservation": False, diff --git a/tests/commands/test_api_error.py b/tests/commands/test_api_error.py index 1fd8a55ad..10391c49b 100644 --- a/tests/commands/test_api_error.py +++ b/tests/commands/test_api_error.py @@ -131,7 +131,7 @@ def test_record_build(self): self.assert_success(result) self.assertEqual(result.exception, None) # Since HTTPError is occurred outside of LaunchableClient, the count is 1. - self.assert_tracking_count(tracking=tracking, count=1) + self.assert_tracking_count(tracking=tracking, count=2) success_server.shutdown() thread.join() @@ -165,7 +165,7 @@ def test_record_build(self): self.assert_success(result) self.assertEqual(result.exception, None) # Since HTTPError is occurred outside of LaunchableClient, the count is 1. - self.assert_tracking_count(tracking=tracking, count=1) + self.assert_tracking_count(tracking=tracking, count=2) error_server.shutdown() thread.join() @@ -390,6 +390,14 @@ def test_all_workflow_when_server_down(self): CLI_TRACKING_URL.format( base=get_base_url()), body=ReadTimeout("error")) + # setup state + responses.replace( + responses.GET, + "{base}/intake/organizations/{org}/workspaces/{ws}/state".format( + base=get_base_url(), + org=self.organization, + ws=self.workspace), + body=ReadTimeout("error")) # setup build responses.replace( responses.POST, @@ -429,7 +437,7 @@ def test_all_workflow_when_server_down(self): self.assert_success(result) # Since Timeout error is caught inside of LaunchableClient, the tracking event is sent twice. - self.assert_tracking_count(tracking=tracking, count=4) + self.assert_tracking_count(tracking=tracking, count=5) # set delete=False to solve the error `PermissionError: [Errno 13] Permission denied:` on Windows. with tempfile.NamedTemporaryFile(delete=False) as rest_file: @@ -446,12 +454,12 @@ def test_all_workflow_when_server_down(self): self.assert_success(result) # Since Timeout error is caught inside of LaunchableClient, the tracking event is sent twice. - self.assert_tracking_count(tracking=tracking, count=6) + self.assert_tracking_count(tracking=tracking, count=8) result = self.cli("record", "tests", "--session", self.session, "minitest", str(self.test_files_dir) + "/") self.assert_success(result) # Since Timeout error is caught inside of LaunchableClient, the tracking event is sent twice. - self.assert_tracking_count(tracking=tracking, count=9) + self.assert_tracking_count(tracking=tracking, count=12) def assert_tracking_count(self, tracking, count: int): # Prior to 3.6, `Response` object can't be obtained. diff --git a/tests/commands/test_helper.py b/tests/commands/test_helper.py index 3a3924e69..82a05c22f 100644 --- a/tests/commands/test_helper.py +++ b/tests/commands/test_helper.py @@ -6,8 +6,9 @@ import responses # type: ignore from launchable.commands.helper import _check_observation_mode_status +from launchable.utils.commands import Command from launchable.utils.http_client import get_base_url -from launchable.utils.tracking import Tracking, TrackingClient +from launchable.utils.tracking import TrackingClient from tests.cli_test_case import CliTestCase @@ -21,7 +22,7 @@ def test_check_observation_mode_status(self): self.session_id, ) - tracking_client = TrackingClient(Tracking.Command.RECORD_TESTS) + tracking_client = TrackingClient(Command.RECORD_TESTS) with mock.patch('sys.stderr', new=StringIO()) as stderr: _check_observation_mode_status(test_session, False, tracking_client=tracking_client) diff --git a/tests/commands/test_subset.py b/tests/commands/test_subset.py index 076ca7e49..247ec2c67 100644 --- a/tests/commands/test_subset.py +++ b/tests/commands/test_subset.py @@ -205,7 +205,7 @@ def test_subset_targetless(self): mix_stderr=False) self.assert_success(result) - payload = json.loads(gzip.decompress(responses.calls[0].request.body).decode()) + payload = json.loads(gzip.decompress(responses.calls[1].request.body).decode()) self.assertTrue(payload.get('useServerSideOptimizationTarget')) @responses.activate @@ -238,7 +238,7 @@ def test_subset_goalspec(self): input="test_aaa.py") self.assert_success(result) - payload = json.loads(gzip.decompress(responses.calls[0].request.body).decode()) + payload = json.loads(gzip.decompress(responses.calls[1].request.body).decode()) self.assertEqual(payload.get('goal').get('goal'), "foo(),bar(zot=3%)") @responses.activate @@ -280,7 +280,7 @@ def test_subset_ignore_flaky_tests_above(self): mix_stderr=False) self.assert_success(result) - payload = json.loads(gzip.decompress(responses.calls[0].request.body).decode()) + payload = json.loads(gzip.decompress(responses.calls[1].request.body).decode()) self.assertEqual(payload.get('dropFlakinessThreshold'), 0.05) @responses.activate @@ -545,5 +545,5 @@ def test_subset_prioritize_tests_failed_within_hours(self): mix_stderr=False) self.assert_success(result) - payload = json.loads(gzip.decompress(responses.calls[0].request.body).decode()) + payload = json.loads(gzip.decompress(responses.calls[1].request.body).decode()) self.assertEqual(payload.get('hoursToPrioritizeFailedTest'), 24) From d4bd4498f31dcfbe41b67100122dbc210e322cde Mon Sep 17 00:00:00 2001 From: Konboi Date: Wed, 18 Jun 2025 17:43:45 +0900 Subject: [PATCH 11/29] rm unused logic --- launchable/utils/fail_fast_mode_validator.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/launchable/utils/fail_fast_mode_validator.py b/launchable/utils/fail_fast_mode_validator.py index 917743330..0a602eaee 100644 --- a/launchable/utils/fail_fast_mode_validator.py +++ b/launchable/utils/fail_fast_mode_validator.py @@ -42,9 +42,6 @@ def validate(self): self._validate_record_tests() def _validate_record_session(self): - """ - Validate the record session command options - """ if self.build is None: self.errors.append("Your workspace requires the use of the `--build` option to issue a session.") if self.is_no_build: @@ -55,9 +52,6 @@ def _validate_record_session(self): self.errors.append( "Your workspace requires the use of the `--test-suite` option to issue a session. Please specify a test suite such as \"unit-test\" or \"e2e\".") # noqa: E501 - if self.command != "record_session": - return - self._print_errors() def _validate_require_session_option(self, cmd_name: str): From dd94851c77433b59ab12dbeed1829b65954f93d8 Mon Sep 17 00:00:00 2001 From: Konboi Date: Mon, 23 Jun 2025 15:00:51 +0900 Subject: [PATCH 12/29] if is_fail_fast_mode enabled, stop the process --- launchable/commands/record/commit.py | 15 ++++++++++----- launchable/commands/record/session.py | 22 +--------------------- launchable/commands/record/tests.py | 21 +++++++++++++++++++-- 3 files changed, 30 insertions(+), 28 deletions(-) diff --git a/launchable/commands/record/commit.py b/launchable/commands/record/commit.py index 21554c258..d022115c6 100644 --- a/launchable/commands/record/commit.py +++ b/launchable/commands/record/commit.py @@ -54,12 +54,13 @@ def commit(ctx, source: str, executable: bool, max_days: int, scrub_pii: bool, i if executable == 'docker': sys.exit("--executable docker is no longer supported") - if import_git_log_output: - _import_git_log(import_git_log_output, ctx.obj) - return - tracking_client = TrackingClient(Command.COMMIT, app=ctx.obj) client = LaunchableClient(tracking_client=tracking_client, app=ctx.obj) + is_fail_fast_mode = client.is_fail_fast_mode() + + if import_git_log_output: + _import_git_log(import_git_log_output, ctx.obj, is_fail_fast_mode) + return # Commit messages are not collected in the default. is_collect_message = False @@ -89,6 +90,8 @@ def commit(ctx, source: str, executable: bool, max_days: int, scrub_pii: bool, i fg='yellow'), err=True) print(e) + if is_fail_fast_mode: + sys.exit(1) def exec_jar(source: str, max_days: int, app: Application, is_collect_message: bool): @@ -133,7 +136,7 @@ def exec_jar(source: str, max_days: int, app: Application, is_collect_message: b ) -def _import_git_log(output_file: str, app: Application): +def _import_git_log(output_file: str, app: Application, is_fail_fast_mode: bool = False): try: with click.open_file(output_file) as fp: commits = parse_git_log(fp) @@ -146,6 +149,8 @@ def _import_git_log(output_file: str, app: Application): click.style("Failed to import the git-log output", fg='yellow'), err=True) print(e) + if is_fail_fast_mode: + sys.exit(1) def _build_proxy_option(https_proxy: Optional[str]) -> List[str]: diff --git a/launchable/commands/record/session.py b/launchable/commands/record/session.py index a8b838f48..8cfabed15 100644 --- a/launchable/commands/record/session.py +++ b/launchable/commands/record/session.py @@ -3,7 +3,7 @@ import re import sys from http import HTTPStatus -from typing import List, Optional, Sequence, Tuple +from typing import Optional, Sequence, Tuple import click @@ -146,26 +146,6 @@ def session( test_suite=test_suite, ).validate() - if is_fail_fast_mode: - errors: List[str] = [] - if build_name is None: - errors.append("Your workspace requires the use of the `--build` option to issue a session.") # noqa: E501 - if is_no_build: - errors.append("If you want to import historical data, running `record build` command with the `--timestamp` option.") # noqa: E501 - - if test_suite is None: - errors.append( - "Your workspace requires the use of the `--test-suite` option to issue a session. Please specify a test suite such as \"unit-test\" or \"e2e\".") # noqa: E501 - - if len(errors) > 0: - msg = "\n".join(map(lambda x: click.style(x, fg='red'), errors)) - tracking_client.send_error_event( - event_name=Tracking.ErrorEvent.USER_ERROR, - stack_trace=msg, - ) - click.echo(msg, err=True) - sys.exit(1) - if not is_no_build and not build_name: raise click.UsageError("Error: Missing option '--build'") diff --git a/launchable/commands/record/tests.py b/launchable/commands/record/tests.py index ab82bf44c..b2ee777d8 100644 --- a/launchable/commands/record/tests.py +++ b/launchable/commands/record/tests.py @@ -2,6 +2,7 @@ import glob import os import re +import sys import xml.etree.ElementTree as ET from http import HTTPStatus from typing import Callable, Dict, Generator, List, Optional, Sequence, Tuple, Union @@ -229,6 +230,9 @@ def tests( "WARNING: `--session` and `--no-build` are set.\nUsing --session option value ({}) and ignoring `--no-build` option".format(session), # noqa: E501 fg='yellow'), err=True) + if is_fail_fast_mode: + sys.exit(1) + is_no_build = False try: @@ -370,11 +374,14 @@ def parse(report: str) -> Generator[CaseEventType, None, None]: try: xml = JUnitXml.fromfile(report, f) except Exception as e: - click.echo(click.style("Warning: error reading JUnitXml file {filename}: {error}".format( - filename=report, error=e), fg="yellow"), err=True) # `JUnitXml.fromfile()` will raise `JUnitXmlError` and other lxml related errors # if the file has wrong format. # https://github.com/weiwei/junitparser/blob/master/junitparser/junitparser.py#L321 + click.echo(click.style("Warning: error reading JUnitXml file {filename}: {error}".format( + filename=report, error=e), fg="yellow"), err=True) + if is_fail_fast_mode: + sys.exit(1) + return if isinstance(xml, JUnitXml): testsuites = [suite for suite in xml] @@ -390,6 +397,8 @@ def parse(report: str) -> Generator[CaseEventType, None, None]: except Exception as e: click.echo(click.style("Warning: error parsing JUnitXml file {filename}: {error}".format( filename=report, error=e), fg="yellow"), err=True) + if is_fail_fast_mode: + sys.exit(1) self.parse_func = parse @@ -528,6 +537,8 @@ def send(payload: Dict[str, Union[str, List]]) -> None: build), 'yellow'), err=True) + if is_fail_fast_mode: + sys.exit(1) elif build_name: click.echo( click.style( @@ -538,6 +549,8 @@ def send(payload: Dict[str, Union[str, List]]) -> None: build_name), 'yellow'), err=True) + if is_fail_fast_mode: + sys.exit(1) res.raise_for_status() @@ -626,6 +639,8 @@ def recorded_result() -> Tuple[int, int, int, float]: "Make sure to run your tests after you run `launchable record build`.\n" "Otherwise, if these are really correct test reports, use the `--allow-test-before-build` option.".format( len(self.skipped_reports)), 'yellow')) + if is_fail_fast_mode: + sys.exit(1) return else: click.echo( @@ -633,6 +648,8 @@ def recorded_result() -> Tuple[int, int, int, float]: "Looks like tests didn't run? " "If not, make sure the right files/directories were passed into `launchable record tests`", 'yellow')) + if is_fail_fast_mode: + sys.exit(1) return file_count = len(self.reports) From 5949df92d4fb7e168d24ee4a745c49daa0d308ff Mon Sep 17 00:00:00 2001 From: Konboi Date: Mon, 23 Jun 2025 15:40:15 +0900 Subject: [PATCH 13/29] fix validation --- launchable/utils/fail_fast_mode_validator.py | 47 +++++--------------- 1 file changed, 12 insertions(+), 35 deletions(-) diff --git a/launchable/utils/fail_fast_mode_validator.py b/launchable/utils/fail_fast_mode_validator.py index 0a602eaee..acb636fa5 100644 --- a/launchable/utils/fail_fast_mode_validator.py +++ b/launchable/utils/fail_fast_mode_validator.py @@ -42,47 +42,24 @@ def validate(self): self._validate_record_tests() def _validate_record_session(self): - if self.build is None: - self.errors.append("Your workspace requires the use of the `--build` option to issue a session.") - if self.is_no_build: - self.errors.append( - "If you want to import historical data, running `record build` command with the `--timestamp` option.") - - if self.test_suite is None: - self.errors.append( - "Your workspace requires the use of the `--test-suite` option to issue a session. Please specify a test suite such as \"unit-test\" or \"e2e\".") # noqa: E501 - self._print_errors() def _validate_require_session_option(self, cmd_name: str): - if self.is_no_build: - self.errors.append("Your workspace doesn't support the `--no-build` option in the {} command.".format(cmd_name)) - self.errors.append( - "Please run `launchable record build` command to create a build first, then run `launchable record session` command to create a session.\n") # noqa: E501 - - if self.build: - self.errors.append("Your workspace doesn't support the `--build` option to execute the {} command.".format(cmd_name)) - self.errors.append("Please run `launchable record sessions` command to create a session first.\n") - - if self.session is None: - self.errors.append( - "Your workspace requires the use of `--session` option to execute the {} command.".format(cmd_name)) - self.errors.append("Please run `launchable record session` command to create a session first.\n") + if self.session: + if self.test_suite: + self.errors.append("`--test-suite` option was ignored in the {} command. Add `--test-suite` option to the `record session` command instead.".format(cmd_name)) # noqa: E501 - if self.test_suite: - self.errors.append("Your workspace doesn't support the `--test-suite` option in the {} command. Please set the option to the `record session` command instead.".format(cmd_name)) # noqa: E501 - - if self.is_observation: - self.errors.append( - "Your workspace doesn't support the `--observation` option in the {} command. Please set the option to the `record session` command instead.".format(cmd_name)) # noqa: E501 + if self.is_observation: + self.errors.append( + "`--observation` was ignored in the {} command. Add `--observation` option to the `record session` command instead.".format(cmd_name)) # noqa: E501 - if len(self.flavor) > 0: - self.errors.append( - "Your workspace doesn't support the `--flavor` option in the {} command. Please set the option to the `record session` command instead.".format(cmd_name)) # noqa: E501 + if len(self.flavor) > 0: + self.errors.append( + "`--flavor` option was ignored in the {} command. Add `--flavor` option to the `record session` command instead.".format(cmd_name)) # noqa: E501 - if len(self.links) > 0: - self.errors.append( - "Your workspace doesn't support the `--link` option in the {} command. Please set the option to the `record session` command instead.".format(cmd_name)) # noqa: E501 + if len(self.links) > 0: + self.errors.append( + "`--link` option was ignored in the {} command. Add `link` option to the `record session` command instead.".format(cmd_name)) # noqa: E501 def _validate_subset(self): self._validate_require_session_option("subset") From 676b7067ec5094f71d39d0c980af439bfd657e76 Mon Sep 17 00:00:00 2001 From: Konboi Date: Mon, 23 Jun 2025 15:47:21 +0900 Subject: [PATCH 14/29] stop process if fail fast mode is enabled --- launchable/commands/subset.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/launchable/commands/subset.py b/launchable/commands/subset.py index 3c94e3c8d..ee4b09c24 100644 --- a/launchable/commands/subset.py +++ b/launchable/commands/subset.py @@ -286,6 +286,8 @@ def subset( "WARNING: `--session` and `--no-build` are set.\nUsing --session option value ({}) and ignoring `--no-build` option".format(session), # noqa: E501 fg='yellow'), err=True) + if is_fail_fast_mode: + sys.exit(1) is_no_build = False session_id = None @@ -435,6 +437,8 @@ def stdin(self) -> Union[TextIO, List]: "Did you forget to pipe from another command?", fg='yellow'), err=True) + if is_fail_fast_mode: + sys.exit(1) return sys.stdin @staticmethod @@ -605,6 +609,8 @@ def run(self): if len(original_subset) == 0: click.echo(click.style("Error: no tests found matching the path.", 'yellow'), err=True) + if is_fail_fast_mode: + sys.exit(1) return if split: From 6d394f5fdb9681f880746b67bd547c221db7de03 Mon Sep 17 00:00:00 2001 From: Konboi Date: Tue, 24 Jun 2025 17:30:12 +0900 Subject: [PATCH 15/29] add total api calling count, since added calling workspace state endpoint --- tests/commands/test_api_error.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/commands/test_api_error.py b/tests/commands/test_api_error.py index 10391c49b..97fdff4ee 100644 --- a/tests/commands/test_api_error.py +++ b/tests/commands/test_api_error.py @@ -131,7 +131,7 @@ def test_record_build(self): self.assert_success(result) self.assertEqual(result.exception, None) # Since HTTPError is occurred outside of LaunchableClient, the count is 1. - self.assert_tracking_count(tracking=tracking, count=2) + self.assert_tracking_count(tracking=tracking, count=3) success_server.shutdown() thread.join() @@ -165,7 +165,7 @@ def test_record_build(self): self.assert_success(result) self.assertEqual(result.exception, None) # Since HTTPError is occurred outside of LaunchableClient, the count is 1. - self.assert_tracking_count(tracking=tracking, count=2) + self.assert_tracking_count(tracking=tracking, count=3) error_server.shutdown() thread.join() @@ -437,7 +437,7 @@ def test_all_workflow_when_server_down(self): self.assert_success(result) # Since Timeout error is caught inside of LaunchableClient, the tracking event is sent twice. - self.assert_tracking_count(tracking=tracking, count=5) + self.assert_tracking_count(tracking=tracking, count=6) # set delete=False to solve the error `PermissionError: [Errno 13] Permission denied:` on Windows. with tempfile.NamedTemporaryFile(delete=False) as rest_file: @@ -454,12 +454,12 @@ def test_all_workflow_when_server_down(self): self.assert_success(result) # Since Timeout error is caught inside of LaunchableClient, the tracking event is sent twice. - self.assert_tracking_count(tracking=tracking, count=8) + self.assert_tracking_count(tracking=tracking, count=9) result = self.cli("record", "tests", "--session", self.session, "minitest", str(self.test_files_dir) + "/") self.assert_success(result) # Since Timeout error is caught inside of LaunchableClient, the tracking event is sent twice. - self.assert_tracking_count(tracking=tracking, count=12) + self.assert_tracking_count(tracking=tracking, count=13) def assert_tracking_count(self, tracking, count: int): # Prior to 3.6, `Response` object can't be obtained. From da401a9a905bebd6f17d488cc620a63910208a9c Mon Sep 17 00:00:00 2001 From: Konboi Date: Wed, 25 Jun 2025 09:24:19 +0900 Subject: [PATCH 16/29] defined utility methods for fail fast mode --- ...st_mode_validator.py => fail_fast_mode.py} | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) rename launchable/utils/{fail_fast_mode_validator.py => fail_fast_mode.py} (81%) diff --git a/launchable/utils/fail_fast_mode_validator.py b/launchable/utils/fail_fast_mode.py similarity index 81% rename from launchable/utils/fail_fast_mode_validator.py rename to launchable/utils/fail_fast_mode.py index acb636fa5..3ac31eb6b 100644 --- a/launchable/utils/fail_fast_mode_validator.py +++ b/launchable/utils/fail_fast_mode.py @@ -5,12 +5,36 @@ from .commands import Command +_fail_fast_mode_cache: bool = None + + +def set_fail_fast_mode(enabled: bool): + global _fail_fast_mode_cache + _fail_fast_mode_cache = enabled + + +def is_fail_fast_mode() -> bool: + global _fail_fast_mode_cache + if _fail_fast_mode_cache: + return _fail_fast_mode_cache + + # Default to False if not set + return False + + +def warning_and_exit_if_fail_fast_mode(message: str): + color = 'yellow' if is_fail_fast_mode() else 'red' + message = click.style(message, fg=color) + + click.echo(message, err=True) + if is_fail_fast_mode(): + sys.exit(1) + class FailFastModeValidator: def __init__( self, command: Command, - fail_fast_mode: bool = False, build: Optional[str] = None, is_no_build: bool = False, test_suite: Optional[str] = None, @@ -20,7 +44,6 @@ def __init__( flavor: Sequence[Tuple[str, str]] = (), ): self.command = command - self.fail_fast_mode = fail_fast_mode self.build = build self.is_no_build = is_no_build self.test_suite = test_suite @@ -31,7 +54,7 @@ def __init__( self.errors: List[str] = [] def validate(self): - if not self.fail_fast_mode: + if not is_fail_fast_mode(): return if self.command == Command.RECORD_SESSION: From 5891fa1de9a1301793c285df40a3d0b2b0cd588d Mon Sep 17 00:00:00 2001 From: Konboi Date: Wed, 25 Jun 2025 11:48:47 +0900 Subject: [PATCH 17/29] use util method --- launchable/commands/subset.py | 30 +++++++++--------------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/launchable/commands/subset.py b/launchable/commands/subset.py index ee4b09c24..1a5568d0b 100644 --- a/launchable/commands/subset.py +++ b/launchable/commands/subset.py @@ -19,7 +19,7 @@ from ..utils.click import DURATION, KEY_VALUE, PERCENTAGE, DurationType, PercentageType, ignorable_error from ..utils.commands import Command from ..utils.env_keys import REPORT_ERROR_KEY -from ..utils.fail_fast_mode_validator import FailFastModeValidator +from ..utils.fail_fast_mode import FailFastModeValidator, set_fail_fast_mode, warning_and_exit_if_fail_fast_mode from ..utils.launchable_client import LaunchableClient from .helper import find_or_create_session from .test_path_writer import TestPathWriter @@ -233,10 +233,9 @@ def subset( app=app, tracking_client=tracking_client) - is_fail_fast_mode = client.is_fail_fast_mode() + set_fail_fast_mode(client.is_fail_fast_mode()) FailFastModeValidator( command=Command.SUBSET, - fail_fast_mode=is_fail_fast_mode, session=session, build=build_name, flavor=flavor, @@ -281,13 +280,8 @@ def subset( sys.exit(1) if is_no_build and session: - click.echo( - click.style( - "WARNING: `--session` and `--no-build` are set.\nUsing --session option value ({}) and ignoring `--no-build` option".format(session), # noqa: E501 - fg='yellow'), - err=True) - if is_fail_fast_mode: - sys.exit(1) + warning_and_exit_if_fail_fast_mode( + "WARNING: `--session` and `--no-build` are set.\nUsing --session option value ({}) and ignoring `--no-build` option".format(session)) # noqa: E501 is_no_build = False session_id = None @@ -431,14 +425,10 @@ def stdin(self) -> Union[TextIO, List]: they didn't feed anything from stdin """ if sys.stdin.isatty(): - click.echo( - click.style( - "Warning: this command reads from stdin but it doesn't appear to be connected to anything. " - "Did you forget to pipe from another command?", - fg='yellow'), - err=True) - if is_fail_fast_mode: - sys.exit(1) + warning_and_exit_if_fail_fast_mode( + "Warning: this command reads from stdin but it doesn't appear to be connected to anything. " + "Did you forget to pipe from another command?" + ) return sys.stdin @staticmethod @@ -608,9 +598,7 @@ def run(self): e, "Warning: the service failed to subset. Falling back to running all tests") if len(original_subset) == 0: - click.echo(click.style("Error: no tests found matching the path.", 'yellow'), err=True) - if is_fail_fast_mode: - sys.exit(1) + warning_and_exit_if_fail_fast_mode("Error: no tests found matching the path.", 'yellow') return if split: From 49051276c57a83d9f4136182272224c3feca573e Mon Sep 17 00:00:00 2001 From: Konboi Date: Wed, 25 Jun 2025 14:50:56 +0900 Subject: [PATCH 18/29] fix tests command --- launchable/commands/record/tests.py | 76 ++++++++--------------------- 1 file changed, 21 insertions(+), 55 deletions(-) diff --git a/launchable/commands/record/tests.py b/launchable/commands/record/tests.py index b2ee777d8..9dc40b51d 100644 --- a/launchable/commands/record/tests.py +++ b/launchable/commands/record/tests.py @@ -2,7 +2,6 @@ import glob import os import re -import sys import xml.etree.ElementTree as ET from http import HTTPStatus from typing import Callable, Dict, Generator, List, Optional, Sequence, Tuple, Union @@ -20,7 +19,7 @@ from ...utils.click import DATETIME_WITH_TZ, KEY_VALUE, validate_past_datetime from ...utils.commands import Command from ...utils.exceptions import InvalidJUnitXMLException -from ...utils.fail_fast_mode_validator import FailFastModeValidator +from ...utils.fail_fast_mode import FailFastModeValidator, set_fail_fast_mode, warning_and_exit_if_fail_fast_mode from ...utils.launchable_client import LaunchableClient from ...utils.logger import Logger from ...utils.no_build import NO_BUILD_BUILD_NAME, NO_BUILD_TEST_SESSION_ID @@ -198,12 +197,10 @@ def tests( tracking_client = TrackingClient(Command.RECORD_TESTS, app=context.obj) client = LaunchableClient(test_runner=test_runner, app=context.obj, tracking_client=tracking_client) - - is_fail_fast_mode = client.is_fail_fast_mode() + set_fail_fast_mode(client.is_fail_fast_mode()) FailFastModeValidator( command=Command.RECORD_TESTS, - fail_fast_mode=is_fail_fast_mode, session=session, build=build_name, flavor=flavor, @@ -225,13 +222,9 @@ def tests( raise click.UsageError(message=msg) # noqa: E501 if is_no_build and session: - click.echo( - click.style( - "WARNING: `--session` and `--no-build` are set.\nUsing --session option value ({}) and ignoring `--no-build` option".format(session), # noqa: E501 - fg='yellow'), - err=True) - if is_fail_fast_mode: - sys.exit(1) + warning_and_exit_if_fail_fast_mode( + "WARNING: `--session` and `--no-build` are set.\nUsing --session option value ({}) and ignoring `--no-build` option".format(session), # noqa: E501 + ) is_no_build = False @@ -377,11 +370,9 @@ def parse(report: str) -> Generator[CaseEventType, None, None]: # `JUnitXml.fromfile()` will raise `JUnitXmlError` and other lxml related errors # if the file has wrong format. # https://github.com/weiwei/junitparser/blob/master/junitparser/junitparser.py#L321 - click.echo(click.style("Warning: error reading JUnitXml file {filename}: {error}".format( - filename=report, error=e), fg="yellow"), err=True) - if is_fail_fast_mode: - sys.exit(1) - + warning_and_exit_if_fail_fast_mode( + "Warning: error reading JUnitXml file {filename}: {error}".format( + filename=report, error=e)) return if isinstance(xml, JUnitXml): testsuites = [suite for suite in xml] @@ -395,10 +386,9 @@ def parse(report: str) -> Generator[CaseEventType, None, None]: for case in suite: yield CaseEvent.from_case_and_suite(self.path_builder, case, suite, report, self.metadata_builder) except Exception as e: - click.echo(click.style("Warning: error parsing JUnitXml file {filename}: {error}".format( - filename=report, error=e), fg="yellow"), err=True) - if is_fail_fast_mode: - sys.exit(1) + warning_and_exit_if_fail_fast_mode( + "Warning: error parsing JUnitXml file {filename}: {error}".format( + filename=report, error=e)) self.parse_func = parse @@ -528,29 +518,12 @@ def send(payload: Dict[str, Union[str, List]]) -> None: if res.status_code == HTTPStatus.NOT_FOUND: if session: build, _ = parse_session(session) - click.echo( - click.style( - "Session {} was not found. " - "Make sure to run `launchable record session --build {}` " - "before `launchable record tests`".format( - session, - build), - 'yellow'), - err=True) - if is_fail_fast_mode: - sys.exit(1) + warning_and_exit_if_fail_fast_mode( + "Session {} was not found. Make sure to run `launchable record session --build {}` before `launchable record tests`".format(session, build)) # noqa: E501 + elif build_name: - click.echo( - click.style( - "Build {} was not found. " - "Make sure to run `launchable record build --name {}` " - "before `launchable record tests`".format( - build_name, - build_name), - 'yellow'), - err=True) - if is_fail_fast_mode: - sys.exit(1) + warning_and_exit_if_fail_fast_mode( + "Build {} was not found. Make sure to run `launchable record build --name {}` before `launchable record tests`".format(build_name, build_name)) # noqa: E501 res.raise_for_status() @@ -634,22 +607,15 @@ def recorded_result() -> Tuple[int, int, int, float]: if count == 0: if len(self.skipped_reports) != 0: - click.echo(click.style( + warning_and_exit_if_fail_fast_mode( "{} test report(s) were skipped because they were created before this build was recorded.\n" "Make sure to run your tests after you run `launchable record build`.\n" - "Otherwise, if these are really correct test reports, use the `--allow-test-before-build` option.".format( - len(self.skipped_reports)), 'yellow')) - if is_fail_fast_mode: - sys.exit(1) + "Otherwise, if these are really correct test reports, use the `--allow-test-before-build` option.". + format(len(self.skipped_reports))) return else: - click.echo( - click.style( - "Looks like tests didn't run? " - "If not, make sure the right files/directories were passed into `launchable record tests`", - 'yellow')) - if is_fail_fast_mode: - sys.exit(1) + warning_and_exit_if_fail_fast_mode( + "Looks like tests didn't run? If not, make sure the right files/directories were passed into `launchable record tests`") # noqa: E501 return file_count = len(self.reports) From 23f16f3a61b88095cc44e6ed50e1550944269cd0 Mon Sep 17 00:00:00 2001 From: Konboi Date: Wed, 25 Jun 2025 14:54:17 +0900 Subject: [PATCH 19/29] fix commit command --- launchable/commands/record/commit.py | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/launchable/commands/record/commit.py b/launchable/commands/record/commit.py index d022115c6..f9965e56c 100644 --- a/launchable/commands/record/commit.py +++ b/launchable/commands/record/commit.py @@ -13,6 +13,7 @@ from ...utils.commands import Command from ...utils.commit_ingester import upload_commits from ...utils.env_keys import COMMIT_TIMEOUT, REPORT_ERROR_KEY +from ...utils.fail_fast_mode import set_fail_fast_mode, warning_and_exit_if_fail_fast_mode from ...utils.git_log_parser import parse_git_log from ...utils.http_client import get_base_url from ...utils.java import cygpath, get_java_command @@ -56,10 +57,10 @@ def commit(ctx, source: str, executable: bool, max_days: int, scrub_pii: bool, i tracking_client = TrackingClient(Command.COMMIT, app=ctx.obj) client = LaunchableClient(tracking_client=tracking_client, app=ctx.obj) - is_fail_fast_mode = client.is_fail_fast_mode() + set_fail_fast_mode(client.is_fail_fast_mode()) if import_git_log_output: - _import_git_log(import_git_log_output, ctx.obj, is_fail_fast_mode) + _import_git_log(import_git_log_output, ctx.obj) return # Commit messages are not collected in the default. @@ -83,15 +84,9 @@ def commit(ctx, source: str, executable: bool, max_days: int, scrub_pii: bool, i if os.getenv(REPORT_ERROR_KEY): raise e else: - click.echo(click.style( + warning_and_exit_if_fail_fast_mode( "Couldn't get commit history from `{}`. Do you run command root of git-controlled directory? " - "If not, please set a directory use by --source option." - .format(cwd), - fg='yellow'), - err=True) - print(e) - if is_fail_fast_mode: - sys.exit(1) + "If not, please set a directory use by --source option.\nerror: {}".format(cwd, e)) def exec_jar(source: str, max_days: int, app: Application, is_collect_message: bool): @@ -136,7 +131,7 @@ def exec_jar(source: str, max_days: int, app: Application, is_collect_message: b ) -def _import_git_log(output_file: str, app: Application, is_fail_fast_mode: bool = False): +def _import_git_log(output_file: str, app: Application): try: with click.open_file(output_file) as fp: commits = parse_git_log(fp) @@ -145,12 +140,7 @@ def _import_git_log(output_file: str, app: Application, is_fail_fast_mode: bool if os.getenv(REPORT_ERROR_KEY): raise e else: - click.echo( - click.style("Failed to import the git-log output", fg='yellow'), - err=True) - print(e) - if is_fail_fast_mode: - sys.exit(1) + warning_and_exit_if_fail_fast_mode("Failed to import the git-log output\n error: {}".format(e)) def _build_proxy_option(https_proxy: Optional[str]) -> List[str]: From 5ef3da468c5bb9622e6aa2aeae3d2e1e55e2b653 Mon Sep 17 00:00:00 2001 From: Konboi Date: Wed, 25 Jun 2025 14:58:48 +0900 Subject: [PATCH 20/29] fix build command --- launchable/commands/record/build.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/launchable/commands/record/build.py b/launchable/commands/record/build.py index 1150f3bd9..e9d8b02f2 100644 --- a/launchable/commands/record/build.py +++ b/launchable/commands/record/build.py @@ -14,6 +14,7 @@ from ...utils.authentication import get_org_workspace from ...utils.click import DATETIME_WITH_TZ, KEY_VALUE, validate_past_datetime from ...utils.commands import Command +from ...utils.fail_fast_mode import set_fail_fast_mode, warning_and_exit_if_fail_fast_mode from ...utils.launchable_client import LaunchableClient from ...utils.session import clean_session_files, write_build from .commit import commit @@ -116,8 +117,7 @@ def build( tracking_client = TrackingClient(Command.RECORD_BUILD, app=ctx.obj) client = LaunchableClient(app=ctx.obj, tracking_client=tracking_client) - - is_fail_fast_mode = client.is_fail_fast_mode() + set_fail_fast_mode(client.is_fail_fast_mode()) if "/" in build_name or "%2f" in build_name.lower(): sys.exit("--name must not contain a slash and an encoded slash") @@ -272,15 +272,7 @@ def compute_hash_and_branch(ws: List[Workspace]): sys.exit(1) if not ws_by_name.get(kv[0]): - message = "Invalid repository name {repo} in a --branch option.\nThe repository “{repo}” is not specified via `--source` or `--commit` option.".format(repo=kv[0]) # noqa: E501 - fg_color = "red" if is_fail_fast_mode else "yellow" - click.echo(click.style(message, fg=fg_color), err=True) - if is_fail_fast_mode: - tracking_client.send_error_event( - event_name=Tracking.ErrorEvent.USER_ERROR, - stack_trace=message, - ) - sys.exit(1) + warning_and_exit_if_fail_fast_mode("Invalid repository name {repo} in a --branch option.\nThe repository “{repo}” is not specified via `--source` or `--commit` option.".format(repo=kv[0])) # noqa: E501 branch_name_map[kv[0]] = kv[1] From 5ece995c0efad43f39558b0caaf39fe2068bd21d Mon Sep 17 00:00:00 2001 From: Konboi Date: Wed, 25 Jun 2025 15:31:18 +0900 Subject: [PATCH 21/29] fix session command --- launchable/commands/record/session.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/launchable/commands/record/session.py b/launchable/commands/record/session.py index 8cfabed15..7bcf2db3c 100644 --- a/launchable/commands/record/session.py +++ b/launchable/commands/record/session.py @@ -13,7 +13,7 @@ from ...utils.click import KEY_VALUE from ...utils.commands import Command -from ...utils.fail_fast_mode_validator import FailFastModeValidator +from ...utils.fail_fast_mode import FailFastModeValidator, set_fail_fast_mode from ...utils.launchable_client import LaunchableClient from ...utils.no_build import NO_BUILD_BUILD_NAME from ...utils.session import _session_file_path, read_build, write_session @@ -136,11 +136,10 @@ def session( tracking_client = TrackingClient(Command.RECORD_SESSION, app=ctx.obj) client = LaunchableClient(app=ctx.obj, tracking_client=tracking_client) - is_fail_fast_mode = client.is_fail_fast_mode() + set_fail_fast_mode(client.is_fail_fast_mode()) FailFastModeValidator( command=Command.RECORD_SESSION, - fail_fast_mode=is_fail_fast_mode, build=build_name, is_no_build=is_no_build, test_suite=test_suite, From 35d77c16dabbfbe919bdf1864055a57908fba668 Mon Sep 17 00:00:00 2001 From: Konboi Date: Wed, 25 Jun 2025 15:33:35 +0900 Subject: [PATCH 22/29] fix subset command --- launchable/commands/subset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launchable/commands/subset.py b/launchable/commands/subset.py index 1a5568d0b..d0e84063b 100644 --- a/launchable/commands/subset.py +++ b/launchable/commands/subset.py @@ -598,7 +598,7 @@ def run(self): e, "Warning: the service failed to subset. Falling back to running all tests") if len(original_subset) == 0: - warning_and_exit_if_fail_fast_mode("Error: no tests found matching the path.", 'yellow') + warning_and_exit_if_fail_fast_mode("Error: no tests found matching the path.") return if split: From 19c0ced23f32f1997c20d6bbed43107992208f06 Mon Sep 17 00:00:00 2001 From: Konboi Date: Wed, 25 Jun 2025 15:39:36 +0900 Subject: [PATCH 23/29] fix type check error --- launchable/utils/fail_fast_mode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launchable/utils/fail_fast_mode.py b/launchable/utils/fail_fast_mode.py index 3ac31eb6b..f405d905a 100644 --- a/launchable/utils/fail_fast_mode.py +++ b/launchable/utils/fail_fast_mode.py @@ -5,7 +5,7 @@ from .commands import Command -_fail_fast_mode_cache: bool = None +_fail_fast_mode_cache: Optional[bool] = None def set_fail_fast_mode(enabled: bool): From 2273b40b88ea4e528502b92a3c3ebdf528e0577a Mon Sep 17 00:00:00 2001 From: Konboi Date: Wed, 25 Jun 2025 15:44:08 +0900 Subject: [PATCH 24/29] fix lint error- --- launchable/utils/fail_fast_mode.py | 1 - 1 file changed, 1 deletion(-) diff --git a/launchable/utils/fail_fast_mode.py b/launchable/utils/fail_fast_mode.py index f405d905a..99cbd44ea 100644 --- a/launchable/utils/fail_fast_mode.py +++ b/launchable/utils/fail_fast_mode.py @@ -14,7 +14,6 @@ def set_fail_fast_mode(enabled: bool): def is_fail_fast_mode() -> bool: - global _fail_fast_mode_cache if _fail_fast_mode_cache: return _fail_fast_mode_cache From 23488f00ed6834c7897614aba491a5bd4a18095e Mon Sep 17 00:00:00 2001 From: Konboi Date: Wed, 25 Jun 2025 17:04:58 +0900 Subject: [PATCH 25/29] add test for fail_fast_mode validation --- tests/utils/test_fail_fast_mode.py | 40 ++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 tests/utils/test_fail_fast_mode.py diff --git a/tests/utils/test_fail_fast_mode.py b/tests/utils/test_fail_fast_mode.py new file mode 100644 index 000000000..f1fb687f8 --- /dev/null +++ b/tests/utils/test_fail_fast_mode.py @@ -0,0 +1,40 @@ +import io +from contextlib import contextmanager, redirect_stderr + +from launchable.utils.commands import Command +from launchable.utils.fail_fast_mode import FailFastModeValidator +from tests.cli_test_case import CliTestCase + + +class FailFastModeTest(CliTestCase): + def test_validate_subset(self): + validator = FailFastModeValidator( + command=Command.SUBSET, + session='session1', + test_suite='test_suite1', + is_observation=True, + ) + stderr = io.StringIO() + + with self.assertRaises(SystemExit) as cm: + with tmp_set_fail_fast_mode(False), redirect_stderr(stderr): + # `--observation` option won't be applied but the cli won't be error + validator.validate() + self.assertEqual(stderr.getvalue(), "") + + with tmp_set_fail_fast_mode(True), redirect_stderr(stderr): + validator.validate() + self.assertEqual(cm.exception.code, 1) + self.assertIn("ignored", stderr.getvalue()) + + +@contextmanager +def tmp_set_fail_fast_mode(enabled: bool): + from launchable.utils.fail_fast_mode import _fail_fast_mode_cache, set_fail_fast_mode + original = _fail_fast_mode_cache + try: + set_fail_fast_mode(enabled) + yield + finally: + if original is not None: + set_fail_fast_mode(original) From d861e28e5032f570b6390bf7dab77a09b4d52ca4 Mon Sep 17 00:00:00 2001 From: Konboi Date: Wed, 25 Jun 2025 17:07:26 +0900 Subject: [PATCH 26/29] don't need to check but leave method and comment --- launchable/utils/fail_fast_mode.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/launchable/utils/fail_fast_mode.py b/launchable/utils/fail_fast_mode.py index 99cbd44ea..b7a0d2e51 100644 --- a/launchable/utils/fail_fast_mode.py +++ b/launchable/utils/fail_fast_mode.py @@ -64,7 +64,8 @@ def validate(self): self._validate_record_tests() def _validate_record_session(self): - self._print_errors() + # Now, there isn't any validation for the `record session` command in fail-fast mode. + return def _validate_require_session_option(self, cmd_name: str): if self.session: From 00a894421a57f675519d96f074d5e1a25064446a Mon Sep 17 00:00:00 2001 From: Konboi Date: Fri, 27 Jun 2025 09:45:39 +0900 Subject: [PATCH 27/29] fixed based on feedback --- launchable/commands/record/session.py | 6 +- launchable/commands/record/tests.py | 7 +- launchable/commands/subset.py | 7 +- launchable/utils/commands.py | 3 + launchable/utils/fail_fast_mode.py | 100 +++++++++++++------------- tests/utils/test_fail_fast_mode.py | 10 +-- 6 files changed, 70 insertions(+), 63 deletions(-) diff --git a/launchable/commands/record/session.py b/launchable/commands/record/session.py index 7bcf2db3c..b5376e1a0 100644 --- a/launchable/commands/record/session.py +++ b/launchable/commands/record/session.py @@ -13,7 +13,7 @@ from ...utils.click import KEY_VALUE from ...utils.commands import Command -from ...utils.fail_fast_mode import FailFastModeValidator, set_fail_fast_mode +from ...utils.fail_fast_mode import FailFastModeValidateParams, fail_fast_mode_validate, set_fail_fast_mode from ...utils.launchable_client import LaunchableClient from ...utils.no_build import NO_BUILD_BUILD_NAME from ...utils.session import _session_file_path, read_build, write_session @@ -138,12 +138,12 @@ def session( client = LaunchableClient(app=ctx.obj, tracking_client=tracking_client) set_fail_fast_mode(client.is_fail_fast_mode()) - FailFastModeValidator( + fail_fast_mode_validate(FailFastModeValidateParams( command=Command.RECORD_SESSION, build=build_name, is_no_build=is_no_build, test_suite=test_suite, - ).validate() + )) if not is_no_build and not build_name: raise click.UsageError("Error: Missing option '--build'") diff --git a/launchable/commands/record/tests.py b/launchable/commands/record/tests.py index 9dc40b51d..1eb40dbe0 100644 --- a/launchable/commands/record/tests.py +++ b/launchable/commands/record/tests.py @@ -19,7 +19,8 @@ from ...utils.click import DATETIME_WITH_TZ, KEY_VALUE, validate_past_datetime from ...utils.commands import Command from ...utils.exceptions import InvalidJUnitXMLException -from ...utils.fail_fast_mode import FailFastModeValidator, set_fail_fast_mode, warning_and_exit_if_fail_fast_mode +from ...utils.fail_fast_mode import (FailFastModeValidateParams, fail_fast_mode_validate, + set_fail_fast_mode, warning_and_exit_if_fail_fast_mode) from ...utils.launchable_client import LaunchableClient from ...utils.logger import Logger from ...utils.no_build import NO_BUILD_BUILD_NAME, NO_BUILD_TEST_SESSION_ID @@ -199,7 +200,7 @@ def tests( client = LaunchableClient(test_runner=test_runner, app=context.obj, tracking_client=tracking_client) set_fail_fast_mode(client.is_fail_fast_mode()) - FailFastModeValidator( + fail_fast_mode_validate(FailFastModeValidateParams( command=Command.RECORD_TESTS, session=session, build=build_name, @@ -207,7 +208,7 @@ def tests( links=links, is_no_build=is_no_build, test_suite=test_suite, - ).validate() + )) file_path_normalizer = FilePathNormalizer(base_path, no_base_path_inference=no_base_path_inference) diff --git a/launchable/commands/subset.py b/launchable/commands/subset.py index d0e84063b..fa7c1ff3c 100644 --- a/launchable/commands/subset.py +++ b/launchable/commands/subset.py @@ -19,7 +19,8 @@ from ..utils.click import DURATION, KEY_VALUE, PERCENTAGE, DurationType, PercentageType, ignorable_error from ..utils.commands import Command from ..utils.env_keys import REPORT_ERROR_KEY -from ..utils.fail_fast_mode import FailFastModeValidator, set_fail_fast_mode, warning_and_exit_if_fail_fast_mode +from ..utils.fail_fast_mode import (FailFastModeValidateParams, fail_fast_mode_validate, + set_fail_fast_mode, warning_and_exit_if_fail_fast_mode) from ..utils.launchable_client import LaunchableClient from .helper import find_or_create_session from .test_path_writer import TestPathWriter @@ -234,7 +235,7 @@ def subset( tracking_client=tracking_client) set_fail_fast_mode(client.is_fail_fast_mode()) - FailFastModeValidator( + fail_fast_mode_validate(FailFastModeValidateParams( command=Command.SUBSET, session=session, build=build_name, @@ -243,7 +244,7 @@ def subset( links=links, is_no_build=is_no_build, test_suite=test_suite, - ).validate() + )) if is_observation and is_output_exclusion_rules: msg = ( diff --git a/launchable/utils/commands.py b/launchable/utils/commands.py index 9503d227c..6a8e357b6 100644 --- a/launchable/utils/commands.py +++ b/launchable/utils/commands.py @@ -8,3 +8,6 @@ class Command(Enum): RECORD_SESSION = 'RECORD_SESSION' SUBSET = 'SUBSET' COMMIT = 'COMMIT' + + def display_name(self): + return self.value.lower().replace('_', ' ') diff --git a/launchable/utils/fail_fast_mode.py b/launchable/utils/fail_fast_mode.py index b7a0d2e51..3fec96a6c 100644 --- a/launchable/utils/fail_fast_mode.py +++ b/launchable/utils/fail_fast_mode.py @@ -30,18 +30,11 @@ def warning_and_exit_if_fail_fast_mode(message: str): sys.exit(1) -class FailFastModeValidator: - def __init__( - self, - command: Command, - build: Optional[str] = None, - is_no_build: bool = False, - test_suite: Optional[str] = None, - session: Optional[str] = None, - links: Sequence[Tuple[str, str]] = (), - is_observation: bool = False, - flavor: Sequence[Tuple[str, str]] = (), - ): +class FailFastModeValidateParams: + def __init__(self, command: Command, build: Optional[str] = None, is_no_build: bool = False, + test_suite: Optional[str] = None, session: Optional[str] = None, + links: Sequence[Tuple[str, str]] = (), is_observation: bool = False, + flavor: Sequence[Tuple[str, str]] = ()): self.command = command self.build = build self.is_no_build = is_no_build @@ -50,50 +43,59 @@ def __init__( self.links = links self.is_observation = is_observation self.flavor = flavor - self.errors: List[str] = [] - def validate(self): - if not is_fail_fast_mode(): - return - if self.command == Command.RECORD_SESSION: - self._validate_record_session() - if self.command == Command.SUBSET: - self._validate_subset() - if self.command == Command.RECORD_TESTS: - self._validate_record_tests() - - def _validate_record_session(self): - # Now, there isn't any validation for the `record session` command in fail-fast mode. +def fail_fast_mode_validate(params: FailFastModeValidateParams): + if not is_fail_fast_mode(): return - def _validate_require_session_option(self, cmd_name: str): - if self.session: - if self.test_suite: - self.errors.append("`--test-suite` option was ignored in the {} command. Add `--test-suite` option to the `record session` command instead.".format(cmd_name)) # noqa: E501 + if params.command == Command.RECORD_SESSION: + _validate_record_session(params) + if params.command == Command.SUBSET: + _validate_subset(params) + if params.command == Command.RECORD_TESTS: + _validate_record_tests(params) + + +def _validate_require_session_option(params: FailFastModeValidateParams) -> List[str]: + errors: List[str] = [] + cmd_name = params.command.display_name() + if params.session: + if params.test_suite: + errors.append("`--test-suite` option was ignored in the {} command. Add `--test-suite` option to the `record session` command instead.".format(cmd_name)) # noqa: E501 + + if params.is_observation: + errors.append( + "`--observation` was ignored in the {} command. Add `--observation` option to the `record session` command instead.".format(cmd_name)) # noqa: E501 + + if len(params.flavor) > 0: + errors.append( + "`--flavor` option was ignored in the {} command. Add `--flavor` option to the `record session` command instead.".format(cmd_name)) # noqa: E501 - if self.is_observation: - self.errors.append( - "`--observation` was ignored in the {} command. Add `--observation` option to the `record session` command instead.".format(cmd_name)) # noqa: E501 + if len(params.links) > 0: + errors.append( + "`--link` option was ignored in the {} command. Add `link` option to the `record session` command instead.".format(cmd_name)) # noqa: E501 - if len(self.flavor) > 0: - self.errors.append( - "`--flavor` option was ignored in the {} command. Add `--flavor` option to the `record session` command instead.".format(cmd_name)) # noqa: E501 + return errors - if len(self.links) > 0: - self.errors.append( - "`--link` option was ignored in the {} command. Add `link` option to the `record session` command instead.".format(cmd_name)) # noqa: E501 - def _validate_subset(self): - self._validate_require_session_option("subset") - self._print_errors() +def _validate_record_session(params: FailFastModeValidateParams): + # Now, there isn't any validation for the `record session` command in fail-fast mode. + return - def _validate_record_tests(self): - self._validate_require_session_option("record tests") - self._print_errors() - def _print_errors(self): - if len(self.errors) > 0: - msg = "\n".join(map(lambda x: click.style(x, fg='red'), self.errors)) - click.echo(msg, err=True) - sys.exit(1) +def _validate_subset(params: FailFastModeValidateParams): + errors = _validate_require_session_option(params) + _exit_if_errors(errors) + + +def _validate_record_tests(params: FailFastModeValidateParams): + errors = _validate_require_session_option(params) + _exit_if_errors(errors) + + +def _exit_if_errors(errors: List[str]): + if errors: + msg = "\n".join(map(lambda x: click.style(x, fg='red'), errors)) + click.echo(msg, err=True) + sys.exit(1) diff --git a/tests/utils/test_fail_fast_mode.py b/tests/utils/test_fail_fast_mode.py index f1fb687f8..c150e5f8f 100644 --- a/tests/utils/test_fail_fast_mode.py +++ b/tests/utils/test_fail_fast_mode.py @@ -2,13 +2,13 @@ from contextlib import contextmanager, redirect_stderr from launchable.utils.commands import Command -from launchable.utils.fail_fast_mode import FailFastModeValidator +from launchable.utils.fail_fast_mode import FailFastModeValidateParams, fail_fast_mode_validate from tests.cli_test_case import CliTestCase class FailFastModeTest(CliTestCase): - def test_validate_subset(self): - validator = FailFastModeValidator( + def test_fail_fast_mode_validate(self): + params = FailFastModeValidateParams( command=Command.SUBSET, session='session1', test_suite='test_suite1', @@ -19,11 +19,11 @@ def test_validate_subset(self): with self.assertRaises(SystemExit) as cm: with tmp_set_fail_fast_mode(False), redirect_stderr(stderr): # `--observation` option won't be applied but the cli won't be error - validator.validate() + fail_fast_mode_validate(params) self.assertEqual(stderr.getvalue(), "") with tmp_set_fail_fast_mode(True), redirect_stderr(stderr): - validator.validate() + fail_fast_mode_validate(params) self.assertEqual(cm.exception.code, 1) self.assertIn("ignored", stderr.getvalue()) From 78a194381d1c1729778eb9c065d78c6aec031667 Mon Sep 17 00:00:00 2001 From: Konboi Date: Fri, 27 Jun 2025 09:58:53 +0900 Subject: [PATCH 28/29] opposite... when fail fast mode is enabled, we should dispaly error message with red color --- launchable/utils/fail_fast_mode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launchable/utils/fail_fast_mode.py b/launchable/utils/fail_fast_mode.py index 3fec96a6c..d03870304 100644 --- a/launchable/utils/fail_fast_mode.py +++ b/launchable/utils/fail_fast_mode.py @@ -22,7 +22,7 @@ def is_fail_fast_mode() -> bool: def warning_and_exit_if_fail_fast_mode(message: str): - color = 'yellow' if is_fail_fast_mode() else 'red' + color = 'red' if is_fail_fast_mode() else 'yellow' message = click.style(message, fg=color) click.echo(message, err=True) From 016dec9d56105810cea7a0c4bd937ae8b613c82a Mon Sep 17 00:00:00 2001 From: Konboi Date: Fri, 27 Jun 2025 14:49:51 +0900 Subject: [PATCH 29/29] fix method name based on feedback --- launchable/commands/record/build.py | 4 ++-- launchable/commands/record/commit.py | 6 +++--- launchable/commands/record/tests.py | 16 ++++++++-------- launchable/commands/subset.py | 8 ++++---- launchable/utils/fail_fast_mode.py | 2 +- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/launchable/commands/record/build.py b/launchable/commands/record/build.py index e9d8b02f2..ddaec2547 100644 --- a/launchable/commands/record/build.py +++ b/launchable/commands/record/build.py @@ -14,7 +14,7 @@ from ...utils.authentication import get_org_workspace from ...utils.click import DATETIME_WITH_TZ, KEY_VALUE, validate_past_datetime from ...utils.commands import Command -from ...utils.fail_fast_mode import set_fail_fast_mode, warning_and_exit_if_fail_fast_mode +from ...utils.fail_fast_mode import set_fail_fast_mode, warn_and_exit_if_fail_fast_mode from ...utils.launchable_client import LaunchableClient from ...utils.session import clean_session_files, write_build from .commit import commit @@ -272,7 +272,7 @@ def compute_hash_and_branch(ws: List[Workspace]): sys.exit(1) if not ws_by_name.get(kv[0]): - warning_and_exit_if_fail_fast_mode("Invalid repository name {repo} in a --branch option.\nThe repository “{repo}” is not specified via `--source` or `--commit` option.".format(repo=kv[0])) # noqa: E501 + warn_and_exit_if_fail_fast_mode("Invalid repository name {repo} in a --branch option.\nThe repository “{repo}” is not specified via `--source` or `--commit` option.".format(repo=kv[0])) # noqa: E501 branch_name_map[kv[0]] = kv[1] diff --git a/launchable/commands/record/commit.py b/launchable/commands/record/commit.py index f9965e56c..5cd831ae2 100644 --- a/launchable/commands/record/commit.py +++ b/launchable/commands/record/commit.py @@ -13,7 +13,7 @@ from ...utils.commands import Command from ...utils.commit_ingester import upload_commits from ...utils.env_keys import COMMIT_TIMEOUT, REPORT_ERROR_KEY -from ...utils.fail_fast_mode import set_fail_fast_mode, warning_and_exit_if_fail_fast_mode +from ...utils.fail_fast_mode import set_fail_fast_mode, warn_and_exit_if_fail_fast_mode from ...utils.git_log_parser import parse_git_log from ...utils.http_client import get_base_url from ...utils.java import cygpath, get_java_command @@ -84,7 +84,7 @@ def commit(ctx, source: str, executable: bool, max_days: int, scrub_pii: bool, i if os.getenv(REPORT_ERROR_KEY): raise e else: - warning_and_exit_if_fail_fast_mode( + warn_and_exit_if_fail_fast_mode( "Couldn't get commit history from `{}`. Do you run command root of git-controlled directory? " "If not, please set a directory use by --source option.\nerror: {}".format(cwd, e)) @@ -140,7 +140,7 @@ def _import_git_log(output_file: str, app: Application): if os.getenv(REPORT_ERROR_KEY): raise e else: - warning_and_exit_if_fail_fast_mode("Failed to import the git-log output\n error: {}".format(e)) + warn_and_exit_if_fail_fast_mode("Failed to import the git-log output\n error: {}".format(e)) def _build_proxy_option(https_proxy: Optional[str]) -> List[str]: diff --git a/launchable/commands/record/tests.py b/launchable/commands/record/tests.py index 1eb40dbe0..8d07a075e 100644 --- a/launchable/commands/record/tests.py +++ b/launchable/commands/record/tests.py @@ -20,7 +20,7 @@ from ...utils.commands import Command from ...utils.exceptions import InvalidJUnitXMLException from ...utils.fail_fast_mode import (FailFastModeValidateParams, fail_fast_mode_validate, - set_fail_fast_mode, warning_and_exit_if_fail_fast_mode) + set_fail_fast_mode, warn_and_exit_if_fail_fast_mode) from ...utils.launchable_client import LaunchableClient from ...utils.logger import Logger from ...utils.no_build import NO_BUILD_BUILD_NAME, NO_BUILD_TEST_SESSION_ID @@ -223,7 +223,7 @@ def tests( raise click.UsageError(message=msg) # noqa: E501 if is_no_build and session: - warning_and_exit_if_fail_fast_mode( + warn_and_exit_if_fail_fast_mode( "WARNING: `--session` and `--no-build` are set.\nUsing --session option value ({}) and ignoring `--no-build` option".format(session), # noqa: E501 ) @@ -371,7 +371,7 @@ def parse(report: str) -> Generator[CaseEventType, None, None]: # `JUnitXml.fromfile()` will raise `JUnitXmlError` and other lxml related errors # if the file has wrong format. # https://github.com/weiwei/junitparser/blob/master/junitparser/junitparser.py#L321 - warning_and_exit_if_fail_fast_mode( + warn_and_exit_if_fail_fast_mode( "Warning: error reading JUnitXml file {filename}: {error}".format( filename=report, error=e)) return @@ -387,7 +387,7 @@ def parse(report: str) -> Generator[CaseEventType, None, None]: for case in suite: yield CaseEvent.from_case_and_suite(self.path_builder, case, suite, report, self.metadata_builder) except Exception as e: - warning_and_exit_if_fail_fast_mode( + warn_and_exit_if_fail_fast_mode( "Warning: error parsing JUnitXml file {filename}: {error}".format( filename=report, error=e)) @@ -519,11 +519,11 @@ def send(payload: Dict[str, Union[str, List]]) -> None: if res.status_code == HTTPStatus.NOT_FOUND: if session: build, _ = parse_session(session) - warning_and_exit_if_fail_fast_mode( + warn_and_exit_if_fail_fast_mode( "Session {} was not found. Make sure to run `launchable record session --build {}` before `launchable record tests`".format(session, build)) # noqa: E501 elif build_name: - warning_and_exit_if_fail_fast_mode( + warn_and_exit_if_fail_fast_mode( "Build {} was not found. Make sure to run `launchable record build --name {}` before `launchable record tests`".format(build_name, build_name)) # noqa: E501 res.raise_for_status() @@ -608,14 +608,14 @@ def recorded_result() -> Tuple[int, int, int, float]: if count == 0: if len(self.skipped_reports) != 0: - warning_and_exit_if_fail_fast_mode( + warn_and_exit_if_fail_fast_mode( "{} test report(s) were skipped because they were created before this build was recorded.\n" "Make sure to run your tests after you run `launchable record build`.\n" "Otherwise, if these are really correct test reports, use the `--allow-test-before-build` option.". format(len(self.skipped_reports))) return else: - warning_and_exit_if_fail_fast_mode( + warn_and_exit_if_fail_fast_mode( "Looks like tests didn't run? If not, make sure the right files/directories were passed into `launchable record tests`") # noqa: E501 return diff --git a/launchable/commands/subset.py b/launchable/commands/subset.py index fa7c1ff3c..2426ee165 100644 --- a/launchable/commands/subset.py +++ b/launchable/commands/subset.py @@ -20,7 +20,7 @@ from ..utils.commands import Command from ..utils.env_keys import REPORT_ERROR_KEY from ..utils.fail_fast_mode import (FailFastModeValidateParams, fail_fast_mode_validate, - set_fail_fast_mode, warning_and_exit_if_fail_fast_mode) + set_fail_fast_mode, warn_and_exit_if_fail_fast_mode) from ..utils.launchable_client import LaunchableClient from .helper import find_or_create_session from .test_path_writer import TestPathWriter @@ -281,7 +281,7 @@ def subset( sys.exit(1) if is_no_build and session: - warning_and_exit_if_fail_fast_mode( + warn_and_exit_if_fail_fast_mode( "WARNING: `--session` and `--no-build` are set.\nUsing --session option value ({}) and ignoring `--no-build` option".format(session)) # noqa: E501 is_no_build = False @@ -426,7 +426,7 @@ def stdin(self) -> Union[TextIO, List]: they didn't feed anything from stdin """ if sys.stdin.isatty(): - warning_and_exit_if_fail_fast_mode( + warn_and_exit_if_fail_fast_mode( "Warning: this command reads from stdin but it doesn't appear to be connected to anything. " "Did you forget to pipe from another command?" ) @@ -599,7 +599,7 @@ def run(self): e, "Warning: the service failed to subset. Falling back to running all tests") if len(original_subset) == 0: - warning_and_exit_if_fail_fast_mode("Error: no tests found matching the path.") + warn_and_exit_if_fail_fast_mode("Error: no tests found matching the path.") return if split: diff --git a/launchable/utils/fail_fast_mode.py b/launchable/utils/fail_fast_mode.py index d03870304..e24d580ee 100644 --- a/launchable/utils/fail_fast_mode.py +++ b/launchable/utils/fail_fast_mode.py @@ -21,7 +21,7 @@ def is_fail_fast_mode() -> bool: return False -def warning_and_exit_if_fail_fast_mode(message: str): +def warn_and_exit_if_fail_fast_mode(message: str): color = 'red' if is_fail_fast_mode() else 'yellow' message = click.style(message, fg=color)