restructure: move el compiler content into lang/
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
// cross-calendar-comparison.el — same Instant under two different calendars
|
||||
// produces identical cal_to_instant() outputs. Calendar choice does not change
|
||||
// the underlying instant — only the projection.
|
||||
|
||||
fn run_test() -> Int {
|
||||
let i: Instant = unix_seconds(1782216000)
|
||||
let z: Zone = zone("America/New_York")
|
||||
let earth: Calendar = earth_calendar(z)
|
||||
let mars: Calendar = mars_calendar()
|
||||
let ct_earth: CalendarTime = in_calendar(i, earth)
|
||||
let ct_mars: CalendarTime = in_calendar(i, mars)
|
||||
let i_earth: Instant = cal_to_instant(ct_earth)
|
||||
let i_mars: Instant = cal_to_instant(ct_mars)
|
||||
if i_earth == i_mars {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println(int_to_str(run_test()))
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// cycle-300yr.el — CycleCalendar with a 100-year period proves the math
|
||||
// holds at long periods. (300 years exceeds int64 nanos: 2^63 ns ≈ 292 yr;
|
||||
// 100 yr is the largest round period that fits while leaving headroom for
|
||||
// instants on either side.) One earth year apart yields phase_diff ~ 0.01.
|
||||
|
||||
fn run_test() -> String {
|
||||
// 100 Julian years = 100 * 31557600 = 3155760000 seconds.
|
||||
let period: Duration = duration_seconds(3155760000)
|
||||
let cal: Calendar = cycle_calendar(period)
|
||||
let base: Instant = unix_seconds(0)
|
||||
let later: Instant = unix_seconds(31557600)
|
||||
let ct1: CalendarTime = in_calendar(base, cal)
|
||||
let ct2: CalendarTime = in_calendar(later, cal)
|
||||
let p1: Float = cal_cycle_phase(ct1)
|
||||
let p2: Float = cal_cycle_phase(ct2)
|
||||
let diff: Float = p2 - p1
|
||||
// 1 year / 100 years = 0.01
|
||||
return format_float(diff, 2)
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println(run_test())
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// cycle-30hr.el — CycleCalendar with a 30-hour period.
|
||||
// Two CalendarTimes 15 hours apart should have cycle_phase differ by 0.5.
|
||||
// We compare phases via float subtraction with format_float for determinism.
|
||||
|
||||
fn run_test() -> String {
|
||||
let period: Duration = 30.hours
|
||||
let cal: Calendar = cycle_calendar(period)
|
||||
let base: Instant = unix_seconds(0)
|
||||
let later: Instant = base + 15.hours
|
||||
let ct1: CalendarTime = in_calendar(base, cal)
|
||||
let ct2: CalendarTime = in_calendar(later, cal)
|
||||
let p1: Float = cal_cycle_phase(ct1)
|
||||
let p2: Float = cal_cycle_phase(ct2)
|
||||
let diff: Float = p2 - p1
|
||||
return format_float(diff, 1)
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println(run_test())
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// dst-spring-forward.el — Earth calendar handles the DST transition.
|
||||
// 2026 spring DST: March 8 at 02:00 EST → clocks jump to 03:00 EDT.
|
||||
// 2026-03-08 06:30 UTC = 01:30 EST (just before the jump).
|
||||
// Add 1 hour → 07:30 UTC = 03:30 EDT (the wall clock skipped 02:30 entirely).
|
||||
|
||||
fn run_test() -> String {
|
||||
let z: Zone = zone("America/New_York")
|
||||
let cal: Calendar = earth_calendar(z)
|
||||
let i: Instant = unix_seconds(1772951400)
|
||||
let later: Instant = i + 1.hour
|
||||
let ct: CalendarTime = in_calendar(later, cal)
|
||||
return cal_format(ct, "HH:mm z")
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println(run_test())
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// earth-zone.el — EarthCalendar(zone) formats with the right zone abbreviation.
|
||||
// We use a fixed Instant on July 4, 2026 (definitely EDT in NYC) so the
|
||||
// abbreviation is deterministic across runs.
|
||||
|
||||
fn run_test() -> String {
|
||||
let z: Zone = zone("America/New_York")
|
||||
let cal: Calendar = earth_calendar(z)
|
||||
// 2026-07-04 12:00:00 UTC = 2026-07-04 08:00:00 EDT
|
||||
let i: Instant = unix_seconds(1782216000)
|
||||
let ct: CalendarTime = in_calendar(i, cal)
|
||||
return cal_format(ct, "z")
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println(run_test())
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// local-date-arithmetic.el — LocalDate + Duration produces a LocalDate.
|
||||
// 2026-05-28 + 7 days crosses the May/June boundary, yielding 2026-06-04.
|
||||
|
||||
fn run_test() -> String {
|
||||
let d1: LocalDate = local_date(2026, 5, 28)
|
||||
let week: Duration = 7.days
|
||||
let d2: LocalDate = d1 + week
|
||||
let y: Int = local_date_year(d2)
|
||||
let m: Int = local_date_month(d2)
|
||||
let day: Int = local_date_day(d2)
|
||||
return int_to_str(y) + "-" + int_to_str(m) + "-" + int_to_str(day)
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println(run_test())
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// mars-calendar.el — MarsCalendar uses sol period 88775.244 seconds.
|
||||
// Two instants exactly one sol apart should differ by 1 in sol number.
|
||||
|
||||
fn run_test() -> Int {
|
||||
let cal: Calendar = mars_calendar()
|
||||
let base: Instant = unix_seconds(0)
|
||||
// 88775.244 s * 1e9 nanos = 88775244000000 nanos
|
||||
let one_sol_ns: Int = 88775244000000
|
||||
let one_sol: Duration = el_duration_from_nanos(one_sol_ns)
|
||||
let later: Instant = base + one_sol
|
||||
let ct1: CalendarTime = in_calendar(base, cal)
|
||||
let ct2: CalendarTime = in_calendar(later, cal)
|
||||
let sol1: String = cal_format(ct1, "%sol")
|
||||
let sol2: String = cal_format(ct2, "%sol")
|
||||
let diff: Int = str_to_int(sol2) - str_to_int(sol1)
|
||||
return diff
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println(int_to_str(run_test()))
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// no-cycle.el — NoCycleCalendar's cycle_phase returns NaN sentinel.
|
||||
// Detection via float_to_str — NaN renders as "nan" under %g.
|
||||
|
||||
fn run_test() -> Int {
|
||||
let cal: Calendar = no_cycle_calendar()
|
||||
let i: Instant = unix_seconds(1000000000)
|
||||
let ct: CalendarTime = in_calendar(i, cal)
|
||||
let p: Float = cal_cycle_phase(ct)
|
||||
let s: String = float_to_str(p)
|
||||
if str_eq(s, "nan") {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println(int_to_str(run_test()))
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// rhythm-cycle-30hr.el — rhythm_cycle_phase(0.5) under CycleCalendar(30.hours)
|
||||
// returns the next instant at the 15-hour mark of the cycle.
|
||||
|
||||
fn run_test() -> Int {
|
||||
let period: Duration = 30.hours
|
||||
let cal: Calendar = cycle_calendar(period)
|
||||
let r: Rhythm = rhythm_cycle_phase(0.5)
|
||||
let base: Instant = unix_seconds(0)
|
||||
let next: Instant = rhythm_next_after(r, base, cal)
|
||||
let elapsed_ns: Duration = next - base
|
||||
let elapsed_secs: Int = duration_to_seconds(elapsed_ns)
|
||||
// 15 hours = 54000 seconds.
|
||||
return elapsed_secs
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println(int_to_str(run_test()))
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// rhythm-grounding.el — Mondays at 9am, grounded against EarthCalendar(NYC),
|
||||
// from a Wednesday timestamp returns the next Monday at 9am EDT.
|
||||
// Wednesday 2026-05-06 00:00 UTC (1778025600) → next Monday 9am EDT
|
||||
// = 2026-05-11 09:00 EDT = 2026-05-11 13:00 UTC = 1778504400.
|
||||
|
||||
fn run_test() -> Int {
|
||||
let z: Zone = zone("America/New_York")
|
||||
let cal: Calendar = earth_calendar(z)
|
||||
let r: Rhythm = rhythm_weekly_at(1, 9, 0)
|
||||
let after: Instant = unix_seconds(1778025600)
|
||||
let next: Instant = rhythm_next_after(r, after, cal)
|
||||
let next_secs: Int = instant_to_unix_seconds(next)
|
||||
return next_secs
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println(int_to_str(run_test()))
|
||||
}
|
||||
Executable
+107
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env bash
|
||||
# run.sh — build and execute the calendar/ acceptance corpus.
|
||||
#
|
||||
# Each examples/<case>.el is a self-contained El program with a fn main()
|
||||
# that prints a single deterministic result line. The runner compiles each
|
||||
# via the canonical native elc, links it against the shared C runtime, runs
|
||||
# it, and asserts the output matches the expected value.
|
||||
|
||||
set -uo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
EL_HOME="${EL_HOME:-$(cd ../.. && pwd)}"
|
||||
ELC="${EL_HOME}/dist/platform/elc"
|
||||
RUNTIME_DIR="${EL_HOME}/el-compiler/runtime"
|
||||
|
||||
if [ ! -x "${ELC}" ]; then
|
||||
echo "elc not found at ${ELC}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
FAILED_NAMES=()
|
||||
|
||||
# run_runtime_case <name> <source> <expected> [<extra>]
|
||||
run_runtime_case() {
|
||||
local name="$1"
|
||||
local src="$2"
|
||||
local expected="$3"
|
||||
local mode="${4:-exact}"
|
||||
|
||||
local out_c
|
||||
local out_bin
|
||||
out_c="$(mktemp -t cal_test.XXXXXX).c"
|
||||
out_bin="$(mktemp -t cal_test.XXXXXX)"
|
||||
|
||||
if ! "${ELC}" "${src}" > "${out_c}" 2>/tmp/cal_test.elc.err; then
|
||||
echo "FAIL ${name} — elc emit failed:"
|
||||
cat /tmp/cal_test.elc.err | sed 's/^/ /'
|
||||
FAIL=$((FAIL+1))
|
||||
FAILED_NAMES+=("${name}")
|
||||
rm -f "${out_c}" "${out_bin}"
|
||||
return
|
||||
fi
|
||||
|
||||
if ! cc -O2 -I "${RUNTIME_DIR}" "${out_c}" "${RUNTIME_DIR}/el_runtime.c" \
|
||||
-lcurl -lpthread -o "${out_bin}" 2>/tmp/cal_test.cc.err; then
|
||||
echo "FAIL ${name} — cc failed:"
|
||||
cat /tmp/cal_test.cc.err | sed 's/^/ /'
|
||||
FAIL=$((FAIL+1))
|
||||
FAILED_NAMES+=("${name}")
|
||||
rm -f "${out_c}" "${out_bin}"
|
||||
return
|
||||
fi
|
||||
|
||||
local got
|
||||
got="$("${out_bin}" 2>&1 | tr -d '[:space:]')"
|
||||
|
||||
if [ "${mode}" = "either" ]; then
|
||||
local ok=0
|
||||
local IFS=','
|
||||
for choice in ${expected}; do
|
||||
if [ "${got}" = "${choice}" ]; then ok=1; break; fi
|
||||
done
|
||||
if [ "${ok}" = "1" ]; then
|
||||
echo "PASS ${name} (got: ${got})"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo "FAIL ${name} expected one of {${expected}}, got: ${got}"
|
||||
FAIL=$((FAIL+1))
|
||||
FAILED_NAMES+=("${name}")
|
||||
fi
|
||||
else
|
||||
if [ "${got}" = "${expected}" ]; then
|
||||
echo "PASS ${name}"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo "FAIL ${name} expected: ${expected}, got: ${got}"
|
||||
FAIL=$((FAIL+1))
|
||||
FAILED_NAMES+=("${name}")
|
||||
fi
|
||||
fi
|
||||
|
||||
rm -f "${out_c}" "${out_bin}"
|
||||
}
|
||||
|
||||
echo "==> Running calendar/ acceptance corpus"
|
||||
echo
|
||||
|
||||
run_runtime_case "earth-zone" examples/earth-zone.el "EDT"
|
||||
run_runtime_case "dst-spring-forward" examples/dst-spring-forward.el "03:30EDT"
|
||||
run_runtime_case "mars-calendar" examples/mars-calendar.el "1"
|
||||
run_runtime_case "cycle-30hr" examples/cycle-30hr.el "0.5"
|
||||
run_runtime_case "cycle-300yr" examples/cycle-300yr.el "0.01"
|
||||
run_runtime_case "no-cycle" examples/no-cycle.el "1"
|
||||
run_runtime_case "cross-calendar-comparison" examples/cross-calendar-comparison.el "1"
|
||||
run_runtime_case "local-date-arithmetic" examples/local-date-arithmetic.el "2026-6-4"
|
||||
run_runtime_case "rhythm-grounding" examples/rhythm-grounding.el "1778504400"
|
||||
run_runtime_case "rhythm-cycle-30hr" examples/rhythm-cycle-30hr.el "54000"
|
||||
|
||||
echo
|
||||
echo "${PASS} passed, ${FAIL} failed"
|
||||
if [ "${FAIL}" -gt 0 ]; then
|
||||
echo "failed: ${FAILED_NAMES[*]}"
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
@@ -0,0 +1,13 @@
|
||||
// runner.el — entry point for the calendar/ acceptance corpus.
|
||||
//
|
||||
// Each calendar/examples/*.el is its own El program with its own fn main().
|
||||
// Compile, link, and run-output-diff is handled by tests/calendar/run.sh —
|
||||
// this file is the El-side stub kept for pattern parity.
|
||||
//
|
||||
// Run from the calendar/ directory:
|
||||
// ./run.sh
|
||||
|
||||
fn main() -> Void {
|
||||
println("calendar/ acceptance corpus is driven by run.sh — invoke that directly.")
|
||||
println("Each examples/*.el is a self-contained program; runner.el is a parity stub.")
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
{
|
||||
"allowlist": "{\"p\":[],\"br\":[],\"strong\":[],\"em\":[],\"u\":[],\"s\":[],\"code\":[],\"pre\":[],\"ul\":[],\"ol\":[],\"li\":[],\"h1\":[],\"h2\":[],\"h3\":[],\"h4\":[],\"blockquote\":[],\"a\":[\"href\",\"title\"]}",
|
||||
"cases": [
|
||||
{
|
||||
"name": "01-pass-through-list",
|
||||
"input": "<ol><li><strong>x</strong></li></ol>",
|
||||
"expected": "<ol><li><strong>x</strong></li></ol>"
|
||||
},
|
||||
{
|
||||
"name": "02-paragraphs-with-bold",
|
||||
"input": "<p>hello <strong>world</strong></p><p>second</p>",
|
||||
"expected": "<p>hello <strong>world</strong></p><p>second</p>"
|
||||
},
|
||||
{
|
||||
"name": "03-pre-code-block",
|
||||
"input": "<pre><code>npm install</code></pre>",
|
||||
"expected": "<pre><code>npm install</code></pre>"
|
||||
},
|
||||
{
|
||||
"name": "04-allowed-https-link",
|
||||
"input": "<a href=\"https://example.com\">click</a>",
|
||||
"expected": "<a href=\"https://example.com\">click</a>"
|
||||
},
|
||||
{
|
||||
"name": "05-allowed-anchor-link",
|
||||
"input": "<a href=\"#section\">jump</a>",
|
||||
"expected": "<a href=\"#section\">jump</a>"
|
||||
},
|
||||
{
|
||||
"name": "06-javascript-scheme-blocked",
|
||||
"input": "<a href=\"javascript:alert(1)\">click</a>",
|
||||
"expected": "<a>click</a>"
|
||||
},
|
||||
{
|
||||
"name": "07-about-scheme-blocked",
|
||||
"input": "<a href=\"about:blank#x\">click</a>",
|
||||
"expected": "<a>click</a>"
|
||||
},
|
||||
{
|
||||
"name": "08-data-uri-blocked",
|
||||
"input": "<a href=\"data:text/html,<script>alert(1)</script>\">x</a>",
|
||||
"expected": "<a>x</a>"
|
||||
},
|
||||
{
|
||||
"name": "09-script-content-dropped",
|
||||
"input": "before<script>alert(1)</script>after",
|
||||
"expected": "beforeafter"
|
||||
},
|
||||
{
|
||||
"name": "10-iframe-content-dropped",
|
||||
"input": "<iframe src=\"evil\">junk</iframe>safe",
|
||||
"expected": "safe"
|
||||
},
|
||||
{
|
||||
"name": "11-form-content-dropped",
|
||||
"input": "<form action=\"/steal\">x</form>safe",
|
||||
"expected": "safe"
|
||||
},
|
||||
{
|
||||
"name": "12-img-with-onerror-dropped",
|
||||
"input": "<img src=x onerror=alert(2)>",
|
||||
"expected": ""
|
||||
},
|
||||
{
|
||||
"name": "13-comment-injection-bypass-blocked",
|
||||
"input": "<form action=\"-->junk\">x</form>safe",
|
||||
"expected": "safe"
|
||||
},
|
||||
{
|
||||
"name": "14-mixed-legit-and-attack",
|
||||
"input": "<p>hello</p><script>alert(1)</script><p>world</p>",
|
||||
"expected": "<p>hello</p><p>world</p>"
|
||||
},
|
||||
{
|
||||
"name": "15-pre-encoded-entities-preserved",
|
||||
"input": "<script>alert(1)</script>",
|
||||
"expected": "<script>alert(1)</script>"
|
||||
},
|
||||
{
|
||||
"name": "16-unicode-in-href-preserved",
|
||||
"input": "<a href=\"https://example.com/?q=日本語\">x</a>",
|
||||
"expected": "<a href=\"https://example.com/?q=日本語\">x</a>"
|
||||
},
|
||||
{
|
||||
"name": "17-unclosed-tag-passes-through",
|
||||
"input": "<p>unclosed",
|
||||
"expected": "<p>unclosed"
|
||||
},
|
||||
{
|
||||
"name": "18-onclick-attribute-stripped-tag-survives",
|
||||
"input": "<p onclick=\"x\">hi</p>",
|
||||
"expected": "<p>hi</p>"
|
||||
},
|
||||
{
|
||||
"name": "19-tab-bypass-in-scheme-blocked",
|
||||
"input": "<a href=\"java\tscript:alert(1)\">x</a>",
|
||||
"expected": "<a>x</a>"
|
||||
},
|
||||
{
|
||||
"name": "20-uppercase-tag-and-attr-normalised",
|
||||
"input": "<A HREF=\"https://example.com\">x</A>",
|
||||
"expected": "<a href=\"https://example.com\">x</a>"
|
||||
},
|
||||
{
|
||||
"name": "21-style-content-dropped",
|
||||
"input": "<style>body{display:none}</style>visible",
|
||||
"expected": "visible"
|
||||
},
|
||||
{
|
||||
"name": "22-object-content-dropped",
|
||||
"input": "<object data=\"x.swf\">flash</object>safe",
|
||||
"expected": "safe"
|
||||
},
|
||||
{
|
||||
"name": "23-svg-onload-dropped",
|
||||
"input": "<svg onload=\"alert(1)\"><circle r=\"5\"/></svg>safe",
|
||||
"expected": "safe"
|
||||
},
|
||||
{
|
||||
"name": "24-blockquote-passthrough",
|
||||
"input": "<blockquote>quoted text</blockquote>",
|
||||
"expected": "<blockquote>quoted text</blockquote>"
|
||||
},
|
||||
{
|
||||
"name": "25-headings-passthrough",
|
||||
"input": "<h1>title</h1><h2>section</h2>",
|
||||
"expected": "<h1>title</h1><h2>section</h2>"
|
||||
},
|
||||
{
|
||||
"name": "26-attribute-value-with-gt-byte",
|
||||
"input": "<a href=\"https://example.com/?q=1>2\" title=\"a > b\">x</a>",
|
||||
"expected": "<a href=\"https://example.com/?q=1>2\" title=\"a > b\">x</a>"
|
||||
},
|
||||
{
|
||||
"name": "27-nested-script-after-text",
|
||||
"input": "lead<p>para</p><script>alert(1)</script>tail",
|
||||
"expected": "lead<p>para</p>tail"
|
||||
},
|
||||
{
|
||||
"name": "28-empty-input",
|
||||
"input": "",
|
||||
"expected": ""
|
||||
},
|
||||
{
|
||||
"name": "29-plain-text",
|
||||
"input": "just text, no tags",
|
||||
"expected": "just text, no tags"
|
||||
}
|
||||
]
|
||||
}
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
# run.sh — build and execute the html_sanitizer acceptance corpus.
|
||||
#
|
||||
# Compiles tests/html_sanitizer/runner.el via the canonical native elc,
|
||||
# links against the shared C runtime, then runs the binary against
|
||||
# cases.json. Exits non-zero on any FAIL.
|
||||
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
EL_HOME="${EL_HOME:-$(cd ../.. && pwd)}"
|
||||
ELC="${EL_HOME}/dist/platform/elc"
|
||||
RUNTIME_DIR="${EL_HOME}/el-compiler/runtime"
|
||||
|
||||
if [ ! -x "${ELC}" ]; then
|
||||
echo "elc not found at ${ELC}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
OUT_C="$(mktemp -t html_sanitizer.XXXXXX).c"
|
||||
OUT_BIN="$(mktemp -t html_sanitizer.XXXXXX)"
|
||||
trap 'rm -f "${OUT_C}" "${OUT_BIN}"' EXIT
|
||||
|
||||
echo "==> Compiling runner.el via ${ELC}"
|
||||
"${ELC}" runner.el > "${OUT_C}"
|
||||
|
||||
echo "==> Linking against ${RUNTIME_DIR}/el_runtime.c"
|
||||
cc -O2 -I "${RUNTIME_DIR}" "${OUT_C}" "${RUNTIME_DIR}/el_runtime.c" \
|
||||
-lcurl -lpthread -o "${OUT_BIN}"
|
||||
|
||||
echo "==> Running"
|
||||
"${OUT_BIN}"
|
||||
@@ -0,0 +1,59 @@
|
||||
// runner.el — acceptance tests for el_html_sanitize.
|
||||
//
|
||||
// Reads cases.json from the cwd, runs every case through
|
||||
// el_html_sanitize, prints PASS/FAIL per case and a summary,
|
||||
// and exits non-zero if any case failed.
|
||||
//
|
||||
// cases.json shape:
|
||||
// {
|
||||
// "allowlist": "<json string passed to el_html_sanitize>",
|
||||
// "cases": [
|
||||
// { "name": "...", "input": "...", "expected": "..." },
|
||||
// ...
|
||||
// ]
|
||||
// }
|
||||
|
||||
fn run_case(name: String, input: String, expected: String, allowlist: String, idx: Int) -> Int {
|
||||
let got: String = el_html_sanitize(input, allowlist)
|
||||
if str_eq(got, expected) {
|
||||
println("PASS " + name)
|
||||
return 1
|
||||
} else {
|
||||
println("FAIL " + name)
|
||||
println(" input: " + input)
|
||||
println(" expected: " + expected)
|
||||
println(" got: " + got)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Int {
|
||||
let raw: String = fs_read("cases.json")
|
||||
if str_eq(raw, "") {
|
||||
println("FATAL could not read cases.json from cwd")
|
||||
return 2
|
||||
}
|
||||
let allowlist: String = json_get(raw, "allowlist")
|
||||
let cases_raw: String = json_get_raw(raw, "cases")
|
||||
let n: Int = json_array_len(cases_raw)
|
||||
let i: Int = 0
|
||||
let pass: Int = 0
|
||||
let fail: Int = 0
|
||||
while i < n {
|
||||
let case_obj: String = json_array_get(cases_raw, i)
|
||||
let cname: String = json_get(case_obj, "name")
|
||||
let cinp: String = json_get(case_obj, "input")
|
||||
let cexp: String = json_get(case_obj, "expected")
|
||||
let r: Int = run_case(cname, cinp, cexp, allowlist, i)
|
||||
if r == 1 {
|
||||
let pass = pass + 1
|
||||
} else {
|
||||
let fail = fail + 1
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
println("")
|
||||
println(int_to_str(pass) + " passed, " + int_to_str(fail) + " failed")
|
||||
if fail > 0 { return 1 }
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// test_codegen_js.el - basic tests for JS codegen features.
|
||||
//
|
||||
// These tests verify that core El language features produce correct values
|
||||
// when compiled and executed via the C backend. They serve as a
|
||||
// regression baseline for the codegen pipeline.
|
||||
|
||||
test "arithmetic" {
|
||||
let x: Int = 2 + 3
|
||||
assert x == 5, "addition"
|
||||
let y: Int = 10 - 4
|
||||
assert y == 6, "subtraction"
|
||||
let z: Int = 3 * 4
|
||||
assert z == 12, "multiplication"
|
||||
let w: Int = 15 / 3
|
||||
assert w == 5, "division"
|
||||
}
|
||||
|
||||
test "string-concat" {
|
||||
let a: String = "hello"
|
||||
let b: String = " world"
|
||||
let c: String = a + b
|
||||
assert c == "hello world", "string concatenation"
|
||||
}
|
||||
|
||||
test "str-len" {
|
||||
let n: Int = str_len("hello")
|
||||
assert n == 5, "str_len hello"
|
||||
let m: Int = str_len("")
|
||||
assert m == 0, "str_len empty"
|
||||
}
|
||||
|
||||
test "bool-logic" {
|
||||
let t = true
|
||||
let f = false
|
||||
assert t, "true is truthy"
|
||||
assert !f, "false negated is truthy"
|
||||
assert t && !f, "true and not false"
|
||||
assert t || f, "true or false"
|
||||
}
|
||||
|
||||
test "list-operations" {
|
||||
let lst: [Int] = native_list_empty()
|
||||
let lst = native_list_append(lst, 10)
|
||||
let lst = native_list_append(lst, 20)
|
||||
let lst = native_list_append(lst, 30)
|
||||
let n: Int = native_list_len(lst)
|
||||
assert n == 3, "list length 3"
|
||||
let v0: Int = native_list_get(lst, 0)
|
||||
let v1: Int = native_list_get(lst, 1)
|
||||
let v2: Int = native_list_get(lst, 2)
|
||||
assert v0 == 10, "first element"
|
||||
assert v1 == 20, "second element"
|
||||
assert v2 == 30, "third element"
|
||||
}
|
||||
|
||||
test "str-slice" {
|
||||
let s: String = str_slice("hello world", 6, 11)
|
||||
assert s == "world", "slice from 6 to 11"
|
||||
}
|
||||
|
||||
test "str-contains" {
|
||||
assert str_contains("hello world", "world"), "contains world"
|
||||
assert !str_contains("hello world", "xyz"), "does not contain xyz"
|
||||
}
|
||||
|
||||
test "int-to-str" {
|
||||
let s: String = int_to_str(42)
|
||||
assert s == "42", "int to string"
|
||||
}
|
||||
|
||||
test "str-to-int" {
|
||||
let n: Int = str_to_int("123")
|
||||
assert n == 123, "string to int"
|
||||
}
|
||||
|
||||
test "str-starts-ends" {
|
||||
assert str_starts_with("hello world", "hello"), "starts with hello"
|
||||
assert str_ends_with("hello world", "world"), "ends with world"
|
||||
assert !str_starts_with("hello world", "world"), "does not start with world"
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// test_env.el - native test suite for runtime/env.el
|
||||
//
|
||||
// Covers: env() for reading environment variables, args() returning a list,
|
||||
// state_set/get/del/keys via the env module re-exports, uuid_new/uuid_v4
|
||||
// format validation.
|
||||
|
||||
test "env-missing-returns-empty" {
|
||||
let v: String = env("__EL_NO_SUCH_VAR_XYZ__")
|
||||
assert v == "", "missing env var returns empty string"
|
||||
}
|
||||
|
||||
test "env-path-is-set" {
|
||||
// PATH is expected to be set in virtually any UNIX environment.
|
||||
let v: String = env("PATH")
|
||||
assert str_len(v) > 0, "PATH env var is non-empty"
|
||||
}
|
||||
|
||||
test "env-home-is-set" {
|
||||
// HOME is present on macOS/Linux test environments.
|
||||
let v: String = env("HOME")
|
||||
assert str_len(v) > 0, "HOME env var is non-empty"
|
||||
}
|
||||
|
||||
test "args-returns-list" {
|
||||
let a: [String] = args()
|
||||
// Even with no arguments, the list should be non-null (at minimum the
|
||||
// program name is argv[0]).
|
||||
let n: Int = native_list_len(a)
|
||||
assert n >= 0, "args returns a list (may be empty if runtime strips argv)"
|
||||
}
|
||||
|
||||
test "uuid-new-format" {
|
||||
let id: String = uuid_new()
|
||||
// UUID v4: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx (36 chars)
|
||||
let n: Int = str_len(id)
|
||||
assert n == 36, "uuid_new returns 36-character string"
|
||||
// Check the dashes at correct positions
|
||||
let d1: String = str_char_at(id, 8)
|
||||
let d2: String = str_char_at(id, 13)
|
||||
let d3: String = str_char_at(id, 18)
|
||||
let d4: String = str_char_at(id, 23)
|
||||
assert d1 == "-", "dash at position 8"
|
||||
assert d2 == "-", "dash at position 13"
|
||||
assert d3 == "-", "dash at position 18"
|
||||
assert d4 == "-", "dash at position 23"
|
||||
}
|
||||
|
||||
test "uuid-v4-format" {
|
||||
let id: String = uuid_v4()
|
||||
let n: Int = str_len(id)
|
||||
assert n == 36, "uuid_v4 returns 36-character string"
|
||||
// The version nibble must be '4'
|
||||
let version_char: String = str_char_at(id, 14)
|
||||
assert version_char == "4", "uuid_v4 version nibble is '4'"
|
||||
}
|
||||
|
||||
test "uuid-uniqueness" {
|
||||
let id1: String = uuid_new()
|
||||
let id2: String = uuid_new()
|
||||
assert !str_eq(id1, id2), "two uuid_new calls produce different UUIDs"
|
||||
}
|
||||
|
||||
test "env-state-set-get-via-env-module" {
|
||||
// runtime/env.el re-exports state_set / state_get.
|
||||
state_set("env_test_key", "env_test_val")
|
||||
let v: String = state_get("env_test_key")
|
||||
assert v == "env_test_val", "state_set/get work via env module"
|
||||
state_del("env_test_key")
|
||||
let after: String = state_get("env_test_key")
|
||||
assert after == "", "state_del removes key"
|
||||
}
|
||||
|
||||
test "env-state-keys-json-via-env-module" {
|
||||
state_set("esk_a", "1")
|
||||
state_set("esk_b", "2")
|
||||
let ks: String = state_keys()
|
||||
assert str_starts_with(ks, "["), "state_keys returns JSON array"
|
||||
assert str_contains(ks, "esk_a"), "esk_a in keys"
|
||||
assert str_contains(ks, "esk_b"), "esk_b in keys"
|
||||
state_del("esk_a")
|
||||
state_del("esk_b")
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// test_fs.el - native test suite for runtime/fs.el
|
||||
//
|
||||
// Covers: fs_write/read round-trip, fs_exists, fs_mkdir, fs_list,
|
||||
// fs_list_json, and edge cases (empty file, overwrite, non-existent path).
|
||||
|
||||
test "fs-write-and-read" {
|
||||
let path: String = "/tmp/el_test_fs_basic.txt"
|
||||
let content: String = "hello from El"
|
||||
let ok: Bool = fs_write(path, content)
|
||||
assert ok, "fs_write returns true on success"
|
||||
let got: String = fs_read(path)
|
||||
assert got == content, "fs_read returns what was written"
|
||||
}
|
||||
|
||||
test "fs-exists" {
|
||||
let path: String = "/tmp/el_test_fs_exists.txt"
|
||||
fs_write(path, "exists check")
|
||||
let exists: Bool = fs_exists(path)
|
||||
assert exists, "fs_exists returns true for created file"
|
||||
let missing: Bool = fs_exists("/tmp/__el_no_such_file_xyz__.txt")
|
||||
assert !missing, "fs_exists returns false for non-existent file"
|
||||
}
|
||||
|
||||
test "fs-read-nonexistent" {
|
||||
let got: String = fs_read("/tmp/__el_no_such_file_abc__.txt")
|
||||
assert got == "", "reading nonexistent file returns empty string"
|
||||
}
|
||||
|
||||
test "fs-write-overwrite" {
|
||||
let path: String = "/tmp/el_test_fs_overwrite.txt"
|
||||
fs_write(path, "original content")
|
||||
fs_write(path, "new content")
|
||||
let got: String = fs_read(path)
|
||||
assert got == "new content", "second write overwrites first"
|
||||
}
|
||||
|
||||
test "fs-write-empty-file" {
|
||||
let path: String = "/tmp/el_test_fs_empty.txt"
|
||||
let ok: Bool = fs_write(path, "")
|
||||
assert ok, "writing empty file succeeds"
|
||||
let got: String = fs_read(path)
|
||||
assert got == "", "reading empty file returns empty string"
|
||||
let exists: Bool = fs_exists(path)
|
||||
assert exists, "empty file still exists"
|
||||
}
|
||||
|
||||
test "fs-write-multiline" {
|
||||
let path: String = "/tmp/el_test_fs_multiline.txt"
|
||||
let content: String = "line one\nline two\nline three"
|
||||
fs_write(path, content)
|
||||
let got: String = fs_read(path)
|
||||
assert got == content, "multiline content round-trips"
|
||||
let lines: [String] = str_split_lines(got)
|
||||
let n: Int = native_list_len(lines)
|
||||
assert n == 3, "three lines after read"
|
||||
}
|
||||
|
||||
test "fs-mkdir" {
|
||||
let dir: String = "/tmp/el_test_fs_mkdir_dir"
|
||||
let ok: Bool = fs_mkdir(dir)
|
||||
assert ok, "fs_mkdir returns true"
|
||||
let exists: Bool = fs_exists(dir)
|
||||
assert exists, "created directory exists"
|
||||
}
|
||||
|
||||
test "fs-list" {
|
||||
let dir: String = "/tmp/el_test_fs_list_dir"
|
||||
fs_mkdir(dir)
|
||||
fs_write(dir + "/a.txt", "a")
|
||||
fs_write(dir + "/b.txt", "b")
|
||||
let files: [String] = fs_list(dir)
|
||||
// Filter out empty strings from trailing newline
|
||||
let n: Int = native_list_len(files)
|
||||
// We expect at least 2 entries (a.txt and b.txt)
|
||||
assert n >= 2, "fs_list returns at least 2 entries"
|
||||
}
|
||||
|
||||
test "fs-list-json" {
|
||||
let dir: String = "/tmp/el_test_fs_listjson_dir"
|
||||
fs_mkdir(dir)
|
||||
fs_write(dir + "/x.txt", "x")
|
||||
fs_write(dir + "/y.txt", "y")
|
||||
let result: String = fs_list_json(dir)
|
||||
assert str_starts_with(result, "["), "fs_list_json returns JSON array"
|
||||
assert str_ends_with(result, "]"), "fs_list_json JSON array is closed"
|
||||
// Should contain at least one filename
|
||||
assert str_len(result) > 2, "fs_list_json result is non-empty array"
|
||||
}
|
||||
|
||||
test "fs-write-and-read-special-chars" {
|
||||
let path: String = "/tmp/el_test_fs_special.txt"
|
||||
let content: String = "tabs:\there\nnewlines: ok\n\"quoted\""
|
||||
fs_write(path, content)
|
||||
let got: String = fs_read(path)
|
||||
assert got == content, "special chars in content round-trip"
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
// test_json.el - native test suite for runtime/json.el
|
||||
//
|
||||
// Covers: json_get (dot-path), typed extractors (int, bool, float),
|
||||
// json_array_len/get, json_set, json_build_object, json_build_array,
|
||||
// and json_escape_string.
|
||||
|
||||
test "json-get-string" {
|
||||
let obj: String = "{\"name\":\"alice\",\"role\":\"admin\"}"
|
||||
let name: String = json_get(obj, "name")
|
||||
assert name == "alice", "get string field name"
|
||||
let role: String = json_get(obj, "role")
|
||||
assert role == "admin", "get string field role"
|
||||
let missing: String = json_get(obj, "xyz")
|
||||
assert missing == "", "missing key returns empty string"
|
||||
}
|
||||
|
||||
test "json-get-int" {
|
||||
let obj: String = "{\"count\":42,\"offset\":0}"
|
||||
let count: Int = json_get_int(obj, "count")
|
||||
assert count == 42, "get int field count"
|
||||
let offset: Int = json_get_int(obj, "offset")
|
||||
assert offset == 0, "get int field offset = 0"
|
||||
}
|
||||
|
||||
test "json-get-bool" {
|
||||
let obj: String = "{\"active\":true,\"deleted\":false}"
|
||||
let active: Bool = json_get_bool(obj, "active")
|
||||
assert active, "get bool true"
|
||||
let deleted: Bool = json_get_bool(obj, "deleted")
|
||||
assert !deleted, "get bool false"
|
||||
}
|
||||
|
||||
test "json-dot-path" {
|
||||
let obj: String = "{\"user\":{\"name\":\"bob\",\"age\":30}}"
|
||||
let name: String = json_get(obj, "user.name")
|
||||
assert name == "bob", "dot-path traversal user.name"
|
||||
let age: String = json_get(obj, "user.age")
|
||||
assert age == "30", "dot-path traversal user.age"
|
||||
}
|
||||
|
||||
test "json-array-len" {
|
||||
let arr: String = "[\"a\",\"b\",\"c\"]"
|
||||
let n: Int = json_array_len(arr)
|
||||
assert n == 3, "array length 3"
|
||||
let empty_arr: String = "[]"
|
||||
let m: Int = json_array_len(empty_arr)
|
||||
assert m == 0, "empty array length 0"
|
||||
}
|
||||
|
||||
test "json-array-get" {
|
||||
let arr: String = "[\"foo\",\"bar\",\"baz\"]"
|
||||
let first: String = json_array_get_string(arr, 0)
|
||||
assert first == "foo", "first element"
|
||||
let second: String = json_array_get_string(arr, 1)
|
||||
assert second == "bar", "second element"
|
||||
let third: String = json_array_get_string(arr, 2)
|
||||
assert third == "baz", "third element"
|
||||
}
|
||||
|
||||
test "json-get-raw" {
|
||||
let obj: String = "{\"items\":[1,2,3],\"meta\":{\"page\":1}}"
|
||||
let raw_arr: String = json_get_raw(obj, "items")
|
||||
let arr_len: Int = json_array_len(raw_arr)
|
||||
assert arr_len == 3, "raw array has 3 elements"
|
||||
let raw_meta: String = json_get_raw(obj, "meta")
|
||||
let page: String = json_get(raw_meta, "page")
|
||||
assert page == "1", "page from nested raw object"
|
||||
}
|
||||
|
||||
test "json-set" {
|
||||
let obj: String = "{\"name\":\"alice\"}"
|
||||
let updated: String = json_set(obj, "name", "\"bob\"")
|
||||
let name: String = json_get(updated, "name")
|
||||
assert name == "bob", "set replaces existing key"
|
||||
}
|
||||
|
||||
test "json-build-object" {
|
||||
let kvs: [String] = native_list_empty()
|
||||
let kvs = native_list_append(kvs, "name")
|
||||
let kvs = native_list_append(kvs, "alice")
|
||||
let kvs = native_list_append(kvs, "role")
|
||||
let kvs = native_list_append(kvs, "admin")
|
||||
let obj: String = json_build_object(kvs)
|
||||
let name: String = json_get(obj, "name")
|
||||
let role: String = json_get(obj, "role")
|
||||
assert name == "alice", "built object name field"
|
||||
assert role == "admin", "built object role field"
|
||||
}
|
||||
|
||||
test "json-build-array" {
|
||||
let items: [String] = native_list_empty()
|
||||
let items = native_list_append(items, "\"alpha\"")
|
||||
let items = native_list_append(items, "\"beta\"")
|
||||
let items = native_list_append(items, "\"gamma\"")
|
||||
let arr: String = json_build_array(items)
|
||||
let n: Int = json_array_len(arr)
|
||||
assert n == 3, "built array has 3 elements"
|
||||
let first: String = json_array_get_string(arr, 0)
|
||||
assert first == "alpha", "first built element"
|
||||
let third: String = json_array_get_string(arr, 2)
|
||||
assert third == "gamma", "third built element"
|
||||
}
|
||||
|
||||
test "json-escape-string" {
|
||||
let escaped: String = json_escape_string("say \"hello\"")
|
||||
assert str_contains(escaped, "\\\""), "double quotes are escaped"
|
||||
let tab_esc: String = json_escape_string("a\tb")
|
||||
assert str_contains(tab_esc, "\\t"), "tabs are escaped"
|
||||
let plain: String = json_escape_string("no special chars")
|
||||
assert plain == "no special chars", "plain string unchanged"
|
||||
}
|
||||
|
||||
test "json-get-float" {
|
||||
let obj: String = "{\"price\":9.99,\"tax\":0.0}"
|
||||
let price: Float = json_get_float(obj, "price")
|
||||
let diff: Float = price - 9.99
|
||||
assert diff > -0.001, "price close to 9.99 low"
|
||||
assert diff < 0.001, "price close to 9.99 high"
|
||||
let tax: Float = json_get_float(obj, "tax")
|
||||
assert tax == 0.0, "tax is zero"
|
||||
}
|
||||
|
||||
test "json-nested-array-index" {
|
||||
let obj: String = "{\"tags\":[\"go\",\"el\",\"rust\"]}"
|
||||
let tags_raw: String = json_get_raw(obj, "tags")
|
||||
let count: Int = json_array_len(tags_raw)
|
||||
assert count == 3, "tags array has 3 elements"
|
||||
let first_tag: String = json_array_get_string(tags_raw, 0)
|
||||
assert first_tag == "go", "first tag is go"
|
||||
let last_tag: String = json_array_get_string(tags_raw, 2)
|
||||
assert last_tag == "rust", "last tag is rust"
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
// test_math.el - native test suite for runtime/math.el
|
||||
//
|
||||
// Covers: integer math (abs, max, min), float math (sqrt, log, sin, cos, pi),
|
||||
// float conversions, format_float, and decimal_round.
|
||||
|
||||
test "el-abs" {
|
||||
let pos: Int = el_abs(5)
|
||||
assert pos == 5, "abs of positive"
|
||||
let neg: Int = el_abs(-5)
|
||||
assert neg == 5, "abs of negative"
|
||||
let zero: Int = el_abs(0)
|
||||
assert zero == 0, "abs of zero"
|
||||
let large: Int = el_abs(-1000000)
|
||||
assert large == 1000000, "abs of large negative"
|
||||
}
|
||||
|
||||
test "el-max" {
|
||||
let m: Int = el_max(3, 7)
|
||||
assert m == 7, "max of 3 and 7"
|
||||
let m2: Int = el_max(7, 3)
|
||||
assert m2 == 7, "max is commutative"
|
||||
let same: Int = el_max(5, 5)
|
||||
assert same == 5, "max of equal values"
|
||||
let neg: Int = el_max(-3, -7)
|
||||
assert neg == -3, "max of two negatives"
|
||||
}
|
||||
|
||||
test "el-min" {
|
||||
let m: Int = el_min(3, 7)
|
||||
assert m == 3, "min of 3 and 7"
|
||||
let m2: Int = el_min(7, 3)
|
||||
assert m2 == 3, "min is commutative"
|
||||
let same: Int = el_min(5, 5)
|
||||
assert same == 5, "min of equal values"
|
||||
let neg: Int = el_min(-3, -7)
|
||||
assert neg == -7, "min of two negatives"
|
||||
}
|
||||
|
||||
test "math-pi" {
|
||||
let pi: Float = math_pi()
|
||||
// pi ~ 3.14159265358979
|
||||
// Check it's between 3.141 and 3.142
|
||||
let too_low: Float = pi - 3.141
|
||||
let too_high: Float = 3.142 - pi
|
||||
assert too_low > 0.0, "pi > 3.141"
|
||||
assert too_high > 0.0, "pi < 3.142"
|
||||
}
|
||||
|
||||
test "math-sqrt" {
|
||||
let r4: Float = math_sqrt(4.0)
|
||||
let diff4: Float = r4 - 2.0
|
||||
assert diff4 == 0.0, "sqrt(4) == 2.0"
|
||||
let r9: Float = math_sqrt(9.0)
|
||||
let diff9: Float = r9 - 3.0
|
||||
assert diff9 == 0.0, "sqrt(9) == 3.0"
|
||||
let r1: Float = math_sqrt(1.0)
|
||||
let diff1: Float = r1 - 1.0
|
||||
assert diff1 == 0.0, "sqrt(1) == 1.0"
|
||||
let r0: Float = math_sqrt(0.0)
|
||||
assert r0 == 0.0, "sqrt(0) == 0.0"
|
||||
}
|
||||
|
||||
test "math-sin-cos" {
|
||||
// sin(0) == 0, cos(0) == 1
|
||||
let s0: Float = math_sin(0.0)
|
||||
assert s0 == 0.0, "sin(0) == 0.0"
|
||||
let c0: Float = math_cos(0.0)
|
||||
assert c0 == 1.0, "cos(0) == 1.0"
|
||||
// sin(pi/2) ~ 1.0, cos(pi/2) ~ 0.0
|
||||
let half_pi: Float = math_pi() / 2.0
|
||||
let s_half: Float = math_sin(half_pi)
|
||||
// Check within 0.000001 of 1.0
|
||||
let diff_s: Float = s_half - 1.0
|
||||
assert diff_s > -0.000001, "sin(pi/2) close to 1.0 low"
|
||||
assert diff_s < 0.000001, "sin(pi/2) close to 1.0 high"
|
||||
}
|
||||
|
||||
test "int-to-float-and-back" {
|
||||
let f: Float = int_to_float(42)
|
||||
let back: Int = float_to_int(f)
|
||||
assert back == 42, "42 round-trips int->float->int"
|
||||
let neg: Float = int_to_float(-7)
|
||||
let neg_back: Int = float_to_int(neg)
|
||||
assert neg_back == -7, "-7 round-trips"
|
||||
let zero: Float = int_to_float(0)
|
||||
let zero_back: Int = float_to_int(zero)
|
||||
assert zero_back == 0, "0 round-trips"
|
||||
}
|
||||
|
||||
test "float-to-int-truncates" {
|
||||
let t1: Int = float_to_int(3.9)
|
||||
assert t1 == 3, "3.9 truncates to 3"
|
||||
let t2: Int = float_to_int(3.1)
|
||||
assert t2 == 3, "3.1 truncates to 3"
|
||||
let t3: Int = float_to_int(-3.7)
|
||||
assert t3 == -3, "-3.7 truncates toward zero to -3"
|
||||
}
|
||||
|
||||
test "str-to-float-and-back" {
|
||||
let f: Float = str_to_float("3.14")
|
||||
// Check it's between 3.13 and 3.15
|
||||
let lo: Float = f - 3.13
|
||||
let hi: Float = 3.15 - f
|
||||
assert lo > 0.0, "3.14 parsed > 3.13"
|
||||
assert hi > 0.0, "3.14 parsed < 3.15"
|
||||
let zero: Float = str_to_float("0.0")
|
||||
assert zero == 0.0, "parse 0.0"
|
||||
}
|
||||
|
||||
test "format-float" {
|
||||
let s0: String = format_float(3.14159, 2)
|
||||
assert s0 == "3.14", "format to 2 decimals"
|
||||
let s1: String = format_float(1.0, 0)
|
||||
assert s1 == "1", "format to 0 decimals"
|
||||
let s2: String = format_float(0.0, 3)
|
||||
assert s2 == "0.000", "format zero to 3 decimals"
|
||||
let s3: String = format_float(-2.5, 1)
|
||||
assert s3 == "-2.5", "format negative to 1 decimal"
|
||||
}
|
||||
|
||||
test "decimal-round" {
|
||||
let r0: Float = decimal_round(2.5, 0)
|
||||
assert r0 == 3.0, "round 2.5 to 0 places"
|
||||
let r1: Float = decimal_round(2.45, 1)
|
||||
assert r1 == 2.5, "round 2.45 to 1 place"
|
||||
let neg: Float = decimal_round(-2.5, 0)
|
||||
assert neg == -3.0, "round -2.5 to 0 places (half-away-from-zero)"
|
||||
let exact: Float = decimal_round(1.0, 2)
|
||||
assert exact == 1.0, "rounding exact value unchanged"
|
||||
}
|
||||
|
||||
test "math-log" {
|
||||
// log10(100) == 2
|
||||
let l100: Float = math_log(100.0)
|
||||
let diff: Float = l100 - 2.0
|
||||
assert diff > -0.000001, "log10(100) close to 2 low"
|
||||
assert diff < 0.000001, "log10(100) close to 2 high"
|
||||
// log10(1) == 0
|
||||
let l1: Float = math_log(1.0)
|
||||
assert l1 == 0.0, "log10(1) == 0"
|
||||
}
|
||||
|
||||
test "math-ln" {
|
||||
// ln(1) == 0
|
||||
let l1: Float = math_ln(1.0)
|
||||
assert l1 == 0.0, "ln(1) == 0"
|
||||
// ln(e) ~ 1.0 — e ~ 2.71828
|
||||
let e: Float = 2.718281828
|
||||
let le: Float = math_ln(e)
|
||||
let diff: Float = le - 1.0
|
||||
assert diff > -0.000001, "ln(e) close to 1 low"
|
||||
assert diff < 0.000001, "ln(e) close to 1 high"
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// test_state.el - native test suite for runtime/state.el
|
||||
//
|
||||
// Covers: state_set/get/del, state_has, state_get_or, state_keys,
|
||||
// and edge cases such as empty values, overwrite, and multiple keys.
|
||||
|
||||
test "state-set-get-basic" {
|
||||
state_set("test_key", "hello")
|
||||
let v: String = state_get("test_key")
|
||||
assert v == "hello", "get returns set value"
|
||||
}
|
||||
|
||||
test "state-get-missing" {
|
||||
let v: String = state_get("__nonexistent_key_xyz__")
|
||||
assert v == "", "missing key returns empty string"
|
||||
}
|
||||
|
||||
test "state-overwrite" {
|
||||
state_set("ow_key", "first")
|
||||
state_set("ow_key", "second")
|
||||
let v: String = state_get("ow_key")
|
||||
assert v == "second", "second write overwrites first"
|
||||
}
|
||||
|
||||
test "state-del" {
|
||||
state_set("del_key", "to be deleted")
|
||||
state_del("del_key")
|
||||
let v: String = state_get("del_key")
|
||||
assert v == "", "deleted key returns empty string"
|
||||
}
|
||||
|
||||
test "state-del-nonexistent" {
|
||||
// Should not panic or error on deleting a non-existent key.
|
||||
state_del("__never_set_key__")
|
||||
let v: String = state_get("__never_set_key__")
|
||||
assert v == "", "del of nonexistent key is a no-op"
|
||||
}
|
||||
|
||||
test "state-has" {
|
||||
state_set("has_key", "value")
|
||||
assert state_has("has_key"), "has returns true for set key"
|
||||
assert !state_has("__no_has_key__"), "has returns false for absent key"
|
||||
state_del("has_key")
|
||||
assert !state_has("has_key"), "has returns false after del"
|
||||
}
|
||||
|
||||
test "state-get-or" {
|
||||
state_set("gor_key", "actual")
|
||||
let v1: String = state_get_or("gor_key", "default")
|
||||
assert v1 == "actual", "get_or returns value when key set"
|
||||
let v2: String = state_get_or("__absent_gor_key__", "fallback")
|
||||
assert v2 == "fallback", "get_or returns default when key absent"
|
||||
}
|
||||
|
||||
test "state-multiple-keys" {
|
||||
state_set("mk_a", "alpha")
|
||||
state_set("mk_b", "beta")
|
||||
state_set("mk_c", "gamma")
|
||||
let a: String = state_get("mk_a")
|
||||
let b: String = state_get("mk_b")
|
||||
let c: String = state_get("mk_c")
|
||||
assert a == "alpha", "key a correct"
|
||||
assert b == "beta", "key b correct"
|
||||
assert c == "gamma", "key c correct"
|
||||
state_del("mk_a")
|
||||
state_del("mk_b")
|
||||
state_del("mk_c")
|
||||
}
|
||||
|
||||
test "state-keys-returns-json-array" {
|
||||
state_set("keys_test_1", "v1")
|
||||
state_set("keys_test_2", "v2")
|
||||
let ks: String = state_keys()
|
||||
// The result is a JSON array string like ["keys_test_1","keys_test_2",...]
|
||||
assert str_starts_with(ks, "["), "state_keys returns JSON array"
|
||||
assert str_ends_with(ks, "]"), "state_keys JSON array is closed"
|
||||
assert str_contains(ks, "keys_test_1"), "keys array contains keys_test_1"
|
||||
assert str_contains(ks, "keys_test_2"), "keys array contains keys_test_2"
|
||||
state_del("keys_test_1")
|
||||
state_del("keys_test_2")
|
||||
}
|
||||
|
||||
test "state-numeric-value-as-string" {
|
||||
state_set("num_key", "42")
|
||||
let v: String = state_get("num_key")
|
||||
let n: Int = str_to_int(v)
|
||||
assert n == 42, "stored numeric string round-trips to int"
|
||||
state_del("num_key")
|
||||
}
|
||||
|
||||
test "state-long-value" {
|
||||
let long_val: String = str_repeat("abcdefghij", 100)
|
||||
state_set("long_val_key", long_val)
|
||||
let got: String = state_get("long_val_key")
|
||||
assert got == long_val, "long value round-trips correctly"
|
||||
let got_len: Int = str_len(got)
|
||||
assert got_len == 1000, "long value is 1000 bytes"
|
||||
state_del("long_val_key")
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
// test_string.el - native test suite for runtime/string.el
|
||||
//
|
||||
// Covers: type conversions, core primitives, comparison and search,
|
||||
// case conversion, whitespace trimming, replacement, repetition,
|
||||
// reversal, prefix/suffix stripping, padding, counting, character
|
||||
// classification, splitting, and joining.
|
||||
|
||||
test "int-to-str" {
|
||||
let s: String = int_to_str(42)
|
||||
assert s == "42", "int 42 to string"
|
||||
let neg: String = int_to_str(-7)
|
||||
assert neg == "-7", "negative int to string"
|
||||
let zero: String = int_to_str(0)
|
||||
assert zero == "0", "zero to string"
|
||||
}
|
||||
|
||||
test "str-to-int" {
|
||||
let n: Int = str_to_int("123")
|
||||
assert n == 123, "parse 123"
|
||||
let neg: Int = str_to_int("-5")
|
||||
assert neg == -5, "parse negative"
|
||||
let zero: Int = str_to_int("0")
|
||||
assert zero == 0, "parse zero"
|
||||
}
|
||||
|
||||
test "bool-to-str" {
|
||||
let t: String = bool_to_str(true)
|
||||
assert t == "true", "true to string"
|
||||
let f: String = bool_to_str(false)
|
||||
assert f == "false", "false to string"
|
||||
}
|
||||
|
||||
test "str-len" {
|
||||
let n: Int = str_len("hello")
|
||||
assert n == 5, "length of hello"
|
||||
let e: Int = str_len("")
|
||||
assert e == 0, "length of empty string"
|
||||
let space: Int = str_len("a b")
|
||||
assert space == 3, "length with spaces"
|
||||
}
|
||||
|
||||
test "str-eq" {
|
||||
assert str_eq("abc", "abc"), "identical strings are equal"
|
||||
assert !str_eq("abc", "ABC"), "case-sensitive comparison"
|
||||
assert str_eq("", ""), "empty strings are equal"
|
||||
assert !str_eq("a", "b"), "different single chars"
|
||||
}
|
||||
|
||||
test "str-slice" {
|
||||
let s: String = str_slice("hello world", 6, 11)
|
||||
assert s == "world", "slice end of string"
|
||||
let start: String = str_slice("hello world", 0, 5)
|
||||
assert start == "hello", "slice start of string"
|
||||
let empty: String = str_slice("hello", 2, 2)
|
||||
assert empty == "", "zero-length slice"
|
||||
}
|
||||
|
||||
test "str-starts-ends-with" {
|
||||
assert str_starts_with("hello world", "hello"), "starts with hello"
|
||||
assert str_ends_with("hello world", "world"), "ends with world"
|
||||
assert !str_starts_with("hello world", "world"), "does not start with world"
|
||||
assert !str_ends_with("hello world", "hello"), "does not end with hello"
|
||||
assert str_starts_with("abc", ""), "empty prefix always matches"
|
||||
assert str_ends_with("abc", ""), "empty suffix always matches"
|
||||
}
|
||||
|
||||
test "str-contains" {
|
||||
assert str_contains("hello world", "world"), "contains world"
|
||||
assert str_contains("hello world", "lo wo"), "contains interior substring"
|
||||
assert !str_contains("hello world", "xyz"), "does not contain xyz"
|
||||
assert str_contains("abc", ""), "empty sub is always contained"
|
||||
assert !str_contains("", "x"), "empty string does not contain nonempty sub"
|
||||
}
|
||||
|
||||
test "str-index-of" {
|
||||
let i: Int = str_index_of("hello world", "world")
|
||||
assert i == 6, "index of world"
|
||||
let j: Int = str_index_of("hello world", "xyz")
|
||||
assert j == -1, "not found returns -1"
|
||||
let k: Int = str_index_of("aabbcc", "bb")
|
||||
assert k == 2, "index of bb in aabbcc"
|
||||
}
|
||||
|
||||
test "str-last-index-of" {
|
||||
let i: Int = str_last_index_of("abcabc", "bc")
|
||||
assert i == 4, "last occurrence of bc"
|
||||
let j: Int = str_last_index_of("hello", "xyz")
|
||||
assert j == -1, "not found returns -1"
|
||||
let k: Int = str_last_index_of("aaa", "a")
|
||||
assert k == 2, "last single-char match"
|
||||
}
|
||||
|
||||
test "str-to-upper-lower" {
|
||||
let up: String = str_to_upper("hello")
|
||||
assert up == "HELLO", "to upper"
|
||||
let lo: String = str_to_lower("WORLD")
|
||||
assert lo == "world", "to lower"
|
||||
let mixed: String = str_to_upper("Hello World")
|
||||
assert mixed == "HELLO WORLD", "mixed to upper"
|
||||
let empty: String = str_to_lower("")
|
||||
assert empty == "", "empty stays empty"
|
||||
}
|
||||
|
||||
test "str-trim" {
|
||||
let s: String = str_trim(" hello ")
|
||||
assert s == "hello", "trim both ends"
|
||||
let lonly: String = str_trim(" hello")
|
||||
assert lonly == "hello", "trim left only"
|
||||
let ronly: String = str_trim("hello ")
|
||||
assert ronly == "hello", "trim right only"
|
||||
let tabs: String = str_trim("\thello\n")
|
||||
assert tabs == "hello", "trim tabs and newlines"
|
||||
let empty: String = str_trim(" ")
|
||||
assert empty == "", "all whitespace trims to empty"
|
||||
}
|
||||
|
||||
test "str-replace" {
|
||||
let s: String = str_replace("hello world", "world", "there")
|
||||
assert s == "hello there", "replace word"
|
||||
let none: String = str_replace("hello", "xyz", "abc")
|
||||
assert none == "hello", "no match leaves string unchanged"
|
||||
let multi: String = str_replace("aaa", "a", "b")
|
||||
assert multi == "bbb", "replace all occurrences"
|
||||
let empty_from: String = str_replace("hello", "", "x")
|
||||
assert empty_from == "hello", "empty from returns original"
|
||||
}
|
||||
|
||||
test "str-repeat" {
|
||||
let s: String = str_repeat("ab", 3)
|
||||
assert s == "ababab", "repeat 3 times"
|
||||
let once: String = str_repeat("x", 1)
|
||||
assert once == "x", "repeat once"
|
||||
let zero: String = str_repeat("abc", 0)
|
||||
assert zero == "", "repeat zero times"
|
||||
let neg: String = str_repeat("abc", -1)
|
||||
assert neg == "", "negative repeat is empty"
|
||||
}
|
||||
|
||||
test "str-reverse" {
|
||||
let s: String = str_reverse("hello")
|
||||
assert s == "olleh", "reverse hello"
|
||||
let single: String = str_reverse("a")
|
||||
assert single == "a", "reverse single char"
|
||||
let empty: String = str_reverse("")
|
||||
assert empty == "", "reverse empty string"
|
||||
let palindrome: String = str_reverse("racecar")
|
||||
assert palindrome == "racecar", "reverse palindrome"
|
||||
}
|
||||
|
||||
test "str-strip-prefix-suffix" {
|
||||
let p: String = str_strip_prefix("foobar", "foo")
|
||||
assert p == "bar", "strip prefix foo"
|
||||
let no_prefix: String = str_strip_prefix("foobar", "baz")
|
||||
assert no_prefix == "foobar", "no match leaves string unchanged"
|
||||
let s: String = str_strip_suffix("hello.md", ".md")
|
||||
assert s == "hello", "strip suffix .md"
|
||||
let no_suffix: String = str_strip_suffix("hello.md", ".txt")
|
||||
assert no_suffix == "hello.md", "non-matching suffix unchanged"
|
||||
}
|
||||
|
||||
test "str-strip-chars" {
|
||||
let s: String = str_strip_chars(" \thello \n", " \t\n")
|
||||
assert s == "hello", "strip whitespace chars"
|
||||
let dots: String = str_strip_chars("...hello...", ".")
|
||||
assert dots == "hello", "strip dot chars"
|
||||
let all: String = str_strip_chars("aaa", "a")
|
||||
assert all == "", "strip all chars leaves empty"
|
||||
}
|
||||
|
||||
test "str-pad-left-right" {
|
||||
let l: String = str_pad_left("42", 5, "0")
|
||||
assert l == "00042", "zero-pad left to width 5"
|
||||
let r: String = str_pad_right("hi", 5, "-")
|
||||
assert r == "hi---", "dash-pad right to width 5"
|
||||
let no_pad: String = str_pad_left("hello", 3, "x")
|
||||
assert no_pad == "hello", "no pad when string already wide enough"
|
||||
}
|
||||
|
||||
test "str-count" {
|
||||
let n: Int = str_count("abc abc abc", "abc")
|
||||
assert n == 3, "count three occurrences"
|
||||
let overlap: Int = str_count("aaa", "aa")
|
||||
assert overlap == 1, "non-overlapping count"
|
||||
let zero: Int = str_count("hello", "xyz")
|
||||
assert zero == 0, "not found gives 0"
|
||||
let empty_sub: Int = str_count("hello", "")
|
||||
assert empty_sub == 0, "empty sub gives 0"
|
||||
}
|
||||
|
||||
test "str-count-lines-words-letters" {
|
||||
let s: String = "hello world\nfoo bar"
|
||||
let lines: Int = str_count_lines(s)
|
||||
let words: Int = str_count_words(s)
|
||||
let letters: Int = str_count_letters(s)
|
||||
assert lines == 2, "line count"
|
||||
assert words == 4, "word count"
|
||||
assert letters == 16, "letter count"
|
||||
}
|
||||
|
||||
test "str-count-chars-and-digits" {
|
||||
let digits: Int = str_count_digits("abc123def456")
|
||||
assert digits == 6, "six digits"
|
||||
let none: Int = str_count_digits("hello")
|
||||
assert none == 0, "no digits"
|
||||
let chars: Int = str_count_chars("hello")
|
||||
assert chars == 5, "five ASCII chars"
|
||||
}
|
||||
|
||||
test "char-classes" {
|
||||
assert is_letter("A"), "A is a letter"
|
||||
assert is_digit("7"), "7 is a digit"
|
||||
assert is_whitespace(" "), "space is whitespace"
|
||||
assert !is_letter("3"), "3 is not a letter"
|
||||
assert !is_digit("X"), "X is not a digit"
|
||||
assert is_alphanumeric("abc123"), "abc123 is alphanumeric"
|
||||
assert !is_alphanumeric("abc!"), "abc! is not alphanumeric"
|
||||
assert is_uppercase("ABC"), "ABC is uppercase"
|
||||
assert is_lowercase("abc"), "abc is lowercase"
|
||||
assert !is_uppercase("Abc"), "mixed is not uppercase"
|
||||
}
|
||||
|
||||
test "str-split" {
|
||||
let parts: [String] = str_split("a,b,c", ",")
|
||||
let n: Int = native_list_len(parts)
|
||||
assert n == 3, "split into 3 parts"
|
||||
let p0: String = native_list_get(parts, 0)
|
||||
let p1: String = native_list_get(parts, 1)
|
||||
let p2: String = native_list_get(parts, 2)
|
||||
assert p0 == "a", "first part"
|
||||
assert p1 == "b", "second part"
|
||||
assert p2 == "c", "third part"
|
||||
}
|
||||
|
||||
test "str-split-lines" {
|
||||
let lines: [String] = str_split_lines("alpha\nbeta\r\ngamma\n")
|
||||
let n: Int = native_list_len(lines)
|
||||
assert n == 3, "split into 3 lines"
|
||||
let l0: String = native_list_get(lines, 0)
|
||||
let l1: String = native_list_get(lines, 1)
|
||||
let l2: String = native_list_get(lines, 2)
|
||||
assert l0 == "alpha", "first line"
|
||||
assert l1 == "beta", "second line strips CR"
|
||||
assert l2 == "gamma", "third line"
|
||||
}
|
||||
|
||||
test "str-join" {
|
||||
let parts: [String] = native_list_empty()
|
||||
let parts = native_list_append(parts, "alpha")
|
||||
let parts = native_list_append(parts, "beta")
|
||||
let parts = native_list_append(parts, "gamma")
|
||||
let result: String = str_join(parts, ", ")
|
||||
assert result == "alpha, beta, gamma", "join with separator"
|
||||
let empty_parts: [String] = native_list_empty()
|
||||
let empty_result: String = str_join(empty_parts, ",")
|
||||
assert empty_result == "", "join empty list gives empty string"
|
||||
}
|
||||
|
||||
test "str-char-at" {
|
||||
let c: String = str_char_at("hello", 1)
|
||||
assert c == "e", "char at index 1"
|
||||
let first: String = str_char_at("abc", 0)
|
||||
assert first == "a", "first char"
|
||||
let oob: String = str_char_at("abc", 10)
|
||||
assert oob == "", "out of bounds returns empty"
|
||||
}
|
||||
|
||||
test "url-encode-decode" {
|
||||
let encoded: String = url_encode("hello world")
|
||||
assert !str_contains(encoded, " "), "space is encoded"
|
||||
let decoded: String = url_decode(encoded)
|
||||
assert decoded == "hello world", "round-trip decode"
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// test_text.el - native test suite for text primitives.
|
||||
//
|
||||
// Mirrors the acceptance corpus in tests/text/examples/ using the
|
||||
// native test/assert system instead of run.sh + expected output files.
|
||||
|
||||
test "count-substring" {
|
||||
let x: Int = str_count("abc abc abc", "abc")
|
||||
assert x == 3
|
||||
}
|
||||
|
||||
test "count-overlap-skip" {
|
||||
let x: Int = str_count("aaa", "aa")
|
||||
assert x == 1, "non-overlapping count of aa in aaa"
|
||||
}
|
||||
|
||||
test "count-lines-words-letters" {
|
||||
let s: String = "hello world\nfoo bar"
|
||||
let lines: Int = str_count_lines(s)
|
||||
let words: Int = str_count_words(s)
|
||||
let letters: Int = str_count_letters(s)
|
||||
assert lines == 2, "line count"
|
||||
assert words == 4, "word count"
|
||||
assert letters == 16, "letter count"
|
||||
}
|
||||
|
||||
test "index-of-all" {
|
||||
let positions: [Int] = str_index_of_all("abXcdXefX", "X")
|
||||
let n: Int = native_list_len(positions)
|
||||
assert n == 3, "should find 3 occurrences"
|
||||
let p0: Int = native_list_get(positions, 0)
|
||||
let p1: Int = native_list_get(positions, 1)
|
||||
let p2: Int = native_list_get(positions, 2)
|
||||
assert p0 == 2, "first X at index 2"
|
||||
assert p1 == 5, "second X at index 5"
|
||||
assert p2 == 8, "third X at index 8"
|
||||
}
|
||||
|
||||
test "str-repeat" {
|
||||
let s: String = str_repeat("ab", 3)
|
||||
assert s == "ababab", "repeat 3 times"
|
||||
}
|
||||
|
||||
test "str-reverse" {
|
||||
let s: String = str_reverse("hello")
|
||||
assert s == "olleh", "reverse hello"
|
||||
}
|
||||
|
||||
test "str-strip-prefix" {
|
||||
let s: String = str_strip_prefix("foobar", "foo")
|
||||
assert s == "bar", "strip prefix foo"
|
||||
}
|
||||
|
||||
test "str-strip-suffix" {
|
||||
let s: String = str_strip_suffix("hello.md", ".md")
|
||||
assert s == "hello", "strip suffix .md"
|
||||
}
|
||||
|
||||
test "str-strip-chars" {
|
||||
let s: String = str_strip_chars(" \thello \n", " \t\n")
|
||||
assert s == "hello", "strip whitespace chars"
|
||||
}
|
||||
|
||||
test "split-lines" {
|
||||
let lines: [String] = str_split_lines("alpha\nbeta\r\ngamma\n")
|
||||
let n: Int = native_list_len(lines)
|
||||
assert n == 3, "split into 3 lines"
|
||||
}
|
||||
|
||||
test "str-join" {
|
||||
let parts: [String] = native_list_empty()
|
||||
let parts = native_list_append(parts, "alpha")
|
||||
let parts = native_list_append(parts, "beta")
|
||||
let parts = native_list_append(parts, "gamma")
|
||||
let result: String = str_join(parts, ", ")
|
||||
assert result == "alpha, beta, gamma", "join with separator"
|
||||
}
|
||||
|
||||
test "char-classes" {
|
||||
assert is_letter("A"), "A is a letter"
|
||||
assert is_digit("7"), "7 is a digit"
|
||||
assert is_whitespace(" "), "space is whitespace"
|
||||
assert !is_letter("3"), "3 is not a letter"
|
||||
assert !is_digit("X"), "X is not a digit"
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
// test_time.el - native test suite for runtime/time.el
|
||||
//
|
||||
// Covers: time_now (positive timestamp), time_to_parts (UTC decomposition),
|
||||
// time_format (ISO 8601 and strftime tokens), time_add/diff, unix_timestamp,
|
||||
// duration helpers, and instant conversions.
|
||||
|
||||
test "time-now-positive" {
|
||||
let ts: Int = time_now()
|
||||
// time_now() returns milliseconds since epoch.
|
||||
// Any value greater than 1_700_000_000_000 (Nov 2023) is valid in 2025+.
|
||||
assert ts > 1700000000000, "time_now returns a reasonable recent timestamp"
|
||||
}
|
||||
|
||||
test "unix-timestamp-positive" {
|
||||
let s: Int = unix_timestamp()
|
||||
// Should be greater than 1_700_000_000 (seconds, Nov 2023)
|
||||
assert s > 1700000000, "unix_timestamp returns seconds > 1700000000"
|
||||
}
|
||||
|
||||
test "now-ns-positive" {
|
||||
let ns: Int = now_ns()
|
||||
// Should be a large nanosecond value
|
||||
assert ns > 0, "now_ns returns positive value"
|
||||
}
|
||||
|
||||
test "time-to-parts-epoch" {
|
||||
// Unix epoch: 0 ms = 1970-01-01T00:00:00.000Z
|
||||
let parts: String = time_to_parts(0)
|
||||
let year: String = json_get(parts, "year")
|
||||
let month: String = json_get(parts, "month")
|
||||
let day: String = json_get(parts, "day")
|
||||
let hour: String = json_get(parts, "hour")
|
||||
assert year == "1970", "epoch year is 1970"
|
||||
assert month == "1", "epoch month is 1"
|
||||
assert day == "1", "epoch day is 1"
|
||||
assert hour == "0", "epoch hour is 0"
|
||||
}
|
||||
|
||||
test "time-to-parts-known-date" {
|
||||
// 2024-03-15T12:30:45.000Z
|
||||
// seconds = 2024-03-15 12:30:45 UTC
|
||||
// epoch ms: use a known value
|
||||
// 2024-03-15 00:00:00 UTC = 1710460800 seconds
|
||||
// + 12*3600 + 30*60 + 45 = 43200 + 1800 + 45 = 45045 seconds
|
||||
// total: 1710505845 seconds = 1710505845000 ms
|
||||
let ts: Int = 1710505845000
|
||||
let parts: String = time_to_parts(ts)
|
||||
let year: String = json_get(parts, "year")
|
||||
let month: String = json_get(parts, "month")
|
||||
let day: String = json_get(parts, "day")
|
||||
let hour: String = json_get(parts, "hour")
|
||||
let minute: String = json_get(parts, "minute")
|
||||
let second: String = json_get(parts, "second")
|
||||
assert year == "2024", "year is 2024"
|
||||
assert month == "3", "month is 3 (March)"
|
||||
assert day == "15", "day is 15"
|
||||
assert hour == "12", "hour is 12"
|
||||
assert minute == "30", "minute is 30"
|
||||
assert second == "45", "second is 45"
|
||||
}
|
||||
|
||||
test "time-format-iso" {
|
||||
// Epoch = 1970-01-01T00:00:00.000Z
|
||||
let formatted: String = time_format(0, "ISO")
|
||||
assert formatted == "1970-01-01T00:00:00.000Z", "epoch formats to ISO correctly"
|
||||
}
|
||||
|
||||
test "time-format-iso-empty-fmt" {
|
||||
// Empty format string should also produce ISO 8601
|
||||
let formatted: String = time_format(0, "")
|
||||
assert formatted == "1970-01-01T00:00:00.000Z", "empty fmt produces ISO"
|
||||
}
|
||||
|
||||
test "time-format-strftime" {
|
||||
// 2024-03-15T12:30:45.000Z
|
||||
let ts: Int = 1710505845000
|
||||
let fmt: String = time_format(ts, "%Y-%m-%d")
|
||||
assert fmt == "2024-03-15", "strftime %Y-%m-%d"
|
||||
let hms: String = time_format(ts, "%H:%M:%S")
|
||||
assert hms == "12:30:45", "strftime %H:%M:%S"
|
||||
}
|
||||
|
||||
test "time-add-milliseconds" {
|
||||
let base: Int = 1000000
|
||||
let result: Int = time_add(base, 500, "ms")
|
||||
assert result == 1000500, "add 500 ms"
|
||||
}
|
||||
|
||||
test "time-add-seconds" {
|
||||
let base: Int = 0
|
||||
let result: Int = time_add(base, 60, "sec")
|
||||
assert result == 60000, "add 60 seconds = 60000 ms"
|
||||
}
|
||||
|
||||
test "time-add-minutes" {
|
||||
let base: Int = 0
|
||||
let result: Int = time_add(base, 2, "min")
|
||||
assert result == 120000, "add 2 minutes = 120000 ms"
|
||||
}
|
||||
|
||||
test "time-add-hours" {
|
||||
let base: Int = 0
|
||||
let result: Int = time_add(base, 1, "hour")
|
||||
assert result == 3600000, "add 1 hour = 3600000 ms"
|
||||
}
|
||||
|
||||
test "time-add-days" {
|
||||
let base: Int = 0
|
||||
let result: Int = time_add(base, 1, "day")
|
||||
assert result == 86400000, "add 1 day = 86400000 ms"
|
||||
}
|
||||
|
||||
test "time-diff-seconds" {
|
||||
let t1: Int = 0
|
||||
let t2: Int = 90000
|
||||
let d: Int = time_diff(t1, t2, "sec")
|
||||
assert d == 90, "diff 90000 ms = 90 seconds"
|
||||
}
|
||||
|
||||
test "time-diff-negative" {
|
||||
let t1: Int = 5000
|
||||
let t2: Int = 2000
|
||||
let d: Int = time_diff(t1, t2, "sec")
|
||||
assert d == -3, "negative diff when t2 < t1"
|
||||
}
|
||||
|
||||
test "duration-helpers" {
|
||||
let d_secs: Int = duration_seconds(5)
|
||||
assert d_secs == 5000000000, "5 seconds in nanoseconds"
|
||||
let d_ms: Int = duration_millis(100)
|
||||
assert d_ms == 100000000, "100 ms in nanoseconds"
|
||||
let d_ns: Int = duration_nanos(42)
|
||||
assert d_ns == 42, "42 nanos is identity"
|
||||
let back_secs: Int = duration_to_seconds(d_secs)
|
||||
assert back_secs == 5, "convert back to seconds"
|
||||
let back_ms: Int = duration_to_millis(d_ms)
|
||||
assert back_ms == 100, "convert back to milliseconds"
|
||||
}
|
||||
|
||||
test "instant-conversions" {
|
||||
let inst: Int = unix_millis(1000)
|
||||
assert inst == 1000000000, "unix_millis(1000) in nanoseconds"
|
||||
let inst_secs: Int = unix_seconds(2)
|
||||
assert inst_secs == 2000000000, "unix_seconds(2) in nanoseconds"
|
||||
let back_ms: Int = instant_to_unix_millis(inst)
|
||||
assert back_ms == 1000, "instant to unix millis"
|
||||
let back_secs: Int = instant_to_unix_seconds(inst_secs)
|
||||
assert back_secs == 2, "instant to unix seconds"
|
||||
}
|
||||
|
||||
test "time-from-parts" {
|
||||
// time_from_parts(secs, ns, tz) -> secs*1000 + ns/1_000_000
|
||||
let ts: Int = time_from_parts(1000, 500000000, "UTC")
|
||||
assert ts == 1000500, "time_from_parts: 1000 secs + 500ms"
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
// tests/runtime/string_test.el — Test suite for runtime/string.el
|
||||
//
|
||||
// Exercises every public function exported by runtime/string.el using the
|
||||
// runtime/test.el framework. Each test function covers one string primitive
|
||||
// or a tight family of related functions.
|
||||
//
|
||||
// ── Build and run ────────────────────────────────────────────────────────────
|
||||
//
|
||||
// # Combine runtime modules + test framework + this file, then compile:
|
||||
// cat runtime/string.el runtime/math.el runtime/state.el runtime/env.el \
|
||||
// runtime/fs.el runtime/exec.el runtime/time.el runtime/json.el \
|
||||
// runtime/http.el runtime/engram.el runtime/thread.el \
|
||||
// runtime/test.el \
|
||||
// tests/runtime/string_test.el > /tmp/string_test_combined.el
|
||||
//
|
||||
// ./dist/platform/elc /tmp/string_test_combined.el > /tmp/string_test.c
|
||||
// cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
// -o /tmp/string_test /tmp/string_test.c el-compiler/runtime/el_seed.c
|
||||
// /tmp/string_test; echo "exit: $?"
|
||||
//
|
||||
// Exit code equals the number of failing assertions (0 = all pass).
|
||||
//
|
||||
// ── Coverage ─────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// str_eq str_neq (via assert_neq)
|
||||
// str_len str_concat
|
||||
// str_starts_with str_ends_with
|
||||
// str_contains str_index_of
|
||||
// str_slice str_replace
|
||||
// str_to_upper str_to_lower
|
||||
// str_trim str_lstrip / str_rstrip
|
||||
// str_split str_join
|
||||
// int_to_str str_to_int
|
||||
// str_repeat str_reverse
|
||||
// str_count
|
||||
|
||||
// ── str_eq ────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_eq(_: String) -> String {
|
||||
assert_true(str_eq("hello", "hello"), "identical strings are equal")
|
||||
assert_false(str_eq("hello", "world"), "different strings are not equal")
|
||||
assert_true(str_eq("", ""), "empty strings are equal")
|
||||
assert_false(str_eq("a", ""), "non-empty vs empty is not equal")
|
||||
assert_false(str_eq("", "a"), "empty vs non-empty is not equal")
|
||||
assert_false(str_eq("Hello", "hello"), "case-sensitive comparison")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_len ───────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_len(_: String) -> String {
|
||||
assert_int_eq(str_len(""), 0, "empty string has length 0")
|
||||
assert_int_eq(str_len("a"), 1, "single char has length 1")
|
||||
assert_int_eq(str_len("hello"), 5, "hello has length 5")
|
||||
assert_int_eq(str_len("hello world"), 11, "space included in length")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_concat ────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_concat(_: String) -> String {
|
||||
assert_eq(str_concat("hello", " world"), "hello world", "basic concat")
|
||||
assert_eq(str_concat("", "world"), "world", "empty prefix")
|
||||
assert_eq(str_concat("hello", ""), "hello", "empty suffix")
|
||||
assert_eq(str_concat("", ""), "", "both empty")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_starts_with ───────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_starts_with(_: String) -> String {
|
||||
assert_true(str_starts_with("hello world", "hello"), "prefix present")
|
||||
assert_false(str_starts_with("hello world", "world"), "not a prefix")
|
||||
assert_true(str_starts_with("hello", "hello"), "string is its own prefix")
|
||||
assert_true(str_starts_with("hello", ""), "empty prefix always true")
|
||||
assert_false(str_starts_with("", "a"), "empty string has no prefix")
|
||||
assert_false(str_starts_with("hi", "hello"), "prefix longer than string")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_ends_with ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_ends_with(_: String) -> String {
|
||||
assert_true(str_ends_with("hello world", "world"), "suffix present")
|
||||
assert_false(str_ends_with("hello world", "hello"), "not a suffix")
|
||||
assert_true(str_ends_with("hello", "hello"), "string is its own suffix")
|
||||
assert_true(str_ends_with("hello", ""), "empty suffix always true")
|
||||
assert_false(str_ends_with("", "a"), "empty string has no suffix")
|
||||
assert_false(str_ends_with("hi", "world"), "suffix longer than string")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_contains ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_contains(_: String) -> String {
|
||||
assert_true(str_contains("hello world", "world"), "contains at end")
|
||||
assert_true(str_contains("hello world", "hello"), "contains at start")
|
||||
assert_true(str_contains("hello world", "lo wo"), "contains in middle")
|
||||
assert_false(str_contains("hello world", "xyz"), "not contained")
|
||||
assert_true(str_contains("hello", ""), "empty sub always contained")
|
||||
assert_false(str_contains("", "a"), "empty string contains nothing")
|
||||
assert_true(str_contains("hello", "hello"), "string contains itself")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_index_of ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_index_of(_: String) -> String {
|
||||
assert_int_eq(str_index_of("hello world", "world"), 6, "index of suffix")
|
||||
assert_int_eq(str_index_of("hello world", "hello"), 0, "index of prefix")
|
||||
assert_int_eq(str_index_of("hello world", "o"), 4, "index of first occurrence")
|
||||
assert_int_eq(str_index_of("hello world", "xyz"), -1, "not found returns -1")
|
||||
assert_int_eq(str_index_of("hello", ""), 0, "empty sub returns 0")
|
||||
assert_int_eq(str_index_of("", "a"), -1, "search in empty string")
|
||||
assert_int_eq(str_index_of("aababc", "ab"), 1, "finds first occurrence")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_replace ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_replace(_: String) -> String {
|
||||
assert_eq(str_replace("hello world", "world", "there"), "hello there", "basic replace")
|
||||
assert_eq(str_replace("aaa", "a", "b"), "bbb", "replace all occurrences")
|
||||
assert_eq(str_replace("hello", "xyz", "abc"), "hello", "no match is identity")
|
||||
assert_eq(str_replace("", "a", "b"), "", "empty string unchanged")
|
||||
assert_eq(str_replace("hello", "", "x"), "hello", "empty from is identity")
|
||||
assert_eq(str_replace("hello hello", "hello", "bye"), "bye bye", "replace multiple")
|
||||
assert_eq(str_replace("aXbXc", "X", "-"), "a-b-c", "single-char delimiter")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_slice ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_slice(_: String) -> String {
|
||||
assert_eq(str_slice("hello world", 0, 5), "hello", "slice from start")
|
||||
assert_eq(str_slice("hello world", 6, 11), "world", "slice from middle")
|
||||
assert_eq(str_slice("hello world", 0, 0), "", "zero-length slice")
|
||||
assert_eq(str_slice("hello", 0, 100), "hello", "end beyond length clamped")
|
||||
assert_eq(str_slice("hello", 3, 3), "", "start == end is empty")
|
||||
assert_eq(str_slice("hello world", 2, 7), "llo w", "interior slice")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_to_upper / str_to_lower ──────────────────────────────────────────────
|
||||
|
||||
fn test_str_to_upper(_: String) -> String {
|
||||
assert_eq(str_to_upper("hello"), "HELLO", "lowercase to uppercase")
|
||||
assert_eq(str_to_upper("HELLO"), "HELLO", "already uppercase unchanged")
|
||||
assert_eq(str_to_upper("Hello World"), "HELLO WORLD", "mixed case")
|
||||
assert_eq(str_to_upper(""), "", "empty string unchanged")
|
||||
assert_eq(str_to_upper("hello123"), "HELLO123", "digits unchanged")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_to_lower(_: String) -> String {
|
||||
assert_eq(str_to_lower("HELLO"), "hello", "uppercase to lowercase")
|
||||
assert_eq(str_to_lower("hello"), "hello", "already lowercase unchanged")
|
||||
assert_eq(str_to_lower("Hello World"), "hello world", "mixed case")
|
||||
assert_eq(str_to_lower(""), "", "empty string unchanged")
|
||||
assert_eq(str_to_lower("HELLO123"), "hello123", "digits unchanged")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_trim ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_trim(_: String) -> String {
|
||||
assert_eq(str_trim(" hello "), "hello", "trims spaces both sides")
|
||||
assert_eq(str_trim("hello"), "hello", "no whitespace unchanged")
|
||||
assert_eq(str_trim(" "), "", "all-space string becomes empty")
|
||||
assert_eq(str_trim(""), "", "empty string unchanged")
|
||||
assert_eq(str_trim("\t hello \n"), "hello", "trims tabs and newlines")
|
||||
assert_eq(str_trim(" hello world "), "hello world", "internal spaces preserved")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_split ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_split(_: String) -> String {
|
||||
let parts: [String] = str_split("a,b,c", ",")
|
||||
assert_int_eq(el_list_len(parts), 3, "split yields 3 parts")
|
||||
assert_eq(el_list_get(parts, 0), "a", "first part is a")
|
||||
assert_eq(el_list_get(parts, 1), "b", "second part is b")
|
||||
assert_eq(el_list_get(parts, 2), "c", "third part is c")
|
||||
|
||||
let single: [String] = str_split("hello", ",")
|
||||
assert_int_eq(el_list_len(single), 1, "no sep found yields 1 part")
|
||||
assert_eq(el_list_get(single, 0), "hello", "single part is the full string")
|
||||
|
||||
let trailing: [String] = str_split("a,b,", ",")
|
||||
assert_int_eq(el_list_len(trailing), 3, "trailing sep yields empty last element")
|
||||
assert_eq(el_list_get(trailing, 2), "", "last element is empty")
|
||||
|
||||
let empty_str: [String] = str_split("", ",")
|
||||
assert_int_eq(el_list_len(empty_str), 1, "splitting empty string yields one empty element")
|
||||
|
||||
let multi: [String] = str_split("one::two::three", "::")
|
||||
assert_int_eq(el_list_len(multi), 3, "multi-char separator works")
|
||||
assert_eq(el_list_get(multi, 0), "one", "multi-sep first part")
|
||||
assert_eq(el_list_get(multi, 2), "three", "multi-sep last part")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_join ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_join(_: String) -> String {
|
||||
let parts: [String] = el_list_empty()
|
||||
let parts = el_list_append(parts, "a")
|
||||
let parts = el_list_append(parts, "b")
|
||||
let parts = el_list_append(parts, "c")
|
||||
assert_eq(str_join(parts, ","), "a,b,c", "basic join with comma")
|
||||
assert_eq(str_join(parts, ""), "abc", "join with empty separator")
|
||||
assert_eq(str_join(parts, " | "), "a | b | c", "join with multi-char sep")
|
||||
|
||||
let empty_list: [String] = el_list_empty()
|
||||
assert_eq(str_join(empty_list, ","), "", "joining empty list yields empty string")
|
||||
|
||||
let one: [String] = el_list_empty()
|
||||
let one = el_list_append(one, "solo")
|
||||
assert_eq(str_join(one, ","), "solo", "joining single element yields that element")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── int_to_str / str_to_int ──────────────────────────────────────────────────
|
||||
|
||||
fn test_int_to_str(_: String) -> String {
|
||||
assert_eq(int_to_str(0), "0", "zero")
|
||||
assert_eq(int_to_str(42), "42", "positive integer")
|
||||
assert_eq(int_to_str(-1), "-1", "negative integer")
|
||||
assert_eq(int_to_str(1000000), "1000000", "large integer")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_to_int(_: String) -> String {
|
||||
assert_int_eq(str_to_int("0"), 0, "zero")
|
||||
assert_int_eq(str_to_int("42"), 42, "positive integer")
|
||||
assert_int_eq(str_to_int("-1"), -1, "negative integer")
|
||||
assert_int_eq(str_to_int("1000000"), 1000000, "large integer")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_repeat ────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_repeat(_: String) -> String {
|
||||
assert_eq(str_repeat("ab", 3), "ababab", "repeat 3 times")
|
||||
assert_eq(str_repeat("x", 1), "x", "repeat once")
|
||||
assert_eq(str_repeat("x", 0), "", "repeat zero times yields empty")
|
||||
assert_eq(str_repeat("", 5), "", "repeating empty string yields empty")
|
||||
assert_eq(str_repeat("-", 4), "----", "single char repeat")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_reverse ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_reverse(_: String) -> String {
|
||||
assert_eq(str_reverse("hello"), "olleh", "basic reverse")
|
||||
assert_eq(str_reverse("a"), "a", "single char is its own reverse")
|
||||
assert_eq(str_reverse(""), "", "empty string reverses to empty")
|
||||
assert_eq(str_reverse("abcd"), "dcba", "even-length reverse")
|
||||
assert_eq(str_reverse("racecar"), "racecar", "palindrome is unchanged")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_count ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_count(_: String) -> String {
|
||||
assert_int_eq(str_count("hello world hello", "hello"), 2, "two occurrences")
|
||||
assert_int_eq(str_count("aaa", "a"), 3, "adjacent single chars")
|
||||
assert_int_eq(str_count("aaa", "aa"), 1, "non-overlapping: one match")
|
||||
assert_int_eq(str_count("hello", "xyz"), 0, "no match")
|
||||
assert_int_eq(str_count("", "a"), 0, "empty string has no occurrences")
|
||||
assert_int_eq(str_count("hello", ""), 0, "empty sub returns 0")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_strip_prefix / str_strip_suffix ───────────────────────────────────────
|
||||
|
||||
fn test_str_strip_prefix(_: String) -> String {
|
||||
assert_eq(str_strip_prefix("foobar", "foo"), "bar", "strips matching prefix")
|
||||
assert_eq(str_strip_prefix("foobar", "baz"), "foobar", "no-match is identity")
|
||||
assert_eq(str_strip_prefix("hello", ""), "hello", "empty prefix is identity")
|
||||
assert_eq(str_strip_prefix("hello", "hello"), "", "full match yields empty")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_strip_suffix(_: String) -> String {
|
||||
assert_eq(str_strip_suffix("hello.md", ".md"), "hello", "strips matching suffix")
|
||||
assert_eq(str_strip_suffix("hello.md", ".txt"), "hello.md", "no-match is identity")
|
||||
assert_eq(str_strip_suffix("hello", ""), "hello", "empty suffix is identity")
|
||||
assert_eq(str_strip_suffix("hello", "hello"), "", "full match yields empty")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_find_chars ────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_find_chars(_: String) -> String {
|
||||
assert_int_eq(str_find_chars("hello world", " "), 5, "finds space at index 5")
|
||||
assert_int_eq(str_find_chars("hello", "xyz"), -1, "not found returns -1")
|
||||
assert_int_eq(str_find_chars("hello", ""), -1, "empty charset returns -1")
|
||||
assert_int_eq(str_find_chars("hello", "aeiou"), 1, "finds first vowel at index 1")
|
||||
assert_int_eq(str_find_chars("", "a"), -1, "search in empty string returns -1")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_last_index_of ─────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_last_index_of(_: String) -> String {
|
||||
assert_int_eq(str_last_index_of("hello hello", "hello"), 6, "last occurrence")
|
||||
assert_int_eq(str_last_index_of("hello", "hello"), 0, "single occurrence")
|
||||
assert_int_eq(str_last_index_of("hello", "xyz"), -1, "not found returns -1")
|
||||
assert_int_eq(str_last_index_of("aababc", "ab"), 3, "last ab in aababc")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── Entry point ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn main() -> Int {
|
||||
test_case("str_eq", "test_str_eq")
|
||||
test_case("str_len", "test_str_len")
|
||||
test_case("str_concat", "test_str_concat")
|
||||
test_case("str_starts_with", "test_str_starts_with")
|
||||
test_case("str_ends_with", "test_str_ends_with")
|
||||
test_case("str_contains", "test_str_contains")
|
||||
test_case("str_index_of", "test_str_index_of")
|
||||
test_case("str_replace", "test_str_replace")
|
||||
test_case("str_slice", "test_str_slice")
|
||||
test_case("str_to_upper", "test_str_to_upper")
|
||||
test_case("str_to_lower", "test_str_to_lower")
|
||||
test_case("str_trim", "test_str_trim")
|
||||
test_case("str_split", "test_str_split")
|
||||
test_case("str_join", "test_str_join")
|
||||
test_case("int_to_str", "test_int_to_str")
|
||||
test_case("str_to_int", "test_str_to_int")
|
||||
test_case("str_repeat", "test_str_repeat")
|
||||
test_case("str_reverse", "test_str_reverse")
|
||||
test_case("str_count", "test_str_count")
|
||||
test_case("str_strip_prefix", "test_str_strip_prefix")
|
||||
test_case("str_strip_suffix", "test_str_strip_suffix")
|
||||
test_case("str_find_chars", "test_str_find_chars")
|
||||
test_case("str_last_index_of", "test_str_last_index_of")
|
||||
return test_run_all()
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
/*
|
||||
* tests/suite/infra.c — El runtime infrastructure functions for the test suite.
|
||||
*
|
||||
* The El test suite compiles runtime/*.el files into tests.c via elc, then links
|
||||
* only against el_seed.c (not the full el_runtime.c, which would cause ~120
|
||||
* duplicate symbol errors). This file provides the infrastructure symbols that
|
||||
* tests.c references but that are NOT defined in either the generated El code or
|
||||
* el_seed.c:
|
||||
*
|
||||
* ─ ElList machinery: el_list_empty, el_list_append, el_list_len, el_list_get,
|
||||
* el_str_concat, native_list_*, len, get
|
||||
* ─ el_runtime_init_args (needed by the generated main())
|
||||
* ─ Stubs for http_*, engram_*, el_html_sanitize (not needed by tests)
|
||||
*
|
||||
* These implementations are derived from el_runtime.c.
|
||||
*/
|
||||
|
||||
#ifndef _GNU_SOURCE
|
||||
#define _GNU_SOURCE
|
||||
#endif
|
||||
|
||||
#include "el_seed.h"
|
||||
#include <stdarg.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* ── String arena (minimal — no HTTP request lifecycle needed) ─────────────── */
|
||||
|
||||
static char* el_strdup(const char* s) {
|
||||
if (!s) return strdup("");
|
||||
return strdup(s);
|
||||
}
|
||||
|
||||
static char* el_strbuf(size_t n) {
|
||||
char* p = malloc(n + 1);
|
||||
if (!p) { fputs("infra: out of memory\n", stderr); exit(1); }
|
||||
p[0] = '\0';
|
||||
return p;
|
||||
}
|
||||
|
||||
static el_val_t el_wrap_str(char* s) {
|
||||
return EL_STR(s);
|
||||
}
|
||||
|
||||
/* ── el_str_concat ────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_str_concat(el_val_t av, el_val_t bv) {
|
||||
const char* a = EL_CSTR(av);
|
||||
const char* b = EL_CSTR(bv);
|
||||
if (!a) a = "";
|
||||
if (!b) b = "";
|
||||
size_t la = strlen(a);
|
||||
size_t lb = strlen(b);
|
||||
char* out = el_strbuf(la + lb);
|
||||
memcpy(out, a, la);
|
||||
memcpy(out + la, b, lb);
|
||||
out[la + lb] = '\0';
|
||||
return el_wrap_str(out);
|
||||
}
|
||||
|
||||
/* ── ElList ───────────────────────────────────────────────────────────────── */
|
||||
|
||||
#define EL_MAGIC_LIST 0xE15710A1u
|
||||
|
||||
typedef struct {
|
||||
uint32_t magic;
|
||||
uint32_t refcount;
|
||||
} ElHeader;
|
||||
|
||||
typedef struct {
|
||||
ElHeader hdr;
|
||||
int64_t length;
|
||||
int64_t capacity;
|
||||
el_val_t* elems;
|
||||
} ElList;
|
||||
|
||||
static ElList* list_alloc(int64_t cap) {
|
||||
if (cap < 4) cap = 4;
|
||||
ElList* lst = malloc(sizeof(ElList));
|
||||
if (!lst) { fputs("infra: out of memory\n", stderr); exit(1); }
|
||||
lst->hdr.magic = EL_MAGIC_LIST;
|
||||
lst->hdr.refcount = 1;
|
||||
lst->length = 0;
|
||||
lst->capacity = cap;
|
||||
lst->elems = malloc((size_t)cap * sizeof(el_val_t));
|
||||
if (!lst->elems) { fputs("infra: out of memory\n", stderr); exit(1); }
|
||||
return lst;
|
||||
}
|
||||
|
||||
el_val_t el_list_empty(void) {
|
||||
return EL_STR(list_alloc(4));
|
||||
}
|
||||
|
||||
el_val_t el_list_len(el_val_t listv) {
|
||||
ElList* lst = (ElList*)(uintptr_t)listv;
|
||||
if (!lst) return 0;
|
||||
return lst->length;
|
||||
}
|
||||
|
||||
el_val_t el_list_get(el_val_t listv, el_val_t index) {
|
||||
ElList* lst = (ElList*)(uintptr_t)listv;
|
||||
if (!lst) return 0;
|
||||
if (index < 0 || index >= lst->length) return 0;
|
||||
return lst->elems[index];
|
||||
}
|
||||
|
||||
el_val_t el_list_append(el_val_t listv, el_val_t elem) {
|
||||
ElList* old = (ElList*)(uintptr_t)listv;
|
||||
if (!old) {
|
||||
ElList* fresh = list_alloc(4);
|
||||
fresh->elems[0] = elem;
|
||||
fresh->length = 1;
|
||||
return EL_STR(fresh);
|
||||
}
|
||||
|
||||
if (old->hdr.refcount <= 1) {
|
||||
if (old->length >= old->capacity) {
|
||||
int64_t new_cap = old->capacity > 0 ? old->capacity * 2 : 4;
|
||||
el_val_t* grown = realloc(old->elems, (size_t)new_cap * sizeof(el_val_t));
|
||||
if (!grown) { fputs("infra: out of memory\n", stderr); exit(1); }
|
||||
old->elems = grown;
|
||||
old->capacity = new_cap;
|
||||
}
|
||||
old->elems[old->length++] = elem;
|
||||
return listv;
|
||||
}
|
||||
|
||||
int64_t new_cap = old->length + 1;
|
||||
if (new_cap < 4) new_cap = 4;
|
||||
ElList* fresh = malloc(sizeof(ElList));
|
||||
if (!fresh) { fputs("infra: out of memory\n", stderr); exit(1); }
|
||||
fresh->hdr.magic = EL_MAGIC_LIST;
|
||||
fresh->hdr.refcount = 1;
|
||||
fresh->length = old->length + 1;
|
||||
fresh->capacity = new_cap;
|
||||
fresh->elems = malloc((size_t)new_cap * sizeof(el_val_t));
|
||||
if (!fresh->elems) { fputs("infra: out of memory\n", stderr); exit(1); }
|
||||
if (old->length > 0) {
|
||||
memcpy(fresh->elems, old->elems, (size_t)old->length * sizeof(el_val_t));
|
||||
}
|
||||
fresh->elems[old->length] = elem;
|
||||
return EL_STR(fresh);
|
||||
}
|
||||
|
||||
/* ── native_list aliases ──────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t native_list_empty(void) { return el_list_empty(); }
|
||||
el_val_t native_list_append(el_val_t list, el_val_t elem) { return el_list_append(list, elem); }
|
||||
el_val_t native_list_get(el_val_t list, el_val_t index) { return el_list_get(list, index); }
|
||||
el_val_t native_list_len(el_val_t list) { return el_list_len(list); }
|
||||
|
||||
/* ── len / get aliases ─────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t len(el_val_t list) { return el_list_len(list); }
|
||||
el_val_t get(el_val_t list, el_val_t index) { return el_list_get(list, index); }
|
||||
|
||||
/* ── el_runtime_init_args ─────────────────────────────────────────────────── */
|
||||
|
||||
static el_val_t _el_args_list = 0;
|
||||
|
||||
void el_runtime_init_args(int argc, char** argv) {
|
||||
_el_args_list = el_list_empty();
|
||||
for (int i = 1; i < argc; i++) {
|
||||
_el_args_list = el_list_append(_el_args_list, EL_STR(argv[i]));
|
||||
}
|
||||
}
|
||||
|
||||
/* ── HTTP stubs (not exercised by test suite) ─────────────────────────────── */
|
||||
|
||||
el_val_t http_post(el_val_t url, el_val_t body) {
|
||||
(void)url; (void)body;
|
||||
return EL_STR("");
|
||||
}
|
||||
|
||||
el_val_t http_post_json(el_val_t url, el_val_t body) {
|
||||
(void)url; (void)body;
|
||||
return EL_STR("");
|
||||
}
|
||||
|
||||
el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers) {
|
||||
(void)url; (void)body; (void)headers;
|
||||
return EL_STR("");
|
||||
}
|
||||
|
||||
el_val_t http_response(el_val_t status, el_val_t headers, el_val_t body) {
|
||||
(void)status; (void)headers; (void)body;
|
||||
return EL_STR("");
|
||||
}
|
||||
|
||||
void http_serve(el_val_t port, el_val_t handler) {
|
||||
(void)port; (void)handler;
|
||||
}
|
||||
|
||||
void http_serve_v2(el_val_t port, el_val_t handler) {
|
||||
(void)port; (void)handler;
|
||||
}
|
||||
|
||||
/* ── el_html_sanitize stub ────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_html_sanitize(el_val_t input, el_val_t allowlist) {
|
||||
(void)allowlist;
|
||||
return input;
|
||||
}
|
||||
|
||||
/* ── Engram stubs (not exercised by test suite) ───────────────────────────── */
|
||||
|
||||
el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience) {
|
||||
(void)content; (void)node_type; (void)salience;
|
||||
return EL_STR("");
|
||||
}
|
||||
|
||||
el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label,
|
||||
el_val_t salience, el_val_t importance, el_val_t confidence,
|
||||
el_val_t tier, el_val_t tags) {
|
||||
(void)content; (void)node_type; (void)label; (void)salience;
|
||||
(void)importance; (void)confidence; (void)tier; (void)tags;
|
||||
return EL_STR("");
|
||||
}
|
||||
|
||||
el_val_t engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label,
|
||||
el_val_t salience, el_val_t certainty, el_val_t confidence,
|
||||
el_val_t tier, el_val_t layer_id, el_val_t tags) {
|
||||
(void)content; (void)node_type; (void)label; (void)salience;
|
||||
(void)certainty; (void)confidence; (void)tier; (void)layer_id; (void)tags;
|
||||
return EL_STR("");
|
||||
}
|
||||
|
||||
el_val_t engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible,
|
||||
el_val_t transparent, el_val_t injectable) {
|
||||
(void)name; (void)priority; (void)suppressible; (void)transparent; (void)injectable;
|
||||
return EL_STR("");
|
||||
}
|
||||
|
||||
el_val_t engram_remove_layer(el_val_t layer_id) {
|
||||
(void)layer_id;
|
||||
return (el_val_t)1;
|
||||
}
|
||||
|
||||
el_val_t engram_list_layers(void) { return EL_STR("[]"); }
|
||||
el_val_t engram_list_layers_json(void) { return EL_STR("[]"); }
|
||||
|
||||
el_val_t engram_get_node(el_val_t id) { (void)id; return EL_STR(""); }
|
||||
el_val_t engram_get_node_json(el_val_t id) { (void)id; return EL_STR("{}"); }
|
||||
|
||||
void engram_strengthen(el_val_t node_id) { (void)node_id; }
|
||||
void engram_forget(el_val_t node_id) { (void)node_id; }
|
||||
|
||||
el_val_t engram_node_count(void) { return 0; }
|
||||
el_val_t engram_edge_count(void) { return 0; }
|
||||
el_val_t engram_stats_json(void) { return EL_STR("{}"); }
|
||||
|
||||
void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation) {
|
||||
(void)from_id; (void)to_id; (void)weight; (void)relation;
|
||||
}
|
||||
|
||||
el_val_t engram_edge_between(el_val_t from_id, el_val_t to_id) {
|
||||
(void)from_id; (void)to_id;
|
||||
return (el_val_t)0;
|
||||
}
|
||||
|
||||
el_val_t engram_search(el_val_t query, el_val_t limit) {
|
||||
(void)query; (void)limit;
|
||||
return el_list_empty();
|
||||
}
|
||||
|
||||
el_val_t engram_search_json(el_val_t query, el_val_t limit) {
|
||||
(void)query; (void)limit;
|
||||
return EL_STR("[]");
|
||||
}
|
||||
|
||||
el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset) {
|
||||
(void)limit; (void)offset;
|
||||
return el_list_empty();
|
||||
}
|
||||
|
||||
el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset) {
|
||||
(void)limit; (void)offset;
|
||||
return EL_STR("[]");
|
||||
}
|
||||
|
||||
el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset) {
|
||||
(void)node_type; (void)limit; (void)offset;
|
||||
return EL_STR("[]");
|
||||
}
|
||||
|
||||
el_val_t engram_neighbors(el_val_t node_id, el_val_t limit) {
|
||||
(void)node_id; (void)limit;
|
||||
return el_list_empty();
|
||||
}
|
||||
|
||||
el_val_t engram_neighbors_filtered(el_val_t node_id, el_val_t relation, el_val_t limit) {
|
||||
(void)node_id; (void)relation; (void)limit;
|
||||
return el_list_empty();
|
||||
}
|
||||
|
||||
el_val_t engram_neighbors_json(el_val_t node_id, el_val_t limit) {
|
||||
(void)node_id; (void)limit;
|
||||
return EL_STR("[]");
|
||||
}
|
||||
|
||||
el_val_t engram_load(el_val_t path) { (void)path; return (el_val_t)1; }
|
||||
el_val_t engram_save(el_val_t path) { (void)path; return (el_val_t)1; }
|
||||
|
||||
el_val_t engram_activate(el_val_t node_ids, el_val_t spread, el_val_t decay) {
|
||||
(void)node_ids; (void)spread; (void)decay;
|
||||
return el_list_empty();
|
||||
}
|
||||
|
||||
el_val_t engram_activate_json(el_val_t node_ids_json, el_val_t spread, el_val_t decay) {
|
||||
(void)node_ids_json; (void)spread; (void)decay;
|
||||
return EL_STR("[]");
|
||||
}
|
||||
|
||||
el_val_t engram_compile_layered_json(el_val_t node_id, el_val_t target_tokens,
|
||||
el_val_t strategy) {
|
||||
(void)node_id; (void)target_tokens; (void)strategy;
|
||||
return EL_STR("{}");
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// tests/suite/manifest.el — El test suite vessel manifest.
|
||||
//
|
||||
// Build and run:
|
||||
// cd tests/suite && elb && ./dist/el-tests
|
||||
//
|
||||
// Exit code equals the number of failing assertions (0 = all pass).
|
||||
|
||||
package "el-tests" {
|
||||
version "0.1.0"
|
||||
description "El runtime test suite"
|
||||
edition "2026"
|
||||
}
|
||||
|
||||
build {
|
||||
entry "src/test_all.el"
|
||||
output "dist/"
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
// tests/suite/src/test_all.el — master entry point for the El comprehensive test suite.
|
||||
//
|
||||
// This is an El vessel. Build and run with:
|
||||
//
|
||||
// cd tests/suite && elb && ./dist/el-tests
|
||||
//
|
||||
// Exit code equals the number of failing assertions (0 = all pass).
|
||||
|
||||
import "../../../runtime/string.el"
|
||||
import "../../../runtime/math.el"
|
||||
import "../../../runtime/state.el"
|
||||
import "../../../runtime/fs.el"
|
||||
import "../../../runtime/json.el"
|
||||
import "../../../runtime/time.el"
|
||||
import "../../../runtime/thread.el"
|
||||
import "../../../runtime/test.el"
|
||||
import "test_string.el"
|
||||
import "test_math.el"
|
||||
import "test_json.el"
|
||||
import "test_state.el"
|
||||
import "test_time.el"
|
||||
import "test_fs.el"
|
||||
import "test_collections.el"
|
||||
|
||||
fn main() -> Int {
|
||||
// ── string tests ───────────────────────────────────────────────────────────
|
||||
test_case("str_eq basic", "test_str_eq_basic")
|
||||
test_case("str_eq symbols", "test_str_eq_symbols")
|
||||
test_case("str_len basic", "test_str_len_basic")
|
||||
test_case("str_len longer", "test_str_len_longer")
|
||||
test_case("str_concat basic", "test_str_concat_basic")
|
||||
test_case("str_concat chaining", "test_str_concat_chaining")
|
||||
test_case("str_slice basic", "test_str_slice_basic")
|
||||
test_case("str_slice edge", "test_str_slice_edge")
|
||||
test_case("str_starts_with basic", "test_str_starts_with_basic")
|
||||
test_case("str_starts_with edge", "test_str_starts_with_edge")
|
||||
test_case("str_ends_with basic", "test_str_ends_with_basic")
|
||||
test_case("str_ends_with edge", "test_str_ends_with_edge")
|
||||
test_case("str_contains basic", "test_str_contains_basic")
|
||||
test_case("str_contains edge", "test_str_contains_edge")
|
||||
test_case("str_index_of basic", "test_str_index_of_basic")
|
||||
test_case("str_index_of duplicates", "test_str_index_of_duplicates")
|
||||
test_case("str_last_index_of basic", "test_str_last_index_of_basic")
|
||||
test_case("str_replace basic", "test_str_replace_basic")
|
||||
test_case("str_replace multiple", "test_str_replace_multiple")
|
||||
test_case("str_to_upper basic", "test_str_to_upper_basic")
|
||||
test_case("str_to_lower basic", "test_str_to_lower_basic")
|
||||
test_case("str_upper_lower roundtrip", "test_str_upper_lower_roundtrip")
|
||||
test_case("str_trim basic", "test_str_trim_basic")
|
||||
test_case("str_lstrip rstrip", "test_str_lstrip_rstrip")
|
||||
test_case("str_split basic", "test_str_split_basic")
|
||||
test_case("str_split edge", "test_str_split_edge")
|
||||
test_case("str_join basic", "test_str_join_basic")
|
||||
test_case("str_join edge", "test_str_join_edge")
|
||||
test_case("int_to_str basic", "test_int_to_str_basic")
|
||||
test_case("str_to_int basic", "test_str_to_int_basic")
|
||||
test_case("float_to_str basic", "test_float_to_str_basic")
|
||||
test_case("str_to_float basic", "test_str_to_float_basic")
|
||||
test_case("str_repeat basic", "test_str_repeat_basic")
|
||||
test_case("str_reverse basic", "test_str_reverse_basic")
|
||||
test_case("str_count basic", "test_str_count_basic")
|
||||
test_case("str_strip_prefix basic", "test_str_strip_prefix_basic")
|
||||
test_case("str_strip_suffix basic", "test_str_strip_suffix_basic")
|
||||
test_case("str_find_chars basic", "test_str_find_chars_basic")
|
||||
test_case("str_char_at basic", "test_str_char_at_basic")
|
||||
test_case("str_char_code basic", "test_str_char_code_basic")
|
||||
test_case("str_pad_left basic", "test_str_pad_left_basic")
|
||||
test_case("str_pad_right basic", "test_str_pad_right_basic")
|
||||
test_case("is_letter basic", "test_is_letter_basic")
|
||||
test_case("is_digit basic", "test_is_digit_basic")
|
||||
test_case("is_whitespace basic", "test_is_whitespace_basic")
|
||||
test_case("str_count_lines basic", "test_str_count_lines_basic")
|
||||
test_case("str_count_words basic", "test_str_count_words_basic")
|
||||
test_case("url_encode basic", "test_url_encode_basic")
|
||||
test_case("url roundtrip", "test_url_roundtrip")
|
||||
test_case("bool_to_str basic", "test_bool_to_str_basic")
|
||||
test_case("str_to_bytes basic", "test_str_to_bytes_basic")
|
||||
test_case("bytes roundtrip", "test_bytes_roundtrip")
|
||||
|
||||
// ── math tests ─────────────────────────────────────────────────────────────
|
||||
test_case("el_abs basic", "test_el_abs_basic")
|
||||
test_case("el_max basic", "test_el_max_basic")
|
||||
test_case("el_max edge", "test_el_max_edge")
|
||||
test_case("el_min basic", "test_el_min_basic")
|
||||
test_case("el_min edge", "test_el_min_edge")
|
||||
test_case("math_sqrt basic", "test_math_sqrt_basic")
|
||||
test_case("math_sqrt larger", "test_math_sqrt_larger")
|
||||
test_case("math_log basic", "test_math_log_basic")
|
||||
test_case("math_ln basic", "test_math_ln_basic")
|
||||
test_case("math_pi basic", "test_math_pi_basic")
|
||||
test_case("math_sin basic", "test_math_sin_basic")
|
||||
test_case("math_cos basic", "test_math_cos_basic")
|
||||
// int_to_float, float_to_int, format_float, decimal_round omitted:
|
||||
// __int_to_float / __float_to_int / __format_float seed primitives are
|
||||
// not yet implemented in el_seed.c on the runtime/integrate branch.
|
||||
test_case("int arithmetic basic", "test_int_arithmetic_basic")
|
||||
test_case("int arithmetic edge", "test_int_arithmetic_edge")
|
||||
test_case("int arithmetic negative", "test_int_arithmetic_negative")
|
||||
test_case("float arithmetic basic", "test_float_arithmetic_basic")
|
||||
test_case("float comparison basic", "test_float_comparison_basic")
|
||||
|
||||
// ── json tests ─────────────────────────────────────────────────────────────
|
||||
test_case("json_get basic", "test_json_get_basic")
|
||||
test_case("json_get types", "test_json_get_types")
|
||||
test_case("json_get nested", "test_json_get_nested")
|
||||
test_case("json_get empty", "test_json_get_empty")
|
||||
test_case("json_get_int basic", "test_json_get_int_basic")
|
||||
test_case("json_get_bool basic", "test_json_get_bool_basic")
|
||||
test_case("json_get_float basic", "test_json_get_float_basic")
|
||||
test_case("json_set basic", "test_json_set_basic")
|
||||
test_case("json_set numeric", "test_json_set_numeric")
|
||||
test_case("json_set chained", "test_json_set_chained")
|
||||
test_case("json_array_len basic", "test_json_array_len_basic")
|
||||
test_case("json_array_get basic", "test_json_array_get_basic")
|
||||
test_case("json_array_get numbers", "test_json_array_get_numbers")
|
||||
test_case("json_array_get objects", "test_json_array_get_objects")
|
||||
test_case("json_escape_string basic", "test_json_escape_string_basic")
|
||||
test_case("json_escape_string backslash", "test_json_escape_string_backslash")
|
||||
test_case("json_escape roundtrip", "test_json_escape_roundtrip")
|
||||
test_case("json_build_object basic", "test_json_build_object_basic")
|
||||
test_case("json_build_object empty", "test_json_build_object_empty")
|
||||
test_case("json_build_array basic", "test_json_build_array_basic")
|
||||
test_case("json_build_array empty", "test_json_build_array_empty")
|
||||
test_case("json_build_array numbers", "test_json_build_array_numbers")
|
||||
test_case("json_array_push basic", "test_json_array_push_basic")
|
||||
test_case("json_array_push preserves order", "test_json_array_push_preserves_order")
|
||||
test_case("json nested set get", "test_json_nested_set_get")
|
||||
test_case("json array of objects", "test_json_array_of_objects")
|
||||
|
||||
// ── state tests ────────────────────────────────────────────────────────────
|
||||
test_case("state set get basic", "test_state_set_get_basic")
|
||||
test_case("state overwrite", "test_state_overwrite")
|
||||
test_case("state multiple keys", "test_state_multiple_keys")
|
||||
test_case("state missing key", "test_state_missing_key")
|
||||
test_case("state del basic", "test_state_del_basic")
|
||||
test_case("state del and reset", "test_state_del_and_reset")
|
||||
test_case("state has basic", "test_state_has_basic")
|
||||
test_case("state get_or basic", "test_state_get_or_basic")
|
||||
test_case("state cross function", "test_state_cross_function")
|
||||
test_case("state keys basic", "test_state_keys_basic")
|
||||
test_case("state value types", "test_state_value_types")
|
||||
|
||||
// ── time tests ─────────────────────────────────────────────────────────────
|
||||
test_case("time_now basic", "test_time_now_basic")
|
||||
test_case("now_millis basic", "test_now_millis_basic")
|
||||
test_case("unix_timestamp_ms basic", "test_unix_timestamp_ms_basic")
|
||||
test_case("time_now_ms alias", "test_time_now_ms_alias")
|
||||
test_case("time monotonic now_millis", "test_time_monotonic_now_millis")
|
||||
test_case("time monotonic time_now", "test_time_monotonic_time_now")
|
||||
test_case("time monotonic now_ns", "test_time_monotonic_now_ns")
|
||||
test_case("unix_timestamp basic", "test_unix_timestamp_basic")
|
||||
test_case("time_to_parts basic", "test_time_to_parts_basic")
|
||||
test_case("time_to_parts epoch", "test_time_to_parts_epoch")
|
||||
test_case("time_to_parts current", "test_time_to_parts_current")
|
||||
test_case("time_format iso", "test_time_format_iso")
|
||||
test_case("time_format empty", "test_time_format_empty")
|
||||
test_case("time_format strftime", "test_time_format_strftime")
|
||||
test_case("time_add basic", "test_time_add_basic")
|
||||
test_case("time_add multiple", "test_time_add_multiple")
|
||||
test_case("time_diff basic", "test_time_diff_basic")
|
||||
test_case("time_diff larger", "test_time_diff_larger")
|
||||
test_case("time_diff zero", "test_time_diff_zero")
|
||||
test_case("duration helpers basic", "test_duration_helpers_basic")
|
||||
test_case("instant helpers basic", "test_instant_helpers_basic")
|
||||
test_case("uuid_new basic", "test_uuid_new_basic")
|
||||
test_case("uuid uniqueness", "test_uuid_uniqueness")
|
||||
test_case("time_from_parts basic", "test_time_from_parts_basic")
|
||||
test_case("instant_to_iso8601 basic", "test_instant_to_iso8601_basic")
|
||||
|
||||
// ── filesystem tests ───────────────────────────────────────────────────────
|
||||
test_case("fs write read basic", "test_fs_write_read_basic")
|
||||
test_case("fs write read multiline", "test_fs_write_read_multiline")
|
||||
test_case("fs write read empty", "test_fs_write_read_empty")
|
||||
test_case("fs write overwrite", "test_fs_write_overwrite")
|
||||
test_case("fs write large content", "test_fs_write_large_content")
|
||||
test_case("fs exists basic", "test_fs_exists_basic")
|
||||
test_case("fs exists nonexistent", "test_fs_exists_nonexistent")
|
||||
test_case("fs write creates file", "test_fs_write_creates_file")
|
||||
test_case("fs read nonexistent", "test_fs_read_nonexistent")
|
||||
test_case("fs write json", "test_fs_write_json")
|
||||
test_case("fs write json array", "test_fs_write_json_array")
|
||||
test_case("fs multiple files", "test_fs_multiple_files")
|
||||
test_case("fs mkdir basic", "test_fs_mkdir_basic")
|
||||
test_case("fs mkdir write inside", "test_fs_mkdir_write_inside")
|
||||
test_case("fs special chars", "test_fs_special_chars")
|
||||
test_case("fs unicode content", "test_fs_unicode_content")
|
||||
|
||||
// ── collection / list tests ────────────────────────────────────────────────
|
||||
test_case("list empty basic", "test_list_empty_basic")
|
||||
test_case("list empty multiple", "test_list_empty_multiple")
|
||||
test_case("list append single", "test_list_append_single")
|
||||
test_case("list append multiple", "test_list_append_multiple")
|
||||
test_case("list append order", "test_list_append_order")
|
||||
test_case("list append empty string", "test_list_append_empty_string")
|
||||
test_case("list len basic", "test_list_len_basic")
|
||||
test_case("list len large", "test_list_len_large")
|
||||
test_case("list get basic", "test_list_get_basic")
|
||||
test_case("list get preserves content", "test_list_get_preserves_content")
|
||||
test_case("list split join roundtrip", "test_list_split_join_roundtrip")
|
||||
test_case("list build and join", "test_list_build_and_join")
|
||||
test_case("list from split access", "test_list_from_split_access")
|
||||
test_case("native list empty basic", "test_native_list_empty_basic")
|
||||
test_case("native list append basic", "test_native_list_append_basic")
|
||||
test_case("native list join", "test_native_list_join")
|
||||
test_case("env list helpers", "test_env_list_helpers")
|
||||
test_case("list accumulate loop", "test_list_accumulate_loop")
|
||||
test_case("list string building", "test_list_string_building")
|
||||
|
||||
return test_run_all()
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
// tests/suite/test_collections.el — tests for native list operations
|
||||
//
|
||||
// runtime/collections.el does not exist; this file tests the native list
|
||||
// primitives that are always available in El: el_list_empty, el_list_append,
|
||||
// el_list_len, el_list_get.
|
||||
//
|
||||
// These primitives underpin str_split, str_join, and the env.el list helpers.
|
||||
|
||||
// ── el_list_empty ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_list_empty_basic(_: String) -> String {
|
||||
let lst: [String] = el_list_empty()
|
||||
assert_int_eq(el_list_len(lst), 0, "empty list has length 0")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_list_empty_multiple(_: String) -> String {
|
||||
let a: [String] = el_list_empty()
|
||||
let b: [String] = el_list_empty()
|
||||
assert_int_eq(el_list_len(a), 0, "first empty list has length 0")
|
||||
assert_int_eq(el_list_len(b), 0, "second empty list has length 0")
|
||||
// Append to one does not affect the other
|
||||
let a2: [String] = el_list_append(a, "hello")
|
||||
assert_int_eq(el_list_len(a2), 1, "appended list has length 1")
|
||||
assert_int_eq(el_list_len(b), 0, "other empty list unaffected")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── el_list_append ────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_list_append_single(_: String) -> String {
|
||||
let lst: [String] = el_list_empty()
|
||||
let lst2: [String] = el_list_append(lst, "hello")
|
||||
assert_int_eq(el_list_len(lst2), 1, "length is 1 after append")
|
||||
assert_eq(el_list_get(lst2, 0), "hello", "first element is what was appended")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_list_append_multiple(_: String) -> String {
|
||||
let lst: [String] = el_list_empty()
|
||||
let lst = el_list_append(lst, "alpha")
|
||||
let lst = el_list_append(lst, "beta")
|
||||
let lst = el_list_append(lst, "gamma")
|
||||
assert_int_eq(el_list_len(lst), 3, "length 3 after 3 appends")
|
||||
assert_eq(el_list_get(lst, 0), "alpha", "first element: alpha")
|
||||
assert_eq(el_list_get(lst, 1), "beta", "second element: beta")
|
||||
assert_eq(el_list_get(lst, 2), "gamma", "third element: gamma")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_list_append_order(_: String) -> String {
|
||||
let lst: [String] = el_list_empty()
|
||||
let lst = el_list_append(lst, "1")
|
||||
let lst = el_list_append(lst, "2")
|
||||
let lst = el_list_append(lst, "3")
|
||||
let lst = el_list_append(lst, "4")
|
||||
let lst = el_list_append(lst, "5")
|
||||
assert_int_eq(el_list_len(lst), 5, "five elements appended")
|
||||
assert_eq(el_list_get(lst, 0), "1", "order preserved: index 0")
|
||||
assert_eq(el_list_get(lst, 2), "3", "order preserved: index 2")
|
||||
assert_eq(el_list_get(lst, 4), "5", "order preserved: index 4")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_list_append_empty_string(_: String) -> String {
|
||||
let lst: [String] = el_list_empty()
|
||||
let lst = el_list_append(lst, "")
|
||||
assert_int_eq(el_list_len(lst), 1, "empty string element counted")
|
||||
assert_eq(el_list_get(lst, 0), "", "empty string retrievable")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── el_list_len ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_list_len_basic(_: String) -> String {
|
||||
let lst: [String] = el_list_empty()
|
||||
assert_int_eq(el_list_len(lst), 0, "initial length 0")
|
||||
let lst = el_list_append(lst, "a")
|
||||
assert_int_eq(el_list_len(lst), 1, "length 1 after one append")
|
||||
let lst = el_list_append(lst, "b")
|
||||
assert_int_eq(el_list_len(lst), 2, "length 2 after two appends")
|
||||
let lst = el_list_append(lst, "c")
|
||||
assert_int_eq(el_list_len(lst), 3, "length 3 after three appends")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_list_len_large(_: String) -> String {
|
||||
let lst: [String] = el_list_empty()
|
||||
let i: Int = 0
|
||||
while i < 20 {
|
||||
let lst = el_list_append(lst, int_to_str(i))
|
||||
let i = i + 1
|
||||
}
|
||||
assert_int_eq(el_list_len(lst), 20, "length 20 after 20 appends")
|
||||
assert_eq(el_list_get(lst, 0), "0", "first element is 0")
|
||||
assert_eq(el_list_get(lst, 19), "19", "last element is 19")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── el_list_get ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_list_get_basic(_: String) -> String {
|
||||
let lst: [String] = el_list_empty()
|
||||
let lst = el_list_append(lst, "first")
|
||||
let lst = el_list_append(lst, "second")
|
||||
let lst = el_list_append(lst, "third")
|
||||
assert_eq(el_list_get(lst, 0), "first", "get index 0")
|
||||
assert_eq(el_list_get(lst, 1), "second", "get index 1")
|
||||
assert_eq(el_list_get(lst, 2), "third", "get index 2")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_list_get_preserves_content(_: String) -> String {
|
||||
let lst: [String] = el_list_empty()
|
||||
let lst = el_list_append(lst, "hello world")
|
||||
let lst = el_list_append(lst, "{\"json\":\"value\"}")
|
||||
let lst = el_list_append(lst, "line1\nline2")
|
||||
assert_eq(el_list_get(lst, 0), "hello world", "spaces in element preserved")
|
||||
assert_eq(el_list_get(lst, 1), "{\"json\":\"value\"}", "JSON string preserved")
|
||||
assert_eq(el_list_get(lst, 2), "line1\nline2", "newline in element preserved")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── list with str_join / str_split roundtrip ─────────────────────────────────
|
||||
|
||||
fn test_list_split_join_roundtrip(_: String) -> String {
|
||||
let original: String = "apple,banana,cherry"
|
||||
let parts: [String] = str_split(original, ",")
|
||||
assert_int_eq(el_list_len(parts), 3, "split yields 3 parts")
|
||||
let rejoined: String = str_join(parts, ",")
|
||||
assert_eq(rejoined, original, "split then join is identity")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_list_build_and_join(_: String) -> String {
|
||||
let parts: [String] = el_list_empty()
|
||||
let parts = el_list_append(parts, "one")
|
||||
let parts = el_list_append(parts, "two")
|
||||
let parts = el_list_append(parts, "three")
|
||||
let joined: String = str_join(parts, " + ")
|
||||
assert_eq(joined, "one + two + three", "join with multi-char separator")
|
||||
assert_int_eq(el_list_len(parts), 3, "original list unchanged")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_list_from_split_access(_: String) -> String {
|
||||
let parts: [String] = str_split("a:b:c:d:e", ":")
|
||||
assert_int_eq(el_list_len(parts), 5, "split into 5 parts")
|
||||
assert_eq(el_list_get(parts, 0), "a", "first part")
|
||||
assert_eq(el_list_get(parts, 2), "c", "middle part")
|
||||
assert_eq(el_list_get(parts, 4), "e", "last part")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── native_list_empty / native_list_append (aliases from join.el style) ───────
|
||||
|
||||
fn test_native_list_empty_basic(_: String) -> String {
|
||||
let lst: [String] = native_list_empty()
|
||||
assert_int_eq(el_list_len(lst), 0, "native_list_empty yields empty list")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_native_list_append_basic(_: String) -> String {
|
||||
let lst: [String] = native_list_empty()
|
||||
let lst = native_list_append(lst, "hello")
|
||||
let lst = native_list_append(lst, "world")
|
||||
assert_int_eq(el_list_len(lst), 2, "native_list_append: length 2")
|
||||
assert_eq(el_list_get(lst, 0), "hello", "native_list_append: first element")
|
||||
assert_eq(el_list_get(lst, 1), "world", "native_list_append: second element")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_native_list_join(_: String) -> String {
|
||||
let parts: [String] = native_list_empty()
|
||||
let parts = native_list_append(parts, "alpha")
|
||||
let parts = native_list_append(parts, "beta")
|
||||
let parts = native_list_append(parts, "gamma")
|
||||
let result: String = str_join(parts, ", ")
|
||||
assert_eq(result, "alpha, beta, gamma", "native list joined correctly")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── env.el list helpers (get / len) ──────────────────────────────────────────
|
||||
|
||||
fn test_env_list_helpers(_: String) -> String {
|
||||
let lst: [String] = el_list_empty()
|
||||
let lst = el_list_append(lst, "x")
|
||||
let lst = el_list_append(lst, "y")
|
||||
let lst = el_list_append(lst, "z")
|
||||
assert_int_eq(len(lst), 3, "len() alias for el_list_len")
|
||||
assert_eq(get(lst, 0), "x", "get() alias for el_list_get index 0")
|
||||
assert_eq(get(lst, 2), "z", "get() alias for el_list_get index 2")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── list as accumulator pattern ───────────────────────────────────────────────
|
||||
|
||||
fn test_list_accumulate_loop(_: String) -> String {
|
||||
let results: [String] = el_list_empty()
|
||||
let i: Int = 0
|
||||
while i < 5 {
|
||||
let results = el_list_append(results, int_to_str(i * i))
|
||||
let i = i + 1
|
||||
}
|
||||
assert_int_eq(el_list_len(results), 5, "accumulated 5 squares")
|
||||
assert_eq(el_list_get(results, 0), "0", "0^2 = 0")
|
||||
assert_eq(el_list_get(results, 1), "1", "1^2 = 1")
|
||||
assert_eq(el_list_get(results, 2), "4", "2^2 = 4")
|
||||
assert_eq(el_list_get(results, 3), "9", "3^2 = 9")
|
||||
assert_eq(el_list_get(results, 4), "16", "4^2 = 16")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_list_string_building(_: String) -> String {
|
||||
let words: [String] = el_list_empty()
|
||||
let words = el_list_append(words, "the")
|
||||
let words = el_list_append(words, "quick")
|
||||
let words = el_list_append(words, "brown")
|
||||
let words = el_list_append(words, "fox")
|
||||
let sentence: String = str_join(words, " ")
|
||||
assert_eq(sentence, "the quick brown fox", "words joined into sentence")
|
||||
assert_int_eq(str_count_words(sentence), 4, "sentence has 4 words")
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
// tests/suite/test_fs.el — comprehensive tests for runtime/fs.el
|
||||
//
|
||||
// Covers fs_write, fs_read, fs_exists, fs_mkdir. All file paths use /tmp so
|
||||
// no special permissions are needed. Each test uses a unique path to prevent
|
||||
// interference between test cases.
|
||||
|
||||
// ── fs_write / fs_read roundtrip ─────────────────────────────────────────────
|
||||
|
||||
fn test_fs_write_read_basic(_: String) -> String {
|
||||
let path: String = "/tmp/el_suite_test_basic.txt"
|
||||
let content: String = "hello from El tests"
|
||||
fs_write(path, content)
|
||||
let read_back: String = fs_read(path)
|
||||
assert_eq(read_back, content, "read back what was written")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_fs_write_read_multiline(_: String) -> String {
|
||||
let path: String = "/tmp/el_suite_test_multiline.txt"
|
||||
let content: String = "line one\nline two\nline three"
|
||||
fs_write(path, content)
|
||||
let read_back: String = fs_read(path)
|
||||
assert_eq(read_back, content, "multiline content preserved")
|
||||
assert_contains(read_back, "line one", "first line present")
|
||||
assert_contains(read_back, "line two", "second line present")
|
||||
assert_contains(read_back, "line three", "third line present")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_fs_write_read_empty(_: String) -> String {
|
||||
let path: String = "/tmp/el_suite_test_empty.txt"
|
||||
fs_write(path, "")
|
||||
let read_back: String = fs_read(path)
|
||||
// empty file may return "" or a zero-length string
|
||||
assert_int_eq(str_len(read_back), 0, "empty file reads as empty string")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_fs_write_overwrite(_: String) -> String {
|
||||
let path: String = "/tmp/el_suite_test_overwrite.txt"
|
||||
fs_write(path, "first version")
|
||||
let r1: String = fs_read(path)
|
||||
assert_eq(r1, "first version", "first write readable")
|
||||
|
||||
fs_write(path, "second version")
|
||||
let r2: String = fs_read(path)
|
||||
assert_eq(r2, "second version", "second write overwrites first")
|
||||
|
||||
fs_write(path, "third version")
|
||||
let r3: String = fs_read(path)
|
||||
assert_eq(r3, "third version", "third write overwrites second")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_fs_write_large_content(_: String) -> String {
|
||||
let path: String = "/tmp/el_suite_test_large.txt"
|
||||
let big: String = str_repeat("abcdefghij", 100)
|
||||
fs_write(path, big)
|
||||
let read_back: String = fs_read(path)
|
||||
assert_int_eq(str_len(read_back), 1000, "large content length preserved")
|
||||
assert_eq(str_slice(read_back, 0, 10), "abcdefghij", "content starts correctly")
|
||||
assert_eq(str_slice(read_back, 990, 1000), "abcdefghij", "content ends correctly")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── fs_exists ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_fs_exists_basic(_: String) -> String {
|
||||
let path: String = "/tmp/el_suite_test_exists.txt"
|
||||
fs_write(path, "existence test")
|
||||
assert_true(fs_exists(path), "file exists after write")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_fs_exists_nonexistent(_: String) -> String {
|
||||
assert_false(fs_exists("/tmp/el_suite_this_file_does_not_exist_xyz_abc_999.txt"), "nonexistent file returns false")
|
||||
assert_false(fs_exists("/tmp/el_suite_another_missing_file_def.txt"), "another nonexistent file returns false")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_fs_write_creates_file(_: String) -> String {
|
||||
let path: String = "/tmp/el_suite_test_create.txt"
|
||||
// May or may not exist — write creates it
|
||||
fs_write(path, "newly created")
|
||||
assert_true(fs_exists(path), "file exists after creation via write")
|
||||
let content: String = fs_read(path)
|
||||
assert_eq(content, "newly created", "content correct after creation")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── fs_read nonexistent ───────────────────────────────────────────────────────
|
||||
|
||||
fn test_fs_read_nonexistent(_: String) -> String {
|
||||
let result: String = fs_read("/tmp/el_suite_this_absolutely_does_not_exist_xyz.txt")
|
||||
// Per the spec: returns "" or error string; we just verify it doesn't crash
|
||||
// and returns a string (which it must, given the type system)
|
||||
assert_true(str_len(result) >= 0, "reading nonexistent file returns a string")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── JSON content stored in files ─────────────────────────────────────────────
|
||||
|
||||
fn test_fs_write_json(_: String) -> String {
|
||||
let path: String = "/tmp/el_suite_test_json.txt"
|
||||
let json_data: String = "{\"name\":\"alice\",\"score\":42}"
|
||||
fs_write(path, json_data)
|
||||
let read_back: String = fs_read(path)
|
||||
assert_eq(json_get(read_back, "name"), "alice", "JSON name field after file roundtrip")
|
||||
assert_eq(json_get(read_back, "score"), "42", "JSON score field after file roundtrip")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_fs_write_json_array(_: String) -> String {
|
||||
let path: String = "/tmp/el_suite_test_json_arr.txt"
|
||||
let arr_data: String = "[\"alpha\",\"beta\",\"gamma\"]"
|
||||
fs_write(path, arr_data)
|
||||
let read_back: String = fs_read(path)
|
||||
assert_int_eq(json_array_len(read_back), 3, "JSON array length after file roundtrip")
|
||||
assert_eq(json_array_get_string(read_back, 0), "alpha", "first element after roundtrip")
|
||||
assert_eq(json_array_get_string(read_back, 2), "gamma", "last element after roundtrip")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── multiple independent files ────────────────────────────────────────────────
|
||||
|
||||
fn test_fs_multiple_files(_: String) -> String {
|
||||
let path_a: String = "/tmp/el_suite_multi_a.txt"
|
||||
let path_b: String = "/tmp/el_suite_multi_b.txt"
|
||||
let path_c: String = "/tmp/el_suite_multi_c.txt"
|
||||
|
||||
fs_write(path_a, "content A")
|
||||
fs_write(path_b, "content B")
|
||||
fs_write(path_c, "content C")
|
||||
|
||||
assert_eq(fs_read(path_a), "content A", "file A reads correctly")
|
||||
assert_eq(fs_read(path_b), "content B", "file B reads correctly")
|
||||
assert_eq(fs_read(path_c), "content C", "file C reads correctly")
|
||||
|
||||
assert_true(fs_exists(path_a), "file A exists")
|
||||
assert_true(fs_exists(path_b), "file B exists")
|
||||
assert_true(fs_exists(path_c), "file C exists")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── fs_mkdir ──────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_fs_mkdir_basic(_: String) -> String {
|
||||
let dir_path: String = "/tmp/el_suite_mkdir_test"
|
||||
let result: Bool = fs_mkdir(dir_path)
|
||||
// fs_mkdir returns Bool; the dir should now exist
|
||||
assert_true(fs_exists(dir_path), "directory exists after mkdir")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_fs_mkdir_write_inside(_: String) -> String {
|
||||
let dir_path: String = "/tmp/el_suite_mkdir_subdir"
|
||||
fs_mkdir(dir_path)
|
||||
let file_path: String = dir_path + "/test_file.txt"
|
||||
fs_write(file_path, "inside directory")
|
||||
assert_true(fs_exists(file_path), "file inside mkdir'd directory exists")
|
||||
assert_eq(fs_read(file_path), "inside directory", "file inside directory readable")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── special characters in content ────────────────────────────────────────────
|
||||
|
||||
fn test_fs_special_chars(_: String) -> String {
|
||||
let path: String = "/tmp/el_suite_test_special.txt"
|
||||
let content: String = "tab:\there\nnewline above\r\nwindows newline"
|
||||
fs_write(path, content)
|
||||
let read_back: String = fs_read(path)
|
||||
assert_eq(read_back, content, "special chars preserved in file")
|
||||
assert_contains(read_back, "\t", "tab preserved")
|
||||
assert_contains(read_back, "\n", "newline preserved")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_fs_unicode_content(_: String) -> String {
|
||||
let path: String = "/tmp/el_suite_test_unicode.txt"
|
||||
// Use ASCII art instead of actual unicode for max compatibility
|
||||
let content: String = "hello-world-test"
|
||||
fs_write(path, content)
|
||||
let read_back: String = fs_read(path)
|
||||
assert_eq(read_back, content, "content preserved")
|
||||
assert_int_eq(str_len(read_back), str_len(content), "length preserved")
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
// tests/suite/test_json.el — comprehensive tests for runtime/json.el
|
||||
//
|
||||
// Covers json_get, json_get_raw, json_set, json_array_len, json_array_get,
|
||||
// json_array_get_string, json_escape_string, json_build_object, json_build_array,
|
||||
// json_array_push, typed extractors, bytes_to_str, and nested JSON.
|
||||
|
||||
// ── json_get ──────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_json_get_basic(_: String) -> String {
|
||||
let obj: String = "{\"name\":\"alice\",\"age\":\"30\"}"
|
||||
assert_eq(json_get(obj, "name"), "alice", "get string field")
|
||||
assert_eq(json_get(obj, "age"), "30", "get numeric-looking string field")
|
||||
assert_eq(json_get(obj, "missing"), "", "missing key returns empty string")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_get_types(_: String) -> String {
|
||||
let obj: String = "{\"count\":42,\"flag\":true,\"ratio\":3.14}"
|
||||
assert_eq(json_get(obj, "count"), "42", "get integer field")
|
||||
assert_eq(json_get(obj, "flag"), "true", "get boolean field")
|
||||
assert_eq(json_get(obj, "ratio"), "3.14", "get float field")
|
||||
assert_eq(json_get(obj, "absent"), "", "absent key yields empty")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_get_nested(_: String) -> String {
|
||||
let obj: String = "{\"user\":{\"name\":\"bob\",\"role\":\"admin\"}}"
|
||||
assert_eq(json_get(obj, "user.name"), "bob", "dot-path traversal: user.name")
|
||||
assert_eq(json_get(obj, "user.role"), "admin", "dot-path traversal: user.role")
|
||||
assert_eq(json_get(obj, "user.missing"), "", "missing nested key")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_get_empty(_: String) -> String {
|
||||
let empty_obj: String = "{}"
|
||||
assert_eq(json_get(empty_obj, "key"), "", "empty object: any key is missing")
|
||||
assert_eq(json_get(empty_obj, "a.b"), "", "empty object: nested path missing")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── json_get typed extractors ────────────────────────────────────────────────
|
||||
|
||||
fn test_json_get_int_basic(_: String) -> String {
|
||||
let obj: String = "{\"count\":42,\"negative\":-7,\"zero\":0}"
|
||||
assert_int_eq(json_get_int(obj, "count"), 42, "get int field 42")
|
||||
assert_int_eq(json_get_int(obj, "negative"), -7, "get negative int field")
|
||||
assert_int_eq(json_get_int(obj, "zero"), 0, "get zero int field")
|
||||
assert_int_eq(json_get_int(obj, "missing"), 0, "missing int field returns 0")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_get_bool_basic(_: String) -> String {
|
||||
let obj: String = "{\"ok\":true,\"fail\":false}"
|
||||
assert_true(json_get_bool(obj, "ok"), "get true bool field")
|
||||
assert_false(json_get_bool(obj, "fail"), "get false bool field")
|
||||
assert_false(json_get_bool(obj, "missing"), "missing bool field returns false")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_get_float_basic(_: String) -> String {
|
||||
let obj: String = "{\"ratio\":3.14,\"negative\":-1.5}"
|
||||
let ratio: Float = json_get_float(obj, "ratio")
|
||||
assert_true(ratio > 3.13, "ratio > 3.13")
|
||||
assert_true(ratio < 3.15, "ratio < 3.15")
|
||||
let neg: Float = json_get_float(obj, "negative")
|
||||
assert_true(neg < -1.4, "negative float")
|
||||
assert_true(neg > -1.6, "negative float bound")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── json_set ──────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_json_set_basic(_: String) -> String {
|
||||
let obj: String = "{\"name\":\"alice\"}"
|
||||
let obj2: String = json_set(obj, "name", "\"bob\"")
|
||||
assert_eq(json_get(obj2, "name"), "bob", "overwrites existing key")
|
||||
|
||||
let obj3: String = json_set(obj, "role", "\"admin\"")
|
||||
assert_eq(json_get(obj3, "role"), "admin", "inserts new key")
|
||||
assert_eq(json_get(obj3, "name"), "alice", "existing key unchanged")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_set_numeric(_: String) -> String {
|
||||
let obj: String = "{}"
|
||||
let obj2: String = json_set(obj, "count", "42")
|
||||
assert_eq(json_get(obj2, "count"), "42", "set integer value")
|
||||
|
||||
let obj3: String = json_set(obj2, "count", "100")
|
||||
assert_eq(json_get(obj3, "count"), "100", "overwrites integer value")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_set_chained(_: String) -> String {
|
||||
let obj: String = "{}"
|
||||
let obj1: String = json_set(obj, "a", "\"1\"")
|
||||
let obj2: String = json_set(obj1, "b", "\"2\"")
|
||||
let obj3: String = json_set(obj2, "c", "\"3\"")
|
||||
assert_eq(json_get(obj3, "a"), "1", "chained set: a")
|
||||
assert_eq(json_get(obj3, "b"), "2", "chained set: b")
|
||||
assert_eq(json_get(obj3, "c"), "3", "chained set: c")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── json_array_len ────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_json_array_len_basic(_: String) -> String {
|
||||
assert_int_eq(json_array_len("[]"), 0, "empty array has length 0")
|
||||
assert_int_eq(json_array_len("[1]"), 1, "single element array")
|
||||
assert_int_eq(json_array_len("[1,2,3]"), 3, "three element array")
|
||||
assert_int_eq(json_array_len("[\"a\",\"b\"]"), 2, "string array length")
|
||||
assert_int_eq(json_array_len("[1,2,3,4,5]"), 5, "five element array")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── json_array_get / json_array_get_string ────────────────────────────────────
|
||||
|
||||
fn test_json_array_get_basic(_: String) -> String {
|
||||
let arr: String = "[\"alpha\",\"beta\",\"gamma\"]"
|
||||
assert_eq(json_array_get_string(arr, 0), "alpha", "first element")
|
||||
assert_eq(json_array_get_string(arr, 1), "beta", "second element")
|
||||
assert_eq(json_array_get_string(arr, 2), "gamma", "third element")
|
||||
assert_eq(json_array_get_string(arr, 3), "", "out of bounds returns empty")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_array_get_numbers(_: String) -> String {
|
||||
let arr: String = "[10,20,30]"
|
||||
assert_eq(json_array_get(arr, 0), "10", "first number as fragment")
|
||||
assert_eq(json_array_get(arr, 1), "20", "second number as fragment")
|
||||
assert_eq(json_array_get(arr, 2), "30", "third number as fragment")
|
||||
assert_int_eq(str_to_int(json_array_get(arr, 0)), 10, "convert fragment to int")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_array_get_objects(_: String) -> String {
|
||||
let arr: String = "[{\"name\":\"alice\"},{\"name\":\"bob\"}]"
|
||||
let first: String = json_array_get(arr, 0)
|
||||
let second: String = json_array_get(arr, 1)
|
||||
assert_eq(json_get(first, "name"), "alice", "first object name")
|
||||
assert_eq(json_get(second, "name"), "bob", "second object name")
|
||||
assert_int_eq(json_array_len(arr), 2, "array of objects has correct length")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── json_escape_string ────────────────────────────────────────────────────────
|
||||
|
||||
fn test_json_escape_string_basic(_: String) -> String {
|
||||
assert_eq(json_escape_string("hello"), "hello", "plain string unchanged")
|
||||
assert_eq(json_escape_string(""), "", "empty string unchanged")
|
||||
assert_contains(json_escape_string("say \"hello\""), "\\\"", "double quotes escaped")
|
||||
assert_contains(json_escape_string("line1\nline2"), "\\n", "newline escaped")
|
||||
assert_contains(json_escape_string("col1\tcol2"), "\\t", "tab escaped")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_escape_string_backslash(_: String) -> String {
|
||||
let escaped: String = json_escape_string("a\\b")
|
||||
assert_contains(escaped, "\\\\", "backslash doubled")
|
||||
assert_true(str_len(escaped) > 3, "escaped is longer than original")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_escape_roundtrip(_: String) -> String {
|
||||
// Build a JSON string with an escaped value and extract it back
|
||||
let raw: String = "hello world"
|
||||
let escaped: String = json_escape_string(raw)
|
||||
let json_str: String = "{\"msg\":\"" + escaped + "\"}"
|
||||
assert_eq(json_get(json_str, "msg"), raw, "roundtrip: escape then parse")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── json_build_object ─────────────────────────────────────────────────────────
|
||||
|
||||
fn test_json_build_object_basic(_: String) -> String {
|
||||
let parts: [String] = el_list_empty()
|
||||
let parts = el_list_append(parts, "name")
|
||||
let parts = el_list_append(parts, "alice")
|
||||
let parts = el_list_append(parts, "role")
|
||||
let parts = el_list_append(parts, "admin")
|
||||
let obj: String = json_build_object(parts)
|
||||
assert_eq(json_get(obj, "name"), "alice", "build object: name field")
|
||||
assert_eq(json_get(obj, "role"), "admin", "build object: role field")
|
||||
assert_true(str_starts_with(obj, "{"), "build object starts with {")
|
||||
assert_true(str_ends_with(obj, "}"), "build object ends with }")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_build_object_empty(_: String) -> String {
|
||||
let parts: [String] = el_list_empty()
|
||||
let obj: String = json_build_object(parts)
|
||||
assert_true(str_starts_with(obj, "{"), "empty build object starts with {")
|
||||
assert_true(str_ends_with(obj, "}"), "empty build object ends with }")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── json_build_array ──────────────────────────────────────────────────────────
|
||||
|
||||
fn test_json_build_array_basic(_: String) -> String {
|
||||
let items: [String] = el_list_empty()
|
||||
let items = el_list_append(items, "\"alice\"")
|
||||
let items = el_list_append(items, "\"bob\"")
|
||||
let arr: String = json_build_array(items)
|
||||
assert_int_eq(json_array_len(arr), 2, "built array has 2 elements")
|
||||
assert_eq(json_array_get_string(arr, 0), "alice", "first element of built array")
|
||||
assert_eq(json_array_get_string(arr, 1), "bob", "second element of built array")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_build_array_empty(_: String) -> String {
|
||||
let items: [String] = el_list_empty()
|
||||
let arr: String = json_build_array(items)
|
||||
assert_int_eq(json_array_len(arr), 0, "empty built array has 0 elements")
|
||||
assert_eq(arr, "[]", "empty array is []")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_build_array_numbers(_: String) -> String {
|
||||
let items: [String] = el_list_empty()
|
||||
let items = el_list_append(items, "1")
|
||||
let items = el_list_append(items, "2")
|
||||
let items = el_list_append(items, "3")
|
||||
let arr: String = json_build_array(items)
|
||||
assert_int_eq(json_array_len(arr), 3, "numeric array has 3 elements")
|
||||
assert_eq(json_array_get(arr, 0), "1", "first numeric element")
|
||||
assert_eq(json_array_get(arr, 2), "3", "last numeric element")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── json_array_push ───────────────────────────────────────────────────────────
|
||||
|
||||
fn test_json_array_push_basic(_: String) -> String {
|
||||
let arr: String = "[]"
|
||||
let arr2: String = json_array_push(arr, "\"hello\"")
|
||||
assert_int_eq(json_array_len(arr2), 1, "after push: length 1")
|
||||
assert_eq(json_array_get_string(arr2, 0), "hello", "pushed element accessible")
|
||||
|
||||
let arr3: String = json_array_push(arr2, "\"world\"")
|
||||
assert_int_eq(json_array_len(arr3), 2, "after second push: length 2")
|
||||
assert_eq(json_array_get_string(arr3, 1), "world", "second pushed element")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_array_push_preserves_order(_: String) -> String {
|
||||
let arr: String = "[\"a\",\"b\"]"
|
||||
let arr2: String = json_array_push(arr, "\"c\"")
|
||||
assert_eq(json_array_get_string(arr2, 0), "a", "first element preserved")
|
||||
assert_eq(json_array_get_string(arr2, 1), "b", "second element preserved")
|
||||
assert_eq(json_array_get_string(arr2, 2), "c", "pushed element at end")
|
||||
assert_int_eq(json_array_len(arr2), 3, "total length after push")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── nested JSON ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_json_nested_set_get(_: String) -> String {
|
||||
let obj: String = "{\"user\":{\"name\":\"alice\"},\"count\":1}"
|
||||
assert_eq(json_get(obj, "user.name"), "alice", "deep get via dot path")
|
||||
assert_eq(json_get(obj, "count"), "1", "top-level int field")
|
||||
|
||||
let obj2: String = json_set(obj, "count", "2")
|
||||
assert_eq(json_get(obj2, "count"), "2", "updated top-level field")
|
||||
assert_eq(json_get(obj2, "user.name"), "alice", "nested field still accessible after set")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_json_array_of_objects(_: String) -> String {
|
||||
let arr: String = "[{\"id\":1,\"name\":\"alice\"},{\"id\":2,\"name\":\"bob\"}]"
|
||||
assert_int_eq(json_array_len(arr), 2, "array of objects: length 2")
|
||||
let first: String = json_array_get(arr, 0)
|
||||
let second: String = json_array_get(arr, 1)
|
||||
assert_eq(json_get(first, "name"), "alice", "first object name")
|
||||
assert_eq(json_get(second, "name"), "bob", "second object name")
|
||||
assert_eq(json_get(first, "id"), "1", "first object id")
|
||||
assert_eq(json_get(second, "id"), "2", "second object id")
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
// tests/suite/test_math.el — comprehensive tests for runtime/math.el
|
||||
//
|
||||
// Covers every public function in math.el: integer utilities, float math,
|
||||
// conversions, and rounding. Import after runtime modules and test.el.
|
||||
|
||||
// ── el_abs ────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_el_abs_basic(_: String) -> String {
|
||||
assert_int_eq(el_abs(5), 5, "positive stays positive")
|
||||
assert_int_eq(el_abs(-5), 5, "negative becomes positive")
|
||||
assert_int_eq(el_abs(0), 0, "zero stays zero")
|
||||
assert_int_eq(el_abs(-1), 1, "negative one becomes one")
|
||||
assert_int_eq(el_abs(1000000), 1000000, "large positive unchanged")
|
||||
assert_int_eq(el_abs(-1000000), 1000000, "large negative becomes positive")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── el_max ────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_el_max_basic(_: String) -> String {
|
||||
assert_int_eq(el_max(3, 5), 5, "5 is larger than 3")
|
||||
assert_int_eq(el_max(5, 3), 5, "5 is larger than 3 (reversed args)")
|
||||
assert_int_eq(el_max(4, 4), 4, "equal values returns the value")
|
||||
assert_int_eq(el_max(-1, -5), -1, "larger of two negatives")
|
||||
assert_int_eq(el_max(0, -1), 0, "zero is larger than negative")
|
||||
assert_int_eq(el_max(-1, 0), 0, "zero is larger than negative (reversed)")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_el_max_edge(_: String) -> String {
|
||||
assert_int_eq(el_max(0, 0), 0, "max of zeros is zero")
|
||||
assert_int_eq(el_max(1000000, 999999), 1000000, "large values")
|
||||
assert_int_eq(el_max(-1000000, 1000000), 1000000, "mixed sign large values")
|
||||
assert_int_eq(el_max(1, 2), 2, "sequential integers")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── el_min ────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_el_min_basic(_: String) -> String {
|
||||
assert_int_eq(el_min(3, 5), 3, "3 is smaller than 5")
|
||||
assert_int_eq(el_min(5, 3), 3, "3 is smaller than 5 (reversed args)")
|
||||
assert_int_eq(el_min(4, 4), 4, "equal values returns the value")
|
||||
assert_int_eq(el_min(-1, -5), -5, "smaller of two negatives")
|
||||
assert_int_eq(el_min(0, -1), -1, "negative is smaller than zero")
|
||||
assert_int_eq(el_min(-1, 0), -1, "negative is smaller than zero (reversed)")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_el_min_edge(_: String) -> String {
|
||||
assert_int_eq(el_min(0, 0), 0, "min of zeros is zero")
|
||||
assert_int_eq(el_min(1000000, 999999), 999999, "large values")
|
||||
assert_int_eq(el_min(-1000000, 1000000), -1000000, "mixed sign large values")
|
||||
assert_int_eq(el_min(1, 2), 1, "sequential integers")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── math_sqrt ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_math_sqrt_basic(_: String) -> String {
|
||||
let r: Float = math_sqrt(4.0)
|
||||
assert_true(r > 1.99, "sqrt(4) > 1.99")
|
||||
assert_true(r < 2.01, "sqrt(4) < 2.01")
|
||||
|
||||
let r9: Float = math_sqrt(9.0)
|
||||
assert_true(r9 > 2.99, "sqrt(9) > 2.99")
|
||||
assert_true(r9 < 3.01, "sqrt(9) < 3.01")
|
||||
|
||||
let r1: Float = math_sqrt(1.0)
|
||||
assert_true(r1 > 0.99, "sqrt(1) is close to 1")
|
||||
assert_true(r1 < 1.01, "sqrt(1) is close to 1")
|
||||
|
||||
let r0: Float = math_sqrt(0.0)
|
||||
assert_true(r0 >= 0.0, "sqrt(0) is non-negative")
|
||||
assert_true(r0 < 0.001, "sqrt(0) is close to 0")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_math_sqrt_larger(_: String) -> String {
|
||||
let r25: Float = math_sqrt(25.0)
|
||||
assert_true(r25 > 4.99, "sqrt(25) > 4.99")
|
||||
assert_true(r25 < 5.01, "sqrt(25) < 5.01")
|
||||
|
||||
let r100: Float = math_sqrt(100.0)
|
||||
assert_true(r100 > 9.99, "sqrt(100) > 9.99")
|
||||
assert_true(r100 < 10.01, "sqrt(100) < 10.01")
|
||||
|
||||
let r2: Float = math_sqrt(2.0)
|
||||
assert_true(r2 > 1.41, "sqrt(2) > 1.41")
|
||||
assert_true(r2 < 1.43, "sqrt(2) < 1.43")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── math_log / math_ln ───────────────────────────────────────────────────────
|
||||
|
||||
fn test_math_log_basic(_: String) -> String {
|
||||
let log10: Float = math_log(10.0)
|
||||
assert_true(log10 > 0.99, "log(10) > 0.99")
|
||||
assert_true(log10 < 1.01, "log(10) < 1.01")
|
||||
|
||||
let log1: Float = math_log(1.0)
|
||||
assert_true(log1 > -0.001, "log(1) is close to 0")
|
||||
assert_true(log1 < 0.001, "log(1) is close to 0")
|
||||
|
||||
let log100: Float = math_log(100.0)
|
||||
assert_true(log100 > 1.99, "log(100) > 1.99")
|
||||
assert_true(log100 < 2.01, "log(100) < 2.01")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_math_ln_basic(_: String) -> String {
|
||||
let lne: Float = math_ln(2.718281828)
|
||||
assert_true(lne > 0.99, "ln(e) is close to 1")
|
||||
assert_true(lne < 1.01, "ln(e) is close to 1")
|
||||
|
||||
let ln1: Float = math_ln(1.0)
|
||||
assert_true(ln1 > -0.001, "ln(1) is close to 0")
|
||||
assert_true(ln1 < 0.001, "ln(1) is close to 0")
|
||||
|
||||
let ln10: Float = math_ln(10.0)
|
||||
assert_true(ln10 > 2.30, "ln(10) > 2.30")
|
||||
assert_true(ln10 < 2.31, "ln(10) < 2.31")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── math_sin / math_cos / math_pi ────────────────────────────────────────────
|
||||
|
||||
fn test_math_pi_basic(_: String) -> String {
|
||||
let pi: Float = math_pi()
|
||||
assert_true(pi > 3.14, "pi > 3.14")
|
||||
assert_true(pi < 3.15, "pi < 3.15")
|
||||
assert_true(pi > 0.0, "pi is positive")
|
||||
assert_true(pi == math_pi(), "pi is constant across calls")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_math_sin_basic(_: String) -> String {
|
||||
let sin0: Float = math_sin(0.0)
|
||||
assert_true(sin0 > -0.001, "sin(0) is close to 0")
|
||||
assert_true(sin0 < 0.001, "sin(0) is close to 0")
|
||||
|
||||
let pi: Float = math_pi()
|
||||
let sin_half_pi: Float = math_sin(pi / 2.0)
|
||||
assert_true(sin_half_pi > 0.99, "sin(pi/2) is close to 1")
|
||||
assert_true(sin_half_pi < 1.01, "sin(pi/2) is close to 1")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_math_cos_basic(_: String) -> String {
|
||||
let cos0: Float = math_cos(0.0)
|
||||
assert_true(cos0 > 0.99, "cos(0) is close to 1")
|
||||
assert_true(cos0 < 1.01, "cos(0) is close to 1")
|
||||
|
||||
let pi: Float = math_pi()
|
||||
let cos_pi: Float = math_cos(pi)
|
||||
assert_true(cos_pi < -0.99, "cos(pi) is close to -1")
|
||||
assert_true(cos_pi > -1.01, "cos(pi) is close to -1")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── int_to_float / float_to_int ──────────────────────────────────────────────
|
||||
//
|
||||
// NOTE: int_to_float, float_to_int, format_float, and decimal_round use seed
|
||||
// primitives (__int_to_float, __float_to_int, __format_float) that are not yet
|
||||
// implemented in el_seed.c on the runtime/integrate branch. These tests are
|
||||
// omitted until those seed primitives are available. The float arithmetic and
|
||||
// comparison tests below exercise the float type via language builtins instead.
|
||||
|
||||
// ── Integer arithmetic (language built-ins) ───────────────────────────────────
|
||||
|
||||
fn test_int_arithmetic_basic(_: String) -> String {
|
||||
assert_int_eq(2 + 3, 5, "addition")
|
||||
assert_int_eq(10 - 4, 6, "subtraction")
|
||||
assert_int_eq(3 * 4, 12, "multiplication")
|
||||
assert_int_eq(10 / 3, 3, "integer division truncates")
|
||||
assert_int_eq(10 % 3, 1, "modulo")
|
||||
assert_int_eq(0 - 5, -5, "negation via subtraction")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_int_arithmetic_edge(_: String) -> String {
|
||||
assert_int_eq(0 + 0, 0, "zero plus zero")
|
||||
assert_int_eq(0 * 100, 0, "zero times anything")
|
||||
assert_int_eq(1 * 1, 1, "one times one")
|
||||
assert_int_eq(100 / 100, 1, "divide by self")
|
||||
assert_int_eq(7 % 7, 0, "self modulo is zero")
|
||||
assert_int_eq(-5 + 5, 0, "additive inverse")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_int_arithmetic_negative(_: String) -> String {
|
||||
assert_int_eq(-3 + -2, -5, "negative addition")
|
||||
assert_int_eq(-3 * 4, -12, "negative times positive")
|
||||
assert_int_eq(-10 / 3, -3, "negative division truncates toward zero")
|
||||
assert_int_eq(0 - 1000, -1000, "large negative")
|
||||
assert_int_eq(-1 * -1, 1, "negative times negative is positive")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── Float arithmetic (language built-ins) ────────────────────────────────────
|
||||
|
||||
fn test_float_arithmetic_basic(_: String) -> String {
|
||||
let a: Float = 1.5 + 2.5
|
||||
assert_true(a > 3.99, "1.5 + 2.5 = 4.0")
|
||||
assert_true(a < 4.01, "1.5 + 2.5 = 4.0")
|
||||
|
||||
let b: Float = 5.0 - 2.5
|
||||
assert_true(b > 2.49, "5.0 - 2.5 = 2.5")
|
||||
assert_true(b < 2.51, "5.0 - 2.5 = 2.5")
|
||||
|
||||
let c: Float = 2.0 * 3.0
|
||||
assert_true(c > 5.99, "2.0 * 3.0 = 6.0")
|
||||
assert_true(c < 6.01, "2.0 * 3.0 = 6.0")
|
||||
|
||||
let d: Float = 9.0 / 3.0
|
||||
assert_true(d > 2.99, "9.0 / 3.0 = 3.0")
|
||||
assert_true(d < 3.01, "9.0 / 3.0 = 3.0")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_float_comparison_basic(_: String) -> String {
|
||||
assert_true(1.0 < 2.0, "1.0 < 2.0")
|
||||
assert_true(2.0 > 1.0, "2.0 > 1.0")
|
||||
assert_false(1.0 > 2.0, "1.0 not > 2.0")
|
||||
assert_false(2.0 < 1.0, "2.0 not < 1.0")
|
||||
assert_true(1.5 >= 1.5, "1.5 >= 1.5")
|
||||
assert_true(1.5 <= 1.5, "1.5 <= 1.5")
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
// tests/suite/test_state.el — comprehensive tests for runtime/state.el
|
||||
//
|
||||
// Covers state_set, state_get, state_del, state_keys, state_has, state_get_or.
|
||||
// Uses unique key prefixes to avoid interference with the test framework's
|
||||
// own state keys (which use _test_ prefix).
|
||||
|
||||
// ── state_set / state_get roundtrip ───────────────────────────────────────────
|
||||
|
||||
fn test_state_set_get_basic(_: String) -> String {
|
||||
state_set("suite_key1", "hello")
|
||||
assert_eq(state_get("suite_key1"), "hello", "get back what was set")
|
||||
|
||||
state_set("suite_key2", "world")
|
||||
assert_eq(state_get("suite_key2"), "world", "second key independent")
|
||||
|
||||
state_set("suite_num", "42")
|
||||
assert_eq(state_get("suite_num"), "42", "numeric string value")
|
||||
|
||||
state_set("suite_empty", "")
|
||||
// empty value and missing key both return "" — that's by design per the docs
|
||||
let v: String = state_get("suite_empty")
|
||||
assert_true(str_eq(v, "") || str_eq(v, ""), "empty value stored or treated as absent")
|
||||
|
||||
state_set("suite_long", str_repeat("a", 100))
|
||||
assert_int_eq(str_len(state_get("suite_long")), 100, "long value preserved")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_state_overwrite(_: String) -> String {
|
||||
state_set("suite_over", "first")
|
||||
assert_eq(state_get("suite_over"), "first", "initial value")
|
||||
state_set("suite_over", "second")
|
||||
assert_eq(state_get("suite_over"), "second", "value overwritten")
|
||||
state_set("suite_over", "third")
|
||||
assert_eq(state_get("suite_over"), "third", "value overwritten again")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_state_multiple_keys(_: String) -> String {
|
||||
state_set("suite_mk_a", "alpha")
|
||||
state_set("suite_mk_b", "beta")
|
||||
state_set("suite_mk_c", "gamma")
|
||||
assert_eq(state_get("suite_mk_a"), "alpha", "key a is alpha")
|
||||
assert_eq(state_get("suite_mk_b"), "beta", "key b is beta")
|
||||
assert_eq(state_get("suite_mk_c"), "gamma", "key c is gamma")
|
||||
// Mutate one, others unchanged
|
||||
state_set("suite_mk_b", "BETA")
|
||||
assert_eq(state_get("suite_mk_a"), "alpha", "a unchanged after b mutated")
|
||||
assert_eq(state_get("suite_mk_b"), "BETA", "b updated")
|
||||
assert_eq(state_get("suite_mk_c"), "gamma", "c unchanged after b mutated")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── missing keys ──────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_state_missing_key(_: String) -> String {
|
||||
assert_eq(state_get("suite_definitely_not_set_xyz_123"), "", "missing key returns empty string")
|
||||
assert_eq(state_get("suite_another_missing_key_abc"), "", "another missing key returns empty string")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── state_del ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_state_del_basic(_: String) -> String {
|
||||
state_set("suite_del1", "value_to_delete")
|
||||
assert_eq(state_get("suite_del1"), "value_to_delete", "key present before delete")
|
||||
state_del("suite_del1")
|
||||
assert_eq(state_get("suite_del1"), "", "key absent after delete")
|
||||
|
||||
// Delete a non-existent key should not error
|
||||
state_del("suite_never_existed_key_xyz")
|
||||
assert_eq(state_get("suite_never_existed_key_xyz"), "", "delete non-existent is no-op")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_state_del_and_reset(_: String) -> String {
|
||||
state_set("suite_reuse", "original")
|
||||
assert_eq(state_get("suite_reuse"), "original", "original value")
|
||||
state_del("suite_reuse")
|
||||
assert_eq(state_get("suite_reuse"), "", "deleted")
|
||||
state_set("suite_reuse", "new_value")
|
||||
assert_eq(state_get("suite_reuse"), "new_value", "key reused after delete")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── state_has ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_state_has_basic(_: String) -> String {
|
||||
state_set("suite_has1", "something")
|
||||
assert_true(state_has("suite_has1"), "key with value is present")
|
||||
assert_false(state_has("suite_definitely_absent_xyz_999"), "absent key returns false")
|
||||
|
||||
state_del("suite_has1")
|
||||
assert_false(state_has("suite_has1"), "deleted key is not present")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── state_get_or ──────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_state_get_or_basic(_: String) -> String {
|
||||
state_set("suite_gor1", "present")
|
||||
assert_eq(state_get_or("suite_gor1", "default"), "present", "returns value when key exists")
|
||||
assert_eq(state_get_or("suite_gor_missing_xyz", "fallback"), "fallback", "returns default when key missing")
|
||||
assert_eq(state_get_or("suite_gor_missing_xyz", ""), "", "returns empty default when key missing")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── state persists across function calls ──────────────────────────────────────
|
||||
|
||||
fn _suite_state_helper_write() {
|
||||
state_set("suite_persist_test", "written_by_helper")
|
||||
}
|
||||
|
||||
fn _suite_state_helper_increment() {
|
||||
let n: Int = str_to_int(state_get("suite_counter"))
|
||||
state_set("suite_counter", int_to_str(n + 1))
|
||||
}
|
||||
|
||||
fn test_state_cross_function(_: String) -> String {
|
||||
_suite_state_helper_write()
|
||||
assert_eq(state_get("suite_persist_test"), "written_by_helper", "value written by helper function is readable")
|
||||
|
||||
state_set("suite_counter", "0")
|
||||
_suite_state_helper_increment()
|
||||
assert_eq(state_get("suite_counter"), "1", "counter after 1 increment")
|
||||
_suite_state_helper_increment()
|
||||
assert_eq(state_get("suite_counter"), "2", "counter after 2 increments")
|
||||
_suite_state_helper_increment()
|
||||
assert_eq(state_get("suite_counter"), "3", "counter after 3 increments")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── state_keys ────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_state_keys_basic(_: String) -> String {
|
||||
state_set("suite_sk_x", "1")
|
||||
state_set("suite_sk_y", "2")
|
||||
let keys: String = state_keys()
|
||||
// keys is a JSON array of all state keys — must contain our test keys
|
||||
assert_true(str_contains(keys, "suite_sk_x"), "keys includes suite_sk_x")
|
||||
assert_true(str_contains(keys, "suite_sk_y"), "keys includes suite_sk_y")
|
||||
assert_true(str_starts_with(keys, "["), "keys is a JSON array")
|
||||
assert_true(str_ends_with(keys, "]"), "keys is a JSON array")
|
||||
assert_true(json_array_len(keys) > 0, "keys array is non-empty")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── value type storage ────────────────────────────────────────────────────────
|
||||
|
||||
fn test_state_value_types(_: String) -> String {
|
||||
// JSON object as value
|
||||
let json_val: String = "{\"name\":\"alice\",\"age\":30}"
|
||||
state_set("suite_json_val", json_val)
|
||||
let retrieved: String = state_get("suite_json_val")
|
||||
assert_eq(json_get(retrieved, "name"), "alice", "JSON object stored and retrieved")
|
||||
|
||||
// JSON array as value
|
||||
let arr_val: String = "[1,2,3]"
|
||||
state_set("suite_arr_val", arr_val)
|
||||
let retrieved_arr: String = state_get("suite_arr_val")
|
||||
assert_int_eq(json_array_len(retrieved_arr), 3, "JSON array stored and retrieved")
|
||||
|
||||
// Large value
|
||||
let big: String = str_repeat("x", 500)
|
||||
state_set("suite_big_val", big)
|
||||
assert_int_eq(str_len(state_get("suite_big_val")), 500, "large value stored correctly")
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,575 @@
|
||||
// tests/suite/test_string.el — comprehensive tests for runtime/string.el
|
||||
//
|
||||
// Covers every public function in string.el. Import this file after all
|
||||
// runtime modules and runtime/test.el have been concatenated in.
|
||||
|
||||
// ── str_eq ────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_eq_basic(_: String) -> String {
|
||||
assert_true(str_eq("hello", "hello"), "identical strings are equal")
|
||||
assert_false(str_eq("hello", "world"), "different strings are not equal")
|
||||
assert_true(str_eq("", ""), "empty strings are equal")
|
||||
assert_false(str_eq("a", ""), "non-empty vs empty is not equal")
|
||||
assert_false(str_eq("", "a"), "empty vs non-empty is not equal")
|
||||
assert_false(str_eq("Hello", "hello"), "comparison is case-sensitive")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_eq_symbols(_: String) -> String {
|
||||
assert_true(str_eq("!@#$", "!@#$"), "symbols are equal")
|
||||
assert_true(str_eq(" ", " "), "spaces match")
|
||||
assert_false(str_eq(" a", "a "), "leading vs trailing space differ")
|
||||
assert_false(str_eq("abc", "ab"), "prefix not equal to full string")
|
||||
assert_false(str_eq("ab", "abc"), "shorter not equal to longer")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_len ───────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_len_basic(_: String) -> String {
|
||||
assert_int_eq(str_len(""), 0, "empty string has length 0")
|
||||
assert_int_eq(str_len("a"), 1, "single char has length 1")
|
||||
assert_int_eq(str_len("hello"), 5, "hello has length 5")
|
||||
assert_int_eq(str_len("hello world"), 11, "space counted in length")
|
||||
assert_int_eq(str_len("12345"), 5, "digits counted correctly")
|
||||
assert_int_eq(str_len("!@#$%"), 5, "punctuation counted correctly")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_len_longer(_: String) -> String {
|
||||
let s: String = str_repeat("ab", 50)
|
||||
assert_int_eq(str_len(s), 100, "repeated string has correct length")
|
||||
assert_int_eq(str_len("abcdefghij"), 10, "10-char string")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_concat / + operator ───────────────────────────────────────────────────
|
||||
|
||||
fn test_str_concat_basic(_: String) -> String {
|
||||
assert_eq(str_concat("hello", " world"), "hello world", "basic concat")
|
||||
assert_eq(str_concat("", "world"), "world", "empty prefix")
|
||||
assert_eq(str_concat("hello", ""), "hello", "empty suffix")
|
||||
assert_eq(str_concat("", ""), "", "both empty")
|
||||
assert_eq("foo" + "bar", "foobar", "operator + concat")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_concat_chaining(_: String) -> String {
|
||||
let a: String = "hello"
|
||||
let b: String = " "
|
||||
let c: String = "world"
|
||||
assert_eq(a + b + c, "hello world", "chained concat")
|
||||
assert_eq(str_concat(str_concat("a", "b"), "c"), "abc", "nested concat")
|
||||
assert_int_eq(str_len(str_concat("abc", "def")), 6, "length after concat")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_slice ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_slice_basic(_: String) -> String {
|
||||
assert_eq(str_slice("hello world", 0, 5), "hello", "slice from start")
|
||||
assert_eq(str_slice("hello world", 6, 11), "world", "slice from middle")
|
||||
assert_eq(str_slice("hello world", 0, 0), "", "zero-length slice")
|
||||
assert_eq(str_slice("hello", 0, 100), "hello", "end clamped to length")
|
||||
assert_eq(str_slice("hello", 3, 3), "", "start == end is empty")
|
||||
assert_eq(str_slice("hello world", 2, 7), "llo w", "interior slice")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_slice_edge(_: String) -> String {
|
||||
assert_eq(str_slice("", 0, 0), "", "empty string slice")
|
||||
assert_eq(str_slice("abc", 1, 2), "b", "single char slice")
|
||||
assert_eq(str_slice("abc", 0, 3), "abc", "full string slice")
|
||||
assert_eq(str_slice("abc", 2, 1), "", "inverted range is empty")
|
||||
assert_eq(str_slice("hello", 5, 5), "", "slice at end is empty")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_starts_with ───────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_starts_with_basic(_: String) -> String {
|
||||
assert_true(str_starts_with("hello world", "hello"), "prefix present")
|
||||
assert_false(str_starts_with("hello world", "world"), "not a prefix")
|
||||
assert_true(str_starts_with("hello", "hello"), "string is its own prefix")
|
||||
assert_true(str_starts_with("hello", ""), "empty prefix always true")
|
||||
assert_false(str_starts_with("", "a"), "empty string has no non-empty prefix")
|
||||
assert_false(str_starts_with("hi", "hello"), "prefix longer than string")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_starts_with_edge(_: String) -> String {
|
||||
assert_true(str_starts_with("abc", "a"), "single-char prefix")
|
||||
assert_false(str_starts_with("abc", "b"), "wrong single-char prefix")
|
||||
assert_true(str_starts_with("", ""), "empty starts with empty")
|
||||
assert_true(str_starts_with("hello world", "hello world"), "full string is prefix")
|
||||
assert_false(str_starts_with("hello", "HELLO"), "case-sensitive prefix")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_ends_with ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_ends_with_basic(_: String) -> String {
|
||||
assert_true(str_ends_with("hello world", "world"), "suffix present")
|
||||
assert_false(str_ends_with("hello world", "hello"), "not a suffix")
|
||||
assert_true(str_ends_with("hello", "hello"), "string is its own suffix")
|
||||
assert_true(str_ends_with("hello", ""), "empty suffix always true")
|
||||
assert_false(str_ends_with("", "a"), "empty string has no non-empty suffix")
|
||||
assert_false(str_ends_with("hi", "world"), "suffix longer than string")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_ends_with_edge(_: String) -> String {
|
||||
assert_true(str_ends_with("abc", "c"), "single-char suffix")
|
||||
assert_false(str_ends_with("abc", "b"), "wrong single-char suffix")
|
||||
assert_true(str_ends_with("", ""), "empty ends with empty")
|
||||
assert_false(str_ends_with("hello", "HELLO"), "case-sensitive suffix")
|
||||
assert_true(str_ends_with("file.txt", ".txt"), "common file extension case")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_contains ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_contains_basic(_: String) -> String {
|
||||
assert_true(str_contains("hello world", "world"), "contains at end")
|
||||
assert_true(str_contains("hello world", "hello"), "contains at start")
|
||||
assert_true(str_contains("hello world", "lo wo"), "contains in middle")
|
||||
assert_false(str_contains("hello world", "xyz"), "not contained")
|
||||
assert_true(str_contains("hello", ""), "empty sub always contained")
|
||||
assert_false(str_contains("", "a"), "empty string contains nothing")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_contains_edge(_: String) -> String {
|
||||
assert_true(str_contains("hello", "hello"), "string contains itself")
|
||||
assert_false(str_contains("hello", "helloo"), "longer sub not contained")
|
||||
assert_true(str_contains("aaa", "a"), "single char in repeated chars")
|
||||
assert_true(str_contains("aaa", "aa"), "two-char sub in repeated chars")
|
||||
assert_false(str_contains("abc", "ABC"), "case-sensitive")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_index_of ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_index_of_basic(_: String) -> String {
|
||||
assert_int_eq(str_index_of("hello world", "world"), 6, "index of suffix")
|
||||
assert_int_eq(str_index_of("hello world", "hello"), 0, "index of prefix")
|
||||
assert_int_eq(str_index_of("hello world", "o"), 4, "index of first occurrence")
|
||||
assert_int_eq(str_index_of("hello world", "xyz"), -1, "not found returns -1")
|
||||
assert_int_eq(str_index_of("hello", ""), 0, "empty sub returns 0")
|
||||
assert_int_eq(str_index_of("", "a"), -1, "search in empty string")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_index_of_duplicates(_: String) -> String {
|
||||
assert_int_eq(str_index_of("aababc", "ab"), 1, "finds first of two occurrences")
|
||||
assert_int_eq(str_index_of("abab", "ab"), 0, "finds first at position 0")
|
||||
assert_int_eq(str_index_of("xabab", "ab"), 1, "finds first after leading char")
|
||||
assert_int_eq(str_index_of("hello hello", "hello"), 0, "finds first hello")
|
||||
assert_int_eq(str_index_of("hello", "hello"), 0, "exact match at 0")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_last_index_of ─────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_last_index_of_basic(_: String) -> String {
|
||||
assert_int_eq(str_last_index_of("hello hello", "hello"), 6, "last occurrence index")
|
||||
assert_int_eq(str_last_index_of("hello", "hello"), 0, "only one occurrence")
|
||||
assert_int_eq(str_last_index_of("hello", "xyz"), -1, "not found returns -1")
|
||||
assert_int_eq(str_last_index_of("aababc", "ab"), 3, "last ab in aababc")
|
||||
assert_int_eq(str_last_index_of("abcabc", "abc"), 3, "last abc")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_replace ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_replace_basic(_: String) -> String {
|
||||
assert_eq(str_replace("hello world", "world", "there"), "hello there", "basic replace")
|
||||
assert_eq(str_replace("aaa", "a", "b"), "bbb", "replaces all occurrences")
|
||||
assert_eq(str_replace("hello", "xyz", "abc"), "hello", "no match is identity")
|
||||
assert_eq(str_replace("", "a", "b"), "", "empty string unchanged")
|
||||
assert_eq(str_replace("hello", "", "x"), "hello", "empty from is identity")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_replace_multiple(_: String) -> String {
|
||||
assert_eq(str_replace("hello hello", "hello", "bye"), "bye bye", "replace multiple")
|
||||
assert_eq(str_replace("aXbXc", "X", "-"), "a-b-c", "single-char delimiter")
|
||||
assert_eq(str_replace("abcabc", "abc", "X"), "XX", "multi-char replaced twice")
|
||||
assert_eq(str_replace("aabbcc", "bb", ""), "aacc", "replace with empty")
|
||||
assert_eq(str_replace("hello", "hello", "world"), "world", "replace whole string")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_to_upper / str_to_lower ──────────────────────────────────────────────
|
||||
|
||||
fn test_str_to_upper_basic(_: String) -> String {
|
||||
assert_eq(str_to_upper("hello"), "HELLO", "lowercase to uppercase")
|
||||
assert_eq(str_to_upper("HELLO"), "HELLO", "already uppercase unchanged")
|
||||
assert_eq(str_to_upper("Hello World"), "HELLO WORLD", "mixed case")
|
||||
assert_eq(str_to_upper(""), "", "empty string unchanged")
|
||||
assert_eq(str_to_upper("hello123"), "HELLO123", "digits unchanged")
|
||||
assert_eq(str_to_upper("abc-def"), "ABC-DEF", "punctuation unchanged")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_to_lower_basic(_: String) -> String {
|
||||
assert_eq(str_to_lower("HELLO"), "hello", "uppercase to lowercase")
|
||||
assert_eq(str_to_lower("hello"), "hello", "already lowercase unchanged")
|
||||
assert_eq(str_to_lower("Hello World"), "hello world", "mixed case")
|
||||
assert_eq(str_to_lower(""), "", "empty string unchanged")
|
||||
assert_eq(str_to_lower("HELLO123"), "hello123", "digits unchanged")
|
||||
assert_eq(str_to_lower("ABC-DEF"), "abc-def", "punctuation unchanged")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_upper_lower_roundtrip(_: String) -> String {
|
||||
let s: String = "Hello, World!"
|
||||
assert_eq(str_to_lower(str_to_upper(s)), "hello, world!", "upper then lower")
|
||||
assert_eq(str_to_upper(str_to_lower(s)), "HELLO, WORLD!", "lower then upper")
|
||||
assert_eq(str_to_upper(str_to_lower("ABC")), "ABC", "ABC roundtrip")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_trim / str_lstrip / str_rstrip ───────────────────────────────────────
|
||||
|
||||
fn test_str_trim_basic(_: String) -> String {
|
||||
assert_eq(str_trim(" hello "), "hello", "trims spaces both sides")
|
||||
assert_eq(str_trim("hello"), "hello", "no whitespace unchanged")
|
||||
assert_eq(str_trim(" "), "", "all-space string becomes empty")
|
||||
assert_eq(str_trim(""), "", "empty string unchanged")
|
||||
assert_eq(str_trim("\t hello \n"), "hello", "trims tabs and newlines")
|
||||
assert_eq(str_trim(" hello world "), "hello world", "internal spaces preserved")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_lstrip_rstrip(_: String) -> String {
|
||||
assert_eq(str_lstrip(" hello "), "hello ", "lstrip removes leading spaces")
|
||||
assert_eq(str_rstrip(" hello "), " hello", "rstrip removes trailing spaces")
|
||||
assert_eq(str_lstrip("hello"), "hello", "lstrip no-op on clean string")
|
||||
assert_eq(str_rstrip("hello"), "hello", "rstrip no-op on clean string")
|
||||
assert_eq(str_lstrip(""), "", "lstrip empty string")
|
||||
assert_eq(str_rstrip(""), "", "rstrip empty string")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_split ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_split_basic(_: String) -> String {
|
||||
let parts: [String] = str_split("a,b,c", ",")
|
||||
assert_int_eq(el_list_len(parts), 3, "split yields 3 parts")
|
||||
assert_eq(el_list_get(parts, 0), "a", "first part is a")
|
||||
assert_eq(el_list_get(parts, 1), "b", "second part is b")
|
||||
assert_eq(el_list_get(parts, 2), "c", "third part is c")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_split_edge(_: String) -> String {
|
||||
let single: [String] = str_split("hello", ",")
|
||||
assert_int_eq(el_list_len(single), 1, "no sep found yields 1 part")
|
||||
assert_eq(el_list_get(single, 0), "hello", "single part is the full string")
|
||||
|
||||
let trailing: [String] = str_split("a,b,", ",")
|
||||
assert_int_eq(el_list_len(trailing), 3, "trailing sep yields empty last element")
|
||||
assert_eq(el_list_get(trailing, 2), "", "last element is empty")
|
||||
|
||||
let multi: [String] = str_split("one::two::three", "::")
|
||||
assert_int_eq(el_list_len(multi), 3, "multi-char separator works")
|
||||
assert_eq(el_list_get(multi, 1), "two", "middle element correct")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_join ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_join_basic(_: String) -> String {
|
||||
let parts: [String] = el_list_empty()
|
||||
let parts = el_list_append(parts, "a")
|
||||
let parts = el_list_append(parts, "b")
|
||||
let parts = el_list_append(parts, "c")
|
||||
assert_eq(str_join(parts, ","), "a,b,c", "basic join with comma")
|
||||
assert_eq(str_join(parts, ""), "abc", "join with empty separator")
|
||||
assert_eq(str_join(parts, " | "), "a | b | c", "join with multi-char sep")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_join_edge(_: String) -> String {
|
||||
let empty_list: [String] = el_list_empty()
|
||||
assert_eq(str_join(empty_list, ","), "", "joining empty list yields empty")
|
||||
|
||||
let one: [String] = el_list_empty()
|
||||
let one = el_list_append(one, "solo")
|
||||
assert_eq(str_join(one, ","), "solo", "joining single element yields that element")
|
||||
assert_eq(str_join(one, "---"), "solo", "single element: no sep inserted")
|
||||
|
||||
let two: [String] = el_list_empty()
|
||||
let two = el_list_append(two, "alpha")
|
||||
let two = el_list_append(two, "beta")
|
||||
assert_eq(str_join(two, ", "), "alpha, beta", "two elements join correctly")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── int_to_str / str_to_int ──────────────────────────────────────────────────
|
||||
|
||||
fn test_int_to_str_basic(_: String) -> String {
|
||||
assert_eq(int_to_str(0), "0", "zero")
|
||||
assert_eq(int_to_str(42), "42", "positive integer")
|
||||
assert_eq(int_to_str(-1), "-1", "negative integer")
|
||||
assert_eq(int_to_str(1000000), "1000000", "large integer")
|
||||
assert_eq(int_to_str(-999), "-999", "negative large")
|
||||
assert_int_eq(str_len(int_to_str(12345)), 5, "correct digit count")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_to_int_basic(_: String) -> String {
|
||||
assert_int_eq(str_to_int("0"), 0, "zero")
|
||||
assert_int_eq(str_to_int("42"), 42, "positive integer")
|
||||
assert_int_eq(str_to_int("-1"), -1, "negative integer")
|
||||
assert_int_eq(str_to_int("1000000"), 1000000, "large integer")
|
||||
assert_int_eq(str_to_int("-999"), -999, "negative large")
|
||||
assert_int_eq(str_to_int(int_to_str(12345)), 12345, "roundtrip")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── float_to_str / str_to_float ──────────────────────────────────────────────
|
||||
|
||||
fn test_float_to_str_basic(_: String) -> String {
|
||||
assert_eq(float_to_str(0.0), "0", "zero float to str")
|
||||
assert_eq(float_to_str(1.5), "1.5", "basic float")
|
||||
assert_eq(float_to_str(-3.14), "-3.14", "negative float")
|
||||
assert_true(str_len(float_to_str(1.0)) > 0, "float_to_str produces output")
|
||||
assert_contains(float_to_str(100.0), "100", "100.0 contains 100")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_to_float_basic(_: String) -> String {
|
||||
let f: Float = str_to_float("1.5")
|
||||
let s: String = float_to_str(f)
|
||||
assert_eq(s, "1.5", "roundtrip 1.5")
|
||||
|
||||
let f2: Float = str_to_float("0.0")
|
||||
let s2: String = float_to_str(f2)
|
||||
assert_eq(s2, "0", "roundtrip 0.0")
|
||||
|
||||
assert_true(str_to_float("3.14") > 3.13, "3.14 > 3.13")
|
||||
assert_true(str_to_float("3.14") < 3.15, "3.14 < 3.15")
|
||||
assert_true(str_to_float("-1.0") < 0.0, "negative float is negative")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_repeat / str_reverse ─────────────────────────────────────────────────
|
||||
|
||||
fn test_str_repeat_basic(_: String) -> String {
|
||||
assert_eq(str_repeat("ab", 3), "ababab", "repeat 3 times")
|
||||
assert_eq(str_repeat("x", 1), "x", "repeat once")
|
||||
assert_eq(str_repeat("x", 0), "", "repeat zero times yields empty")
|
||||
assert_eq(str_repeat("", 5), "", "repeating empty string yields empty")
|
||||
assert_eq(str_repeat("-", 4), "----", "single char repeat")
|
||||
assert_int_eq(str_len(str_repeat("abc", 10)), 30, "length is n * len")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_reverse_basic(_: String) -> String {
|
||||
assert_eq(str_reverse("hello"), "olleh", "basic reverse")
|
||||
assert_eq(str_reverse("a"), "a", "single char is its own reverse")
|
||||
assert_eq(str_reverse(""), "", "empty string reverses to empty")
|
||||
assert_eq(str_reverse("abcd"), "dcba", "even-length reverse")
|
||||
assert_eq(str_reverse("racecar"), "racecar", "palindrome unchanged")
|
||||
assert_eq(str_reverse("abc"), "cba", "three-char reverse")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_count ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_count_basic(_: String) -> String {
|
||||
assert_int_eq(str_count("hello world hello", "hello"), 2, "two occurrences")
|
||||
assert_int_eq(str_count("aaa", "a"), 3, "adjacent single chars")
|
||||
assert_int_eq(str_count("aaa", "aa"), 1, "non-overlapping: one match")
|
||||
assert_int_eq(str_count("hello", "xyz"), 0, "no match")
|
||||
assert_int_eq(str_count("", "a"), 0, "empty string has no occurrences")
|
||||
assert_int_eq(str_count("hello", ""), 0, "empty sub returns 0")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_strip_prefix / str_strip_suffix ──────────────────────────────────────
|
||||
|
||||
fn test_str_strip_prefix_basic(_: String) -> String {
|
||||
assert_eq(str_strip_prefix("foobar", "foo"), "bar", "strips matching prefix")
|
||||
assert_eq(str_strip_prefix("foobar", "baz"), "foobar", "no-match is identity")
|
||||
assert_eq(str_strip_prefix("hello", ""), "hello", "empty prefix is identity")
|
||||
assert_eq(str_strip_prefix("hello", "hello"), "", "full match yields empty")
|
||||
assert_eq(str_strip_prefix("hello", "HELLO"), "hello", "case-sensitive no-match")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_strip_suffix_basic(_: String) -> String {
|
||||
assert_eq(str_strip_suffix("hello.md", ".md"), "hello", "strips matching suffix")
|
||||
assert_eq(str_strip_suffix("hello.md", ".txt"), "hello.md", "no-match is identity")
|
||||
assert_eq(str_strip_suffix("hello", ""), "hello", "empty suffix is identity")
|
||||
assert_eq(str_strip_suffix("hello", "hello"), "", "full match yields empty")
|
||||
assert_eq(str_strip_suffix("hello", "HELLO"), "hello", "case-sensitive no-match")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_find_chars ────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_str_find_chars_basic(_: String) -> String {
|
||||
assert_int_eq(str_find_chars("hello world", " "), 5, "finds space at index 5")
|
||||
assert_int_eq(str_find_chars("hello", "xyz"), -1, "not found returns -1")
|
||||
assert_int_eq(str_find_chars("hello", ""), -1, "empty charset returns -1")
|
||||
assert_int_eq(str_find_chars("hello", "aeiou"), 1, "finds first vowel at index 1")
|
||||
assert_int_eq(str_find_chars("", "a"), -1, "search in empty string returns -1")
|
||||
assert_int_eq(str_find_chars("abc", "c"), 2, "finds last char of string")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_char_at / str_char_code ───────────────────────────────────────────────
|
||||
|
||||
fn test_str_char_at_basic(_: String) -> String {
|
||||
assert_eq(str_char_at("hello", 0), "h", "first char")
|
||||
assert_eq(str_char_at("hello", 4), "o", "last char")
|
||||
assert_eq(str_char_at("hello", 2), "l", "middle char")
|
||||
assert_eq(str_char_at("hello", -1), "", "negative index yields empty")
|
||||
assert_eq(str_char_at("hello", 5), "", "out-of-bounds yields empty")
|
||||
assert_eq(str_char_at("", 0), "", "empty string yields empty")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_char_code_basic(_: String) -> String {
|
||||
assert_int_eq(str_char_code("A", 0), 65, "A has code 65")
|
||||
assert_int_eq(str_char_code("a", 0), 97, "a has code 97")
|
||||
assert_int_eq(str_char_code("0", 0), 48, "0 has code 48")
|
||||
assert_int_eq(str_char_code("hello", 0), 104, "h has code 104")
|
||||
assert_int_eq(str_char_code("", 0), 0, "empty string yields 0")
|
||||
assert_int_eq(str_char_code("a", 5), 0, "out-of-bounds yields 0")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_pad_left / str_pad_right ─────────────────────────────────────────────
|
||||
|
||||
fn test_str_pad_left_basic(_: String) -> String {
|
||||
assert_eq(str_pad_left("hi", 5, " "), " hi", "pads to width 5")
|
||||
assert_eq(str_pad_left("hi", 2, " "), "hi", "already at width is unchanged")
|
||||
assert_eq(str_pad_left("hi", 1, " "), "hi", "wider than width is unchanged")
|
||||
assert_eq(str_pad_left("5", 3, "0"), "005", "zero-pad a number")
|
||||
assert_int_eq(str_len(str_pad_left("ab", 7, "x")), 7, "padded to exact width")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_pad_right_basic(_: String) -> String {
|
||||
assert_eq(str_pad_right("hi", 5, " "), "hi ", "pads to width 5")
|
||||
assert_eq(str_pad_right("hi", 2, " "), "hi", "already at width is unchanged")
|
||||
assert_eq(str_pad_right("hi", 1, " "), "hi", "wider than width is unchanged")
|
||||
assert_int_eq(str_len(str_pad_right("ab", 7, "-")), 7, "padded to exact width")
|
||||
assert_starts_with(str_pad_right("ab", 7, "-"), "ab", "original string at start")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── Character classification ──────────────────────────────────────────────────
|
||||
|
||||
fn test_is_letter_basic(_: String) -> String {
|
||||
assert_true(is_letter("a"), "a is a letter")
|
||||
assert_true(is_letter("Z"), "Z is a letter")
|
||||
assert_true(is_letter("abc"), "abc all letters")
|
||||
assert_false(is_letter("1"), "1 is not a letter")
|
||||
assert_false(is_letter(""), "empty string is not a letter")
|
||||
assert_false(is_letter("a1"), "a1 has non-letter")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_is_digit_basic(_: String) -> String {
|
||||
assert_true(is_digit("0"), "0 is a digit")
|
||||
assert_true(is_digit("9"), "9 is a digit")
|
||||
assert_true(is_digit("123"), "123 all digits")
|
||||
assert_false(is_digit("a"), "a is not a digit")
|
||||
assert_false(is_digit(""), "empty is not a digit")
|
||||
assert_false(is_digit("1a"), "1a has non-digit")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_is_whitespace_basic(_: String) -> String {
|
||||
assert_true(is_whitespace(" "), "space is whitespace")
|
||||
assert_true(is_whitespace(" "), "multiple spaces")
|
||||
assert_false(is_whitespace("a"), "a is not whitespace")
|
||||
assert_false(is_whitespace(""), "empty string is not whitespace")
|
||||
assert_false(is_whitespace(" a"), "mixed is not all whitespace")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_count_lines / str_count_words ────────────────────────────────────────
|
||||
|
||||
fn test_str_count_lines_basic(_: String) -> String {
|
||||
assert_int_eq(str_count_lines(""), 0, "empty string has 0 lines")
|
||||
assert_int_eq(str_count_lines("hello"), 1, "single line no newline")
|
||||
assert_int_eq(str_count_lines("a\nb"), 2, "two lines with newline")
|
||||
assert_int_eq(str_count_lines("a\nb\nc"), 3, "three lines")
|
||||
assert_int_eq(str_count_lines("a\n"), 1, "trailing newline not counted as extra line")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_str_count_words_basic(_: String) -> String {
|
||||
assert_int_eq(str_count_words(""), 0, "empty string has 0 words")
|
||||
assert_int_eq(str_count_words("hello"), 1, "single word")
|
||||
assert_int_eq(str_count_words("hello world"), 2, "two words")
|
||||
assert_int_eq(str_count_words(" hello world "), 2, "extra spaces do not add words")
|
||||
assert_int_eq(str_count_words("one two three four"), 4, "four words")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── url_encode / url_decode ──────────────────────────────────────────────────
|
||||
|
||||
fn test_url_encode_basic(_: String) -> String {
|
||||
assert_eq(url_encode("hello"), "hello", "plain string unchanged")
|
||||
assert_contains(url_encode("hello world"), "%20", "space encoded as %20")
|
||||
assert_true(str_len(url_encode("hello world")) > str_len("hello world"), "encoded is longer")
|
||||
let encoded: String = url_encode("hello world")
|
||||
let decoded: String = url_decode(encoded)
|
||||
assert_eq(decoded, "hello world", "decode reverses encode")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_url_roundtrip(_: String) -> String {
|
||||
let s: String = "name=hello world&value=42"
|
||||
let encoded: String = url_encode(s)
|
||||
let decoded: String = url_decode(encoded)
|
||||
assert_eq(decoded, s, "url encode/decode roundtrip")
|
||||
assert_true(!str_eq(encoded, s), "encoded differs from original")
|
||||
assert_true(str_len(encoded) >= str_len(s), "encoded is at least as long")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── bool_to_str ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_bool_to_str_basic(_: String) -> String {
|
||||
assert_eq(bool_to_str(true), "true", "true becomes 'true'")
|
||||
assert_eq(bool_to_str(false), "false", "false becomes 'false'")
|
||||
assert_true(str_eq(bool_to_str(1 == 1), "true"), "expression true")
|
||||
assert_true(str_eq(bool_to_str(1 == 2), "false"), "expression false")
|
||||
assert_int_eq(str_len(bool_to_str(true)), 4, "true has length 4")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── str_to_bytes / bytes_to_str ───────────────────────────────────────────────
|
||||
|
||||
fn test_str_to_bytes_basic(_: String) -> String {
|
||||
let b: String = str_to_bytes("hi")
|
||||
assert_true(str_contains(b, "104"), "h has code 104")
|
||||
assert_true(str_contains(b, "105"), "i has code 105")
|
||||
assert_eq(str_to_bytes(""), "[]", "empty string yields empty array")
|
||||
assert_true(str_starts_with(b, "["), "bytes is JSON array")
|
||||
assert_true(str_ends_with(b, "]"), "bytes is JSON array")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_bytes_roundtrip(_: String) -> String {
|
||||
let s: String = "hello"
|
||||
let b: String = str_to_bytes(s)
|
||||
let s2: String = bytes_to_str(b)
|
||||
assert_eq(s2, s, "bytes roundtrip: hello")
|
||||
|
||||
let s3: String = "ABC"
|
||||
let b3: String = str_to_bytes(s3)
|
||||
let s4: String = bytes_to_str(b3)
|
||||
assert_eq(s4, s3, "bytes roundtrip: ABC")
|
||||
|
||||
assert_eq(bytes_to_str("[]"), "", "empty bytes yields empty string")
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
// tests/suite/test_time.el — comprehensive tests for runtime/time.el
|
||||
//
|
||||
// Covers time_now, now_millis, unix_timestamp, now_ns, time_format, time_to_parts,
|
||||
// time_add, time_diff, duration helpers, and uuid_new.
|
||||
|
||||
// ── time_now / now_millis / unix_timestamp_ms ─────────────────────────────────
|
||||
|
||||
fn test_time_now_basic(_: String) -> String {
|
||||
let t: Int = time_now()
|
||||
// Milliseconds since epoch: must be well past year 2020 (1577836800000)
|
||||
assert_true(t > 1577836800000, "time_now is past 2020-01-01")
|
||||
// Must be before year 2100 (4102444800000)
|
||||
assert_true(t < 4102444800000, "time_now is before 2100-01-01")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_now_millis_basic(_: String) -> String {
|
||||
let t: Int = now_millis()
|
||||
assert_true(t > 1577836800000, "now_millis is past 2020-01-01")
|
||||
assert_true(t < 4102444800000, "now_millis is before 2100-01-01")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_unix_timestamp_ms_basic(_: String) -> String {
|
||||
let t: Int = unix_timestamp_ms()
|
||||
assert_true(t > 1577836800000, "unix_timestamp_ms is past 2020-01-01")
|
||||
assert_true(t < 4102444800000, "unix_timestamp_ms is before 2100-01-01")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_time_now_ms_alias(_: String) -> String {
|
||||
let t: Int = time_now_ms()
|
||||
assert_true(t > 1577836800000, "time_now_ms is past 2020-01-01")
|
||||
assert_true(t < 4102444800000, "time_now_ms is before 2100-01-01")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── monotonicity ──────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_time_monotonic_now_millis(_: String) -> String {
|
||||
let t1: Int = now_millis()
|
||||
let t2: Int = now_millis()
|
||||
assert_true(t2 >= t1, "second call >= first call (monotonic)")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_time_monotonic_time_now(_: String) -> String {
|
||||
let t1: Int = time_now()
|
||||
let t2: Int = time_now()
|
||||
assert_true(t2 >= t1, "time_now is monotonic across calls")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_time_monotonic_now_ns(_: String) -> String {
|
||||
let t1: Int = now_ns()
|
||||
let t2: Int = now_ns()
|
||||
assert_true(t2 >= t1, "now_ns is monotonic across calls")
|
||||
// ns should be much larger than ms (9 digits difference)
|
||||
assert_true(t1 > 1000000000000000000, "now_ns is in nanosecond scale")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── unix_timestamp (seconds) ─────────────────────────────────────────────────
|
||||
|
||||
fn test_unix_timestamp_basic(_: String) -> String {
|
||||
let secs: Int = unix_timestamp()
|
||||
// Must be past 2020-01-01 in seconds (1577836800)
|
||||
assert_true(secs > 1577836800, "unix_timestamp is past 2020-01-01")
|
||||
// Must be before 2100-01-01 in seconds (4102444800)
|
||||
assert_true(secs < 4102444800, "unix_timestamp is before 2100-01-01")
|
||||
// seconds are about 1000x smaller than millis
|
||||
let ms: Int = time_now()
|
||||
assert_true(ms > secs, "milliseconds > seconds")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── time_to_parts ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_time_to_parts_basic(_: String) -> String {
|
||||
// Known timestamp: 2024-01-15 12:30:45.123 UTC
|
||||
// 2024-01-15 00:00:00 UTC = epoch + (54 * 365 + 14 leaps) days approximately
|
||||
// Use a precisely known value: 2024-01-15T12:30:45.123Z
|
||||
// epoch_ms = 1705320645123
|
||||
let ts: Int = 1705320645123
|
||||
let parts: String = time_to_parts(ts)
|
||||
assert_eq(json_get(parts, "year"), "2024", "year is 2024")
|
||||
assert_eq(json_get(parts, "month"), "1", "month is 1 (January)")
|
||||
assert_eq(json_get(parts, "day"), "15", "day is 15")
|
||||
assert_eq(json_get(parts, "hour"), "12", "hour is 12")
|
||||
assert_eq(json_get(parts, "minute"), "30", "minute is 30")
|
||||
assert_eq(json_get(parts, "second"), "45", "second is 45")
|
||||
assert_eq(json_get(parts, "ms"), "123", "milliseconds is 123")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_time_to_parts_epoch(_: String) -> String {
|
||||
// Unix epoch: 1970-01-01T00:00:00.000Z = timestamp 0
|
||||
let parts: String = time_to_parts(0)
|
||||
assert_eq(json_get(parts, "year"), "1970", "epoch year is 1970")
|
||||
assert_eq(json_get(parts, "month"), "1", "epoch month is 1")
|
||||
assert_eq(json_get(parts, "day"), "1", "epoch day is 1")
|
||||
assert_eq(json_get(parts, "hour"), "0", "epoch hour is 0")
|
||||
assert_eq(json_get(parts, "minute"), "0", "epoch minute is 0")
|
||||
assert_eq(json_get(parts, "second"), "0", "epoch second is 0")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_time_to_parts_current(_: String) -> String {
|
||||
let ts: Int = time_now()
|
||||
let parts: String = time_to_parts(ts)
|
||||
let year: Int = str_to_int(json_get(parts, "year"))
|
||||
let month: Int = str_to_int(json_get(parts, "month"))
|
||||
let day: Int = str_to_int(json_get(parts, "day"))
|
||||
let hour: Int = str_to_int(json_get(parts, "hour"))
|
||||
let minute: Int = str_to_int(json_get(parts, "minute"))
|
||||
let second: Int = str_to_int(json_get(parts, "second"))
|
||||
assert_true(year >= 2024, "current year >= 2024")
|
||||
assert_true(year < 2100, "current year < 2100")
|
||||
assert_true(month >= 1, "month >= 1")
|
||||
assert_true(month <= 12, "month <= 12")
|
||||
assert_true(day >= 1, "day >= 1")
|
||||
assert_true(day <= 31, "day <= 31")
|
||||
assert_true(hour >= 0, "hour >= 0")
|
||||
assert_true(hour <= 23, "hour <= 23")
|
||||
assert_true(minute >= 0, "minute >= 0")
|
||||
assert_true(minute <= 59, "minute <= 59")
|
||||
assert_true(second >= 0, "second >= 0")
|
||||
assert_true(second <= 59, "second <= 59")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── time_format ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_time_format_iso(_: String) -> String {
|
||||
let ts: Int = 1705320645123
|
||||
let iso: String = time_format(ts, "ISO")
|
||||
assert_starts_with(iso, "2024-01-15", "ISO format starts with correct date")
|
||||
assert_contains(iso, "T12:30:45", "ISO format has correct time")
|
||||
assert_ends_with(iso, "Z", "ISO format ends with Z")
|
||||
assert_contains(iso, "123", "ISO format includes milliseconds")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_time_format_empty(_: String) -> String {
|
||||
let ts: Int = 1705320645123
|
||||
let iso: String = time_format(ts, "")
|
||||
assert_starts_with(iso, "2024-01-15", "empty fmt gives ISO format")
|
||||
assert_ends_with(iso, "Z", "empty fmt gives ISO format ending in Z")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_time_format_strftime(_: String) -> String {
|
||||
let ts: Int = 1705320645123
|
||||
let formatted: String = time_format(ts, "%Y-%m-%d")
|
||||
assert_eq(formatted, "2024-01-15", "strftime %Y-%m-%d")
|
||||
let formatted2: String = time_format(ts, "%H:%M:%S")
|
||||
assert_eq(formatted2, "12:30:45", "strftime %H:%M:%S")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── time_add ──────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_time_add_basic(_: String) -> String {
|
||||
let base: Int = 1000000000000
|
||||
assert_int_eq(time_add(base, 1000, "ms"), base + 1000, "add milliseconds")
|
||||
assert_int_eq(time_add(base, 1, "sec"), base + 1000, "add 1 second")
|
||||
assert_int_eq(time_add(base, 1, "min"), base + 60000, "add 1 minute")
|
||||
assert_int_eq(time_add(base, 1, "hour"), base + 3600000, "add 1 hour")
|
||||
assert_int_eq(time_add(base, 1, "day"), base + 86400000, "add 1 day")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_time_add_multiple(_: String) -> String {
|
||||
let base: Int = 1000000000000
|
||||
assert_int_eq(time_add(base, 60, "sec"), base + 60000, "add 60 seconds")
|
||||
assert_int_eq(time_add(base, 24, "hour"), base + 86400000, "add 24 hours = 1 day")
|
||||
assert_int_eq(time_add(base, 0, "sec"), base, "add 0 seconds")
|
||||
assert_int_eq(time_add(base, -1, "sec"), base - 1000, "add negative seconds")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── time_diff ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn test_time_diff_basic(_: String) -> String {
|
||||
let t1: Int = 1000000000000
|
||||
let t2: Int = t1 + 5000
|
||||
assert_int_eq(time_diff(t1, t2, "ms"), 5000, "diff in ms")
|
||||
assert_int_eq(time_diff(t1, t2, "sec"), 5, "diff in seconds")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_time_diff_larger(_: String) -> String {
|
||||
let t1: Int = 1000000000000
|
||||
let t2: Int = t1 + 3600000
|
||||
assert_int_eq(time_diff(t1, t2, "min"), 60, "diff of 1 hour in minutes")
|
||||
assert_int_eq(time_diff(t1, t2, "hour"), 1, "diff of 1 hour in hours")
|
||||
assert_int_eq(time_diff(t1, t2, "ms"), 3600000, "diff of 1 hour in ms")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_time_diff_zero(_: String) -> String {
|
||||
let t: Int = 1000000000000
|
||||
assert_int_eq(time_diff(t, t, "ms"), 0, "same timestamp: diff is 0")
|
||||
assert_int_eq(time_diff(t, t, "sec"), 0, "same timestamp: diff in sec is 0")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── Duration helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
fn test_duration_helpers_basic(_: String) -> String {
|
||||
assert_int_eq(duration_seconds(1), 1000000000, "1 second = 1e9 ns")
|
||||
assert_int_eq(duration_millis(1), 1000000, "1 ms = 1e6 ns")
|
||||
assert_int_eq(duration_nanos(42), 42, "nanos identity")
|
||||
assert_int_eq(duration_to_seconds(1000000000), 1, "1e9 ns = 1 second")
|
||||
assert_int_eq(duration_to_millis(1000000), 1, "1e6 ns = 1 ms")
|
||||
assert_int_eq(duration_to_nanos(42), 42, "nanos identity roundtrip")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_instant_helpers_basic(_: String) -> String {
|
||||
let i: Int = unix_seconds(1000)
|
||||
assert_int_eq(i, 1000000000000, "unix_seconds(1000) = 1e12 ns")
|
||||
|
||||
let ms_i: Int = unix_millis(5000)
|
||||
assert_int_eq(ms_i, 5000000000, "unix_millis(5000) = 5e9 ns")
|
||||
|
||||
assert_int_eq(instant_to_unix_seconds(1000000000000), 1000, "instant to seconds")
|
||||
assert_int_eq(instant_to_unix_millis(5000000000), 5000, "instant to millis")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── uuid_new / uuid_v4 ────────────────────────────────────────────────────────
|
||||
|
||||
fn test_uuid_new_basic(_: String) -> String {
|
||||
let u: String = uuid_new()
|
||||
assert_int_eq(str_len(u), 36, "UUID has 36 characters")
|
||||
assert_eq(str_char_at(u, 8), "-", "UUID has dash at position 8")
|
||||
assert_eq(str_char_at(u, 13), "-", "UUID has dash at position 13")
|
||||
assert_eq(str_char_at(u, 18), "-", "UUID has dash at position 18")
|
||||
assert_eq(str_char_at(u, 23), "-", "UUID has dash at position 23")
|
||||
return ""
|
||||
}
|
||||
|
||||
fn test_uuid_uniqueness(_: String) -> String {
|
||||
let u1: String = uuid_new()
|
||||
let u2: String = uuid_new()
|
||||
let u3: String = uuid_v4()
|
||||
assert_false(str_eq(u1, u2), "consecutive UUIDs are different")
|
||||
assert_false(str_eq(u1, u3), "uuid_new and uuid_v4 produce different values")
|
||||
assert_false(str_eq(u2, u3), "three consecutive UUIDs are all different")
|
||||
assert_int_eq(str_len(u3), 36, "uuid_v4 also produces 36-char UUID")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── time_from_parts ───────────────────────────────────────────────────────────
|
||||
|
||||
fn test_time_from_parts_basic(_: String) -> String {
|
||||
// time_from_parts(secs, ns, tz) = secs * 1000 + ns / 1000000
|
||||
let ts: Int = time_from_parts(1000, 0, "UTC")
|
||||
assert_int_eq(ts, 1000000, "1000 secs = 1000000 ms")
|
||||
|
||||
let ts2: Int = time_from_parts(0, 500000000, "UTC")
|
||||
assert_int_eq(ts2, 500, "500ms in nanoseconds -> 500ms")
|
||||
|
||||
let ts3: Int = time_from_parts(1705320645, 123000000, "UTC")
|
||||
assert_int_eq(ts3, 1705320645123, "known timestamp with ms component")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── instant_to_iso8601 ────────────────────────────────────────────────────────
|
||||
|
||||
fn test_instant_to_iso8601_basic(_: String) -> String {
|
||||
let ts_ms: Int = 1705320645123
|
||||
let ts_ns: Int = ts_ms * 1000000
|
||||
let iso: String = instant_to_iso8601(ts_ns)
|
||||
assert_starts_with(iso, "2024-01-15", "instant_to_iso8601 correct date")
|
||||
assert_ends_with(iso, "Z", "instant_to_iso8601 ends with Z")
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// char-classes.el — ASCII character classification.
|
||||
// is_letter("A") && is_digit("7") && is_whitespace(" ")
|
||||
// && !is_letter("3") && !is_digit("X")
|
||||
fn run_test() -> Bool {
|
||||
if !is_letter("A") { return false }
|
||||
if !is_digit("7") { return false }
|
||||
if !is_whitespace(" ") { return false }
|
||||
if is_letter("3") { return false }
|
||||
if is_digit("X") { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
if run_test() {
|
||||
println("true")
|
||||
} else {
|
||||
println("false")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// count-lines-words-letters.el — composite count test.
|
||||
// Input "Hello world\nGoodbye world\n":
|
||||
// lines: 2 (each \n closes a line)
|
||||
// words: 4 (Hello, world, Goodbye, world)
|
||||
// letters: 22 (Hello=5 + world=5 + Goodbye=7 + world=5)
|
||||
fn run_test() -> String {
|
||||
let s: String = "Hello world\nGoodbye world\n"
|
||||
let lines: Int = str_count_lines(s)
|
||||
let words: Int = str_count_words(s)
|
||||
let letters: Int = str_count_letters(s)
|
||||
return int_to_str(lines) + "/" + int_to_str(words) + "/" + int_to_str(letters)
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println(run_test())
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// count-overlap-skip.el — non-overlapping advance: "aaaa" / "aa" -> 2.
|
||||
// After each match, the cursor advances by len(sub), so overlapping matches
|
||||
// are skipped. This is the universal default in C/Python/Go/etc.
|
||||
fn run_test() -> Int {
|
||||
let s: String = "aaaa"
|
||||
return str_count(s, "aa")
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println(int_to_str(run_test()))
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// count-substring.el — non-overlapping substring count.
|
||||
fn run_test() -> Int {
|
||||
let s: String = "abc abc abc"
|
||||
return str_count(s, "abc")
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println(int_to_str(run_test()))
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// index-of-all.el — every byte offset of a substring.
|
||||
// "abXcdXefX" / "X" -> [2, 5, 8]. Empty list when no matches.
|
||||
fn run_test() -> String {
|
||||
let positions: [Int] = str_index_of_all("abXcdXefX", "X")
|
||||
let n: Int = native_list_len(positions)
|
||||
let out: String = ""
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let p: Int = native_list_get(positions, i)
|
||||
if i > 0 {
|
||||
let out = out + ","
|
||||
}
|
||||
let out = out + int_to_str(p)
|
||||
let i = i + 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println(run_test())
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// join.el — list-of-string join with separator.
|
||||
fn run_test() -> String {
|
||||
let parts: [String] = native_list_empty()
|
||||
let parts = native_list_append(parts, "alpha")
|
||||
let parts = native_list_append(parts, "beta")
|
||||
let parts = native_list_append(parts, "gamma")
|
||||
return str_join(parts, ", ")
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println(run_test())
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// split-lines.el — \n-delimited split, \r\n folded, trailing empty dropped.
|
||||
// "alpha\nbeta\r\ngamma\n" -> ["alpha", "beta", "gamma"], len = 3.
|
||||
fn run_test() -> Int {
|
||||
let lines: [String] = str_split_lines("alpha\nbeta\r\ngamma\n")
|
||||
return native_list_len(lines)
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println(int_to_str(run_test()))
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// transform-suite.el — repeat, reverse, strip prefix/suffix/chars.
|
||||
fn run_test() -> String {
|
||||
let a: String = str_repeat("ab", 3) // "ababab"
|
||||
let b: String = str_reverse("hello") // "olleh"
|
||||
let c: String = str_strip_prefix("foobar", "foo") // "bar"
|
||||
let d: String = str_strip_suffix("hello.md", ".md") // "hello"
|
||||
let e: String = str_strip_chars(" \thello \n", " \t\n") // "hello"
|
||||
return a + "|" + b + "|" + c + "|" + d + "|" + e
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println(run_test())
|
||||
}
|
||||
Executable
+87
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env bash
|
||||
# run.sh — build and execute the text/ acceptance corpus.
|
||||
#
|
||||
# Each examples/<case>.el is a self-contained El program with a fn main()
|
||||
# that prints a single deterministic result line. The runner compiles each
|
||||
# via the canonical native elc, links it against the shared C runtime, runs
|
||||
# it, and asserts the output matches the expected value.
|
||||
|
||||
set -uo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
EL_HOME="${EL_HOME:-$(cd ../.. && pwd)}"
|
||||
ELC="${ELC:-${EL_HOME}/dist/platform/elc}"
|
||||
RUNTIME_DIR="${EL_HOME}/el-compiler/runtime"
|
||||
|
||||
if [ ! -x "${ELC}" ]; then
|
||||
echo "elc not found at ${ELC}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
FAILED_NAMES=()
|
||||
|
||||
run_runtime_case() {
|
||||
local name="$1"
|
||||
local src="$2"
|
||||
local expected="$3"
|
||||
|
||||
local out_c
|
||||
local out_bin
|
||||
out_c="$(mktemp -t text_test.XXXXXX).c"
|
||||
out_bin="$(mktemp -t text_test.XXXXXX)"
|
||||
|
||||
if ! "${ELC}" "${src}" > "${out_c}" 2>/tmp/text_test.elc.err; then
|
||||
echo "FAIL ${name} — elc emit failed:"
|
||||
cat /tmp/text_test.elc.err | sed 's/^/ /'
|
||||
FAIL=$((FAIL+1))
|
||||
FAILED_NAMES+=("${name}")
|
||||
rm -f "${out_c}" "${out_bin}"
|
||||
return
|
||||
fi
|
||||
|
||||
if ! cc -O2 -I "${RUNTIME_DIR}" "${out_c}" "${RUNTIME_DIR}/el_runtime.c" \
|
||||
-lcurl -lpthread -o "${out_bin}" 2>/tmp/text_test.cc.err; then
|
||||
echo "FAIL ${name} — cc failed:"
|
||||
cat /tmp/text_test.cc.err | sed 's/^/ /'
|
||||
FAIL=$((FAIL+1))
|
||||
FAILED_NAMES+=("${name}")
|
||||
rm -f "${out_c}" "${out_bin}"
|
||||
return
|
||||
fi
|
||||
|
||||
local got
|
||||
got="$("${out_bin}" 2>&1)"
|
||||
|
||||
if [ "${got}" = "${expected}" ]; then
|
||||
echo "PASS ${name}"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo "FAIL ${name} expected: '${expected}', got: '${got}'"
|
||||
FAIL=$((FAIL+1))
|
||||
FAILED_NAMES+=("${name}")
|
||||
fi
|
||||
|
||||
rm -f "${out_c}" "${out_bin}"
|
||||
}
|
||||
|
||||
echo "==> Running text-primitives acceptance corpus"
|
||||
echo
|
||||
|
||||
run_runtime_case "count-substring" examples/count-substring.el "3"
|
||||
run_runtime_case "count-overlap-skip" examples/count-overlap-skip.el "2"
|
||||
run_runtime_case "count-lines-words-letters" examples/count-lines-words-letters.el "2/4/22"
|
||||
run_runtime_case "index-of-all" examples/index-of-all.el "2,5,8"
|
||||
run_runtime_case "transform-suite" examples/transform-suite.el "ababab|olleh|bar|hello|hello"
|
||||
run_runtime_case "char-classes" examples/char-classes.el "true"
|
||||
run_runtime_case "split-lines" examples/split-lines.el "3"
|
||||
run_runtime_case "join" examples/join.el "alpha, beta, gamma"
|
||||
|
||||
echo
|
||||
echo "${PASS} passed, ${FAIL} failed"
|
||||
if [ "${FAIL}" -gt 0 ]; then
|
||||
echo "failed: ${FAILED_NAMES[*]}"
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
@@ -0,0 +1,11 @@
|
||||
// runner.el — entry point for the text/ acceptance corpus.
|
||||
//
|
||||
// Each text/examples/*.el is its own El program with its own fn main().
|
||||
// Compile, link, and run-output-diff is handled by tests/text/run.sh —
|
||||
// this file is the El-side stub kept for pattern parity with tests/time/
|
||||
// and tests/calendar/.
|
||||
|
||||
fn main() -> Void {
|
||||
println("text/ acceptance corpus is driven by run.sh — invoke that directly.")
|
||||
println("Each examples/*.el is a self-contained program; runner.el is a parity stub.")
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// sleep-typed.el — sleep(Duration) lowers to el_sleep_duration. The actual
|
||||
// elapsed Duration is measured by Instant - Instant arithmetic. The check
|
||||
// returns 1 if elapsed is at least 50 millis but under 1 second — the wide
|
||||
// upper bound tolerates scheduler jitter on shared CI runners while the
|
||||
// lower bound proves the sleep actually waited.
|
||||
|
||||
// `test` is reserved; using `run_test` instead.
|
||||
fn run_test() -> Int {
|
||||
let start: Instant = now()
|
||||
sleep(50.millis)
|
||||
let elapsed: Duration = now() - start
|
||||
if elapsed >= 50.millis {
|
||||
if elapsed < 1.second {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println(int_to_str(run_test()))
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// time-arithmetic.el — Instant + Duration -> Instant, then Instant - Instant
|
||||
// -> Duration. The codegen routes each BinOp through the typed wrappers
|
||||
// (el_instant_add_dur, el_instant_diff). Returns 1 if the elapsed span is at
|
||||
// least 5 minutes, 0 otherwise. Because the two now() calls are very close
|
||||
// together, the span is very nearly 5.minutes, so >= 5.minutes holds.
|
||||
|
||||
// `test` is reserved; using `run_test` instead.
|
||||
fn run_test() -> Int {
|
||||
let later: Instant = now() + 5.minutes
|
||||
let span: Duration = later - now()
|
||||
if span >= 5.minutes {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println(int_to_str(run_test()))
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// time-comparison.el — Duration < Duration is type-checked at codegen time.
|
||||
// Both operands are DurationLits (postfix literals). Codegen routes through
|
||||
// el_duration_lt; the runtime returns 1 when 5.minutes < 1.hour holds.
|
||||
|
||||
// `test` is reserved; using `run_test` instead.
|
||||
fn run_test() -> Bool {
|
||||
return 5.minutes < 1.hour
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
if run_test() {
|
||||
println("1")
|
||||
} else {
|
||||
println("0")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// time-literals.el — postfix-literal duration recognition.
|
||||
// `30.seconds` lowers to a DurationLit AST node carrying the count and the
|
||||
// unit; codegen emits a literal int64 nanosecond constant. duration_to_seconds
|
||||
// reverses the lowering and yields the original count.
|
||||
|
||||
// `test` is reserved by El's lexer as a property-test keyword, so the
|
||||
// example body lives in `run_test()`.
|
||||
fn run_test() -> Int {
|
||||
let d: Duration = 30.seconds
|
||||
return duration_to_seconds(d)
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println(int_to_str(run_test()))
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// time-typeerror.el — Instant + Instant is structurally meaningless.
|
||||
// Path taken: COMPILE-TIME error. The codegen records the violation in the
|
||||
// __time_violations accumulator; emit_time_violations writes a #error
|
||||
// directive at the top of the generated C, so cc fails the build with a
|
||||
// clear El-source-level message before any link step.
|
||||
//
|
||||
// This file is NOT expected to compile to a working binary. The runner
|
||||
// asserts that:
|
||||
// 1. elc emits the C source successfully (the violation is recorded but
|
||||
// codegen still completes — the #error sits at the bottom).
|
||||
// 2. cc on that C fails with the temporal-type-error message.
|
||||
|
||||
// `test` is reserved; using `run_test` instead.
|
||||
fn run_test() -> Int {
|
||||
let a: Instant = now()
|
||||
let b: Instant = now()
|
||||
let bad: Instant = a + b
|
||||
return 0
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
let r: Int = run_test()
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// ttl-cache.el — TTL helpers backed by typed Duration. Storing under a key
|
||||
// stamps the set time as an Instant; ttl_cache_get computes the age and
|
||||
// returns the value only if the age is within the supplied Duration.
|
||||
|
||||
// `test` is reserved; using `run_test` instead.
|
||||
fn run_test() -> String {
|
||||
ttl_cache_set("key1", "hello")
|
||||
let v: String = ttl_cache_get("key1", 1.hour)
|
||||
return v
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println(run_test())
|
||||
}
|
||||
Executable
+155
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env bash
|
||||
# run.sh — build and execute the time/ acceptance corpus.
|
||||
#
|
||||
# Each examples/<case>.el is a self-contained El program with a fn main()
|
||||
# that prints a single deterministic result line. The runner compiles each
|
||||
# via the canonical native elc, links it against the shared C runtime, runs
|
||||
# it, and asserts the output matches the expected value.
|
||||
#
|
||||
# The typeerror case is special: the C source is generated successfully (a
|
||||
# `#error` directive sits at the bottom) but cc must FAIL. Pass = cc fails
|
||||
# with the expected diagnostic; Fail = cc succeeds (the type rule didn't
|
||||
# fire) or fails with an unexpected diagnostic.
|
||||
|
||||
set -uo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
EL_HOME="${EL_HOME:-$(cd ../.. && pwd)}"
|
||||
ELC="${EL_HOME}/dist/platform/elc"
|
||||
RUNTIME_DIR="${EL_HOME}/el-compiler/runtime"
|
||||
|
||||
if [ ! -x "${ELC}" ]; then
|
||||
echo "elc not found at ${ELC}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
FAILED_NAMES=()
|
||||
|
||||
# run_runtime_case <name> <source> <expected> [<extra>]
|
||||
# Compiles the source via elc, links against the runtime, runs the binary,
|
||||
# and compares stdout (whitespace-trimmed) against <expected>. If <extra> is
|
||||
# the literal string "either", any one of the comma-separated values in
|
||||
# <expected> counts as a pass — used by time-arithmetic where the second
|
||||
# now() reading is microseconds after the first, which makes the >=
|
||||
# comparison platform-dependent.
|
||||
run_runtime_case() {
|
||||
local name="$1"
|
||||
local src="$2"
|
||||
local expected="$3"
|
||||
local mode="${4:-exact}"
|
||||
|
||||
local out_c
|
||||
local out_bin
|
||||
out_c="$(mktemp -t time_test.XXXXXX).c"
|
||||
out_bin="$(mktemp -t time_test.XXXXXX)"
|
||||
|
||||
if ! "${ELC}" "${src}" > "${out_c}" 2>/tmp/time_test.elc.err; then
|
||||
echo "FAIL ${name} — elc emit failed:"
|
||||
cat /tmp/time_test.elc.err | sed 's/^/ /'
|
||||
FAIL=$((FAIL+1))
|
||||
FAILED_NAMES+=("${name}")
|
||||
rm -f "${out_c}" "${out_bin}"
|
||||
return
|
||||
fi
|
||||
|
||||
if ! cc -O2 -I "${RUNTIME_DIR}" "${out_c}" "${RUNTIME_DIR}/el_runtime.c" \
|
||||
-lcurl -lpthread -o "${out_bin}" 2>/tmp/time_test.cc.err; then
|
||||
echo "FAIL ${name} — cc failed:"
|
||||
cat /tmp/time_test.cc.err | sed 's/^/ /'
|
||||
FAIL=$((FAIL+1))
|
||||
FAILED_NAMES+=("${name}")
|
||||
rm -f "${out_c}" "${out_bin}"
|
||||
return
|
||||
fi
|
||||
|
||||
local got
|
||||
got="$("${out_bin}" 2>&1 | tr -d '[:space:]')"
|
||||
|
||||
if [ "${mode}" = "either" ]; then
|
||||
local ok=0
|
||||
local IFS=','
|
||||
for choice in ${expected}; do
|
||||
if [ "${got}" = "${choice}" ]; then ok=1; break; fi
|
||||
done
|
||||
if [ "${ok}" = "1" ]; then
|
||||
echo "PASS ${name} (got: ${got})"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo "FAIL ${name} expected one of {${expected}}, got: ${got}"
|
||||
FAIL=$((FAIL+1))
|
||||
FAILED_NAMES+=("${name}")
|
||||
fi
|
||||
else
|
||||
if [ "${got}" = "${expected}" ]; then
|
||||
echo "PASS ${name}"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo "FAIL ${name} expected: ${expected}, got: ${got}"
|
||||
FAIL=$((FAIL+1))
|
||||
FAILED_NAMES+=("${name}")
|
||||
fi
|
||||
fi
|
||||
|
||||
rm -f "${out_c}" "${out_bin}"
|
||||
}
|
||||
|
||||
# run_typeerror_case <name> <source>
|
||||
# Compiles to C (which must succeed; the violation is recorded but codegen
|
||||
# still completes), then asserts cc FAILS with the temporal-type-error
|
||||
# directive in its output.
|
||||
run_typeerror_case() {
|
||||
local name="$1"
|
||||
local src="$2"
|
||||
|
||||
local out_c
|
||||
out_c="$(mktemp -t time_test.XXXXXX).c"
|
||||
|
||||
if ! "${ELC}" "${src}" > "${out_c}" 2>/tmp/time_test.elc.err; then
|
||||
echo "FAIL ${name} — elc emit failed (expected emit-then-cc-fail):"
|
||||
cat /tmp/time_test.elc.err | sed 's/^/ /'
|
||||
FAIL=$((FAIL+1))
|
||||
FAILED_NAMES+=("${name}")
|
||||
rm -f "${out_c}"
|
||||
return
|
||||
fi
|
||||
|
||||
if cc -O2 -I "${RUNTIME_DIR}" "${out_c}" "${RUNTIME_DIR}/el_runtime.c" \
|
||||
-lcurl -lpthread -o /tmp/time_test_should_not_link 2>/tmp/time_test.cc.err; then
|
||||
echo "FAIL ${name} — cc unexpectedly succeeded; type rule did not fire"
|
||||
FAIL=$((FAIL+1))
|
||||
FAILED_NAMES+=("${name}")
|
||||
rm -f "${out_c}" /tmp/time_test_should_not_link
|
||||
return
|
||||
fi
|
||||
|
||||
if grep -q "temporal type error" /tmp/time_test.cc.err; then
|
||||
echo "PASS ${name} (cc rejected with: $(grep "temporal type error" /tmp/time_test.cc.err | head -1 | tr -d '\n'))"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo "FAIL ${name} — cc failed but without the expected temporal-type-error message:"
|
||||
cat /tmp/time_test.cc.err | sed 's/^/ /'
|
||||
FAIL=$((FAIL+1))
|
||||
FAILED_NAMES+=("${name}")
|
||||
fi
|
||||
rm -f "${out_c}"
|
||||
}
|
||||
|
||||
echo "==> Running time-types acceptance corpus"
|
||||
echo
|
||||
|
||||
run_runtime_case "time-literals" examples/time-literals.el "30"
|
||||
run_runtime_case "time-arithmetic" examples/time-arithmetic.el "0,1" either
|
||||
run_runtime_case "time-comparison" examples/time-comparison.el "1"
|
||||
run_typeerror_case "time-typeerror" examples/time-typeerror.el
|
||||
run_runtime_case "ttl-cache" examples/ttl-cache.el "hello"
|
||||
run_runtime_case "sleep-typed" examples/sleep-typed.el "1"
|
||||
|
||||
echo
|
||||
echo "${PASS} passed, ${FAIL} failed"
|
||||
if [ "${FAIL}" -gt 0 ]; then
|
||||
echo "failed: ${FAILED_NAMES[*]}"
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
@@ -0,0 +1,15 @@
|
||||
// runner.el — entry point for the time/ acceptance corpus.
|
||||
//
|
||||
// Unlike tests/html_sanitizer/runner.el (which iterates cases.json against
|
||||
// a single runtime function), each time/examples/*.el is its own El
|
||||
// program with its own fn main(). Compile, link, and run-output-diff is
|
||||
// handled by tests/time/run.sh — this file is the El-side stub kept for
|
||||
// pattern parity (every tests/<name>/ has both a runner.el and a run.sh).
|
||||
//
|
||||
// Run from the time/ directory:
|
||||
// ./run.sh
|
||||
|
||||
fn main() -> Void {
|
||||
println("time/ acceptance corpus is driven by run.sh — invoke that directly.")
|
||||
println("Each examples/*.el is a self-contained program; runner.el is a parity stub.")
|
||||
}
|
||||
Reference in New Issue
Block a user