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
45 changes: 45 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: CI

# Runs the test suites for both halves of the repo on every pull request and
# on pushes to main. This is the one-command verification story for the
# legal-accuracy core (season-window parsing in pipeline/normalize.py and the
# MapLibre filter expressions in web/src/legal.ts) — see plans/002.

on:
pull_request: {}
push:
branches: [main]

# Least privilege: these jobs only need to read the repo to check out code
# and run tests; nothing here writes back to GitHub or external services.
permissions:
contents: read

jobs:
web:
runs-on: ubuntu-latest
steps:
# Actions pinned to commit SHAs (not mutable tags) so a retag can't
# swap the code that runs in CI.
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
cache: npm
cache-dependency-path: web/package-lock.json
- name: Install, typecheck, and test
run: |
cd web
npm ci
npx tsc --noEmit
npm test

pipeline:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
- name: Install deps and test
run: |
uv sync --locked
uv run pytest
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: help fetch normalize tiles data web-install dev build clean
.PHONY: help fetch normalize tiles data web-install dev build clean test

help:
@echo "ca-mvum build pipeline"
Expand All @@ -9,6 +9,7 @@ help:
@echo " make web-install npm install in web/"
@echo " make dev run the Vite dev server"
@echo " make build production static build -> web/dist"
@echo " make test run Python (pytest) and web (vitest) test suites"

fetch:
uv run python -m pipeline.fetch_mvum
Expand All @@ -32,3 +33,7 @@ build:

clean:
rm -f data/*.geojson

test:
uv run pytest
cd web && npm test
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,9 @@ dependencies = [

[tool.ca-mvum]
# Build pipeline is run via the Makefile; see `make help`.

[dependency-groups]
dev = ["pytest>=8"]

[tool.pytest.ini_options]
testpaths = ["tests"]
Empty file added tests/__init__.py
Empty file.
9 changes: 9 additions & 0 deletions tests/test_forests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from pipeline.forests import slug


def test_slug_simple():
assert slug("San Bernardino National Forest") == "san-bernardino"


def test_slug_hyphenated():
assert slug("Humboldt-Toiyabe National Forest") == "humboldt-toiyabe"
121 changes: 121 additions & 0 deletions tests/test_normalize.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
from pipeline.normalize import (
_doy,
_doy_to_md,
_window_text,
normalize_feature,
parse_window,
)


class TestParseWindow:
def test_none_is_none(self):
assert parse_window(None) is None

def test_empty_string_is_none(self):
assert parse_window("") is None

def test_whitespace_only_is_none(self):
assert parse_window(" ") is None

def test_open_literal(self):
assert parse_window("open") == (1, 365)

def test_yearlong_literal_case_insensitive(self):
assert parse_window("Yearlong") == (1, 365)

def test_year_long_with_space(self):
assert parse_window("year long") == (1, 365)

def test_bounded_window(self):
assert parse_window("05/01-11/15") == (121, 319)

def test_full_year_window(self):
assert parse_window("01/01-12/31") == (1, 365)

def test_unparseable_but_present_treated_as_yearlong(self):
# Present-but-unparseable text = permitted, treated as year-round
# (current documented policy).
assert parse_window("see ranger") == (1, 365)


class TestDoy:
def test_jan_1(self):
assert _doy(1, 1) == 1

def test_dec_31(self):
assert _doy(12, 31) == 365

def test_march_1_non_leap_convention(self):
assert _doy(3, 1) == 60


class TestWindowText:
def test_yearlong(self):
assert _window_text(1, 365) == "Yearlong"

def test_bounded(self):
assert _window_text(121, 319) == "05/01-11/15"


class TestDoyToMd:
def test_jan_1(self):
assert _doy_to_md(1) == "01/01"

def test_may_1(self):
assert _doy_to_md(121) == "05/01"


class TestNormalizeFeature:
def test_no_dates_fields_returns_none(self):
assert normalize_feature({}) is None

def test_single_yearlong_class(self):
result = normalize_feature({"passengervehicle_datesopen": "open"})
assert result is not None
assert result["classes"] == ",passenger,"
assert result["season"] == "yearlong"
assert (result["open_start"], result["open_end"]) == (1, 365)

def test_multi_class_representative_window_collapse(self):
# Characterizes the representative-window collapse: a bounded window
# on ANY permitted class makes the whole route read "seasonal", even
# though the passenger class itself is yearlong-permitted here.
# Plan 005 changes this per-class behavior; update this test then.
result = normalize_feature(
{
"passengervehicle_datesopen": "open",
"motorcycle_datesopen": "05/01-11/15",
}
)
assert result is not None
assert "passenger" in result["classes"]
assert "motorcycle" in result["classes"]
assert result["season"] == "seasonal"
assert result["open_start"] == 121
assert result["open_end"] == 319

def test_ebike_gate_yes(self):
result = normalize_feature(
{"passengervehicle_datesopen": "open", "e_bike_class1": "yes"}
)
assert result is not None
assert "e_bike1" in result["classes"]

def test_ebike_gate_no(self):
result = normalize_feature(
{"passengervehicle_datesopen": "open", "e_bike_class1": "no"}
)
assert result is not None
assert "e_bike1" not in result["classes"]

def test_miles_rounding(self):
result = normalize_feature(
{"passengervehicle_datesopen": "open", "gis_miles": 1.234}
)
assert result is not None
assert result["miles"] == 1.23

def test_miles_missing_defaults_zero(self):
result = normalize_feature({"passengervehicle_datesopen": "open"})
assert result is not None
assert result["miles"] == 0.0
69 changes: 69 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading