mirror of
https://git.pumpin.online/funkpower/typikon.git
synced 2026-10-22 18:57:13 +00:00
Every desktop shell that reads typikon today runs the binary and parses its JSON. That costs about 1.5ms and is the right answer for a calendar that changes once a day, but it is the wrong shape for anything with an FFI and no business spawning a process on every redraw. Dates cross as Julian Day Numbers, so no struct layout is part of the contract. A day is opaque and owns its strings, built once when it is reckoned: an accessor returns the same borrowed pointer every time, so a statusline polling one in a loop allocates nothing. The accessors carry every field of the JSON, so nothing on the other side needs a parser. The Python bindings and this were resolving a jurisdiction by two copies of the same logic, and the copy here leaked one per call. Both now share jurisdictions::resolve_full, which interns the combinations instead. The header is a promise made in a second file, and a second file drifts: a test reads both and fails if either has a symbol the other does not, and does the same for the LuaJIT cdef, where a stale declaration is a segfault rather than a compile error.
104 lines
4.6 KiB
YAML
104 lines
4.6 KiB
YAML
name: ci
|
|
|
|
# Deliberately not triggered by pull_request.
|
|
#
|
|
# The forge this runs on is public and takes pull requests from strangers, and a
|
|
# runner executes whatever the workflow checks out. Triggering on pull_request
|
|
# would mean a stranger's first contribution runs code on the runner host before
|
|
# anyone has read it. Triggering only on push means the runner only ever executes
|
|
# code that someone with push rights put there.
|
|
#
|
|
# The cost is that a pull request gets no automatic check. Run it by pushing the
|
|
# branch yourself once you have read the diff.
|
|
on:
|
|
push:
|
|
# Branches only. A tag push does not match a branch filter, so release.yml
|
|
# owns tags and this owns everything else.
|
|
branches: ['**']
|
|
|
|
jobs:
|
|
test:
|
|
runs-on: docker
|
|
container:
|
|
image: rust:1.98-bookworm
|
|
steps:
|
|
# Not actions/checkout. That is a JavaScript action: the runner executes it
|
|
# with `node` *inside this container*, and a Rust image carries no node, so
|
|
# the job dies at the first step with "node: executable file not found in
|
|
# $PATH". actions/cache and actions/upload-artifact are the same. Using an
|
|
# image that also carries node would mean installing Rust on every run;
|
|
# cloning by hand costs four lines and keeps the lean image.
|
|
- name: Checkout
|
|
run: |
|
|
git init -q .
|
|
git remote add origin "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY"
|
|
git fetch -q --depth 1 origin "$GITHUB_SHA"
|
|
git checkout -q FETCH_HEAD
|
|
|
|
# No `cargo fmt --check`. The tables in this crate are aligned by hand —
|
|
# MONTHS, JURISDICTIONS — and rustfmt wants one entry per line, which loses
|
|
# the shape that makes them readable. A gate that fights the house style is
|
|
# a gate that gets ignored.
|
|
- name: Lint
|
|
run: |
|
|
rustup component add clippy
|
|
cargo clippy --all-targets -- -D warnings
|
|
# The binding code only exists under the feature, so a bare clippy
|
|
# run never looks at src/python.rs.
|
|
cargo clippy --features python --all-targets -- -D warnings
|
|
cargo clippy --features capi --all-targets -- -D warnings
|
|
|
|
- name: Rust tests
|
|
run: |
|
|
cargo test
|
|
cargo test --features capi
|
|
|
|
# The header and the LuaJIT cdef are checked against the library by the
|
|
# Rust tests; this checks that a C compiler agrees, which they cannot.
|
|
- name: C ABI
|
|
run: |
|
|
apt-get update && apt-get install -y --no-install-recommends gcc luajit
|
|
cargo build --release --features capi
|
|
cc -Wall -Wextra -Werror -Iinclude examples/c/demo.c \
|
|
-Ltarget/release -ltypikon -o /tmp/demo
|
|
LD_LIBRARY_PATH=target/release /tmp/demo rocor
|
|
LD_LIBRARY_PATH=target/release /tmp/demo --list
|
|
TYPIKON_LIB=target/release/libtypikon.so \
|
|
luajit examples/lua/statusline.lua rocor
|
|
|
|
# The CLI and the bindings are the same engine, so they have to agree. This
|
|
# is the check that caught the bindings resolving their own default
|
|
# jurisdiction while the CLI read the configured one.
|
|
- name: Python bindings
|
|
run: |
|
|
apt-get update && apt-get install -y --no-install-recommends python3 python3-venv python3-dev
|
|
python3 -m venv /tmp/venv
|
|
/tmp/venv/bin/pip install --quiet maturin pytest mypy
|
|
cargo build --release
|
|
/tmp/venv/bin/maturin build --release --out dist
|
|
/tmp/venv/bin/pip install --quiet --no-index --find-links dist typikon
|
|
/tmp/venv/bin/python -m pytest tests/python -q
|
|
# The bindings resolve the default jurisdiction themselves, and once
|
|
# read only the config file while the CLI read the environment first.
|
|
# The two then answered for different churches on the same machine.
|
|
cli=$(TYPIKON_JURISDICTION=rocor ./target/release/typikon \
|
|
--date 2026-03-18 --format tsv --fields jurisdiction | tail -1)
|
|
py=$(TYPIKON_JURISDICTION=rocor /tmp/venv/bin/python -c "import typikon, datetime; print(typikon.describe(datetime.date(2026, 3, 18)).jurisdiction)")
|
|
test "$cli" = "$py" || { echo "cli=$cli python=$py"; exit 1; }
|
|
|
|
- name: Type stubs
|
|
run: |
|
|
cat > /tmp/typecheck.py <<'EOF'
|
|
import datetime, typikon
|
|
d: typikon.Day = typikon.describe(datetime.date(2026, 4, 12))
|
|
day: datetime.date = d.date
|
|
feast: str | None = d.feast
|
|
n: int = d.days_to_pascha
|
|
EOF
|
|
/tmp/venv/bin/python -m mypy --strict /tmp/typecheck.py
|
|
|
|
- name: README examples
|
|
run: |
|
|
cargo build --release
|
|
python3 tests/readme-examples.py
|