Spec.hs (162216B)
1 module Main where 2 3 import Eval 4 import FileEval 5 import Lexer 6 import Parser 7 import REPL 8 import Research 9 import Wire 10 import ContentStore 11 import ContentStore.Bundle 12 import Module.Manifest 13 import Module.Resolver 14 import IODriver (IOPermissions(..), checkIOSentinel, runIO, runIOWithEnv, runIOWith, unsafePerms, defaultPerms) 15 16 import Control.Exception (bracket, evaluate, try, SomeException) 17 import System.IO.Unsafe (unsafePerformIO) 18 import qualified Network.Socket as NS 19 import Control.Monad (forM, forM_) 20 import Control.Monad.IO.Class (liftIO) 21 import System.IO.Temp (withSystemTempDirectory) 22 import System.Directory (createDirectory, doesFileExist, doesDirectoryExist, listDirectory, getCurrentDirectory) 23 import System.FilePath ((</>)) 24 import Data.Bits (xor) 25 import Data.Char (digitToInt) 26 import Data.List (find, isInfixOf) 27 import Data.Text (Text, unpack, pack) 28 import Data.Word (Word8) 29 import Test.Tasty 30 import Test.Tasty.HUnit 31 import Text.Megaparsec (runParser) 32 33 import Data.ByteString (ByteString) 34 import qualified Data.Foldable as Foldable 35 import qualified Data.ByteString as BS 36 import qualified Data.Map as Map 37 import qualified Data.Sequence as Seq 38 import qualified Data.Set as Set 39 import qualified Data.Vector as V 40 41 main :: IO () 42 main = defaultMain tests 43 44 tricuTestString :: String -> String 45 tricuTestString s = show $ result (evalTricu Map.empty $ parseTricu s) 46 47 testStore :: StorePath 48 testStore = StorePath "/tmp/tricu-test-store" 49 {-# NOINLINE testStore #-} 50 51 allTestLibsEnv :: Env 52 allTestLibsEnv = unsafePerformIO $ do 53 base <- evaluateFile "./lib/base.tri" 54 bytes <- evaluateFile "./lib/bytes.tri" 55 bin <- evaluateFile "./lib/binary.tri" 56 http <- evaluateFile "./lib/http.tri" 57 arbor <- evaluateFile "./lib/arboricx/arboricx.tri" 58 io <- evaluateFile "./lib/io.tri" 59 sock <- evaluateFile "./lib/socket.tri" 60 intensional <- evaluateFile "./lib/intensionalContracts.tri" 61 pure (injectKernel (Map.unions [base, bytes, bin, http, arbor, io, sock, intensional])) 62 {-# NOINLINE allTestLibsEnv #-} 63 64 tests :: TestTree 65 tests = testGroup "Tricu Tests" 66 [ lexer 67 , parser 68 , simpleEvaluation 69 , lambdas 70 , arithmetic 71 , providedLibraries 72 , contractsTests 73 , maybeTests 74 , fileEval 75 , demos 76 , decoding 77 , elimLambdaSingle 78 , stressElimLambda 79 , byteMarshallingTests 80 , wireTests 81 , tricuReaderTests 82 , byteListUtilities 83 , binaryParserTests 84 , httpParsingTests 85 , contentStoreTests 86 , ioDriverTests 87 ] 88 89 lexer :: TestTree 90 lexer = testGroup "Lexer Tests" 91 [ testCase "Lex simple identifiers" $ do 92 let input = "x a b = a" 93 expect = Right [LIdentifier "x", LIdentifier "a", LIdentifier "b", LAssign, LIdentifier "a"] 94 runParser tricuLexer "" input @?= expect 95 96 , testCase "Lex Tree Calculus terms" $ do 97 let input = "t t t" 98 expect = Right [LKeywordT, LKeywordT, LKeywordT] 99 runParser tricuLexer "" input @?= expect 100 101 , testCase "Lex escaped characters in strings" $ do 102 let input = "\"hello\\nworld\"" 103 expect = Right [LStringLiteral "hello\nworld"] 104 runParser tricuLexer "" input @?= expect 105 106 , testCase "Lex multiple escaped characters in strings" $ do 107 let input = "\"tab:\\t newline:\\n quote:\\\" backslash:\\\\\"" 108 expect = Right [LStringLiteral "tab:\t newline:\n quote:\" backslash:\\"] 109 runParser tricuLexer "" input @?= expect 110 111 , testCase "Lex escaped characters in string literals" $ do 112 let input = "x = \"line1\\nline2\\tindented\"" 113 expect = Right [LIdentifier "x", LAssign, LStringLiteral "line1\nline2\tindented"] 114 runParser tricuLexer "" input @?= expect 115 116 , testCase "Lex empty string with escape sequence" $ do 117 let input = "\"\\\"\"" 118 expect = Right [LStringLiteral "\""] 119 runParser tricuLexer "" input @?= expect 120 121 , testCase "Lex mixed literals" $ do 122 let input = "t \"string\" 42" 123 expect = Right [LKeywordT, LStringLiteral "string", LIntegerLiteral 42] 124 runParser tricuLexer "" input @?= expect 125 126 , testCase "Lex invalid token" $ do 127 let input = "&invalid" 128 case runParser tricuLexer "" input of 129 Left _ -> return () 130 Right _ -> assertFailure "Expected lexer to fail on invalid token" 131 132 , testCase "Drop trailing whitespace in definitions" $ do 133 let input = "x = 5 " 134 expect = [LIdentifier "x",LAssign,LIntegerLiteral 5] 135 case (runParser tricuLexer "" input) of 136 Left _ -> assertFailure "Failed to lex input" 137 Right i -> i @?= expect 138 139 , testCase "Error when using invalid characters in identifiers" $ do 140 case (runParser tricuLexer "" "!result = 5") of 141 Left _ -> return () 142 Right _ -> assertFailure "Expected failure when trying to assign the value of !result" 143 144 , testCase "Lex <| as arrow-left token" $ do 145 let input = "f <| g" 146 expect = Right [LIdentifier "f", LArrowLeft, LIdentifier "g"] 147 runParser tricuLexer "" input @?= expect 148 149 , testCase "Lex <| without surrounding spaces" $ do 150 let input = "a<|b" 151 expect = Right [LIdentifier "a", LArrowLeft, LIdentifier "b"] 152 runParser tricuLexer "" input @?= expect 153 154 , testCase "Lex |> as arrow-right token" $ do 155 let input = "f |> g" 156 expect = Right [LIdentifier "f", LArrowRight, LIdentifier "g"] 157 runParser tricuLexer "" input @?= expect 158 159 , testCase "Lex |> without surrounding spaces" $ do 160 let input = "a|>b" 161 expect = Right [LIdentifier "a", LArrowRight, LIdentifier "b"] 162 runParser tricuLexer "" input @?= expect 163 164 , testCase "Lex <- as bind arrow token" $ do 165 let input = "x <- action" 166 expect = Right [LIdentifier "x", LBindArrow, LIdentifier "action"] 167 runParser tricuLexer "" input @?= expect 168 169 , testCase "Lex $ remains legal identifier char" $ do 170 let input = "foo$bar = 1" 171 expect = Right [LIdentifier "foo$bar", LAssign, LIntegerLiteral 1] 172 runParser tricuLexer "" input @?= expect 173 174 , testCase "Lex @ and =@ as annotation tokens" $ do 175 let input = "f x@Bool =@String x" 176 expect = Right 177 [ LIdentifier "f" 178 , LIdentifier "x" 179 , LAt 180 , LIdentifier "Bool" 181 , LAssignAt 182 , LIdentifier "String" 183 , LIdentifier "x" 184 ] 185 runParser tricuLexer "" input @?= expect 186 ] 187 188 parser :: TestTree 189 parser = testGroup "Parser Tests" 190 [ testCase "Error when assigning a value to T" $ do 191 let tokens = lexTricu "t = x" 192 case parseSingleExpr tokens of 193 Left _ -> return () 194 Right _ -> assertFailure "Expected failure when trying to assign the value of T" 195 196 , testCase "Parse function definitions" $ do 197 let input = "x = (a b c : a)" 198 expect = SDef "x" [] (SLambda ["a"] (SLambda ["b"] (SLambda ["c"] (SVar "a" Nothing)))) 199 parseSingle input @?= expect 200 201 , testCase "Parse nested Tree Calculus terms" $ do 202 let input = "t (t t) t" 203 expect = SApp (SApp TLeaf (SApp TLeaf TLeaf)) TLeaf 204 parseSingle input @?= expect 205 206 , testCase "Parse sequential Tree Calculus terms" $ do 207 let input = "t t t" 208 expect = SApp (SApp TLeaf TLeaf) TLeaf 209 parseSingle input @?= expect 210 211 , testCase "Parse mixed list literals" $ do 212 let input = "[t (\"hello\") t]" 213 expect = SList [TLeaf, SStr "hello", TLeaf] 214 parseSingle input @?= expect 215 216 , testCase "Parse function with applications" $ do 217 let input = "f = (x : t x)" 218 expect = SDef "f" [] (SLambda ["x"] (SApp TLeaf (SVar "x" Nothing))) 219 parseSingle input @?= expect 220 221 , testCase "Parse nested lists" $ do 222 let input = "[t [(t t)]]" 223 expect = SList [TLeaf,SList [SApp TLeaf TLeaf]] 224 parseSingle input @?= expect 225 226 , testCase "Parse complex parentheses" $ do 227 let input = "t (t t (t t))" 228 expect = SApp TLeaf (SApp (SApp TLeaf TLeaf) (SApp TLeaf TLeaf)) 229 parseSingle input @?= expect 230 231 , testCase "Parse empty list" $ do 232 let input = "[]" 233 expect = SList [] 234 parseSingle input @?= expect 235 236 , testCase "Parse multiple nested lists" $ do 237 let input = "[[t t] [t (t t)]]" 238 expect = SList [SList [TLeaf,TLeaf],SList [TLeaf,SApp TLeaf TLeaf]] 239 parseSingle input @?= expect 240 241 , testCase "Parse whitespace variance" $ do 242 let input1 = "[t t]" 243 let input2 = "[ t t ]" 244 expect = SList [TLeaf, TLeaf] 245 parseSingle input1 @?= expect 246 parseSingle input2 @?= expect 247 248 , testCase "Parse string in list" $ do 249 let input = "[(\"hello\")]" 250 expect = SList [SStr "hello"] 251 parseSingle input @?= expect 252 253 , testCase "Parse parentheses inside list" $ do 254 let input = "[t (t t)]" 255 expect = SList [TLeaf,SApp TLeaf TLeaf] 256 parseSingle input @?= expect 257 258 , testCase "Parse nested parentheses in function body" $ do 259 let input = "f = (x : t (t (t t)))" 260 expect = SDef "f" [] (SLambda ["x"] (SApp TLeaf (SApp TLeaf (SApp TLeaf TLeaf)))) 261 parseSingle input @?= expect 262 263 , testCase "Parse lambda abstractions" $ do 264 let input = "(a : a)" 265 expect = (SLambda ["a"] (SVar "a" Nothing)) 266 parseSingle input @?= expect 267 268 , testCase "Parse multiple arguments to lambda abstractions" $ do 269 let input = "x = (a b : a)" 270 expect = SDef "x" [] (SLambda ["a"] (SLambda ["b"] (SVar "a" Nothing))) 271 parseSingle input @?= expect 272 273 , testCase "Parse top-level definition arguments" $ do 274 let input = "const a b = a" 275 expect = SDef "const" ["a", "b"] (SVar "a" Nothing) 276 parseSingle input @?= expect 277 278 , testCase "Evaluate top-level definition arguments" $ do 279 tricuTestString "const a b = a\nconst 1 2" @?= "Fork (Stem Leaf) Leaf" 280 281 , testCase "Parse annotated definition binders" $ do 282 let input = "foo x@Bool xs@(List Bool) =@String x" 283 expect = SDefAnn 284 "foo" 285 [DefBinder "x" (Just (VEName "Bool")), DefBinder "xs" (Just (VEApp (VEName "List") (VEName "Bool")))] 286 (Just (VEName "String")) 287 (SVar "x" Nothing) 288 parseSingle input @?= expect 289 290 , testCase "Parse phantom tail annotations" $ do 291 let input = "foo x@Bool @(List Bool) =@String x" 292 expect = SDefAnn 293 "foo" 294 [DefBinder "x" (Just (VEName "Bool")), DefPhantom (VEApp (VEName "List") (VEName "Bool"))] 295 (Just (VEName "String")) 296 (SVar "x" Nothing) 297 parseSingle input @?= expect 298 299 , testCase "Parse pure phantom function annotation" $ do 300 let input = "foo @Bool @(Fn [Bool] String) =@Unit (x : x)" 301 expect = SDefAnn 302 "foo" 303 [DefPhantom (VEName "Bool"), DefPhantom (VEApp (VEApp (VEName "Fn") (VEList [VEName "Bool"])) (VEName "String"))] 304 (Just (VEName "Unit")) 305 (SLambda ["x"] (SVar "x" Nothing)) 306 parseSingle input @?= expect 307 308 , testCase "Evaluate annotated definition applies runtime contract boundary" $ do 309 let input = unlines 310 [ "bool? = guardC \"not a boolean\" (b : or? (equal? b true) (equal? b false))" 311 , "id x@bool? =@bool? x" 312 , "main = id true" 313 ] 314 env = evalTricu allTestLibsEnv (parseTricu input) 315 result env @?= trueT 316 317 , testCase "Reject named binders after phantom annotations" $ do 318 let tokens = lexTricu "foo @Bool x@Bool =@Bool x" 319 case parseSingleExpr tokens of 320 Left _ -> return () 321 Right ast -> assertFailure $ "Expected parse failure, got " ++ show ast 322 323 , testCase "Unparenthesized annotation names remain ordinary aliases" $ do 324 let input = "foo x@List Bool =@Bool x" 325 expect = SDefAnn "foo" 326 [DefBinder "x" (Just (VEName "List")), DefBinder "Bool" Nothing] 327 (Just (VEName "Bool")) 328 (SVar "x" Nothing) 329 parseSingle input @?= expect 330 331 , testCase "Parse let expression" $ do 332 let input = "let x = t t in x" 333 expect = SLet "x" (SApp TLeaf TLeaf) (SVar "x" Nothing) 334 parseSingle input @?= expect 335 336 , testCase "Evaluate let expression" $ do 337 tricuTestString "let x = 1 in x" @?= "Fork (Stem Leaf) Leaf" 338 339 , testCase "Parse let function binding" $ do 340 let input = "let f x = x in f t" 341 expect = SLet "f" (SLambda ["x"] (SVar "x" Nothing)) 342 (SApp (SVar "f" Nothing) TLeaf) 343 parseSingle input @?= expect 344 345 , testCase "Parse where expression" $ do 346 let input = "x where x = t t" 347 expect = SLet "x" (SApp TLeaf TLeaf) (SVar "x" Nothing) 348 parseSingle input @?= expect 349 350 , testCase "Evaluate where expression" $ do 351 tricuTestString "x where x = 1" @?= "Fork (Stem Leaf) Leaf" 352 353 , testCase "Parse where binding with arguments (SLet)" $ do 354 let input = "f 3 where f x = x" 355 expect = SLet "f" (SLambda ["x"] (SVar "x" Nothing)) 356 (SApp (SVar "f" Nothing) (SInt 3)) 357 parseSingle input @?= expect 358 359 , testCase "Evaluate where binding with arguments matches applied lambda" $ do 360 tricuTestString "f (t t) where f x = t x x" 361 @?= tricuTestString "(f : f (t t)) (x : t x x)" 362 363 , testCase "Evaluate nested let bindings" $ do 364 tricuTestString "let a = t t in let b = t in t a b" 365 @?= tricuTestString "t (t t) t" 366 367 , testCase "Inner let binding shadows outer binding" $ do 368 tricuTestString "let x = t in let x = t t in x" 369 @?= tricuTestString "t t" 370 371 , testCase "Parse indented multiline definition body" $ do 372 let input = "x =\n t\n t" 373 expect = SDef "x" [] (SApp TLeaf TLeaf) 374 parseSingle input @?= expect 375 376 , testCase "Evaluate indented multiline let" $ do 377 tricuTestString "let\n x =\n 1\nin\n x" @?= "Fork (Stem Leaf) Leaf" 378 379 , testCase "Evaluate indented multiline where" $ do 380 tricuTestString "x\n where x =\n 1" @?= "Fork (Stem Leaf) Leaf" 381 382 , testCase "Parse explicit custom-bind do" $ do 383 let input = "do bind\n x <- pure t\n pure x" 384 expect = SApp 385 (SApp (SVar "bind" Nothing) (SApp (SVar "pure" Nothing) TLeaf)) 386 (SLambda ["x"] (SApp (SVar "pure" Nothing) (SVar "x" Nothing))) 387 parseSingle input @?= expect 388 389 , testCase "Parse do statement without binder" $ do 390 let input = "do bind\n pure t\n pure t" 391 expect = SApp 392 (SApp (SVar "bind" Nothing) (SApp (SVar "pure" Nothing) TLeaf)) 393 (SLambda ["_"] (SApp (SVar "pure" Nothing) TLeaf)) 394 parseSingle input @?= expect 395 396 , testCase "Reject bare do without explicit bind operator" $ do 397 parsed <- try (evaluate (parseSingle "do\n x <- pure t\n pure x")) :: IO (Either SomeException TricuAST) 398 case parsed of 399 Left _ -> pure () 400 Right _ -> assertFailure "Expected bare do to fail" 401 402 , testCase "Grouping T terms with parentheses in function application" $ do 403 let input = "x = (a : a)\nx (t)" 404 expect = [SDef "x" [] (SLambda ["a"] (SVar "a" Nothing)),SApp (SVar "x" Nothing) TLeaf] 405 parseTricu input @?= expect 406 407 , testCase "Comments 1" $ do 408 let input = "(t) (t) -- (t)" 409 expect = [SApp TLeaf TLeaf] 410 parseTricu input @?= expect 411 412 , testCase "Comments 2" $ do 413 let input = "(t) -- (t) -- (t)" 414 expect = [TLeaf] 415 parseTricu input @?= expect 416 417 , testCase "Parse <| as low-precedence application" $ do 418 let input = "f x <| g y" 419 expect = SApp (SApp (SVar "f" Nothing) (SVar "x" Nothing)) 420 (SApp (SVar "g" Nothing) (SVar "y" Nothing)) 421 parseSingle input @?= expect 422 423 , testCase "Parse chained <| as left-associative" $ do 424 let input = "f <| g <| h" 425 expect = SApp (SApp (SVar "f" Nothing) (SVar "g" Nothing)) 426 (SVar "h" Nothing) 427 parseSingle input @?= expect 428 429 , testCase "Parse <| after newline inside parens" $ do 430 let input = "(f x <|\n g y)" 431 expect = SApp (SApp (SVar "f" Nothing) (SVar "x" Nothing)) 432 (SApp (SVar "g" Nothing) (SVar "y" Nothing)) 433 parseSingle input @?= expect 434 435 , testCase "Parse <| in lambda body" $ do 436 let input = "(x : f x <| g)" 437 expect = SLambda ["x"] (SApp (SApp (SVar "f" Nothing) (SVar "x" Nothing)) 438 (SVar "g" Nothing)) 439 parseSingle input @?= expect 440 441 , testCase "Parse |> as low-precedence application" $ do 442 let input = "f x |> g y" 443 expect = SApp (SApp (SVar "g" Nothing) (SVar "y" Nothing)) 444 (SApp (SVar "f" Nothing) (SVar "x" Nothing)) 445 parseSingle input @?= expect 446 447 , testCase "Parse chained |> as left-associative" $ do 448 let input = "f |> g |> h" 449 expect = SApp (SVar "h" Nothing) 450 (SApp (SVar "g" Nothing) (SVar "f" Nothing)) 451 parseSingle input @?= expect 452 453 , testCase "Parse |> after newline inside parens" $ do 454 let input = "(f x |>\n g y)" 455 expect = SApp (SApp (SVar "g" Nothing) (SVar "y" Nothing)) 456 (SApp (SVar "f" Nothing) (SVar "x" Nothing)) 457 parseSingle input @?= expect 458 459 , testCase "Parse |> in lambda body" $ do 460 let input = "(x : f x |> g)" 461 expect = SLambda ["x"] (SApp (SVar "g" Nothing) 462 (SApp (SVar "f" Nothing) (SVar "x" Nothing))) 463 parseSingle input @?= expect 464 465 , testCase "Parse mixed <| and |>" $ do 466 let input = "f |> g <| h" 467 expect = SApp (SApp (SVar "g" Nothing) (SVar "f" Nothing)) 468 (SVar "h" Nothing) 469 parseSingle input @?= expect 470 471 , testCase "Parse forward pipe chain" $ do 472 let input = "x |> f |> g" 473 expect = SApp (SVar "g" Nothing) 474 (SApp (SVar "f" Nothing) (SVar "x" Nothing)) 475 parseSingle input @?= expect 476 477 , testCase "Parse backward pipe" $ do 478 let input = "f <| x" 479 expect = SApp (SVar "f" Nothing) (SVar "x" Nothing) 480 parseSingle input @?= expect 481 482 , testCase "Parse backward pipe chain left associative" $ do 483 let input = "f <| x <| y" 484 expect = SApp (SApp (SVar "f" Nothing) (SVar "x" Nothing)) 485 (SVar "y" Nothing) 486 parseSingle input @?= expect 487 488 , testCase "Parse newline after forward pipe" $ do 489 let input = "x |>\nf" 490 expect = SApp (SVar "f" Nothing) (SVar "x" Nothing) 491 parseSingle input @?= expect 492 493 , testCase "Parse newline after backward pipe" $ do 494 let input = "f <|\nx" 495 expect = SApp (SVar "f" Nothing) (SVar "x" Nothing) 496 parseSingle input @?= expect 497 ] 498 499 simpleEvaluation :: TestTree 500 simpleEvaluation = testGroup "Evaluation Tests" 501 [ testCase "Evaluate single Leaf" $ do 502 let input = "t" 503 let ast = parseSingle input 504 (result $ evalSingle Map.empty ast) @?= Leaf 505 506 , testCase "Evaluate single Stem" $ do 507 let input = "t t" 508 let ast = parseSingle input 509 (result $ evalSingle Map.empty ast) @?= Stem Leaf 510 511 , testCase "Evaluate single Fork" $ do 512 let input = "t t t" 513 let ast = parseSingle input 514 (result $ evalSingle Map.empty ast) @?= Fork Leaf Leaf 515 516 , testCase "Evaluate nested Fork and Stem" $ do 517 let input = "t (t t) t" 518 let ast = parseSingle input 519 (result $ evalSingle Map.empty ast) @?= Fork (Stem Leaf) Leaf 520 521 , testCase "Evaluate `not` function" $ do 522 let input = "t (t (t t) (t t t)) t" 523 let ast = parseSingle input 524 (result $ evalSingle Map.empty ast) @?= 525 Fork (Fork (Stem Leaf) (Fork Leaf Leaf)) Leaf 526 527 , testCase "Environment updates with definitions" $ do 528 let input = "x = t\ny = x" 529 env = evalTricu Map.empty (parseTricu input) 530 Map.lookup "x" env @?= Just Leaf 531 Map.lookup "y" env @?= Just Leaf 532 533 , testCase "Variable substitution" $ do 534 let input = "x = t t\ny = t x\ny" 535 env = evalTricu Map.empty (parseTricu input) 536 (result env) @?= Stem (Stem Leaf) 537 538 , testCase "Multiline input evaluation" $ do 539 let input = "x = t\ny = t t\nx" 540 env = evalTricu Map.empty (parseTricu input) 541 (result env) @?= Leaf 542 543 , testCase "Evaluate string literal" $ do 544 let input = "\"hello\"" 545 let ast = parseSingle input 546 (result $ evalSingle Map.empty ast) @?= ofString "hello" 547 548 , testCase "Evaluate list literal" $ do 549 let input = "[t (t t)]" 550 let ast = parseSingle input 551 (result $ evalSingle Map.empty ast) @?= ofList [Leaf, Stem Leaf] 552 553 , testCase "Evaluate empty list" $ do 554 let input = "[]" 555 let ast = parseSingle input 556 (result $ evalSingle Map.empty ast) @?= ofList [] 557 558 , testCase "Evaluate variable dependency chain" $ do 559 let input = "x = t (t t)\n \ 560 \ y = x\n \ 561 \ z = y\n \ 562 \ variablewithamuchlongername = z\n \ 563 \ variablewithamuchlongername" 564 env = evalTricu Map.empty (parseTricu input) 565 (result env) @?= (Stem (Stem Leaf)) 566 567 568 , testCase "Immutable definitions" $ do 569 let input = "x = t t\nx = t\nx" 570 env = evalTricu Map.empty (parseTricu input) 571 result <- try (evaluate (tricuTestString input)) :: IO (Either SomeException String) 572 case result of 573 Left _ -> return () 574 Right _ -> assertFailure "Expected evaluation error" 575 576 577 , testCase "Apply identity to Boolean Not" $ do 578 let not = "(t (t (t t) (t t t)) t)" 579 let input = "x = (a : a)\nx " ++ not 580 env = evalTricu Map.empty (parseTricu input) 581 result env @?= Fork (Fork (Stem Leaf) (Fork Leaf Leaf)) Leaf 582 ] 583 584 lambdas :: TestTree 585 lambdas = testGroup "Lambda Evaluation Tests" 586 [ testCase "Lambda Identity Function" $ do 587 let input = "id = (x : x)\nid t" 588 tricuTestString input @?= "Leaf" 589 590 , testCase "Lambda Constant Function (K combinator)" $ do 591 let input = "k = (x y : x)\nk t (t t)" 592 tricuTestString input @?= "Leaf" 593 594 , testCase "Lambda Application with Variable" $ do 595 let input = "id = (x : x)\nval = t t\nid val" 596 tricuTestString input @?= "Stem Leaf" 597 598 , testCase "Lambda Application with Multiple Arguments" $ do 599 let input = "apply = (f x y : f x y)\nk = (a b : a)\napply k t (t t)" 600 tricuTestString input @?= "Leaf" 601 602 , testCase "Nested Lambda Application" $ do 603 let input = "apply = (f x y : f x y)\nid = (x : x)\napply (f x : f x) id t" 604 tricuTestString input @?= "Leaf" 605 606 , testCase "Lambda with a complex body" $ do 607 let input = "f = (x : t (t x))\nf t" 608 tricuTestString input @?= "Stem (Stem Leaf)" 609 610 , testCase "Lambda returning a function" $ do 611 let input = "f = (x : (y : x))\ng = f t\ng (t t)" 612 tricuTestString input @?= "Leaf" 613 614 , testCase "Lambda with Shadowing" $ do 615 let input = "f = (x : (x : x))\nf t (t t)" 616 tricuTestString input @?= "Stem Leaf" 617 618 , testCase "Lambda returning another lambda" $ do 619 let input = "k = (x : (y : x))\nk_app = k t\nk_app (t t)" 620 tricuTestString input @?= "Leaf" 621 622 , testCase "Lambda with free variables" $ do 623 let input = "y = t t\nf = (x : y)\nf t" 624 tricuTestString input @?= "Stem Leaf" 625 626 , testCase "SKI Composition" $ do 627 let input = "s = (x y z : x z (y z))\nk = (x y : x)\ni = (x : x)\ncomp = s k i\ncomp t (t t)" 628 tricuTestString input @?= "Stem (Stem Leaf)" 629 630 , testCase "Lambda with multiple parameters and application" $ do 631 let input = "f = (a b c : t a b c)\nf t (t t) (t t t)" 632 tricuTestString input @?= "Stem Leaf" 633 634 , testCase "Lambda with nested application in the body" $ do 635 let input = "f = (x : t (t (t x)))\nf t" 636 tricuTestString input @?= "Stem (Stem (Stem Leaf))" 637 638 , testCase "Lambda returning a function and applying it" $ do 639 let input = "f = (x : (y : t x y))\ng = f t\ng (t t)" 640 tricuTestString input @?= "Fork Leaf (Stem Leaf)" 641 642 , testCase "Lambda applying a variable" $ do 643 let input = "id = (x : x)\na = t t\nid a" 644 tricuTestString input @?= "Stem Leaf" 645 646 , testCase "Nested lambda abstractions in the same expression" $ do 647 let input = "f = (x : (y : x y))\ng = (z : z)\nf g t" 648 tricuTestString input @?= "Leaf" 649 650 , testCase "Lambda applied to string literal" $ do 651 let input = "f = (x : x)\nf \"hello\"" 652 tricuTestString input @?= "Fork (Fork Leaf (Fork Leaf (Fork Leaf (Fork (Stem Leaf) (Fork Leaf (Fork (Stem Leaf) (Fork (Stem Leaf) Leaf))))))) (Fork (Fork (Stem Leaf) (Fork Leaf (Fork (Stem Leaf) (Fork Leaf (Fork Leaf (Fork (Stem Leaf) (Fork (Stem Leaf) Leaf))))))) (Fork (Fork Leaf (Fork Leaf (Fork (Stem Leaf) (Fork (Stem Leaf) (Fork Leaf (Fork (Stem Leaf) (Fork (Stem Leaf) Leaf))))))) (Fork (Fork Leaf (Fork Leaf (Fork (Stem Leaf) (Fork (Stem Leaf) (Fork Leaf (Fork (Stem Leaf) (Fork (Stem Leaf) Leaf))))))) (Fork (Fork (Stem Leaf) (Fork (Stem Leaf) (Fork (Stem Leaf) (Fork (Stem Leaf) (Fork Leaf (Fork (Stem Leaf) (Fork (Stem Leaf) Leaf))))))) Leaf))))" 653 654 655 , testCase "Lambda applied to integer literal" $ do 656 let input = "f = (x : x)\nf 42" 657 tricuTestString input @?= "Fork Leaf (Fork (Stem Leaf) (Fork Leaf (Fork (Stem Leaf) (Fork Leaf (Fork (Stem Leaf) Leaf)))))" 658 659 , testCase "Lambda applied to list literal" $ do 660 let input = "f = (x : x)\nf [t (t t)]" 661 tricuTestString input @?= "Fork Leaf (Fork (Stem Leaf) Leaf)" 662 663 , testCase "Lambda containing list literal" $ do 664 let input = "(a : [(a)]) 1" 665 tricuTestString input @?= "Fork (Fork (Stem Leaf) Leaf) Leaf" 666 ] 667 668 maybeTests :: TestTree 669 maybeTests = testGroup "Maybe Tests" 670 [ testCase "nothing is Leaf" $ do 671 let input = "nothing" 672 env = evalTricu allTestLibsEnv (parseTricu input) 673 result env @?= Leaf 674 675 , testCase "just wraps value in Stem" $ do 676 let input = "just (t t)" 677 env = evalTricu allTestLibsEnv (parseTricu input) 678 result env @?= Stem (Stem Leaf) 679 680 , testCase "matchMaybe on nothing returns default" $ do 681 let input = "matchMaybe \"empty\" (x : x) nothing" 682 env = evalTricu allTestLibsEnv (parseTricu input) 683 result env @?= ofString "empty" 684 685 , testCase "matchMaybe on just extracts value" $ do 686 let input = "matchMaybe \"empty\" (x : x) (just (t t))" 687 env = evalTricu allTestLibsEnv (parseTricu input) 688 result env @?= Stem Leaf 689 690 , testCase "maybe applies f inside just" $ do 691 let input = "maybe 0 (x : succ x) (just 5)" 692 env = evalTricu allTestLibsEnv (parseTricu input) 693 result env @?= ofNumber 6 694 695 , testCase "maybe returns default on nothing" $ do 696 let input = "maybe 0 (x : succ x) nothing" 697 env = evalTricu allTestLibsEnv (parseTricu input) 698 result env @?= ofNumber 0 699 700 , testCase "maybeMap transforms just value" $ do 701 let input = "maybeMap (x : succ x) (just 3)" 702 env = evalTricu allTestLibsEnv (parseTricu input) 703 result env @?= justT (ofNumber 4) 704 705 , testCase "maybeMap returns nothing on nothing" $ do 706 let input = "maybeMap (x : succ x) nothing" 707 env = evalTricu allTestLibsEnv (parseTricu input) 708 result env @?= nothingT 709 710 , testCase "maybeBind flattens just" $ do 711 let input = "maybeBind (just 3) (x : just (succ x))" 712 env = evalTricu allTestLibsEnv (parseTricu input) 713 result env @?= justT (ofNumber 4) 714 715 , testCase "maybeBind returns nothing on nothing" $ do 716 let input = "maybeBind nothing (x : just (succ x))" 717 env = evalTricu allTestLibsEnv (parseTricu input) 718 result env @?= Leaf 719 720 , testCase "maybeOr returns just value" $ do 721 let input = "maybeOr 99 (just 5)" 722 env = evalTricu allTestLibsEnv (parseTricu input) 723 result env @?= ofNumber 5 724 725 , testCase "maybeOr returns default on nothing" $ do 726 let input = "maybeOr 99 nothing" 727 env = evalTricu allTestLibsEnv (parseTricu input) 728 result env @?= ofNumber 99 729 730 , testCase "maybe? on just is true" $ do 731 let input = "maybe? (just t)" 732 env = evalTricu allTestLibsEnv (parseTricu input) 733 result env @?= trueT 734 735 , testCase "maybe? on nothing is false" $ do 736 let input = "maybe? nothing" 737 env = evalTricu allTestLibsEnv (parseTricu input) 738 result env @?= falseT 739 ] 740 741 providedLibraries :: TestTree 742 providedLibraries = testGroup "Library Tests" 743 [ testCase "Triage test Leaf" $ do 744 let input = "test t" 745 env = decodeResult $ result $ evalTricu allTestLibsEnv (parseTricu input) 746 env @?= "\"Leaf\"" 747 748 , testCase "Triage test (Stem Leaf)" $ do 749 let input = "test (t t)" 750 env = decodeResult $ result $ evalTricu allTestLibsEnv (parseTricu input) 751 env @?= "\"Stem\"" 752 753 , testCase "Triage test (Fork Leaf Leaf)" $ do 754 let input = "test (t t t)" 755 env = decodeResult $ result $ evalTricu allTestLibsEnv (parseTricu input) 756 env @?= "\"Fork\"" 757 758 , testCase "Boolean NOT: true" $ do 759 let input = "not? true" 760 env = result $ evalTricu allTestLibsEnv (parseTricu input) 761 env @?= Leaf 762 763 , testCase "Boolean NOT: false" $ do 764 let input = "not? false" 765 env = result $ evalTricu allTestLibsEnv (parseTricu input) 766 env @?= Stem Leaf 767 768 769 , testCase "Boolean AND TF" $ do 770 let input = "and? (t t) (t)" 771 env = evalTricu allTestLibsEnv (parseTricu input) 772 result env @?= Leaf 773 774 , testCase "Boolean AND FT" $ do 775 let input = "and? (t) (t t)" 776 env = evalTricu allTestLibsEnv (parseTricu input) 777 result env @?= Leaf 778 779 , testCase "Boolean AND FF" $ do 780 let input = "and? (t) (t)" 781 env = evalTricu allTestLibsEnv (parseTricu input) 782 result env @?= Leaf 783 784 , testCase "Boolean AND TT" $ do 785 let input = "and? (t t) (t t)" 786 env = evalTricu allTestLibsEnv (parseTricu input) 787 result env @?= Stem Leaf 788 789 , testCase "List head" $ do 790 let input = "head [(t) (t t) (t t t)]" 791 env = evalTricu allTestLibsEnv (parseTricu input) 792 result env @?= Leaf 793 794 , testCase "List tail" $ do 795 let input = "head (tail (tail [(t) (t t) (t t t)]))" 796 env = evalTricu allTestLibsEnv (parseTricu input) 797 result env @?= Fork Leaf Leaf 798 799 , testCase "List map" $ do 800 let input = "head (tail (map (a : (t t t)) [(t) (t) (t)]))" 801 env = evalTricu allTestLibsEnv (parseTricu input) 802 result env @?= Fork Leaf Leaf 803 804 , testCase "Empty list check" $ do 805 let input = "emptyList? []" 806 env = evalTricu allTestLibsEnv (parseTricu input) 807 result env @?= Stem Leaf 808 809 , testCase "Non-empty list check" $ do 810 let input = "not? (emptyList? [(1) (2) (3)])" 811 env = evalTricu allTestLibsEnv (parseTricu input) 812 result env @?= Stem Leaf 813 814 , testCase "Concatenate strings" $ do 815 let input = "append \"Hello, \" \"world!\"" 816 env = decodeResult $ result $ evalTricu allTestLibsEnv (parseTricu input) 817 env @?= "\"Hello, world!\"" 818 819 , testCase "Verifying Equality" $ do 820 let input = "equal? (t t t) (t t t)" 821 env = evalTricu allTestLibsEnv (parseTricu input) 822 result env @?= Stem Leaf 823 824 , testCase "headMaybe on empty list" $ do 825 let input = "headMaybe []" 826 env = evalTricu allTestLibsEnv (parseTricu input) 827 result env @?= nothingT 828 829 , testCase "headMaybe on non-empty list" $ do 830 let input = "headMaybe [(t) (t t)]" 831 env = evalTricu allTestLibsEnv (parseTricu input) 832 result env @?= justT Leaf 833 834 , testCase "lastMaybe on empty list" $ do 835 let input = "lastMaybe []" 836 env = evalTricu allTestLibsEnv (parseTricu input) 837 result env @?= nothingT 838 839 , testCase "lastMaybe on single element" $ do 840 let input = "lastMaybe [(t t)]" 841 env = evalTricu allTestLibsEnv (parseTricu input) 842 result env @?= justT (Stem Leaf) 843 844 , testCase "lastMaybe on multi-element list" $ do 845 let input = "lastMaybe [(t) (t t) (t t t)]" 846 env = evalTricu allTestLibsEnv (parseTricu input) 847 result env @?= justT (Fork Leaf Leaf) 848 849 , testCase "nthMaybe first element" $ do 850 let input = "nthMaybe 0 [(t) (t t)]" 851 env = evalTricu allTestLibsEnv (parseTricu input) 852 result env @?= justT Leaf 853 854 , testCase "nthMaybe middle element" $ do 855 let input = "nthMaybe 1 [(t) (t t) (t t t)]" 856 env = evalTricu allTestLibsEnv (parseTricu input) 857 result env @?= justT (Stem Leaf) 858 859 , testCase "nthMaybe out of bounds" $ do 860 let input = "nthMaybe 5 [(t) (t t)]" 861 env = evalTricu allTestLibsEnv (parseTricu input) 862 result env @?= nothingT 863 864 , testCase "reverse empty list" $ do 865 let input = "reverse []" 866 env = evalTricu allTestLibsEnv (parseTricu input) 867 result env @?= ofList [] 868 869 , testCase "reverse non-empty list" $ do 870 let input = "reverse [(1) (2) (3)]" 871 env = evalTricu allTestLibsEnv (parseTricu input) 872 result env @?= ofList [ofNumber 3, ofNumber 2, ofNumber 1] 873 874 , testCase "take 0 any list = empty" $ do 875 let input = "take 0 [(1) (2) (3)]" 876 env = evalTricu allTestLibsEnv (parseTricu input) 877 result env @?= ofList [] 878 879 , testCase "take 2 [1,2,3] = [1,2]" $ do 880 let input = "take 2 [(1) (2) (3)]" 881 env = evalTricu allTestLibsEnv (parseTricu input) 882 result env @?= ofList [ofNumber 1, ofNumber 2] 883 884 , testCase "take overlong returns whole list" $ do 885 let input = "take 5 [(1) (2)]" 886 env = evalTricu allTestLibsEnv (parseTricu input) 887 result env @?= ofList [ofNumber 1, ofNumber 2] 888 889 , testCase "drop 0 any list = list" $ do 890 let input = "drop 0 [(1) (2) (3)]" 891 env = evalTricu allTestLibsEnv (parseTricu input) 892 result env @?= ofList [ofNumber 1, ofNumber 2, ofNumber 3] 893 894 , testCase "drop 2 [1,2,3] = [3]" $ do 895 let input = "drop 2 [(1) (2) (3)]" 896 env = evalTricu allTestLibsEnv (parseTricu input) 897 result env @?= ofList [ofNumber 3] 898 899 , testCase "drop overlong returns empty" $ do 900 let input = "drop 5 [(1) (2)]" 901 env = evalTricu allTestLibsEnv (parseTricu input) 902 result env @?= ofList [] 903 904 , testCase "splitAt 0 [1,2] = pair [] [1,2]" $ do 905 let input = "splitAt 0 [(1) (2)]" 906 env = evalTricu allTestLibsEnv (parseTricu input) 907 result env @?= pairT (ofList []) (ofList [ofNumber 1, ofNumber 2]) 908 909 , testCase "splitAt 2 [1,2,3] = pair [1,2] [3]" $ do 910 let input = "splitAt 2 [(1) (2) (3)]" 911 env = evalTricu allTestLibsEnv (parseTricu input) 912 result env @?= pairT (ofList [ofNumber 1, ofNumber 2]) (ofList [ofNumber 3]) 913 914 , testCase "splitAt overlong = pair [1,2] []" $ do 915 let input = "splitAt 5 [(1) (2)]" 916 env = evalTricu allTestLibsEnv (parseTricu input) 917 result env @?= pairT (ofList [ofNumber 1, ofNumber 2]) (ofList []) 918 919 , testCase "concatMap on empty list" $ do 920 let input = "concatMap (x : [(x) (x)]) []" 921 env = evalTricu allTestLibsEnv (parseTricu input) 922 result env @?= ofList [] 923 924 , testCase "concatMap doubles elements" $ do 925 let input = "concatMap (x : [(x) (x)]) [(1) (2)]" 926 env = evalTricu allTestLibsEnv (parseTricu input) 927 result env @?= ofList [ofNumber 1, ofNumber 1, ofNumber 2, ofNumber 2] 928 929 , testCase "find on empty list" $ do 930 let input = "find (x : equal? x 2) []" 931 env = evalTricu allTestLibsEnv (parseTricu input) 932 result env @?= nothingT 933 934 , testCase "find finds element" $ do 935 let input = "find (x : equal? x 2) [(1) (2) (3)]" 936 env = evalTricu allTestLibsEnv (parseTricu input) 937 result env @?= justT (ofNumber 2) 938 939 , testCase "find missing element" $ do 940 let input = "find (x : equal? x 9) [(1) (2) (3)]" 941 env = evalTricu allTestLibsEnv (parseTricu input) 942 result env @?= nothingT 943 944 , testCase "partition empty list" $ do 945 let input = "partition (x : equal? x 2) []" 946 env = evalTricu allTestLibsEnv (parseTricu input) 947 result env @?= pairT (ofList []) (ofList []) 948 949 , testCase "partition splits list" $ do 950 let input = "partition (x : lt? 2 x) [(1) (2) (3) (4)]" 951 env = evalTricu allTestLibsEnv (parseTricu input) 952 result env @?= pairT (ofList [ofNumber 3, ofNumber 4]) (ofList [ofNumber 1, ofNumber 2]) 953 954 , testCase "zipWith on empty lists" $ do 955 let input = "zipWith add [] []" 956 env = evalTricu allTestLibsEnv (parseTricu input) 957 result env @?= ofList [] 958 959 , testCase "zipWith adds pairwise" $ do 960 let input = "zipWith add [(1) (2)] [(10) (20)]" 961 env = evalTricu allTestLibsEnv (parseTricu input) 962 result env @?= ofList [ofNumber 11, ofNumber 22] 963 964 , testCase "zipWith truncates to shorter list" $ do 965 let input = "zipWith add [(1) (2)] [(10)]" 966 env = evalTricu allTestLibsEnv (parseTricu input) 967 result env @?= ofList [ofNumber 11] 968 969 , testCase "strLength" $ do 970 let input = "strLength \"hello\"" 971 env = evalTricu allTestLibsEnv (parseTricu input) 972 result env @?= ofNumber 5 973 974 , testCase "strAppend" $ do 975 let input = "strAppend \"hello\" \" world\"" 976 env = evalTricu allTestLibsEnv (parseTricu input) 977 result env @?= ofString "hello world" 978 979 , testCase "equal? equal strings" $ do 980 let input = "equal? \"abc\" \"abc\"" 981 env = evalTricu allTestLibsEnv (parseTricu input) 982 result env @?= trueT 983 984 , testCase "equal? different strings" $ do 985 let input = "equal? \"abc\" \"def\"" 986 env = evalTricu allTestLibsEnv (parseTricu input) 987 result env @?= falseT 988 989 , testCase "strEmpty? on empty" $ do 990 let input = "strEmpty? \"\"" 991 env = evalTricu allTestLibsEnv (parseTricu input) 992 result env @?= trueT 993 994 , testCase "strEmpty? on non-empty" $ do 995 let input = "strEmpty? \"a\"" 996 env = evalTricu allTestLibsEnv (parseTricu input) 997 result env @?= falseT 998 999 , testCase "startsWith? prefix matches" $ do 1000 let input = "startsWith? \"he\" \"hello\"" 1001 env = evalTricu allTestLibsEnv (parseTricu input) 1002 result env @?= trueT 1003 1004 , testCase "startsWith? prefix too long" $ do 1005 let input = "startsWith? \"hello\" \"he\"" 1006 env = evalTricu allTestLibsEnv (parseTricu input) 1007 result env @?= falseT 1008 1009 , testCase "startsWith? empty prefix" $ do 1010 let input = "startsWith? \"\" \"hello\"" 1011 env = evalTricu allTestLibsEnv (parseTricu input) 1012 result env @?= trueT 1013 1014 , testCase "endsWith? suffix matches" $ do 1015 let input = "endsWith? \"lo\" \"hello\"" 1016 env = evalTricu allTestLibsEnv (parseTricu input) 1017 result env @?= trueT 1018 1019 , testCase "endsWith? suffix too long" $ do 1020 let input = "endsWith? \"hello\" \"lo\"" 1021 env = evalTricu allTestLibsEnv (parseTricu input) 1022 result env @?= falseT 1023 1024 , testCase "endsWith? empty suffix" $ do 1025 let input = "endsWith? \"\" \"hello\"" 1026 env = evalTricu allTestLibsEnv (parseTricu input) 1027 result env @?= trueT 1028 1029 , testCase "contains? substring found" $ do 1030 let input = "contains? \"ell\" \"hello\"" 1031 env = evalTricu allTestLibsEnv (parseTricu input) 1032 result env @?= trueT 1033 1034 , testCase "contains? substring missing" $ do 1035 let input = "contains? \"xyz\" \"hello\"" 1036 env = evalTricu allTestLibsEnv (parseTricu input) 1037 result env @?= falseT 1038 1039 , testCase "contains? empty needle" $ do 1040 let input = "contains? \"\" \"hello\"" 1041 env = evalTricu allTestLibsEnv (parseTricu input) 1042 result env @?= trueT 1043 1044 , testCase "lines splits on newline" $ do 1045 let input = "lines \"a\\nb\\nc\"" 1046 env = evalTricu allTestLibsEnv (parseTricu input) 1047 result env @?= ofList [ofString "a", ofString "b", ofString "c"] 1048 1049 , testCase "lines single line" $ do 1050 let input = "lines \"hello\"" 1051 env = evalTricu allTestLibsEnv (parseTricu input) 1052 result env @?= ofList [ofString "hello"] 1053 1054 , testCase "lines empty string" $ do 1055 let input = "lines \"\"" 1056 env = evalTricu allTestLibsEnv (parseTricu input) 1057 result env @?= ofList [ofString ""] 1058 1059 , testCase "lines trailing newline" $ do 1060 let input = "lines \"a\\n\"" 1061 env = evalTricu allTestLibsEnv (parseTricu input) 1062 result env @?= ofList [ofString "a", ofString ""] 1063 1064 , testCase "unlines joins with newline" $ do 1065 let input = "unlines [(\"a\") (\"b\")]" 1066 env = evalTricu allTestLibsEnv (parseTricu input) 1067 result env @?= ofString "a\nb\n" 1068 1069 , testCase "unlines empty list" $ do 1070 let input = "unlines []" 1071 env = evalTricu allTestLibsEnv (parseTricu input) 1072 result env @?= ofString "" 1073 1074 , testCase "words splits on space" $ do 1075 let input = "words \"hello world\"" 1076 env = evalTricu allTestLibsEnv (parseTricu input) 1077 result env @?= ofList [ofString "hello", ofString "world"] 1078 1079 , testCase "words empty string" $ do 1080 let input = "words \"\"" 1081 env = evalTricu allTestLibsEnv (parseTricu input) 1082 result env @?= ofList [] 1083 1084 , testCase "words multiple spaces" $ do 1085 let input = "words \" hello world \"" 1086 env = evalTricu allTestLibsEnv (parseTricu input) 1087 result env @?= ofList [ofString "hello", ofString "world"] 1088 1089 , testCase "unwords joins with space" $ do 1090 let input = "unwords [(\"hello\") (\"world\")]" 1091 env = evalTricu allTestLibsEnv (parseTricu input) 1092 result env @?= ofString "hello world" 1093 1094 , testCase "unwords single word" $ do 1095 let input = "unwords [(\"hello\")]" 1096 env = evalTricu allTestLibsEnv (parseTricu input) 1097 result env @?= ofString "hello" 1098 1099 , testCase "unwords empty list" $ do 1100 let input = "unwords []" 1101 env = evalTricu allTestLibsEnv (parseTricu input) 1102 result env @?= ofString "" 1103 1104 , testCase "intercalate joins fields" $ do 1105 let input = "intercalate \", \" [(\"a\") (\"b\") (\"c\")]" 1106 env = evalTricu allTestLibsEnv (parseTricu input) 1107 result env @?= ofString "a, b, c" 1108 1109 , testCase "intercalate leaves a lone field alone" $ do 1110 let input = "intercalate \", \" [(\"a\")]" 1111 env = evalTricu allTestLibsEnv (parseTricu input) 1112 result env @?= ofString "a" 1113 1114 , testCase "intercalate empty list" $ do 1115 let input = "intercalate \", \" []" 1116 env = evalTricu allTestLibsEnv (parseTricu input) 1117 result env @?= ofString "" 1118 1119 , testCase "joinSuffix terminates every field" $ do 1120 let input = "joinSuffix \"-\" [(\"a\") (\"b\")]" 1121 env = evalTricu allTestLibsEnv (parseTricu input) 1122 result env @?= ofString "a-b-" 1123 1124 , testCase "splitOnByte splits on a byte" $ do 1125 let input = "splitOnByte 58 \"a:b:c\"" 1126 env = evalTricu allTestLibsEnv (parseTricu input) 1127 result env @?= ofList [ofString "a", ofString "b", ofString "c"] 1128 1129 , testCase "splitOnByte keeps empty fields" $ do 1130 let input = "splitOnByte 58 \"a::b\"" 1131 env = evalTricu allTestLibsEnv (parseTricu input) 1132 result env @?= ofList [ofString "a", ofString "", ofString "b"] 1133 1134 , testCase "splitOnByte trailing separator leaves an empty field" $ do 1135 let input = "splitOnByte 58 \"a:\"" 1136 env = evalTricu allTestLibsEnv (parseTricu input) 1137 result env @?= ofList [ofString "a", ofString ""] 1138 1139 , testCase "splitOnByte without a match" $ do 1140 let input = "splitOnByte 58 \"abc\"" 1141 env = evalTricu allTestLibsEnv (parseTricu input) 1142 result env @?= ofList [ofString "abc"] 1143 1144 , testCase "splitOnByte empty input" $ do 1145 let input = "splitOnByte 58 \"\"" 1146 env = evalTricu allTestLibsEnv (parseTricu input) 1147 result env @?= ofList [ofString ""] 1148 1149 , testCase "intercalate round trips splitOnByte" $ do 1150 let input = "equal? (intercalate \":\" (splitOnByte 58 \"a:b:c\")) \"a:b:c\"" 1151 env = evalTricu allTestLibsEnv (parseTricu input) 1152 result env @?= trueT 1153 1154 , testCase "takeWhile keeps the matching prefix" $ do 1155 let input = "takeWhile (n : lt? n 3) [(1) (2) (3) (1)]" 1156 env = evalTricu allTestLibsEnv (parseTricu input) 1157 result env @?= ofList [ofNumber 1, ofNumber 2] 1158 1159 , testCase "takeWhile stops at the first mismatch" $ do 1160 let input = "takeWhile (n : lt? n 3) [(3) (1)]" 1161 env = evalTricu allTestLibsEnv (parseTricu input) 1162 result env @?= ofList [] 1163 1164 , testCase "dropWhile drops the matching prefix" $ do 1165 let input = "dropWhile (n : lt? n 3) [(1) (2) (3) (1)]" 1166 env = evalTricu allTestLibsEnv (parseTricu input) 1167 result env @?= ofList [ofNumber 3, ofNumber 1] 1168 1169 , testCase "dropWhile on an all matching list" $ do 1170 let input = "dropWhile (n : lt? n 3) [(1) (2)]" 1171 env = evalTricu allTestLibsEnv (parseTricu input) 1172 result env @?= ofList [] 1173 1174 , testCase "trim strips surrounding spaces and tabs" $ do 1175 let input = "trim \" \\ttrimmed \\t\"" 1176 env = evalTricu allTestLibsEnv (parseTricu input) 1177 result env @?= ofString "trimmed" 1178 1179 , testCase "trim leaves interior bytes alone" $ do 1180 let input = "trim \" a b \"" 1181 env = evalTricu allTestLibsEnv (parseTricu input) 1182 result env @?= ofString "a b" 1183 1184 , testCase "trim all whitespace is empty" $ do 1185 let input = "trim \" \\t \"" 1186 env = evalTricu allTestLibsEnv (parseTricu input) 1187 result env @?= ofString "" 1188 ] 1189 1190 contractsTests :: TestTree 1191 contractsTests = testGroup "Contracts library tests" 1192 [ testCase "anyC passes any value" $ do 1193 let input = "main = resultIsOk (checkContract anyC 7)" 1194 env = evalTricu allTestLibsEnv (parseTricu input) 1195 result env @?= trueT 1196 1197 , testCase "guardC rejects failures" $ do 1198 let input = "main = resultIsErr (checkContract (guardC \"odd\" even?) 5)" 1199 env = evalTricu allTestLibsEnv (parseTricu input) 1200 result env @?= trueT 1201 1202 , testCase "andC combines contracts" $ do 1203 let input = "main = resultIsOk (checkContract (andC (guardC \"odd\" odd?) (guardC \"gt0\" (n : gt? n 0))) 5)" 1204 env = evalTricu allTestLibsEnv (parseTricu input) 1205 result env @?= trueT 1206 1207 , testCase "listOf checks every element" $ do 1208 let input = "main = resultIsOk (checkContract (listOf (guardC \"odd\" odd?)) [1 3 5])" 1209 env = evalTricu allTestLibsEnv (parseTricu input) 1210 result env @?= trueT 1211 1212 , testCase "fnContract wraps a function" $ do 1213 let input = "main = resultIsOk (checkContract (fnContract (guardC \"odd\" odd?) anyC) (x : x))" 1214 env = evalTricu allTestLibsEnv (parseTricu input) 1215 result env @?= trueT 1216 1217 , testCase "runM reduces pureM" $ do 1218 let input = "main = resultIsOk (runM (pureM 42))" 1219 env = evalTricu allTestLibsEnv (parseTricu input) 1220 result env @?= trueT 1221 1222 , testCase "do notation chains checkM and pureM" $ do 1223 let input = unlines 1224 [ "program = (xs :" 1225 , " do bindM" 1226 , " n <- pureM (length xs)" 1227 , " _ <- checkM (guardC \"positive\" (n : gte? n 1)) n" 1228 , " pureM n)" 1229 , "main = runM (program [1 2 3 4])" 1230 ] 1231 env = evalTricu allTestLibsEnv (parseTricu input) 1232 decodeResult (result env) @?= "[t t, 4]" 1233 1234 , testCase "handleM replaces exception with a new action" $ do 1235 let input = unlines 1236 [ "lookupConfig = (key defaultValue :" 1237 , " exceptE \"missing\" key (resume : pureM defaultValue))" 1238 , "pipeline = (xs :" 1239 , " do bindM" 1240 , " divisor <- lookupConfig \"divisor\" 1" 1241 , " total <- pureM (sum xs)" 1242 , " scaled <- liftM (x : div x divisor) total" 1243 , " pureM scaled)" 1244 , "handled = handleM \"missing\" (key k : pureM 2) (pipeline [10 20 30])" 1245 , "main = runM handled" 1246 ] 1247 env = evalTricu allTestLibsEnv (parseTricu input) 1248 decodeResult (result env) @?= "[t t, 30]" 1249 1250 , testCase "=@ return annotation passes" $ do 1251 let input = unlines 1252 [ "five =@anyC 5" 1253 , "main = five" 1254 ] 1255 env = evalTricu allTestLibsEnv (parseTricu input) 1256 result env @?= ofNumber 5 1257 1258 , testCase "=@ return annotation fails" $ do 1259 let input = unlines 1260 [ "boom =@(neverC \"boom\") 5" 1261 , "main = boom" 1262 ] 1263 env = evalTricu allTestLibsEnv (parseTricu input) 1264 decodeResult (result env) @?= "[t, \"boom\"]" 1265 1266 , testCase "@ argument annotation passes" $ do 1267 let input = unlines 1268 [ "idNat x@anyC =@anyC x" 1269 , "main = idNat 5" 1270 ] 1271 env = evalTricu allTestLibsEnv (parseTricu input) 1272 result env @?= ofNumber 5 1273 1274 , testCase "@ argument annotation fails" $ do 1275 let input = unlines 1276 [ "idNat x@(neverC \"bad\") =@anyC x" 1277 , "main = idNat 5" 1278 ] 1279 env = evalTricu allTestLibsEnv (parseTricu input) 1280 decodeResult (result env) @?= "[t, \"bad\"]" 1281 ] 1282 1283 arithmetic :: TestTree 1284 arithmetic = testGroup "Arithmetic Tests" 1285 [ testCase "isZero? on 0" $ do 1286 let input = "isZero? 0" 1287 env = evalTricu allTestLibsEnv (parseTricu input) 1288 result env @?= trueT 1289 1290 , testCase "isZero? on 5" $ do 1291 let input = "isZero? 5" 1292 env = evalTricu allTestLibsEnv (parseTricu input) 1293 result env @?= falseT 1294 1295 , testCase "add 0 3 = 3" $ do 1296 let input = "add 0 3" 1297 env = evalTricu allTestLibsEnv (parseTricu input) 1298 result env @?= ofNumber 3 1299 1300 , testCase "add 3 0 = 3" $ do 1301 let input = "add 3 0" 1302 env = evalTricu allTestLibsEnv (parseTricu input) 1303 result env @?= ofNumber 3 1304 1305 , testCase "add 2 3 = 5" $ do 1306 let input = "add 2 3" 1307 env = evalTricu allTestLibsEnv (parseTricu input) 1308 result env @?= ofNumber 5 1309 1310 , testCase "sub 5 2 = 3" $ do 1311 let input = "sub 5 2" 1312 env = evalTricu allTestLibsEnv (parseTricu input) 1313 result env @?= ofNumber 3 1314 1315 , testCase "sub 2 5 = 0 (saturated)" $ do 1316 let input = "sub 2 5" 1317 env = evalTricu allTestLibsEnv (parseTricu input) 1318 result env @?= ofNumber 0 1319 1320 , testCase "sub 5 5 = 0" $ do 1321 let input = "sub 5 5" 1322 env = evalTricu allTestLibsEnv (parseTricu input) 1323 result env @?= ofNumber 0 1324 1325 , testCase "lt? 2 3 = true" $ do 1326 let input = "lt? 2 3" 1327 env = evalTricu allTestLibsEnv (parseTricu input) 1328 result env @?= trueT 1329 1330 , testCase "lt? 3 2 = false" $ do 1331 let input = "lt? 3 2" 1332 env = evalTricu allTestLibsEnv (parseTricu input) 1333 result env @?= falseT 1334 1335 , testCase "lt? 2 2 = false" $ do 1336 let input = "lt? 2 2" 1337 env = evalTricu allTestLibsEnv (parseTricu input) 1338 result env @?= falseT 1339 1340 , testCase "lte? 2 3 = true" $ do 1341 let input = "lte? 2 3" 1342 env = evalTricu allTestLibsEnv (parseTricu input) 1343 result env @?= trueT 1344 1345 , testCase "lte? 3 2 = false" $ do 1346 let input = "lte? 3 2" 1347 env = evalTricu allTestLibsEnv (parseTricu input) 1348 result env @?= falseT 1349 1350 , testCase "lte? 2 2 = true" $ do 1351 let input = "lte? 2 2" 1352 env = evalTricu allTestLibsEnv (parseTricu input) 1353 result env @?= trueT 1354 1355 , testCase "mul 0 5 = 0" $ do 1356 let input = "mul 0 5" 1357 env = evalTricu allTestLibsEnv (parseTricu input) 1358 result env @?= ofNumber 0 1359 1360 , testCase "mul 5 0 = 0" $ do 1361 let input = "mul 5 0" 1362 env = evalTricu allTestLibsEnv (parseTricu input) 1363 result env @?= ofNumber 0 1364 1365 , testCase "mul 2 3 = 6" $ do 1366 let input = "mul 2 3" 1367 env = evalTricu allTestLibsEnv (parseTricu input) 1368 result env @?= ofNumber 6 1369 1370 , testCase "mul 3 3 = 9" $ do 1371 let input = "mul 3 3" 1372 env = evalTricu allTestLibsEnv (parseTricu input) 1373 result env @?= ofNumber 9 1374 1375 , testCase "pred 0 = 0" $ do 1376 let input = "pred 0" 1377 env = evalTricu allTestLibsEnv (parseTricu input) 1378 result env @?= ofNumber 0 1379 1380 , testCase "pred 1 = 0" $ do 1381 let input = "pred 1" 1382 env = evalTricu allTestLibsEnv (parseTricu input) 1383 result env @?= ofNumber 0 1384 1385 , testCase "pred 5 = 4" $ do 1386 let input = "pred 5" 1387 env = evalTricu allTestLibsEnv (parseTricu input) 1388 result env @?= ofNumber 4 1389 1390 , testCase "add is commutative" $ do 1391 let input = "equal? (add 4 7) (add 7 4)" 1392 env = evalTricu allTestLibsEnv (parseTricu input) 1393 result env @?= trueT 1394 1395 , testCase "add is associative" $ do 1396 let input = "equal? (add (add 2 3) 4) (add 2 (add 3 4))" 1397 env = evalTricu allTestLibsEnv (parseTricu input) 1398 result env @?= trueT 1399 1400 , testCase "sub x 0 = x" $ do 1401 let input = "sub 7 0" 1402 env = evalTricu allTestLibsEnv (parseTricu input) 1403 result env @?= ofNumber 7 1404 1405 , testCase "sub chained" $ do 1406 let input = "sub (sub 10 3) 2" 1407 env = evalTricu allTestLibsEnv (parseTricu input) 1408 result env @?= ofNumber 5 1409 1410 , testCase "mul identity 1" $ do 1411 let input = "mul 1 5" 1412 env = evalTricu allTestLibsEnv (parseTricu input) 1413 result env @?= ofNumber 5 1414 1415 , testCase "mul identity 2" $ do 1416 let input = "mul 5 1" 1417 env = evalTricu allTestLibsEnv (parseTricu input) 1418 result env @?= ofNumber 5 1419 1420 , testCase "mul is commutative" $ do 1421 let input = "equal? (mul 3 4) (mul 4 3)" 1422 env = evalTricu allTestLibsEnv (parseTricu input) 1423 result env @?= trueT 1424 1425 , testCase "mul is associative" $ do 1426 let input = "equal? (mul (mul 2 3) 4) (mul 2 (mul 3 4))" 1427 env = evalTricu allTestLibsEnv (parseTricu input) 1428 result env @?= trueT 1429 1430 , testCase "mul distributes over add" $ do 1431 let input = "equal? (mul 2 (add 3 4)) (add (mul 2 3) (mul 2 4))" 1432 env = evalTricu allTestLibsEnv (parseTricu input) 1433 result env @?= trueT 1434 1435 , testCase "lt? reflexive is false" $ do 1436 let input = "lt? 5 5" 1437 env = evalTricu allTestLibsEnv (parseTricu input) 1438 result env @?= falseT 1439 1440 , testCase "lte? reflexive is true" $ do 1441 let input = "lte? 5 5" 1442 env = evalTricu allTestLibsEnv (parseTricu input) 1443 result env @?= trueT 1444 1445 , testCase "lt? transitivity" $ do 1446 let input = "and? (lt? 2 5) (lt? 5 7)" 1447 env = evalTricu allTestLibsEnv (parseTricu input) 1448 result env @?= trueT 1449 1450 , testCase "add larger numbers" $ do 1451 let input = "add 12 15" 1452 env = evalTricu allTestLibsEnv (parseTricu input) 1453 result env @?= ofNumber 27 1454 1455 , testCase "mul larger numbers" $ do 1456 let input = "mul 5 6" 1457 env = evalTricu allTestLibsEnv (parseTricu input) 1458 result env @?= ofNumber 30 1459 1460 , testCase "isZero? on add 0 0" $ do 1461 let input = "isZero? (add 0 0)" 1462 env = evalTricu allTestLibsEnv (parseTricu input) 1463 result env @?= trueT 1464 1465 , testCase "div 10 3 = 3" $ do 1466 let input = "div 10 3" 1467 env = evalTricu allTestLibsEnv (parseTricu input) 1468 result env @?= ofNumber 3 1469 1470 , testCase "div 12 4 = 3 (exact)" $ do 1471 let input = "div 12 4" 1472 env = evalTricu allTestLibsEnv (parseTricu input) 1473 result env @?= ofNumber 3 1474 1475 , testCase "div 3 5 = 0 (divisor larger)" $ do 1476 let input = "div 3 5" 1477 env = evalTricu allTestLibsEnv (parseTricu input) 1478 result env @?= ofNumber 0 1479 1480 , testCase "div 7 1 = 7 (identity)" $ do 1481 let input = "div 7 1" 1482 env = evalTricu allTestLibsEnv (parseTricu input) 1483 result env @?= ofNumber 7 1484 1485 , testCase "div 0 5 = 0" $ do 1486 let input = "div 0 5" 1487 env = evalTricu allTestLibsEnv (parseTricu input) 1488 result env @?= ofNumber 0 1489 1490 , testCase "div 5 0 = 0 (div by zero)" $ do 1491 let input = "div 5 0" 1492 env = evalTricu allTestLibsEnv (parseTricu input) 1493 result env @?= ofNumber 0 1494 1495 , testCase "mod 10 3 = 1" $ do 1496 let input = "mod 10 3" 1497 env = evalTricu allTestLibsEnv (parseTricu input) 1498 result env @?= ofNumber 1 1499 1500 , testCase "mod 12 4 = 0 (exact)" $ do 1501 let input = "mod 12 4" 1502 env = evalTricu allTestLibsEnv (parseTricu input) 1503 result env @?= ofNumber 0 1504 1505 , testCase "mod 3 5 = 3 (divisor larger)" $ do 1506 let input = "mod 3 5" 1507 env = evalTricu allTestLibsEnv (parseTricu input) 1508 result env @?= ofNumber 3 1509 1510 , testCase "mod 7 1 = 0" $ do 1511 let input = "mod 7 1" 1512 env = evalTricu allTestLibsEnv (parseTricu input) 1513 result env @?= ofNumber 0 1514 1515 , testCase "mod 5 0 = 0 (mod by zero)" $ do 1516 let input = "mod 5 0" 1517 env = evalTricu allTestLibsEnv (parseTricu input) 1518 result env @?= ofNumber 0 1519 1520 , testCase "div mod consistency" $ do 1521 let input = "equal? (add (mul 3 7) 4) 25" 1522 env = evalTricu allTestLibsEnv (parseTricu input) 1523 result env @?= trueT 1524 1525 , testCase "pow 2 0 = 1" $ do 1526 let input = "pow 2 0" 1527 env = evalTricu allTestLibsEnv (parseTricu input) 1528 result env @?= ofNumber 1 1529 1530 , testCase "pow 2 3 = 8" $ do 1531 let input = "pow 2 3" 1532 env = evalTricu allTestLibsEnv (parseTricu input) 1533 result env @?= ofNumber 8 1534 1535 , testCase "pow 3 2 = 9" $ do 1536 let input = "pow 3 2" 1537 env = evalTricu allTestLibsEnv (parseTricu input) 1538 result env @?= ofNumber 9 1539 1540 , testCase "pow 0 0 = 1" $ do 1541 let input = "pow 0 0" 1542 env = evalTricu allTestLibsEnv (parseTricu input) 1543 result env @?= ofNumber 1 1544 1545 , testCase "pow 0 5 = 0" $ do 1546 let input = "pow 0 5" 1547 env = evalTricu allTestLibsEnv (parseTricu input) 1548 result env @?= ofNumber 0 1549 1550 , testCase "pow 1 10 = 1" $ do 1551 let input = "pow 1 10" 1552 env = evalTricu allTestLibsEnv (parseTricu input) 1553 result env @?= ofNumber 1 1554 1555 , testCase "pow 5 1 = 5" $ do 1556 let input = "pow 5 1" 1557 env = evalTricu allTestLibsEnv (parseTricu input) 1558 result env @?= ofNumber 5 1559 1560 , testCase "min 3 7 = 3" $ do 1561 let input = "min 3 7" 1562 env = evalTricu allTestLibsEnv (parseTricu input) 1563 result env @?= ofNumber 3 1564 1565 , testCase "min 7 3 = 3" $ do 1566 let input = "min 7 3" 1567 env = evalTricu allTestLibsEnv (parseTricu input) 1568 result env @?= ofNumber 3 1569 1570 , testCase "min 5 5 = 5" $ do 1571 let input = "min 5 5" 1572 env = evalTricu allTestLibsEnv (parseTricu input) 1573 result env @?= ofNumber 5 1574 1575 , testCase "min 0 5 = 0" $ do 1576 let input = "min 0 5" 1577 env = evalTricu allTestLibsEnv (parseTricu input) 1578 result env @?= ofNumber 0 1579 1580 , testCase "max 3 7 = 7" $ do 1581 let input = "max 3 7" 1582 env = evalTricu allTestLibsEnv (parseTricu input) 1583 result env @?= ofNumber 7 1584 1585 , testCase "max 7 3 = 7" $ do 1586 let input = "max 7 3" 1587 env = evalTricu allTestLibsEnv (parseTricu input) 1588 result env @?= ofNumber 7 1589 1590 , testCase "max 5 5 = 5" $ do 1591 let input = "max 5 5" 1592 env = evalTricu allTestLibsEnv (parseTricu input) 1593 result env @?= ofNumber 5 1594 1595 , testCase "max 0 5 = 5" $ do 1596 let input = "max 0 5" 1597 env = evalTricu allTestLibsEnv (parseTricu input) 1598 result env @?= ofNumber 5 1599 1600 , testCase "even? 0 = true" $ do 1601 let input = "even? 0" 1602 env = evalTricu allTestLibsEnv (parseTricu input) 1603 result env @?= trueT 1604 1605 , testCase "even? 1 = false" $ do 1606 let input = "even? 1" 1607 env = evalTricu allTestLibsEnv (parseTricu input) 1608 result env @?= falseT 1609 1610 , testCase "even? 2 = true" $ do 1611 let input = "even? 2" 1612 env = evalTricu allTestLibsEnv (parseTricu input) 1613 result env @?= trueT 1614 1615 , testCase "even? 7 = false" $ do 1616 let input = "even? 7" 1617 env = evalTricu allTestLibsEnv (parseTricu input) 1618 result env @?= falseT 1619 1620 , testCase "odd? 0 = false" $ do 1621 let input = "odd? 0" 1622 env = evalTricu allTestLibsEnv (parseTricu input) 1623 result env @?= falseT 1624 1625 , testCase "odd? 1 = true" $ do 1626 let input = "odd? 1" 1627 env = evalTricu allTestLibsEnv (parseTricu input) 1628 result env @?= trueT 1629 1630 , testCase "odd? 2 = false" $ do 1631 let input = "odd? 2" 1632 env = evalTricu allTestLibsEnv (parseTricu input) 1633 result env @?= falseT 1634 1635 , testCase "odd? 7 = true" $ do 1636 let input = "odd? 7" 1637 env = evalTricu allTestLibsEnv (parseTricu input) 1638 result env @?= trueT 1639 ] 1640 1641 fileEval :: TestTree 1642 fileEval = testGroup "File evaluation tests" 1643 [ testCase "Forks" $ do 1644 res <- liftIO $ evaluateFileResult "./test/fork.tri" 1645 res @?= Fork Leaf Leaf 1646 1647 , testCase "File ends with comment" $ do 1648 res <- liftIO $ evaluateFileResult "./test/comments-1.tri" 1649 res @?= Fork (Stem Leaf) Leaf 1650 1651 , testCase "Mapping and Equality" $ do 1652 fEnv <- liftIO $ evaluateFileWithContext allTestLibsEnv "./test/map.tri" 1653 (mainResult fEnv) @?= Stem Leaf 1654 1655 , testCase "Eval and decoding string" $ do 1656 res <- liftIO $ evaluateFileWithContext allTestLibsEnv "./test/string.tri" 1657 decodeResult (result res) @?= "\"String test!\"" 1658 ] 1659 1660 -- All of our demo tests are also module tests 1661 demos :: TestTree 1662 demos = testGroup "Test provided demo functionality" 1663 [ testCase "Structural equality demo" $ do 1664 res <- liftIO $ evaluateFileResult "./demos/equality.tri" 1665 decodeResult res @?= "t t" 1666 , testCase "Convert values back to source code demo" $ do 1667 res <- liftIO $ evaluateFileResult "./demos/toSource.tri" 1668 decodeResult res @?= "\"(t (t (t t) (t t t)) (t t (t t t)))\"" 1669 , testCase "Determining the size of functions" $ do 1670 res <- liftIO $ evaluateFileResult "./demos/size.tri" 1671 decodeResult res @?= "321" 1672 , testCase "Level Order Traversal demo" $ do 1673 res <- liftIO $ evaluateFileResult "./demos/levelOrderTraversal.tri" 1674 decodeResult res @?= "\"\n1 \n2 3 \n4 5 6 7 \n8 11 10 9 12 \"" 1675 , testCase "Contract effect demo with do notation" $ do 1676 res <- liftIO $ evaluateFileResult "./demos/contractEffects.tri" 1677 decodeResult res @?= "[t t, 10]" 1678 , testCase "Safe base wrappers demo" $ do 1679 res <- liftIO $ evaluateFileResult "./demos/contractBasics.tri" 1680 decodeResult res @?= "[3, t t, t, t t]" 1681 ] 1682 1683 decoding :: TestTree 1684 decoding = testGroup "Decoding Tests" 1685 [ testCase "Decode Leaf" $ do 1686 decodeResult Leaf @?= "t" 1687 1688 , testCase "Decode list of non-ASCII numbers" $ do 1689 let input = ofList [ofNumber 1, ofNumber 14, ofNumber 6] 1690 decodeResult input @?= "[1, 14, 6]" 1691 1692 , testCase "Decode list of ASCII numbers as a string" $ do 1693 let input = ofList [ofNumber 97, ofNumber 98, ofNumber 99] 1694 decodeResult input @?= "\"abc\"" 1695 1696 , testCase "Decode small number" $ do 1697 decodeResult (ofNumber 42) @?= "42" 1698 1699 , testCase "Decode large number" $ do 1700 decodeResult (ofNumber 9999) @?= "9999" 1701 1702 , testCase "Decode string in list" $ do 1703 let input = ofList [ofString "hello", ofString "world"] 1704 decodeResult input @?= "[\"hello\", \"world\"]" 1705 1706 , testCase "Decode mixed list with strings" $ do 1707 let input = ofList [ofString "hello", ofNumber 42, ofString "world"] 1708 decodeResult input @?= "[\"hello\", 42, \"world\"]" 1709 1710 , testCase "Decode nested lists with strings" $ do 1711 let input = ofList [ofList [ofString "nested"], ofString "string"] 1712 decodeResult input @?= "[[\"nested\"], \"string\"]" 1713 ] 1714 1715 elimLambdaSingle :: TestTree 1716 elimLambdaSingle = testCase "elimLambda preserves eval, fires eta, and SDef binds" $ do 1717 -- 1) eta reduction, purely structural and parsed from source 1718 let [etaIn] = parseTricu "x : f x" 1719 [fRef ] = parseTricu "f" 1720 elimLambda etaIn @?= fRef 1721 1722 -- 2) SDef binds its own name and parameters 1723 let [defFXY] = parseTricu "f x y : f x" 1724 fv = freeVars defFXY 1725 assertBool "f should be bound in SDef" ("f" `Set.notMember` fv) 1726 assertBool "x should be bound in SDef" ("x" `Set.notMember` fv) 1727 assertBool "y should be bound in SDef" ("y" `Set.notMember` fv) 1728 1729 -- 3) semantics preserved on a small program that exercises compose and triage 1730 let src = 1731 unlines 1732 [ "false = t" 1733 , "_ = t" 1734 , "true = t t" 1735 , "id = a : a" 1736 , "const = a b : a" 1737 , "compose = f g x : f (g x)" 1738 , "triage = leaf stem fork : t (t leaf stem) fork" 1739 , "test = triage \"Leaf\" (_ : \"Stem\") (_ _ : \"Fork\")" 1740 , "main = compose id id test" 1741 ] 1742 prog = parseTricu src 1743 progElim = map elimLambda prog 1744 evalBefore = result (evalTricu Map.empty prog) 1745 evalAfter = result (evalTricu Map.empty progElim) 1746 evalAfter @?= evalBefore 1747 1748 stressElimLambda :: TestTree 1749 stressElimLambda = testCase "stress elimLambda on wide list under deep curried lambda" $ do 1750 let numVars = 200 1751 numBody = 800 1752 vars = [ "x" ++ show i | i <- [1..numVars] ] 1753 body = "(" ++ unwords (replicate numBody "t") ++ ")" 1754 etaOne = "h : f h" 1755 etaTwo = "k : id k" 1756 defId = "id = a : a" 1757 lambda = unwords vars ++ " : " ++ body 1758 src = unlines 1759 [ defId 1760 , etaOne 1761 , "compose = f g x : f (g x)" 1762 , "f = t t" 1763 , etaTwo 1764 , lambda 1765 , "main = compose id id (" ++ head vars ++ " : f " ++ head vars ++ ")" 1766 ] 1767 prog = parseTricu src 1768 1769 let out = map elimLambda prog 1770 let noLambda term = case term of 1771 SLambda _ _ -> False 1772 SApp f g -> noLambda f && noLambda g 1773 SList xs -> all noLambda xs 1774 TFork l r -> noLambda l && noLambda r 1775 TStem u -> noLambda u 1776 _ -> True 1777 1778 assertBool "all lambdas eliminated" (all noLambda out) 1779 1780 let before = result (evalTricu Map.empty prog) 1781 after = result (evalTricu Map.empty out) 1782 after @?= before 1783 1784 -- -------------------------------------------------------------------------- 1785 -- Byte marshalling tests 1786 -- -------------------------------------------------------------------------- 1787 1788 byteMarshallingTests :: TestTree 1789 byteMarshallingTests = testGroup "Byte Marshalling Tests" 1790 [ testCase "ofByte / toByte round-trip: 0" $ do 1791 let w8 = (0 :: Word8) 1792 toByte (ofByte w8) @?= Right w8 1793 1794 , testCase "ofByte / toByte round-trip: 1" $ do 1795 let w8 = (1 :: Word8) 1796 toByte (ofByte w8) @?= Right w8 1797 1798 , testCase "ofByte / toByte round-trip: 127" $ do 1799 let w8 = (127 :: Word8) 1800 toByte (ofByte w8) @?= Right w8 1801 1802 , testCase "ofByte / toByte round-trip: 128" $ do 1803 let w8 = (128 :: Word8) 1804 toByte (ofByte w8) @?= Right w8 1805 1806 , testCase "ofByte / toByte round-trip: 255" $ do 1807 let w8 = (255 :: Word8) 1808 toByte (ofByte w8) @?= Right w8 1809 1810 , testCase "toByte rejects value > 255" $ do 1811 -- ofNumber 256 = Fork Leaf (Fork Leaf Leaf) — value 256 1812 toByte (ofNumber 256) @?= Left "Byte value out of range: 256" 1813 1814 , testCase "toByte accepts Leaf" $ do 1815 toByte (Leaf) @?= Right 0 1816 1817 , testCase "toByte rejects non-number tree" $ do 1818 toByte (Stem Leaf) @?= Left "Invalid Tree Calculus number" 1819 toByte (Stem (Stem Leaf)) @?= Left "Invalid Tree Calculus number" 1820 1821 , testCase "ofBytes / toBytes round-trip: empty ByteString" $ do 1822 toBytes (ofBytes BS.empty) @?= Right BS.empty 1823 1824 , testCase "ofBytes / toBytes round-trip: [0x00]" $ do 1825 toBytes (ofBytes (BS.pack [0x00])) @?= Right (BS.pack [0x00]) 1826 1827 , testCase "ofBytes / toBytes round-trip: [0xff]" $ do 1828 toBytes (ofBytes (BS.pack [0xff])) @?= Right (BS.pack [0xff]) 1829 1830 , testCase "ofBytes / toBytes round-trip: mixed bytes" $ do 1831 let bytes = BS.pack [0x00, 0x01, 0x7f, 0x80, 0xff, 0x41, 0x42, 0x43] 1832 toBytes (ofBytes bytes) @?= Right bytes 1833 1834 , testCase "toBytes rejects non-list tree" $ do 1835 -- Leaf is a valid list (empty), so this won't work. 1836 -- Stem Leaf is not a list. 1837 toBytes (Stem Leaf) @?= Left "Invalid Tree Calculus list" 1838 1839 , testCase "toBytes rejects list containing invalid byte (>255)" $ do 1840 -- [ofNumber 256, ofNumber 1] — first element is > 255 1841 let badList = ofList [ofNumber 256, ofNumber 1] 1842 toBytes badList @?= Left "Byte value out of range: 256" 1843 1844 , testCase "nodePayloadToTreeBytes / treeBytesToNodePayload: Leaf payload" $ do 1845 -- Leaf payload is 0x00 (1 byte) 1846 let payload = BS.pack [0x00] 1847 treeBytesToNodePayload (nodePayloadToTreeBytes payload) @?= Right payload 1848 1849 , testCase "nodePayloadToTreeBytes / treeBytesToNodePayload: Stem payload" $ do 1850 -- Stem payload: 0x01 || 32-byte hash = 33 bytes 1851 let payload = BS.pack (0x01 : replicate 32 0x42) 1852 treeBytesToNodePayload (nodePayloadToTreeBytes payload) @?= Right payload 1853 1854 , testCase "nodePayloadToTreeBytes / treeBytesToNodePayload: Fork payload" $ do 1855 -- Fork payload: 0x02 || 32-byte hash || 32-byte hash = 65 bytes 1856 let payload = BS.pack (0x02 : replicate 64 0x42) 1857 treeBytesToNodePayload (nodePayloadToTreeBytes payload) @?= Right payload 1858 1859 , testCase "hashToTreeBytes / treeBytesToHash round-trip" $ do 1860 -- Use a known 32-byte hash (SHA256 of "") 1861 let hashStr :: MerkleHash 1862 hashStr = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" 1863 case hashToTreeBytes hashStr of 1864 Left err -> assertFailure $ "hashToTreeBytes failed: " ++ err 1865 Right tree -> treeBytesToHash tree @?= Right hashStr 1866 1867 , testCase "hashToTreeBytes rejects invalid hex hash" $ do 1868 hashToTreeBytes "not-a-hash" @?= Left "Invalid hex MerkleHash" 1869 1870 , testCase "hashToTreeBytes rejects non-32-byte hash" $ do 1871 -- "00" decodes to 1 byte, not 32 1872 hashToTreeBytes "00" @?= Left "Hash raw bytes must be 32 bytes" 1873 1874 , testCase "treeBytesToHash rejects wrong byte count" $ do 1875 -- Only 16 bytes, not 32 1876 let t16 = ofBytes (BS.pack [0x41 | _ <- [1..16]]) 1877 treeBytesToHash t16 @?= Left "Expected exactly 32 byte elements for hash" 1878 ] 1879 1880 -- -------------------------------------------------------------------------- 1881 -- Content store tests 1882 -- -------------------------------------------------------------------------- 1883 1884 contentStoreTests :: TestTree 1885 contentStoreTests = testGroup "Content Store Tests" 1886 [ testCase "Filesystem CAS: put/get object and sharded path" $ 1887 withSystemTempDirectory "tricu-store" $ \dir -> do 1888 let store = StorePath dir 1889 domain = Domain "test.object.v1" 1890 payload = BS.pack [1, 2, 3, 4] 1891 h <- putObject store domain payload 1892 shardForHash h @?= take 3 (unpack h) 1893 objectPath store h @?= dir </> "objects" </> take 3 (unpack h) </> unpack h 1894 doesFileExist (objectPath store h) >>= (@?= True) 1895 getObject store h >>= (@?= Just payload) 1896 1897 , testCase "Filesystem CAS: idempotent object writes" $ 1898 withSystemTempDirectory "tricu-store" $ \dir -> do 1899 let store = StorePath dir 1900 domain = Domain "test.object.v1" 1901 payload = BS.pack [9, 8, 7] 1902 h1 <- putObject store domain payload 1903 h2 <- putObject store domain payload 1904 h1 @?= h2 1905 countStoredObjects store >>= (@?= 1) 1906 1907 , testCase "Filesystem CAS: putTree/getTree round trip" $ 1908 withSystemTempDirectory "tricu-store" $ \dir -> do 1909 let store = StorePath dir 1910 term = Fork (Stem Leaf) (Fork Leaf (Stem Leaf)) 1911 leafHash = nodeHash NLeaf 1912 stemHash = nodeHash (NStem leafHash) 1913 rightHash = nodeHash (NFork leafHash stemHash) 1914 expectedRoot = nodeHash (NFork stemHash rightHash) 1915 root <- putTree store term 1916 root @?= expectedRoot 1917 getTree store root >>= (@?= Just term) 1918 1919 , testCase "Filesystem CAS: shared subtrees are deduplicated" $ 1920 withSystemTempDirectory "tricu-store" $ \dir -> do 1921 let store = StorePath dir 1922 shared = Stem Leaf 1923 term = Fork shared shared 1924 _ <- putTree store term 1925 countStoredObjects store >>= (@?= 3) 1926 1927 , testCase "Workspace aliases: write/read/list object refs" $ 1928 withSystemTempDirectory "tricu-store" $ \dir -> do 1929 let store = StorePath dir 1930 ref = ObjectRef "arboricx.tree-root.v1" "abc123" 1931 writeAlias store NameAlias "main" ref 1932 readAlias store NameAlias "main" >>= (@?= Just ref) 1933 listAliases store NameAlias >>= (@?= [("main", ref)]) 1934 1935 , testCase "Module manifests: put/get round trip through CAS" $ 1936 withSystemTempDirectory "tricu-store" $ \dir -> do 1937 let store = StorePath dir 1938 term = Fork Leaf (Stem Leaf) 1939 manifestFor root = ModuleManifest [] 1940 [ ModuleExport 1941 "main" 1942 (ObjectRef (unDomain treeTermDomain) root) 1943 "arboricx.abi.tree.v1" 1944 ] 1945 root <- putTreeTerm store term 1946 h <- putManifest store (manifestFor root) 1947 getManifest store h >>= (@?= Just (manifestFor root)) 1948 1949 , testCase "ObjectResolver: resolves manifests and trees without filesystem coupling" $ do 1950 let term = Fork Leaf (Stem Leaf) 1951 leafH = nodeHash NLeaf 1952 stemH = nodeHash (NStem leafH) 1953 rootH = nodeHash (NFork leafH stemH) 1954 termH = hashObject treeTermDomain (encodeTreeTerm term) 1955 manifest = ModuleManifest [] 1956 [ ModuleExport 1957 "value" 1958 (ObjectRef (unDomain treeTermDomain) termH) 1959 "arboricx.abi.tree.v1" 1960 ] 1961 manifestBytes = encodeManifest manifest 1962 manifestH = hashObject manifestDomain manifestBytes 1963 objects = Map.fromList 1964 [ (("arboricx.merkle.node.v1", leafH), serializeNode NLeaf) 1965 , (("arboricx.merkle.node.v1", stemH), serializeNode (NStem leafH)) 1966 , (("arboricx.merkle.node.v1", rootH), serializeNode (NFork leafH stemH)) 1967 , ((unDomain treeTermDomain, termH), encodeTreeTerm term) 1968 , ((unDomain manifestDomain, manifestH), manifestBytes) 1969 ] 1970 hydrate objs h = case deserializeNode <$> Map.lookup ("arboricx.merkle.node.v1", h) objs of 1971 Nothing -> return Nothing 1972 Just NLeaf -> return (Just Leaf) 1973 Just (NStem child) -> fmap Stem <$> hydrate objs child 1974 Just (NFork left right) -> do 1975 l <- hydrate objs left 1976 r <- hydrate objs right 1977 return $ Fork <$> l <*> r 1978 resolver = ObjectResolver 1979 { resolverAlias = \kind name -> return $ if kind == ModuleAlias && name == "demo" 1980 then Just (ObjectRef (unDomain manifestDomain) manifestH) 1981 else Nothing 1982 , resolverObject = \ref -> return $ Map.lookup (objectRefKind ref, objectRefHash ref) objects 1983 , resolverManifest = \h -> return $ do 1984 bytes <- Map.lookup (unDomain manifestDomain, h) objects 1985 either (const Nothing) Just (decodeManifest bytes) 1986 , resolverTree = hydrate objects 1987 } 1988 resolverAlias resolver ModuleAlias "demo" >>= (@?= Just (ObjectRef (unDomain manifestDomain) manifestH)) 1989 resolveManifest resolver manifestH >>= (@?= Just manifest) 1990 resolveTree resolver rootH >>= (@?= Just term) 1991 1992 , testCase "Workspace modules: exported names are local top-level definitions only" $ 1993 withSystemTempDirectory "tricu-workspace-local-exports" $ \dir -> do 1994 let store = StorePath (dir </> "store") 1995 depPath = dir </> "dep.tri" 1996 libPath = dir </> "util.tri" 1997 mainPath = dir </> "main.tri" 1998 writeFile (dir </> "tricu.workspace") "module dep = dep.tri\nmodule util = util.tri\n" 1999 writeFile depPath "helper = t t\n" 2000 writeFile libPath "!import \"dep\" !Local\n\nvalue = helper\n" 2001 writeFile mainPath "!import \"util\" Util\n\nmain = Util.value\n" 2002 env <- evaluateFileWithStore (Just store) mainPath 2003 result env @?= Stem Leaf 2004 mAlias <- readAlias store ModuleAlias "util" 2005 case mAlias of 2006 Nothing -> assertFailure "expected workspace build to write util module alias" 2007 Just ref -> do 2008 mManifest <- getManifest store (objectRefHash ref) 2009 case mManifest of 2010 Nothing -> assertFailure "expected workspace module manifest" 2011 Just manifest -> map moduleExportName (moduleManifestExports manifest) @?= ["value"] 2012 2013 , testCase "Workspace modules: contract annotations travel with exported definitions" $ 2014 withSystemTempDirectory "tricu-workspace-contract-export" $ \dir -> do 2015 let store = StorePath (dir </> "store") 2016 libPath = dir </> "util.tri" 2017 mainPath = dir </> "main.tri" 2018 cwd <- getCurrentDirectory 2019 writeFile (dir </> "tricu.workspace") ("module base = \"" ++ cwd </> "lib/base.tri\"\nmodule util = \"" ++ dir </> "util.tri\"\n") 2020 writeFile libPath "!import \"base\" !Local\n\nalwaysOk = (value rest : ok value rest)\n\nneverOk = (value rest : err \"nope\" rest)\n\nsafeId n@alwaysOk =@alwaysOk n\n\nbadId n@neverOk =@neverOk n\n" 2021 writeFile mainPath "!import \"util\" Util\n\nmain = Util.safeId 5\n" 2022 env <- evaluateFileWithStore (Just store) mainPath 2023 result env @?= ofNumber 5 2024 writeFile mainPath "!import \"util\" Util\n\nmain = Util.badId 5\n" 2025 envFail <- evaluateFileWithStore (Just store) mainPath 2026 decodeResult (result envFail) @?= "[t, \"nope\"]" 2027 2028 , testCase "Module imports: resolve manifest exports from store" $ 2029 withSystemTempDirectory "tricu-module-import" $ \dir -> do 2030 let store = StorePath (dir </> "store") 2031 sourcePath = dir </> "consumer.tri" 2032 term = Fork Leaf (Stem Leaf) 2033 manifestFor root = ModuleManifest [] 2034 [ ModuleExport 2035 "value" 2036 (ObjectRef (unDomain treeTermDomain) root) 2037 "arboricx.abi.tree.v1" 2038 ] 2039 root <- putTreeTerm store term 2040 manifestHash <- putManifest store (manifestFor root) 2041 writeAlias store ModuleAlias "demo" (ObjectRef (unDomain manifestDomain) manifestHash) 2042 writeFile sourcePath "!import \"demo\" Demo\n\nmain = Demo.value\n" 2043 env <- evaluateFileWithStore (Just store) sourcePath 2044 result env @?= term 2045 2046 , testCase "Module resolver diagnostics: missing alias names workspace/module guidance" $ do 2047 let resolver = filesystemResolver (StorePath "/tmp/tricu-test-missing-module-store") 2048 outcome <- try (resolveModuleImport resolver "definitely-not-a-module" "Demo") :: IO (Either SomeException ResolvedModule) 2049 case outcome of 2050 Right _ -> assertFailure "expected missing module alias failure" 2051 Left err -> show err `containsAll` ["Module alias not found", "definitely-not-a-module", "tricu.workspace", "ModuleAlias"] 2052 2053 , testCase "Module resolver diagnostics: alias kind mismatch names expected kind" $ do 2054 let resolver = ObjectResolver 2055 { resolverAlias = \kind name -> return $ if kind == ModuleAlias && name == "demo" 2056 then Just (ObjectRef "arboricx.tree-root.v1" "abc123") 2057 else Nothing 2058 , resolverObject = \_ -> return Nothing 2059 , resolverManifest = \_ -> return Nothing 2060 , resolverTree = \_ -> return Nothing 2061 } 2062 outcome <- try (resolveModuleImport resolver "demo" "Demo") :: IO (Either SomeException ResolvedModule) 2063 case outcome of 2064 Right _ -> assertFailure "expected alias kind mismatch failure" 2065 Left err -> show err `containsAll` ["Module alias", "demo", "unsupported object kind", "arboricx.tree-root.v1", "arboricx.module-manifest.v1"] 2066 2067 , testCase "Module resolver diagnostics: missing tree term names export and hash" $ do 2068 let root = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 2069 manifest = ModuleManifest [] 2070 [ ModuleExport "value" (ObjectRef (unDomain treeTermDomain) root) "arboricx.abi.tree.v1" ] 2071 resolver = ObjectResolver 2072 { resolverAlias = \kind name -> return $ if kind == ModuleAlias && name == "demo" 2073 then Just (ObjectRef (unDomain manifestDomain) "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") 2074 else Nothing 2075 , resolverObject = \_ -> return Nothing 2076 , resolverManifest = \_ -> return (Just manifest) 2077 , resolverTree = \_ -> return Nothing 2078 } 2079 outcome <- try (resolveModuleImport resolver "demo" "Demo") :: IO (Either SomeException ResolvedModule) 2080 case outcome of 2081 Right _ -> assertFailure "expected missing tree term failure" 2082 Left err -> show err `containsAll` ["Module export", "value", "missing tree term", unpack root] 2083 2084 , testCase "Arboricx bundle: unpack transport bundle into CAS tree terms" $ 2085 withSystemTempDirectory "tricu-store" $ \dir -> do 2086 let store = StorePath dir 2087 term = Fork (Stem Leaf) Leaf 2088 bundle = buildBundle [("main", term)] 2089 exports <- unpackBundleToStore store (encodeBundle bundle) 2090 case exports of 2091 [("main", root)] -> getTreeTerm store root >>= (@?= Just term) 2092 other -> assertFailure $ "unexpected exports: " ++ show other 2093 2094 , testCase "Arboricx bundle: pack CAS tree terms into transport bundle" $ 2095 withSystemTempDirectory "tricu-store" $ \dir -> do 2096 let store = StorePath dir 2097 term = Fork Leaf (Stem Leaf) 2098 root <- putTreeTerm store term 2099 bundle <- packBundleFromStore store [("main", root)] 2100 bundleRoots bundle @?= [2] 2101 let terms = reconstructBundleTermsForTest (bundleNodes bundle) 2102 case manifestExports (bundleManifest bundle) of 2103 [exported] -> do 2104 exportName exported @?= "main" 2105 terms V.! fromIntegral (exportRoot exported) @?= term 2106 other -> assertFailure $ "unexpected exports: " ++ show other 2107 ] 2108 2109 reconstructBundleTermsForTest :: Seq.Seq BundleNode -> V.Vector T 2110 reconstructBundleTermsForTest nodes = V.fromList (go <$> Foldable.toList nodes) 2111 where 2112 built = V.fromList (go <$> Foldable.toList nodes) 2113 go BNLeaf = Leaf 2114 go (BNStem child) = Stem (built V.! fromIntegral child) 2115 go (BNFork left right) = Fork (built V.! fromIntegral left) (built V.! fromIntegral right) 2116 2117 countStoredObjects :: StorePath -> IO Int 2118 countStoredObjects store@(StorePath root) = do 2119 ensureStore store 2120 shards <- listDirectory (root </> "objects") 2121 fmap sum $ forM shards $ \shard -> do 2122 files <- listDirectory (root </> "objects" </> shard) 2123 return (length files) 2124 2125 -- -------------------------------------------------------------------------- 2126 -- Wire module tests 2127 -- -------------------------------------------------------------------------- 2128 2129 -- | Helper: create a temporary file-backed DB, store a term, return the 2130 2131 wireTests :: TestTree 2132 wireTests = testGroup "Wire Tests" 2133 [ testCase "Indexed bundle: header and manifest declare indexed format" $ do 2134 let term = result $ evalTricu Map.empty $ parseTricu "id = a : a\nmain = id t" 2135 bundle = buildBundle [("main", term)] 2136 wireData = encodeBundle bundle 2137 BS.take 8 wireData @?= BS.pack [0x41, 0x52, 0x42, 0x4f, 0x52, 0x49, 0x43, 0x58] 2138 case decodeBundle wireData of 2139 Left err -> assertFailure $ "decodeBundle failed: " ++ err 2140 Right decoded -> do 2141 let manifest = bundleManifest decoded 2142 tree = manifestTree manifest 2143 hashSpec = treeNodeHash tree 2144 manifestSchema manifest @?= "arboricx.bundle.manifest.v1" 2145 manifestBundleType manifest @?= "tree-calculus-executable-object" 2146 manifestClosure manifest @?= ClosureComplete 2147 treeCalculus tree @?= "tree-calculus.v1" 2148 treeNodePayload tree @?= "arboricx.indexed.payload.v1" 2149 nodeHashAlgorithm hashSpec @?= "indexed" 2150 nodeHashDomain hashSpec @?= "arboricx.indexed.node.v1" 2151 bundleRoots decoded @?= bundleRoots bundle 2152 case manifestExports manifest of 2153 [exported] -> do 2154 exportName exported @?= "main" 2155 exportRoot exported @?= head (bundleRoots bundle) 2156 exportKind exported @?= "term" 2157 exportAbi exported @?= "arboricx.abi.tree.v1" 2158 exports -> assertFailure $ "Expected one export, got: " ++ show exports 2159 2160 , testCase "Indexed bundle: deterministic encoding" $ do 2161 let term = result $ evalTricu Map.empty $ parseTricu "x = t t\nmain = t x" 2162 bundle1 = buildBundle [("main", term)] 2163 bundle2 = buildBundle [("main", term)] 2164 encodeBundle bundle1 @?= encodeBundle bundle2 2165 2166 , testCase "Indexed bundle: renaming export changes bytes" $ do 2167 let term = result $ evalTricu Map.empty $ parseTricu "f = a : a\nmain = f t" 2168 mainBundle = buildBundle [("main", term)] 2169 renamedBundle = buildBundle [("validate", term)] 2170 encodeBundle mainBundle /= encodeBundle renamedBundle @? "different export names should produce different bytes" 2171 -- But nodes are identical 2172 bundleNodes mainBundle @?= bundleNodes renamedBundle 2173 2174 , testCase "Indexed bundle: verify rejects out-of-bounds root" $ do 2175 let term = Leaf 2176 bundle = buildBundle [("main", term)] 2177 badBundle = bundle { bundleRoots = [99] } 2178 case verifyBundle badBundle of 2179 Left err -> assertBool ("Expected bounds error, got: " ++ err) ("out of bounds" `isInfixOf` err) 2180 Right () -> assertFailure "Expected out-of-bounds root to be rejected" 2181 2182 , testCase "Indexed bundle: verify rejects out-of-bounds child index" $ do 2183 let bundle = Bundle 2184 { bundleVersion = 1000 2185 , bundleRoots = [1] 2186 , bundleNodes = Seq.fromList [BNLeaf, BNStem 99] 2187 , bundleManifest = (bundleManifest $ buildBundle [("main", Leaf)]) 2188 { manifestRoots = [BundleRoot 1 "default"] 2189 , manifestExports = [BundleExport "main" 1 "term" "arboricx.abi.tree.v1"] 2190 } 2191 , bundleManifestBytes = BS.empty 2192 } 2193 case verifyBundle bundle of 2194 Left err -> assertBool ("Expected bounds error, got: " ++ err) ("references child 99" `isInfixOf` err) 2195 Right () -> assertFailure "Expected out-of-bounds child to be rejected" 2196 2197 , testCase "Indexed bundle: verify rejects acyclic (forward reference)" $ do 2198 let bundle = Bundle 2199 { bundleVersion = 1000 2200 , bundleRoots = [1] 2201 , bundleNodes = Seq.fromList [BNStem 1, BNLeaf] -- index 0 refers to 1 (forward) 2202 , bundleManifest = (bundleManifest $ buildBundle [("main", Leaf)]) 2203 { manifestRoots = [BundleRoot 1 "default"] 2204 , manifestExports = [BundleExport "main" 1 "term" "arboricx.abi.tree.v1"] 2205 } 2206 , bundleManifestBytes = BS.empty 2207 } 2208 case verifyBundle bundle of 2209 Left err -> assertBool ("Expected acyclicity error, got: " ++ err) ("references child 1" `isInfixOf` err) 2210 Right () -> assertFailure "Expected forward reference to be rejected" 2211 2212 , testCase "Indexed bundle: verify rejects duplicate nodes" $ do 2213 let bundle = Bundle 2214 { bundleVersion = 1000 2215 , bundleRoots = [0] 2216 , bundleNodes = Seq.fromList [BNLeaf, BNLeaf] 2217 , bundleManifest = (bundleManifest $ buildBundle [("main", Leaf)]) 2218 { manifestRoots = [BundleRoot 0 "default"] 2219 , manifestExports = [BundleExport "main" 0 "term" "arboricx.abi.tree.v1"] 2220 } 2221 , bundleManifestBytes = BS.empty 2222 } 2223 case verifyBundle bundle of 2224 Left err -> assertBool ("Expected duplicate error, got: " ++ err) ("duplicate" `isInfixOf` err) 2225 Right () -> assertFailure "Expected duplicate nodes to be rejected" 2226 2227 , testCase "Indexed bundle: unpack into filesystem CAS" $ 2228 withSystemTempDirectory "tricu-store" $ \dir -> do 2229 let term = result $ evalTricu Map.empty $ parseTricu "validateEmail = a : a\nmain = validateEmail t" 2230 bundle = buildBundle [("validateEmail", term)] 2231 wireData = encodeBundle bundle 2232 store = StorePath dir 2233 roots <- unpackBundleToStore store wireData 2234 case roots of 2235 [("validateEmail", root)] -> getTree store root >>= (@?= Just term) 2236 other -> assertFailure $ "unexpected roots: " ++ show other 2237 2238 , testCase "Indexed bundle: round-trip decode and verify" $ do 2239 let term = result $ evalTricu Map.empty $ parseTricu "x = t\ny = t x\nz = t y\nmain = z" 2240 bundle = buildBundle [("main", term)] 2241 wireData = encodeBundle bundle 2242 case decodeBundle wireData of 2243 Left err -> assertFailure $ "decodeBundle failed: " ++ err 2244 Right decoded -> case verifyBundle decoded of 2245 Left err -> assertFailure $ "verifyBundle failed: " ++ err 2246 Right () -> do 2247 bundleRoots decoded @?= bundleRoots bundle 2248 Seq.length (bundleNodes decoded) @?= Seq.length (bundleNodes bundle) 2249 2250 , testCase "Indexed bundle: unsupported manifest semantics rejected" $ do 2251 let term = Leaf 2252 bundle = buildBundle [("main", term)] 2253 manifest = bundleManifest bundle 2254 partialBundle = bundle 2255 { bundleManifest = manifest { manifestClosure = ClosurePartial } 2256 , bundleManifestBytes = BS.empty 2257 } 2258 capabilityBundle = bundle 2259 { bundleManifest = manifest 2260 { manifestRuntime = (manifestRuntime manifest) 2261 { runtimeCapabilities = ["host.io"] } 2262 } 2263 , bundleManifestBytes = BS.empty 2264 } 2265 wrongHashBundle = bundle 2266 { bundleManifest = manifest 2267 { manifestTree = (manifestTree manifest) 2268 { treeNodeHash = (treeNodeHash $ manifestTree manifest) 2269 { nodeHashAlgorithm = "blake3" } 2270 } 2271 } 2272 , bundleManifestBytes = BS.empty 2273 } 2274 case verifyBundle partialBundle of 2275 Left err -> assertBool ("Expected closure error, got: " ++ err) ("closure = complete" `isInfixOf` err) 2276 Right () -> assertFailure "Expected partial closure to be rejected" 2277 case verifyBundle capabilityBundle of 2278 Left err -> assertBool ("Expected capability error, got: " ++ err) ("capabilities" `isInfixOf` err) 2279 Right () -> assertFailure "Expected runtime capabilities to be rejected" 2280 case verifyBundle wrongHashBundle of 2281 Left err -> assertBool ("Expected hash algorithm error, got: " ++ err) ("node hash algorithm" `isInfixOf` err) 2282 Right () -> assertFailure "Expected unsupported node hash algorithm to be rejected" 2283 ] 2284 2285 -- -------------------------------------------------------------------------- 2286 -- Tricu reader tests 2287 -- Smoke-test the tricu-native Arboricx reader against indexed bundles. 2288 -- -------------------------------------------------------------------------- 2289 2290 tricuReaderTests :: TestTree 2291 tricuReaderTests = testGroup "Tricu Reader Tests" 2292 [ testCase "Tricu reader parses indexed bundle (id fixture)" $ do 2293 bundleBytes <- BS.readFile "./test/fixtures/id.arboricx" 2294 let bundleT = ofBytes bundleBytes 2295 let env = Map.insert "testBundle" bundleT allTestLibsEnv 2296 tagExpr = parseTricu "pairFirst (runArboricx testBundle t)" 2297 tag = result (evalTricu env tagExpr) 2298 codeExpr = parseTricu "pairFirst (pairSecond (runArboricx testBundle t))" 2299 code = result (evalTricu env codeExpr) 2300 tag @?= trueT 2301 2302 , testCase "Tricu reader parses indexed bundle (append fixture)" $ do 2303 bundleBytes <- BS.readFile "./test/fixtures/append.arboricx" 2304 let bundleT = ofBytes bundleBytes 2305 let env = Map.insert "testBundle" bundleT allTestLibsEnv 2306 tagExpr = parseTricu "pairFirst (runArboricx testBundle t)" 2307 tag = result (evalTricu env tagExpr) 2308 tag @?= trueT 2309 2310 , testCase "Tricu reader parses indexed bundle (bool fixtures)" $ do 2311 forM_ ["true", "false"] $ \name -> do 2312 bundleBytes <- BS.readFile ("./test/fixtures/" ++ name ++ ".arboricx") 2313 let bundleT = ofBytes bundleBytes 2314 let env = Map.insert "testBundle" bundleT allTestLibsEnv 2315 tagExpr = parseTricu "pairFirst (runArboricx testBundle t)" 2316 tag = result (evalTricu env tagExpr) 2317 tag @?= trueT 2318 ] 2319 2320 -- -------------------------------------------------------------------------- 2321 -- Byte-list utility tests 2322 -- Expected values built with canonical Haskell-side T constructors. 2323 -- -------------------------------------------------------------------------- 2324 2325 -- | Helpers for byte-list test expectations. 2326 2327 trueT :: T 2328 trueT = Stem Leaf 2329 2330 falseT :: T 2331 falseT = Leaf 2332 2333 nothingT :: T 2334 nothingT = Leaf 2335 2336 justT :: T -> T 2337 justT = Stem 2338 2339 pairT :: T -> T -> T 2340 pairT = Fork 2341 2342 byteT :: Integer -> T 2343 byteT = ofNumber 2344 2345 bytesT :: [Integer] -> T 2346 bytesT = ofList . fmap byteT 2347 2348 bytesExpr :: [Integer] -> String 2349 bytesExpr xs = "[" ++ unwords (map (\n -> "(" ++ show n ++ ")") xs) ++ "]" 2350 2351 u16 :: Integer -> [Integer] 2352 u16 n = [0,n] 2353 2354 u32 :: Integer -> [Integer] 2355 u32 n = [0,0,0,n] 2356 2357 u64 :: Integer -> [Integer] 2358 u64 n = [0,0,0,0,0,0,0,n] 2359 2360 arboricxHeaderBytes :: Integer -> [Integer] 2361 arboricxHeaderBytes sectionCount = 2362 [65,82,66,79,82,73,67,88] 2363 ++ u16 1 2364 ++ u16 0 2365 ++ u32 sectionCount 2366 ++ u64 0 2367 ++ u64 32 2368 2369 sectionEntryBytes :: [Integer] -> Integer -> Integer -> [Integer] 2370 sectionEntryBytes sectionType offset lengthBytes = 2371 sectionType 2372 ++ u16 1 2373 ++ u16 1 2374 ++ u16 0 2375 ++ u16 1 2376 ++ u64 offset 2377 ++ u64 lengthBytes 2378 ++ replicate 32 0 2379 2380 manifestSectionIdBytes :: [Integer] 2381 manifestSectionIdBytes = [0,0,0,1] 2382 2383 nodesSectionIdBytes :: [Integer] 2384 nodesSectionIdBytes = [0,0,0,2] 2385 2386 hexTextBytes :: Text -> [Integer] 2387 hexTextBytes h = go (unpack h) 2388 where 2389 go [] = [] 2390 go (a:b:rest) = toInteger (digitToInt a * 16 + digitToInt b) : go rest 2391 go _ = error "odd-length hex text" 2392 2393 manifestEntryBytes :: Integer -> Integer -> [Integer] 2394 manifestEntryBytes = sectionEntryBytes manifestSectionIdBytes 2395 2396 nodesEntryBytes :: Integer -> Integer -> [Integer] 2397 nodesEntryBytes = sectionEntryBytes nodesSectionIdBytes 2398 2399 simpleContainerBytes :: [Integer] -> [Integer] -> [Integer] 2400 simpleContainerBytes manifestBytes nodesBytes = 2401 let manifestOffset = 152 2402 nodesOffset = manifestOffset + fromIntegral (length manifestBytes) 2403 in arboricxHeaderBytes 2 2404 ++ manifestEntryBytes manifestOffset (fromIntegral $ length manifestBytes) 2405 ++ nodesEntryBytes nodesOffset (fromIntegral $ length nodesBytes) 2406 ++ manifestBytes 2407 ++ nodesBytes 2408 2409 singleSectionContainerBytes :: [Integer] -> [Integer] -> [Integer] 2410 singleSectionContainerBytes sectionType sectionBytes = 2411 arboricxHeaderBytes 1 2412 ++ sectionEntryBytes sectionType 92 (fromIntegral $ length sectionBytes) 2413 ++ sectionBytes 2414 2415 arboricxHeaderT :: Integer -> T 2416 arboricxHeaderT sectionCount = 2417 pairT (bytesT [0,1]) 2418 (pairT (bytesT [0,0]) 2419 (pairT (bytesT $ u32 sectionCount) 2420 (pairT (bytesT $ u64 0) 2421 (bytesT $ u64 32)))) 2422 2423 sectionRecordT :: [Integer] -> Integer -> Integer -> T 2424 sectionRecordT sectionType offset lengthBytes = 2425 pairT (bytesT sectionType) 2426 (pairT (bytesT [0,1]) 2427 (pairT (bytesT [0,1]) 2428 (pairT (bytesT [0,0]) 2429 (pairT (bytesT [0,1]) 2430 (pairT (bytesT $ u64 offset) 2431 (pairT (bytesT $ u64 lengthBytes) 2432 (bytesT $ replicate 32 0))))))) 2433 2434 sectionRecordExpr :: [Integer] -> Integer -> Integer -> String 2435 sectionRecordExpr sectionType offset lengthBytes = 2436 "(pair " ++ bytesExpr sectionType 2437 ++ " (pair " ++ bytesExpr [0,1] 2438 ++ " (pair " ++ bytesExpr [0,1] 2439 ++ " (pair " ++ bytesExpr [0,0] 2440 ++ " (pair " ++ bytesExpr [0,1] 2441 ++ " (pair " ++ bytesExpr (u64 offset) 2442 ++ " (pair " ++ bytesExpr (u64 lengthBytes) 2443 ++ " " ++ bytesExpr (replicate 32 0) 2444 ++ ")))))))" 2445 2446 byteListUtilities :: TestTree 2447 byteListUtilities = testGroup "Byte List Utility Tests" 2448 [ testCase "isNil: empty list is nil" $ do 2449 let input = "bytesNil? []" 2450 let env = evalTricu allTestLibsEnv (parseTricu input) 2451 result env @?= trueT 2452 2453 , testCase "isNil: non-empty list is not nil" $ do 2454 let input = "bytesNil? [(1)]" 2455 let env = evalTricu allTestLibsEnv (parseTricu input) 2456 result env @?= falseT 2457 2458 , testCase "head: empty list is nothing" $ do 2459 let input = "bytesHead []" 2460 let env = evalTricu allTestLibsEnv (parseTricu input) 2461 result env @?= nothingT 2462 2463 , testCase "head: non-empty list returns first element" $ do 2464 let input = "bytesHead [(1) (2)]" 2465 let env = evalTricu allTestLibsEnv (parseTricu input) 2466 result env @?= justT (byteT 1) 2467 2468 , testCase "tail: empty list is nothing" $ do 2469 let input = "bytesTail []" 2470 let env = evalTricu allTestLibsEnv (parseTricu input) 2471 result env @?= nothingT 2472 2473 , testCase "tail: non-empty list returns rest" $ do 2474 let input = "bytesTail [(1) (2)]" 2475 let env = evalTricu allTestLibsEnv (parseTricu input) 2476 result env @?= justT (bytesT [2]) 2477 2478 , testCase "length: empty list is zero" $ do 2479 let input = "bytesLength []" 2480 let env = evalTricu allTestLibsEnv (parseTricu input) 2481 result env @?= ofNumber 0 2482 2483 , testCase "length: single element list is one" $ do 2484 let input = "bytesLength [(1)]" 2485 let env = evalTricu allTestLibsEnv (parseTricu input) 2486 result env @?= ofNumber 1 2487 2488 , testCase "length: three element list is three" $ do 2489 let input = "bytesLength [(1) (2) (3)]" 2490 let env = evalTricu allTestLibsEnv (parseTricu input) 2491 result env @?= ofNumber 3 2492 2493 , testCase "append: empty ++ [1,2] = [1,2]" $ do 2494 let input = "bytesAppend [] [(1) (2)]" 2495 let env = evalTricu allTestLibsEnv (parseTricu input) 2496 result env @?= bytesT [1,2] 2497 2498 , testCase "append: [1,2] ++ [3] = [1,2,3]" $ do 2499 let input = "bytesAppend [(1) (2)] [(3)]" 2500 let env = evalTricu allTestLibsEnv (parseTricu input) 2501 result env @?= bytesT [1,2,3] 2502 2503 , testCase "append: [1,2] ++ empty = [1,2]" $ do 2504 let input = "bytesAppend [(1) (2)] []" 2505 let env = evalTricu allTestLibsEnv (parseTricu input) 2506 result env @?= bytesT [1,2] 2507 2508 , testCase "take: take 0 any list = empty" $ do 2509 let input = "bytesTake 0 [(1) (2) (3)]" 2510 let env = evalTricu allTestLibsEnv (parseTricu input) 2511 result env @?= bytesT [] 2512 2513 , testCase "take: take 2 [1,2,3] = [1,2]" $ do 2514 let input = "bytesTake 2 [(1) (2) (3)]" 2515 let env = evalTricu allTestLibsEnv (parseTricu input) 2516 result env @?= bytesT [1,2] 2517 2518 , testCase "take: take 5 [1,2] = [1,2] (overlong)" $ do 2519 let input = "bytesTake 5 [(1) (2)]" 2520 let env = evalTricu allTestLibsEnv (parseTricu input) 2521 result env @?= bytesT [1,2] 2522 2523 , testCase "drop: drop 0 any list = list" $ do 2524 let input = "bytesDrop 0 [(1) (2) (3)]" 2525 let env = evalTricu allTestLibsEnv (parseTricu input) 2526 result env @?= bytesT [1,2,3] 2527 2528 , testCase "drop: drop 2 [1,2,3] = [3]" $ do 2529 let input = "bytesDrop 2 [(1) (2) (3)]" 2530 let env = evalTricu allTestLibsEnv (parseTricu input) 2531 result env @?= bytesT [3] 2532 2533 , testCase "drop: drop 5 [1,2] = empty (overlong)" $ do 2534 let input = "bytesDrop 5 [(1) (2)]" 2535 let env = evalTricu allTestLibsEnv (parseTricu input) 2536 result env @?= bytesT [] 2537 2538 , testCase "splitAt: splitAt 0 [1,2] = pair [] [1,2]" $ do 2539 let input = "bytesSplitAt 0 [(1) (2)]" 2540 let env = evalTricu allTestLibsEnv (parseTricu input) 2541 result env @?= pairT (bytesT []) (bytesT [1,2]) 2542 2543 , testCase "splitAt: splitAt 2 [1,2,3] = pair [1,2] [3]" $ do 2544 let input = "bytesSplitAt 2 [(1) (2) (3)]" 2545 let env = evalTricu allTestLibsEnv (parseTricu input) 2546 result env @?= pairT (bytesT [1,2]) (bytesT [3]) 2547 2548 , testCase "splitAt: splitAt 5 [1,2] = pair [1,2] []" $ do 2549 let input = "bytesSplitAt 5 [(1) (2)]" 2550 let env = evalTricu allTestLibsEnv (parseTricu input) 2551 result env @?= pairT (bytesT [1,2]) (bytesT []) 2552 2553 , testCase "byteEq: equal bytes are equal" $ do 2554 let input = "equal? 1 1" 2555 let env = evalTricu allTestLibsEnv (parseTricu input) 2556 result env @?= trueT 2557 2558 , testCase "byteEq: unequal bytes are not equal" $ do 2559 let input = "equal? 1 2" 2560 let env = evalTricu allTestLibsEnv (parseTricu input) 2561 result env @?= falseT 2562 2563 , testCase "bytesEq: empty == empty" $ do 2564 let input = "bytesEq? [] []" 2565 let env = evalTricu allTestLibsEnv (parseTricu input) 2566 result env @?= trueT 2567 2568 , testCase "bytesEq: empty != [1]" $ do 2569 let input = "bytesEq? [] [(1)]" 2570 let env = evalTricu allTestLibsEnv (parseTricu input) 2571 result env @?= falseT 2572 2573 , testCase "bytesEq: [1] != empty" $ do 2574 let input = "bytesEq? [(1)] []" 2575 let env = evalTricu allTestLibsEnv (parseTricu input) 2576 result env @?= falseT 2577 2578 , testCase "bytesEq: equal lists are equal" $ do 2579 let input = "bytesEq? [(1) (2) (3)] [(1) (2) (3)]" 2580 let env = evalTricu allTestLibsEnv (parseTricu input) 2581 result env @?= trueT 2582 2583 , testCase "bytesEq: different last element" $ do 2584 let input = "bytesEq? [(1) (2) (3)] [(1) (2) (4)]" 2585 let env = evalTricu allTestLibsEnv (parseTricu input) 2586 result env @?= falseT 2587 2588 , testCase "bytesEq: different lengths" $ do 2589 let input = "bytesEq? [(1) (2)] [(1) (2) (3)]" 2590 let env = evalTricu allTestLibsEnv (parseTricu input) 2591 result env @?= falseT 2592 ] 2593 2594 -- -------------------------------------------------------------------------- 2595 -- Binary parser combinator tests 2596 -- -------------------------------------------------------------------------- 2597 2598 parserOk :: T -> T -> T 2599 parserOk val rest = Fork trueT (Fork val rest) 2600 2601 parserErr :: T -> T -> T 2602 parserErr code rest = Fork falseT (Fork code rest) 2603 2604 binaryParserTests :: TestTree 2605 binaryParserTests = testGroup "Binary Parser Tests" 2606 [ testCase "pureParser succeeds" $ do 2607 let input = "pureParser 42 [(1) (2)]" 2608 env = evalTricu allTestLibsEnv (parseTricu input) 2609 result env @?= parserOk (ofNumber 42) (bytesT [1, 2]) 2610 2611 , testCase "failParser fails" $ do 2612 let input = "failParser 99 [(1) (2)]" 2613 env = evalTricu allTestLibsEnv (parseTricu input) 2614 result env @?= parserErr (ofNumber 99) (bytesT [1, 2]) 2615 2616 , testCase "mapParser transforms value" $ do 2617 let input = "mapParser succ readU8 [(1) (2)]" 2618 env = evalTricu allTestLibsEnv (parseTricu input) 2619 result env @?= parserOk (ofNumber 2) (bytesT [2]) 2620 2621 , testCase "bindParser chains parsers" $ do 2622 let input = "bindParser readU8 (x : readU8) [(1) (2)]" 2623 env = evalTricu allTestLibsEnv (parseTricu input) 2624 result env @?= parserOk (ofNumber 2) (bytesT []) 2625 2626 , testCase "thenParser discards first result" $ do 2627 let input = "thenParser readU8 readU8 [(1) (2)]" 2628 env = evalTricu allTestLibsEnv (parseTricu input) 2629 result env @?= parserOk (ofNumber 2) (bytesT []) 2630 2631 , testCase "orParser tries second on first failure" $ do 2632 let input = "orParser (failParser 1) readU8 [(5)]" 2633 env = evalTricu allTestLibsEnv (parseTricu input) 2634 result env @?= parserOk (ofNumber 5) (bytesT []) 2635 2636 , testCase "orParser returns first on success" $ do 2637 let input = "orParser readU8 (failParser 1) [(5)]" 2638 env = evalTricu allTestLibsEnv (parseTricu input) 2639 result env @?= parserOk (ofNumber 5) (bytesT []) 2640 2641 , testCase "readWhile consumes matching bytes" $ do 2642 let input = "readWhile (x : lt? x 3) [(1) (2) (3) (4)]" 2643 env = evalTricu allTestLibsEnv (parseTricu input) 2644 result env @?= parserOk (bytesT [1, 2]) (bytesT [3, 4]) 2645 2646 , testCase "readWhile leaves non-matching byte" $ do 2647 let input = "bindParser (readWhile (x : lt? x 3)) (x : readU8) [(1) (2) (3)]" 2648 env = evalTricu allTestLibsEnv (parseTricu input) 2649 result env @?= parserOk (ofNumber 3) (bytesT []) 2650 2651 , testCase "readUntil stops at matching byte" $ do 2652 let input = "readUntil (x : equal? x 3) [(1) (2) (3) (4)]" 2653 env = evalTricu allTestLibsEnv (parseTricu input) 2654 result env @?= parserOk (bytesT [1, 2]) (bytesT [3, 4]) 2655 2656 , testCase "readRemaining returns all bytes" $ do 2657 let input = "readRemaining [(1) (2) (3)]" 2658 env = evalTricu allTestLibsEnv (parseTricu input) 2659 result env @?= parserOk (bytesT [1, 2, 3]) (bytesT []) 2660 2661 , testCase "peekU8 does not consume" $ do 2662 let input = "bindParser peekU8 (x : readU8) [(7) (8)]" 2663 env = evalTricu allTestLibsEnv (parseTricu input) 2664 result env @?= parserOk (ofNumber 7) (bytesT [8]) 2665 2666 , testCase "peekU8 second read gets same byte" $ do 2667 let input = "bindParser peekU8 (x : bindParser peekU8 (y : pureParser (pair x y))) [(7)]" 2668 env = evalTricu allTestLibsEnv (parseTricu input) 2669 result env @?= parserOk (pairT (ofNumber 7) (ofNumber 7)) (bytesT [7]) 2670 2671 , testCase "eof? succeeds at empty input" $ do 2672 let input = "eof? []" 2673 env = evalTricu allTestLibsEnv (parseTricu input) 2674 result env @?= parserOk Leaf (bytesT []) 2675 2676 , testCase "eof? fails on non-empty input" $ do 2677 let input = "eof? [(1)]" 2678 env = evalTricu allTestLibsEnv (parseTricu input) 2679 result env @?= parserErr (ofNumber 1) (bytesT [1]) 2680 2681 , testCase "expectAscii matches string" $ do 2682 let input = "expectAscii \"hi\" [(104) (105) (106)]" 2683 env = evalTricu allTestLibsEnv (parseTricu input) 2684 result env @?= parserOk Leaf (bytesT [106]) 2685 2686 , testCase "expectAscii fails on mismatch" $ do 2687 let input = "expectAscii \"hi\" [(104) (99)]" 2688 env = evalTricu allTestLibsEnv (parseTricu input) 2689 result env @?= parserErr (ofNumber 2) (bytesT [104, 99]) 2690 ] 2691 2692 -- -------------------------------------------------------------------------- 2693 -- IO driver tests 2694 -- -------------------------------------------------------------------------- 2695 2696 ioDriverTests :: TestTree 2697 ioDriverTests = testGroup "IO driver tests" 2698 [ -- Existing behaviour tests 2699 testCase "readFile through onReadFile returns file contents" $ 2700 withSystemTempDirectory "tricu-io-read" $ \dir -> do 2701 let sourcePath = dir ++ "/input.txt" 2702 writeFile sourcePath "abc123" 2703 final <- runIOSource $ 2704 unlines 2705 [ "main = io (onReadFile \"" ++ sourcePath ++ "\"" 2706 , " (err rest : pure \"read failed\")" 2707 , " (contents rest : pure contents))" 2708 ] 2709 final @?= ofString "abc123" 2710 2711 , testCase "readFile error path returns explicit error branch" $ 2712 withSystemTempDirectory "tricu-io-read-missing" $ \dir -> do 2713 let sourcePath = dir ++ "/missing.txt" 2714 final <- runIOSource $ 2715 unlines 2716 [ "main = io (onReadFile \"" ++ sourcePath ++ "\"" 2717 , " (err rest : pure \"read failed\")" 2718 , " (contents rest : pure contents))" 2719 ] 2720 final @?= ofString "read failed" 2721 2722 , testCase "chains multiple readFile actions through Result-aware helper" $ 2723 withSystemTempDirectory "tricu-io-chain" $ \dir -> do 2724 let firstPath = dir ++ "/first.txt" 2725 secondPath = dir ++ "/second.txt" 2726 writeFile firstPath "abc" 2727 writeFile secondPath "def" 2728 final <- runIOSource $ 2729 unlines 2730 [ "main = io (onReadFile \"" ++ firstPath ++ "\"" 2731 , " (err rest : pure \"first read failed\")" 2732 , " (first rest : onReadFile \"" ++ secondPath ++ "\"" 2733 , " (err rest : pure \"second read failed\")" 2734 , " (second rest : pure (append first second))))" 2735 ] 2736 final @?= ofString "abcdef" 2737 2738 -- Monad law tests 2739 , testCase "left identity: bind (pure x) f == f x" $ do 2740 left <- runIOSource $ 2741 unlines 2742 [ "f = x : pure (append x \"!\")" 2743 , "main = io (bind (pure \"abc\") f)" 2744 ] 2745 right <- runIOSource $ 2746 unlines 2747 [ "f = x : pure (append x \"!\")" 2748 , "main = io (f \"abc\")" 2749 ] 2750 left @?= right 2751 left @?= ofString "abc!" 2752 2753 , testCase "right identity: bind m pure == m" $ 2754 withSystemTempDirectory "tricu-io-right-id" $ \dir -> do 2755 let path = dir ++ "/input.txt" 2756 writeFile path "abc" 2757 left <- runIOSource $ 2758 unlines 2759 [ "main = io (bind (readFile \"" ++ path ++ "\")" 2760 , " (result : pure result))" 2761 ] 2762 right <- runIOSource $ 2763 unlines 2764 [ "main = io (readFile \"" ++ path ++ "\")" 2765 ] 2766 left @?= right 2767 left @?= ioOkResult (ofString "abc") 2768 2769 , testCase "associativity: bind (bind m f) g == bind m (x : bind (f x) g)" $ 2770 withSystemTempDirectory "tricu-io-assoc" $ \dir -> do 2771 let path = dir ++ "/input.txt" 2772 writeFile path "abc" 2773 left <- runIOSource $ 2774 unlines 2775 [ "m = readFile \"" ++ path ++ "\"" 2776 , "f = result : matchResult (err rest : pure \"read failed\") (contents rest : pure (append contents \"-f\")) result" 2777 , "g = value : pure (append value \"-g\")" 2778 , "main = io (bind (bind m f) g)" 2779 ] 2780 right <- runIOSource $ 2781 unlines 2782 [ "m = readFile \"" ++ path ++ "\"" 2783 , "f = result : matchResult (err rest : pure \"read failed\") (contents rest : pure (append contents \"-f\")) result" 2784 , "g = value : pure (append value \"-g\")" 2785 , "main = io (bind m (x : bind (f x) g))" 2786 ] 2787 left @?= right 2788 left @?= ofString "abc-f-g" 2789 2790 , testCase "associativity preserves error flow" $ 2791 withSystemTempDirectory "tricu-io-assoc-err" $ \dir -> do 2792 let missingPath = dir ++ "/missing.txt" 2793 left <- runIOSource $ 2794 unlines 2795 [ "m = readFile \"" ++ missingPath ++ "\"" 2796 , "f = result : matchResult (err rest : pure \"handled\") (contents rest : pure (append contents \"-ok\")) result" 2797 , "g = value : pure (append value \"-g\")" 2798 , "main = io (bind (bind m f) g)" 2799 ] 2800 right <- runIOSource $ 2801 unlines 2802 [ "m = readFile \"" ++ missingPath ++ "\"" 2803 , "f = result : matchResult (err rest : pure \"handled\") (contents rest : pure (append contents \"-ok\")) result" 2804 , "g = value : pure (append value \"-g\")" 2805 , "main = io (bind m (x : bind (f x) g))" 2806 ] 2807 left @?= right 2808 left @?= ofString "handled-g" 2809 2810 , testCase "bind defers continuation until left action completes" $ 2811 withSystemTempDirectory "tricu-io-lazy-k" $ \dir -> do 2812 let path = dir ++ "/created.txt" 2813 final <- runIOSource $ 2814 unlines 2815 [ "main = io (bind (writeFile \"" ++ path ++ "\" \"created\")" 2816 , " (_ : readFile \"" ++ path ++ "\"))" 2817 ] 2818 final @?= ioOkResult (ofString "created") 2819 2820 -- Primitive effect shape tests 2821 , testCase "readFile without continuation returns Result" $ 2822 withSystemTempDirectory "tricu-io-raw-read" $ \dir -> do 2823 let path = dir ++ "/input.txt" 2824 writeFile path "abc" 2825 final <- runIOSource $ 2826 unlines 2827 [ "main = io (readFile \"" ++ path ++ "\")" 2828 ] 2829 final @?= ioOkResult (ofString "abc") 2830 2831 , testCase "writeFile then readFile executes exactly once" $ 2832 withSystemTempDirectory "tricu-io-once" $ \dir -> do 2833 let path = dir ++ "/test.txt" 2834 final <- runIOSource $ 2835 unlines 2836 [ "main = io (bind (writeFile \"" ++ path ++ "\" \"abc\")" 2837 , " (_ : readFile \"" ++ path ++ "\"))" 2838 ] 2839 final @?= ioOkResult (ofString "abc") 2840 2841 , testCase "sequencing order is left-to-right" $ 2842 withSystemTempDirectory "tricu-io-order" $ \dir -> do 2843 let path = dir ++ "/test.txt" 2844 final <- runIOSource $ 2845 unlines 2846 [ "main = io (bind (writeFile \"" ++ path ++ "\" \"a\")" 2847 , " (_ : bind (writeFile \"" ++ path ++ "\" \"ab\")" 2848 , " (_ : readFile \"" ++ path ++ "\")))" 2849 ] 2850 final @?= ioOkResult (ofString "ab") 2851 2852 , testCase "thenIO sequences two actions and discards first result" $ 2853 withSystemTempDirectory "tricu-io-then" $ \dir -> do 2854 let path = dir ++ "/test.txt" 2855 final <- runIOSource $ 2856 unlines 2857 [ "main = io (thenIO (writeFile \"" ++ path ++ "\" \"x\")" 2858 , " (readFile \"" ++ path ++ "\"))" 2859 ] 2860 final @?= ioOkResult (ofString "x") 2861 2862 , testCase "bind does not short-circuit on readFile error" $ 2863 withSystemTempDirectory "tricu-io-no-short" $ \dir -> do 2864 let path = dir ++ "/missing.txt" 2865 final <- runIOSource $ 2866 unlines 2867 [ "main = io (bind (readFile \"" ++ path ++ "\")" 2868 , " (result : pure \"continued\"))" 2869 ] 2870 final @?= ofString "continued" 2871 2872 , testCase "mapIO transforms pure value" $ do 2873 final <- runIOSource $ 2874 unlines 2875 [ "main = io (mapIO (pure \"abc\") (x : append x \"!\"))" 2876 ] 2877 final @?= ofString "abc!" 2878 2879 -- Malformed action tests 2880 , testCase "unknown IO action tag returns err result" $ do 2881 final <- runIOSource "main = io (pair 99 t)" 2882 final @?= ioErrResult "invalid action" 2883 2884 , testCase "malformed Bind returns err result" $ do 2885 final <- runIOSource "main = io (pair 1 t)" 2886 final @?= ioErrResult "invalid action" 2887 2888 , testCase "malformed ReadFile payload returns err result" $ do 2889 final <- runIOSource "main = io (readFile (t t))" 2890 final @?= ioErrResult "invalid string" 2891 2892 -- Permission tests 2893 , testCase "allowed read path succeeds" $ 2894 withSystemTempDirectory "tricu-io-allowed" $ \dir -> do 2895 let path = dir ++ "/allowed.txt" 2896 writeFile path "allowed" 2897 let perms = defaultPerms { allowRead = [path] } 2898 result <- runIOSourceWithPerms perms $ 2899 unlines 2900 [ "main = io (readFile \"" ++ path ++ "\")" 2901 ] 2902 result @?= ioOkResult (ofString "allowed") 2903 2904 , testCase "readFile denied path returns err result" $ 2905 withSystemTempDirectory "tricu-io-read-denied" $ \dir -> do 2906 let allowedPath = dir ++ "/allowed.txt" 2907 deniedPath = dir ++ "/denied.txt" 2908 writeFile allowedPath "allowed" 2909 writeFile deniedPath "denied" 2910 let perms = defaultPerms { allowRead = [allowedPath] } 2911 result <- runIOSourceWithPerms perms $ 2912 unlines 2913 [ "main = io (readFile \"" ++ deniedPath ++ "\")" 2914 ] 2915 result @?= ioErrResult "permission denied" 2916 2917 , testCase "writeFile denied path returns err result" $ 2918 withSystemTempDirectory "tricu-io-write-denied" $ \dir -> do 2919 let allowedPath = dir ++ "/allowed.txt" 2920 deniedPath = dir ++ "/denied.txt" 2921 let perms = defaultPerms { allowWrite = [allowedPath] } 2922 result <- runIOSourceWithPerms perms $ 2923 unlines 2924 [ "main = io (writeFile \"" ++ deniedPath ++ "\" \"x\")" 2925 ] 2926 result @?= ioErrResult "permission denied" 2927 2928 , testCase "path prefix does not allow prefix bypass" $ 2929 withSystemTempDirectory "tricu-io-prefix" $ \dir -> do 2930 let allowedDir = dir ++ "/foo" 2931 bypassPath = dir ++ "/foobar/secret.txt" 2932 createDirectory allowedDir 2933 createDirectory (dir ++ "/foobar") 2934 writeFile bypassPath "secret" 2935 let perms = defaultPerms { allowRead = [allowedDir] } 2936 result <- runIOSourceWithPerms perms $ 2937 unlines 2938 [ "main = io (readFile \"" ++ bypassPath ++ "\")" 2939 ] 2940 result @?= ioErrResult "permission denied" 2941 2942 -- Pure test 2943 , testCase "pure performs no effects" $ do 2944 final <- runIOSource "main = io (pure \"abc\")" 2945 final @?= ofString "abc" 2946 2947 -- Reader tests 2948 , testCase "ask returns initial environment" $ do 2949 final <- runIOSourceWithEnv unsafePerms (ofString "dev") $ 2950 unlines 2951 [ "main = io (bind ask (env : pure env))" 2952 ] 2953 final @?= ofString "dev" 2954 2955 , testCase "local transforms environment" $ do 2956 final <- runIOSourceWithEnv unsafePerms (ofString "root") $ 2957 unlines 2958 [ "main = io (local (env : append env \"-local\") (bind ask (env : pure env)))" 2959 ] 2960 final @?= ofString "root-local" 2961 2962 , testCase "local restores environment afterward" $ do 2963 final <- runIOSourceWithEnv unsafePerms (ofString "root") $ 2964 unlines 2965 [ "main = io (bind ask (before :" 2966 , " bind (local (env : append env \"-local\") (bind ask (env : pure env))) (inside :" 2967 , " bind ask (after :" 2968 , " pure (pair before (pair inside after))))))" 2969 ] 2970 final @?= Fork (ofString "root") (Fork (ofString "root-local") (ofString "root")) 2971 2972 , testCase "nested local composes correctly" $ do 2973 final <- runIOSourceWithEnv unsafePerms (ofString "root") $ 2974 unlines 2975 [ "f = x : append x \"-f\"" 2976 , "g = x : append x \"-g\"" 2977 , "main = io (bind" 2978 , " (local f (local g (bind ask (env : pure env))))" 2979 , " (inner :" 2980 , " bind ask (after :" 2981 , " pure (pair inner after))))" 2982 ] 2983 final @?= Fork (ofString "root-f-g") (ofString "root") 2984 2985 , testCase "local result passes through bind correctly" $ do 2986 final <- runIOSourceWithEnv unsafePerms (ofString "root") $ 2987 unlines 2988 [ "main = io (bind" 2989 , " (local (env : append env \"-local\") (pure \"value\"))" 2990 , " (x : pure x))" 2991 ] 2992 final @?= ofString "value" 2993 2994 , testCase "IO inside local uses transformed environment and restores after" $ do 2995 final <- runIOSourceWithEnv unsafePerms (ofString "root") $ 2996 unlines 2997 [ "main = io (bind" 2998 , " (local (env : append env \"-local\")" 2999 , " (bind ask (env : pure env)))" 3000 , " (result :" 3001 , " bind ask (after :" 3002 , " pure (pair result after))))" 3003 ] 3004 final @?= Fork (ofString "root-local") (ofString "root") 3005 3006 , testCase "local does not affect outer bind continuation" $ do 3007 final <- runIOSourceWithEnv unsafePerms (ofString "root") $ 3008 unlines 3009 [ "main = io (bind" 3010 , " (local (env : append env \"-local\") (pure \"x\"))" 3011 , " (_ : bind ask (env : pure env)))" 3012 ] 3013 final @?= ofString "root" 3014 3015 , testCase "local environment persists across inner binds" $ do 3016 final <- runIOSourceWithEnv unsafePerms (ofString "root") $ 3017 unlines 3018 [ "main = io (local (env : append env \"-local\")" 3019 , " (bind (pure t) (_ :" 3020 , " bind ask (env : pure env))))" 3021 ] 3022 final @?= ofString "root-local" 3023 3024 , testCase "local restores environment when scoped action returns error value" $ do 3025 final <- runIOSourceWithEnv defaultPerms (ofString "root") $ 3026 unlines 3027 [ "main = io (bind" 3028 , " (local (env : append env \"-local\") (readFile \"definitely-missing.txt\"))" 3029 , " (_ : bind ask (env : pure env)))" 3030 ] 3031 final @?= ofString "root" 3032 3033 -- State tests 3034 , testCase "get returns initial state" $ do 3035 (final, st) <- runIOSourceWith unsafePerms Leaf (ofNumber 42) $ 3036 unlines 3037 [ "main = io (bind get (s : pure s))" 3038 ] 3039 final @?= ofNumber 42 3040 st @?= ofNumber 42 3041 3042 , testCase "put updates state" $ do 3043 (final, st) <- runIOSourceWith unsafePerms Leaf (ofNumber 0) $ 3044 unlines 3045 [ "main = io (bind (put 100) (_ : bind get (s : pure s)))" 3046 ] 3047 final @?= ofNumber 100 3048 st @?= ofNumber 100 3049 3050 , testCase "state persists through bind" $ do 3051 (final, st) <- runIOSourceWith unsafePerms Leaf (ofNumber 5) $ 3052 unlines 3053 [ "main = io (bind get (s1 :" 3054 , " bind (put (succ s1)) (_ :" 3055 , " bind get (s2 :" 3056 , " pure (pair s1 s2)))))" 3057 ] 3058 final @?= Fork (ofNumber 5) (ofNumber 6) 3059 st @?= ofNumber 6 3060 3061 , testCase "local does not restore state" $ do 3062 (final, st) <- runIOSourceWith unsafePerms Leaf (ofNumber 0) $ 3063 unlines 3064 [ "main = io (bind (put 10) (_ :" 3065 , " bind (local (env : env) (put 20)) (_ :" 3066 , " bind get (s :" 3067 , " pure s))))" 3068 ] 3069 final @?= ofNumber 20 3070 st @?= ofNumber 20 3071 3072 , testCase "state and reader are independent" $ do 3073 (final, st) <- runIOSourceWith unsafePerms (ofString "hello") (ofNumber 42) $ 3074 unlines 3075 [ "main = io (bind ask (env :" 3076 , " bind get (s :" 3077 , " pure (pair env s))))" 3078 ] 3079 final @?= Fork (ofString "hello") (ofNumber 42) 3080 st @?= ofNumber 42 3081 3082 -- Async tests 3083 , testCase "fork returns handle and await returns child value" $ do 3084 (final, st) <- runIOSourceWith unsafePerms Leaf Leaf $ 3085 unlines 3086 [ "main = io (bind (fork (pure \"child\")) (h :" 3087 , " await h))" 3088 ] 3089 final @?= ofString "child" 3090 st @?= Leaf 3091 3092 , testCase "main completion abandons unawaited child" $ do 3093 (final, _) <- runIOSourceWith unsafePerms Leaf Leaf $ 3094 unlines 3095 [ "main = io (bind (fork (pure \"child\")) (_ :" 3096 , " pure \"main\"))" 3097 ] 3098 final @?= ofString "main" 3099 3100 , testCase "fork captures reader environment at fork point" $ do 3101 (final, _) <- runIOSourceWith unsafePerms (ofString "root") Leaf $ 3102 unlines 3103 [ "main = io (local (env : append env \"-local\")" 3104 , " (bind (fork (bind ask (env : pure env))) (h :" 3105 , " await h)))" 3106 ] 3107 final @?= ofString "root-local" 3108 3109 , testCase "fork inside local captures child env and parent restores env" $ do 3110 (final, _) <- runIOSourceWith unsafePerms (ofString "root") Leaf $ 3111 unlines 3112 [ "main = io (bind" 3113 , " (local (env : append env \"-local\")" 3114 , " (fork (bind ask (env : pure env))))" 3115 , " (h : bind ask (after :" 3116 , " bind (await h) (child :" 3117 , " pure (pair after child)))))" 3118 ] 3119 final @?= Fork (ofString "root") (ofString "root-local") 3120 3121 , testCase "fork copies state and child state does not merge" $ do 3122 (final, st) <- runIOSourceWith unsafePerms Leaf (ofNumber 0) $ 3123 unlines 3124 [ "main = io (bind (put 1) (_ :" 3125 , " bind (fork (bind (put 99) (_ : bind get (s : pure s)))) (h :" 3126 , " bind (put 2) (_ :" 3127 , " bind (await h) (childState :" 3128 , " bind get (parentState :" 3129 , " pure (pair childState parentState)))))))" 3130 ] 3131 final @?= Fork (ofNumber 99) (ofNumber 2) 3132 st @?= ofNumber 2 3133 3134 , testCase "multiple awaiters receive same completed value" $ do 3135 (final, _) <- runIOSourceWith unsafePerms Leaf Leaf $ 3136 unlines 3137 [ "main = io (bind (fork (pure \"done\")) (h :" 3138 , " bind (await h) (a :" 3139 , " bind (await h) (b :" 3140 , " pure (pair a b)))))" 3141 ] 3142 final @?= Fork (ofString "done") (ofString "done") 3143 3144 , testCase "self await returns async error" $ do 3145 (final, _) <- runIOSourceWith unsafePerms Leaf Leaf $ 3146 unlines 3147 [ "main = io (await (pair \"task\" 0))" 3148 ] 3149 final @?= ioErrResult "self await" 3150 3151 , testCase "await invalid handle returns async error" $ do 3152 (final, _) <- runIOSourceWith unsafePerms Leaf Leaf $ 3153 unlines 3154 [ "main = io (await 123)" 3155 ] 3156 final @?= ioErrResult "invalid task handle" 3157 3158 , testCase "yield returns unit and resumes continuation" $ do 3159 (final, _) <- runIOSourceWith unsafePerms Leaf Leaf $ 3160 unlines 3161 [ "main = io (bind yield (_ : pure \"after\"))" 3162 ] 3163 final @?= ofString "after" 3164 3165 , testCase "sleep resumes continuation" $ do 3166 (final, _) <- runIOSourceWith unsafePerms Leaf Leaf $ 3167 unlines 3168 [ "main = io (bind (sleep 1) (_ : pure \"awake\"))" 3169 ] 3170 final @?= ofString "awake" 3171 3172 , testCase "await waits for sleeping child" $ do 3173 (final, _) <- runIOSourceWith unsafePerms Leaf Leaf $ 3174 unlines 3175 [ "main = io (bind (fork (bind (sleep 1) (_ : pure \"awake\"))) (h :" 3176 , " await h))" 3177 ] 3178 final @?= ofString "awake" 3179 3180 , testCase "await waits for sleeping child and returns child value" $ do 3181 (final, st) <- runIOSourceWith unsafePerms Leaf Leaf $ 3182 unlines 3183 [ "main = io (bind (fork (bind (sleep 1) (_ : pure \"child done\"))) (h :" 3184 , " await h))" 3185 ] 3186 final @?= ofString "child done" 3187 st @?= Leaf 3188 3189 , testCase "sleep inside bind resumes as unit" $ do 3190 (final, st) <- runIOSourceWith unsafePerms Leaf Leaf $ 3191 unlines 3192 [ "main = io (bind (sleep 1) (_ : pure \"awake\"))" 3193 ] 3194 final @?= ofString "awake" 3195 st @?= Leaf 3196 3197 , testCase "fork await returns child value" $ do 3198 (final, st) <- runIOSourceWith unsafePerms Leaf Leaf $ 3199 unlines 3200 [ "main = io (bind (fork (pure \"child done\")) (h :" 3201 , " await h))" 3202 ] 3203 final @?= ofString "child done" 3204 st @?= Leaf 3205 3206 -- Scheduler hardening tests 3207 , testCase "runIO rejects non-IO tree with sentinel error" $ do 3208 result <- runIO unsafePerms (ofString "not an io program") 3209 case result of 3210 Left _ -> return () 3211 Right _ -> assertFailure "Expected Left for invalid sentinel" 3212 3213 , testCase "cyclic await returns error instead of hanging" $ do 3214 (final, _) <- runIOSourceWith unsafePerms Leaf Leaf $ 3215 unlines 3216 [ "main = io (bind (fork (await (pair \"task\" 0))) (h :" 3217 , " await h))" 3218 ] 3219 final @?= ioErrResult "cyclic await" 3220 3221 , testCase "writeBytes and readFile roundtrip binary data" $ 3222 withSystemTempDirectory "tricu-io-bytes" $ \dir -> do 3223 let path = dir ++ "/binary.bin" 3224 final <- runIOSource $ 3225 unlines 3226 [ "main = io (bind (writeBytes \"" ++ path ++ "\" [(0) (255) (128) (1)])" 3227 , " (_ : readFile \"" ++ path ++ "\"))" 3228 ] 3229 final @?= ioOkResult (ofBytes (BS.pack [0, 255, 128, 1])) 3230 3231 , testCase "stress test: many concurrent sleepers complete promptly" $ do 3232 let n = 5000 3233 (final, _) <- runIOSourceWith unsafePerms Leaf Leaf $ 3234 unlines 3235 [ "spawner = y (self n acc : if (equal? n 0) (pure acc) (bind (fork (sleep 1)) (h : self (pred n) (pair h acc))))" 3236 , "awaitAll = y (self hs : matchList (pure \"done\") (h r : bind (await h) (_ : self r)) hs)" 3237 , "main = io (bind (spawner " ++ show n ++ " t) (hs : awaitAll hs))" 3238 ] 3239 final @?= ofString "done" 3240 3241 , testCase "long fork await loop does not leak" $ do 3242 let n = 200 3243 build 0 = "pure \"done\"" 3244 build k = "bind (fork (pure \"x\")) (h : bind (await h) (_ : " ++ build (k - 1) ++ "))" 3245 (final, _) <- runIOSourceWith unsafePerms Leaf Leaf ("main = io (" ++ build n ++ ")") 3246 final @?= ofString "done" 3247 3248 , testGroup "Socket primitives" 3249 [ testCase "socket returns ok result with valid handle" $ do 3250 final <- runIOSource "main = io socket" 3251 final @?= ioOkResult (Fork (ofString "sock") (ofNumber 0)) 3252 3253 , testCase "closeSocket on invalid handle returns error" $ do 3254 final <- runIOSource "main = io (closeSocket (pair \"sock\" 99999))" 3255 final @?= ioErrResult "invalid socket handle" 3256 3257 , testCase "bindSocket and listen succeed on loopback port 0" $ do 3258 final <- runIOSource $ 3259 unlines 3260 [ "main = io (" 3261 , " onOk socket (server rest :" 3262 , " onOk (bindSocket server \"127.0.0.1\" 0) (_ rest :" 3263 , " bind (listen server 1) (listenResult :" 3264 , " pure listenResult))))" 3265 ] 3266 final @?= ioOkResult Leaf 3267 3268 , testCase "connect to non-listening port returns error" $ do 3269 final <- runIOSource "main = io (onOk socket (sock rest : connect sock \"127.0.0.1\" 1))" 3270 case final of 3271 Fork Leaf (Fork _ Leaf) -> return () 3272 other -> assertFailure $ "Expected error result, got: " ++ show other 3273 3274 , testCase "accept and recv receive bytes from forked client" $ 3275 withFreePort $ \port -> do 3276 final <- runIOSource $ 3277 unlines 3278 [ "client = port :" 3279 , " onOk socket (sock rest :" 3280 , " onOk (connect sock \"127.0.0.1\" port) (_ rest :" 3281 , " send sock [104 105]))" 3282 , "" 3283 , "main = io (" 3284 , " onOk socket (server rest :" 3285 , " onOk (bindSocket server \"127.0.0.1\" " ++ show port ++ ") (_ rest :" 3286 , " onOk (listen server 1) (_ rest :" 3287 , " bind (fork (client " ++ show port ++ ")) (_ :" 3288 , " onOk (accept server) (accepted rest :" 3289 , " recv (fst accepted) 2))))))" 3290 ] 3291 final @?= ioOkResult (ofBytes (BS.pack [104, 105])) 3292 3293 , testCase "client recv receives server response via accepted socket" $ 3294 withFreePort $ \port -> do 3295 final <- runIOSource $ 3296 unlines 3297 [ "serverTask = (server :" 3298 , " onOk (accept server) (accepted rest :" 3299 , " onOk (recv (fst accepted) 4) (msg rest :" 3300 , " send (fst accepted) [112 111 110 103])))" 3301 , "" 3302 , "clientTask = (port :" 3303 , " onOk socket (sock rest :" 3304 , " onOk (connect sock \"127.0.0.1\" port) (_ rest :" 3305 , " bind (send sock [112 105 110 103]) (_ :" 3306 , " recv sock 4))))" 3307 , "" 3308 , "main = io (" 3309 , " onOk socket (server rest :" 3310 , " onOk (bindSocket server \"127.0.0.1\" " ++ show port ++ ") (_ rest :" 3311 , " onOk (listen server 1) (_ rest :" 3312 , " bind (fork (serverTask server)) (_ :" 3313 , " clientTask " ++ show port ++ ")))))" 3314 ] 3315 final @?= ioOkResult (ofBytes (BS.pack [112, 111, 110, 103])) 3316 3317 , testCase "recv on closed peer returns connection closed" $ 3318 withFreePort $ \port -> do 3319 final <- runIOSource $ 3320 unlines 3321 [ "clientTask = port :" 3322 , " onOk socket (sock rest :" 3323 , " onOk (connect sock \"127.0.0.1\" port) (_ rest :" 3324 , " closeSocket sock))" 3325 , "" 3326 , "main = io (" 3327 , " onOk socket (server rest :" 3328 , " onOk (bindSocket server \"127.0.0.1\" " ++ show port ++ ") (_ rest :" 3329 , " onOk (listen server 1) (_ rest :" 3330 , " bind (fork (clientTask " ++ show port ++ ")) (_ :" 3331 , " onOk (accept server) (accepted rest :" 3332 , " bind (yield) (_ :" 3333 , " recv (fst accepted) 1)))))))" 3334 ] 3335 final @?= ioErrResult "connection closed" 3336 3337 , testCase "accept invalid socket handle returns error" $ do 3338 final <- runIOSource "main = io (accept (pair \"sock\" 99999))" 3339 final @?= ioErrResult "invalid socket handle" 3340 3341 , testCase "recv invalid socket handle returns error" $ do 3342 final <- runIOSource "main = io (recv (pair \"sock\" 99999) 1)" 3343 final @?= ioErrResult "invalid socket handle" 3344 3345 , testCase "send invalid socket handle returns error" $ do 3346 final <- runIOSource "main = io (send (pair \"sock\" 99999) [(1)])" 3347 final @?= ioErrResult "invalid socket handle" 3348 3349 , testCase "getSocketName returns positive port after bind 0" $ do 3350 final <- runIOSource $ 3351 unlines 3352 [ "main = io (" 3353 , " onOk socket (server rest :" 3354 , " onOk (bindSocket server \"127.0.0.1\" 0) (_ rest :" 3355 , " bind (getSocketName server) (nameResult :" 3356 , " pure nameResult))))" 3357 ] 3358 case final of 3359 Fork (Stem Leaf) (Fork val Leaf) -> 3360 case toNumber val of 3361 Right port | port > 0 -> return () 3362 Right 0 -> assertFailure "Expected positive port, got 0" 3363 Left _ -> assertFailure $ "Expected numeric port, got: " ++ show val 3364 other -> assertFailure $ "Expected ok result, got: " ++ show other 3365 3366 , testCase "connectTo creates connected socket" $ 3367 withFreePort $ \port -> do 3368 final <- runIOSource $ 3369 unlines 3370 [ "clientTask = port :" 3371 , " onOk (connectTo \"127.0.0.1\" port) (client rest :" 3372 , " onOk (send client [104 105]) (_ rest :" 3373 , " pure t))" 3374 , "" 3375 , "main = io (" 3376 , " onOk socket (server rest :" 3377 , " onOk (bindSocket server \"127.0.0.1\" " ++ show port ++ ") (_ rest :" 3378 , " onOk (listen server 1) (_ rest :" 3379 , " bind (fork (clientTask " ++ show port ++ ")) (_ :" 3380 , " onOk (accept server) (accepted rest :" 3381 , " onOk (recv (fst accepted) 2) (msg rest :" 3382 , " pure msg)))))))" 3383 ] 3384 final @?= ofBytes (BS.pack [104, 105]) 3385 3386 , testCase "serveOnce handles a single client connection" $ 3387 withFreePort $ \port -> do 3388 final <- runIOSource $ 3389 unlines 3390 [ "echoHandler = (client peer :" 3391 , " onOk (recv client 2) (msg rest :" 3392 , " onOk (send client msg) (_ rest :" 3393 , " pure t)))" 3394 , "" 3395 , "clientTask = (port :" 3396 , " onOk socket (sock rest :" 3397 , " onOk (connect sock \"127.0.0.1\" port) (_ rest :" 3398 , " onOk (send sock [104 105]) (_ rest :" 3399 , " onOk (recv sock 2) (msg rest :" 3400 , " pure msg)))))" 3401 , "" 3402 , "main = io (" 3403 , " onOk socket (server rest :" 3404 , " onOk (bindSocket server \"127.0.0.1\" " ++ show port ++ ") (_ rest :" 3405 , " onOk (listen server 1) (_ rest :" 3406 , " bind (fork (serveOnce server echoHandler)) (_ :" 3407 , " clientTask " ++ show port ++ ")))))" 3408 ] 3409 final @?= ofBytes (BS.pack [104, 105]) 3410 3411 , testCase "finally preserves successful action result" $ do 3412 final <- runIOSource $ 3413 unlines 3414 [ "main = io (finally (pure 42) (pure 99))" 3415 ] 3416 final @?= ofNumber 42 3417 3418 , testCase "finally runs cleanup after successful action" $ 3419 withSystemTempDirectory "tricu-finally" $ \dir -> do 3420 let cleanupPath = dir ++ "/cleanup.txt" 3421 final <- runIOSource $ 3422 unlines 3423 [ "main = io (finally" 3424 , " (pure 42)" 3425 , " (writeFile \"" ++ cleanupPath ++ "\" \"cleaned\"))" 3426 ] 3427 final @?= ofNumber 42 3428 contents <- readFile cleanupPath 3429 contents @?= "cleaned" 3430 3431 , testCase "bracket passes acquired resource to use" $ do 3432 final <- runIOSource $ 3433 unlines 3434 [ "main = io (bracket (pure 41) (_ : pure t) (r : pure (succ r)))" 3435 ] 3436 final @?= ofNumber 42 3437 3438 , testCase "bracket preserves successful use result over release result" $ do 3439 final <- runIOSource $ 3440 unlines 3441 [ "main = io (bracket (pure \"res\") (_ : pure 123) (_ : pure 99))" 3442 ] 3443 final @?= ofNumber 99 3444 3445 , testCase "bracket runs release on successful use" $ 3446 withSystemTempDirectory "tricu-bracket" $ \dir -> do 3447 let releasePath = dir ++ "/release.txt" 3448 final <- runIOSource $ 3449 unlines 3450 [ "main = io (bracket" 3451 , " (pure \"" ++ releasePath ++ "\")" 3452 , " (path : writeFile path \"released\")" 3453 , " (path : pure 99))" 3454 ] 3455 final @?= ofNumber 99 3456 contents <- readFile releasePath 3457 contents @?= "released" 3458 3459 , testCase "bracket passes acquired resource to release" $ 3460 withSystemTempDirectory "tricu-bracket-release-resource" $ \dir -> do 3461 let releasePath = dir ++ "/release.txt" 3462 final <- runIOSource $ 3463 unlines 3464 [ "main = io (bracket" 3465 , " (pure \"" ++ releasePath ++ "\")" 3466 , " (path : writeFile path \"released\")" 3467 , " (_ : pure 99))" 3468 ] 3469 final @?= ofNumber 99 3470 contents <- readFile releasePath 3471 contents @?= "released" 3472 3473 -- Directory and file management primitives 3474 , testGroup "listDirectory" 3475 [ testCase "listDirectory returns entry names" $ 3476 withSystemTempDirectory "tricu-listdir" $ \dir -> do 3477 writeFile (dir ++ "/a.txt") "a" 3478 writeFile (dir ++ "/b.txt") "b" 3479 final <- runIOSource $ 3480 unlines 3481 [ "main = io (onListDirectory \"" ++ dir ++ "\"" 3482 , " (err rest : pure false)" 3483 , " (entries rest :" 3484 , " pure (pair (lExist? \"a.txt\" entries) (lExist? \"b.txt\" entries))))" 3485 ] 3486 final @?= Fork (Stem Leaf) (Stem Leaf) 3487 3488 , testCase "listDirectory missing path returns does not exist" $ do 3489 final <- runIOSource $ 3490 unlines 3491 [ "main = io (onListDirectory \"/nonexistent/path/12345\"" 3492 , " (err rest : pure err)" 3493 , " (_ rest : pure \"ok\"))" 3494 ] 3495 final @?= ofString "does not exist" 3496 3497 , testCase "listDirectory on file returns not a directory" $ 3498 withSystemTempDirectory "tricu-listdir-file" $ \dir -> do 3499 let path = dir ++ "/file.txt" 3500 writeFile path "x" 3501 final <- runIOSource $ 3502 unlines 3503 [ "main = io (onListDirectory \"" ++ path ++ "\"" 3504 , " (err rest : pure err)" 3505 , " (_ rest : pure \"ok\"))" 3506 ] 3507 final @?= ofString "not a directory" 3508 3509 , testCase "listDirectory denied path returns permission denied" $ 3510 withSystemTempDirectory "tricu-listdir-denied" $ \dir -> do 3511 let allowedDir = dir ++ "/allowed" 3512 deniedDir = dir ++ "/denied" 3513 createDirectory allowedDir 3514 createDirectory deniedDir 3515 let perms = defaultPerms { allowRead = [allowedDir] } 3516 final <- runIOSourceWithPerms perms $ 3517 unlines 3518 [ "main = io (listDirectory \"" ++ deniedDir ++ "\")" 3519 ] 3520 final @?= ioErrResult "permission denied" 3521 ] 3522 3523 , testCase "listDirectory excludes dot entries" $ 3524 withSystemTempDirectory "tricu-listdir-dot" $ \dir -> do 3525 final <- runIOSource $ 3526 unlines 3527 [ "main = io (onListDirectory \"" ++ dir ++ "\"" 3528 , " (err rest : pure false)" 3529 , " (entries rest :" 3530 , " pure (pair (lExist? \".\" entries) (lExist? \"..\" entries))))" 3531 ] 3532 final @?= Fork Leaf Leaf 3533 3534 , testGroup "renameFile" 3535 [ testCase "renameFile moves file atomically" $ 3536 withSystemTempDirectory "tricu-rename" $ \dir -> do 3537 let oldPath = dir ++ "/old.txt" 3538 newPath = dir ++ "/new.txt" 3539 writeFile oldPath "contents" 3540 final <- runIOSource $ 3541 unlines 3542 [ "main = io (onRenameFile \"" ++ oldPath ++ "\" \"" ++ newPath ++ "\"" 3543 , " (err rest : pure err)" 3544 , " (_ rest : pure \"ok\"))" 3545 ] 3546 final @?= ofString "ok" 3547 newExists <- doesFileExist newPath 3548 oldExists <- doesFileExist oldPath 3549 newExists @?= True 3550 oldExists @?= False 3551 3552 , testCase "renameFile missing source returns does not exist" $ do 3553 final <- runIOSource $ 3554 unlines 3555 [ "main = io (onRenameFile \"/nonexistent/old.txt\" \"/nonexistent/new.txt\"" 3556 , " (err rest : pure err)" 3557 , " (_ rest : pure \"ok\"))" 3558 ] 3559 final @?= ofString "does not exist" 3560 3561 , testCase "renameFile denied destination returns permission denied" $ 3562 withSystemTempDirectory "tricu-rename-denied" $ \dir -> do 3563 let allowedDir = dir ++ "/allowed" 3564 deniedDir = dir ++ "/denied" 3565 createDirectory allowedDir 3566 createDirectory deniedDir 3567 let oldPath = allowedDir ++ "/old.txt" 3568 newPath = deniedDir ++ "/new.txt" 3569 writeFile oldPath "contents" 3570 let perms = defaultPerms { allowWrite = [allowedDir] } 3571 final <- runIOSourceWithPerms perms $ 3572 unlines 3573 [ "main = io (renameFile \"" ++ oldPath ++ "\" \"" ++ newPath ++ "\")" 3574 ] 3575 final @?= ioErrResult "permission denied" 3576 3577 , testCase "renameFile replaces existing destination atomically" $ 3578 withSystemTempDirectory "tricu-rename-replace" $ \dir -> do 3579 let oldPath = dir ++ "/old.txt" 3580 newPath = dir ++ "/new.txt" 3581 writeFile oldPath "new" 3582 writeFile newPath "old" 3583 final <- runIOSource $ 3584 unlines 3585 [ "main = io (onRenameFile \"" ++ oldPath ++ "\" \"" ++ newPath ++ "\"" 3586 , " (err rest : pure err)" 3587 , " (_ rest : pure \"ok\"))" 3588 ] 3589 final @?= ofString "ok" 3590 readFile newPath >>= (@?= "new") 3591 oldExists <- doesFileExist oldPath 3592 oldExists @?= False 3593 ] 3594 3595 , testGroup "createDirectory" 3596 [ testCase "createDirectory creates new directory" $ 3597 withSystemTempDirectory "tricu-mkdir" $ \dir -> do 3598 let newDir = dir ++ "/subdir" 3599 final <- runIOSource $ 3600 unlines 3601 [ "main = io (onCreateDirectory \"" ++ newDir ++ "\"" 3602 , " (err rest : pure err)" 3603 , " (_ rest : pure \"ok\"))" 3604 ] 3605 final @?= ofString "ok" 3606 exists <- doesDirectoryExist newDir 3607 exists @?= True 3608 3609 , testCase "createDirectory is idempotent for existing directory" $ 3610 withSystemTempDirectory "tricu-mkdir-idempotent" $ \dir -> do 3611 let existingDir = dir ++ "/exists" 3612 createDirectory existingDir 3613 final <- runIOSource $ 3614 unlines 3615 [ "main = io (onCreateDirectory \"" ++ existingDir ++ "\"" 3616 , " (err rest : pure err)" 3617 , " (_ rest : pure \"ok\"))" 3618 ] 3619 final @?= ofString "ok" 3620 3621 , testCase "createDirectory on existing file returns already exists" $ 3622 withSystemTempDirectory "tricu-mkdir-file" $ \dir -> do 3623 let path = dir ++ "/file.txt" 3624 writeFile path "x" 3625 final <- runIOSource $ 3626 unlines 3627 [ "main = io (onCreateDirectory \"" ++ path ++ "\"" 3628 , " (err rest : pure err)" 3629 , " (_ rest : pure \"ok\"))" 3630 ] 3631 final @?= ofString "already exists" 3632 3633 , testCase "createDirectory missing parent returns does not exist" $ do 3634 final <- runIOSource $ 3635 unlines 3636 [ "main = io (onCreateDirectory \"/nonexistent/path/12345/sub\"" 3637 , " (err rest : pure err)" 3638 , " (_ rest : pure \"ok\"))" 3639 ] 3640 final @?= ofString "does not exist" 3641 3642 , testCase "createDirectory denied path returns permission denied" $ 3643 withSystemTempDirectory "tricu-mkdir-denied" $ \dir -> do 3644 let allowedDir = dir ++ "/allowed" 3645 deniedDir = dir ++ "/denied" 3646 createDirectory allowedDir 3647 createDirectory deniedDir 3648 let perms = defaultPerms { allowWrite = [allowedDir] } 3649 final <- runIOSourceWithPerms perms $ 3650 unlines 3651 [ "main = io (createDirectory \"" ++ deniedDir ++ "/new\")" 3652 ] 3653 final @?= ioErrResult "permission denied" 3654 , testCase "createDirectory with file parent returns not a directory or does not exist" $ 3655 withSystemTempDirectory "tricu-mkdir-file-parent" $ \dir -> do 3656 let parentFile = dir ++ "/file" 3657 child = parentFile ++ "/sub" 3658 writeFile parentFile "x" 3659 final <- runIOSource $ 3660 unlines 3661 [ "main = io (onCreateDirectory \"" ++ child ++ "\"" 3662 , " (err rest : pure err)" 3663 , " (_ rest : pure \"ok\"))" 3664 ] 3665 final @?= ofString "not a directory" 3666 ] 3667 3668 , testGroup "deleteFile" 3669 [ testCase "deleteFile removes file" $ 3670 withSystemTempDirectory "tricu-delete" $ \dir -> do 3671 let path = dir ++ "/del.txt" 3672 writeFile path "x" 3673 final <- runIOSource $ 3674 unlines 3675 [ "main = io (onDeleteFile \"" ++ path ++ "\"" 3676 , " (err rest : pure err)" 3677 , " (_ rest : pure \"ok\"))" 3678 ] 3679 final @?= ofString "ok" 3680 exists <- doesFileExist path 3681 exists @?= False 3682 3683 , testCase "deleteFile is idempotent for missing file" $ do 3684 final <- runIOSource $ 3685 unlines 3686 [ "main = io (onDeleteFile \"/nonexistent/path/12345.txt\"" 3687 , " (err rest : pure err)" 3688 , " (_ rest : pure \"ok\"))" 3689 ] 3690 final @?= ofString "ok" 3691 3692 , testCase "deleteFile on directory returns is a directory" $ 3693 withSystemTempDirectory "tricu-delete-dir" $ \dir -> do 3694 let subDir = dir ++ "/subdir" 3695 createDirectory subDir 3696 final <- runIOSource $ 3697 unlines 3698 [ "main = io (onDeleteFile \"" ++ subDir ++ "\"" 3699 , " (err rest : pure err)" 3700 , " (_ rest : pure \"ok\"))" 3701 ] 3702 final @?= ofString "is a directory" 3703 3704 , testCase "deleteFile denied path returns permission denied" $ 3705 withSystemTempDirectory "tricu-delete-denied" $ \dir -> do 3706 let allowedDir = dir ++ "/allowed" 3707 deniedDir = dir ++ "/denied" 3708 createDirectory allowedDir 3709 createDirectory deniedDir 3710 let path = deniedDir ++ "/file.txt" 3711 writeFile path "x" 3712 let perms = defaultPerms { allowWrite = [allowedDir] } 3713 final <- runIOSourceWithPerms perms $ 3714 unlines 3715 [ "main = io (deleteFile \"" ++ path ++ "\")" 3716 ] 3717 final @?= ioErrResult "permission denied" 3718 ] 3719 3720 , testGroup "fileExists" 3721 [ testCase "fileExists true for existing file" $ 3722 withSystemTempDirectory "tricu-exists" $ \dir -> do 3723 let path = dir ++ "/file.txt" 3724 writeFile path "x" 3725 final <- runIOSource $ 3726 unlines 3727 [ "main = io (onFileExists \"" ++ path ++ "\"" 3728 , " (err rest : pure err)" 3729 , " (exists rest : pure exists))" 3730 ] 3731 final @?= Stem Leaf 3732 3733 , testCase "fileExists false for missing path" $ do 3734 final <- runIOSource $ 3735 unlines 3736 [ "main = io (onFileExists \"/nonexistent/path/12345.txt\"" 3737 , " (err rest : pure err)" 3738 , " (exists rest : pure exists))" 3739 ] 3740 final @?= Leaf 3741 3742 , testCase "fileExists denied path returns permission denied" $ 3743 withSystemTempDirectory "tricu-exists-denied" $ \dir -> do 3744 let allowedDir = dir ++ "/allowed" 3745 deniedDir = dir ++ "/denied" 3746 createDirectory allowedDir 3747 createDirectory deniedDir 3748 let path = deniedDir ++ "/file.txt" 3749 writeFile path "x" 3750 let perms = defaultPerms { allowRead = [allowedDir] } 3751 final <- runIOSourceWithPerms perms $ 3752 unlines 3753 [ "main = io (fileExists \"" ++ path ++ "\")" 3754 ] 3755 final @?= ioErrResult "permission denied" 3756 ] 3757 3758 , testGroup "sha256Hex" 3759 [ testCase "sha256Hex returns lowercase hex digest" $ do 3760 final <- runIOSource $ 3761 unlines 3762 [ "main = io (onSha256Hex [(104) (105)]" 3763 , " (err rest : pure err)" 3764 , " (hex rest : pure hex))" 3765 ] 3766 final @?= ofString "8f434346648f6b96df89dda901c5176b10a6d83961dd3c1ac88b59b2dc327aa4" 3767 3768 , testCase "sha256Hex empty bytes returns empty digest" $ do 3769 final <- runIOSource $ 3770 unlines 3771 [ "main = io (onSha256Hex []" 3772 , " (err rest : pure err)" 3773 , " (hex rest : pure hex))" 3774 ] 3775 final @?= ofString "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" 3776 3777 , testCase "sha256Hex hashes raw bytes" $ do 3778 final <- runIOSource $ 3779 unlines 3780 [ "main = io (onSha256Hex [(0) (255) (1)]" 3781 , " (err rest : pure err)" 3782 , " (hex rest : pure hex))" 3783 ] 3784 final @?= ofString "47ffa3ea45a70b8a41c2c0825df323c00a8b7a01c1ea06083cc41dddcc001123" 3785 ] 3786 3787 , testGroup "currentTime" 3788 [ testCase "currentTime returns a positive integer" $ do 3789 final <- runIOSource $ 3790 unlines 3791 [ "main = io (onCurrentTime" 3792 , " (err rest : pure 0)" 3793 , " (v rest : pure v))" 3794 ] 3795 case toNumber final of 3796 Right n | n > 1600000000 -> return () -- after ~Sep 2020 3797 Right n -> assertFailure $ "Expected recent timestamp, got: " ++ show n 3798 Left err -> assertFailure $ "Expected number, got error: " ++ err 3799 ] 3800 ] 3801 ] 3802 3803 httpParsingTests :: TestTree 3804 httpParsingTests = testGroup "HTTP Parsing Tests" 3805 [ 3806 -- chomp / request-line reader 3807 testCase "chomp strips trailing CR" $ do 3808 let input = "chomp [(104) (105) (13)]" 3809 env = evalTricu allTestLibsEnv (parseTricu input) 3810 result env @?= bytesT [104, 105] 3811 3812 , testCase "chomp leaves line without CR" $ do 3813 let input = "chomp [(104) (105)]" 3814 env = evalTricu allTestLibsEnv (parseTricu input) 3815 result env @?= bytesT [104, 105] 3816 3817 , testCase "chomp empty list" $ do 3818 let input = "chomp []" 3819 env = evalTricu allTestLibsEnv (parseTricu input) 3820 result env @?= bytesT [] 3821 3822 , testCase "readLineBytes with CRLF" $ do 3823 let input = "readLineBytes [(104) (105) (13) (10) (120)]" 3824 env = evalTricu allTestLibsEnv (parseTricu input) 3825 result env @?= pairT (bytesT [104, 105]) (bytesT [120]) 3826 3827 , testCase "readLineBytes with bare LF" $ do 3828 let input = "readLineBytes [(104) (105) (10) (120)]" 3829 env = evalTricu allTestLibsEnv (parseTricu input) 3830 result env @?= pairT (bytesT [104, 105]) (bytesT [120]) 3831 3832 , testCase "readLineBytes empty line" $ do 3833 let input = "readLineBytes [(13) (10) (120)]" 3834 env = evalTricu allTestLibsEnv (parseTricu input) 3835 result env @?= pairT (bytesT []) (bytesT [120]) 3836 3837 , testCase "readLineBytes EOF mid-line returns line" $ do 3838 let input = "readLineBytes [(104) (105)]" 3839 env = evalTricu allTestLibsEnv (parseTricu input) 3840 result env @?= pairT (bytesT [104, 105]) (bytesT []) 3841 3842 -- parseRequestLine 3843 , testCase "parseRequestLine GET slash" $ do 3844 let input = "parseRequestLine (append \"GET / HTTP/1.1\\r\\n\" \"x\")" 3845 env = evalTricu allTestLibsEnv (parseTricu input) 3846 result env @?= parserOk 3847 (pairT (ofString "GET") (pairT (ofString "/") (ofString "HTTP/1.1"))) 3848 (ofString "x") 3849 3850 , testCase "parseRequestLine POST path" $ do 3851 let input = "parseRequestLine \"POST /foo/bar HTTP/1.1\\r\\n\"" 3852 env = evalTricu allTestLibsEnv (parseTricu input) 3853 result env @?= parserOk 3854 (pairT (ofString "POST") (pairT (ofString "/foo/bar") (ofString "HTTP/1.1"))) 3855 (ofString "") 3856 3857 , testCase "parseRequestLine too short" $ do 3858 let input = "parseRequestLine \"GET\\r\\n\"" 3859 env = evalTricu allTestLibsEnv (parseTricu input) 3860 result env @?= parserErr (ofNumber 400) (ofString "Bad Request\n") 3861 3862 , testCase "parseRequestLine no version" $ do 3863 let input = "parseRequestLine \"GET /foo\\r\\n\"" 3864 env = evalTricu allTestLibsEnv (parseTricu input) 3865 result env @?= parserErr (ofNumber 400) (ofString "Bad Request\n") 3866 3867 , testCase "parseRequestLine empty line" $ do 3868 let input = "parseRequestLine \"\\r\\n\"" 3869 env = evalTricu allTestLibsEnv (parseTricu input) 3870 result env @?= parserErr (ofNumber 400) (ofString "Bad Request\n") 3871 3872 , testCase "parseRequestLine rejects extra fields" $ do 3873 let input = "parseRequestLine \"GET / HTTP/1.1 wat\\r\\n\"" 3874 env = evalTricu allTestLibsEnv (parseTricu input) 3875 result env @?= parserErr (ofNumber 400) (ofString "Bad Request\n") 3876 3877 -- parseHeaders 3878 , testCase "parseHeaders two headers lowercases names" $ do 3879 let input = "parseHeaders (append \"Host: localhost\\r\\nContent-Length: 42\\r\\n\\r\\n\" \"x\")" 3880 env = evalTricu allTestLibsEnv (parseTricu input) 3881 result env @?= parserOk 3882 (ofList 3883 [ pairT (ofString "host") (ofString "localhost") 3884 , pairT (ofString "content-length") (ofString "42") 3885 ]) 3886 (ofString "x") 3887 3888 , testCase "parseHeaders preserves colon in value" $ do 3889 let input = "parseHeaders (append \"X-Custom: a: b\\r\\n\\r\\n\" \"x\")" 3890 env = evalTricu allTestLibsEnv (parseTricu input) 3891 result env @?= parserOk 3892 (ofList [pairT (ofString "x-custom") (ofString "a: b")]) 3893 (ofString "x") 3894 3895 , testCase "parseHeaders accepts empty value" $ do 3896 let input = "parseHeaders (append \"X-Empty:\\r\\n\\r\\n\" \"x\")" 3897 env = evalTricu allTestLibsEnv (parseTricu input) 3898 result env @?= parserOk 3899 (ofList [pairT (ofString "x-empty") (ofString "")]) 3900 (ofString "x") 3901 3902 , testCase "parseHeaders immediate blank" $ do 3903 let input = "parseHeaders \"\\r\\nx\"" 3904 env = evalTricu allTestLibsEnv (parseTricu input) 3905 result env @?= parserOk (ofList []) (ofString "x") 3906 3907 , testCase "parseHeaders rejects missing colon" $ do 3908 let input = "parseHeaders \"Host\\r\\n\\r\\n\"" 3909 env = evalTricu allTestLibsEnv (parseTricu input) 3910 result env @?= parserErr (ofNumber 400) (ofString "Bad Request\n") 3911 3912 , testCase "parseContentLengthValue accepts max body bytes" $ do 3913 let input = "matchResult \"err\" (maybeLen rest : \"ok\") (parseContentLengthValue \"1048576\")" 3914 env = evalTricu allTestLibsEnv (parseTricu input) 3915 result env @?= ofString "ok" 3916 3917 , testCase "parseContentLengthValue accepts shorter decimal below max" $ do 3918 let input = "matchResult \"err\" (maybeLen rest : \"ok\") (parseContentLengthValue \"999999\")" 3919 env = evalTricu allTestLibsEnv (parseTricu input) 3920 result env @?= ofString "ok" 3921 3922 , testCase "parseContentLengthValue strips leading zeros before limit check" $ do 3923 let input = "parseContentLengthValue \"0000000000001\"" 3924 env = evalTricu allTestLibsEnv (parseTricu input) 3925 result env @?= parserOk (justT (ofNumber 1)) Leaf 3926 3927 , testCase "parseContentLengthValue rejects body above max" $ do 3928 let input = "parseContentLengthValue \"1048577\"" 3929 env = evalTricu allTestLibsEnv (parseTricu input) 3930 result env @?= parserErr (ofNumber 413) (ofString "Request body too large\n") 3931 3932 , testCase "parseContentLengthValue rejects longer body above max" $ do 3933 let input = "parseContentLengthValue \"2000000\"" 3934 env = evalTricu allTestLibsEnv (parseTricu input) 3935 result env @?= parserErr (ofNumber 413) (ofString "Request body too large\n") 3936 3937 -- statusLine / headerLine 3938 , testCase "statusLine 200 OK" $ do 3939 let input = "statusLine 200 \"OK\"" 3940 env = evalTricu allTestLibsEnv (parseTricu input) 3941 result env @?= ofString "HTTP/1.1 200 OK\r\n" 3942 3943 , testCase "headerLine Content-Length" $ do 3944 let input = "headerLine \"Content-Length\" \"42\"" 3945 env = evalTricu allTestLibsEnv (parseTricu input) 3946 result env @?= ofString "Content-Length: 42\r\n" 3947 3948 -- statusPhrase 3949 , testCase "statusPhrase 200" $ do 3950 let input = "statusPhrase 200" 3951 env = evalTricu allTestLibsEnv (parseTricu input) 3952 result env @?= ofString "OK" 3953 3954 , testCase "statusPhrase 201" $ do 3955 let input = "statusPhrase 201" 3956 env = evalTricu allTestLibsEnv (parseTricu input) 3957 result env @?= ofString "Created" 3958 3959 , testCase "statusPhrase 204" $ do 3960 let input = "statusPhrase 204" 3961 env = evalTricu allTestLibsEnv (parseTricu input) 3962 result env @?= ofString "No Content" 3963 3964 , testCase "statusPhrase 400" $ do 3965 let input = "statusPhrase 400" 3966 env = evalTricu allTestLibsEnv (parseTricu input) 3967 result env @?= ofString "Bad Request" 3968 3969 , testCase "statusPhrase 404" $ do 3970 let input = "statusPhrase 404" 3971 env = evalTricu allTestLibsEnv (parseTricu input) 3972 result env @?= ofString "Not Found" 3973 3974 , testCase "statusPhrase 405" $ do 3975 let input = "statusPhrase 405" 3976 env = evalTricu allTestLibsEnv (parseTricu input) 3977 result env @?= ofString "Method Not Allowed" 3978 3979 , testCase "statusPhrase 431" $ do 3980 let input = "statusPhrase 431" 3981 env = evalTricu allTestLibsEnv (parseTricu input) 3982 result env @?= ofString "Request Header Fields Too Large" 3983 3984 , testCase "statusPhrase 501" $ do 3985 let input = "statusPhrase 501" 3986 env = evalTricu allTestLibsEnv (parseTricu input) 3987 result env @?= ofString "Not Implemented" 3988 3989 , testCase "statusPhrase 505" $ do 3990 let input = "statusPhrase 505" 3991 env = evalTricu allTestLibsEnv (parseTricu input) 3992 result env @?= ofString "HTTP Version Not Supported" 3993 3994 , testCase "statusPhrase 500" $ do 3995 let input = "statusPhrase 500" 3996 env = evalTricu allTestLibsEnv (parseTricu input) 3997 result env @?= ofString "Internal Server Error" 3998 3999 , testCase "statusPhrase unknown" $ do 4000 let input = "statusPhrase 999" 4001 env = evalTricu allTestLibsEnv (parseTricu input) 4002 result env @?= ofString "Internal Server Error" 4003 4004 -- buildResponse 4005 , testCase "buildResponse 200 no headers" $ do 4006 let input = "buildResponse 200 [] \"hi\"" 4007 env = evalTricu allTestLibsEnv (parseTricu input) 4008 result env @?= ofString "HTTP/1.1 200 OK\r\n\r\nhi" 4009 4010 , testCase "buildResponse 404 with header" $ do 4011 let input = "buildResponse 404 [(pair \"Content-Length\" \"9\")] \"Not found\"" 4012 env = evalTricu allTestLibsEnv (parseTricu input) 4013 result env @?= ofString "HTTP/1.1 404 Not Found\r\nContent-Length: 9\r\n\r\nNot found" 4014 4015 -- convenience responses 4016 , testCase "okResponse" $ do 4017 let input = "okResponse \"hi\"" 4018 env = evalTricu allTestLibsEnv (parseTricu input) 4019 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" 4020 4021 , testCase "notFoundResponse" $ do 4022 let input = "notFoundResponse" 4023 env = evalTricu allTestLibsEnv (parseTricu input) 4024 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" 4025 4026 , testCase "textResponse" $ do 4027 let input = "textResponse \"hi\"" 4028 env = evalTricu allTestLibsEnv (parseTricu input) 4029 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" 4030 4031 , testCase "jsonResponse" $ do 4032 let input = "jsonResponse \"{}\"" 4033 env = evalTricu allTestLibsEnv (parseTricu input) 4034 result env @?= ofString "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 2\r\nConnection: close\r\n\r\n{}" 4035 4036 , testCase "createdResponse" $ do 4037 let input = "createdResponse \"created\\n\"" 4038 env = evalTricu allTestLibsEnv (parseTricu input) 4039 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" 4040 4041 , testCase "emptyResponse 204" $ do 4042 let input = "emptyResponse 204" 4043 env = evalTricu allTestLibsEnv (parseTricu input) 4044 result env @?= ofString "HTTP/1.1 204 No Content\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" 4045 4046 , testCase "badRequestResponse" $ do 4047 let input = "badRequestResponse \"Bad Request\\n\"" 4048 env = evalTricu allTestLibsEnv (parseTricu input) 4049 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" 4050 4051 , testCase "errorResponse 405" $ do 4052 let input = "errorResponse 405 \"Method Not Allowed\\n\"" 4053 env = evalTricu allTestLibsEnv (parseTricu input) 4054 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" 4055 ] 4056 4057 containsAll :: String -> [String] -> Assertion 4058 containsAll text needles = 4059 forM_ needles $ \needle -> 4060 assertBool ("expected " ++ show needle ++ " in: " ++ text) (needle `isInfixOf` text) 4061 4062 withFreePort :: (Int -> IO a) -> IO a 4063 withFreePort action = 4064 bracket 4065 (NS.socket NS.AF_INET NS.Stream NS.defaultProtocol) 4066 NS.close 4067 (\s -> do 4068 NS.setSocketOption s NS.ReuseAddr 1 4069 NS.bind s (NS.SockAddrInet 0 (NS.tupleToHostAddress (127, 0, 0, 1))) 4070 port <- NS.socketPort s 4071 action (fromIntegral port)) 4072 4073 runIOSourceWith :: IOPermissions -> T -> T -> String -> IO (T, T) 4074 runIOSourceWith perms readerEnv initialState source = do 4075 let asts = parseTricu source 4076 evalEnv = evalTricu allTestLibsEnv asts 4077 fullTree = mainResult evalEnv 4078 result <- runIOWith perms readerEnv initialState fullTree 4079 case result of 4080 Left err -> assertFailure ("IO runtime error: " ++ err) 4081 Right pair -> pure pair 4082 4083 runIOSource :: String -> IO T 4084 runIOSource source = fmap fst $ runIOSourceWith unsafePerms Leaf Leaf source 4085 4086 runIOSourceWithPerms :: IOPermissions -> String -> IO T 4087 runIOSourceWithPerms perms source = fmap fst $ runIOSourceWith perms Leaf Leaf source 4088 4089 runIOSourceWithEnv :: IOPermissions -> T -> String -> IO T 4090 runIOSourceWithEnv perms readerEnv source = fmap fst $ runIOSourceWith perms readerEnv Leaf source 4091 4092 ioOkResult :: T -> T 4093 ioOkResult val = Fork (Stem Leaf) (Fork val Leaf) 4094 4095 ioErrResult :: String -> T 4096 ioErrResult msg = Fork Leaf (Fork (ofString msg) Leaf)