diff --git a/src/python/pants/core/goals/test.py b/src/python/pants/core/goals/test.py index 96e3b490f38..07d0224addd 100644 --- a/src/python/pants/core/goals/test.py +++ b/src/python/pants/core/goals/test.py @@ -7,7 +7,7 @@ from dataclasses import dataclass from enum import Enum from pathlib import PurePath -from typing import Dict, Iterable, List, Optional, Tuple, Type, TypeVar, cast +from typing import Any, Dict, Iterable, List, Optional, Tuple, Type, TypeVar, Union, cast from pants.core.util_rules.filter_empty_sources import ( FieldSetsWithSources, @@ -136,6 +136,19 @@ def message(self) -> str: output = f"{output.rstrip()}\n\n" return f"{message}{output}" + def __lt__(self, other: Union[Any, "EnrichedTestResult"]) -> bool: + """We sort first by status (skipped vs failed vs succeeded), then alphanumerically within + each group.""" + if not isinstance(other, EnrichedTestResult): + return NotImplemented + if self.exit_code == other.exit_code: + return self.address.spec < other.address.spec + if self.exit_code is None: + return True + if other.exit_code is None: + return False + return self.exit_code < other.exit_code + @dataclass(frozen=True) class TestDebugRequest: @@ -371,22 +384,21 @@ async def run_tests( Get(EnrichedTestResult, TestFieldSet, field_set) for field_set in field_sets_with_sources ) - # Print summary + # Print summary. exit_code = 0 if results: console.print_stderr("") - for result in results: + for result in sorted(results): if result.skipped: continue - result_desc = console.green("SUCCESS") if result.exit_code == 0 else console.red("FAILURE") - # The right-align logic sees the color control codes as characters, so we have - # to account for that. In f-strings the alignment field widths must be literals, - # so we have to indirect via a call to .format(). - right_align = 19 if console.use_colors else 10 - format_str = f"{{addr:80}}.....{{result:>{right_align}}}" - console.print_stderr(format_str.format(addr=result.address.spec, result=result_desc)) - if result.exit_code is not None and result.exit_code != 0: - exit_code = result.exit_code + if result.exit_code == 0: + sigil = console.green("✓") + status = "succeeded" + else: + sigil = console.red("𐄂") + status = "failed" + exit_code = cast(int, result.exit_code) + console.print_stderr(f"{sigil} {result.address} {status}.") merged_xml_results = await Get( Digest, MergeDigests(result.xml_results for result in results if result.xml_results), diff --git a/src/python/pants/core/goals/test_test.py b/src/python/pants/core/goals/test_test.py index a1da4fe25b5..4b56dfbc417 100644 --- a/src/python/pants/core/goals/test_test.py +++ b/src/python/pants/core/goals/test_test.py @@ -217,10 +217,10 @@ def test_summary(self) -> None: ) assert exit_code == ConditionallySucceedsFieldSet.exit_code(bad_address) assert stderr == dedent( - f"""\ + """\ - {good_address} ..... SUCCESS - {bad_address} ..... FAILURE + ✓ //:good succeeded. + 𐄂 //:bad failed. """ ) @@ -242,6 +242,26 @@ def test_coverage(self) -> None: assert stderr.strip().endswith(f"Ran coverage on {addr1.spec}, {addr2.spec}") +def sort_results() -> None: + create_test_result = partial( + EnrichedTestResult, stdout="", stderr="", output_setting=ShowOutput.ALL + ) + skip1 = create_test_result(exit_code=None, address=Address("t1")) + skip2 = create_test_result(exit_code=None, address=Address("t2")) + success1 = create_test_result(exit_code=0, address=Address("t1")) + success2 = create_test_result(exit_code=0, address=Address("t2")) + fail1 = create_test_result(exit_code=1, address=Address("t1")) + fail2 = create_test_result(exit_code=1, address=Address("t2")) + assert sorted([fail2, success2, skip2, fail1, success1, skip1]) == [ + skip1, + skip2, + success1, + success2, + fail1, + fail2, + ] + + def assert_streaming_output( *, exit_code: Optional[int],