feat: add summary to Action (#2469)

* chore: add summary to Action

* refactor: new summary table

* fix: fixup tests and formatting

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* fix: pyodide missing some logging

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* fix: nicer printout, nicer in-place summary

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* fix: use summary for everything

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* fix: support only one output wheel from repair

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* Add new Github summary format

* Remove a couple of humanize uses

* fix: filter ANSI codes in summary

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* fix: add sha256

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* fix: nicer wheel/wheels depending on how many are present

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

---------

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
Co-authored-by: Joe Rickerby <joerick@mac.com>
This commit is contained in:
Henry Schreiner
2025-07-15 20:45:53 -07:00
committed by GitHub
co-authored by Joe Rickerby
parent a2c263d4ab
commit d0e4aded3e
14 changed files with 231 additions and 95 deletions
+1
View File
@@ -29,6 +29,7 @@ repos:
additional_dependencies: &mypy-dependencies additional_dependencies: &mypy-dependencies
- bracex - bracex
- dependency-groups>=1.2 - dependency-groups>=1.2
- humanize
- nox>=2025.2.9 - nox>=2025.2.9
- orjson - orjson
- packaging - packaging
+2 -39
View File
@@ -7,10 +7,9 @@ import shutil
import sys import sys
import tarfile import tarfile
import textwrap import textwrap
import time
import traceback import traceback
import typing import typing
from collections.abc import Generator, Iterable, Sequence from collections.abc import Iterable, Sequence
from pathlib import Path from pathlib import Path
from tempfile import mkdtemp from tempfile import mkdtemp
from typing import Any, Literal, TextIO from typing import Any, Literal, TextIO
@@ -286,42 +285,6 @@ def _compute_platform(args: CommandLineArguments) -> PlatformName:
return _compute_platform_auto() return _compute_platform_auto()
@contextlib.contextmanager
def print_new_wheels(msg: str, output_dir: Path) -> Generator[None, None, None]:
"""
Prints the new items in a directory upon exiting. The message to display
can include {n} for number of wheels, {s} for total number of seconds,
and/or {m} for total number of minutes. Does not print anything if this
exits via exception.
"""
start_time = time.time()
existing_contents = set(output_dir.iterdir())
yield
final_contents = set(output_dir.iterdir())
new_contents = [
FileReport(wheel.name, f"{(wheel.stat().st_size + 1023) // 1024:,d}")
for wheel in final_contents - existing_contents
]
if not new_contents:
return
max_name_len = max(len(f.name) for f in new_contents)
max_size_len = max(len(f.size) for f in new_contents)
n = len(new_contents)
s = time.time() - start_time
m = s / 60
print(
msg.format(n=n, s=s, m=m),
*sorted(
f" {f.name:<{max_name_len}s} {f.size:>{max_size_len}s} kB" for f in new_contents
),
sep="\n",
)
def build_in_directory(args: CommandLineArguments) -> None: def build_in_directory(args: CommandLineArguments) -> None:
platform: PlatformName = _compute_platform(args) platform: PlatformName = _compute_platform(args)
if platform == "pyodide" and sys.platform == "win32": if platform == "pyodide" and sys.platform == "win32":
@@ -382,7 +345,7 @@ def build_in_directory(args: CommandLineArguments) -> None:
tmp_path = Path(mkdtemp(prefix="cibw-run-")).resolve(strict=True) tmp_path = Path(mkdtemp(prefix="cibw-run-")).resolve(strict=True)
try: try:
with print_new_wheels("\n{n} wheels produced in {m:.0f} minutes:", output_dir): with log.print_summary(options=options):
platform_module.build(options, tmp_path) platform_module.build(options, tmp_path)
finally: finally:
# avoid https://github.com/python/cpython/issues/86962 by performing # avoid https://github.com/python/cpython/issues/86962 by performing
+12 -2
View File
@@ -4,6 +4,8 @@ from enum import Enum
from .util.helpers import strtobool from .util.helpers import strtobool
ANSI_CODE_REGEX = re.compile(r"(\033\[[0-9;]*m)")
class CIProvider(Enum): class CIProvider(Enum):
# official support # official support
@@ -46,7 +48,7 @@ def fix_ansi_codes_for_github_actions(text: str) -> str:
Github Actions forgets the current ANSI style on every new line. This Github Actions forgets the current ANSI style on every new line. This
function repeats the current ANSI style on every new line. function repeats the current ANSI style on every new line.
""" """
ansi_code_regex = re.compile(r"(\033\[[0-9;]*m)")
ansi_codes: list[str] = [] ansi_codes: list[str] = []
output = "" output = ""
@@ -55,7 +57,7 @@ def fix_ansi_codes_for_github_actions(text: str) -> str:
output += "".join(ansi_codes) + line output += "".join(ansi_codes) + line
# split the line at each ANSI code # split the line at each ANSI code
parts = ansi_code_regex.split(line) parts = ANSI_CODE_REGEX.split(line)
# if there are any ANSI codes, save them # if there are any ANSI codes, save them
if len(parts) > 1: if len(parts) > 1:
# iterate over the ANSI codes in this line # iterate over the ANSI codes in this line
@@ -67,3 +69,11 @@ def fix_ansi_codes_for_github_actions(text: str) -> str:
ansi_codes.append(code) ansi_codes.append(code)
return output return output
def filter_ansi_codes(text: str, /) -> str:
"""
Remove ANSI codes from text.
"""
return ANSI_CODE_REGEX.sub("", text)
+144 -10
View File
@@ -1,11 +1,24 @@
import codecs import codecs
import contextlib
import dataclasses
import functools
import hashlib
import io
import os import os
import re import re
import sys import sys
import textwrap
import time import time
from typing import IO, AnyStr, Final, Literal from collections.abc import Generator
from pathlib import Path
from typing import IO, TYPE_CHECKING, AnyStr, Final, Literal
from .ci import CIProvider, detect_ci_provider import humanize
from .ci import CIProvider, detect_ci_provider, filter_ansi_codes
if TYPE_CHECKING:
from .options import Options
FoldPattern = tuple[str, str] FoldPattern = tuple[str, str]
DEFAULT_FOLD_PATTERN: Final[FoldPattern] = ("{name}", "") DEFAULT_FOLD_PATTERN: Final[FoldPattern] = ("{name}", "")
@@ -69,6 +82,33 @@ class Symbols:
self.error = "✕" if unicode else "failed" self.error = "✕" if unicode else "failed"
@dataclasses.dataclass(kw_only=True, frozen=True)
class BuildInfo:
identifier: str
filename: Path | None
duration: float
@functools.cached_property
def size(self) -> str | None:
if self.filename is None:
return None
return humanize.naturalsize(self.filename.stat().st_size)
@functools.cached_property
def sha256(self) -> str | None:
if self.filename is None:
return None
with self.filename.open("rb") as f:
digest = hashlib.file_digest(f, "sha256")
return digest.hexdigest()
def __str__(self) -> str:
duration = humanize.naturaldelta(self.duration)
if self.filename:
return f"{self.identifier}: {self.filename.name} {self.size} in {duration}, SHA256={self.sha256}"
return f"{self.identifier}: {duration} (test only)"
class Logger: class Logger:
fold_mode: Literal["azure", "github", "travis", "disabled"] fold_mode: Literal["azure", "github", "travis", "disabled"]
colors_enabled: bool colors_enabled: bool
@@ -77,6 +117,7 @@ class Logger:
build_start_time: float | None = None build_start_time: float | None = None
step_start_time: float | None = None step_start_time: float | None = None
active_fold_group_name: str | None = None active_fold_group_name: str | None = None
summary: list[BuildInfo]
def __init__(self) -> None: def __init__(self) -> None:
if sys.platform == "win32" and hasattr(sys.stdout, "reconfigure"): if sys.platform == "win32" and hasattr(sys.stdout, "reconfigure"):
@@ -88,26 +129,29 @@ class Logger:
ci_provider = detect_ci_provider() ci_provider = detect_ci_provider()
if ci_provider == CIProvider.azure_pipelines: match ci_provider:
case CIProvider.azure_pipelines:
self.fold_mode = "azure" self.fold_mode = "azure"
self.colors_enabled = True self.colors_enabled = True
elif ci_provider == CIProvider.github_actions: case CIProvider.github_actions:
self.fold_mode = "github" self.fold_mode = "github"
self.colors_enabled = True self.colors_enabled = True
elif ci_provider == CIProvider.travis_ci: case CIProvider.travis_ci:
self.fold_mode = "travis" self.fold_mode = "travis"
self.colors_enabled = True self.colors_enabled = True
elif ci_provider == CIProvider.appveyor: case CIProvider.appveyor:
self.fold_mode = "disabled" self.fold_mode = "disabled"
self.colors_enabled = True self.colors_enabled = True
else: case _:
self.fold_mode = "disabled" self.fold_mode = "disabled"
self.colors_enabled = file_supports_color(sys.stdout) self.colors_enabled = file_supports_color(sys.stdout)
self.summary = []
def build_start(self, identifier: str) -> None: def build_start(self, identifier: str) -> None:
self.step_end() self.step_end()
c = self.colors c = self.colors
@@ -120,7 +164,7 @@ class Logger:
self.build_start_time = time.time() self.build_start_time = time.time()
self.active_build_identifier = identifier self.active_build_identifier = identifier
def build_end(self) -> None: def build_end(self, filename: Path | None) -> None:
assert self.build_start_time is not None assert self.build_start_time is not None
assert self.active_build_identifier is not None assert self.active_build_identifier is not None
self.step_end() self.step_end()
@@ -128,11 +172,14 @@ class Logger:
c = self.colors c = self.colors
s = self.symbols s = self.symbols
duration = time.time() - self.build_start_time duration = time.time() - self.build_start_time
duration_str = humanize.naturaldelta(duration, minimum_unit="milliseconds")
print() print()
print( print(f"{c.green}{s.done} {c.end}{self.active_build_identifier} finished in {duration_str}")
f"{c.green}{s.done} {c.end}{self.active_build_identifier} finished in {duration:.2f}s" self.summary.append(
BuildInfo(identifier=self.active_build_identifier, filename=filename, duration=duration)
) )
self.build_start_time = None self.build_start_time = None
self.active_build_identifier = None self.active_build_identifier = None
@@ -147,6 +194,7 @@ class Logger:
c = self.colors c = self.colors
s = self.symbols s = self.symbols
duration = time.time() - self.step_start_time duration = time.time() - self.step_start_time
if success: if success:
print(f"{c.green}{s.done} {c.end}{duration:.2f}s".rjust(78)) print(f"{c.green}{s.done} {c.end}{duration:.2f}s".rjust(78))
else: else:
@@ -183,6 +231,26 @@ class Logger:
c = self.colors c = self.colors
print(f"cibuildwheel: {c.bright_red}error{c.end}: {error}\n", file=sys.stderr) print(f"cibuildwheel: {c.bright_red}error{c.end}: {error}\n", file=sys.stderr)
@contextlib.contextmanager
def print_summary(self, *, options: "Options") -> Generator[None, None, None]:
start = time.time()
yield
duration = time.time() - start
if summary_path := os.environ.get("GITHUB_STEP_SUMMARY"):
github_summary = self._github_step_summary(duration=duration, options=options)
Path(summary_path).write_text(filter_ansi_codes(github_summary), encoding="utf-8")
n = len(self.summary)
s = "s" if n > 1 else ""
duration_str = humanize.naturaldelta(duration)
print()
self._start_fold_group(f"{n} wheel{s} produced in {duration_str}")
for build_info in self.summary:
print(" ", build_info)
self._end_fold_group()
self.summary = []
@property @property
def step_active(self) -> bool: def step_active(self) -> bool:
return self.step_start_time is not None return self.step_start_time is not None
@@ -222,6 +290,72 @@ class Logger:
# lowercase, shorten # lowercase, shorten
return identifier.lower()[:20] return identifier.lower()[:20]
def _github_step_summary(self, duration: float, options: "Options") -> str:
"""
Returns the GitHub step summary, in markdown format.
"""
out = io.StringIO()
options_summary = options.summary(
identifiers=[bi.identifier for bi in self.summary], skip_unset=True
)
out.write(
textwrap.dedent("""\
### 🎡 cibuildwheel
<details>
<summary>
Build options
</summary>
```yaml
{options_summary}
```
</details>
""").format(options_summary=options_summary)
)
n_wheels = len([b for b in self.summary if b.filename])
wheel_rows = "\n".join(
"<tr>"
f"<td nowrap>{'<samp>' + b.filename.name + '</samp>' if b.filename else '*Build only*'}</td>"
f"<td nowrap>{b.size or 'N/A'}</td>"
f"<td nowrap><samp>{b.identifier}</samp></td>"
f"<td nowrap>{humanize.naturaldelta(b.duration)}</td>"
f"<td nowrap><samp>{b.sha256 or 'N/A'}</samp></td>"
"</tr>"
for b in self.summary
)
out.write(
textwrap.dedent("""\
<table>
<thead>
<tr>
<th align="left">Wheel</th>
<th align="left">Size</th>
<th align="left">Build identifier</th>
<th align="left">Time</th>
<th align="left">SHA256</th>
</tr>
</thead>
<tbody>
{wheel_rows}
</tbody>
</table>
<div align="right"><sup>{n} wheel{s} created in {duration_str}</sup></div>
""").format(
wheel_rows=wheel_rows,
n=n_wheels,
duration_str=humanize.naturaldelta(duration),
s="s" if n_wheels > 1 else "",
)
)
out.write("\n")
out.write("---")
out.write("\n")
return out.getvalue()
@property @property
def colors(self) -> Colors: def colors(self) -> Colors:
return Colors(enabled=self.colors_enabled) return Colors(enabled=self.colors_enabled)
+20 -5
View File
@@ -907,14 +907,18 @@ class Options:
defaults=True, defaults=True,
) )
def summary(self, identifiers: Iterable[str]) -> str: def summary(self, identifiers: Iterable[str], skip_unset: bool = False) -> str:
lines = [] lines = []
global_option_names = sorted(f.name for f in dataclasses.fields(self.globals)) global_option_names = sorted(f.name for f in dataclasses.fields(self.globals))
for option_name in global_option_names: for option_name in global_option_names:
option_value = getattr(self.globals, option_name) option_value = getattr(self.globals, option_name)
default_value = getattr(self.defaults.globals, option_name) default_value = getattr(self.defaults.globals, option_name)
lines.append(self.option_summary(option_name, option_value, default_value)) line = self.option_summary(
option_name, option_value, default_value, skip_unset=skip_unset
)
if line is not None:
lines.append(line)
build_options = self.build_options(identifier=None) build_options = self.build_options(identifier=None)
build_options_defaults = self.defaults.build_options(identifier=None) build_options_defaults = self.defaults.build_options(identifier=None)
@@ -934,9 +938,15 @@ class Options:
i: getattr(build_options_for_identifier[i], option_name) for i in identifiers i: getattr(build_options_for_identifier[i], option_name) for i in identifiers
} }
lines.append( line = self.option_summary(
self.option_summary(option_name, option_value, default_value, overrides=overrides) option_name,
option_value,
default_value,
overrides=overrides,
skip_unset=skip_unset,
) )
if line is not None:
lines.append(line)
return "\n".join(lines) return "\n".join(lines)
@@ -946,7 +956,8 @@ class Options:
option_value: Any, option_value: Any,
default_value: Any, default_value: Any,
overrides: Mapping[str, Any] | None = None, overrides: Mapping[str, Any] | None = None,
) -> str: skip_unset: bool = False,
) -> str | None:
""" """
Return a summary of the option value, including any overrides, with Return a summary of the option value, including any overrides, with
ANSI 'dim' color if it's the default. ANSI 'dim' color if it's the default.
@@ -960,6 +971,10 @@ class Options:
overrides_value_strs = {k: v for k, v in overrides_value_strs.items() if v != value_str} overrides_value_strs = {k: v for k, v in overrides_value_strs.items() if v != value_str}
has_been_set = (value_str != default_value_str) or overrides_value_strs has_been_set = (value_str != default_value_str) or overrides_value_strs
if skip_unset and not has_been_set:
return None
c = log.colors c = log.colors
result = c.gray if not has_been_set else "" result = c.gray if not has_been_set else ""
+2 -1
View File
@@ -673,6 +673,7 @@ def build(options: Options, tmp_path: Path) -> None:
log.step_end() log.step_end()
# We're all done here; move it to output (overwrite existing) # We're all done here; move it to output (overwrite existing)
output_wheel: Path | None = None
if compatible_wheel is None: if compatible_wheel is None:
output_wheel = build_options.output_dir.joinpath(built_wheel.name) output_wheel = build_options.output_dir.joinpath(built_wheel.name)
moved_wheel = move_file(built_wheel, output_wheel) moved_wheel = move_file(built_wheel, output_wheel)
@@ -685,7 +686,7 @@ def build(options: Options, tmp_path: Path) -> None:
# Clean up # Clean up
shutil.rmtree(identifier_tmp_dir) shutil.rmtree(identifier_tmp_dir)
log.build_end() log.build_end(output_wheel)
except subprocess.CalledProcessError as error: except subprocess.CalledProcessError as error:
msg = f"Command {error.cmd} failed with code {error.returncode}. {error.stdout or ''}" msg = f"Command {error.cmd} failed with code {error.returncode}. {error.stdout or ''}"
raise errors.FatalError(msg) from error raise errors.FatalError(msg) from error
+4 -2
View File
@@ -414,13 +414,15 @@ def build_in_container(
# clean up test environment # clean up test environment
container.call(["rm", "-rf", testing_temp_dir]) container.call(["rm", "-rf", testing_temp_dir])
# move repaired wheels to output # move repaired wheel to output
output_wheel: Path | None = None
if compatible_wheel is None: if compatible_wheel is None:
container.call(["mkdir", "-p", container_output_dir]) container.call(["mkdir", "-p", container_output_dir])
container.call(["mv", repaired_wheel, container_output_dir]) container.call(["mv", repaired_wheel, container_output_dir])
built_wheels.append(container_output_dir / repaired_wheel.name) built_wheels.append(container_output_dir / repaired_wheel.name)
output_wheel = options.globals.output_dir / repaired_wheel.name
log.build_end() log.build_end(output_wheel)
log.step("Copying wheels back to host...") log.step("Copying wheels back to host...")
# copy the output back into the host # copy the output back into the host
+2 -1
View File
@@ -732,6 +732,7 @@ def build(options: Options, tmp_path: Path) -> None:
shell_with_arch(test_command_prepared, cwd=test_cwd, env=virtualenv_env) shell_with_arch(test_command_prepared, cwd=test_cwd, env=virtualenv_env)
# we're all done here; move it to output (overwrite existing) # we're all done here; move it to output (overwrite existing)
output_wheel = None
if compatible_wheel is None: if compatible_wheel is None:
output_wheel = build_options.output_dir.joinpath(repaired_wheel.name) output_wheel = build_options.output_dir.joinpath(repaired_wheel.name)
moved_wheel = move_file(repaired_wheel, output_wheel) moved_wheel = move_file(repaired_wheel, output_wheel)
@@ -744,7 +745,7 @@ def build(options: Options, tmp_path: Path) -> None:
# clean up # clean up
shutil.rmtree(identifier_tmp_dir) shutil.rmtree(identifier_tmp_dir)
log.build_end() log.build_end(output_wheel)
except subprocess.CalledProcessError as error: except subprocess.CalledProcessError as error:
msg = f"Command {error.cmd} failed with code {error.returncode}. {error.stdout or ''}" msg = f"Command {error.cmd} failed with code {error.returncode}. {error.stdout or ''}"
raise errors.FatalError(msg) from error raise errors.FatalError(msg) from error
+2 -1
View File
@@ -540,6 +540,7 @@ def build(options: Options, tmp_path: Path) -> None:
shell(test_command_prepared, cwd=test_cwd, env=virtualenv_env) shell(test_command_prepared, cwd=test_cwd, env=virtualenv_env)
# we're all done here; move it to output (overwrite existing) # we're all done here; move it to output (overwrite existing)
output_wheel: Path | None = None
if compatible_wheel is None: if compatible_wheel is None:
output_wheel = build_options.output_dir.joinpath(repaired_wheel.name) output_wheel = build_options.output_dir.joinpath(repaired_wheel.name)
moved_wheel = move_file(repaired_wheel, output_wheel) moved_wheel = move_file(repaired_wheel, output_wheel)
@@ -548,7 +549,7 @@ def build(options: Options, tmp_path: Path) -> None:
f"{repaired_wheel} was moved to {moved_wheel} instead of {output_wheel}" f"{repaired_wheel} was moved to {moved_wheel} instead of {output_wheel}"
) )
built_wheels.append(output_wheel) built_wheels.append(output_wheel)
log.build_end() log.build_end(output_wheel)
except subprocess.CalledProcessError as error: except subprocess.CalledProcessError as error:
msg = f"Command {error.cmd} failed with code {error.returncode}. {error.stdout or ''}" msg = f"Command {error.cmd} failed with code {error.returncode}. {error.stdout or ''}"
+2 -1
View File
@@ -613,6 +613,7 @@ def build(options: Options, tmp_path: Path) -> None:
shell(test_command_prepared, cwd=test_cwd, env=virtualenv_env) shell(test_command_prepared, cwd=test_cwd, env=virtualenv_env)
# we're all done here; move it to output (remove if already exists) # we're all done here; move it to output (remove if already exists)
output_wheel = None
if compatible_wheel is None: if compatible_wheel is None:
output_wheel = build_options.output_dir.joinpath(repaired_wheel.name) output_wheel = build_options.output_dir.joinpath(repaired_wheel.name)
moved_wheel = move_file(repaired_wheel, output_wheel) moved_wheel = move_file(repaired_wheel, output_wheel)
@@ -627,7 +628,7 @@ def build(options: Options, tmp_path: Path) -> None:
# don't want to abort a build because of that) # don't want to abort a build because of that)
shutil.rmtree(identifier_tmp_dir, ignore_errors=True) shutil.rmtree(identifier_tmp_dir, ignore_errors=True)
log.build_end() log.build_end(output_wheel)
except subprocess.CalledProcessError as error: except subprocess.CalledProcessError as error:
msg = f"Command {error.cmd} failed with code {error.returncode}. {error.stdout or ''}" msg = f"Command {error.cmd} failed with code {error.returncode}. {error.stdout or ''}"
raise errors.FatalError(msg) from error raise errors.FatalError(msg) from error
+1
View File
@@ -42,6 +42,7 @@ dependencies = [
"certifi", "certifi",
"dependency-groups>=1.2", "dependency-groups>=1.2",
"filelock", "filelock",
"humanize",
"packaging>=20.9", "packaging>=20.9",
"platformdirs" "platformdirs"
] ]
+3 -2
View File
@@ -6,7 +6,8 @@ from pathlib import Path
import pytest import pytest
from cibuildwheel import __main__, architecture from cibuildwheel import architecture
from cibuildwheel.logger import Logger
from cibuildwheel.platforms import linux, macos, pyodide, windows from cibuildwheel.platforms import linux, macos, pyodide, windows
from cibuildwheel.util import file from cibuildwheel.util import file
@@ -58,7 +59,7 @@ def disable_print_wheels(monkeypatch):
def empty_cm(*args, **kwargs): def empty_cm(*args, **kwargs):
yield yield
monkeypatch.setattr(__main__, "print_new_wheels", empty_cm) monkeypatch.setattr(Logger, "print_summary", empty_cm)
@pytest.fixture @pytest.fixture
+1 -1
View File
@@ -45,7 +45,7 @@ def mock_build_container(monkeypatch):
"cibuildwheel.platforms.linux.build_in_container", "cibuildwheel.platforms.linux.build_in_container",
mock.Mock(spec=platforms.linux.build_in_container), mock.Mock(spec=platforms.linux.build_in_container),
) )
monkeypatch.setattr("cibuildwheel.__main__.print_new_wheels", ignore_context_call) monkeypatch.setattr("cibuildwheel.logger.Logger.print_summary", ignore_context_call)
@pytest.mark.usefixtures("mock_build_container", "fake_package_dir") @pytest.mark.usefixtures("mock_build_container", "fake_package_dir")
+25 -20
View File
@@ -1,34 +1,39 @@
from pathlib import Path
import pytest import pytest
from cibuildwheel.__main__ import print_new_wheels from cibuildwheel.logger import BuildInfo, Logger
from cibuildwheel.options import CommandLineArguments, Options
OPTIONS_DEFAULTS = Options("linux", CommandLineArguments.defaults(), {}, defaults=True)
FILE = Path(__file__)
def test_printout_wheels(tmp_path, capsys): def test_printout_wheels(capsys):
tmp_path.joinpath("example.0").touch() log = Logger()
with print_new_wheels("TEST_MSG: {n}", tmp_path): log.fold_mode = "disabled"
tmp_path.joinpath("example.1").write_bytes(b"0" * 1023) log.colors_enabled = False
tmp_path.joinpath("example.2").write_bytes(b"0" * 1025)
with log.print_summary(options=OPTIONS_DEFAULTS):
log.summary = [
BuildInfo(identifier="id1", filename=None, duration=3),
BuildInfo(identifier="id2", filename=FILE, duration=2),
]
captured = capsys.readouterr() captured = capsys.readouterr()
assert captured.err == "" assert captured.err == ""
assert "example.0" not in captured.out assert "id1" in captured.out
assert "example.1 1 kB\n" in captured.out assert "id2" in captured.out
assert "example.2 2 kB\n" in captured.out assert "wheels produced in" in captured.out
assert "TEST_MSG:" in captured.out assert "SHA256=" in captured.out
assert "TEST_MSG: 2\n" in captured.out
def test_no_printout_on_error(tmp_path, capsys): def test_no_printout_on_error(capsys):
tmp_path.joinpath("example.0").touch() log = Logger()
with pytest.raises(RuntimeError), print_new_wheels("TEST_MSG: {n}", tmp_path): # noqa: PT012 with pytest.raises(RuntimeError), log.print_summary(options=OPTIONS_DEFAULTS):
tmp_path.joinpath("example.1").touch()
raise RuntimeError() raise RuntimeError()
captured = capsys.readouterr() captured = capsys.readouterr()
assert captured.err == "" assert captured.err == ""
assert captured.out == ""
assert "example.0" not in captured.out
assert "example.1" not in captured.out
assert "example.2" not in captured.out
assert "TEST_MSG:" not in captured.out