fix: guard FDCapture.snap() against prematurely closed tmpfile #6
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: "validate #14528 (Windows capture teardown crash)" | |
| on: | |
| push: | |
| branches: | |
| - "fix/14528-*" | |
| pull_request: | |
| branches: | |
| - main | |
| permissions: {} | |
| jobs: | |
| reproduce: | |
| name: "validate-14528 (windows-py314)" | |
| runs-on: windows-latest | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 | |
| with: | |
| python-version: "3.14.3" | |
| allow-prereleases: true | |
| - name: Install pytest from this branch | |
| run: pip install -e . | |
| - name: Verify setup | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | |
| with: | |
| script: | | |
| const { execSync } = require('child_process'); | |
| const checks = [ | |
| 'python --version', | |
| 'python -m pytest --version', | |
| 'python -c "import sys; print(sys.platform, sys.version)"', | |
| ]; | |
| for (const cmd of checks) { | |
| const out = execSync(cmd, { encoding: 'utf-8' }).trim(); | |
| core.info(`${cmd} → ${out}`); | |
| } | |
| - name: Create reproducer tree and run diagnostics | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| const { execSync } = require('child_process'); | |
| // Create reproducer tree matching issue #14528 | |
| const base = path.join('.claude', 'skills', 'actionmail', 'tests'); | |
| fs.mkdirSync(path.join(base, 'fixtures'), { recursive: true }); | |
| fs.writeFileSync(path.join(base, '__init__.py'), ''); | |
| fs.writeFileSync(path.join(base, 'fixtures', 'data.json'), '{"key": "value"}'); | |
| for (const name of ['test_v3_heuristics.py', 'test_v3_integration.py']) { | |
| fs.writeFileSync(path.join(base, name), [ | |
| 'import sys, pathlib', | |
| 'SKILL_DIR = str(pathlib.Path(__file__).parent.parent)', | |
| 'sys.path.insert(0, SKILL_DIR)', | |
| 'import run', | |
| `def test_${name.replace('.py', '')}(): pass`, | |
| ].join('\n')); | |
| } | |
| core.info(`Reproducer tree: ${path.resolve(base)}`); | |
| // Artifacts directory | |
| const artifacts = 'trace-artifacts'; | |
| fs.mkdirSync(artifacts, { recursive: true }); | |
| // Helper: run a command, capture output, save to file, log exit code | |
| function run(label, cmd, env = {}) { | |
| const outFile = path.join(artifacts, label + '.txt'); | |
| core.info(`--- ${label} ---`); | |
| core.info(`cmd: ${cmd}`); | |
| let stdout, exitCode; | |
| try { | |
| stdout = execSync(cmd, { | |
| encoding: 'utf-8', | |
| env: { ...process.env, ...env }, | |
| timeout: 60_000, | |
| }); | |
| exitCode = 0; | |
| } catch (err) { | |
| stdout = (err.stdout || '') + '\n--- STDERR ---\n' + (err.stderr || ''); | |
| exitCode = err.status; | |
| } | |
| const content = `# ${label}\n# cmd: ${cmd}\n# exit_code: ${exitCode}\n\n${stdout}`; | |
| fs.writeFileSync(outFile, content); | |
| core.info(`exit_code=${exitCode} saved=${outFile}`); | |
| return { stdout, exitCode }; | |
| } | |
| const target = '.claude/skills/actionmail/tests/'; | |
| // 1. Normal pytest run (the actual issue scenario) | |
| run('01-pytest-normal', | |
| `python -m pytest ${target} -v --tb=long`); | |
| // 2. pytest with --collect-only | |
| run('02-pytest-collect-only', | |
| `python -m pytest ${target} --collect-only -v --tb=long`); | |
| // 3. pytest with capture disabled (reporter says this suppresses crash) | |
| run('03-pytest-no-capture', | |
| `python -m pytest ${target} -v -s --tb=long`); | |
| // 4. Python -v for import tracing during pytest | |
| run('04-pytest-python-verbose', | |
| `python -v -m pytest ${target} -v --tb=long`); | |
| // 5. Python with faulthandler and GC debug | |
| run('05-pytest-gc-debug', | |
| `python -c "import gc; gc.set_debug(gc.DEBUG_STATS); import pytest; pytest.main(['${target.replace(/\\/g, '/')}', '-v', '--tb=long'])"`); | |
| // 6. Python version and GC info | |
| run('06-python-info', | |
| `python -c "import sys, gc; print('version:', sys.version); print('platform:', sys.platform); print('gc.get_threshold:', gc.get_threshold()); print('gc.get_stats:', gc.get_stats())"`); | |
| // 7. Trace EncodedFile.close() calls via monkeypatch | |
| const traceScript = ` | |
| import os, sys, traceback | |
| from _pytest.capture import EncodedFile | |
| _orig_close = EncodedFile.close | |
| _trace_fd = os.open('trace-artifacts/07-close-trace-raw.txt', os.O_WRONLY | os.O_CREAT | os.O_TRUNC) | |
| def _traced_close(self): | |
| msg = ( | |
| f"\\n=== EncodedFile.close() closed={self.closed} ===\\n" | |
| + "".join(traceback.format_stack()) | |
| ) | |
| os.write(_trace_fd, msg.encode()) | |
| return _orig_close(self) | |
| EncodedFile.close = _traced_close | |
| import pytest | |
| code = pytest.main(['${target.replace(/\\/g, '/')}', '-v', '--tb=long']) | |
| os.close(_trace_fd) | |
| sys.exit(code) | |
| `; | |
| fs.writeFileSync('_trace_close.py', traceScript); | |
| run('07-pytest-traced-close', | |
| 'python _trace_close.py'); | |
| - name: Upload trace artifacts | |
| if: always() | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | |
| with: | |
| name: issue-14528-traces | |
| path: trace-artifacts/ | |
| retention-days: 30 |