diff --git a/.github/workflows/ballotpedia.yml b/.github/workflows/ballotpedia.yml new file mode 100644 index 0000000..ffaf8ee --- /dev/null +++ b/.github/workflows/ballotpedia.yml @@ -0,0 +1,140 @@ +name: Ballotpedia through ICU + +on: + push: + branches: + - ballotpedia-cli + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + ballotpedia: + runs-on: ubuntu-24.04 + steps: + - name: Checkout Idriç CLI + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + + - name: Checkout Idriç compiler boundary + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + repository: isomorphisms/Idric + ref: a8baedff0a536376a3c1411d3d2d3f0bc0194eb5 + path: .tools/Idric + + - name: Checkout Idric-Net + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + repository: isomorphisms/Idric-Net + ref: 3e7643c8d8dd2a940d5b6dc96403ddae765402cd + path: .tools/Idric-Net + + - name: Checkout checked ICU request boundary + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + repository: dilapidated-shed/icu + ref: 2dd3b855786993feb662edfdd8d740f083f90c8d + path: .tools/icu + + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install --yes --no-install-recommends chezscheme libssl-dev + + - name: Build Idriç and install typed dependencies + env: + IDRIS2_PREFIX: ${{ github.workspace }}/.tools/Idric/_/bootstrap-build + run: | + make -C .tools/Idric/_ bootstrap SCHEME=scheme + cd .tools/Idric-Net + ../Idric/_/build/exec/idris2 --install idric-net.ipkg + cd ../Idric/_/libs/contrib + ../../build/exec/idris2 --install contrib.ipkg + + - name: Build ICU and Ballotpedia + env: + IDRIS2_PREFIX: ${{ github.workspace }}/.tools/Idric/_/bootstrap-build + run: | + make -C .tools/icu \ + IDRIC="$GITHUB_WORKSPACE/.tools/Idric/_/build/exec/idris2" + make ballotpedia-check \ + IDRIC="$GITHUB_WORKSPACE/.tools/Idric/_/build/exec/idris2" + + - name: Start deterministic Ballotpedia-shaped server + run: | + cat > "$RUNNER_TEMP/ballotpedia-mock.py" <<'PY' + from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + from pathlib import Path + import os + + fixture = Path(os.environ["GITHUB_WORKSPACE"]) / \ + "checkpoints/ballotpedia/fixture/elections_by_state.json" + body = fixture.read_bytes() + + class Handler(BaseHTTPRequestHandler): + protocol_version = "HTTP/1.0" + + def log_message(self, format, *args): + pass + + def do_GET(self): + expected = "/data/elections_by_state?state=MI&election_date=2026-11-03&page=1" + if self.path != expected: + self.send_error(400, "wrong request target") + return + if self.headers.get("x-api-key") != "test-key": + self.send_error(401, "wrong API key") + return + if self.headers.get("Accept") != "application/json": + self.send_error(400, "wrong Accept header") + return + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + ThreadingHTTPServer(("127.0.0.1", 18083), Handler).serve_forever() + PY + python3 "$RUNNER_TEMP/ballotpedia-mock.py" \ + > "$RUNNER_TEMP/ballotpedia-mock.log" 2>&1 & + echo $! > "$RUNNER_TEMP/ballotpedia-mock.pid" + sleep 1 + + - name: Prove request headers, captured body, typed JSON, and TSV + env: + BALLOTPEDIA_API_KEY: test-key + BALLOTPEDIA_API_BASE: http://127.0.0.1:18083/data/elections_by_state + ICU: ${{ github.workspace }}/.tools/icu/build/exec/icu + LD_LIBRARY_PATH: ${{ github.workspace }}/.tools/icu + run: | + ./build/exec/ballotpedia elections MI 2026-11-03 \ + > "$RUNNER_TEMP/ballotpedia.tsv" + cmp checkpoints/ballotpedia/fixture/expected.tsv \ + "$RUNNER_TEMP/ballotpedia.tsv" + + if env -u BALLOTPEDIA_API_KEY \ + ./build/exec/ballotpedia elections MI 2026-11-03 \ + > "$RUNNER_TEMP/missing-key.out" \ + 2> "$RUNNER_TEMP/missing-key.err"; then + echo 'missing API key unexpectedly succeeded' >&2 + exit 1 + fi + test ! -s "$RUNNER_TEMP/missing-key.out" + grep -F 'missing BALLOTPEDIA_API_KEY' "$RUNNER_TEMP/missing-key.err" + + if BALLOTPEDIA_API_KEY= \ + ./build/exec/ballotpedia elections MI 2026-11-03 \ + > "$RUNNER_TEMP/empty-key.out" \ + 2> "$RUNNER_TEMP/empty-key.err"; then + echo 'empty API key unexpectedly succeeded' >&2 + exit 1 + fi + test ! -s "$RUNNER_TEMP/empty-key.out" + grep -F 'BALLOTPEDIA_API_KEY is empty' "$RUNNER_TEMP/empty-key.err" + + - name: Show mock log on failure + if: failure() + run: cat "$RUNNER_TEMP/ballotpedia-mock.log" 2>/dev/null || true diff --git a/Ballotpedia.idric b/Ballotpedia.idric new file mode 100644 index 0000000..44b8fd8 --- /dev/null +++ b/Ballotpedia.idric @@ -0,0 +1,328 @@ +module Ballotpedia + +import Language.JSON +import Network.URL +import System +import System.File + +%default total + +api_base : String +api_base = "https://api4.ballotpedia.org/data/elections_by_state" + +usage : String +usage = + "usage:\n" ++ + " ballotpedia url STATE YYYY-MM-DD\n" ++ + " ballotpedia elections STATE YYYY-MM-DD\n" ++ + "\nBALLOTPEDIA_API_KEY is required for elections." + +public export +choice command one_of + show_url String String + list_elections String String + +public export +record Candidate where + constructor MakeCandidate + person_id : Maybe String + name : String + parties : List String + district : Maybe String + race : Maybe String + +public export +choice ballotpedia_failure one_of + invalid_request String + missing_api_key + empty_api_key + network_failure Int + http_failure String + invalid_json + invalid_response String + +digit : Char → Bool +digit character = character >= '0' && character <= '9' + +public export +valid_state : String → Bool +valid_state state = + case unpack state of + [first, second] => + first >= 'A' && first <= 'Z' && + second >= 'A' && second <= 'Z' + _ => False + +public export +valid_date : String → Bool +valid_date date = + case unpack date of + [y1, y2, y3, y4, '-', m1, m2, '-', d1, d2] => + digit y1 && digit y2 && digit y3 && digit y4 && + digit m1 && digit m2 && digit d1 && digit d2 + _ => False + +public export +elections_url_text : String → String → String +elections_url_text state date = + api_base ++ "?state=" ++ state ++ "&election_date=" ++ date ++ "&page=1" + +elections_url_from_base : String → String → String → String +elections_url_from_base base state date = + base ++ "?state=" ++ state ++ "&election_date=" ++ date ++ "&page=1" + +parse_target_text : String → String → String → Either ballotpedia_failure url +parse_target_text target_text state date = + if not (valid_state state) + then Left (invalid_request "STATE must be two uppercase ASCII letters") + else if not (valid_date date) + then Left (invalid_request "date must have the form YYYY-MM-DD") + else case parse_url target_text of + Left problem => Left (invalid_request problem) + Right target => Right target + +public export +parse_target : String → String → Either ballotpedia_failure url +parse_target state date = + parse_target_text (elections_url_text state date) state date + +public export +parse_command : List String → Either String command +parse_command ["url", state, date] = Right (show_url state date) +parse_command ["elections", state, date] = Right (list_elections state date) +parse_command _ = Left usage + +object_field : String → List (String, JSON) → Maybe JSON +object_field wanted [] = Nothing +object_field wanted ((field_name, value) :: rest) = + if wanted == field_name then Just value else object_field wanted rest + +required_object : String → List (String, JSON) → Either String (List (String, JSON)) +required_object field_name object = + case object_field field_name object of + Just (JObject value) => Right value + Just _ => Left (field_name ++ " is not an object") + Nothing => Left ("missing " ++ field_name) + +required_string : String → List (String, JSON) → Either String String +required_string field_name object = + case object_field field_name object of + Just (JString value) => Right value + Just _ => Left (field_name ++ " is not a string") + Nothing => Left ("missing " ++ field_name) + +optional_string : String → List (String, JSON) → Maybe String +optional_string field_name object = + case object_field field_name object of + Just (JString value) => Just value + _ => Nothing + +optional_identifier : String → List (String, JSON) → Maybe String +optional_identifier field_name object = + case object_field field_name object of + Just (JString value) => Just value + Just (JNumber value) => Just (show value) + _ => Nothing + +optional_array : String → List (String, JSON) → Either String (List JSON) +optional_array field_name object = + case object_field field_name object of + Nothing => Right [] + Just JNull => Right [] + Just (JArray values) => Right values + Just _ => Left (field_name ++ " is not an array") + +party_name : JSON → Either String String +party_name (JObject object) = required_string "name" object +party_name _ = Left "party affiliation is not an object" + +party_names : List JSON → Either String (List String) +party_names [] = Right [] +party_names (value :: rest) = do + name ← party_name value + remaining ← party_names rest + pure (name :: remaining) + +decode_candidate : Maybe String → Maybe String → JSON → Either String Candidate +decode_candidate district_name race_name (JObject object) = do + person ← required_object "person" object + candidate_name ← required_string "name" person + affiliations ← optional_array "party_affiliation" object + affiliation_names ← party_names affiliations + pure + (MakeCandidate + (optional_identifier "id" person) + candidate_name + affiliation_names + district_name + race_name) +decode_candidate _ _ _ = Left "candidate is not an object" + +decode_candidate_values : + Maybe String → Maybe String → List JSON → Either String (List Candidate) +decode_candidate_values _ _ [] = Right [] +decode_candidate_values district_name race_name (value :: rest) = do + candidate ← decode_candidate district_name race_name value + remaining ← decode_candidate_values district_name race_name rest + pure (candidate :: remaining) + +decode_race : Maybe String → JSON → Either String (List Candidate) +decode_race district_name (JObject object) = do + candidates ← optional_array "candidates" object + decode_candidate_values district_name (optional_string "name" object) candidates +decode_race _ _ = Left "race is not an object" + +decode_races : Maybe String → List JSON → Either String (List Candidate) +decode_races _ [] = Right [] +decode_races district_name (value :: rest) = do + candidates ← decode_race district_name value + remaining ← decode_races district_name rest + pure (candidates ++ remaining) + +decode_district : JSON → Either String (List Candidate) +decode_district (JObject object) = do + races ← optional_array "races" object + decode_races (optional_string "name" object) races +decode_district _ = Left "district is not an object" + +decode_districts : List JSON → Either String (List Candidate) +decode_districts [] = Right [] +decode_districts (value :: rest) = do + candidates ← decode_district value + remaining ← decode_districts rest + pure (candidates ++ remaining) + +decode_root : JSON → Either String (List Candidate) +decode_root (JObject root) = do + data_object ← required_object "data" root + case object_field "districts" data_object of + Just (JArray districts) => decode_districts districts + Just _ => Left "districts is not an array" + Nothing => Left "missing districts" +decode_root _ = Left "response root is not an object" + +public export +decode_candidates : String → Either ballotpedia_failure (List Candidate) +decode_candidates body = + case Language.JSON.parse body of + Nothing => Left invalid_json + Just value => + case decode_root value of + Left problem => Left (invalid_response problem) + Right candidates => Right candidates + +clean_character : Char → Char +clean_character '\t' = ' ' +clean_character '\n' = ' ' +clean_character '\r' = ' ' +clean_character character = character + +public export +clean_field : String → String +clean_field text = pack (map clean_character (unpack text)) + +join_with : String → List String → String +join_with _ [] = "" +join_with _ [value] = value +join_with separator (value :: rest) = value ++ separator ++ join_with separator rest + +show_optional : Maybe String → String +show_optional Nothing = "" +show_optional (Just value) = value + +public export +render_candidate : Candidate → String +render_candidate candidate = + clean_field (show_optional candidate.person_id) ++ "\t" ++ + clean_field candidate.name ++ "\t" ++ + clean_field (join_with "; " candidate.parties) ++ "\t" ++ + clean_field (show_optional candidate.district) ++ "\t" ++ + clean_field (show_optional candidate.race) + +print_candidates : List Candidate → IO () +print_candidates [] = pure () +print_candidates (candidate :: rest) = do + putStrLn (render_candidate candidate) + print_candidates rest + +public export +icu_arguments : String → String → String → List String +icu_arguments executable api_key target_url = + [ executable + , "get" + , "-H" + , "x-api-key: " ++ api_key + , "-H" + , "Accept: application/json" + , target_url + ] + +covering +perform_elections : String → String → IO (Either ballotpedia_failure (List Candidate)) +perform_elections state date = do + maybe_base ← environment_value "BALLOTPEDIA_API_BASE" + let base = case maybe_base of + Just configured => configured + Nothing => api_base + let target_text = elections_url_from_base base state date + case parse_target_text target_text state date of + Left problem => pure (Left problem) + Right _ => do + maybe_key ← environment_value "BALLOTPEDIA_API_KEY" + case maybe_key of + Nothing => pure (Left missing_api_key) + Just "" => pure (Left empty_api_key) + Just api_key => do + maybe_icu ← environment_value "ICU" + let executable = case maybe_icu of + Just path => path + Nothing => "icu" + (body, status) ← run + (icu_arguments executable api_key target_text) + if status == 0 + then pure (decode_candidates body) + else if status == 10 + then pure (Left (http_failure body)) + else pure (Left (network_failure status)) + +public export +render_failure : ballotpedia_failure → String +render_failure (invalid_request problem) = "ballotpedia: invalid request: " ++ problem +render_failure missing_api_key = "ballotpedia: missing BALLOTPEDIA_API_KEY" +render_failure empty_api_key = "ballotpedia: BALLOTPEDIA_API_KEY is empty" +render_failure (network_failure status) = + "ballotpedia: ICU network/transport failure (exit " ++ show status ++ ")" +render_failure (http_failure _) = "ballotpedia: Ballotpedia returned a non-2xx HTTP response" +render_failure invalid_json = "ballotpedia: refused invalid JSON" +render_failure (invalid_response problem) = + "ballotpedia: refused unexpected response: " ++ problem + +fail_with : String → IO () +fail_with problem = do + _ ← fPutStrLn stderr problem + exitFailure + +covering +run_command : command → IO () +run_command (show_url state date) = + case parse_target state date of + Left problem => fail_with (render_failure problem) + Right _ => putStrLn (elections_url_text state date) +run_command (list_elections state date) = do + outcome ← perform_elections state date + case outcome of + Left problem => fail_with (render_failure problem) + Right candidates => do + putStrLn "person_id\tname\tparties\tdistrict\trace" + print_candidates candidates + +covering +main : IO () +main = do + arguments ← getArgs + case arguments of + _ :: rest => + case parse_command rest of + Left problem => fail_with problem + Right value => run_command value + [] => fail_with usage diff --git a/Makefile b/Makefile index 418650d..f67ec75 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,20 @@ PREFIX ?= /usr/local DESTDIR ?= SHELL ?= /bin/sh +IDRIC ?= idris2 -.PHONY: test install +.PHONY: test ballotpedia ballotpedia-check install test: bash test/az-test.sh bash test/abe-test.sh +ballotpedia: + $(IDRIC) --build ballotpedia.ipkg + +ballotpedia-check: + sh test/ballotpedia/check + install: install -d "$(DESTDIR)$(PREFIX)/bin" install -m 0755 bin/az "$(DESTDIR)$(PREFIX)/bin/az" diff --git a/README.md b/README.md index abe7022..dd64aed 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ This repository is the consolidation point for the CLI/API-access programs that - `bin/az` — Amazon product/price access and append-only price observations. - `bin/abe` — AbeBooks delivered-price lookup and Impact affiliate links. - `Ap.idric` — Associated Press API checkpoint. +- `Ballotpedia.idric` — executable Ballotpedia API4 page-one elections-by-state client, entirely in Idriç with ICU as its HTTP boundary. - `Economist.idric` — Economist API checkpoint. - `Ft.idric` — Financial Times API checkpoint. - `Guardian.idric` — Guardian API checkpoint. @@ -17,9 +18,9 @@ This repository is the consolidation point for the CLI/API-access programs that - `Reuters.idric` — Reuters GraphQL checkpoint. - `Wayback.idric` — Internet Archive Wayback/CDX checkpoint. -The top-level `.idric` files are symbolic links to the canonical sources under `checkpoints/`, so the important source is visible without digging through directories. +The important Idriç sources are exposed at the repository top level so they can be inspected without digging through directories. Most are symbolic links to canonical sources under `checkpoints/`; Ballotpedia's top-level file is currently its canonical source. -Some Idriç clients intentionally contain named holes for compiler/library boundaries that are not implemented yet. Keep those boundaries visible; do not make a client appear green by silently substituting another HTTP implementation. +Some Idriç clients intentionally contain named holes for compiler/library boundaries that are not implemented yet. Keep those boundaries visible; do not make a client appear green by silently substituting another HTTP implementation. Ballotpedia's former generic environment, header/body, and JSON holes have been replaced by executable boundaries and focused receipts; its remaining limits are recorded in `checkpoints/ballotpedia/README.md`. ## Networking @@ -27,6 +28,10 @@ Where these clients need networking, ICU/Idric-Net remains the intended transpor ## Tests -`make test` runs the existing Amazon and AbeBooks smoke tests. Reddit has a separate manual compiler checkpoint at `checkpoints/reddit/check`; it is not part of `make test` while named Idriç holes remain. +`make test` runs the existing Amazon and AbeBooks smoke tests. Reddit retains a separate manual compiler checkpoint while named Idriç holes remain. Ballotpedia has a separate reproducible receipt: + +```text +make ballotpedia-check IDRIC=/opt/Idric/_/build/exec/idris2 +``` See `PROVENANCE.md` for the source branches copied into this repository. diff --git a/ballotpedia.ipkg b/ballotpedia.ipkg new file mode 100644 index 0000000..94ca0b3 --- /dev/null +++ b/ballotpedia.ipkg @@ -0,0 +1,9 @@ +package ballotpedia + +version = 0.1.0 +depends = contrib, idric_net +sourcedir = "." + +modules = Ballotpedia +main = Ballotpedia +executable = ballotpedia diff --git a/checkpoints/ballotpedia/README.md b/checkpoints/ballotpedia/README.md new file mode 100644 index 0000000..4a70507 --- /dev/null +++ b/checkpoints/ballotpedia/README.md @@ -0,0 +1,72 @@ +# Ballotpedia CLI checkpoint + +The Idriç command is: + +```text +ballotpedia url MI 2026-11-03 +ballotpedia elections MI 2026-11-03 +``` + +It constructs an API4 `/data/elections_by_state` request and reads the API +key from `BALLOTPEDIA_API_KEY`. The key is sent only as the `x-api-key` +header. + +Ownership stays explicit: + +```text +arguments → Idriç validation → ICU command → HTTPS + → captured response body → Idriç JSON decoding → typed candidates → TSV +``` + +The request uses Ballotpedia's `election_date` parameter and explicitly asks +for page 1. The response decoder follows the available nested shape: + +```text +data → districts → races → candidates → person / party_affiliation +``` + +The earlier checkpoint's `date` parameter and flat `data.candidates` fixture +were not supported by the available client implementations and have been +removed. + +## Re-evaluated boundaries + +The three original named holes are no longer one undifferentiated block: + +- Idriç #64 supplies `environment_value`; the later source-layout change + accidentally dropped it from the buildable library tree, so the acceptance + workflow pins the narrow restoration until that regression is merged. +- ICU #13 accepts checked repeatable `-H` values, writes the response body to + stdout, and assigns distinct nonzero outcomes to HTTP and transport failure. + Ballotpedia invokes that command through `System.run`; there is no second + HTTP implementation here. +- `Language.JSON` performs the parse, and the Ballotpedia module projects the + nested fixture into a typed candidate record before rendering TSV. + +The remaining boundaries are specific rather than generic library holes: + +- ICU's current executable interface carries caller header values in the + child-process argument vector; a non-argv credential channel is not yet + available to this CLI; +- page 1 is implemented; following every Ballotpedia page is not; +- the nested synthetic fixture covers person name/id, party affiliation, and + optional district/race names, but a captured live response fixture is still + needed before claiming the rest of Ballotpedia's paid response contract; +- no live receipt runs without an explicitly supplied Ballotpedia API key. + +`BALLOTPEDIA_API_BASE` exists for deterministic local receipts. Normal use +leaves it unset and uses `https://api4.ballotpedia.org/data/elections_by_state`. + +The request parameter, pagination, and `data.districts` envelope are also +consistent with the available Ballotpedia Python client implementation. A +separate public adapter corroborates the nested races, candidates, person, and +party-affiliation path. Neither source substitutes for a retained live fixture. + +The `url` command and deterministic fixture receipt do not need a key. The +production path remains Idriç plus ICU; curl, Python, JavaScript, and SDKs are +not fallback transports. + +Sources used to correct the checkpoint: + +- +- diff --git a/checkpoints/ballotpedia/fixture/elections_by_state.json b/checkpoints/ballotpedia/fixture/elections_by_state.json new file mode 100644 index 0000000..8d1fd6a --- /dev/null +++ b/checkpoints/ballotpedia/fixture/elections_by_state.json @@ -0,0 +1,26 @@ +{ + "success": true, + "data": { + "districts": [ + { + "name": "Ward 1", + "races": [ + { + "name": "City Council", + "candidates": [ + { + "person": { + "id": "200", + "name": "Ada Example" + }, + "party_affiliation": [ + { "name": "Independent" } + ] + } + ] + } + ] + } + ] + } +} diff --git a/checkpoints/ballotpedia/fixture/expected.tsv b/checkpoints/ballotpedia/fixture/expected.tsv new file mode 100644 index 0000000..93b29b6 --- /dev/null +++ b/checkpoints/ballotpedia/fixture/expected.tsv @@ -0,0 +1,2 @@ +person_id name parties district race +200 Ada Example Independent Ward 1 City Council diff --git a/test/ballotpedia/BallotpediaTests.idric b/test/ballotpedia/BallotpediaTests.idric new file mode 100644 index 0000000..667e049 --- /dev/null +++ b/test/ballotpedia/BallotpediaTests.idric @@ -0,0 +1,68 @@ +module Main + +import Ballotpedia +import System +import System.File + +check : String → Bool → IO Bool +check name True = do + putStrLn ("PASS\t" ++ name) + pure True +check name False = do + putStrLn ("FAIL\t" ++ name) + pure False + +request_shape : Bool +request_shape = + elections_url_text "MI" "2026-11-03" == + "https://api4.ballotpedia.org/data/elections_by_state?state=MI&election_date=2026-11-03&page=1" + +header_shape : Bool +header_shape = + icu_arguments "icu" "test-key" "https://example.test/elections" == + [ "icu" + , "get" + , "-H" + , "x-api-key: test-key" + , "-H" + , "Accept: application/json" + , "https://example.test/elections" + ] + +decoded_shape : String → Bool +decoded_shape body = + case decode_candidates body of + Right [candidate] => + candidate.person_id == Just "200" && + candidate.name == "Ada Example" && + candidate.parties == ["Independent"] && + candidate.district == Just "Ward 1" && + candidate.race == Just "City Council" + _ => False + +invalid_json_refused : String → Bool +invalid_json_refused body = + case decode_candidates body of + Left invalid_json => True + _ => False + +wrong_envelope_refused : String → Bool +wrong_envelope_refused body = + case decode_candidates body of + Left (invalid_response _) => True + _ => False + +clean_tsv_field : Bool +clean_tsv_field = clean_field "one\ttwo\nthree\rfour" == "one two three four" + +main : IO () +main = do + Right fixture ← readFile "checkpoints/ballotpedia/fixture/elections_by_state.json" + | Left _ => exitFailure + one ← check "Ballotpedia request uses election_date and page" request_shape + two ← check "ICU owns checked caller headers" header_shape + three ← check "typed nested candidate decode" (decoded_shape fixture) + four ← check "invalid JSON refused" (invalid_json_refused "not-json") + five ← check "wrong response envelope refused" (wrong_envelope_refused "{\"data\":{}}") + six ← check "TSV separators flattened" clean_tsv_field + if one && two && three && four && five && six then pure () else exitFailure diff --git a/test/ballotpedia/check b/test/ballotpedia/check new file mode 100755 index 0000000..56beeee --- /dev/null +++ b/test/ballotpedia/check @@ -0,0 +1,19 @@ +#!/bin/sh + +set -u + +HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +ROOT=$(CDPATH= cd -- "$HERE/../.." && pwd) +IDRIC=${IDRIC:-idris2} + +if ! command -v "$IDRIC" >/dev/null 2>&1 && ! test -x "$IDRIC"; then + printf 'SKIP\tballotpedia/idric (not found: %s)\n' "$IDRIC" + exit 0 +fi + +cd "$ROOT" || exit 1 + +"$IDRIC" --build ballotpedia.ipkg +IDRIS2_PATH="$ROOT" "$IDRIC" -p contrib -p idric_net \ + test/ballotpedia/BallotpediaTests.idric -o ballotpedia-tests +"$ROOT/build/exec/ballotpedia-tests"