From 4d3fa728745b99ecdd7c472fd8f3f6dabcceea38 Mon Sep 17 00:00:00 2001 From: Anillc Date: Mon, 22 Jun 2026 10:02:41 +0800 Subject: [PATCH 1/5] fix(ci): download wheels from artifacts --- .github/actions/setup-python-deps/action.yml | 62 ++++++++++++++++---- .github/workflows/ci.yml | 11 ++++ .github/workflows/release.yml | 1 + 3 files changed, 61 insertions(+), 13 deletions(-) diff --git a/.github/actions/setup-python-deps/action.yml b/.github/actions/setup-python-deps/action.yml index c92905ac..9d5ead11 100644 --- a/.github/actions/setup-python-deps/action.yml +++ b/.github/actions/setup-python-deps/action.yml @@ -1,6 +1,12 @@ name: Setup Python Dependencies description: Setup Python 3.11, uv, and install project dependencies. +inputs: + build-all: + description: Skip installing prebuilt native wheels and install all packages with uv sync. + required: false + default: "false" + runs: using: composite steps: @@ -16,6 +22,7 @@ runs: enable-cache: "true" - name: Install native tool wheels + if: ${{ inputs.build-all != 'true' }} shell: bash env: GH_TOKEN: ${{ github.token }} @@ -25,24 +32,53 @@ runs: mkdir -p dist/native-wheels uv venv --python 3.11 - dreamplace_version="$(sed -n 's/^version = "\(.*\)"$/\1/p' chipcompiler/thirdparty/ecc-dreamplace/pyproject.toml)" - ecc_tools_version="$(sed -n 's/^version = "\(.*\)"$/\1/p' chipcompiler/thirdparty/ecc-tools/pyproject.toml)" + download_wheel_artifact() { + local repo="$1" + local submodule_path="$2" + local artifact_name="$3" + local commit + local run_id + + commit="$(git rev-parse "HEAD:${submodule_path}")" + run_id="$( + gh api "repos/${repo}/actions/workflows/ci.yml/runs?head_sha=${commit}&per_page=20" \ + --jq '[.workflow_runs[] | select(.conclusion == "success") | .id][0] // empty' + )" + + if [ -z "$run_id" ]; then + echo "::error::No successful CI artifact found for ${repo} at submodule commit ${commit}" + exit 1 + fi + + echo "Downloading ${artifact_name} from ${repo} CI run ${run_id} (${commit})" + gh run download "$run_id" \ + --repo "$repo" \ + --name "$artifact_name" \ + --dir dist/native-wheels + } - gh release download "v$dreamplace_version" \ - --repo "openecos-projects/ecc-dreamplace" \ - --pattern '*.whl' \ - --dir dist/native-wheels + download_wheel_artifact \ + "openecos-projects/ecc-dreamplace" \ + "chipcompiler/thirdparty/ecc-dreamplace" \ + "ecc-dreamplace-wheel" - gh release download "v$ecc_tools_version" \ - --repo "openecos-projects/ecc-tools" \ - --pattern '*.whl' \ - --dir dist/native-wheels + download_wheel_artifact \ + "openecos-projects/ecc-tools" \ + "chipcompiler/thirdparty/ecc-tools" \ + "ecc-tools-wheel" uv pip install --python .venv/bin/python --no-deps dist/native-wheels/*.whl - name: Install Python dependencies shell: bash run: | - uv sync --frozen --all-groups --python 3.11 --inexact \ - --no-install-package ecc-dreamplace \ - --no-install-package ecc-tools-bin + if [ "${{ inputs.build-all }}" = "true" ]; then + uv sync --frozen --all-groups --python 3.11 \ + --no-build-isolation-package ecc-dreamplace \ + --no-build-isolation-package ecc-tools-bin \ + --verbose + else + uv sync --frozen --all-groups --python 3.11 --inexact \ + --no-install-package ecc-dreamplace \ + --no-install-package ecc-tools-bin + fi diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4e391b29..8b6dc392 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,12 @@ name: CI on: workflow_dispatch: + inputs: + build-all: + description: Build native dependencies from source instead of downloading prebuilt native wheels. + required: false + type: boolean + default: false push: branches: [main] pull_request: @@ -27,6 +33,7 @@ concurrency: cancel-in-progress: true permissions: + actions: read contents: read env: @@ -55,6 +62,8 @@ jobs: - name: Setup Python dependencies uses: ./.github/actions/setup-python-deps + with: + build-all: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs['build-all'] == 'true' && 'true' || 'false' }} - name: Setup PDK run: | @@ -105,6 +114,8 @@ jobs: - name: Setup Python dependencies uses: ./.github/actions/setup-python-deps + with: + build-all: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs['build-all'] == 'true' && 'true' || 'false' }} - name: Smoke test native toolchain wheels run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 42781571..c5e086ab 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,6 +11,7 @@ on: default: 'v0.1.0-alpha.3' permissions: + actions: read contents: write jobs: From 193726062d0a2a57822272295eba032d2a268324 Mon Sep 17 00:00:00 2001 From: Anillc Date: Mon, 22 Jun 2026 10:59:20 +0800 Subject: [PATCH 2/5] fix(ci): install dependenices for build --- .github/actions/setup-python-deps/action.yml | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/.github/actions/setup-python-deps/action.yml b/.github/actions/setup-python-deps/action.yml index 9d5ead11..4d14cbaf 100644 --- a/.github/actions/setup-python-deps/action.yml +++ b/.github/actions/setup-python-deps/action.yml @@ -3,7 +3,7 @@ description: Setup Python 3.11, uv, and install project dependencies. inputs: build-all: - description: Skip installing prebuilt native wheels and install all packages with uv sync. + description: Build native dependencies from source instead of downloading prebuilt native wheels. required: false default: "false" @@ -21,6 +21,24 @@ runs: version: latest enable-cache: "true" + - name: Install native build dependencies + if: ${{ inputs.build-all == 'true' }} + shell: bash + run: | + sudo apt-get update + sudo apt-get install -y \ + build-essential \ + cmake ninja-build pkg-config patchelf mold lld \ + libboost-all-dev libcairo2-dev libgflags-dev libgoogle-glog-dev \ + flex libfl-dev bison libeigen3-dev libgtest-dev \ + libtbb-dev libhwloc-dev libcurl4-openssl-dev libunwind-dev \ + libmetis-dev libgmp-dev tcl-dev \ + unzip zip + + - name: Setup Rust + if: ${{ inputs.build-all == 'true' }} + uses: dtolnay/rust-toolchain@stable + - name: Install native tool wheels if: ${{ inputs.build-all != 'true' }} shell: bash From a5dc1ad6a2229889ab62deb367e35fb3a83bc9b6 Mon Sep 17 00:00:00 2001 From: Anillc Date: Mon, 22 Jun 2026 12:03:10 +0800 Subject: [PATCH 3/5] feat(ci): add build cache --- .github/actions/setup-python-deps/action.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/actions/setup-python-deps/action.yml b/.github/actions/setup-python-deps/action.yml index 4d14cbaf..2e2536f2 100644 --- a/.github/actions/setup-python-deps/action.yml +++ b/.github/actions/setup-python-deps/action.yml @@ -39,6 +39,10 @@ runs: if: ${{ inputs.build-all == 'true' }} uses: dtolnay/rust-toolchain@stable + - name: Setup sccache + if: ${{ inputs.build-all == 'true' }} + uses: mozilla-actions/sccache-action@v0.0.9 + - name: Install native tool wheels if: ${{ inputs.build-all != 'true' }} shell: bash @@ -91,6 +95,8 @@ runs: shell: bash run: | if [ "${{ inputs.build-all }}" = "true" ]; then + export SCCACHE_GHA_ENABLED="true" + export CMAKE_ARGS="-DCMAKE_C_COMPILER_LAUNCHER=sccache -DCMAKE_CXX_COMPILER_LAUNCHER=sccache" uv sync --frozen --all-groups --python 3.11 \ --no-build-isolation-package ecc-dreamplace \ --no-build-isolation-package ecc-tools-bin \ From 20ff00b6bb1b251d298a2eb9ad9523e309aea77f Mon Sep 17 00:00:00 2001 From: Anillc Date: Mon, 22 Jun 2026 13:31:01 +0800 Subject: [PATCH 4/5] fix(ci): build with manylinux container --- .github/actions/setup-python-deps/action.yml | 31 ++--- .github/workflows/ci.yml | 2 + .github/workflows/release.yml | 1 + test/cli/commands/test_log.py | 126 +++++++++---------- test/cli/rendering/test_log_view.py | 22 ++-- 5 files changed, 94 insertions(+), 88 deletions(-) diff --git a/.github/actions/setup-python-deps/action.yml b/.github/actions/setup-python-deps/action.yml index 2e2536f2..fe1b9081 100644 --- a/.github/actions/setup-python-deps/action.yml +++ b/.github/actions/setup-python-deps/action.yml @@ -10,10 +10,9 @@ inputs: runs: using: composite steps: - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: "3.11" + - name: Setup Python 3.11 + shell: bash + run: echo "/opt/python/cp311-cp311/bin" >> "$GITHUB_PATH" - name: Setup uv uses: astral-sh/setup-uv@v3 @@ -21,19 +20,23 @@ runs: version: latest enable-cache: "true" + - name: Trust GitHub workspace + shell: bash + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + - name: Install native build dependencies - if: ${{ inputs.build-all == 'true' }} shell: bash run: | - sudo apt-get update - sudo apt-get install -y \ - build-essential \ - cmake ninja-build pkg-config patchelf mold lld \ - libboost-all-dev libcairo2-dev libgflags-dev libgoogle-glog-dev \ - flex libfl-dev bison libeigen3-dev libgtest-dev \ - libtbb-dev libhwloc-dev libcurl4-openssl-dev libunwind-dev \ - libmetis-dev libgmp-dev tcl-dev \ - unzip zip + dnf install -y epel-release dnf-plugins-core + dnf config-manager --set-enabled crb || true + dnf config-manager --add-repo https://cli.github.com/packages/rpm/gh-cli.repo + dnf install -y \ + cmake ninja-build pkgconfig patchelf mold lld \ + boost-devel cairo-devel gflags-devel glog-devel \ + flex flex-devel bison eigen3-devel gtest-devel \ + tbb-devel hwloc-devel libcurl-devel libunwind-devel \ + metis-devel gmp-devel tcl-devel \ + unzip zip gh - name: Setup Rust if: ${{ inputs.build-all == 'true' }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8b6dc392..828ee7d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,6 +54,7 @@ jobs: name: Test needs: check-version runs-on: ubuntu-latest + container: quay.io/pypa/manylinux_2_34_x86_64 steps: - name: Checkout uses: actions/checkout@v4 @@ -106,6 +107,7 @@ jobs: name: Build PyInstaller Bundle needs: check-version runs-on: ubuntu-latest + container: quay.io/pypa/manylinux_2_34_x86_64 steps: - name: Checkout uses: actions/checkout@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c5e086ab..b977a5f7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,6 +33,7 @@ jobs: name: Build CLI Bundle needs: check-version runs-on: ubuntu-22.04 + container: quay.io/pypa/manylinux_2_34_x86_64 steps: - name: Checkout uses: actions/checkout@v4 diff --git a/test/cli/commands/test_log.py b/test/cli/commands/test_log.py index eda44862..e28afc71 100644 --- a/test/cli/commands/test_log.py +++ b/test/cli/commands/test_log.py @@ -533,46 +533,46 @@ def test_artifacts_log_disclosure_no_errors( assert "--errors" not in out -class TestLogUnreadableFile: - """AC-9: Unreadable log files return non-zero with OS error.""" - - def test_unreadable_log_returns_nonzero(self, tmp_path, capsys, create_cli_project): - project_dir = create_cli_project() - run_dir = os.path.join(project_dir, "runs", "default") - step_dir = os.path.join(run_dir, "Synthesis_yosys", "log") - os.makedirs(step_dir, exist_ok=True) - log_path = os.path.join(step_dir, "synthesis.log") - with open(log_path, "w") as f: - f.write("content\n") - os.chmod(log_path, 0o000) - - try: - rc = cli_main.run(["log", "synthesis", "--project", project_dir]) - assert rc == 1 - out = capsys.readouterr().out - assert "unreadable" in out - finally: - os.chmod(log_path, 0o644) - - def test_unreadable_log_jsonl(self, tmp_path, capsys, create_cli_project): - project_dir = create_cli_project() - run_dir = os.path.join(project_dir, "runs", "default") - step_dir = os.path.join(run_dir, "Synthesis_yosys", "log") - os.makedirs(step_dir, exist_ok=True) - log_path = os.path.join(step_dir, "synthesis.log") - with open(log_path, "w") as f: - f.write("content\n") - os.chmod(log_path, 0o000) - - try: - rc = cli_main.run(["log", "synthesis", "--jsonl", "--project", project_dir]) - assert rc == 1 - record = json.loads(capsys.readouterr().out.strip()) - assert record["log_status"] == "unreadable" - assert "source" in record - assert "error" in record - finally: - os.chmod(log_path, 0o644) +# class TestLogUnreadableFile: +# """AC-9: Unreadable log files return non-zero with OS error.""" + +# def test_unreadable_log_returns_nonzero(self, tmp_path, capsys, create_cli_project): +# project_dir = create_cli_project() +# run_dir = os.path.join(project_dir, "runs", "default") +# step_dir = os.path.join(run_dir, "Synthesis_yosys", "log") +# os.makedirs(step_dir, exist_ok=True) +# log_path = os.path.join(step_dir, "synthesis.log") +# with open(log_path, "w") as f: +# f.write("content\n") +# os.chmod(log_path, 0o000) + +# try: +# rc = cli_main.run(["log", "synthesis", "--project", project_dir]) +# assert rc == 1 +# out = capsys.readouterr().out +# assert "unreadable" in out +# finally: +# os.chmod(log_path, 0o644) + +# def test_unreadable_log_jsonl(self, tmp_path, capsys, create_cli_project): +# project_dir = create_cli_project() +# run_dir = os.path.join(project_dir, "runs", "default") +# step_dir = os.path.join(run_dir, "Synthesis_yosys", "log") +# os.makedirs(step_dir, exist_ok=True) +# log_path = os.path.join(step_dir, "synthesis.log") +# with open(log_path, "w") as f: +# f.write("content\n") +# os.chmod(log_path, 0o000) + +# try: +# rc = cli_main.run(["log", "synthesis", "--jsonl", "--project", project_dir]) +# assert rc == 1 +# record = json.loads(capsys.readouterr().out.strip()) +# assert record["log_status"] == "unreadable" +# assert "source" in record +# assert "error" in record +# finally: +# os.chmod(log_path, 0o644) class TestLogMultiSource: @@ -955,26 +955,26 @@ def test_step_jsonl_unchanged(self, tmp_path, capsys, create_cli_project): assert "tail" not in rec -class TestLogListingUnreadable: - """Unreadable logs in listing mode must omit tail, keep path+inspect, no traceback.""" - - def test_unreadable_step_log_in_listing(self, tmp_path, capsys, create_cli_project): - project_dir = create_cli_project() - run_dir = os.path.join(project_dir, "runs", "default") - step_dir = os.path.join(run_dir, "Synthesis_yosys", "log") - os.makedirs(step_dir, exist_ok=True) - log_path = os.path.join(step_dir, "synthesis.log") - with open(log_path, "w") as f: - f.write("content\n") - os.chmod(log_path, 0o000) - - try: - rc = cli_main.run(["log", "--project", project_dir]) - assert rc == 0 - out = capsys.readouterr().out - assert "tail:" not in out - assert "Synthesis_yosys" in out - assert "inspect:" in out - assert "Traceback" not in out - finally: - os.chmod(log_path, 0o644) +# class TestLogListingUnreadable: +# """Unreadable logs in listing mode must omit tail, keep path+inspect, no traceback.""" + +# def test_unreadable_step_log_in_listing(self, tmp_path, capsys, create_cli_project): +# project_dir = create_cli_project() +# run_dir = os.path.join(project_dir, "runs", "default") +# step_dir = os.path.join(run_dir, "Synthesis_yosys", "log") +# os.makedirs(step_dir, exist_ok=True) +# log_path = os.path.join(step_dir, "synthesis.log") +# with open(log_path, "w") as f: +# f.write("content\n") +# os.chmod(log_path, 0o000) + +# try: +# rc = cli_main.run(["log", "--project", project_dir]) +# assert rc == 0 +# out = capsys.readouterr().out +# assert "tail:" not in out +# assert "Synthesis_yosys" in out +# assert "inspect:" in out +# assert "Traceback" not in out +# finally: +# os.chmod(log_path, 0o644) diff --git a/test/cli/rendering/test_log_view.py b/test/cli/rendering/test_log_view.py index aa64f087..84cfecc0 100644 --- a/test/cli/rendering/test_log_view.py +++ b/test/cli/rendering/test_log_view.py @@ -852,17 +852,17 @@ def test_bel_and_backspace_stripped(self, tmp_path): result = tail_lines_for_log(str(log_file)) assert result == ["abc", "done"] - def test_unreadable_file_returns_empty(self, tmp_path): - from chipcompiler.cli.inspection.log_view import tail_lines_for_log - - log_file = tmp_path / "test.log" - log_file.write_text("content\n") - os.chmod(str(log_file), 0o000) - try: - result = tail_lines_for_log(str(log_file)) - assert result == [] - finally: - os.chmod(str(log_file), 0o644) + # def test_unreadable_file_returns_empty(self, tmp_path): + # from chipcompiler.cli.inspection.log_view import tail_lines_for_log + + # log_file = tmp_path / "test.log" + # log_file.write_text("content\n") + # os.chmod(str(log_file), 0o000) + # try: + # result = tail_lines_for_log(str(log_file)) + # assert result == [] + # finally: + # os.chmod(str(log_file), 0o644) class TestListingTailRendering: From 58e810ad0c51e154602c6aec86c9a7b345df8af7 Mon Sep 17 00:00:00 2001 From: Anillc Date: Wed, 24 Jun 2026 16:08:42 +0800 Subject: [PATCH 5/5] fix(test): avoid chmod for unreadable log tests Co-authored-by: Emin --- test/cli/commands/test_log.py | 133 +++++++++++++++------------- test/cli/rendering/test_log_view.py | 25 +++--- 2 files changed, 82 insertions(+), 76 deletions(-) diff --git a/test/cli/commands/test_log.py b/test/cli/commands/test_log.py index e28afc71..e835a126 100644 --- a/test/cli/commands/test_log.py +++ b/test/cli/commands/test_log.py @@ -4,6 +4,18 @@ from chipcompiler.cli import main as cli_main +def _make_path_unreadable(monkeypatch, unreadable_path): + real_open = open + unreadable_path = os.fspath(unreadable_path) + + def open_unless_target(path, *args, **kwargs): + if os.fspath(path) == unreadable_path: + raise PermissionError("cannot read") + return real_open(path, *args, **kwargs) + + monkeypatch.setattr("builtins.open", open_unless_target) + + class TestLog: def test_log_step_errors(self, tmp_path, capsys, create_cli_project): project_dir = create_cli_project() @@ -533,46 +545,42 @@ def test_artifacts_log_disclosure_no_errors( assert "--errors" not in out -# class TestLogUnreadableFile: -# """AC-9: Unreadable log files return non-zero with OS error.""" - -# def test_unreadable_log_returns_nonzero(self, tmp_path, capsys, create_cli_project): -# project_dir = create_cli_project() -# run_dir = os.path.join(project_dir, "runs", "default") -# step_dir = os.path.join(run_dir, "Synthesis_yosys", "log") -# os.makedirs(step_dir, exist_ok=True) -# log_path = os.path.join(step_dir, "synthesis.log") -# with open(log_path, "w") as f: -# f.write("content\n") -# os.chmod(log_path, 0o000) - -# try: -# rc = cli_main.run(["log", "synthesis", "--project", project_dir]) -# assert rc == 1 -# out = capsys.readouterr().out -# assert "unreadable" in out -# finally: -# os.chmod(log_path, 0o644) - -# def test_unreadable_log_jsonl(self, tmp_path, capsys, create_cli_project): -# project_dir = create_cli_project() -# run_dir = os.path.join(project_dir, "runs", "default") -# step_dir = os.path.join(run_dir, "Synthesis_yosys", "log") -# os.makedirs(step_dir, exist_ok=True) -# log_path = os.path.join(step_dir, "synthesis.log") -# with open(log_path, "w") as f: -# f.write("content\n") -# os.chmod(log_path, 0o000) - -# try: -# rc = cli_main.run(["log", "synthesis", "--jsonl", "--project", project_dir]) -# assert rc == 1 -# record = json.loads(capsys.readouterr().out.strip()) -# assert record["log_status"] == "unreadable" -# assert "source" in record -# assert "error" in record -# finally: -# os.chmod(log_path, 0o644) +class TestLogUnreadableFile: + """AC-9: Unreadable log files return non-zero with OS error.""" + + def test_unreadable_log_returns_nonzero( + self, tmp_path, monkeypatch, capsys, create_cli_project + ): + project_dir = create_cli_project() + run_dir = os.path.join(project_dir, "runs", "default") + step_dir = os.path.join(run_dir, "Synthesis_yosys", "log") + os.makedirs(step_dir, exist_ok=True) + log_path = os.path.join(step_dir, "synthesis.log") + with open(log_path, "w") as f: + f.write("content\n") + _make_path_unreadable(monkeypatch, log_path) + + rc = cli_main.run(["log", "synthesis", "--project", project_dir]) + assert rc == 1 + out = capsys.readouterr().out + assert "unreadable" in out + + def test_unreadable_log_jsonl(self, tmp_path, monkeypatch, capsys, create_cli_project): + project_dir = create_cli_project() + run_dir = os.path.join(project_dir, "runs", "default") + step_dir = os.path.join(run_dir, "Synthesis_yosys", "log") + os.makedirs(step_dir, exist_ok=True) + log_path = os.path.join(step_dir, "synthesis.log") + with open(log_path, "w") as f: + f.write("content\n") + _make_path_unreadable(monkeypatch, log_path) + + rc = cli_main.run(["log", "synthesis", "--jsonl", "--project", project_dir]) + assert rc == 1 + record = json.loads(capsys.readouterr().out.strip()) + assert record["log_status"] == "unreadable" + assert "source" in record + assert "error" in record class TestLogMultiSource: @@ -955,26 +963,25 @@ def test_step_jsonl_unchanged(self, tmp_path, capsys, create_cli_project): assert "tail" not in rec -# class TestLogListingUnreadable: -# """Unreadable logs in listing mode must omit tail, keep path+inspect, no traceback.""" - -# def test_unreadable_step_log_in_listing(self, tmp_path, capsys, create_cli_project): -# project_dir = create_cli_project() -# run_dir = os.path.join(project_dir, "runs", "default") -# step_dir = os.path.join(run_dir, "Synthesis_yosys", "log") -# os.makedirs(step_dir, exist_ok=True) -# log_path = os.path.join(step_dir, "synthesis.log") -# with open(log_path, "w") as f: -# f.write("content\n") -# os.chmod(log_path, 0o000) - -# try: -# rc = cli_main.run(["log", "--project", project_dir]) -# assert rc == 0 -# out = capsys.readouterr().out -# assert "tail:" not in out -# assert "Synthesis_yosys" in out -# assert "inspect:" in out -# assert "Traceback" not in out -# finally: -# os.chmod(log_path, 0o644) +class TestLogListingUnreadable: + """Unreadable logs in listing mode must omit tail, keep path+inspect, no traceback.""" + + def test_unreadable_step_log_in_listing( + self, tmp_path, monkeypatch, capsys, create_cli_project + ): + project_dir = create_cli_project() + run_dir = os.path.join(project_dir, "runs", "default") + step_dir = os.path.join(run_dir, "Synthesis_yosys", "log") + os.makedirs(step_dir, exist_ok=True) + log_path = os.path.join(step_dir, "synthesis.log") + with open(log_path, "w") as f: + f.write("content\n") + _make_path_unreadable(monkeypatch, log_path) + + rc = cli_main.run(["log", "--project", project_dir]) + assert rc == 0 + out = capsys.readouterr().out + assert "tail:" not in out + assert "Synthesis_yosys" in out + assert "inspect:" in out + assert "Traceback" not in out diff --git a/test/cli/rendering/test_log_view.py b/test/cli/rendering/test_log_view.py index 84cfecc0..18e8c49b 100644 --- a/test/cli/rendering/test_log_view.py +++ b/test/cli/rendering/test_log_view.py @@ -1,5 +1,3 @@ -import os - from chipcompiler.cli.inspection.log_view import ( LineKind, annotate_log_lines, @@ -852,17 +850,18 @@ def test_bel_and_backspace_stripped(self, tmp_path): result = tail_lines_for_log(str(log_file)) assert result == ["abc", "done"] - # def test_unreadable_file_returns_empty(self, tmp_path): - # from chipcompiler.cli.inspection.log_view import tail_lines_for_log - - # log_file = tmp_path / "test.log" - # log_file.write_text("content\n") - # os.chmod(str(log_file), 0o000) - # try: - # result = tail_lines_for_log(str(log_file)) - # assert result == [] - # finally: - # os.chmod(str(log_file), 0o644) + def test_unreadable_file_returns_empty(self, monkeypatch, tmp_path): + from chipcompiler.cli.inspection.log_view import tail_lines_for_log + + log_file = tmp_path / "test.log" + log_file.write_text("content\n") + + def raise_permission_error(*args, **kwargs): + raise PermissionError("cannot read") + + monkeypatch.setattr("builtins.open", raise_permission_error) + result = tail_lines_for_log(str(log_file)) + assert result == [] class TestListingTailRendering: