Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve test output. #10484

Merged
merged 2 commits into from Jul 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
51 changes: 42 additions & 9 deletions src/python/pants/core/goals/test.py
Expand Up @@ -200,6 +200,14 @@ def materialize(self, console: Console, workspace: Workspace) -> Tuple[PurePath,
return tuple(report_paths)


class ShowOutput(Enum):
"""Which tests to emit detailed output for."""

ALL = "all"
FAILED = "failed"
NONE = "none"


class TestSubsystem(GoalSubsystem):
"""Run tests."""

Expand Down Expand Up @@ -228,6 +236,12 @@ def register_options(cls, register) -> None:
default=False,
help="Force the tests to run, even if they could be satisfied from cache.",
)
register(
"--output",
type=ShowOutput,
default=ShowOutput.FAILED,
help="Show stdout/stderr for these tests.",
)
register(
"--use-coverage",
type=bool,
Expand All @@ -252,6 +266,10 @@ def debug(self) -> bool:
def force(self) -> bool:
return cast(bool, self.options.force)

@property
def output(self) -> ShowOutput:
return cast(ShowOutput, self.options.output)

@property
def use_coverage(self) -> bool:
return cast(bool, self.options.use_coverage)
Expand Down Expand Up @@ -307,11 +325,13 @@ async def run_tests(
for field_set in field_sets_with_sources
)

exit_code = PANTS_SUCCEEDED_EXIT_CODE

# Print details.
for result in results:
if result.test_result.status == Status.FAILURE:
exit_code = PANTS_FAILED_EXIT_CODE
if test_subsystem.options.output == ShowOutput.NONE or (
test_subsystem.options.output == ShowOutput.FAILED
and result.test_result.status == Status.SUCCESS
):
continue
has_output = result.test_result.stdout or result.test_result.stderr
if has_output:
status = (
Expand All @@ -328,12 +348,19 @@ async def run_tests(
console.print_stderr("")

# Print summary
if len(results) > 1:
console.print_stderr("")
for result in results:
console.print_stderr(
f"{result.address.reference():80}.....{result.test_result.status.value:>10}"
console.print_stderr("")
for result in results:
color = console.green if result.test_result.status == Status.SUCCESS else console.red
# 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.reference(), result=color(result.test_result.status.value)
)
)

merged_xml_results = await Get(
Digest,
Expand Down Expand Up @@ -374,6 +401,12 @@ async def run_tests(
if coverage_report_files and test_subsystem.open_coverage:
desktop.ui_open(console, interactive_runner, coverage_report_files)

exit_code = (
PANTS_FAILED_EXIT_CODE
if any(res.test_result.status == Status.FAILURE for res in results)
else PANTS_SUCCEEDED_EXIT_CODE
)

return Test(exit_code)


Expand Down
51 changes: 49 additions & 2 deletions src/python/pants/core/goals/test_test.py
Expand Up @@ -13,6 +13,7 @@
CoverageData,
CoverageDataCollection,
CoverageReports,
ShowOutput,
Status,
Test,
TestDebugRequest,
Expand Down Expand Up @@ -137,12 +138,13 @@ def run_test_rule(
targets: List[TargetWithOrigin],
debug: bool = False,
use_coverage: bool = False,
output: ShowOutput = ShowOutput.ALL,
include_sources: bool = True,
valid_targets: bool = True,
) -> Tuple[int, str]:
console = MockConsole(use_colors=False)
test_subsystem = create_goal_subsystem(
TestSubsystem, debug=debug, use_coverage=use_coverage
TestSubsystem, debug=debug, use_coverage=use_coverage, output=output,
)
interactive_runner = InteractiveRunner(self.scheduler)
workspace = Workspace(self.scheduler)
Expand Down Expand Up @@ -244,11 +246,12 @@ def test_single_target(self) -> None:
field_set=SuccessfulFieldSet, targets=[self.make_target_with_origin(address)]
)
assert exit_code == 0
# NB: We don't render a summary when only running one target.
assert stderr == dedent(
f"""\
✓ {address}
{SuccessfulFieldSet.stdout(address)}

{address} ..... SUCCESS
"""
)

Expand Down Expand Up @@ -277,6 +280,50 @@ def test_multiple_targets(self) -> None:
"""
)

def test_output_failed(self) -> None:
good_address = Address.parse(":good")
bad_address = Address.parse(":bad")

exit_code, stderr = self.run_test_rule(
field_set=ConditionallySucceedsFieldSet,
targets=[
self.make_target_with_origin(good_address),
self.make_target_with_origin(bad_address),
],
output=ShowOutput.FAILED,
)
assert exit_code == 1
assert stderr == dedent(
f"""\
𐄂 {bad_address}
{ConditionallySucceedsFieldSet.stderr(bad_address)}

{good_address} ..... SUCCESS
{bad_address} ..... FAILURE
"""
)

def test_output_none(self) -> None:
good_address = Address.parse(":good")
bad_address = Address.parse(":bad")

exit_code, stderr = self.run_test_rule(
field_set=ConditionallySucceedsFieldSet,
targets=[
self.make_target_with_origin(good_address),
self.make_target_with_origin(bad_address),
],
output=ShowOutput.NONE,
)
assert exit_code == 1
assert stderr == dedent(
f"""\

{good_address} ..... SUCCESS
{bad_address} ..... FAILURE
"""
)

def test_debug_target(self) -> None:
exit_code, _ = self.run_test_rule(
field_set=SuccessfulFieldSet, targets=[self.make_target_with_origin()], debug=True,
Expand Down
4 changes: 4 additions & 0 deletions src/python/pants/engine/console.py
Expand Up @@ -95,6 +95,10 @@ def flush(self) -> None:
self.stdout.flush()
self.stderr.flush()

@property
def use_colors(self):
return self._use_colors

def _safe_color(self, text: str, color: Callable[[str], str]) -> str:
"""We should only output color when the global flag --colors is enabled."""
return color(text) if self._use_colors else text
Expand Down
4 changes: 2 additions & 2 deletions src/python/pants/testutil/engine/util.py
Expand Up @@ -235,7 +235,7 @@ class MockConsole:
def __init__(self, use_colors=True):
self.stdout = StringIO()
self.stderr = StringIO()
self._use_colors = use_colors
self.use_colors = use_colors

def write_stdout(self, payload):
self.stdout.write(payload)
Expand All @@ -250,7 +250,7 @@ def print_stderr(self, payload):
print(payload, file=self.stderr)

def _safe_color(self, text: str, color: Callable[[str], str]) -> str:
return color(text) if self._use_colors else text
return color(text) if self.use_colors else text

def blue(self, text: str) -> str:
return self._safe_color(text, blue)
Expand Down