|
| 1 | +"""Regression: uncompress_gds must surface real make failures, not swallow them. |
| 2 | +
|
| 3 | +The previous implementation captured stderr/stdout into PIPE and threw them |
| 4 | +away, used ``subprocess.run`` without ``check=True`` (so its |
| 5 | +``except CalledProcessError`` block was dead code), and returned silently on |
| 6 | +ANY make failure. That meant Fargate-only failures (e.g. disk pressure during |
| 7 | +gunzip on a heavy repo) bubbled up as the misleading downstream error |
| 8 | +"A single valid GDS was not found" with no information about why. See |
| 9 | +chipfoundry/cf-cli#20. |
| 10 | +""" |
| 11 | + |
| 12 | +from __future__ import annotations |
| 13 | + |
| 14 | +import logging |
| 15 | +import subprocess |
| 16 | +from pathlib import Path |
| 17 | +from unittest.mock import patch |
| 18 | + |
| 19 | +import pytest |
| 20 | + |
| 21 | +from cf_precheck.config import uncompress_gds |
| 22 | + |
| 23 | + |
| 24 | +def _completed(returncode: int, stderr: bytes = b"", stdout: bytes = b"") -> subprocess.CompletedProcess: |
| 25 | + return subprocess.CompletedProcess( |
| 26 | + args="make ... uncompress", |
| 27 | + returncode=returncode, |
| 28 | + stderr=stderr, |
| 29 | + stdout=stdout, |
| 30 | + ) |
| 31 | + |
| 32 | + |
| 33 | +def test_success_when_gds_files_present_regardless_of_exit_code( |
| 34 | + tmp_path: Path, caplog: pytest.LogCaptureFixture |
| 35 | +) -> None: |
| 36 | + """Caravel-lite Makefile legitimately exits 2 on the mag/ step after the |
| 37 | + gds/ step succeeds. As long as gds/*.gds is on disk, treat as success.""" |
| 38 | + (tmp_path / "gds").mkdir() |
| 39 | + (tmp_path / "gds" / "user_project_wrapper.gds").write_bytes(b"GDSII") |
| 40 | + (tmp_path / "gds" / "other.gds").write_bytes(b"GDSII") |
| 41 | + |
| 42 | + with patch.object( |
| 43 | + subprocess, "run", |
| 44 | + return_value=_completed(2, stderr=b"gzip: mag/user_project_wrapper.mag already exists"), |
| 45 | + ), caplog.at_level(logging.INFO): |
| 46 | + uncompress_gds(tmp_path, Path("/opt/caravel")) |
| 47 | + |
| 48 | + info_msgs = [r.message for r in caplog.records if r.levelno == logging.INFO] |
| 49 | + assert any("produced 2 gds/*.gds file" in m for m in info_msgs), info_msgs |
| 50 | + assert any("user_project_wrapper.gds" in m for m in info_msgs), info_msgs |
| 51 | + assert any("other.gds" in m for m in info_msgs), info_msgs |
| 52 | + assert any("make exit 2" in m for m in info_msgs), info_msgs |
| 53 | + |
| 54 | + |
| 55 | +def test_critical_with_stderr_when_make_fails_and_no_gds( |
| 56 | + tmp_path: Path, caplog: pytest.LogCaptureFixture |
| 57 | +) -> None: |
| 58 | + """Real make failure (e.g. disk full, missing tool) on a repo with no |
| 59 | + pre-existing gds/*.gds — surface stderr + returncode, then exit 252.""" |
| 60 | + (tmp_path / "gds").mkdir() |
| 61 | + stderr = b"gunzip: write error: No space left on device\n" |
| 62 | + |
| 63 | + with patch.object( |
| 64 | + subprocess, "run", return_value=_completed(2, stderr=stderr), |
| 65 | + ), caplog.at_level(logging.CRITICAL): |
| 66 | + with pytest.raises(SystemExit) as excinfo: |
| 67 | + uncompress_gds(tmp_path, Path("/opt/caravel")) |
| 68 | + |
| 69 | + assert excinfo.value.code == 252 |
| 70 | + crit_msgs = [r.message for r in caplog.records if r.levelno == logging.CRITICAL] |
| 71 | + assert any("make uncompress failed" in m for m in crit_msgs), crit_msgs |
| 72 | + assert any("No space left on device" in m for m in crit_msgs), crit_msgs |
| 73 | + assert any("exit 2" in m for m in crit_msgs), crit_msgs |
| 74 | + |
| 75 | + |
| 76 | +def test_critical_when_make_succeeds_but_no_gds_produced( |
| 77 | + tmp_path: Path, caplog: pytest.LogCaptureFixture |
| 78 | +) -> None: |
| 79 | + """make exits 0 (e.g. no .gds.gz inputs to operate on) but no .gds files |
| 80 | + appear: cf-precheck cannot proceed; surface a clear "produced no gds" |
| 81 | + message instead of the misleading downstream "no valid GDS" error.""" |
| 82 | + (tmp_path / "gds").mkdir() |
| 83 | + |
| 84 | + with patch.object( |
| 85 | + subprocess, "run", return_value=_completed(0, stdout=b"Nothing to do for 'uncompress'."), |
| 86 | + ), caplog.at_level(logging.CRITICAL): |
| 87 | + with pytest.raises(SystemExit) as excinfo: |
| 88 | + uncompress_gds(tmp_path, Path("/opt/caravel")) |
| 89 | + |
| 90 | + assert excinfo.value.code == 252 |
| 91 | + crit_msgs = [r.message for r in caplog.records if r.levelno == logging.CRITICAL] |
| 92 | + assert any("reported success" in m for m in crit_msgs), crit_msgs |
| 93 | + assert any("produced no" in m for m in crit_msgs), crit_msgs |
| 94 | + |
| 95 | + |
| 96 | +def test_missing_gds_dir_treated_as_no_gds( |
| 97 | + tmp_path: Path, caplog: pytest.LogCaptureFixture |
| 98 | +) -> None: |
| 99 | + """If gds/ doesn't exist at all (malformed repo), still surface a clear |
| 100 | + error rather than silently passing or KeyError'ing later.""" |
| 101 | + with patch.object( |
| 102 | + subprocess, "run", |
| 103 | + return_value=_completed(0, stdout=b"make: *** No rule to make target 'uncompress'."), |
| 104 | + ), caplog.at_level(logging.CRITICAL): |
| 105 | + with pytest.raises(SystemExit) as excinfo: |
| 106 | + uncompress_gds(tmp_path, Path("/opt/caravel")) |
| 107 | + |
| 108 | + assert excinfo.value.code == 252 |
0 commit comments