# Copyright Kevin Deldycke <[email protected]> and contributors.
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
"""Tests for :mod:`repomatic.github.outputs` step outputs and annotations."""
from __future__ import annotations
import sys
from pathlib import Path
import pytest
from repomatic.github.outputs import (
MAX_STEP_OUTPUT_BYTES,
AnnotationLevel,
emit_report,
format_file_output,
format_multiline_output,
read_file_output,
report_finding,
trim_to_byte_budget,
write_output_file,
write_text_output,
)
# -- AnnotationLevel ----------------------------------------------------------
[docs]
@pytest.mark.parametrize(
("level", "glyph"),
(
(AnnotationLevel.ERROR, "β"),
(AnnotationLevel.WARNING, "β "),
(AnnotationLevel.NOTICE, "βΉ"),
),
)
def test_annotation_level_glyph(level, glyph):
"""Each level opens its terminal line with its own symbol."""
assert level.glyph == glyph
[docs]
@pytest.mark.parametrize("level", AnnotationLevel)
def test_report_finding_annotates_then_prints(level, capsys):
"""The annotation and the terminal line always agree on the level."""
report_finding(level, "Mango bruised.")
assert capsys.readouterr().out == (
f"::{level.value}::Mango bruised.\n{level.glyph} Mango bruised.\n"
)
# -- Step output size guard ---------------------------------------------------
def _output_value(formatted):
"""Recover the heredoc payload the runner would assign to the variable."""
return "\n".join(formatted.split("\n")[1:-1])
[docs]
def test_trim_to_byte_budget_cuts_on_line_boundaries():
"""A budget landing mid-line drops that line rather than splitting it."""
text = "mango\nkiwi\npapaya"
# Room for "mango\nkiwi\n" and one byte of "papaya".
assert trim_to_byte_budget(text, 12) == "mango\nkiwi"
[docs]
def test_trim_to_byte_budget_returns_empty_when_nothing_fits():
"""A budget too small for even the first line yields nothing."""
assert trim_to_byte_budget("watermelon\nkiwi", 3) == ""
# -- File-backed step outputs -------------------------------------------------
[docs]
def test_write_output_file_lands_in_the_runner_temp_dir(tmp_path, monkeypatch):
"""The runner's own temp dir holds the spill when it exports one."""
monkeypatch.setenv("RUNNER_TEMP", str(tmp_path))
spilled = write_output_file("harvest", "mango")
assert spilled.parent == tmp_path
assert spilled.name.startswith("repomatic-harvest-")
assert spilled.read_text(encoding="UTF-8") == "mango"
[docs]
def test_read_file_output_reads_the_file_the_variable_names(tmp_path, monkeypatch):
"""The `_FILE` variable names the file whose content is the value."""
spilled = tmp_path / "harvest.md"
spilled.write_text("mango", encoding="UTF-8")
monkeypatch.setenv("HARVEST_FILE", str(spilled))
assert read_file_output("HARVEST") == "mango"
[docs]
def test_read_file_output_ignores_an_inline_value(monkeypatch):
"""Only the path counts: a value passed inline is not read."""
monkeypatch.delenv("HARVEST_FILE", raising=False)
monkeypatch.setenv("HARVEST", "kiwi")
assert read_file_output("HARVEST") == ""
[docs]
def test_file_output_round_trips(tmp_path, monkeypatch):
"""What the producer spills is what the consumer reads back."""
monkeypatch.setenv("RUNNER_TEMP", str(tmp_path))
report = "| Fruit | Crate |\n| :-- | :-- |\n| mango | 10 |"
_, _, path = format_file_output("harvest", report).partition("=")
monkeypatch.setenv("HARVEST_FILE", path)
assert read_file_output("HARVEST") == report
# -- Report writes ------------------------------------------------------------
[docs]
def test_emit_report_writes_scalar_outputs_beside_the_path(tmp_path, monkeypatch):
"""Every output of the step lands in the one write the output file gets."""
monkeypatch.setenv("RUNNER_TEMP", str(tmp_path))
output = tmp_path / "github-output"
emit_report(
"Ten crates of mango.",
output,
"github-actions",
key="harvest",
outputs={"season": "summer", "orchard": "Seville"},
)
lines = output.read_text(encoding="UTF-8").splitlines()
assert lines[0].startswith("harvest_file=")
assert Path(lines[0].partition("=")[2]).read_text(encoding="UTF-8") == (
"Ten crates of mango."
)
assert lines[1:] == ["season=summer", "orchard=Seville"]
[docs]
def test_emit_report_keeps_scalar_outputs_out_of_markdown(tmp_path):
"""A markdown report is the report alone."""
output = tmp_path / "report.md"
emit_report(
"Ten crates of mango.", output, "markdown", outputs={"season": "summer"}
)
assert output.read_text(encoding="UTF-8") == "Ten crates of mango.\n"
[docs]
@pytest.mark.parametrize(("body", "output"), (("", "report.md"), ("Mango.", None)))
def test_emit_report_writes_nothing_without_a_body_or_a_path(tmp_path, body, output):
"""An empty report and a missing `--output` both leave the disk alone."""
target = tmp_path / "reports"
target.mkdir()
emit_report(body, target / output if output else None, "markdown")
assert list(target.iterdir()) == []
[docs]
def test_write_text_output_closes_the_file_it_opens(tmp_path, monkeypatch):
"""The stream opened for a path is closed once written, flushing it."""
opened = []
real_open = Path.open
def spy(self, *args, **kwargs):
stream = real_open(self, *args, **kwargs)
opened.append(stream)
return stream
monkeypatch.setattr(Path, "open", spy)
output = tmp_path / "report.md"
write_text_output("Ten crates of mango.", output)
assert [stream.closed for stream in opened] == [True]
assert output.read_text(encoding="UTF-8") == "Ten crates of mango.\n"
[docs]
def test_write_text_output_leaves_stdout_open(capsys):
"""Writing to `-` never closes the live standard output."""
write_text_output("Ten crates of mango.", Path("-"))
assert not sys.stdout.closed
assert capsys.readouterr().out == "Ten crates of mango.\n"