Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62d505a17e
|
+12
-56
@@ -1,73 +1,29 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
precommit-and-security:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
- name: Install pre-commit
|
||||
run: pip install pre-commit
|
||||
|
||||
- name: Run pre-commit hooks
|
||||
run: pre-commit run --all-files --color always
|
||||
|
||||
- name: Install Poetry and export plugin
|
||||
run: |
|
||||
pip install poetry
|
||||
poetry self add poetry-plugin-export
|
||||
|
||||
- name: Install pip-audit
|
||||
run: pip install pip-audit
|
||||
|
||||
- name: Audit dev dependencies (Poetry lockfile)
|
||||
run: |
|
||||
poetry export -f requirements.txt --without-hashes --with dev \
|
||||
| pip-audit -r /dev/stdin
|
||||
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- run: pip install mypy
|
||||
- run: mypy
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.11", "3.12", "3.13", "3.14"]
|
||||
python-version: ["3.11", "3.13"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- run: pip install -e . pytest
|
||||
- run: pytest tests/unit tests/security
|
||||
- name: Integration tests (best effort)
|
||||
continue-on-error: true
|
||||
run: pytest tests/integration
|
||||
|
||||
package:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- run: pip install build
|
||||
- run: python -m build
|
||||
- name: Install project and tooling
|
||||
run: pip install -e . pytest mypy pre-commit
|
||||
|
||||
- name: Lint and typecheck
|
||||
if: matrix.python-version == '3.13'
|
||||
run: |
|
||||
pre-commit run --all-files --color always
|
||||
mypy
|
||||
|
||||
- name: Unit and security tests
|
||||
run: pytest tests/unit tests/security
|
||||
|
||||
@@ -20,6 +20,18 @@ from schedls.security import (
|
||||
validate_name,
|
||||
)
|
||||
|
||||
_UNTRUSTED_UID = 65534
|
||||
|
||||
|
||||
def _relinquish_ownership(path) -> None:
|
||||
"""Own a fixture by a non-root user when the suite itself runs as root.
|
||||
|
||||
The helper checks distinguish root-owned files from user-owned ones, so a
|
||||
root test process must make its "untrusted" fixtures owned by someone else.
|
||||
"""
|
||||
if os.geteuid() == 0:
|
||||
os.chown(path, _UNTRUSTED_UID, _UNTRUSTED_UID)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"name",
|
||||
@@ -114,6 +126,7 @@ def test_resolve_helper_rejects_user_owned_dir_when_root(tmp_path, monkeypatch)
|
||||
helper = helper_dir / "evilhelper"
|
||||
helper.write_text("#!/bin/sh\ntrue\n")
|
||||
helper.chmod(0o755)
|
||||
_relinquish_ownership(helper_dir)
|
||||
monkeypatch.setenv("PATH", str(helper_dir))
|
||||
monkeypatch.setattr(os, "geteuid", lambda: 0)
|
||||
assert resolve_helper("evilhelper") is None
|
||||
@@ -134,6 +147,7 @@ def test_runner_refuses_untrusted_absolute_helper(tmp_path, monkeypatch) -> None
|
||||
helper = tmp_path / "evil"
|
||||
helper.write_text("#!/bin/sh\ntrue\n")
|
||||
helper.chmod(0o755)
|
||||
_relinquish_ownership(helper)
|
||||
monkeypatch.setattr(os, "geteuid", lambda: 0)
|
||||
with pytest.raises(SafetyRefusalError):
|
||||
CommandRunner().run([str(helper)])
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from datetime import UTC, datetime, timedelta, timezone
|
||||
|
||||
import pytest
|
||||
@@ -7,6 +8,15 @@ import pytest
|
||||
from schedls import timefmt
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def london_tz(monkeypatch):
|
||||
monkeypatch.setenv("TZ", "Europe/London")
|
||||
time.tzset()
|
||||
yield
|
||||
monkeypatch.undo()
|
||||
time.tzset()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("value", "seconds"),
|
||||
[
|
||||
@@ -50,10 +60,11 @@ def test_parse_placeholders() -> None:
|
||||
assert timefmt.parse_systemd_timestamp("0") is None
|
||||
|
||||
|
||||
def test_format_datetime_round_trip_zone() -> None:
|
||||
def test_format_datetime_round_trip_zone(london_tz) -> None:
|
||||
dt = datetime(2026, 9, 25, 2, 0, 0, tzinfo=timezone(timedelta(hours=1)))
|
||||
text = timefmt.format_datetime(dt)
|
||||
assert text.startswith("Fri 25 Sep 2026 02:00:00")
|
||||
assert text.endswith("BST")
|
||||
|
||||
|
||||
def test_format_short_relative() -> None:
|
||||
|
||||
Reference in New Issue
Block a user