Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 32 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,31 @@ on:

jobs:
test:
runs-on: ubuntu-latest
name: ${{ matrix.os }} py${{ matrix.python-version }}
runs-on: ${{ matrix.os }}
defaults:
run:
# bash on every platform, so the wheel glob and paths below behave the
# same way. Git Bash ships on the Windows runners.
shell: bash
strategy:
# One platform failing should not hide the others.
fail-fast: false
matrix:
os: [ubuntu-latest]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
include:
# Windows and macOS at the supported floor and ceiling. Console
# encoding and path handling are where they diverge from Linux, and
# both sit on the CLI's hot path -- a check prints "<name> ✓", so the
# curriculum's own output has to survive the console it lands on.
# Ubuntu covers the versions in between.
- os: windows-latest
python-version: "3.9"
- os: windows-latest
python-version: "3.13"
- os: macos-latest
python-version: "3.13"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
Expand All @@ -22,5 +43,13 @@ jobs:
- run: python -m pip install build
- run: python -m build
- run: python -m pip install --force-reinstall dist/*.whl
- run: pythonlings init --path /tmp/pythonlings-workspace
- run: pythonlings --root /tmp/pythonlings-workspace list
- run: pythonlings init --path "${{ runner.temp }}/pythonlings-workspace"
- run: pythonlings --root "${{ runner.temp }}/pythonlings-workspace" list
- name: CLI survives a non-UTF-8 console
# The regression that motivated Windows coverage: status glyphs and
# captured check output crashed on consoles that cannot encode them.
env:
PYTHONIOENCODING: ascii
run: |
pythonlings --root tests/fixtures/passing_curriculum verify
pythonlings --root "${{ runner.temp }}/pythonlings-workspace" list
13 changes: 11 additions & 2 deletions pythonlings/core/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,10 @@ def _check_workspace(root: Path) -> CheckResult:
try:
mode = root.stat().st_mode
except OSError as exc:
if exc.errno == errno.ELOOP:
# Windows reports an unresolvable path (a symlink loop among them) as
# ERROR_CANT_RESOLVE_FILENAME with no matching errno, so match on the
# winerror too rather than letting it escape.
if exc.errno == errno.ELOOP or getattr(exc, "winerror", None) == 1921:
return CheckResult(
"Workspace",
CheckStatus.FAILURE,
Expand All @@ -126,7 +129,13 @@ def _check_workspace(root: Path) -> CheckResult:
CheckStatus.FAILURE,
f"{root} does not exist; run `pythonlings init --path {root}`",
)
raise
# Anything else is still reported rather than raised: doctor exists to
# describe a broken workspace, so it must not crash on one.
return CheckResult(
"Workspace",
CheckStatus.FAILURE,
f"{root} could not be read ({exc.strerror or exc})",
)
if not stat.S_ISDIR(mode):
return CheckResult(
"Workspace", CheckStatus.FAILURE, f"{root} is not a directory"
Expand Down
8 changes: 6 additions & 2 deletions tests/integration/test_cli_doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,12 @@ def test_doctor_unknown_home_user_is_friendly() -> None:

assert result.returncode == 1
assert "[FAIL] Workspace:" in result.stdout
assert root in result.stdout
assert "check the path and symlinks" in result.stdout
# POSIX leaves an unknown ~user unexpanded, so the path stays literal and
# doctor reports it as unresolvable. Windows expands it to a path under the
# users directory, which is merely missing. Both are correct, so assert what
# holds either way -- a reported failure naming the user -- rather than
# pinning one platform's wording.
assert "pythonlings_no_such_user_93847" in result.stdout
assert "Traceback" not in result.stdout + result.stderr


Expand Down
4 changes: 4 additions & 0 deletions tests/integration/test_cli_verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,10 @@ def test_verify_keeps_unicode_symbol_under_utf8(tmp_path: Path) -> None:
[sys.executable, "-m", "pythonlings", "--root", str(tmp_path), "verify"],
capture_output=True,
text=True,
# The child writes UTF-8 because of PYTHONIOENCODING above; decode it
# the same way rather than falling back to the parent's locale, which
# is not UTF-8 on Windows.
encoding="utf-8",
env=env,
)

Expand Down
3 changes: 2 additions & 1 deletion tests/unit/test_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ def test_load_rejects_missing_exercise_path(tmp_path: Path) -> None:
'hint = "h"\n',
encoding="utf-8",
)
with pytest.raises(ManifestError, match="exercises/missing.py"):
# The message echoes the path with the platform's separator.
with pytest.raises(ManifestError, match=r"exercises[\\/]missing\.py"):
load(tmp_path)


Expand Down
Loading