tricu

An interpreted language for exploring Tree Calculus
Log | Files | Refs | README | LICENSE

commit 69b0da8c8068c2d88e97024643d7242494468cdd
parent 38e85b4a5e328be257e4de84a368fa0e643378e6
Author: James Eversole <james@eversole.co>
Date:   Wed, 14 Oct 2026 15:49:23 -0500

Invoke byte and HTTP fixture tests without recovery compilation

Diffstat:
Mdocs/validation.md | 8++++++++
Atest/LibraryValueTests.hs | 422+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/ReferenceFixtures.hs | 96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/Spec.hs | 615+++----------------------------------------------------------------------------
Mtricu.cabal | 2++
5 files changed, 546 insertions(+), 597 deletions(-)

diff --git a/docs/validation.md b/docs/validation.md @@ -117,6 +117,14 @@ binding and the explicitly scoped recovery foundations. Missing/corrupt snapshot fail: there is no fallback to working-tree library source. Unmapped test programs remain live test inputs. Bundle, receipt, namespace, kernel, and ABI checks remain. +`test/ReferenceFixtures.hs` owns authenticated artifact loading and direct +published-interface selection. `test/LibraryValueTests.hs` invokes byte-list and +HTTP exports with host-constructed inputs, without source wrappers or recovery +compilation. HTTP framing/error expectations and no-socket-access controls remain +independent of the compiled implementations. Mixed-cohort reuse verification in +`SupportFixture` still depends on recovery parsing/free-variable analysis; direct +artifact invocation does not remove that separate provenance dependency. + This avoids recompiling libraries on every unit run without silently treating old executable trees as new source. Ordinary test wrappers may use the Haskell frontend; production module resolution does not fall back to those wrappers. diff --git a/test/LibraryValueTests.hs b/test/LibraryValueTests.hs @@ -0,0 +1,422 @@ +-- Value/protocol tests invoke authenticated exports directly. No source wrappers, +-- source import resolution, injected kernel names, or recovery compilation. +module LibraryValueTests (byteListUtilities, httpParsingTests) where + +import Runtime +import Control.Monad (forM_) +import IODriver (runIO, defaultPerms) +import qualified Data.Map as Map +import Test.Tasty +import Test.Tasty.HUnit + +trueT, falseT, nothingT :: T +trueT = Stem Leaf +falseT = Leaf +nothingT = Leaf + +justT :: T -> T +justT = Stem + +pairT :: T -> T -> T +pairT = Fork + +byteT :: Integer -> T +byteT = ofNumber + +bytesT :: [Integer] -> T +bytesT = ofList . map ofNumber + +parserOk, parserErr :: T -> T -> T +parserOk value rest = Fork trueT (Fork value rest) +parserErr code rest = Fork falseT (Fork code rest) + +portableReply :: Bool -> T -> T -> Bool -> Bool -> T +portableReply success payload rest consumed committed = + pairT (if success then trueT else falseT) + (pairT payload (pairT rest (pairT + (if consumed then trueT else falseT) + (if committed then trueT else falseT)))) + +portableDiagnostic :: T -> T -> T -> T -> T -> T +portableDiagnostic code expected found location contexts = + pairT code (pairT expected (pairT found (pairT location contexts))) + +byteListUtilities :: Env -> Env -> TestTree +byteListUtilities env base = testGroup "Byte List Utility Tests" + [ testCase "isNil: empty list is nil" $ do + call "bytesNil?" [ofList []] @?= trueT + + , testCase "isNil: non-empty list is not nil" $ do + call "bytesNil?" [bytesT [1]] @?= falseT + + , testCase "head: empty list is nothing" $ do + call "bytesHead" [ofList []] @?= nothingT + + , testCase "head: non-empty list returns first element" $ do + call "bytesHead" [bytesT [1, 2]] @?= justT (byteT 1) + + , testCase "tail: empty list is nothing" $ do + call "bytesTail" [ofList []] @?= nothingT + + , testCase "tail: non-empty list returns rest" $ do + call "bytesTail" [bytesT [1, 2]] @?= justT (bytesT [2]) + + , testCase "length: empty list is zero" $ do + call "bytesLength" [ofList []] @?= ofNumber 0 + + , testCase "length: single element list is one" $ do + call "bytesLength" [bytesT [1]] @?= ofNumber 1 + + , testCase "length: three element list is three" $ do + call "bytesLength" [bytesT [1, 2, 3]] @?= ofNumber 3 + + , testCase "append: empty ++ [1,2] = [1,2]" $ do + call "bytesAppend" [ofList [], bytesT [1, 2]] @?= bytesT [1,2] + + , testCase "append: [1,2] ++ [3] = [1,2,3]" $ do + call "bytesAppend" [bytesT [1, 2], bytesT [3]] @?= bytesT [1,2,3] + + , testCase "append: [1,2] ++ empty = [1,2]" $ do + call "bytesAppend" [bytesT [1, 2], ofList []] @?= bytesT [1,2] + + , testCase "take: take 0 any list = empty" $ do + call "bytesTake" [ofNumber 0, bytesT [1, 2, 3]] @?= bytesT [] + + , testCase "take: take 2 [1,2,3] = [1,2]" $ do + call "bytesTake" [ofNumber 2, bytesT [1, 2, 3]] @?= bytesT [1,2] + + , testCase "take: take 5 [1,2] = [1,2] (overlong)" $ do + call "bytesTake" [ofNumber 5, bytesT [1, 2]] @?= bytesT [1,2] + + , testCase "drop: drop 0 any list = list" $ do + call "bytesDrop" [ofNumber 0, bytesT [1, 2, 3]] @?= bytesT [1,2,3] + + , testCase "drop: drop 2 [1,2,3] = [3]" $ do + call "bytesDrop" [ofNumber 2, bytesT [1, 2, 3]] @?= bytesT [3] + + , testCase "drop: drop 5 [1,2] = empty (overlong)" $ do + call "bytesDrop" [ofNumber 5, bytesT [1, 2]] @?= bytesT [] + + , testCase "splitAt: splitAt 0 [1,2] = pair [] [1,2]" $ do + call "bytesSplitAt" [ofNumber 0, bytesT [1, 2]] @?= pairT (bytesT []) (bytesT [1,2]) + + , testCase "splitAt: splitAt 2 [1,2,3] = pair [1,2] [3]" $ do + call "bytesSplitAt" [ofNumber 2, bytesT [1, 2, 3]] @?= pairT (bytesT [1,2]) (bytesT [3]) + + , testCase "splitAt: splitAt 5 [1,2] = pair [1,2] []" $ do + call "bytesSplitAt" [ofNumber 5, bytesT [1, 2]] @?= pairT (bytesT [1,2]) (bytesT []) + + , testCase "byteEq: equal bytes are equal" $ do + call "equal?" [ofNumber 1, ofNumber 1] @?= trueT + + , testCase "byteEq: unequal bytes are not equal" $ do + call "equal?" [ofNumber 1, ofNumber 2] @?= falseT + + , testCase "bytesEq: empty == empty" $ do + call "bytesEq?" [ofList [], ofList []] @?= trueT + + , testCase "bytesEq: empty != [1]" $ do + call "bytesEq?" [ofList [], bytesT [1]] @?= falseT + + , testCase "bytesEq: [1] != empty" $ do + call "bytesEq?" [bytesT [1], ofList []] @?= falseT + + , testCase "bytesEq: equal lists are equal" $ do + call "bytesEq?" [bytesT [1, 2, 3], bytesT [1, 2, 3]] @?= trueT + + , testCase "bytesEq: different last element" $ do + call "bytesEq?" [bytesT [1, 2, 3], bytesT [1, 2, 4]] @?= falseT + + , testCase "bytesEq: different lengths" $ do + call "bytesEq?" [bytesT [1, 2], bytesT [1, 2, 3]] @?= falseT + ] + where + call "equal?" = foldl apply (base Map.! "equal?") + call name = foldl apply (env Map.! name) + +httpParsingTests :: Env -> TestTree +httpParsingTests env = testGroup "HTTP Parsing Tests" + [ testCase "HTTP support uses checked native parser exports" $ do + Map.member "httpLineReply" env @?= True + Map.member "httpHeaderLineReply" env @?= True + call "parseHeaders" [ofString "A: b\r\n\r\n"] + @?= parserOk (ofList [pairT (ofString "a") (ofString "b")]) Leaf + , testCase "finite line parser distinguishes CRLF from truncated EOF" $ do + let check source expected = + call "httpLineReply" [ofString source] @?= expected + check "hi\r\nx" (portableReply True + (ofString "hi") (ofString "x") True False) + let eof consumed = portableReply False + (portableDiagnostic (ofNumber 1) (ofString "CRLF") Leaf Leaf + (ofList [ofString "http-line"])) Leaf consumed False + check "hi" (eof True) + check "" (eof False) + , testCase "line parser rejects bare CR/LF with exact failure progress" $ do + forM_ [("h\ri\r\nx", 13, "\ri\r\nx", True), ("hi\nx", 10, "\nx", True), + ("hi\r", 13, "\r", True), ("\nx", 10, "\nx", False)] $ \(source, found, rest, consumed) -> + call "httpLineReply" [ofString source] @?= + portableReply False + (portableDiagnostic (ofNumber 2) (ofString "CRLF") (ofNumber found) Leaf + (ofList [ofString "http-line"])) (ofString rest) consumed False + , testCase "request-line fields use the shared finite-byte parser" $ do + let check input expected = + call "httpThreeWordsReply" [ofString input] @?= expected + fields method target version = + pairT (ofString method) + (pairT (ofString target) (ofString version)) + check "GET / HTTP/1.1" + (portableReply True (fields "GET" "/" "HTTP/1.1") Leaf True False) + check "GET /" + (portableReply False + (portableDiagnostic (ofNumber 1) (ofNumber 32) Leaf Leaf + (ofList [ofString "request-line"])) + Leaf True False) + check "GET / HTTP/1.1 extra" + (portableReply False + (portableDiagnostic (ofNumber 2) Leaf (ofNumber 32) Leaf + (ofList [ofString "request-line"])) + (ofString " extra") True False) + check " " + (portableReply True (fields "" "" "") Leaf True False) + , testCase "request-line Result boundary retains legacy response and rest" $ do + let check input expected = + call "parseThreeWords" [ofString input] @?= expected + check "GET / HTTP/1.1" + (parserOk (pairT (ofString "GET") + (pairT (ofString "/") (ofString "HTTP/1.1"))) Leaf) + check "GET / HTTP/1.1 extra" + (parserErr (ofNumber 400) (ofString "Bad Request\n")) + check "GET /" + (parserErr (ofNumber 400) (ofString "Bad Request\n")) + -- Result-valued request-line reader + , testCase "readLineBytes with CRLF" $ do + call "readLineBytes" [bytesT [104, 105, 13, 10, 120]] @?= parserOk (bytesT [104, 105]) (bytesT [120]) + + , testCase "readLineBytes rejects bare LF" $ do + call "readLineBytes" [bytesT [104, 105, 10, 120]] @?= parserErr (ofNumber 400) (ofString "Bad Request\n") + + , testCase "readLineBytes empty line" $ do + call "readLineBytes" [bytesT [13, 10, 120]] @?= parserOk (bytesT []) (bytesT [120]) + + , testCase "readLineBytes rejects EOF mid-line" $ do + call "readLineBytes" [bytesT [104, 105]] @?= parserErr (ofNumber 400) (ofString "Bad Request\n") + + -- parseRequestLine + , testCase "parseRequestLine GET slash" $ do + call "parseRequestLine" [ofString ("GET / HTTP/1.1\r\n" ++ "x")] @?= parserOk + (pairT (ofString "GET") (pairT (ofString "/") (ofString "HTTP/1.1"))) + (ofString "x") + + , testCase "parseRequestLine POST path" $ do + call "parseRequestLine" [ofString "POST /foo/bar HTTP/1.1\r\n"] @?= parserOk + (pairT (ofString "POST") (pairT (ofString "/foo/bar") (ofString "HTTP/1.1"))) + (ofString "") + + , testCase "parseRequestLine too short" $ do + call "parseRequestLine" [ofString "GET\r\n"] @?= parserErr (ofNumber 400) (ofString "Bad Request\n") + + , testCase "parseRequestLine no version" $ do + call "parseRequestLine" [ofString "GET /foo\r\n"] @?= parserErr (ofNumber 400) (ofString "Bad Request\n") + + , testCase "parseRequestLine empty line" $ do + call "parseRequestLine" [ofString "\r\n"] @?= parserErr (ofNumber 400) (ofString "Bad Request\n") + + , testCase "parseRequestLine rejects extra fields" $ do + call "parseRequestLine" [ofString "GET / HTTP/1.1 wat\r\n"] @?= parserErr (ofNumber 400) (ofString "Bad Request\n") + + -- parseHeaders + , testCase "single header line exposes consumed parser failures" $ do + let check input expected = + call "httpHeaderLineReply" [ofString input] @?= expected + check "Host: a:b" + (portableReply True + (pairT (ofString "host") (ofString "a:b")) Leaf True False) + check "Host" + (portableReply False + (portableDiagnostic (ofNumber 1) (ofString "colon") Leaf Leaf + (ofList [ofString "header-line"])) + Leaf True False) + check ": bad" + (portableReply False + (portableDiagnostic (ofNumber 2) (ofString "header-name") + (ofNumber 58) Leaf (ofList [ofString "header-line"])) + (ofString ": bad") False False) + , testCase "headers reject bad syntax and every truncated block prefix" $ do + let block = "A: b\r\nB: c\r\n\r\n" + inputs = [take n block | n <- [0 .. length block - 1]] ++ + [ "Bad Name: x\r\n\r\n", "Host : x\r\n\r\n", "\tHost: x\r\n\r\n" + , "X\r-Name: x\r\n\r\n", "X: a\rb\r\n\r\n", "X: y\n\n" + , "X: y\r\n\tcontinued\r\n\r\n" ] + forM_ inputs $ \source -> + call "parseHeaders" [ofString source] @?= + parserErr (ofNumber 400) (ofString "Bad Request\n") + , testCase "header values trim SP/HTAB edges but preserve interior whitespace" $ do + call "parseHeaders" [ofString "X:\t a \t b\t \r\nY:\t \r\n\r\nbody"] @?= + parserOk (ofList [pairT (ofString "x") (ofString "a \t b"), + pairT (ofString "y") Leaf]) (ofString "body") + , testCase "request-line scanner rejects missing CRLF and embedded CR" $ do + forM_ ["GET / HTTP/1.1", "GET / HTTP/1.1\n", "GET / HTTP/1.1\r", + "GE\rT / HTTP/1.1\r\n", "GET /a\rb HTTP/1.1\r\n"] $ \source -> + call "parseRequestLine" [ofString source] @?= + parserErr (ofNumber 400) (ofString "Bad Request\n") + , testCase "parseHeaders two headers lowercases names" $ do + call "parseHeaders" [ofString ("Host: localhost\r\nContent-Length: 42\r\n\r\n" ++ "x")] @?= parserOk + (ofList + [ pairT (ofString "host") (ofString "localhost") + , pairT (ofString "content-length") (ofString "42") + ]) + (ofString "x") + + , testCase "parseHeaders preserves colon in value" $ do + call "parseHeaders" [ofString ("X-Custom: a: b\r\n\r\n" ++ "x")] @?= parserOk + (ofList [pairT (ofString "x-custom") (ofString "a: b")]) + (ofString "x") + + , testCase "parseHeaders accepts empty value" $ do + call "parseHeaders" [ofString ("X-Empty:\r\n\r\n" ++ "x")] @?= parserOk + (ofList [pairT (ofString "x-empty") (ofString "")]) + (ofString "x") + + , testCase "parseHeaders immediate blank" $ do + call "parseHeaders" [ofString "\r\nx"] @?= parserOk (ofList []) (ofString "x") + + , testCase "parseHeaders rejects missing colon" $ do + call "parseHeaders" [ofString "Host\r\n\r\n"] @?= parserErr (ofNumber 400) (ofString "Bad Request\n") + + , testCase "bounded Content-Length validates syntax even after overflow" $ do + forM_ ["", "+1", "-2", "1 2", "12x", "x12", "1, 1", "1048577x", + replicate 128 '9' ++ "x"] $ \source -> + call "parseContentLengthValue" [ofString source] @?= + parserErr (ofNumber 400) (ofString "Bad Request\n") + call "parseContentLengthValue" [ofString (replicate 128 '9')] @?= + parserErr (ofNumber 413) (ofString "Request body too large\n") + , testCase "Content-Length selection rejects ambiguity before decoding" $ do + let check headers = call "contentLength" [ofList headers] @?= + parserErr (ofNumber 400) (ofString "Bad Request\n") + forM_ ["0", "1", "01", "1048577", "x"] $ \first -> + forM_ ["0", "1", "01", "1048577", "x"] $ \second -> + check [header "content-length" first, header "content-length" second] + forM_ ["", "chunked", "identity", "gzip"] $ \coding -> do + let transfer = header "transfer-encoding" coding + lengthField = header "content-length" "1048577" + mapM_ check [[transfer], [transfer, lengthField], [lengthField, transfer]] + , testCase "rejected framing produces a pure error before any body IO" $ do + forM_ [([header "transfer-encoding" "chunked"], 400, "Bad Request\n"), + ([header "content-length" "1", header "content-length" "1"], 400, "Bad Request\n"), + ([header "content-length" "1048577"], 413, "Request body too large\n")] $ \(headers, code, message) -> + call "readBody" [Leaf, ofList headers, ofString "body"] @?= + pairT (ofNumber 0) (parserErr (ofNumber code) (ofString message)) + , testCase "buffered body selection preserves exact suffix with no socket access" $ do + forM_ [([], "body", "", "body"), + ([header "content-length" "0"], "body", "", "body"), + ([header "content-length" "3"], "abcXYZ", "abc", "XYZ")] $ \(headers, input, body, rest) -> do + let action = call "readBody" [Leaf, ofList headers, ofString input] + envelope = Fork (ofString "tricuIO") (Fork (ofNumber 1) action) + runIO defaultPerms envelope >>= (@?= Right (parserOk (ofString body) (ofString rest))) + , testCase "parsed Content-Length header validates after framing" $ do + -- Test the pipeline directly; the ignored remainder is not source syntax. + let parsedLength raw = case call "parseHeaders" [ofString raw] of + Fork (Stem Leaf) (Fork headers _) -> call "contentLength" [headers] + other -> error ("expected successful header parsing: " ++ show other) + parsedLength "Content-Length: 42\r\n\r\nbody" @?= + parserOk (justT (ofNumber 42)) Leaf + call "contentLength" [ofList [header "content-length" "42", header "content-length" "99"]] @?= + parserErr (ofNumber 400) (ofString "Bad Request\n") + forM_ ["Content-Length: 1\r\nCONTENT-LENGTH: 1\r\n\r\n", + "Transfer-Encoding: chunked\r\n\r\n"] $ \raw -> + parsedLength raw @?= parserErr (ofNumber 400) (ofString "Bad Request\n") + parsedLength "Content-Length:\t 00042 \t\r\n\r\nbody" @?= + parserOk (justT (ofNumber 42)) Leaf + , testCase "parseContentLengthValue accepts max body bytes" $ + call "parseContentLengthValue" [ofString "1048576"] @?= + parserOk (justT (ofNumber 1048576)) Leaf + , testCase "parseContentLengthValue accepts shorter decimal below max" $ + call "parseContentLengthValue" [ofString "999999"] @?= + parserOk (justT (ofNumber 999999)) Leaf + + , testCase "parseContentLengthValue strips leading zeros before limit check" $ do + call "parseContentLengthValue" [ofString "0000000000001"] @?= parserOk (justT (ofNumber 1)) Leaf + + , testCase "parseContentLengthValue rejects body above max" $ do + call "parseContentLengthValue" [ofString "1048577"] @?= parserErr (ofNumber 413) (ofString "Request body too large\n") + + , testCase "parseContentLengthValue rejects longer body above max" $ do + call "parseContentLengthValue" [ofString "2000000"] @?= parserErr (ofNumber 413) (ofString "Request body too large\n") + + -- statusLine / headerLine + , testCase "statusLine 200 OK" $ do + call "statusLine" [ofNumber 200, ofString "OK"] @?= ofString "HTTP/1.1 200 OK\r\n" + + , testCase "headerLine Content-Length" $ do + call "headerLine" [ofString "Content-Length", ofString "42"] @?= ofString "Content-Length: 42\r\n" + + -- statusPhrase + , testCase "statusPhrase 200" $ do + call "statusPhrase" [ofNumber 200] @?= ofString "OK" + + , testCase "statusPhrase 201" $ do + call "statusPhrase" [ofNumber 201] @?= ofString "Created" + + , testCase "statusPhrase 204" $ do + call "statusPhrase" [ofNumber 204] @?= ofString "No Content" + + , testCase "statusPhrase 400" $ do + call "statusPhrase" [ofNumber 400] @?= ofString "Bad Request" + + , testCase "statusPhrase 404" $ do + call "statusPhrase" [ofNumber 404] @?= ofString "Not Found" + + , testCase "statusPhrase 405" $ do + call "statusPhrase" [ofNumber 405] @?= ofString "Method Not Allowed" + + , testCase "statusPhrase 431" $ do + call "statusPhrase" [ofNumber 431] @?= ofString "Request Header Fields Too Large" + + , testCase "statusPhrase 501" $ do + call "statusPhrase" [ofNumber 501] @?= ofString "Not Implemented" + + , testCase "statusPhrase 505" $ do + call "statusPhrase" [ofNumber 505] @?= ofString "HTTP Version Not Supported" + + , testCase "statusPhrase 500" $ do + call "statusPhrase" [ofNumber 500] @?= ofString "Internal Server Error" + + , testCase "statusPhrase unknown" $ do + call "statusPhrase" [ofNumber 999] @?= ofString "Internal Server Error" + + -- buildResponse + , testCase "buildResponse 200 no headers" $ do + call "buildResponse" [ofNumber 200, ofList [], ofString "hi"] @?= ofString "HTTP/1.1 200 OK\r\n\r\nhi" + + , testCase "buildResponse 404 with header" $ do + call "buildResponse" [ofNumber 404, ofList [header "Content-Length" "9"], ofString "Not found"] @?= ofString "HTTP/1.1 404 Not Found\r\nContent-Length: 9\r\n\r\nNot found" + + -- convenience responses + , testCase "okResponse" $ do + call "okResponse" [ofString "hi"] @?= ofString "HTTP/1.1 200 OK\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: 2\r\nConnection: close\r\n\r\nhi" + + , testCase "notFoundResponse" $ do + call "notFoundResponse" [] @?= ofString "HTTP/1.1 404 Not Found\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: 10\r\nConnection: close\r\n\r\nNot found\n" + + , testCase "textResponse" $ do + call "textResponse" [ofString "hi"] @?= ofString "HTTP/1.1 200 OK\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: 2\r\nConnection: close\r\n\r\nhi" + + , testCase "jsonResponse" $ do + call "jsonResponse" [ofString "{}"] @?= ofString "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 2\r\nConnection: close\r\n\r\n{}" + + , testCase "createdResponse" $ do + call "createdResponse" [ofString "created\n"] @?= ofString "HTTP/1.1 201 Created\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: 8\r\nConnection: close\r\n\r\ncreated\n" + + , testCase "emptyResponse 204" $ do + call "emptyResponse" [ofNumber 204] @?= ofString "HTTP/1.1 204 No Content\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" + + , testCase "badRequestResponse" $ do + call "badRequestResponse" [ofString "Bad Request\n"] @?= ofString "HTTP/1.1 400 Bad Request\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: 12\r\nConnection: close\r\n\r\nBad Request\n" + + , testCase "errorResponse 405" $ do + call "errorResponse" [ofNumber 405, ofString "Method Not Allowed\n"] @?= ofString "HTTP/1.1 405 Method Not Allowed\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: 19\r\nConnection: close\r\n\r\nMethod Not Allowed\n" + ] + where + call name = foldl apply (env Map.! name) + header name value = pairT (ofString name) (ofString value) + diff --git a/test/ReferenceFixtures.hs b/test/ReferenceFixtures.hs @@ -0,0 +1,96 @@ +-- Authenticated retained library artifacts. This module does not compile source +-- or recreate source import scopes. SupportFixture still uses recovery parsing +-- for mixed-cohort reuse proofs; that dependency is explicit, not a fallback. +module ReferenceFixtures + ( compilerFixture, supportFixture, compiledLibraries, fixtureExports ) where + +import Runtime (Env) +import Wire +import ContentStore (hashObject, Domain(..)) +import SupportFixture (verifyMixedSupport, readFixtureSourceBytes) +import Control.Exception (evaluate) +import Control.Monad (forM_, unless) +import Data.List (isPrefixOf) +import Data.Text (pack, unpack) +import System.IO.Unsafe (unsafePerformIO) +import qualified Data.ByteString as BS +import qualified Data.Map as Map +import qualified Data.Vector as V + +-- The fast gate executes self-built library trees rather than asking the old +-- recovery frontend to parse compiler sources. Every mapped reference source is +-- authenticated independently of working-tree edits. The explicit freshness gate +-- rejects stale cohorts at batch acceptance; these are reference-artifact tests. +compilerFixture :: ([(String, String, FilePath)], Env) +compilerFixture = unsafePerformIO $ do + metadata <- lines <$> readFile "./test/fixtures/compiler-library.v1" + bytes <- BS.readFile "./test/fixtures/compiler-library.arboricx" + unless (take 1 metadata == ["tricu.compiler-library-fixture.v1"]) $ + error "invalid compiler fixture metadata" + let rows = [ (legacy, alias, path, digest) + | row <- tail metadata + , ["module", legacy, alias, path, digest] <- [words row] ] + bundleHash = unpack (hashObject (Domain (pack "tricu.compiler-library-fixture.v1")) bytes) + unless (["bundle", bundleHash] `elem` map words metadata) $ + error "compiler fixture bundle digest mismatch" + forM_ rows $ \(_, _, path, digest) -> do + source <- readFixtureSourceBytes path + unless (unpack (hashObject (Domain (pack "arboricx.source.v1")) source) == digest) $ + error ("mismatched compiler reference source for " ++ path ++ + "; authenticate the fixture source snapshot") + bundle <- either error pure (decodeBundle bytes) + either error pure (verifyBundle bundle) + let terms = reconstructBundleTerms (bundleNodes bundle) + env = Map.fromList + [ (unpack (exportName entry), terms V.! fromIntegral (exportRoot entry)) + | entry <- manifestExports (bundleManifest bundle) ] + pure ([(legacy, alias, path) | (legacy, alias, path, _) <- rows], env) +{-# NOINLINE compilerFixture #-} + +-- Real portable builds own these unmapped general-purpose libraries. Loading +-- verifies authored inputs and the dependency compiler fixture; no body compiler. +supportFixture :: ([(String, String, FilePath)], Env) +supportFixture = unsafePerformIO $ do + metadata <- lines <$> readFile "./test/fixtures/support-library.v1" + bytes <- BS.readFile "./test/fixtures/support-library.arboricx" + compilerBytes <- BS.readFile "./test/fixtures/compiler-library.arboricx" + unless (take 1 metadata `elem` [["tricu.support-library-fixture.v1"], ["tricu.support-library-fixture.v2"], ["tricu.support-library-fixture.v3"]]) $ + error "invalid support fixture metadata" + let rows = [(legacy, alias, path, digest) | row <- tail metadata, + ["module", legacy, alias, path, digest] <- [words row]] + objectHash domain value = unpack (hashObject (Domain (pack domain)) value) + unless (["bundle", objectHash "tricu.support-library-fixture.v1" bytes] `elem` map words metadata) $ + error "support fixture bundle digest mismatch" + unless (["compiler-fixture", objectHash "tricu.compiler-library-fixture.v1" compilerBytes] `elem` map words metadata) $ + error "support fixture dependency mismatch" + if take 1 metadata `elem` [["tricu.support-library-fixture.v2"], ["tricu.support-library-fixture.v3"]] + then verifyMixedSupport metadata bytes compilerBytes + else pure () + _ <- evaluate (length (fst compilerFixture)) + forM_ rows $ \(_, _, path, digest) -> do + source <- readFixtureSourceBytes path + unless (objectHash "arboricx.source.v1" source == digest) $ + error ("mismatched support reference source for " ++ path) + bundle <- either error pure (decodeBundle bytes) + either error pure (verifyBundle bundle) + let terms = reconstructBundleTerms (bundleNodes bundle) + env = Map.fromList [(unpack (exportName entry), terms V.! fromIntegral (exportRoot entry)) + | entry <- manifestExports (bundleManifest bundle)] + pure ([(legacy, alias, path) | (legacy, alias, path, _) <- rows], env) +{-# NOINLINE supportFixture #-} + +compiledLibraries :: ([(String, String, FilePath)], Env) +compiledLibraries = (fst compilerFixture ++ fst supportFixture, + Map.union (snd compilerFixture) (snd supportFixture)) + +-- Select only the published interface, not imported names or recovery kernels. +-- The existing cohort loaders still authenticate sources/receipts/reuse proofs. +fixtureExports :: String -> Env +fixtureExports legacy = + let (rows, exports) = compiledLibraries + in case [alias | (name, alias, _) <- rows, name == legacy] of + [alias] -> Map.fromList + [(drop (length alias + 1) name, tree) | (name, tree) <- Map.toList exports, + (alias ++ ".") `isPrefixOf` name] + _ -> error ("missing or ambiguous fixture module: " ++ legacy) + diff --git a/test/Spec.hs b/test/Spec.hs @@ -1,6 +1,8 @@ module Main where import Eval +import qualified LibraryValueTests +import ReferenceFixtures import PackageTests (packageTests) import InteractionInspectionTests (interactionInspectionTests) import FileEval @@ -11,7 +13,7 @@ import Research import TreeObservation (forceTreeShared) import qualified NodeTable as Nodes import Wire -import SupportFixture (verifyMixedSupport, verifyReuseScopes, verifySupportSources, verifyRelocatedSupportSources, +import SupportFixture (verifyReuseScopes, verifySupportSources, verifyRelocatedSupportSources, verifySupportExports, verifySupportDependencies, readFixtureSourceBytes, readTestSource, readPinnedSource) import ContentStore import ContentStore.Bundle @@ -101,7 +103,7 @@ tests = testGroup "Tricu Tests" , verifiedTreePublicationTests , wireTests , tricuReaderTests - , byteListUtilities + , LibraryValueTests.byteListUtilities (fixtureExports "bytes") (fixtureExports "base") , binaryParserTests , portableParserTests , locatedLexerTests @@ -116,7 +118,7 @@ tests = testGroup "Tricu Tests" , gateUnitTests , gateResolutionTests , gateCompilationTests - , httpParsingTests + , LibraryValueTests.httpParsingTests (fixtureExports "http") , httpPipelineTests , registryContractTests , registryPipelineTests @@ -127,8 +129,6 @@ tests = testGroup "Tricu Tests" ] --- Demand is tested with exceptions rather than unbounded divergent reductions. --- Physical sharing below is a Haskell implementation check, not a tree ABI promise. -- Independent compatibility oracle: production rendering no longer imports ASTs. runtimePresentationTests :: TestTree runtimePresentationTests = testGroup "Runtime presentation" @@ -143,6 +143,8 @@ runtimePresentationTests = testGroup "Runtime presentation" "TFork (TStem TLeaf) (TFork TLeaf TLeaf)" ] +-- Demand uses exceptions rather than unbounded divergent reductions. +-- Physical sharing checks the Haskell implementation, not a tree ABI promise. evaluationDemandTests :: TestTree evaluationDemandTests = localOption (mkTimeout 30000000) $ testGroup "Evaluation demand boundaries" [ testCase "constant application discards its argument" $ @@ -3207,154 +3209,6 @@ sectionRecordExpr sectionType offset lengthBytes = ++ " " ++ bytesExpr (replicate 32 0) ++ ")))))))" -byteListUtilities :: TestTree -byteListUtilities = testGroup "Byte List Utility Tests" - [ testCase "isNil: empty list is nil" $ do - let input = "bytesNil? []" - let env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= trueT - - , testCase "isNil: non-empty list is not nil" $ do - let input = "bytesNil? [(1)]" - let env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= falseT - - , testCase "head: empty list is nothing" $ do - let input = "bytesHead []" - let env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= nothingT - - , testCase "head: non-empty list returns first element" $ do - let input = "bytesHead [(1) (2)]" - let env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= justT (byteT 1) - - , testCase "tail: empty list is nothing" $ do - let input = "bytesTail []" - let env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= nothingT - - , testCase "tail: non-empty list returns rest" $ do - let input = "bytesTail [(1) (2)]" - let env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= justT (bytesT [2]) - - , testCase "length: empty list is zero" $ do - let input = "bytesLength []" - let env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= ofNumber 0 - - , testCase "length: single element list is one" $ do - let input = "bytesLength [(1)]" - let env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= ofNumber 1 - - , testCase "length: three element list is three" $ do - let input = "bytesLength [(1) (2) (3)]" - let env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= ofNumber 3 - - , testCase "append: empty ++ [1,2] = [1,2]" $ do - let input = "bytesAppend [] [(1) (2)]" - let env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= bytesT [1,2] - - , testCase "append: [1,2] ++ [3] = [1,2,3]" $ do - let input = "bytesAppend [(1) (2)] [(3)]" - let env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= bytesT [1,2,3] - - , testCase "append: [1,2] ++ empty = [1,2]" $ do - let input = "bytesAppend [(1) (2)] []" - let env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= bytesT [1,2] - - , testCase "take: take 0 any list = empty" $ do - let input = "bytesTake 0 [(1) (2) (3)]" - let env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= bytesT [] - - , testCase "take: take 2 [1,2,3] = [1,2]" $ do - let input = "bytesTake 2 [(1) (2) (3)]" - let env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= bytesT [1,2] - - , testCase "take: take 5 [1,2] = [1,2] (overlong)" $ do - let input = "bytesTake 5 [(1) (2)]" - let env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= bytesT [1,2] - - , testCase "drop: drop 0 any list = list" $ do - let input = "bytesDrop 0 [(1) (2) (3)]" - let env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= bytesT [1,2,3] - - , testCase "drop: drop 2 [1,2,3] = [3]" $ do - let input = "bytesDrop 2 [(1) (2) (3)]" - let env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= bytesT [3] - - , testCase "drop: drop 5 [1,2] = empty (overlong)" $ do - let input = "bytesDrop 5 [(1) (2)]" - let env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= bytesT [] - - , testCase "splitAt: splitAt 0 [1,2] = pair [] [1,2]" $ do - let input = "bytesSplitAt 0 [(1) (2)]" - let env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= pairT (bytesT []) (bytesT [1,2]) - - , testCase "splitAt: splitAt 2 [1,2,3] = pair [1,2] [3]" $ do - let input = "bytesSplitAt 2 [(1) (2) (3)]" - let env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= pairT (bytesT [1,2]) (bytesT [3]) - - , testCase "splitAt: splitAt 5 [1,2] = pair [1,2] []" $ do - let input = "bytesSplitAt 5 [(1) (2)]" - let env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= pairT (bytesT [1,2]) (bytesT []) - - , testCase "byteEq: equal bytes are equal" $ do - let input = "equal? 1 1" - let env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= trueT - - , testCase "byteEq: unequal bytes are not equal" $ do - let input = "equal? 1 2" - let env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= falseT - - , testCase "bytesEq: empty == empty" $ do - let input = "bytesEq? [] []" - let env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= trueT - - , testCase "bytesEq: empty != [1]" $ do - let input = "bytesEq? [] [(1)]" - let env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= falseT - - , testCase "bytesEq: [1] != empty" $ do - let input = "bytesEq? [(1)] []" - let env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= falseT - - , testCase "bytesEq: equal lists are equal" $ do - let input = "bytesEq? [(1) (2) (3)] [(1) (2) (3)]" - let env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= trueT - - , testCase "bytesEq: different last element" $ do - let input = "bytesEq? [(1) (2) (3)] [(1) (2) (4)]" - let env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= falseT - - , testCase "bytesEq: different lengths" $ do - let input = "bytesEq? [(1) (2)] [(1) (2) (3)]" - let env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= falseT - ] - -- -------------------------------------------------------------------------- -- Portable finite-stream parser protocol -- -------------------------------------------------------------------------- @@ -3741,72 +3595,6 @@ gateLexerTests = testGroup "Feasibility-gate source lexer" -- Single-expression feasibility-gate surface parser -- -------------------------------------------------------------------------- --- The fast gate executes self-built library trees rather than asking the old --- recovery frontend to parse compiler sources. Every mapped reference source is --- authenticated independently of working-tree edits. The explicit freshness gate --- rejects stale cohorts at batch acceptance; these are reference-artifact tests. -compilerFixture :: ([(String, String, FilePath)], Env) -compilerFixture = unsafePerformIO $ do - metadata <- lines <$> readFile "./test/fixtures/compiler-library.v1" - bytes <- BS.readFile "./test/fixtures/compiler-library.arboricx" - unless (take 1 metadata == ["tricu.compiler-library-fixture.v1"]) $ - error "invalid compiler fixture metadata" - let rows = [ (legacy, alias, path, digest) - | row <- tail metadata - , ["module", legacy, alias, path, digest] <- [words row] ] - bundleHash = unpack (hashObject (Domain (pack "tricu.compiler-library-fixture.v1")) bytes) - unless (["bundle", bundleHash] `elem` map words metadata) $ - error "compiler fixture bundle digest mismatch" - forM_ rows $ \(_, _, path, digest) -> do - source <- readFixtureSourceBytes path - unless (unpack (hashObject (Domain (pack "arboricx.source.v1")) source) == digest) $ - error ("mismatched compiler reference source for " ++ path ++ - "; authenticate the fixture source snapshot") - bundle <- either error pure (decodeBundle bytes) - either error pure (verifyBundle bundle) - let terms = reconstructBundleTerms (bundleNodes bundle) - env = Map.fromList - [ (unpack (exportName entry), terms V.! fromIntegral (exportRoot entry)) - | entry <- manifestExports (bundleManifest bundle) ] - pure ([(legacy, alias, path) | (legacy, alias, path, _) <- rows], env) -{-# NOINLINE compilerFixture #-} - --- Real portable builds own these unmapped general-purpose libraries. Loading --- verifies authored inputs and the dependency compiler fixture; no body compiler. -supportFixture :: ([(String, String, FilePath)], Env) -supportFixture = unsafePerformIO $ do - metadata <- lines <$> readFile "./test/fixtures/support-library.v1" - bytes <- BS.readFile "./test/fixtures/support-library.arboricx" - compilerBytes <- BS.readFile "./test/fixtures/compiler-library.arboricx" - unless (take 1 metadata `elem` [["tricu.support-library-fixture.v1"], ["tricu.support-library-fixture.v2"], ["tricu.support-library-fixture.v3"]]) $ - error "invalid support fixture metadata" - let rows = [(legacy, alias, path, digest) | row <- tail metadata, - ["module", legacy, alias, path, digest] <- [words row]] - objectHash domain value = unpack (hashObject (Domain (pack domain)) value) - unless (["bundle", objectHash "tricu.support-library-fixture.v1" bytes] `elem` map words metadata) $ - error "support fixture bundle digest mismatch" - unless (["compiler-fixture", objectHash "tricu.compiler-library-fixture.v1" compilerBytes] `elem` map words metadata) $ - error "support fixture dependency mismatch" - if take 1 metadata `elem` [["tricu.support-library-fixture.v2"], ["tricu.support-library-fixture.v3"]] - then verifyMixedSupport metadata bytes compilerBytes - else pure () - _ <- evaluate (length (fst compilerFixture)) - forM_ rows $ \(_, _, path, digest) -> do - source <- readFixtureSourceBytes path - unless (objectHash "arboricx.source.v1" source == digest) $ - error ("mismatched support reference source for " ++ path) - bundle <- either error pure (decodeBundle bytes) - either error pure (verifyBundle bundle) - let terms = reconstructBundleTerms (bundleNodes bundle) - env = Map.fromList [(unpack (exportName entry), terms V.! fromIntegral (exportRoot entry)) - | entry <- manifestExports (bundleManifest bundle)] - pure ([(legacy, alias, path) | (legacy, alias, path, _) <- rows], env) -{-# NOINLINE supportFixture #-} - -compiledLibraries :: ([(String, String, FilePath)], Env) -compiledLibraries = (fst compilerFixture ++ fst supportFixture, - Map.union (snd compilerFixture) (snd supportFixture)) - compiledModuleEnv :: String -> IO Env compiledModuleEnv legacy = do let (rows, exports) = compiledLibraries @@ -4029,6 +3817,15 @@ compilerFixtureTests = testGroup "self-built reference compiler fixture" (any (\(_, alias, _) -> alias == "patterns") (fst supportFixture)) assertBool "missing unit compiler export" (Map.member "source_unit.compileSource" (snd compilerFixture)) + , testCase "direct fixture interfaces exclude imports and reject unknown modules" $ do + let http = fixtureExports "http" + assertBool "missing owned HTTP export" (Map.member "parseHeaders" http) + forM_ ["append", "kernel", "source_unit.compileSource"] $ \name -> + assertBool ("leaked imported/injected name: " ++ name) (Map.notMember name http) + rejected <- try (evaluate (Map.size (fixtureExports "missing.fixture"))) :: IO (Either SomeException Int) + case rejected of + Left _ -> pure () + Right _ -> assertFailure "unknown fixture module silently accepted" , testCase "every reference compiler module authors native headers" $ do forM_ (fst compilerFixture) $ \(_, _, path) -> do source <- readTestSource path @@ -7140,8 +6937,8 @@ runScriptedEffects initial expected = go (256 :: Int) initial [] expected httpPipelineTests :: TestTree httpPipelineTests = testCase "HTTP reference pipeline: buffers, EOF, receive errors and cleanup" $ do - env <- evaluateTestSupportFile "./lib/http.tri" - let call name = foldl apply (env Map.! name) + let env = fixtureExports "http" + call name = foldl apply (env Map.! name) sock = ofNumber 7 recv size = pairT (ofNumber 76) (pairT sock (ofNumber size)) send bytes = pairT (ofNumber 77) (pairT sock (ofString bytes)) @@ -7188,382 +6985,6 @@ httpPipelineTests = testCase "HTTP reference pipeline: buffers, EOF, receive err Left _ -> pure () Right _ -> assertFailure "HTTP effect interpreter accepted a mismatched script" -httpParsingTests :: TestTree -httpParsingTests = testGroup "HTTP Parsing Tests" - [ testCase "HTTP support uses checked native parser exports" $ do - env <- evaluateTestSupportFile "./lib/http.tri" - Map.member "httpLineReply" env @?= True - Map.member "httpHeaderLineReply" env @?= True - result (evalTricu env (parseTricu "parseHeaders \"A: b\\r\\n\\r\\n\"")) - @?= parserOk (ofList [pairT (ofString "a") (ofString "b")]) Leaf - , testCase "finite line parser distinguishes CRLF from truncated EOF" $ do - let check source expected = - result (evalTricu allTestLibsEnv (parseTricu - ("httpLineReply " ++ show source))) @?= expected - check "hi\r\nx" (portableReply True - (ofString "hi") (ofString "x") True False) - let eof consumed = portableReply False - (portableDiagnostic (ofNumber 1) (ofString "CRLF") Leaf Leaf - (ofList [ofString "http-line"])) Leaf consumed False - check "hi" (eof True) - check "" (eof False) - , testCase "line parser rejects bare CR/LF with exact failure progress" $ do - forM_ [("h\ri\r\nx", 13, "\ri\r\nx", True), ("hi\nx", 10, "\nx", True), - ("hi\r", 13, "\r", True), ("\nx", 10, "\nx", False)] $ \(source, found, rest, consumed) -> - result (evalTricu allTestLibsEnv (parseTricu ("httpLineReply " ++ show source))) @?= - portableReply False - (portableDiagnostic (ofNumber 2) (ofString "CRLF") (ofNumber found) Leaf - (ofList [ofString "http-line"])) (ofString rest) consumed False - , testCase "request-line fields use the shared finite-byte parser" $ do - let check input expected = - result (evalTricu allTestLibsEnv (parseTricu input)) @?= expected - fields method target version = - pairT (ofString method) - (pairT (ofString target) (ofString version)) - check "httpThreeWordsReply \"GET / HTTP/1.1\"" - (portableReply True (fields "GET" "/" "HTTP/1.1") Leaf True False) - check "httpThreeWordsReply \"GET /\"" - (portableReply False - (portableDiagnostic (ofNumber 1) (ofNumber 32) Leaf Leaf - (ofList [ofString "request-line"])) - Leaf True False) - check "httpThreeWordsReply \"GET / HTTP/1.1 extra\"" - (portableReply False - (portableDiagnostic (ofNumber 2) Leaf (ofNumber 32) Leaf - (ofList [ofString "request-line"])) - (ofString " extra") True False) - check "httpThreeWordsReply \" \"" - (portableReply True (fields "" "" "") Leaf True False) - , testCase "request-line Result boundary retains legacy response and rest" $ do - let check input expected = - result (evalTricu allTestLibsEnv (parseTricu input)) @?= expected - check "parseThreeWords \"GET / HTTP/1.1\"" - (parserOk (pairT (ofString "GET") - (pairT (ofString "/") (ofString "HTTP/1.1"))) Leaf) - check "parseThreeWords \"GET / HTTP/1.1 extra\"" - (parserErr (ofNumber 400) (ofString "Bad Request\n")) - check "parseThreeWords \"GET /\"" - (parserErr (ofNumber 400) (ofString "Bad Request\n")) - -- Result-valued request-line reader - , testCase "readLineBytes with CRLF" $ do - let input = "readLineBytes [(104) (105) (13) (10) (120)]" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= parserOk (bytesT [104, 105]) (bytesT [120]) - - , testCase "readLineBytes rejects bare LF" $ do - let input = "readLineBytes [(104) (105) (10) (120)]" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= parserErr (ofNumber 400) (ofString "Bad Request\n") - - , testCase "readLineBytes empty line" $ do - let input = "readLineBytes [(13) (10) (120)]" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= parserOk (bytesT []) (bytesT [120]) - - , testCase "readLineBytes rejects EOF mid-line" $ do - let input = "readLineBytes [(104) (105)]" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= parserErr (ofNumber 400) (ofString "Bad Request\n") - - -- parseRequestLine - , testCase "parseRequestLine GET slash" $ do - let input = "parseRequestLine (append \"GET / HTTP/1.1\\r\\n\" \"x\")" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= parserOk - (pairT (ofString "GET") (pairT (ofString "/") (ofString "HTTP/1.1"))) - (ofString "x") - - , testCase "parseRequestLine POST path" $ do - let input = "parseRequestLine \"POST /foo/bar HTTP/1.1\\r\\n\"" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= parserOk - (pairT (ofString "POST") (pairT (ofString "/foo/bar") (ofString "HTTP/1.1"))) - (ofString "") - - , testCase "parseRequestLine too short" $ do - let input = "parseRequestLine \"GET\\r\\n\"" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= parserErr (ofNumber 400) (ofString "Bad Request\n") - - , testCase "parseRequestLine no version" $ do - let input = "parseRequestLine \"GET /foo\\r\\n\"" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= parserErr (ofNumber 400) (ofString "Bad Request\n") - - , testCase "parseRequestLine empty line" $ do - let input = "parseRequestLine \"\\r\\n\"" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= parserErr (ofNumber 400) (ofString "Bad Request\n") - - , testCase "parseRequestLine rejects extra fields" $ do - let input = "parseRequestLine \"GET / HTTP/1.1 wat\\r\\n\"" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= parserErr (ofNumber 400) (ofString "Bad Request\n") - - -- parseHeaders - , testCase "single header line exposes consumed parser failures" $ do - let check input expected = - result (evalTricu allTestLibsEnv (parseTricu input)) @?= expected - check "httpHeaderLineReply \"Host: a:b\"" - (portableReply True - (pairT (ofString "host") (ofString "a:b")) Leaf True False) - check "httpHeaderLineReply \"Host\"" - (portableReply False - (portableDiagnostic (ofNumber 1) (ofString "colon") Leaf Leaf - (ofList [ofString "header-line"])) - Leaf True False) - check "httpHeaderLineReply \": bad\"" - (portableReply False - (portableDiagnostic (ofNumber 2) (ofString "header-name") - (ofNumber 58) Leaf (ofList [ofString "header-line"])) - (ofString ": bad") False False) - , testCase "headers reject bad syntax and every truncated block prefix" $ do - let block = "A: b\r\nB: c\r\n\r\n" - inputs = [take n block | n <- [0 .. length block - 1]] ++ - [ "Bad Name: x\r\n\r\n", "Host : x\r\n\r\n", "\tHost: x\r\n\r\n" - , "X\r-Name: x\r\n\r\n", "X: a\rb\r\n\r\n", "X: y\n\n" - , "X: y\r\n\tcontinued\r\n\r\n" ] - forM_ inputs $ \source -> - result (evalTricu allTestLibsEnv (parseTricu ("parseHeaders " ++ show source))) @?= - parserErr (ofNumber 400) (ofString "Bad Request\n") - , testCase "header values trim SP/HTAB edges but preserve interior whitespace" $ do - result (evalTricu allTestLibsEnv - (parseTricu "parseHeaders \"X:\\t a \\t b\\t \\r\\nY:\\t \\r\\n\\r\\nbody\"")) @?= - parserOk (ofList [pairT (ofString "x") (ofString "a \t b"), - pairT (ofString "y") Leaf]) (ofString "body") - , testCase "request-line scanner rejects missing CRLF and embedded CR" $ do - forM_ ["GET / HTTP/1.1", "GET / HTTP/1.1\n", "GET / HTTP/1.1\r", - "GE\rT / HTTP/1.1\r\n", "GET /a\rb HTTP/1.1\r\n"] $ \source -> - result (evalTricu allTestLibsEnv (parseTricu ("parseRequestLine " ++ show source))) @?= - parserErr (ofNumber 400) (ofString "Bad Request\n") - , testCase "parseHeaders two headers lowercases names" $ do - let input = "parseHeaders (append \"Host: localhost\\r\\nContent-Length: 42\\r\\n\\r\\n\" \"x\")" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= parserOk - (ofList - [ pairT (ofString "host") (ofString "localhost") - , pairT (ofString "content-length") (ofString "42") - ]) - (ofString "x") - - , testCase "parseHeaders preserves colon in value" $ do - let input = "parseHeaders (append \"X-Custom: a: b\\r\\n\\r\\n\" \"x\")" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= parserOk - (ofList [pairT (ofString "x-custom") (ofString "a: b")]) - (ofString "x") - - , testCase "parseHeaders accepts empty value" $ do - let input = "parseHeaders (append \"X-Empty:\\r\\n\\r\\n\" \"x\")" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= parserOk - (ofList [pairT (ofString "x-empty") (ofString "")]) - (ofString "x") - - , testCase "parseHeaders immediate blank" $ do - let input = "parseHeaders \"\\r\\nx\"" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= parserOk (ofList []) (ofString "x") - - , testCase "parseHeaders rejects missing colon" $ do - let input = "parseHeaders \"Host\\r\\n\\r\\n\"" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= parserErr (ofNumber 400) (ofString "Bad Request\n") - - , testCase "bounded Content-Length validates syntax even after overflow" $ do - forM_ ["", "+1", "-2", "1 2", "12x", "x12", "1, 1", "1048577x", - replicate 128 '9' ++ "x"] $ \source -> - result (evalTricu allTestLibsEnv (parseTricu ("parseContentLengthValue " ++ show source))) @?= - parserErr (ofNumber 400) (ofString "Bad Request\n") - result (evalTricu allTestLibsEnv - (parseTricu ("parseContentLengthValue " ++ show (replicate 128 '9')))) @?= - parserErr (ofNumber 413) (ofString "Request body too large\n") - , testCase "Content-Length selection rejects ambiguity before decoding" $ do - let check source = result (evalTricu allTestLibsEnv (parseTricu source)) @?= - parserErr (ofNumber 400) (ofString "Bad Request\n") - forM_ ["0", "1", "01", "1048577", "x"] $ \first -> - forM_ ["0", "1", "01", "1048577", "x"] $ \second -> - check ("contentLength [(pair \"content-length\" " ++ show first ++ - ") (pair \"content-length\" " ++ show second ++ ")]") - forM_ ["", "chunked", "identity", "gzip"] $ \coding -> do - let transfer = "(pair \"transfer-encoding\" " ++ show coding ++ ")" - lengthField = "(pair \"content-length\" \"1048577\")" - mapM_ check ["contentLength [" ++ transfer ++ "]", - "contentLength [" ++ transfer ++ " " ++ lengthField ++ "]", - "contentLength [" ++ lengthField ++ " " ++ transfer ++ "]"] - , testCase "rejected framing produces a pure error before any body IO" $ do - forM_ [("[(pair \"transfer-encoding\" \"chunked\")]", 400, "Bad Request\n"), - ("[(pair \"content-length\" \"1\") (pair \"content-length\" \"1\")]", 400, "Bad Request\n"), - ("[(pair \"content-length\" \"1048577\")]", 413, "Request body too large\n")] $ \(headers, code, message) -> - result (evalTricu allTestLibsEnv (parseTricu ("readBody t " ++ headers ++ " \"body\""))) @?= - pairT (ofNumber 0) (parserErr (ofNumber code) (ofString message)) - , testCase "buffered body selection preserves exact suffix with no socket access" $ do - forM_ [("[]", "body", "", "body"), - ("[(pair \"content-length\" \"0\")]", "body", "", "body"), - ("[(pair \"content-length\" \"3\")]", "abcXYZ", "abc", "XYZ")] $ \(headers, input, body, rest) -> do - observed <- runIOSourceWithPerms defaultPerms - ("main = io (readBody t " ++ headers ++ " " ++ show input ++ ")") - observed @?= parserOk (ofString body) (ofString rest) - , testCase "parsed Content-Length header validates after framing" $ do - let check input expected = - result (evalTricu allTestLibsEnv (parseTricu input)) @?= expected - check ("matchResult (code msg : err code msg) " - ++ "(headers rest : contentLength headers) " - ++ "(parseHeaders \"Content-Length: 42\\r\\n\\r\\nbody\")") - (parserOk (justT (ofNumber 42)) Leaf) - check "contentLength [(pair \"content-length\" \"42\") (pair \"content-length\" \"99\")]" - (parserErr (ofNumber 400) (ofString "Bad Request\n")) - forM_ ["Content-Length: 1\r\nCONTENT-LENGTH: 1\r\n\r\n", - "Transfer-Encoding: chunked\r\n\r\n"] $ \raw -> - check ("bindResult (parseHeaders " ++ show raw ++ ") (headers rest : contentLength headers)") - (parserErr (ofNumber 400) (ofString "Bad Request\n")) - check ("bindResult (parseHeaders \"Content-Length:\\t 00042 \\t\\r\\n\\r\\nbody\") " ++ - "(headers rest : contentLength headers)") (parserOk (justT (ofNumber 42)) Leaf) - , testCase "parseContentLengthValue accepts max body bytes" $ do - let input = "matchResult \"err\" (maybeLen rest : \"ok\") (parseContentLengthValue \"1048576\")" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= ofString "ok" - - , testCase "parseContentLengthValue accepts shorter decimal below max" $ do - let input = "matchResult \"err\" (maybeLen rest : \"ok\") (parseContentLengthValue \"999999\")" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= ofString "ok" - - , testCase "parseContentLengthValue strips leading zeros before limit check" $ do - let input = "parseContentLengthValue \"0000000000001\"" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= parserOk (justT (ofNumber 1)) Leaf - - , testCase "parseContentLengthValue rejects body above max" $ do - let input = "parseContentLengthValue \"1048577\"" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= parserErr (ofNumber 413) (ofString "Request body too large\n") - - , testCase "parseContentLengthValue rejects longer body above max" $ do - let input = "parseContentLengthValue \"2000000\"" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= parserErr (ofNumber 413) (ofString "Request body too large\n") - - -- statusLine / headerLine - , testCase "statusLine 200 OK" $ do - let input = "statusLine 200 \"OK\"" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= ofString "HTTP/1.1 200 OK\r\n" - - , testCase "headerLine Content-Length" $ do - let input = "headerLine \"Content-Length\" \"42\"" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= ofString "Content-Length: 42\r\n" - - -- statusPhrase - , testCase "statusPhrase 200" $ do - let input = "statusPhrase 200" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= ofString "OK" - - , testCase "statusPhrase 201" $ do - let input = "statusPhrase 201" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= ofString "Created" - - , testCase "statusPhrase 204" $ do - let input = "statusPhrase 204" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= ofString "No Content" - - , testCase "statusPhrase 400" $ do - let input = "statusPhrase 400" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= ofString "Bad Request" - - , testCase "statusPhrase 404" $ do - let input = "statusPhrase 404" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= ofString "Not Found" - - , testCase "statusPhrase 405" $ do - let input = "statusPhrase 405" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= ofString "Method Not Allowed" - - , testCase "statusPhrase 431" $ do - let input = "statusPhrase 431" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= ofString "Request Header Fields Too Large" - - , testCase "statusPhrase 501" $ do - let input = "statusPhrase 501" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= ofString "Not Implemented" - - , testCase "statusPhrase 505" $ do - let input = "statusPhrase 505" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= ofString "HTTP Version Not Supported" - - , testCase "statusPhrase 500" $ do - let input = "statusPhrase 500" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= ofString "Internal Server Error" - - , testCase "statusPhrase unknown" $ do - let input = "statusPhrase 999" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= ofString "Internal Server Error" - - -- buildResponse - , testCase "buildResponse 200 no headers" $ do - let input = "buildResponse 200 [] \"hi\"" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= ofString "HTTP/1.1 200 OK\r\n\r\nhi" - - , testCase "buildResponse 404 with header" $ do - let input = "buildResponse 404 [(pair \"Content-Length\" \"9\")] \"Not found\"" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= ofString "HTTP/1.1 404 Not Found\r\nContent-Length: 9\r\n\r\nNot found" - - -- convenience responses - , testCase "okResponse" $ do - let input = "okResponse \"hi\"" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= ofString "HTTP/1.1 200 OK\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: 2\r\nConnection: close\r\n\r\nhi" - - , testCase "notFoundResponse" $ do - let input = "notFoundResponse" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= ofString "HTTP/1.1 404 Not Found\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: 10\r\nConnection: close\r\n\r\nNot found\n" - - , testCase "textResponse" $ do - let input = "textResponse \"hi\"" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= ofString "HTTP/1.1 200 OK\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: 2\r\nConnection: close\r\n\r\nhi" - - , testCase "jsonResponse" $ do - let input = "jsonResponse \"{}\"" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= ofString "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 2\r\nConnection: close\r\n\r\n{}" - - , testCase "createdResponse" $ do - let input = "createdResponse \"created\\n\"" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= ofString "HTTP/1.1 201 Created\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: 8\r\nConnection: close\r\n\r\ncreated\n" - - , testCase "emptyResponse 204" $ do - let input = "emptyResponse 204" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= ofString "HTTP/1.1 204 No Content\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" - - , testCase "badRequestResponse" $ do - let input = "badRequestResponse \"Bad Request\\n\"" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= ofString "HTTP/1.1 400 Bad Request\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: 12\r\nConnection: close\r\n\r\nBad Request\n" - - , testCase "errorResponse 405" $ do - let input = "errorResponse 405 \"Method Not Allowed\\n\"" - env = evalTricu allTestLibsEnv (parseTricu input) - result env @?= ofString "HTTP/1.1 405 Method Not Allowed\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: 19\r\nConnection: close\r\n\r\nMethod Not Allowed\n" - ] - containsAll :: String -> [String] -> Assertion containsAll text needles = forM_ needles $ \needle -> diff --git a/tricu.cabal b/tricu.cabal @@ -235,3 +235,5 @@ test-suite tricu-tests TreeObservation Wire SupportFixture + LibraryValueTests + ReferenceFixtures