Testing Sky projects
Status: the Rust compiler (
rust/,cargo build --release -p sky) is the primary Sky compiler; the Haskell compiler is preserved underlegacy-haskell-compiler/. Verified by the example sweep + compiler test suite (cargo test+ xtask gates). See../../CHANGELOG.mdfor the changelog. (This link pointed at../compiler/versions.md; there is nodocs/compiler/directory.)
Sky ships with a first-class test framework: the Sky.Test stdlib module plus a sky test CLI command. Tests are plain Sky code and benefit from the same type checker, pattern exhaustiveness, and Error system as production code.
Writing a test module
Every test module exposes a single tests : List Test value. Tests can be individual assertions or grouped into suites.
module StringTest exposing (tests)
import Sky.Core.Prelude exposing (..)
import Sky.Core.String as String
import Sky.Test as Test exposing (Test)
tests : List Test
tests =
[ Test.test "trim removes outer spaces" (\_ ->
Test.equal "hi" (String.trim " hi "))
, Test.test "contains finds substring" (\_ ->
Test.isTrue (String.contains "ell" "hello"))
, Test.test "toInt rejects junk" (\_ ->
Test.err (String.toInt "abc"))
]
The (\_ -> ...) thunk wraps each assertion so a panic in one test doesn't abort the rest of the suite.
Assertions
From Sky.Test:
| Function | Use |
|---|---|
equal : a -> a -> TestResult | strict equality on primitives / records / ADTs |
notEqual : a -> a -> TestResult | negation |
ok : Result e a -> TestResult | asserts Ok _ |
err : Result e a -> TestResult | asserts Err _ |
expectErrorKind : ErrorKind -> Result Error a -> TestResult | asserts specific kind |
isTrue : Bool -> TestResult | asserts True |
isFalse : Bool -> TestResult | asserts False |
fail : String -> TestResult | unconditional failure with message |
pass : TestResult | unconditional pass |
Running tests
# From your project root (containing sky.toml):
sky test tests/MyTest.sky
# Or from any directory:
cd tests && sky test Core/CoreTest.sky
Exit code:
0— every test passed.1— one or more tests failed.2— build failed before any test ran.
Output format:
ok String.trim
ok String.toUpper
FAIL String.split non-empty
expected True, got False
5 passed, 1 failed (6 total)
Machine-readable output (SKY_TEST_JSON)
Set SKY_TEST_JSON to a path and the run additionally writes a per-case JSON
report. The human output above is byte-identical either way, so turning this on
never changes what you read in the terminal.
SKY_TEST_JSON=/tmp/report.json sky test tests/MyTest.sky
{
"schema": "sky-test/v1",
"cases": [
{ "name": "String.trim", "outcome": "pass", "assertions": 1, "message": "" },
{ "name": "String.split non-empty", "outcome": "fail", "assertions": 1,
"message": "expected True, got False" }
],
"total": 6, "passed": 5, "failed": 1, "assertions": 6
}
name is the fully-qualified leaf name, so suite labels appear exactly as the
human summary prints them. This is what lets a CI gate attribute a failure to a
specific test case rather than to a whole suite, and what lets it assert an
exact case count so a suite that silently stops running cases fails instead of
passing with fewer.
Two limits, so they are not mistaken for bugs:
assertionsis 1 per case. ATestleaf is() -> TestResultand yields exactly one result. The count that detects a shrinking suite is the suite-leveltotal.- There is no file/line.
Test.testcarries a name and a thunk; Sky has no source-location intrinsic, so the name is a case's only stable identity.
Sky.Test.jsonReport (pure, List (String, TestResult) -> String) and
Sky.Test.writeJsonReport (a Task, no-op on an empty path) are exposed for
callers that drive Sky.Test.run themselves.
Module discovery
sky test synthesises an entry module that imports your test module and calls Sky.Test.runMain tests. The synthesis derives the module name from the path:
src/Foo/BarTest.sky→Foo.BarTesttests/Core/CoreTest.sky(with[source] root = "tests") →Core.CoreTest
The test file's module declaration must match this derived name. Directory segments are auto-capitalised (tests/core/ → Core.).
Testing Result / Error
Test.test "network errors carry retry hint" (\_ ->
case Http.get "https://example.com/down" of
Ok _ ->
Test.fail "expected failure"
Err e ->
Test.isTrue (Error.isRetryable e)
)
For Result Error a values, expectErrorKind is concise:
Test.test "unauthorised returns PermissionDenied" (\_ ->
Test.expectErrorKind PermissionDenied
(Auth.authenticateUser "bad@email" "wrong-password"))
Example-level verification
For end-to-end verification of example projects (build + run + panic detection + HTTP probe), sky verify is the harness. Use sky test for unit-level stdlib and app logic; use sky verify for full-stack example regression.
Regression discipline
Every bug that reaches production gets a permanent regression test:
- Reproduce with a minimal Sky fixture.
- Add the failing test under
tests/(or as a Rustcargo testspec /runtime-go/rt/*_test.goif the bug lives in the compiler or runtime). - Fix the root cause.
- Verify the regression test passes with the fix and fails without it.
Current permanent regressions:
legacy-haskell-compiler/test/Sky/Build/NestedPatternSpec.hs— nestedOk (Just x)/Ok Truediscrimination.runtime-go/rt/coerce_test.go— nestedSkyMaybe[X]/[]T/map[K]Tshape-mismatch viaResultCoerce.runtime-go/rt/error_adt_shape_test.go— rtErrIovalues are type-compatible with user-sideSky_Core_Error_Error.legacy-haskell-compiler/test/Sky/Format/FormatSpec.hs— formatter idempotency (string escapes, scientific-notation floats, nested case, long pipelines, record updates).legacy-haskell-compiler/test/Sky/ErrorUnificationSpec.hs— forbidden-pattern greps:Result String,Task String,IoError,RemoteData.tests/Core/CoreTest.sky— 30 stdlib semantic tests (String / List / Dict / Maybe / Result). (Said 22;grep -o 'Test\.test' tests/Core/CoreTest.sky | wc -l→ 30. The other seven counts in this list match exactly, so this one had genuinely drifted.)tests/Lang/PatternTest.sky— 10 pattern-matching tests (nested Result/Maybe, enum ADT, Bool-inside-Ok).tests/Live/CounterTest.sky— 19 Sky.Live TEA loop tests (init / update / model invariants / event dispatch).tests/Live/FormTest.sky— 20 Sky.Live form-handling tests (validation / state machine transitions / sign-out).tests/Live/SessionTest.sky— 18 Sky.Live subscription + session round-trip tests.tests/Server/HttpServerTest.sky— 43 Sky.Http.Server pure-seam tests (route matching, path params, response builders, request record shape, status classification).tests/Auth/AuthTest.sky— 28 Sky.Auth state-machine tests (sign-in success/failure, sign-out, session resume, error classification, authenticated/unauthenticated invariants).tests/Db/DbTest.sky— 28 Std.Db pure-seam tests (row building, field extraction, exec/query simulation, not-found vs. error, structured-error mapping).
Known limits
- Nested
Test.suite— currently hits aSkyCallshape issue when the outer list is walked viaList.mapover an ADT-pattern-match closure. Use a flatList Testuntil fixed. Test.equalis deep-structural.==/Test.equalgo throughrt.sky_equal, which recurses into ADTs / records / lists / dicts —runtime-go/rt/eq_deep_test.goexercises the deep-list / deep-map / cross-instantiation paths. (Older versions of this page warned that collections needed scalar extraction; that workaround is no longer required.)