PortableCompilerTests.hs (8431B)
1 -- Artifact-only supervisor controls. These workers are constructed ABI trees, 2 -- not compilations of either the test source or the compiler implementation. 3 module PortableCompilerTests (portableCompilerTests) where 4 5 import PortableCompiler 6 import ContentStore 7 import Module.Manifest 8 import Runtime 9 import Control.Exception (IOException, try) 10 import Control.Monad (forM_) 11 import Data.List (isInfixOf) 12 import qualified Data.ByteString as BS 13 import qualified Data.Text as Text 14 import System.Directory (canonicalizePath, doesFileExist) 15 import System.FilePath ((</>)) 16 import System.IO.Temp (withSystemTempDirectory) 17 import qualified System.Timeout as Timeout 18 import Test.Tasty 19 import Test.Tasty.HUnit 20 21 portableCompilerTests :: TestTree 22 portableCompilerTests = testGroup "portable compiler supervision" 23 [ testCase "valid publication and explicit selection work without source interpretation" $ fresh $ \store source -> do 24 manifest <- putManifest store (ModuleManifest [] []) 25 receipt <- putObject store receiptDomain "independent receipt bytes" 26 options <- worker store (pureAction (success manifest receipt)) 27 checkPortableCompiler options store 28 canonical <- canonicalizePath (unStorePath store) 29 buildPortableResult options (Just (unStorePath store)) source >>= 30 (@?= (StorePath canonical, manifest, receipt)) 31 portableExports options (Just (unStorePath store)) source >>= (@?= []) 32 , testCase "invalid options fail at API entry" $ fresh $ \store source -> do 33 forM_ [0, -1, 86401, maxBound] $ \seconds -> 34 rejected "--compile-timeout" (buildPortableModule (defaults {compilerSeconds = seconds}) Nothing source) 35 let invalid = defaults {compilerToolStore = Just (unStorePath store)} 36 rejected "--tool-store requires --toolchain" (checkPortableCompiler invalid store) 37 , testCase "compiler protocol selection rejects unsupported versions and packaged relabeling" $ fresh $ \store source -> do 38 forM_ [0,3,-1,maxBound] $ \version -> 39 rejected "--toolchain-io-version must be" (buildPortableResult 40 (defaults {compilerIOVersion = version}) (Just (unStorePath store)) source) 41 rejected "IO v1 compilers require a retained v1 runtime" (checkPortableCompiler 42 (defaults {compilerIOVersion = 1}) store) 43 , testCase "explicit v2 compiler uses OS authority without v1 grants or fallback" $ fresh $ \store source -> do 44 manifest <- putManifest store (ModuleManifest [] []) 45 receipt <- putObject store receiptDomain "v2 independent receipt" 46 let remove = Fork (ofNumber 10) (Fork (ofString "path") 47 (Fork (ofNumber 1) (Fork (ofString "remove-file") (ofString source)))) 48 action = Fork (ofNumber 1) (Fork remove (Fork Leaf (pureAction (success manifest receipt)))) 49 options <- worker store action 50 rejected "retained v1 runtime" (buildPortableResult 51 (options {compilerIOVersion = 1}) (Just (unStorePath store)) source) 52 doesFileExist source >>= (@?= True) 53 checkPortableCompiler (options {compilerIOVersion = 2}) store 54 doesFileExist source >>= (@?= True) -- startup verification is inert 55 (_,actual,actualReceipt) <- buildPortableResult (options {compilerIOVersion = 2}) 56 (Just (unStorePath store)) source 57 (actual,actualReceipt) @?= (manifest,receipt) 58 doesFileExist source >>= (@?= False) 59 , testCase "v2 framing never retries retired v1 operations" $ fresh $ \store source -> do 60 options <- worker store (Fork (ofNumber 20) (ofString source)) 61 rejected "Compiler IO error" (buildPortableResult (options {compilerIOVersion = 2}) 62 (Just (unStorePath store)) source) 63 doesFileExist source >>= (@?= True) 64 , testCase "explicit missing compiler never falls back to packaged data" $ fresh $ \store source -> do 65 let options = defaults {compilerManifest = Just (Text.unpack missing)} 66 rejected "Module manifest not found" (checkPortableCompiler options store) 67 rejected "Module manifest not found" (buildPortableModule options (Just (unStorePath store)) source) 68 , testCase "startup verifies the worker export ABI" $ fresh $ \store _ -> do 69 root <- putTreeTerm store Leaf 70 pin <- putManifest store (ModuleManifest [] [ModuleExport "sourceBuildGraph" 71 (ObjectRef (unDomain treeTermDomain) root) "unsupported"]) 72 rejected "Unsupported module export ABI" (checkPortableCompiler 73 (defaults {compilerManifest = Just (Text.unpack pin)}) store) 74 , testCase "compiler Results and malformed actions are checked" $ fresh $ \store source -> do 75 forM_ [(pureAction Leaf, "Invalid portable compiler Result"), 76 (pureAction (Fork Leaf (Fork (ofString "test failure") Leaf)), "Portable compilation failed"), 77 (Leaf, "IO task failed")] $ \(action, diagnostic) -> do 78 options <- worker store action 79 rejected diagnostic (buildPortableModule options (Just (unStorePath store)) source) 80 , testCase "returned manifest and receipt are authenticated" $ fresh $ \store source -> do 81 manifest <- putManifest store (ModuleManifest [] []) 82 receipt <- putObject store receiptDomain "receipt" 83 let check m r diagnostic = do 84 options <- worker store (pureAction (success m r)) 85 rejected diagnostic (buildPortableModule 86 options (Just (unStorePath store)) source) 87 check missing receipt "Module manifest not found" 88 check manifest missing "Compiler receipt missing" 89 check manifest "../escape" "Invalid compiler receipt hash" 90 BS.writeFile (objectPath store receipt) "tampered receipt" 91 check manifest receipt "Compiler receipt digest mismatch" 92 BS.writeFile (objectPath store manifest) "tampered manifest" 93 check manifest receipt "Module manifest digest mismatch" 94 , testCase "retained v1 compiler cannot overwrite source files" $ fresh $ \store source -> do 95 before <- BS.readFile source 96 options <- worker store (Fork (ofNumber 21) 97 (Fork (ofString source) (ofString "overwritten"))) 98 rejected "retained v1 runtime" (buildPortableModule (options {compilerIOVersion = 1}) (Just (unStorePath store)) source) 99 BS.readFile source >>= (@?= before) 100 , testCase "compiler execution remains inside the deadline" $ fresh $ \store source -> do 101 -- Sleep 60 seconds: scheduler effects are interrupted, not just loading. 102 options <- worker store (Fork (ofNumber 10) (Fork (ofString "clock") 103 (Fork (ofNumber 1) (Fork (ofString "sleep") (ofNumber 60000))))) 104 -- Startup verifies an inert worker; it must not execute this sleep. 105 ready <- Timeout.timeout 5000000 (checkPortableCompiler options store) 106 ready @?= Just () 107 completed <- Timeout.timeout 5000000 $ rejected "deadline exceeded" 108 (buildPortableModule (options {compilerSeconds = 1}) (Just (unStorePath store)) source) 109 completed @?= Just () 110 ] 111 112 fresh :: (StorePath -> FilePath -> IO a) -> IO a 113 fresh action = withSystemTempDirectory "portable-supervisor" $ \dir -> do 114 let source = dir </> "module.tri" 115 -- A supervisor must not interpret this input; only the selected worker may. 116 BS.writeFile source "deliberately not tricu source" 117 action (StorePath (dir </> "store")) source 118 119 defaults :: CompilerOptions 120 defaults = CompilerOptions Nothing Nothing 10 False 2 121 122 missing :: Text.Text 123 missing = Text.replicate 64 "0" 124 125 receiptDomain :: Domain 126 receiptDomain = Domain "arboricx.build-receipt.v1" 127 128 pureAction :: T -> T 129 pureAction = Fork Leaf 130 131 success :: Text.Text -> Text.Text -> T 132 success manifest receipt = Fork (Stem Leaf) 133 (Fork (Fork (ofString (Text.unpack manifest)) (ofString (Text.unpack receipt))) Leaf) 134 135 worker :: StorePath -> T -> IO CompilerOptions 136 worker store action = do 137 -- Four ignored build arguments, followed by the bare IO action. 138 root <- putTreeTerm store (iterate (Fork Leaf) action !! 4) 139 pin <- putManifest store (ModuleManifest [] [ModuleExport "sourceBuildGraph" 140 (ObjectRef (unDomain treeTermDomain) root) "arboricx.abi.tree.v1"]) 141 pure defaults {compilerManifest = Just (Text.unpack pin), compilerIOVersion = 2} 142 143 rejected :: String -> IO a -> Assertion 144 rejected needle action = do 145 result <- try (action >> pure ()) :: IO (Either IOException ()) 146 case result of 147 Left problem -> assertBool (show problem) (needle `isInfixOf` show problem) 148 Right () -> assertFailure ("accepted invalid input; expected " ++ needle)