fix: tools/bugreport launcher finds a Python 3.8+ itself and never starts a Store alias (#166)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SnAJ7Z3CpVD3PRbN73QtU2 Files changed: - .gitea/workflows/ci.yml - CHANGES.md - INSTALL.md - VERSION - instructions/bug-report.md - tools/README.md - tools/bugreport - tools/bugreport.ps1 - tools/bugreport.py - tools/chemenu/tests/test_bugreport_launcher.py - tools/chemenu/tests/test_instructions_shell.py - tools/chemenu/tests/test_portability.py
This commit is contained in:
1 parent
c77bda2004
commit
5d937233b1
12 files changed
+539
-21
No files matched your search
@@ -307,12 +307,13 @@ jobs:
|
|||||||
- name: The bug-report collector works in the distribution
|
- name: The bug-report collector works in the distribution
|
||||||
# tools/bugreport.py is the one part of the stack that must run when
|
# tools/bugreport.py is the one part of the stack that must run when
|
||||||
# nothing else does, so it is exercised as shipped: from the export,
|
# nothing else does, so it is exercised as shipped: from the export,
|
||||||
# on the runner's plain python3, in the instance the step above set up.
|
# through the tools/bugreport launcher the instructions name, which picks
|
||||||
|
# the runner's plain python3, in the instance the step above set up.
|
||||||
# The unit tests cover what it collects; this covers that it arrives.
|
# The unit tests cover what it collects; this covers that it arrives.
|
||||||
run: |
|
run: |
|
||||||
set -eu
|
set -eu
|
||||||
cd "$INSTANCE_DIR"
|
cd "$INSTANCE_DIR"
|
||||||
python3 tools/bugreport.py --no-trace
|
tools/bugreport --no-trace
|
||||||
bundle=$(ls -d reports/bugreport-*/ | head -n 1)
|
bundle=$(ls -d reports/bugreport-*/ | head -n 1)
|
||||||
test -f "$bundle/MANIFEST.md"
|
test -f "$bundle/MANIFEST.md"
|
||||||
test -f "$bundle/environment.json"
|
test -f "$bundle/environment.json"
|
||||||
@@ -321,7 +322,7 @@ jobs:
|
|||||||
# The same collector with --pseudonymise: the mapping lies beside the
|
# The same collector with --pseudonymise: the mapping lies beside the
|
||||||
# bundle, not in it or its zip, and this container's hostname is gone.
|
# bundle, not in it or its zip, and this container's hostname is gone.
|
||||||
rm -rf reports/bugreport-*
|
rm -rf reports/bugreport-*
|
||||||
python3 tools/bugreport.py --no-trace --pseudonymise
|
tools/bugreport --no-trace --pseudonymise
|
||||||
bundle=$(ls -d reports/bugreport-*/ | head -n 1)
|
bundle=$(ls -d reports/bugreport-*/ | head -n 1)
|
||||||
bundle=${bundle%/}
|
bundle=${bundle%/}
|
||||||
test -f "$bundle.pseudonyms.json"
|
test -f "$bundle.pseudonyms.json"
|
||||||
|
|||||||
+26
-1
@@ -59,7 +59,7 @@ concern - readable here, never shipped as something to parse.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8.0.0-beta.21 - 2026-10-02 - INSTALL.md an die Installationsinstruktionen gekoppelt: Voraussetzungen generiert, Setup-Fragen geprüft
|
## 8.0.0-beta.22 - 2026-10-02 - tools/bugreport: Starter für den Bugreport-Sammler, überspringt die Store-Aliase (#166)
|
||||||
|
|
||||||
**Author:** Torben Nehmer
|
**Author:** Torben Nehmer
|
||||||
|
|
||||||
@@ -96,6 +96,7 @@ concern - readable here, never shipped as something to parse.
|
|||||||
- trace-hook.ps1: Copilot hooks no longer open Windows' choose-an-app dialog
|
- trace-hook.ps1: Copilot hooks no longer open Windows' choose-an-app dialog
|
||||||
- Windows-Portabilität: Pfadtrenner, Zeilenenden, Encoding und Locks
|
- Windows-Portabilität: Pfadtrenner, Zeilenenden, Encoding und Locks
|
||||||
- INSTALL.md an die Installationsinstruktionen gekoppelt: Voraussetzungen generiert, Setup-Fragen geprüft
|
- INSTALL.md an die Installationsinstruktionen gekoppelt: Voraussetzungen generiert, Setup-Fragen geprüft
|
||||||
|
- tools/bugreport: Starter für den Bugreport-Sammler, überspringt die Store-Aliase (#166)
|
||||||
|
|
||||||
**Low impact**
|
**Low impact**
|
||||||
- version bump no longer points at version release in its output
|
- version bump no longer points at version release in its output
|
||||||
@@ -133,6 +134,30 @@ concern - readable here, never shipped as something to parse.
|
|||||||
- preflight.ps1: the asset-mode error helper is Exit-Asset, so PSScriptAnalyzer passes
|
- preflight.ps1: the asset-mode error helper is Exit-Asset, so PSScriptAnalyzer passes
|
||||||
<!-- /wikitool:bumps -->
|
<!-- /wikitool:bumps -->
|
||||||
|
|
||||||
|
### `tools/bugreport`: the collector finds its own Python (#166)
|
||||||
|
|
||||||
|
The bug-report collector is the one part of the stack that has to run when nothing else does,
|
||||||
|
and it was started as `python3 tools/bugreport.py`. On the Windows target that name is the
|
||||||
|
Microsoft Store's alias in Git Bash, and in PowerShell `python` can be one too - so the report
|
||||||
|
failed exactly where it was needed.
|
||||||
|
|
||||||
|
It now has a launcher pair after the pattern of `tools/wikitool`: `tools/bugreport` (sh, for
|
||||||
|
Linux, macOS and Git Bash) and `tools/bugreport.ps1`, which PowerShell resolves the same string
|
||||||
|
to first. They look for a Python the way the preflight does - `python3`, `python` on `PATH`; on
|
||||||
|
Windows `python`, `py -3`, `python3`, with the registry's `PATH` added under PowerShell - and then
|
||||||
|
try the venv's. Each candidate is probed for 3.8 or newer before it runs anything, and nothing
|
||||||
|
under `WindowsApps` is ever started. One that is too old, or that the machine refuses (a venv
|
||||||
|
`python.exe` blocked by Defender), is passed over. Finding none, the launcher exits 1 and says
|
||||||
|
why, naming any Python it found too old and the direct call with a full path as the way out.
|
||||||
|
Neither launcher assumes the preflight, `.wikitool-tools.json` or the venv, and the PowerShell
|
||||||
|
one keeps to what Windows PowerShell 5.1 understands, so a report never fails on the shell's
|
||||||
|
version first.
|
||||||
|
|
||||||
|
`instructions/bug-report.md`, `INSTALL.md` § Troubleshooting, the stage-2 line the collector
|
||||||
|
prints and the CI step that runs it in the distribution all use `tools/bugreport`. The shell test
|
||||||
|
for shipped instructions now refuses a bare `python3`/`python`/`py` call in a command block.
|
||||||
|
Starting `bugreport.py` directly still works.
|
||||||
|
|
||||||
### INSTALL.md held to the installation instructions (#154)
|
### INSTALL.md held to the installation instructions (#154)
|
||||||
|
|
||||||
The installation procedure has one source, the instructions under `instructions/`; `INSTALL.md`
|
The installation procedure has one source, the instructions under `instructions/`; `INSTALL.md`
|
||||||
|
|||||||
+5
-3
@@ -490,9 +490,11 @@ tools/wikitool instructions verify
|
|||||||
anfasst, um an der Verweigerung vorbeizukommen, öffnet ein Gate aus eigenem Antrieb.
|
anfasst, um an der Verweigerung vorbeizukommen, öffnet ein Gate aus eigenem Antrieb.
|
||||||
- **Setup oder Update schlägt fehl und ich will es melden** - den Agenten
|
- **Setup oder Update schlägt fehl und ich will es melden** - den Agenten
|
||||||
[instructions/bug-report.md](instructions/bug-report.md) ausführen lassen, oder direkt
|
[instructions/bug-report.md](instructions/bug-report.md) ausführen lassen, oder direkt
|
||||||
`python3 tools/bugreport.py`. Das Skript braucht nur Python 3.8 oder neuer, läuft auch ohne
|
`tools/bugreport` aufrufen (aus Bash, Git Bash und PowerShell gleich). Der Aufruf sucht sich
|
||||||
funktionierendes `wikitool` und schreibt ein Bündel nach `reports/bugreport-<Zeitstempel>/`
|
selbst ein Python 3.8 oder neuer und überspringt dabei die Store-Aliase, auf die `python3`
|
||||||
samt Zip. Geheimnisse werden entfernt, und aus allem, was das Skript selbst erzeugt, bleiben
|
unter Windows zeigt. Er braucht weder den Preflight noch ein funktionierendes `wikitool`, und
|
||||||
|
findet er kein Python, sagt er, warum. Das Skript schreibt ein Bündel nach
|
||||||
|
`reports/bugreport-<Zeitstempel>/` samt Zip. Geheimnisse werden entfernt, und aus allem, was das Skript selbst erzeugt, bleiben
|
||||||
Seitentitel draußen (`--titles` nimmt sie mit). Der Sitzungs-Trace ist standardmäßig dabei
|
Seitentitel draußen (`--titles` nimmt sie mit). Der Sitzungs-Trace ist standardmäßig dabei
|
||||||
(`--no-trace` lässt ihn weg) und kann wie Chronologie und Transkripte Seiteninhalt und Titel
|
(`--no-trace` lässt ihn weg) und kann wie Chronologie und Transkripte Seiteninhalt und Titel
|
||||||
enthalten; das Bündel enthält außerdem Maschinen-, Benutzer- und Pfadnamen. Mit `--pseudonymise`
|
enthalten; das Bündel enthält außerdem Maschinen-, Benutzer- und Pfadnamen. Mit `--pseudonymise`
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
type: types/instruction.md
|
type: types/instruction.md
|
||||||
name: bug-report
|
name: bug-report
|
||||||
description: How to collect a bug-report bundle when the stack misbehaves on this machine - run tools/bugreport.py, write a fact-only chronology, tell the user what the bundle contains, and stop short of sending it anywhere.
|
description: How to collect a bug-report bundle when the stack misbehaves on this machine - run tools/bugreport, write a fact-only chronology, tell the user what the bundle contains, and stop short of sending it anywhere.
|
||||||
manual: true
|
manual: true
|
||||||
---
|
---
|
||||||
# Collect a bug report
|
# Collect a bug report
|
||||||
@@ -32,7 +32,9 @@ the pointers at the failure decision points of [setup-instance.md](setup-instanc
|
|||||||
|
|
||||||
`tools/bugreport.py` is a standalone script. It uses the standard library only and imports nothing
|
`tools/bugreport.py` is a standalone script. It uses the standard library only and imports nothing
|
||||||
from the stack, so it still runs when `wikitool` does not start - no venv, a broken package, a Python
|
from the stack, so it still runs when `wikitool` does not start - no venv, a broken package, a Python
|
||||||
that is too old for the stack. It writes `reports/bugreport-<UTC stamp>/` and a zip beside it, in
|
that is too old for the stack. Its launcher `tools/bugreport` assumes no more than that: it looks for
|
||||||
|
a Python 3.8 or later on `PATH` itself, skipping the Microsoft Store's aliases, and needs neither
|
||||||
|
the preflight nor `.wikitool-tools.json`. It writes `reports/bugreport-<UTC stamp>/` and a zip beside it, in
|
||||||
four layers:
|
four layers:
|
||||||
|
|
||||||
| Layer | Files | Holds |
|
| Layer | Files | Holds |
|
||||||
@@ -94,16 +96,20 @@ session. The bundle does not leave that place because of it.
|
|||||||
[template](#chronology-template) below. Facts only - no diagnosis. If the session's own history is
|
[template](#chronology-template) below. Facts only - no diagnosis. If the session's own history is
|
||||||
too long to reconstruct, say what is missing instead of filling the gap.
|
too long to reconstruct, say what is missing instead of filling the gap.
|
||||||
|
|
||||||
3. **Run the collector**, with whichever Python 3.8 or later the machine has:
|
3. **Run the collector** through its launcher, which finds a Python 3.8 or later itself:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 tools/bugreport.py --chronology reports/chronology.md
|
tools/bugreport --chronology reports/chronology.md
|
||||||
```
|
```
|
||||||
|
|
||||||
Add `--no-trace` if the user declined the trace, `--titles` if titles may stay,
|
Add `--no-trace` if the user declined the trace, `--titles` if titles may stay,
|
||||||
`--transcript <file>` (repeatable) for a transcript the user pointed at, `--session <id>` to take a
|
`--transcript <file>` (repeatable) for a transcript the user pointed at, `--session <id>` to take a
|
||||||
trace other than the caller's, `--pseudonymise` if the user chose it (stage 1). `python3` may be `python` or `py -3` on Windows. If no Python starts
|
trace other than the caller's, `--pseudonymise` if the user chose it (stage 1). Do not call a Python yourself: on Windows,
|
||||||
at all, that is the report: give the user the exact error text, verbatim.
|
`python3` - and in PowerShell also `python` - can be the Microsoft Store's alias, which the
|
||||||
|
launcher skips. If the launcher finds no Python, it exits 1 and says why; that is the report -
|
||||||
|
give the user its text verbatim. If the user then names a working Python by its full path, run
|
||||||
|
the collector with it directly (`<full path to python> tools/bugreport.py` and the same
|
||||||
|
options).
|
||||||
|
|
||||||
4. **Stage 2, only if the bundle was pseudonymised.** Read, in the bundle: `CHRONOLOGY.md` and
|
4. **Stage 2, only if the bundle was pseudonymised.** Read, in the bundle: `CHRONOLOGY.md` and
|
||||||
`MANIFEST.md` completely; the review list `reports/bugreport-<stamp>.review.txt` completely; the
|
`MANIFEST.md` completely; the review list `reports/bugreport-<stamp>.review.txt` completely; the
|
||||||
@@ -112,7 +118,7 @@ session. The bundle does not leave that place because of it.
|
|||||||
domain, or project - one per line in `reports/candidates.txt`; `#` starts a comment. Then run:
|
domain, or project - one per line in `reports/candidates.txt`; `#` starts a comment. Then run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 tools/bugreport.py --bundle reports/bugreport-<stamp> --candidates reports/candidates.txt
|
tools/bugreport --bundle reports/bugreport-<stamp> --candidates reports/candidates.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
The script applies the candidates with the machinery of stage 1, reports which it did not apply
|
The script applies the candidates with the machinery of stage 1, reports which it did not apply
|
||||||
|
|||||||
@@ -76,6 +76,9 @@ tools/
|
|||||||
prerequisites.txt what the machine needs, one `|`-separated line per tool - read by the preflight and `doctor`
|
prerequisites.txt what the machine needs, one `|`-separated line per tool - read by the preflight and `doctor`
|
||||||
trace-hook what the harness hooks call: trace_ingest.py under the venv's Python
|
trace-hook what the harness hooks call: trace_ingest.py under the venv's Python
|
||||||
trace-hook.ps1 the same for PowerShell, which resolves `./tools/trace-hook` to this file first - without it Windows asks which app opens the sh script
|
trace-hook.ps1 the same for PowerShell, which resolves `./tools/trace-hook` to this file first - without it Windows asks which app opens the sh script
|
||||||
|
bugreport entry point for the bug-report collector (POSIX sh): finds a Python 3.8+ on PATH itself, skipping the Microsoft Store aliases, and runs bugreport.py - no preflight needed
|
||||||
|
bugreport.ps1 the same for PowerShell, which resolves `tools/bugreport` to this file first; keeps to what Windows PowerShell 5.1 understands
|
||||||
|
bugreport.py the bug-report collector itself - see below
|
||||||
chemenu/
|
chemenu/
|
||||||
cli.py Typer app: registers every command, runs the budget gate, renders `-h`/`--help` from cli_contract
|
cli.py Typer app: registers every command, runs the budget gate, renders `-h`/`--help` from cli_contract
|
||||||
cli_contract.py one data record per command (name, synopsis, properties, exit status) - the source `-h`, the index and CONTRACT.md's generated region render from
|
cli_contract.py one data record per command (name, synopsis, properties, exit status) - the source `-h`, the index and CONTRACT.md's generated region render from
|
||||||
@@ -119,6 +122,13 @@ survive. The mapping, the review list and the candidate file stay beside the bun
|
|||||||
`dist export` ships it with the rest of `tools/`;
|
`dist export` ships it with the rest of `tools/`;
|
||||||
its tests (`tests/test_bugreport.py`) run it on a bare interpreter (`-I -S`) to keep that promise.
|
its tests (`tests/test_bugreport.py`) run it on a bare interpreter (`-I -S`) to keep that promise.
|
||||||
|
|
||||||
|
It is started through `tools/bugreport` (`bugreport.ps1` under PowerShell), which assumes no more
|
||||||
|
than the collector does: no preflight, no `.wikitool-tools.json`, no venv. It looks for the
|
||||||
|
interpreter the way the preflight does - `python3`, `python` on `PATH`; on Windows `python`,
|
||||||
|
`py -3`, `python3` - then tries the venv's, probes each for 3.8 or newer, and never starts one under
|
||||||
|
`WindowsApps`, where `python3` in Git Bash is the Microsoft Store's alias. Finding none, it exits 1
|
||||||
|
and says why.
|
||||||
|
|
||||||
**Two consumers, one core.** The CLI is not the only caller any more. The cores
|
**Two consumers, one core.** The CLI is not the only caller any more. The cores
|
||||||
(`search/service.py`, `lint_core.py`, `types_core.py`, `catalog.py`) hold what
|
(`search/service.py`, `lint_core.py`, `types_core.py`, `catalog.py`) hold what
|
||||||
decides an answer and import no `typer` and no `rich`; the modules under `commands/` turn
|
decides an answer and import no `typer` and no `rich`; the modules under `commands/` turn
|
||||||
|
|||||||
Executable
+129
@@ -0,0 +1,129 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Entry point for the bug-report collector, so agents and people start it through one
|
||||||
|
# stable path on every platform:
|
||||||
|
#
|
||||||
|
# tools/bugreport [options]
|
||||||
|
#
|
||||||
|
# This is the POSIX half - Linux, macOS and Git Bash on Windows. PowerShell resolves
|
||||||
|
# the same string to tools/bugreport.ps1 first.
|
||||||
|
#
|
||||||
|
# tools/bugreport.py is the one part of the stack that has to run when nothing else
|
||||||
|
# does, so this script assumes nothing the preflight sets up: no .wikitool-tools.json,
|
||||||
|
# no venv. It looks for a Python itself, the way the preflight does - on Windows
|
||||||
|
# python, py -3, python3; elsewhere python3, python; every match on PATH in PATH
|
||||||
|
# order - and only then tries the venv's. It never starts anything under WindowsApps:
|
||||||
|
# there, python.exe and python3.exe are the Microsoft Store's aliases, and in Git Bash
|
||||||
|
# `python3` is exactly that. Every candidate is probed first, so one that is too old or
|
||||||
|
# that the machine refuses to run (a venv python.exe blocked by Defender) is passed over
|
||||||
|
# rather than ending the report.
|
||||||
|
#
|
||||||
|
# Without any Python 3.8 or newer it says so and exits 1, never 42: the collector opens
|
||||||
|
# no gate.
|
||||||
|
#
|
||||||
|
# CHEMENU_PREFLIGHT_PLATFORM stands in for the platform, as it does for the preflight;
|
||||||
|
# the test suite is its only user.
|
||||||
|
set -u
|
||||||
|
DIR=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd -P) || exit 1
|
||||||
|
MIN=3.8
|
||||||
|
# Exit 3 marks "a Python, but too old" apart from "did not start at all".
|
||||||
|
PROBE='import sys; sys.exit(0 if sys.version_info >= (3, 8) else 3)'
|
||||||
|
|
||||||
|
platform=${CHEMENU_PREFLIGHT_PLATFORM:-}
|
||||||
|
if [ -z "$platform" ]; then
|
||||||
|
case "$(uname -s 2>/dev/null)" in
|
||||||
|
MINGW*|MSYS*|CYGWIN*) platform=windows ;;
|
||||||
|
*) platform=other ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$platform" = windows ]; then
|
||||||
|
order="python: py:-3 python3:"
|
||||||
|
looked="python, py -3, python3"
|
||||||
|
else
|
||||||
|
order="python3: python:"
|
||||||
|
looked="python3, python"
|
||||||
|
fi
|
||||||
|
|
||||||
|
is_store_alias() {
|
||||||
|
case "$1" in *[Ww]indows[Aa]pps/*|*[Ww]indows[Aa]pps\\*) return 0 ;; esac
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Every executable called <name> on PATH, in PATH order, store aliases dropped.
|
||||||
|
on_path() {
|
||||||
|
set -f
|
||||||
|
old_ifs=$IFS
|
||||||
|
IFS=:
|
||||||
|
for d in $PATH; do
|
||||||
|
[ -n "$d" ] || d=.
|
||||||
|
for f in "$d/$1" "$d/$1.exe"; do
|
||||||
|
if [ -f "$f" ] && [ -x "$f" ] && ! is_store_alias "$f"; then
|
||||||
|
printf '%s\n' "$f"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
IFS=$old_ifs
|
||||||
|
set +f
|
||||||
|
}
|
||||||
|
|
||||||
|
PY="" EXTRA="" TOO_OLD=""
|
||||||
|
|
||||||
|
probe() { # <path> [extra argument, e.g. -3 for py] -> sets PY/EXTRA on success
|
||||||
|
"$@" -c "$PROBE" >/dev/null 2>&1
|
||||||
|
status=$?
|
||||||
|
if [ "$status" -eq 0 ]; then
|
||||||
|
PY=$1 EXTRA=${2:-}
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [ "$status" -eq 3 ]; then
|
||||||
|
TOO_OLD="$TOO_OLD
|
||||||
|
$1"
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
for candidate in $order; do
|
||||||
|
name=${candidate%%:*}
|
||||||
|
extra=${candidate#*:}
|
||||||
|
found=$(on_path "$name")
|
||||||
|
[ -n "$found" ] || continue
|
||||||
|
# A here-document, not a pipe: `break 2` has to leave the outer loop of this
|
||||||
|
# shell, and a path may contain spaces ("Program Files").
|
||||||
|
while IFS= read -r path; do
|
||||||
|
if [ -n "$extra" ]; then
|
||||||
|
probe "$path" "$extra" && break 2
|
||||||
|
else
|
||||||
|
probe "$path" && break 2
|
||||||
|
fi
|
||||||
|
done <<EOF
|
||||||
|
$found
|
||||||
|
EOF
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$PY" ]; then
|
||||||
|
for venv in "$DIR/.venv/bin/python" "$DIR/.venv/Scripts/python.exe"; do
|
||||||
|
[ -f "$venv" ] && probe "$venv" && break
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$PY" ]; then
|
||||||
|
if [ -n "$EXTRA" ]; then
|
||||||
|
exec "$PY" "$EXTRA" "$DIR/bugreport.py" "$@"
|
||||||
|
fi
|
||||||
|
exec "$PY" "$DIR/bugreport.py" "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "No Python $MIN or newer could be started, so the bug-report collector did not run."
|
||||||
|
echo "Looked for $looked on PATH, then the one in tools/.venv. Microsoft Store aliases"
|
||||||
|
echo "under WindowsApps are skipped on purpose - they do not run Python."
|
||||||
|
if [ -n "$TOO_OLD" ]; then
|
||||||
|
echo "Found, but older than $MIN:$TOO_OLD"
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
echo "Install Python $MIN or newer, or start the collector directly with the full path"
|
||||||
|
echo "of one that works:"
|
||||||
|
echo
|
||||||
|
echo " <full path to python> tools/bugreport.py <the same options>"
|
||||||
|
} >&2
|
||||||
|
exit 1
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
# Entry point for the bug-report collector under PowerShell, so agents and people start it
|
||||||
|
# through the same string on every platform:
|
||||||
|
#
|
||||||
|
# tools/bugreport [options]
|
||||||
|
#
|
||||||
|
# PowerShell resolves that to this file first. Without it, `tools/bugreport` is the sh
|
||||||
|
# launcher next to it, which PowerShell does not run. The POSIX half - Linux, macOS and
|
||||||
|
# Git Bash - is tools/bugreport.
|
||||||
|
#
|
||||||
|
# Same rules as the sh twin: tools/bugreport.py has to run when nothing else does, so
|
||||||
|
# nothing the preflight sets up is assumed. The Python is looked for the way the preflight
|
||||||
|
# looks - on Windows python, py -3, python3; elsewhere python3, python; every match on PATH
|
||||||
|
# in PATH order, on Windows with the registry's PATH added for a session that started
|
||||||
|
# before the install - and only then in the venv. Nothing under WindowsApps is ever started:
|
||||||
|
# python.exe and python3.exe there are the Microsoft Store's aliases. Every candidate is
|
||||||
|
# probed first, so one that is too old or refused (a venv python.exe blocked by Defender) is
|
||||||
|
# passed over rather than ending the report. Without any Python 3.8 or newer it says so and
|
||||||
|
# exits 1, never 42: the collector opens no gate.
|
||||||
|
#
|
||||||
|
# There is no `#Requires -Version 7`: someone who needs a bug report should not fail at the
|
||||||
|
# shell's version first, so this file keeps to what Windows PowerShell 5.1 understands too.
|
||||||
|
#
|
||||||
|
# CHEMENU_PREFLIGHT_PLATFORM stands in for the platform, as it does for the preflight; the
|
||||||
|
# test suite is its only user.
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$PSNativeCommandArgumentPassing = 'Standard'
|
||||||
|
|
||||||
|
$Dir = $PSScriptRoot
|
||||||
|
$Collector = Join-Path $Dir 'bugreport.py'
|
||||||
|
$Min = '3.8'
|
||||||
|
# Exit 3 marks "a Python, but too old" apart from "did not start at all".
|
||||||
|
$Probe = 'import sys; sys.exit(0 if sys.version_info >= (3, 8) else 3)'
|
||||||
|
|
||||||
|
$Platform = $env:CHEMENU_PREFLIGHT_PLATFORM
|
||||||
|
if (-not $Platform) {
|
||||||
|
if ($env:OS -eq 'Windows_NT') { $Platform = 'windows' } else { $Platform = 'other' }
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Platform -eq 'windows') {
|
||||||
|
$Order = @(@('python', @()), @('py', @('-3')), @('python3', @()))
|
||||||
|
$Looked = 'python, py -3, python3'
|
||||||
|
} else {
|
||||||
|
$Order = @(@('python3', @()), @('python', @()))
|
||||||
|
$Looked = 'python3, python'
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-StoreAlias {
|
||||||
|
param([string]$Path)
|
||||||
|
return $Path -match '(?i)[\\/]windowsapps[\\/]'
|
||||||
|
}
|
||||||
|
|
||||||
|
# PATH as this process has it, plus - on Windows - what the registry holds that a
|
||||||
|
# long-running session has not picked up yet.
|
||||||
|
function Get-SearchDirectory {
|
||||||
|
$directories = New-Object System.Collections.Generic.List[string]
|
||||||
|
$sources = @($env:PATH)
|
||||||
|
if ($env:OS -eq 'Windows_NT') {
|
||||||
|
foreach ($scope in 'Machine', 'User') {
|
||||||
|
$sources += [Environment]::GetEnvironmentVariable('Path', $scope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($source in $sources) {
|
||||||
|
if (-not $source) { continue }
|
||||||
|
foreach ($entry in $source.Split([IO.Path]::PathSeparator)) {
|
||||||
|
$expanded = [Environment]::ExpandEnvironmentVariables($entry.Trim())
|
||||||
|
if ($expanded -and -not $directories.Contains($expanded)) {
|
||||||
|
$directories.Add($expanded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $directories
|
||||||
|
}
|
||||||
|
|
||||||
|
# Every file called <name> on PATH, in PATH order, store aliases dropped.
|
||||||
|
function Find-OnPath {
|
||||||
|
param([string]$Name)
|
||||||
|
$found = @()
|
||||||
|
foreach ($directory in (Get-SearchDirectory)) {
|
||||||
|
foreach ($file in @($Name, "$Name.exe")) {
|
||||||
|
$candidate = Join-Path $directory $file
|
||||||
|
if ((Test-Path -LiteralPath $candidate -PathType Leaf) -and -not (Test-StoreAlias $candidate)) {
|
||||||
|
$found += $candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $found
|
||||||
|
}
|
||||||
|
|
||||||
|
$script:TooOld = @()
|
||||||
|
|
||||||
|
# 0 when this Python is 3.8 or newer; anything else when it is not, or did not start.
|
||||||
|
function Test-Python {
|
||||||
|
param([string]$Path, [string[]]$Extra)
|
||||||
|
try {
|
||||||
|
& $Path @Extra -c $Probe *> $null
|
||||||
|
$status = $LASTEXITCODE
|
||||||
|
} catch {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
if ($status -eq 3) {
|
||||||
|
$script:TooOld += $Path
|
||||||
|
}
|
||||||
|
return $status -eq 0
|
||||||
|
}
|
||||||
|
|
||||||
|
$Python = ''
|
||||||
|
$PythonExtra = @()
|
||||||
|
foreach ($candidate in $Order) {
|
||||||
|
foreach ($path in (Find-OnPath $candidate[0])) {
|
||||||
|
if (Test-Python $path $candidate[1]) {
|
||||||
|
$Python = $path
|
||||||
|
$PythonExtra = $candidate[1]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($Python) { break }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $Python) {
|
||||||
|
foreach ($venv in @((Join-Path $Dir '.venv/Scripts/python.exe'), (Join-Path $Dir '.venv/bin/python'))) {
|
||||||
|
if ((Test-Path -LiteralPath $venv -PathType Leaf) -and (Test-Python $venv @())) {
|
||||||
|
$Python = $venv
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Python) {
|
||||||
|
& $Python @PythonExtra $Collector @args
|
||||||
|
exit $LASTEXITCODE
|
||||||
|
}
|
||||||
|
|
||||||
|
$message = @(
|
||||||
|
"No Python $Min or newer could be started, so the bug-report collector did not run."
|
||||||
|
"Looked for $Looked on PATH, then the one in tools/.venv. Microsoft Store aliases"
|
||||||
|
'under WindowsApps are skipped on purpose - they do not run Python.'
|
||||||
|
)
|
||||||
|
if ($script:TooOld.Count -gt 0) {
|
||||||
|
$message += "Found, but older than ${Min}:"
|
||||||
|
$message += ($script:TooOld | ForEach-Object { " $_" })
|
||||||
|
}
|
||||||
|
$message += @(
|
||||||
|
''
|
||||||
|
"Install Python $Min or newer, or start the collector directly with the full path"
|
||||||
|
'of one that works:'
|
||||||
|
''
|
||||||
|
' <full path to python> tools/bugreport.py <the same options>'
|
||||||
|
)
|
||||||
|
[Console]::Error.WriteLine(($message -join [Environment]::NewLine))
|
||||||
|
exit 1
|
||||||
+11
-6
@@ -6,10 +6,15 @@ Runs on the base Python with the standard library only, and imports nothing from
|
|||||||
- no venv, a broken package, a Python that is too old. Syntax stays at Python
|
- no venv, a broken package, a Python that is too old. Syntax stays at Python
|
||||||
3.8 so that even an old interpreter can still produce a report.
|
3.8 so that even an old interpreter can still produce a report.
|
||||||
|
|
||||||
python tools/bugreport.py [--chronology FILE] [--transcript FILE]...
|
tools/bugreport [--chronology FILE] [--transcript FILE]...
|
||||||
[--session ID | --no-trace] [--titles]
|
[--session ID | --no-trace] [--titles]
|
||||||
[--pseudonymise] [--root DIR] [--out DIR]
|
[--pseudonymise] [--root DIR] [--out DIR]
|
||||||
python tools/bugreport.py --bundle DIR --candidates FILE
|
tools/bugreport --bundle DIR --candidates FILE
|
||||||
|
|
||||||
|
`tools/bugreport` (and `tools/bugreport.ps1` under PowerShell) finds a Python 3.8
|
||||||
|
or newer and runs this file with it, skipping the Microsoft Store's aliases that
|
||||||
|
`python3` resolves to on Windows. Started directly - `<python> tools/bugreport.py`
|
||||||
|
- it works the same, with whichever interpreter was named.
|
||||||
|
|
||||||
The bundle is `<out>/bugreport-<UTC stamp>/` plus a zip beside it, in four
|
The bundle is `<out>/bugreport-<UTC stamp>/` plus a zip beside it, in four
|
||||||
layers: the environment, the stack, what `wikitool` prints (if it starts), and
|
layers: the environment, the stack, what `wikitool` prints (if it starts), and
|
||||||
@@ -1188,7 +1193,7 @@ def make_zip(bundle_dir: Path, archive: Path) -> None:
|
|||||||
|
|
||||||
def parse_args(argv):
|
def parse_args(argv):
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
prog="bugreport.py",
|
prog="tools/bugreport",
|
||||||
description="Collect a bug-report bundle. Removes secrets, keeps page titles out "
|
description="Collect a bug-report bundle. Removes secrets, keeps page titles out "
|
||||||
"unless --titles is given, uploads nothing.",
|
"unless --titles is given, uploads nothing.",
|
||||||
)
|
)
|
||||||
@@ -1330,7 +1335,7 @@ def _collect(args, root: Path, out: Path, stamp: str, bundle_dir: Path) -> int:
|
|||||||
print("Mapping: %s" % _sibling(bundle_dir, ".pseudonyms.json"))
|
print("Mapping: %s" % _sibling(bundle_dir, ".pseudonyms.json"))
|
||||||
print("Review: %s" % _sibling(bundle_dir, ".review.txt"))
|
print("Review: %s" % _sibling(bundle_dir, ".review.txt"))
|
||||||
print("Both hold originals and stay on this machine: never share them.")
|
print("Both hold originals and stay on this machine: never share them.")
|
||||||
print("Stage 2: python3 tools/bugreport.py --bundle %s --candidates <file>" % bundle_dir)
|
print("Stage 2: tools/bugreport --bundle %s --candidates <file>" % bundle_dir)
|
||||||
print()
|
print()
|
||||||
print(STAGE1_NOTICE if pz is not None else PRIVACY_NOTICE)
|
print(STAGE1_NOTICE if pz is not None else PRIVACY_NOTICE)
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
"""`tools/bugreport` and `tools/bugreport.ps1`, the collector's launchers (Gitea #166).
|
||||||
|
|
||||||
|
The collector has to run when nothing else does, and on Windows `python3` - in PowerShell
|
||||||
|
also `python` - can be the Microsoft Store's alias. So the launchers find a Python 3.8+
|
||||||
|
themselves, never start anything under WindowsApps, and assume nothing the preflight sets
|
||||||
|
up. These tests drive both against the stub machine from `test_preflight.py`: a `PATH` the
|
||||||
|
test builds, stub Pythons in it, and no `.wikitool-tools.json` anywhere.
|
||||||
|
|
||||||
|
A stub Python answers the launcher's probe (`-c ...`) with 0, or 3 for "too old", and
|
||||||
|
otherwise prints the arguments it was started with - which is how a test sees that the
|
||||||
|
collector was run, and with what.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from chemenu.tests.test_preflight import SHELLS, TOOLS, Machine
|
||||||
|
|
||||||
|
PWSH = shutil.which("pwsh")
|
||||||
|
|
||||||
|
# Answers the probe as a current Python, otherwise echoes its arguments.
|
||||||
|
PYTHON = "#!/bin/sh\n[ \"$1\" = -c ] && exit 0\nprintf '%s\\n' \"$@\"\n"
|
||||||
|
# The same for the `py` launcher, which needs -3 in front of the probe.
|
||||||
|
PY_LAUNCHER = "#!/bin/sh\n[ \"$1\" = -3 ] && [ \"$2\" = -c ] && exit 0\nprintf '%s\\n' \"$@\"\n"
|
||||||
|
TOO_OLD = "#!/bin/sh\n[ \"$1\" = -c ] && exit 3\necho 'collector run by a Python that is too old'\n"
|
||||||
|
REFUSED = "#!/bin/sh\necho 'Access is denied.' >&2\nexit 1\n"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def machine(tmp_path: Path) -> Machine:
|
||||||
|
machine = Machine(tmp_path.resolve())
|
||||||
|
for name in ("bugreport", "bugreport.ps1", "bugreport.py"):
|
||||||
|
shutil.copy2(TOOLS / name, machine.tools / name)
|
||||||
|
return machine
|
||||||
|
|
||||||
|
|
||||||
|
def _env(machine: Machine) -> dict[str, str]:
|
||||||
|
return {
|
||||||
|
"PATH": os.pathsep.join(str(d) for d in machine.path_dirs),
|
||||||
|
"HOME": str(machine.base),
|
||||||
|
**machine.extra_env,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _sh(machine: Machine, *args: str, shell: str) -> subprocess.CompletedProcess:
|
||||||
|
return subprocess.run([shell, str(machine.tools / "bugreport"), *args],
|
||||||
|
capture_output=True, text=True, env=_env(machine), timeout=30)
|
||||||
|
|
||||||
|
|
||||||
|
def _pwsh(machine: Machine, *args: str) -> subprocess.CompletedProcess:
|
||||||
|
env = {**_env(machine), "DOTNET_CLI_TELEMETRY_OPTOUT": "1", "POWERSHELL_TELEMETRY_OPTOUT": "1"}
|
||||||
|
return subprocess.run(
|
||||||
|
[PWSH, "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", str(machine.tools / "bugreport.ps1"), *args],
|
||||||
|
capture_output=True, text=True, env=env, timeout=120,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _runners():
|
||||||
|
runners = [pytest.param(lambda m, *a, shell=shell: _sh(m, *a, shell=shell), id=Path(shell).name)
|
||||||
|
for shell in SHELLS]
|
||||||
|
runners.append(pytest.param(
|
||||||
|
_pwsh, id="pwsh", marks=pytest.mark.skipif(PWSH is None, reason="PowerShell 7 (pwsh) is not installed"),
|
||||||
|
))
|
||||||
|
return runners
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(params=_runners())
|
||||||
|
def launch(request):
|
||||||
|
return request.param
|
||||||
|
|
||||||
|
|
||||||
|
def _store_alias(machine: Machine, name: str) -> Path:
|
||||||
|
"""A Store alias first on PATH that leaves a marker if anything ever starts it."""
|
||||||
|
marker = machine.base / f"{name}-alias-was-run"
|
||||||
|
apps = machine.base / "Users" / "u" / "AppData" / "Local" / "Microsoft" / "WindowsApps"
|
||||||
|
machine.stub(name, f"#!/bin/sh\necho ran > '{marker}'\nexit 9009\n", apps)
|
||||||
|
machine.path_dirs.insert(0, apps)
|
||||||
|
return marker
|
||||||
|
|
||||||
|
|
||||||
|
def test_runs_the_collector_with_every_argument(machine, launch):
|
||||||
|
machine.stub("python3", PYTHON)
|
||||||
|
result = launch(machine, "--chronology", "reports/a b.md", "--no-trace")
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
assert result.stdout.splitlines() == [
|
||||||
|
str(machine.tools / "bugreport.py"), "--chronology", "reports/a b.md", "--no-trace",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_needs_nothing_the_preflight_sets_up(machine, launch):
|
||||||
|
machine.stub("python3", PYTHON)
|
||||||
|
assert launch(machine, "--no-trace").returncode == 0
|
||||||
|
assert not (machine.root / ".wikitool-tools.json").exists()
|
||||||
|
assert not (machine.tools / ".venv").exists()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("platform, alias", [("linux", "python3"), ("windows", "python")])
|
||||||
|
def test_a_store_alias_is_never_started(machine, launch, platform, alias):
|
||||||
|
machine.extra_env["CHEMENU_PREFLIGHT_PLATFORM"] = platform
|
||||||
|
marker = _store_alias(machine, alias)
|
||||||
|
machine.stub(alias, PYTHON)
|
||||||
|
result = launch(machine, "--no-trace")
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
assert result.stdout.splitlines()[0] == str(machine.tools / "bugreport.py")
|
||||||
|
assert not marker.exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_on_windows_python_comes_before_python3(machine, launch):
|
||||||
|
"""In Git Bash on the Windows target, `python` is the real one and `python3` the alias
|
||||||
|
(T4) - the order alone must not depend on the alias being recognised."""
|
||||||
|
machine.extra_env["CHEMENU_PREFLIGHT_PLATFORM"] = "windows"
|
||||||
|
machine.stub("python3", "#!/bin/sh\necho 'wrong python'\n")
|
||||||
|
machine.stub("python", PYTHON)
|
||||||
|
result = launch(machine, "--no-trace")
|
||||||
|
assert result.stdout.splitlines() == [str(machine.tools / "bugreport.py"), "--no-trace"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_on_windows_the_py_launcher_gets_minus_3(machine, launch):
|
||||||
|
machine.extra_env["CHEMENU_PREFLIGHT_PLATFORM"] = "windows"
|
||||||
|
machine.stub("py", PY_LAUNCHER)
|
||||||
|
result = launch(machine, "--no-trace")
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
assert result.stdout.splitlines() == ["-3", str(machine.tools / "bugreport.py"), "--no-trace"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_python_too_old_is_passed_over(machine, launch):
|
||||||
|
old = machine.stub("python3", TOO_OLD)
|
||||||
|
machine.stub("python", PYTHON)
|
||||||
|
result = launch(machine, "--no-trace")
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
assert "too old" not in result.stdout
|
||||||
|
assert str(old) not in result.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_python_that_refuses_to_start_is_passed_over(machine, launch):
|
||||||
|
machine.stub("python3", REFUSED)
|
||||||
|
machine.stub("python", PYTHON)
|
||||||
|
result = launch(machine, "--no-trace")
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
assert result.stdout.splitlines()[0] == str(machine.tools / "bugreport.py")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("layout", [("bin", "python"), ("Scripts", "python.exe")])
|
||||||
|
def test_the_venv_python_is_the_last_resort(machine, launch, layout):
|
||||||
|
machine.stub(layout[1], PYTHON, machine.tools / ".venv" / layout[0])
|
||||||
|
result = launch(machine, "--no-trace")
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
assert result.stdout.splitlines()[0] == str(machine.tools / "bugreport.py")
|
||||||
|
|
||||||
|
|
||||||
|
def test_without_a_python_it_says_why_and_exits_1(machine, launch):
|
||||||
|
old = machine.stub("python3", TOO_OLD)
|
||||||
|
marker = _store_alias(machine, "python")
|
||||||
|
result = launch(machine, "--no-trace")
|
||||||
|
assert result.returncode == 1
|
||||||
|
assert "No Python 3.8 or newer" in result.stderr
|
||||||
|
assert "WindowsApps" in result.stderr
|
||||||
|
assert str(old) in result.stderr
|
||||||
|
assert "<full path to python> tools/bugreport.py" in result.stderr
|
||||||
|
assert not marker.exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_collectors_exit_code_passes_through(machine, launch):
|
||||||
|
machine.stub("python3", "#!/bin/sh\n[ \"$1\" = -c ] && exit 0\nexit 1\n")
|
||||||
|
assert launch(machine, "--bundle", "b", "--candidates", "c").returncode == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_both_launchers_ship_and_the_sh_one_is_executable():
|
||||||
|
assert (TOOLS / "bugreport").is_file() and (TOOLS / "bugreport.ps1").is_file()
|
||||||
|
assert os.access(TOOLS / "bugreport", os.X_OK)
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_real_collector_runs_through_the_launcher(tmp_path):
|
||||||
|
"""End to end on this machine's own Python: the launcher finds it and the collector's
|
||||||
|
usage comes back, naming the launcher as the program."""
|
||||||
|
result = subprocess.run([str(TOOLS / "bugreport"), "--help"], capture_output=True, text=True,
|
||||||
|
timeout=60, cwd=tmp_path)
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
assert result.stdout.startswith("usage: tools/bugreport")
|
||||||
@@ -35,6 +35,10 @@ FORBIDDEN = (
|
|||||||
("sha256sum", re.compile(r"\bsha256sum\b")),
|
("sha256sum", re.compile(r"\bsha256sum\b")),
|
||||||
("curl", re.compile(r"\bcurl\b")),
|
("curl", re.compile(r"\bcurl\b")),
|
||||||
("tar", re.compile(r"(^|[;&|]\s*)tar\s")),
|
("tar", re.compile(r"(^|[;&|]\s*)tar\s")),
|
||||||
|
# Which name starts a real Python differs per shell and platform: on Windows `python3` - in
|
||||||
|
# PowerShell also `python` - can be the Microsoft Store's alias. A command goes through a
|
||||||
|
# launcher that finds the interpreter itself (`tools/wikitool`, `tools/bugreport`).
|
||||||
|
("bare interpreter", re.compile(r"(^|[;&|]\s*)(python3?|py)(\.exe)?\s")),
|
||||||
)
|
)
|
||||||
|
|
||||||
# The two sanctioned exceptions, each one line per shell (instructions/CONTRACT.md): the session
|
# The two sanctioned exceptions, each one line per shell (instructions/CONTRACT.md): the session
|
||||||
|
|||||||
@@ -396,6 +396,7 @@ def test_output_into_a_pipe_is_utf8_even_under_a_cp1252_locale():
|
|||||||
@pytest.mark.parametrize("path, eol", [
|
@pytest.mark.parametrize("path, eol", [
|
||||||
("tools/wikitool", "lf"),
|
("tools/wikitool", "lf"),
|
||||||
("tools/trace-hook", "lf"),
|
("tools/trace-hook", "lf"),
|
||||||
|
("tools/bugreport", "lf"),
|
||||||
("tools/preflight.sh", "lf"),
|
("tools/preflight.sh", "lf"),
|
||||||
("AGENTS.md", "lf"),
|
("AGENTS.md", "lf"),
|
||||||
])
|
])
|
||||||
|
|||||||
Reference in new issue
Block a user