tricu

A lazy functional language based on Tree Calculus
Log | Files | Refs | README | LICENSE

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)