From f1dda329f0810517f171ed4299cc4abe38fe0ee0 Mon Sep 17 00:00:00 2001 From: mjohngreene Date: Sat, 31 Jan 2026 12:20:43 -0600 Subject: [PATCH 1/3] Add stdlib test suite with shell runner and CI integration Test 6 stdlib modules (prelude, list, math, option, result, string) with .goth test files that exercise key functions and a shell runner that compares output against expected values. Prelude and math tests define functions inline due to pre-existing parse errors from reserved keyword conflicts (round, pi). Co-Authored-By: Claude Opus 4.5 --- .github/workflows/ci.yml | 3 + tests/stdlib/test_list.goth | 45 ++++++ tests/stdlib/test_math.goth | 124 +++++++++++++++ tests/stdlib/test_option.goth | 45 ++++++ tests/stdlib/test_prelude.goth | 177 ++++++++++++++++++++++ tests/stdlib/test_result.goth | 41 +++++ tests/stdlib/test_string.goth | 39 +++++ tests/stdlib_test.sh | 269 +++++++++++++++++++++++++++++++++ 8 files changed, 743 insertions(+) create mode 100644 tests/stdlib/test_list.goth create mode 100644 tests/stdlib/test_math.goth create mode 100644 tests/stdlib/test_option.goth create mode 100644 tests/stdlib/test_prelude.goth create mode 100644 tests/stdlib/test_result.goth create mode 100644 tests/stdlib/test_string.goth create mode 100755 tests/stdlib_test.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 054c3f9..8cd4ff4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -84,6 +84,9 @@ jobs: echo "Testing numeric/gamma_fact..." cargo run --quiet --package goth-cli -- "$(pwd)/../examples/numeric/gamma_fact.goth" 5.0 + - name: Run stdlib tests + run: GOTH=./crates/target/release/goth bash tests/stdlib_test.sh + - name: Upload binary artifact uses: actions/upload-artifact@v4 with: diff --git a/tests/stdlib/test_list.goth b/tests/stdlib/test_list.goth new file mode 100644 index 0000000..9357bd4 --- /dev/null +++ b/tests/stdlib/test_list.goth @@ -0,0 +1,45 @@ +# Test suite for stdlib/list.goth + +use "../../stdlib/list.goth" + +╭─ main : () → () +╰─ let xs ← [10, 20, 30] in + # Access operations + let _ ← print (toString (head xs)) in + let _ ← print (toString (last xs)) in + let _ ← print (toString (tail xs)) in + let _ ← print (toString (init xs)) in + let _ ← print (toString (getOr xs 1 99)) in + let _ ← print (toString (getOr xs 5 99)) in + + # Construction + let _ ← print (toString (cons 0 xs)) in + let _ ← print (toString (snoc xs 40)) in + let a ← [1, 2] in + let b ← [3, 4] in + let _ ← print (toString (append a b)) in + let _ ← print (toString (replicate 3 7)) in + let _ ← print (toString (rangeTo 1 5)) in + + # Transformation + let ns ← [1, 2, 3] in + let _ ← print (toString (map ns (λ→ ₀ × 10))) in + let big ← [1, 2, 3, 4, 5] in + let _ ← print (toString (filter big (λ→ ₀ > 3))) in + let _ ← print (toString (rev ns)) in + + # Reduction + let fs ← [1.0, 2.0, 3.0] in + let _ ← print (toString (sum fs)) in + let gs ← [2.0, 3.0, 4.0] in + let _ ← print (toString (product gs)) in + let _ ← print (toString (all big (λ→ ₀ > 0))) in + let _ ← print (toString (all big (λ→ ₀ > 3))) in + let _ ← print (toString (any big (λ→ ₀ > 4))) in + let _ ← print (toString (count big (λ→ ₀ > 3))) in + + # Predicates + let _ ← print (toString (null ns)) in + let _ ← print (toString (size ns)) in + + ⟨⟩ diff --git a/tests/stdlib/test_math.goth b/tests/stdlib/test_math.goth new file mode 100644 index 0000000..c85f084 --- /dev/null +++ b/tests/stdlib/test_math.goth @@ -0,0 +1,124 @@ +# Test suite for stdlib/math.goth +# +# Note: math.goth has a parse error due to `pi` being a reserved keyword. +# Until that is fixed, we test math functions by defining them inline here. + +# === Basic ops === +╭─ absI : ℤ → ℤ +╰─ if ₀ < 0 then 0 - ₀ else ₀ + +╭─ absF : F → F +╰─ if ₀ < 0.0 then 0.0 - ₀ else ₀ + +╭─ signI : ℤ → ℤ +╰─ if ₀ < 0 then 0 - 1 else if ₀ > 0 then 1 else 0 + +╭─ signF : F → F +╰─ if ₀ < 0.0 then 0.0 - 1.0 else if ₀ > 0.0 then 1.0 else 0.0 + +# === Powers/roots === +╭─ sq : F → F +╰─ ₀ × ₀ + +╭─ cb : F → F +╰─ ₀ × ₀ × ₀ + +╭─ mySqrt : F → F +╰─ √₀ + +╭─ hypot : F → F → F +╰─ √(₁ × ₁ + ₀ × ₀) + +# === Number theory === +╭─ isEven : ℤ → Bool +╰─ ₀ % 2 = 0 + +╭─ isOdd : ℤ → Bool +╰─ ₀ % 2 ≠ 0 + +╭─ divides : ℤ → ℤ → Bool +╰─ ₀ % ₁ = 0 + +╭─ floorDiv : ℤ → ℤ → ℤ +╰─ ₁ / ₀ + +╭─ myMod : ℤ → ℤ → ℤ +╰─ let r ← ₁ % ₀ + in if r < 0 then r + ₀ else r + +# === Rounding === +╭─ myFloor : F → F +╰─ ⌊₀⌋ + +╭─ myCeil : F → F +╰─ ⌈₀⌉ + +# === Comparison === +╭─ minF : F → F → F +╰─ if ₁ < ₀ then ₁ else ₀ + +╭─ maxF : F → F → F +╰─ if ₁ > ₀ then ₁ else ₀ + +╭─ clamp : F → F → F → F +╰─ if ₀ < ₂ then ₂ else if ₀ > ₁ then ₁ else ₀ + +# === Interpolation === +╭─ lerp : F → F → F → F +╰─ ₁ + ₂ × (₀ - ₁) + +╭─ main : () → () +╰─ # Constants (using built-in π and 𝕖) + let _ ← print (toString (⌊π × 100.0 + 0.5⌋)) in + let _ ← print (toString (⌊𝕖 × 100.0 + 0.5⌋)) in + let _ ← print (toString (⌊((1.0 + √5.0) / 2.0) × 1000.0 + 0.5⌋)) in + let _ ← print (toString (⌊√2.0 × 1000.0 + 0.5⌋)) in + + # Basic ops + let _ ← print (toString (absI (0 - 7))) in + let _ ← print (toString (absI 3)) in + let _ ← print (toString (absF (0.0 - 2.5))) in + let _ ← print (toString (signI (0 - 4))) in + let _ ← print (toString (signI 0)) in + let _ ← print (toString (signI 6)) in + let _ ← print (toString (signF (0.0 - 1.5))) in + let _ ← print (toString (signF 0.0)) in + let _ ← print (toString (signF 3.0)) in + + # Powers/roots + let _ ← print (toString (sq 5.0)) in + let _ ← print (toString (cb 3.0)) in + let _ ← print (toString (mySqrt 16.0)) in + let _ ← print (toString (hypot 3.0 4.0)) in + + # Exponential/log (using built-ins) + let _ ← print (toString (⌊exp 1.0 × 100.0 + 0.5⌋)) in + let _ ← print (toString (⌊ln 𝕖 × 100.0 + 0.5⌋)) in + let _ ← print (toString (⌊log₁₀ 100.0 × 100.0 + 0.5⌋)) in + let _ ← print (toString (⌊log₂ 8.0 × 100.0 + 0.5⌋)) in + + # Number theory + let _ ← print (toString (isEven 6)) in + let _ ← print (toString (isOdd 7)) in + let _ ← print (toString (divides 5 10)) in + let _ ← print (toString (divides 3 10)) in + let _ ← print (toString (floorDiv 3 10)) in + let _ ← print (toString (floorDiv 10 3)) in + let _ ← print (toString (myMod 3 10)) in + let _ ← print (toString (myMod 10 3)) in + + # Rounding + let _ ← print (toString (myFloor 3.7)) in + let _ ← print (toString (myCeil 3.2)) in + + # Comparison + let _ ← print (toString (minF 3.0 7.0)) in + let _ ← print (toString (maxF 3.0 7.0)) in + let _ ← print (toString (clamp 2.0 8.0 5.0)) in + let _ ← print (toString (clamp 2.0 8.0 1.0)) in + let _ ← print (toString (clamp 2.0 8.0 10.0)) in + + # Interpolation + let _ ← print (toString (lerp 0.5 0.0 10.0)) in + + ⟨⟩ diff --git a/tests/stdlib/test_option.goth b/tests/stdlib/test_option.goth new file mode 100644 index 0000000..5a62a71 --- /dev/null +++ b/tests/stdlib/test_option.goth @@ -0,0 +1,45 @@ +# Test suite for stdlib/option.goth + +use "../../stdlib/option.goth" + +╭─ main : () → () +╰─ # Constructors + let _ ← print (toString (someI 42)) in + let _ ← print (toString (someF 3.14)) in + + # Predicates + let _ ← print (toString (isSome (someI 1))) in + let s ← someI 1 in + let _ ← print (toString (isNone s)) in + + # Extractors + let _ ← print (toString (getOrElseI 0 (someI 42))) in + let _ ← print (toString (getOrElseF 0.0 (someF 3.14))) in + + # Transformations + let _ ← print (toString (mapOptI (λ→ ₀ × 2) (someI 5))) in + let _ ← print (toString (mapOptI (λ→ ₀ × 2) (someI 0))) in + + # Filter + let _ ← print (toString (filterOptI (λ→ ₀ > 3) (someI 5))) in + let _ ← print (toString (filterOptI (λ→ ₀ > 3) (someI 2))) in + + # Combining + let a ← someI 1 in + let b ← someI 2 in + let _ ← print (toString (orElseOptI a b)) in + + # Safe operations + let _ ← print (toString (safeDivI 10 2)) in + let _ ← print (toString (safeDivI 10 0)) in + let _ ← print (toString (safeDivF 10.0 0.0)) in + + # Boolean checks + let _ ← print (toString (containsOptI 42 (someI 42))) in + let _ ← print (toString (containsOptI 42 (someI 99))) in + + # Show + let _ ← print (showOptI (someI 42)) in + let _ ← print (showOptF (someF 3.14)) in + + ⟨⟩ diff --git a/tests/stdlib/test_prelude.goth b/tests/stdlib/test_prelude.goth new file mode 100644 index 0000000..720731a --- /dev/null +++ b/tests/stdlib/test_prelude.goth @@ -0,0 +1,177 @@ +# Test suite for stdlib/prelude.goth +# +# Note: prelude.goth has a parse error due to `round` being a reserved keyword. +# Until that is fixed, we test prelude functions by defining them inline here. + +# === Combinators === +╭─ id : α → α +╰─ ₀ + +╭─ const : α → β → α +╰─ ₁ + +╭─ flip : (α → β → γ) → β → α → γ +╰─ ₂ ₀ ₁ + +╭─ compose : (β → γ) → (α → β) → α → γ +╰─ ₂ (₁ ₀) + +# === Boolean === +╭─ not : Bool → Bool +╰─ if ₀ then ⊥ else ⊤ + +╭─ and : Bool → Bool → Bool +╰─ if ₁ then ₀ else ⊥ + +╭─ or : Bool → Bool → Bool +╰─ if ₁ then ⊤ else ₀ + +╭─ xor : Bool → Bool → Bool +╰─ if ₁ then (if ₀ then ⊥ else ⊤) else ₀ + +╭─ implies : Bool → Bool → Bool +╰─ if ₁ then ₀ else ⊤ + +╭─ iff : Bool → Bool → Bool +╰─ if ₁ then ₀ else (if ₀ then ⊥ else ⊤) + +╭─ boolToInt : Bool → ℤ +╰─ if ₀ then 1 else 0 + +╭─ intToBool : ℤ → Bool +╰─ ₀ ≠ 0 + +# === Numeric predicates === +╭─ isZero : ℤ → Bool +╰─ ₀ = 0 + +╭─ isPositive : ℤ → Bool +╰─ ₀ > 0 + +╭─ isNegative : ℤ → Bool +╰─ ₀ < 0 + +╭─ isEven : ℤ → Bool +╰─ ₀ % 2 = 0 + +╭─ isOdd : ℤ → Bool +╰─ ₀ % 2 ≠ 0 + +# === Integer ops === +╭─ inc : ℤ → ℤ +╰─ ₀ + 1 + +╭─ dec : ℤ → ℤ +╰─ ₀ - 1 + +╭─ absInt : ℤ → ℤ +╰─ if ₀ < 0 then 0 - ₀ else ₀ + +╭─ signInt : ℤ → ℤ +╰─ if ₀ < 0 then 0 - 1 + else if ₀ > 0 then 1 + else 0 + +╭─ divInt : ℤ → ℤ → ℤ +╰─ ₁ / ₀ + +╭─ modInt : ℤ → ℤ → ℤ +╰─ ₁ % ₀ + +# === Float ops === +╭─ double : F → F +╰─ ₀ × 2.0 + +╭─ half : F → F +╰─ ₀ / 2.0 + +╭─ square : F → F +╰─ ₀ × ₀ + +╭─ negate : F → F +╰─ 0.0 - ₀ + +# === Tuple ops === +╭─ fst : ⟨α, β⟩ → α +╰─ ₀.0 + +╭─ snd : ⟨α, β⟩ → β +╰─ ₀.1 + +╭─ swap : ⟨α, β⟩ → ⟨β, α⟩ +╰─ ⟨₀.1, ₀.0⟩ + +# === Conditionals === +╭─ ifThenElse : Bool → α → α → α +╰─ if ₂ then ₁ else ₀ + +# === Rounding === +╭─ myFloor : F → F +╰─ ⌊₀⌋ + +╭─ myCeil : F → F +╰─ ⌈₀⌉ + +╭─ main : () → () +╰─ # Combinators + let _ ← print (toString (id 42)) in + let _ ← print (toString (const 10 99)) in + let _ ← print (toString (flip (λ→ λ→ ₁ - ₀) 3 10)) in + let _ ← print (toString (compose (λ→ ₀ × 2) (λ→ ₀ + 1) 5)) in + + # Boolean + let _ ← print (toString (not ⊤)) in + let _ ← print (toString (not ⊥)) in + let _ ← print (toString (and ⊤ ⊥)) in + let _ ← print (toString (and ⊤ ⊤)) in + let _ ← print (toString (or ⊥ ⊥)) in + let _ ← print (toString (or ⊥ ⊤)) in + let _ ← print (toString (xor ⊤ ⊤)) in + let _ ← print (toString (xor ⊤ ⊥)) in + let _ ← print (toString (implies ⊤ ⊥)) in + let _ ← print (toString (implies ⊥ ⊥)) in + let _ ← print (toString (iff ⊤ ⊤)) in + let _ ← print (toString (iff ⊤ ⊥)) in + let _ ← print (toString (boolToInt ⊤)) in + let _ ← print (toString (boolToInt ⊥)) in + let _ ← print (toString (intToBool 0)) in + let _ ← print (toString (intToBool 5)) in + + # Numeric predicates + let _ ← print (toString (isZero 0)) in + let _ ← print (toString (isZero 1)) in + let _ ← print (toString (isPositive 5)) in + let _ ← print (toString (isNegative (0 - 3))) in + let _ ← print (toString (isEven 4)) in + let _ ← print (toString (isOdd 7)) in + + # Integer ops + let _ ← print (toString (inc 10)) in + let _ ← print (toString (dec 10)) in + let _ ← print (toString (absInt (0 - 5))) in + let _ ← print (toString (signInt (0 - 7))) in + let _ ← print (toString (signInt 3)) in + let _ ← print (toString (signInt 0)) in + let _ ← print (toString (divInt 3 10)) in + let _ ← print (toString (modInt 3 10)) in + + # Float ops + let _ ← print (toString (double 3.5)) in + let _ ← print (toString (half 10.0)) in + let _ ← print (toString (square 4.0)) in + let _ ← print (toString (negate 5.0)) in + + # Tuple ops + let _ ← print (toString (fst ⟨10, 20⟩)) in + let _ ← print (toString (snd ⟨10, 20⟩)) in + let _ ← print (toString (swap ⟨1, 2⟩)) in + + # Conditionals + let _ ← print (toString (ifThenElse ⊤ 1 2)) in + let _ ← print (toString (ifThenElse ⊥ 1 2)) in + + # Rounding + let _ ← print (toString (myFloor 3.7)) in + let _ ← print (toString (myCeil 3.2)) in + + ⟨⟩ diff --git a/tests/stdlib/test_result.goth b/tests/stdlib/test_result.goth new file mode 100644 index 0000000..e546283 --- /dev/null +++ b/tests/stdlib/test_result.goth @@ -0,0 +1,41 @@ +# Test suite for stdlib/result.goth + +use "../../stdlib/result.goth" + +╭─ main : () → () +╰─ # Constructors + let _ ← print (toString (okII 42)) in + let _ ← print (toString (errII 1)) in + let _ ← print (toString (okIS 10)) in + let _ ← print (toString (errIS "bad")) in + + # Predicates + let _ ← print (toString (isOk (okII 42))) in + let _ ← print (toString (isOk (errII 1))) in + let _ ← print (toString (isErr (errII 1))) in + let _ ← print (toString (isErr (okII 42))) in + + # Extractors + let _ ← print (toString (unwrapOrII 0 (okII 42))) in + let _ ← print (toString (unwrapOrII 0 (errII 1))) in + let _ ← print (toString (unwrapOrIS 0 (okIS 42))) in + let _ ← print (toString (unwrapOrIS 0 (errIS "bad"))) in + + # Transformations + let _ ← print (toString (mapResII (λ→ ₀ × 2) (okII 5))) in + let _ ← print (toString (mapResII (λ→ ₀ × 2) (errII 1))) in + + # Safe operations + let _ ← print (toString (safeDivResII 10 2)) in + let _ ← print (toString (safeDivResII 10 0)) in + + # Boolean checks + let _ ← print (toString (containsResII 42 (okII 42))) in + let _ ← print (toString (containsResII 42 (okII 99))) in + let _ ← print (toString (containsResII 42 (errII 1))) in + + # Show + let _ ← print (showResII (okII 42)) in + let _ ← print (showResII (errII 1)) in + + ⟨⟩ diff --git a/tests/stdlib/test_string.goth b/tests/stdlib/test_string.goth new file mode 100644 index 0000000..5135c72 --- /dev/null +++ b/tests/stdlib/test_string.goth @@ -0,0 +1,39 @@ +# Test suite for stdlib/string.goth + +use "../../stdlib/string.goth" + +╭─ main : () → () +╰─ # Properties + let _ ← print (toString (strLen "hello")) in + let _ ← print (toString (strEmpty "")) in + let _ ← print (toString (strEmpty "hi")) in + let _ ← print (toString (strNonEmpty "hi")) in + + # Comparison + let _ ← print (toString (strEqual "abc" "abc")) in + let _ ← print (toString (strEqual "abc" "xyz")) in + let _ ← print (toString (strNotEqual "abc" "xyz")) in + let _ ← print (toString (hasPrefix "hello world" "hello")) in + let _ ← print (toString (hasSuffix "hello world" "world")) in + let _ ← print (toString (hasSubstring "hello world" "lo wo")) in + + # Char predicates + let _ ← print (toString (isDigit '5')) in + let _ ← print (toString (isDigit 'a')) in + let _ ← print (toString (isAlpha 'z')) in + let _ ← print (toString (isSpace ' ')) in + + # Conversion + let _ ← print (intToStr 42) in + let _ ← print (boolToStr ⊤) in + let _ ← print (boolToStr ⊥) in + + # Constants + let _ ← print (toString (strLen emptyStr)) in + + # Word/line operations + let _ ← print (toString (wordCount "hello world foo")) in + let _ ← print (firstWord "hello world foo") in + let _ ← print (lastWord "hello world foo") in + + ⟨⟩ diff --git a/tests/stdlib_test.sh b/tests/stdlib_test.sh new file mode 100755 index 0000000..615c926 --- /dev/null +++ b/tests/stdlib_test.sh @@ -0,0 +1,269 @@ +#!/bin/bash +# Stdlib test suite for Goth +# Tests the standard library modules by running .goth test files +# and comparing stdout against expected output. + +set -e + +GOTH="${GOTH:-./crates/target/release/goth}" + +# Color output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +NC='\033[0m' + +PASS=0 +FAIL=0 + +pass() { + echo -e "${GREEN}✓ $1${NC}" + PASS=$((PASS + 1)) +} + +fail() { + echo -e "${RED}✗ $1${NC}" + FAIL=$((FAIL + 1)) +} + +run_test() { + local name="$1" + local file="$2" + local expected="$3" + + local actual + actual=$($GOTH "$file" 2>&1) || true + + if [ "$actual" = "$expected" ]; then + pass "$name" + else + fail "$name" + echo -e "${YELLOW} Expected:${NC}" + echo "$expected" | head -5 + echo -e "${YELLOW} Got:${NC}" + echo "$actual" | head -5 + if [ "$(echo "$expected" | wc -l)" -gt 5 ]; then + echo " ... (truncated)" + fi + fi +} + +echo "=== Goth Stdlib Tests ===" +echo "" + +# --- Test: prelude --- +echo "Module: prelude" +EXPECTED_PRELUDE=$(cat <<'EOF' +42 +10 +7 +12 +⊥ +⊤ +⊥ +⊤ +⊥ +⊤ +⊥ +⊤ +⊥ +⊤ +⊤ +⊥ +1 +0 +⊥ +⊤ +⊤ +⊥ +⊤ +⊤ +⊤ +⊤ +11 +9 +5 +-1 +1 +0 +0 +3 +7 +5 +16 +-5 +10 +20 +⟨2, 1⟩ +1 +2 +3 +4 +EOF +) +run_test "prelude" "tests/stdlib/test_prelude.goth" "$EXPECTED_PRELUDE" + +# --- Test: list --- +echo "Module: list" +EXPECTED_LIST=$(cat <<'EOF' +10 +30 +[20 30] +[10 20] +20 +99 +[0 10 20 30] +[10 20 30 40] +[1 2 3 4] +[7 7 7] +[1 2 3 4 5] +[10 20 30] +[4 5] +[3 2 1] +6 +24 +⊤ +⊥ +⊤ +2 +⊥ +3 +EOF +) +run_test "list" "tests/stdlib/test_list.goth" "$EXPECTED_LIST" + +# --- Test: math --- +echo "Module: math" +EXPECTED_MATH=$(cat <<'EOF' +314 +272 +1618 +1414 +7 +3 +2.5 +-1 +0 +1 +-1 +0 +1 +25 +27 +4 +5 +272 +100 +200 +300 +⊤ +⊤ +⊤ +⊥ +0 +3 +3 +1 +3 +4 +3 +7 +5 +2 +8 +5 +EOF +) +run_test "math" "tests/stdlib/test_math.goth" "$EXPECTED_MATH" + +# --- Test: option --- +echo "Module: option" +EXPECTED_OPTION=$(cat <<'EOF' +⟨⊤, 42⟩ +⟨⊤, 3.14⟩ +⊤ +⊥ +42 +3.14 +⟨⊤, 10⟩ +⟨⊤, 0⟩ +⟨⊤, 5⟩ +⟨⊥, 0⟩ +⟨⊤, 1⟩ +⟨⊤, 5⟩ +⟨⊥, 0⟩ +⟨⊥, 0⟩ +⊤ +⊥ +Some(42) +Some(3.14) +EOF +) +run_test "option" "tests/stdlib/test_option.goth" "$EXPECTED_OPTION" + +# --- Test: result --- +echo "Module: result" +EXPECTED_RESULT=$(cat <<'EOF' +⟨⊤, 42, 0⟩ +⟨⊥, 0, 1⟩ +⟨⊤, 10, ""⟩ +⟨⊥, 0, "bad"⟩ +⊤ +⊥ +⊤ +⊥ +42 +0 +42 +0 +⟨⊤, 10, 0⟩ +⟨⊥, 0, 1⟩ +⟨⊤, 5, 0⟩ +⟨⊥, 0, -1⟩ +⊤ +⊥ +⊥ +Ok(42) +Err(1) +EOF +) +run_test "result" "tests/stdlib/test_result.goth" "$EXPECTED_RESULT" + +# --- Test: string --- +echo "Module: string" +EXPECTED_STRING=$(cat <<'EOF' +5 +⊤ +⊥ +⊤ +⊤ +⊥ +⊤ +⊤ +⊤ +⊤ +⊤ +⊥ +⊤ +⊤ +42 +true +false +0 +3 +hello +foo +EOF +) +run_test "string" "tests/stdlib/test_string.goth" "$EXPECTED_STRING" + +# --- Summary --- +echo "" +TOTAL=$((PASS + FAIL)) +echo "Results: $PASS/$TOTAL passed" +if [ "$FAIL" -gt 0 ]; then + echo -e "${RED}$FAIL test(s) failed${NC}" + exit 1 +else + echo -e "${GREEN}All tests passed!${NC}" +fi From dc1219f4fc912738ff070989d098f237eacaac34 Mon Sep 17 00:00:00 2001 From: mjohngreene Date: Sat, 31 Jan 2026 17:37:19 -0600 Subject: [PATCH 2/3] Update stdlib tests for refactored option/result APIs and pin dependencies Update test_option.goth and test_result.goth to use the new generic stdlib API names (some/none/ok/err instead of someI/noneI/okII/errII). Update expected outputs in stdlib_test.sh accordingly. Pin Cargo.lock dependencies for compatibility with rustc 1.86.0-nightly. Co-Authored-By: Claude Opus 4.5 --- crates/Cargo.lock | 18 +++++++------- tests/stdlib/test_option.goth | 44 ++++++++++++++++++----------------- tests/stdlib/test_result.goth | 40 +++++++++++++++---------------- tests/stdlib_test.sh | 17 +++++++------- 4 files changed, 60 insertions(+), 59 deletions(-) diff --git a/crates/Cargo.lock b/crates/Cargo.lock index 03fa664..62a5372 100644 --- a/crates/Cargo.lock +++ b/crates/Cargo.lock @@ -47,7 +47,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -58,7 +58,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -353,7 +353,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -591,11 +591,11 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "home" -version = "0.5.12" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1059,7 +1059,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1350,7 +1350,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -1634,7 +1634,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] diff --git a/tests/stdlib/test_option.goth b/tests/stdlib/test_option.goth index 5a62a71..43912e3 100644 --- a/tests/stdlib/test_option.goth +++ b/tests/stdlib/test_option.goth @@ -4,42 +4,44 @@ use "../../stdlib/option.goth" ╭─ main : () → () ╰─ # Constructors - let _ ← print (toString (someI 42)) in - let _ ← print (toString (someF 3.14)) in + let _ ← print (toString (some 42)) in + let _ ← print (toString (some 3.14)) in + let _ ← print (toString (none ⟨⟩)) in # Predicates - let _ ← print (toString (isSome (someI 1))) in - let s ← someI 1 in - let _ ← print (toString (isNone s)) in + let _ ← print (toString (isSome (some 1))) in + let _ ← print (toString (isSome (none ⟨⟩))) in + let _ ← print (toString (isNone (none ⟨⟩))) in + let _ ← print (toString (isNone (some 1))) in # Extractors - let _ ← print (toString (getOrElseI 0 (someI 42))) in - let _ ← print (toString (getOrElseF 0.0 (someF 3.14))) in + let _ ← print (toString (getOrElse 0 (some 42))) in + let _ ← print (toString (getOrElse 0 (none ⟨⟩))) in # Transformations - let _ ← print (toString (mapOptI (λ→ ₀ × 2) (someI 5))) in - let _ ← print (toString (mapOptI (λ→ ₀ × 2) (someI 0))) in + let _ ← print (toString (mapOpt (λ→ ₀ × 2) (some 5))) in + let _ ← print (toString (mapOpt (λ→ ₀ × 2) (none ⟨⟩))) in # Filter - let _ ← print (toString (filterOptI (λ→ ₀ > 3) (someI 5))) in - let _ ← print (toString (filterOptI (λ→ ₀ > 3) (someI 2))) in + let _ ← print (toString (filterOpt (λ→ ₀ > 3) (some 5))) in + let _ ← print (toString (filterOpt (λ→ ₀ > 3) (some 2))) in # Combining - let a ← someI 1 in - let b ← someI 2 in - let _ ← print (toString (orElseOptI a b)) in + let a ← some 1 in + let b ← some 2 in + let _ ← print (toString (orElseOpt a b)) in # Safe operations - let _ ← print (toString (safeDivI 10 2)) in - let _ ← print (toString (safeDivI 10 0)) in - let _ ← print (toString (safeDivF 10.0 0.0)) in + let _ ← print (toString (safeDiv 10 2)) in + let _ ← print (toString (safeDiv 10 0)) in # Boolean checks - let _ ← print (toString (containsOptI 42 (someI 42))) in - let _ ← print (toString (containsOptI 42 (someI 99))) in + let _ ← print (toString (containsOpt 42 (some 42))) in + let _ ← print (toString (containsOpt 42 (some 99))) in # Show - let _ ← print (showOptI (someI 42)) in - let _ ← print (showOptF (someF 3.14)) in + let _ ← print (showOpt (some 42)) in + let _ ← print (showOpt (some 3.14)) in + let _ ← print (showOpt (none ⟨⟩)) in ⟨⟩ diff --git a/tests/stdlib/test_result.goth b/tests/stdlib/test_result.goth index e546283..bb7998f 100644 --- a/tests/stdlib/test_result.goth +++ b/tests/stdlib/test_result.goth @@ -4,38 +4,36 @@ use "../../stdlib/result.goth" ╭─ main : () → () ╰─ # Constructors - let _ ← print (toString (okII 42)) in - let _ ← print (toString (errII 1)) in - let _ ← print (toString (okIS 10)) in - let _ ← print (toString (errIS "bad")) in + let _ ← print (toString (ok 42)) in + let _ ← print (toString (err 1)) in + let _ ← print (toString (ok 10)) in + let _ ← print (toString (err "bad")) in # Predicates - let _ ← print (toString (isOk (okII 42))) in - let _ ← print (toString (isOk (errII 1))) in - let _ ← print (toString (isErr (errII 1))) in - let _ ← print (toString (isErr (okII 42))) in + let _ ← print (toString (isOk (ok 42))) in + let _ ← print (toString (isOk (err 1))) in + let _ ← print (toString (isErr (err 1))) in + let _ ← print (toString (isErr (ok 42))) in # Extractors - let _ ← print (toString (unwrapOrII 0 (okII 42))) in - let _ ← print (toString (unwrapOrII 0 (errII 1))) in - let _ ← print (toString (unwrapOrIS 0 (okIS 42))) in - let _ ← print (toString (unwrapOrIS 0 (errIS "bad"))) in + let _ ← print (toString (unwrapOr 0 (ok 42))) in + let _ ← print (toString (unwrapOr 0 (err 1))) in # Transformations - let _ ← print (toString (mapResII (λ→ ₀ × 2) (okII 5))) in - let _ ← print (toString (mapResII (λ→ ₀ × 2) (errII 1))) in + let _ ← print (toString (mapRes (λ→ ₀ × 2) (ok 5))) in + let _ ← print (toString (mapRes (λ→ ₀ × 2) (err 1))) in # Safe operations - let _ ← print (toString (safeDivResII 10 2)) in - let _ ← print (toString (safeDivResII 10 0)) in + let _ ← print (toString (safeDivRes 10 2)) in + let _ ← print (toString (safeDivRes 10 0)) in # Boolean checks - let _ ← print (toString (containsResII 42 (okII 42))) in - let _ ← print (toString (containsResII 42 (okII 99))) in - let _ ← print (toString (containsResII 42 (errII 1))) in + let _ ← print (toString (containsRes 42 (ok 42))) in + let _ ← print (toString (containsRes 42 (ok 99))) in + let _ ← print (toString (containsRes 42 (err 1))) in # Show - let _ ← print (showResII (okII 42)) in - let _ ← print (showResII (errII 1)) in + let _ ← print (showRes (ok 42)) in + let _ ← print (showRes (err 1)) in ⟨⟩ diff --git a/tests/stdlib_test.sh b/tests/stdlib_test.sh index 615c926..1264782 100755 --- a/tests/stdlib_test.sh +++ b/tests/stdlib_test.sh @@ -181,22 +181,25 @@ echo "Module: option" EXPECTED_OPTION=$(cat <<'EOF' ⟨⊤, 42⟩ ⟨⊤, 3.14⟩ +⟨⊥, 0⟩ +⊤ +⊥ ⊤ ⊥ 42 -3.14 +0 ⟨⊤, 10⟩ -⟨⊤, 0⟩ +⟨⊥, 0⟩ ⟨⊤, 5⟩ ⟨⊥, 0⟩ ⟨⊤, 1⟩ ⟨⊤, 5⟩ ⟨⊥, 0⟩ -⟨⊥, 0⟩ ⊤ ⊥ Some(42) Some(3.14) +None EOF ) run_test "option" "tests/stdlib/test_option.goth" "$EXPECTED_OPTION" @@ -206,7 +209,7 @@ echo "Module: result" EXPECTED_RESULT=$(cat <<'EOF' ⟨⊤, 42, 0⟩ ⟨⊥, 0, 1⟩ -⟨⊤, 10, ""⟩ +⟨⊤, 10, 0⟩ ⟨⊥, 0, "bad"⟩ ⊤ ⊥ @@ -214,12 +217,10 @@ EXPECTED_RESULT=$(cat <<'EOF' ⊥ 42 0 -42 -0 ⟨⊤, 10, 0⟩ ⟨⊥, 0, 1⟩ -⟨⊤, 5, 0⟩ -⟨⊥, 0, -1⟩ +⟨⊤, 5, ""⟩ +⟨⊥, 0, "division by zero"⟩ ⊤ ⊥ ⊥ From 3c0f40e828aaad68c9622a2936152384ef71915e Mon Sep 17 00:00:00 2001 From: mjohngreene Date: Sat, 31 Jan 2026 18:45:00 -0600 Subject: [PATCH 3/3] Add CLI smoke test suite with shell runner and CI integration 28 tests across 9 sections covering expression evaluation, file execution, multi-argument programs, parse/AST/type-check modes, JSON workflow, no-main mode, and error handling. Co-Authored-By: Claude Opus 4.5 --- .github/workflows/ci.yml | 3 + tests/cli_test.sh | 206 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 209 insertions(+) create mode 100644 tests/cli_test.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8cd4ff4..8cfa4cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,6 +87,9 @@ jobs: - name: Run stdlib tests run: GOTH=./crates/target/release/goth bash tests/stdlib_test.sh + - name: Run CLI smoke tests + run: GOTH=./crates/target/release/goth bash tests/cli_test.sh + - name: Upload binary artifact uses: actions/upload-artifact@v4 with: diff --git a/tests/cli_test.sh b/tests/cli_test.sh new file mode 100644 index 0000000..7aa8abd --- /dev/null +++ b/tests/cli_test.sh @@ -0,0 +1,206 @@ +#!/bin/bash +# CLI smoke test suite for Goth +# Tests all major CLI modes of the goth interpreter binary. + +set -e + +GOTH="${GOTH:-./crates/target/release/goth}" + +# Color output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +NC='\033[0m' + +PASS=0 +FAIL=0 + +pass() { + echo -e "${GREEN}✓ $1${NC}" + PASS=$((PASS + 1)) +} + +fail() { + echo -e "${RED}✗ $1${NC}" + FAIL=$((FAIL + 1)) +} + +# Test exact output match +run_test() { + local name="$1" + local expected="$2" + shift 2 + + local actual + actual=$("$@" 2>&1) || true + + if [ "$actual" = "$expected" ]; then + pass "$name" + else + fail "$name" + echo -e "${YELLOW} Expected:${NC} $(echo "$expected" | head -1)" + echo -e "${YELLOW} Got:${NC} $(echo "$actual" | head -1)" + fi +} + +# Test that output contains a substring (case-sensitive) +run_test_contains() { + local name="$1" + local substring="$2" + shift 2 + + local actual + actual=$("$@" 2>&1) || true + + if echo "$actual" | grep -qF "$substring"; then + pass "$name" + else + fail "$name" + echo -e "${YELLOW} Expected to contain:${NC} $substring" + echo -e "${YELLOW} Got:${NC} $(echo "$actual" | head -3)" + fi +} + +# Test that output contains a substring (case-insensitive) +run_test_contains_i() { + local name="$1" + local substring="$2" + shift 2 + + local actual + actual=$("$@" 2>&1) || true + + if echo "$actual" | grep -qiF "$substring"; then + pass "$name" + else + fail "$name" + echo -e "${YELLOW} Expected to contain (case-insensitive):${NC} $substring" + echo -e "${YELLOW} Got:${NC} $(echo "$actual" | head -3)" + fi +} + +# Test that stdout is empty +run_test_empty() { + local name="$1" + shift + + local actual + actual=$("$@" 2>&1) || true + + if [ -z "$actual" ]; then + pass "$name" + else + fail "$name" + echo -e "${YELLOW} Expected empty output${NC}" + echo -e "${YELLOW} Got:${NC} $(echo "$actual" | head -1)" + fi +} + +echo "=== Goth CLI Smoke Tests ===" +echo "" + +# --- Section 1: Expression evaluation (-e) --- +echo "Section: Expression evaluation (-e)" +run_test "integer arithmetic" "7" $GOTH -e "1 + 2 * 3" +run_test "negative result" "-7" $GOTH -e "3 - 10" +run_test "boolean true" "⊤" $GOTH -e "3 > 2" +run_test "boolean false" "⊥" $GOTH -e "3 < 2" +run_test "lambda application" "6" $GOTH -e "(λ→ ₀ + 1) 5" +run_test "array sum" "15" $GOTH -e "Σ [1, 2, 3, 4, 5]" +run_test "let binding" "15" $GOTH -e "let x ← 10 in x + 5" +echo "" + +# --- Section 2: File execution --- +echo "Section: File execution" +run_test "identity" "42" $GOTH examples/basic/identity.goth 42 +run_test "add_one" "100" $GOTH examples/basic/add_one.goth 99 +run_test "square" "49" $GOTH examples/basic/square.goth 7 +run_test "factorial" "120" $GOTH examples/recursion/factorial.goth 5 +run_test "fibonacci" "55" $GOTH examples/recursion/fibonacci.goth 10 +run_test "isPrime" "⊤" $GOTH examples/algorithms/isPrime.goth 17 +echo "" + +# --- Section 3: Multi-argument programs --- +echo "Section: Multi-argument programs" +run_test "gcd" "4" $GOTH examples/recursion/gcd.goth 12 8 +run_test "compose" "36" $GOTH examples/higher-order/compose.goth 3 +echo "" + +# --- Section 4: Parse-only mode (-p) --- +echo "Section: Parse-only mode (-p)" +run_test_contains "parse file" "Parsed:" $GOTH -p examples/basic/identity.goth +run_test_empty "parse expression" $GOTH -p -e "1 + 2" +echo "" + +# --- Section 5: AST mode (-a) --- +echo "Section: AST mode (-a)" +run_test_contains "ast expression" "AST:" $GOTH -a -e "1 + 2" +run_test_contains "ast file" "Parsed AST:" $GOTH -a examples/basic/identity.goth +echo "" + +# --- Section 6: Type check mode (-c) --- +echo "Section: Type check mode (-c)" +run_test_contains "type check expression" "Type:" $GOTH -c -e "1 + 2" +run_test "type check file with args" "100" $GOTH -c examples/basic/add_one.goth 99 +echo "" + +# --- Section 7: JSON workflow --- +echo "Section: JSON workflow" +TMPDIR=$(mktemp -d) +trap "rm -rf $TMPDIR" EXIT + +run_test_contains "to-json produces JSON" '"decls"' $GOTH --to-json examples/basic/identity.goth + +# Compact JSON should be a single line +COMPACT_OUTPUT=$($GOTH --compact --to-json examples/basic/identity.goth 2>&1) || true +COMPACT_LINES=$(echo "$COMPACT_OUTPUT" | wc -l) +if [ "$COMPACT_LINES" -eq 1 ] && echo "$COMPACT_OUTPUT" | grep -qF '"decls"'; then + pass "compact to-json is single line" +else + fail "compact to-json is single line" + echo -e "${YELLOW} Lines: $COMPACT_LINES${NC}" +fi + +# JSON round-trip +$GOTH --to-json examples/basic/add_one.goth > "$TMPDIR/add_one.json" 2>&1 +ROUNDTRIP=$($GOTH --from-json "$TMPDIR/add_one.json" 99 2>&1) || true +if echo "$ROUNDTRIP" | grep -qF "100"; then + pass "json round-trip preserves semantics" +else + fail "json round-trip preserves semantics" + echo -e "${YELLOW} Expected output to contain: 100${NC}" + echo -e "${YELLOW} Got:${NC} $ROUNDTRIP" +fi +echo "" + +# --- Section 8: No-main mode --- +echo "Section: No-main mode (--no-main)" +run_test_contains "no-main shows declarations" "fn main" $GOTH --no-main examples/basic/identity.goth +echo "" + +# --- Section 9: Error handling --- +echo "Section: Error handling" +run_test_contains_i "nonexistent file" "error" $GOTH nonexistent_file.goth +run_test_contains_i "syntax error" "error" $GOTH -e "1 + + 2" + +# Render from JSON +$GOTH --to-json examples/basic/add_one.goth > "$TMPDIR/render_test.json" 2>&1 +RENDER_OUTPUT=$($GOTH --render --from-json "$TMPDIR/render_test.json" 2>&1) || true +if echo "$RENDER_OUTPUT" | grep -qF "module" && echo "$RENDER_OUTPUT" | grep -qF "main"; then + pass "render from json produces source" +else + fail "render from json produces source" + echo -e "${YELLOW} Expected 'module' and 'main' in output${NC}" + echo -e "${YELLOW} Got:${NC} $(echo "$RENDER_OUTPUT" | head -3)" +fi +echo "" + +# --- Summary --- +TOTAL=$((PASS + FAIL)) +echo "Results: $PASS/$TOTAL passed" +if [ "$FAIL" -gt 0 ]; then + echo -e "${RED}$FAIL test(s) failed${NC}" + exit 1 +else + echo -e "${GREEN}All tests passed!${NC}" +fi