PortableCompilerTests.hs (6383B)
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) 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 "explicit missing compiler never falls back to packaged data" $ fresh $ \store source -> do 38 let options = defaults {compilerManifest = Just (Text.unpack missing)} 39 rejected "Module manifest not found" (checkPortableCompiler options store) 40 rejected "Module manifest not found" (buildPortableModule options (Just (unStorePath store)) source) 41 , testCase "startup verifies the worker export ABI" $ fresh $ \store _ -> do 42 root <- putTreeTerm store Leaf 43 pin <- putManifest store (ModuleManifest [] [ModuleExport "sourceBuildGraph" 44 (ObjectRef (unDomain treeTermDomain) root) "unsupported"]) 45 rejected "Unsupported module export ABI" (checkPortableCompiler 46 (defaults {compilerManifest = Just (Text.unpack pin)}) store) 47 , testCase "compiler Results and malformed actions are checked" $ fresh $ \store source -> do 48 forM_ [(pureAction Leaf, "Invalid portable compiler Result"), 49 (pureAction (Fork Leaf (Fork (ofString "test failure") Leaf)), "Portable compilation failed"), 50 (Leaf, "invalid action")] $ \(action, diagnostic) -> do 51 options <- worker store action 52 rejected diagnostic (buildPortableModule options (Just (unStorePath store)) source) 53 , testCase "returned manifest and receipt are authenticated" $ fresh $ \store source -> do 54 manifest <- putManifest store (ModuleManifest [] []) 55 receipt <- putObject store receiptDomain "receipt" 56 let check m r diagnostic = do 57 options <- worker store (pureAction (success m r)) 58 rejected diagnostic (buildPortableModule options (Just (unStorePath store)) source) 59 check missing receipt "Module manifest not found" 60 check manifest missing "Compiler receipt missing" 61 check manifest "../escape" "Invalid compiler receipt hash" 62 BS.writeFile (objectPath store receipt) "tampered receipt" 63 check manifest receipt "Compiler receipt digest mismatch" 64 BS.writeFile (objectPath store manifest) "tampered manifest" 65 check manifest receipt "Module manifest digest mismatch" 66 , testCase "compiler cannot overwrite source files" $ fresh $ \store source -> do 67 before <- BS.readFile source 68 options <- worker store (Fork (ofNumber 21) 69 (Fork (ofString source) (ofString "overwritten"))) 70 rejected "permission denied" (buildPortableModule options (Just (unStorePath store)) source) 71 BS.readFile source >>= (@?= before) 72 , testCase "compiler execution remains inside the deadline" $ fresh $ \store source -> do 73 -- Sleep 60 seconds: scheduler effects are interrupted, not just loading. 74 options <- worker store (Fork (ofNumber 63) (ofNumber 60000)) 75 -- Startup verifies an inert worker; it must not execute this sleep. 76 ready <- Timeout.timeout 5000000 (checkPortableCompiler options store) 77 ready @?= Just () 78 completed <- Timeout.timeout 5000000 $ rejected "deadline exceeded" 79 (buildPortableModule (options {compilerSeconds = 1}) (Just (unStorePath store)) source) 80 completed @?= Just () 81 ] 82 83 fresh :: (StorePath -> FilePath -> IO a) -> IO a 84 fresh action = withSystemTempDirectory "portable-supervisor" $ \dir -> do 85 let source = dir </> "module.tri" 86 -- A supervisor must not interpret this input; only the selected worker may. 87 BS.writeFile source "deliberately not tricu source" 88 action (StorePath (dir </> "store")) source 89 90 defaults :: CompilerOptions 91 defaults = CompilerOptions Nothing Nothing 10 False 92 93 missing :: Text.Text 94 missing = Text.replicate 64 "0" 95 96 receiptDomain :: Domain 97 receiptDomain = Domain "arboricx.build-receipt.v1" 98 99 pureAction :: T -> T 100 pureAction = Fork Leaf 101 102 success :: Text.Text -> Text.Text -> T 103 success manifest receipt = Fork (Stem Leaf) 104 (Fork (Fork (ofString (Text.unpack manifest)) (ofString (Text.unpack receipt))) Leaf) 105 106 worker :: StorePath -> T -> IO CompilerOptions 107 worker store action = do 108 -- Four ignored build arguments, followed by the bare IO action. 109 root <- putTreeTerm store (iterate (Fork Leaf) action !! 4) 110 pin <- putManifest store (ModuleManifest [] [ModuleExport "sourceBuildGraph" 111 (ObjectRef (unDomain treeTermDomain) root) "arboricx.abi.tree.v1"]) 112 pure defaults {compilerManifest = Just (Text.unpack pin)} 113 114 rejected :: String -> IO a -> Assertion 115 rejected needle action = do 116 result <- try (action >> pure ()) :: IO (Either IOException ()) 117 case result of 118 Left problem -> assertBool (show problem) (needle `isInfixOf` show problem) 119 Right () -> assertFailure ("accepted invalid input; expected " ++ needle)