packages feed

super-user-spark 0.3.2.0 → 0.4.0.0

raw patch · 87 files changed

+5493/−3240 lines, 87 filesdep +genvaliditydep +genvalidity-hspecdep +genvalidity-hspec-aesondep −pureMD5dep −shellydep ~aesondep ~aeson-prettydep ~basenew-component:exe:super-user-spark

Dependencies added: genvalidity, genvalidity-hspec, genvalidity-hspec-aeson, genvalidity-path, hashable, iostring, path, path-io, validity, validity-path

Dependencies removed: pureMD5, shelly

Dependency ranges changed: aeson, aeson-pretty, base, directory, hspec, hspec-core, optparse-applicative, process, transformers

Files

app/Main.hs view
@@ -1,6 +1,6 @@ module Main (main) where -import           Spark+import           SuperUserSpark  main :: IO () main = spark
− src/Arguments.hs
@@ -1,144 +0,0 @@-{-# LANGUAGE CPP #-}-module Arguments where--#if (MIN_VERSION_base(4,9,0))-import           Data.Monoid-#endif--import           Options.Applicative-import           System.Environment  (getArgs)--import           Config-import           Dispatch.Types-import           Types-import           Utils---getInstructions :: IO Instructions-getInstructions = do-    mins <- fmap transformOptions getOptions-    case mins of-        Right ins -> return ins-        Left err  -> die $ "Failed to parse instructions\n" ++ err--transformOptions :: Options -> Either String Instructions-transformOptions (dispatch, go) = (,) <$> pure dispatch <*> configFromOptions go--configFromOptions :: GlobalOptions -> Either String SparkConfig-configFromOptions go = Right conf-  where-    conf = defaultConfig-          { conf_compile_output             = opt_output go-          , conf_compile_kind               = opt_kind go-          , conf_compile_override           = opt_overrride go-          , conf_deploy_replace_links       = opt_replace_links go       || opt_replace go-          , conf_deploy_replace_files       = opt_replace_files go       || opt_replace go-          , conf_deploy_replace_directories = opt_replace_directories go || opt_replace go-          , conf_debug                      = opt_debug go-          }--getOptions :: IO Options-getOptions = do-    args <- getArgs-    let result = runOptionsParser args-    handleParseResult result--runOptionsParser :: [String] -> ParserResult Options-runOptionsParser strs = execParserPure prefs optionsParser strs-  where-    prefs = ParserPrefs-      { prefMultiSuffix = "SPARK"  -- metavar suffix for multiple options-      , prefDisambiguate = True    -- automatically disambiguate abbreviations (default: False)-      , prefShowHelpOnError = True -- always show help text on parse errors (default: False)-      , prefBacktrack = True       -- backtrack to parent parser when a subcommand fails (default: True)-      , prefColumns = 80           -- number of columns in the terminal, used to format the help page (default: 80)-      }--optionsParser :: ParserInfo Options-optionsParser = info (helper <*> parseOptions) help-  where-    help = fullDesc <> progDesc description-    description = "Super User Spark, author: Tom Sydney Kerckhove"--parseOptions :: Parser Options-parseOptions = (,) <$> parseCommand <*> parseGlobalOptions--parseCommand :: Parser Dispatch-parseCommand = hsubparser $ mconcat-    [ command "parse"   parseParse-    , command "compile" parseCompile-    , command "check"   parseCheck-    , command "deploy"  parseDeploy-    ]--parseParse :: ParserInfo Dispatch-parseParse = info parser modifier-  where-    parser = DispatchParse <$> strArgument (metavar "FILE" <> help "the file to parse")-    modifier = fullDesc-            <> progDesc "Parse a spark file and check for syntactic errors."--parseCompile :: ParserInfo Dispatch-parseCompile = info parser modifier-  where-    parser = DispatchCompile-      <$> argument auto-        (metavar "CARD" <> help "the card file to compile")-    modifier = fullDesc-            <> progDesc "Compile a spark card."--parseCheck :: ParserInfo Dispatch-parseCheck = info parser modifier-  where-    parser = DispatchCheck-      <$> argument auto-         (metavar "CARD" <> help "the card to check")-    modifier = fullDesc-            <> progDesc "Check the deployment of a spark card."--parseDeploy :: ParserInfo Dispatch-parseDeploy = info parser modifier-  where-    parser = DispatchDeploy-      <$> argument auto-        (metavar "CARD" <> help "the card to deploy") -- TODO more help-    modifier = fullDesc-            <> progDesc "Deploy a spark card."---parseGlobalOptions :: Parser GlobalOptions-parseGlobalOptions = GlobalOptions-    <$> option (Just <$> str)-      ( long "output"-        <> short 'o'-        <> value Nothing-        <> metavar "FILE"-        <> help "The output file for compilation" )-    <*> option (Just <$> auto)-      ( long "kind"-        <> short 'k'-        <> value Nothing-        <> metavar "KIND"-        <> help "The kind specification for unspecified deployments (default: link)" )-    <*> option (Just <$> auto)-      ( long "override"-        <> short 'O'-        <> value Nothing-        <> metavar "KIND"-        <> help "Override every deployment to be of the given kind" )-    <*> switch-      ( long "replace-links"-        <> help "Replace links at deploy destinations." )-    <*> switch-      ( long "replace-files"-        <> help "Replace existing files at deploy destinations." )-    <*> switch-      ( long "replace-Directories"-        <> help "Replace existing directories at deploy destinations." )-    <*> switch-      ( long "replace-all"-        <> short 'r'-        <> help "Equivalent to --replace-files --replace-directories --replace-links" )-    <*> switch-      ( long "debug"-        <> help "Show al debug information." )
− src/Check.hs
@@ -1,17 +0,0 @@-module Check (-      check-    , formatDeploymentChecks-    ) where--import           Check.Internal-import           Check.Types-import           Compiler.Types-import           Deployer.Internal--check :: [Deployment] -> IO [DeploymentCheckResult]-check ds = do-    completed <- completeDeployments ds-    diagnosed <- mapM diagnoseDeployment completed-    return $ map checkDeployment diagnosed--
− src/Check/Internal.hs
@@ -1,244 +0,0 @@-module Check.Internal where--import           Check.Types-import           Compiler.Types-import           Constants-import           CoreTypes-import qualified Data.ByteString       as SB-import qualified Data.ByteString.Char8 as SBC-import qualified Data.ByteString.Lazy  as LB-import qualified Data.Digest.Pure.MD5  as H (md5)-import           Data.Maybe            (catMaybes)-import           System.Directory      (getDirectoryContents)-import           System.Exit           (ExitCode (..))-import           System.FilePath       ((</>))-import           System.Posix.Files    (fileExist, getSymbolicLinkStatus,-                                        isBlockDevice, isCharacterDevice,-                                        isDirectory, isNamedPipe, isRegularFile,-                                        isSocket, isSymbolicLink,-                                        readSymbolicLink)-import           System.Process        (readProcess, system)--checkDeployment :: DiagnosedDeployment -> DeploymentCheckResult-checkDeployment (Diagnosed [] (D dst _ _) _)-    = ImpossibleDeployment [unwords ["No source for deployment with destination", dst]]-checkDeployment (Diagnosed srcs dst kind)-    = bestResult $ map (\src -> checkSingle src dst kind) srcs--bestResult :: [CheckResult] -> DeploymentCheckResult-bestResult cs-    | all impossible cs = ImpossibleDeployment $ map (\(Impossible s) -> s) cs-    | otherwise-        -- Will not be empty as per line above-        = case head $ dropWhile impossible cs of-            AlreadyDone  -> DeploymentDone-            Ready i      -> ReadyToDeploy i-            Dirty s i c  -> DirtySituation s i c-            Impossible _ -> error "Cannot be the case"--impossible :: CheckResult -> Bool-impossible (Impossible _) = True-impossible _ = False--impossibleDeployment :: DeploymentCheckResult -> Bool-impossibleDeployment (ImpossibleDeployment _) = True-impossibleDeployment _ = False--dirtyDeployment :: DeploymentCheckResult -> Bool-dirtyDeployment (DirtySituation _ _ _) = True-dirtyDeployment _ = False--deploymentReadyToDeploy :: DeploymentCheckResult -> Bool-deploymentReadyToDeploy (ReadyToDeploy _) = True-deploymentReadyToDeploy _ = False--deploymentIsDone :: DeploymentCheckResult -> Bool-deploymentIsDone DeploymentDone = True-deploymentIsDone _ = False----- | Check a single (@source@, @destination@, @kind@) triple.-checkSingle :: DiagnosedFp -> DiagnosedFp -> DeploymentKind -> CheckResult-checkSingle (D src srcd srch) (D dst dstd dsth) kind =-    case (srcd, dstd, kind) of-        (IsFile     , Nonexistent   , _             ) -> ready--        (IsFile     , IsFile        , LinkDeployment)-            -> e ["Both the source:", src, "and the destination:", dst, "are files for a link deployment."]--        (IsFile     , IsFile        , CopyDeployment)-            -> if srch == dsth-                then AlreadyDone-                else e ["Both the source:", src, "and the destination:", dst, "are files for a copy deployment, but they are not equal."]--        (IsFile     , IsDirectory   , _)-            -> e ["The source: ", src, "is a file but the destination:", dst, "is a directory."]--        (IsFile     , IsLinkTo l    , LinkDeployment)-            -> if l == src-                then AlreadyDone-                else e ["The source:", src, "is a file and the destination:", dst, "is a link for a link deployment but the destination does not point to the source. Instead it points to:", l ++ "."]--        (IsFile     , IsLinkTo _    , CopyDeployment)-            -> e ["The source:", src, "is a file and the destination:", dst, "is a link for a copy deployment."]--        (IsDirectory, Nonexistent   , _             ) -> ready--        (IsDirectory, IsFile        , _             )-            -> e ["The source:", src, "is a directory and the destination:", dst, "is a file."]--        (IsDirectory, IsDirectory   , CopyDeployment)-            -> if srch == dsth-                then AlreadyDone-                else e ["The source:", src, "and destination:", dst, "are directories for a copy deployment, but they are not equal."]--        (IsDirectory, IsDirectory   , LinkDeployment)-            -> e ["The source:", src, "and the destination:", dst, "are directories for a link deployment."]--        (IsDirectory, IsLinkTo l    , LinkDeployment)-            -> if l == src-                then AlreadyDone-                else e ["The source:", src, "is a directory and the destination:", dst, "is a link for a link deployment but the destination does not point to the source. Instead it points to:", l ++ "."]--        (IsDirectory, IsLinkTo _    , CopyDeployment)-            -> e ["The source:", src, "is a directory and the destination:", dst, "is a link for a copy deployment."]--        (Nonexistent, _             , _             )-            -> i ["The source:", src, "does not exist."]--        (IsLinkTo _ , _             , _             )-            -> i ["The source:", src, "is a link."]--        (IsWeird    , IsWeird       , _             )-            -> i ["Both the source:", src, "and the destination:", dst, "are weird."]--        (IsWeird    , _             , _             )-            -> i ["The source:", src, "is weird."]--        (_          , IsWeird       , _             )-            -> i ["The destination:", dst, "is weird."]-  where-    ins = Instruction src dst kind-    ready = Ready ins-    i = Impossible . unlines-    e s = Dirty (unlines s) ins cins-    cins = case dstd of-            IsFile -> CleanFile dst-            IsLinkTo _ -> CleanLink dst-            IsDirectory -> CleanDirectory dst-            _ -> error "should not occur"--diagnoseDeployment :: Deployment -> IO DiagnosedDeployment-diagnoseDeployment (Put srcs dst kind) = do-    dsrcs <- mapM diagnose srcs-    ddst <- diagnose dst-    return $ Diagnosed dsrcs ddst kind--diagnose :: FilePath -> IO DiagnosedFp-diagnose fp = do-    d <- diagnoseFp fp-    hash <- hashFilePath fp-    return $ D fp d hash--diagnoseFp :: FilePath -> IO Diagnostics-diagnoseFp fp = do-    e <- fileExist fp-    if e-    then do-        s <- getSymbolicLinkStatus fp-        if isBlockDevice s || isCharacterDevice s || isSocket s || isNamedPipe s-        then return IsWeird-        else do-            if isSymbolicLink s-            then do-                point <- readSymbolicLink fp-                return $ IsLinkTo point-            else if isDirectory s-                then return IsDirectory-                else if isRegularFile s-                    then return IsFile-                    else error $ "File " ++ fp ++ " was neither a block device, a character device, a socket, a named pipe, a symbolic link, a directory or a regular file"-    else do-        -- If a link exists, but it points to something that doesn't exist, it is considered as non-existent by `fileExist`-        es <- system $ unwords ["test", "-L", fp]-        case es of-            ExitSuccess -> do-                -- Need to do a manual call because readSymbolicLink fails for nonexistent destinations-                point <- readProcess "readlink" [fp] ""-                return $ IsLinkTo $ init point -- remove newline-            ExitFailure _ -> return Nonexistent---md5 :: SB.ByteString -> HashDigest-md5 bs = H.md5 $ LB.fromStrict bs---- | Hash a filepath so that two filepaths with the same contents have the same hash-hashFilePath :: FilePath -> IO HashDigest-hashFilePath fp = do-    d <- diagnoseFp fp-    case d of-        IsFile -> hashFile fp-        IsDirectory -> hashDirectory fp-        IsLinkTo _ -> return $ md5 SB.empty-        IsWeird -> return $ md5 SB.empty-        Nonexistent -> return $ md5 SB.empty--hashFile :: FilePath -> IO HashDigest-hashFile fp = md5 <$> SB.readFile fp--hashDirectory :: FilePath -> IO HashDigest-hashDirectory fp = do-    rawContents <- getDirectoryContents fp-    let contents = map (fp </>) . filter (\f -> not $ f == "." || f == "..") $ rawContents-    hashes <- mapM hashFilePath contents-    let hashbs = map (SBC.pack . show) hashes-    return $ md5 $ SB.concat hashbs---formatDeploymentChecks :: [(Deployment, DeploymentCheckResult)] -> String-formatDeploymentChecks dss-    = if null output-        then "Deployment is done already."-        else unlines output ++-            if all (impossibleDeployment . snd) dss-             then "Deployment is impossible."-             else "Deployment is possible."-    where output = catMaybes $ map formatDeploymentCheck dss---formatDeploymentCheck :: (Deployment, DeploymentCheckResult) -> Maybe String-formatDeploymentCheck (_, (ReadyToDeploy is))         = Just $ "READY: " ++ formatInstruction is-formatDeploymentCheck (_, DeploymentDone)             = Nothing-formatDeploymentCheck (d, ImpossibleDeployment ds)-    = Just $ "IMPOSSIBLE: "-    ++ deployment_dst d-    ++ " cannot be deployed:\n"-    ++ unlines ds ++ "\n"-formatDeploymentCheck (d, (DirtySituation str is c))-    = Just $ "DIRTY: "-    ++ deployment_dst d-    ++ "\n"-    ++ str-    ++ "planned: "-    ++ formatInstruction is-    ++ "\n"-    ++ "cleanup needed:\n"-    ++ formatCleanupInstruction c-    ++ "\n"--formatInstruction :: Instruction -> String-formatInstruction (Instruction src dst k) = unwords $-    [ src-    , kindSymbol k-    , dst-    ]-  where-    kindSymbol LinkDeployment = linkKindSymbol-    kindSymbol CopyDeployment = copyKindSymbol--formatCleanupInstruction :: CleanupInstruction -> String-formatCleanupInstruction (CleanFile fp)         = "remove file " ++ fp-formatCleanupInstruction (CleanDirectory dir)   = "remove directory " ++ dir-formatCleanupInstruction (CleanLink link)       = "remove link " ++ link--
− src/Check/Types.hs
@@ -1,50 +0,0 @@-module Check.Types where--import           CoreTypes-import           Data.Digest.Pure.MD5 (MD5Digest)--type HashDigest = MD5Digest--data Diagnostics-    = Nonexistent-    | IsFile-    | IsDirectory-    | IsLinkTo FilePath-    | IsWeird-    deriving (Show, Eq)--data DiagnosedFp = D-    { diagnosedFilePath    :: FilePath-    , diagnosedDiagnostics :: Diagnostics-    , diagnosedHashDigest  :: HashDigest-    } deriving (Show, Eq)--data Instruction = Instruction FilePath FilePath DeploymentKind-    deriving (Show, Eq)--data CleanupInstruction-    = CleanFile FilePath-    | CleanDirectory FilePath-    | CleanLink FilePath-    deriving (Show, Eq)--data DeploymentCheckResult-    = DeploymentDone                                        -- ^ Done already-    | ReadyToDeploy Instruction                             -- ^ Immediately possible-    | DirtySituation String Instruction CleanupInstruction  -- ^ Possible after cleanup of destination-    | ImpossibleDeployment [String]                         -- ^ Entirely impossible-    deriving (Show, Eq)--data CheckResult-    = AlreadyDone                                   -- ^ Done already-    | Ready Instruction                             -- ^ Immediately possible-    | Dirty String Instruction CleanupInstruction   -- ^ Possible after cleanup-    | Impossible String                             -- ^ Entirely impossible-    deriving (Show, Eq)--data DiagnosedDeployment = Diagnosed-    { diagnosedSrcs :: [DiagnosedFp]-    , diagnosedDst  :: DiagnosedFp-    , diagnosedKind :: DeploymentKind-    } deriving (Show, Eq)-
− src/Compiler.hs
@@ -1,95 +0,0 @@-module Compiler where--import           Compiler.Internal-import           Compiler.Types-import           Control.Monad              (when)-import           Data.Aeson                 (eitherDecode)-import           Data.Aeson.Encode.Pretty   (encodePretty)-import qualified Data.ByteString.Lazy.Char8 as BS-import           Data.List                  (find, stripPrefix)-import           Language.Types-import           Parser-import           PreCompiler-import           System.FilePath            (takeDirectory, (</>))-import           Types--compileJob :: CardFileReference -> Sparker [Deployment]-compileJob cr@(CardFileReference root _) = go "" cr-  where-    go :: FilePath -> CardFileReference -> Sparker [Deployment]-    go base (CardFileReference fp mcn) = do-        sf <- parseFile fp-        let scope = sparkFileCards sf-        unit <- case mcn of-                Nothing -> case scope of-                    [] -> throwError $ CompileError $ "No cards found for compilation in file:" ++ fp-                    (first:_) -> return first-                Just (CardNameReference name) -> do-                    case find (\c -> cardName c == name) scope of-                            Nothing   -> throwError $ CompileError $ unwords ["Card", name, "not found for compilation."]-                            Just cu -> return cu--        -- Inject base outofDir-        let injected = injectBase unit--        -- Precompile checks-        let pces = preCompileChecks injected-        when (not . null $ pces) $ throwError $ PreCompileError pces--        -- Compile this unit-        (deps, crfs) <- embedPureCompiler $ compileUnit injected--        -- Compile the rest of the units-        restDeps <- fmap concat-                    $ mapM compileCardReference-                    $ map (resolveCardReferenceRelativeTo fp) crfs--        return $ deps ++ restDeps-      where--        injectBase :: Card -> Card-        injectBase c@(Card name s) | null base = c-                                   | otherwise = Card name $ Block [OutofDir base, s]--        stripRoot :: FilePath -> FilePath-        stripRoot orig = case stripPrefix (takeDirectory root) orig of-            Nothing -> orig-            Just ('/':new) -> new-            Just new -> new--        composeBases :: FilePath -> FilePath -> FilePath-        composeBases base [] = base-        composeBases _ base2 = takeDirectory (stripRoot base2)---        compileCardReference :: CardReference -> Sparker [Deployment]-        compileCardReference (CardFile cfr@(CardFileReference base2 _)) = go (composeBases base base2) cfr-        compileCardReference (CardName cnr) = go base (CardFileReference fp $ Just cnr)--resolveCardReferenceRelativeTo :: FilePath -> CardReference -> CardReference-resolveCardReferenceRelativeTo fp (CardFile (CardFileReference cfp mcn)) = CardFile $ CardFileReference (takeDirectory fp </> cfp) mcn-resolveCardReferenceRelativeTo _ cn = cn--embedPureCompiler :: PureCompiler a -> Sparker a-embedPureCompiler func = withExceptT CompileError $ mapExceptT (mapReaderT idToIO) func-  where-    idToIO :: Identity a -> IO a-    idToIO = return . runIdentity--outputCompiled :: [Deployment] -> Sparker ()-outputCompiled deps = do-    out <- asks conf_compile_output-    liftIO $ do-        let bs = encodePretty deps-        case out of-            Nothing -> BS.putStrLn bs-            Just fp -> BS.writeFile fp bs--inputCompiled :: FilePath -> Sparker [Deployment]-inputCompiled fp = do-    bs <- liftIO $ BS.readFile fp-    case eitherDecode bs of-        Left err        -> throwError $ CompileError $ "Something went wrong while deserialising json data: " ++ err-        Right ds        -> return ds--
− src/Compiler/Internal.hs
@@ -1,64 +0,0 @@-module Compiler.Internal where--import           Compiler.Types-import           Compiler.Utils-import           Language.Types-import           System.FilePath ((</>))-import           Types--compileUnit :: Card -> PureCompiler ([Deployment], [CardReference])-compileUnit card = do-    initSt <- initialState-    execWriterT $ evalStateT (compileDecs [cardContent card]) initSt--compileDecs :: [Declaration] -> InternalCompiler ()-compileDecs = mapM_ compileDec--compileDec :: Declaration -> InternalCompiler ()-compileDec (Deploy src dst kind) = do-    override <- gets state_deployment_kind_override-    superOverride <- asks conf_compile_override-    let resultKind-            = case (superOverride, override, kind) of-                (Nothing, Nothing, Nothing) -> LinkDeployment-                (Nothing, Nothing, Just k ) -> k-                (Nothing, Just o , _      ) -> o-                (Just o , _      , _      ) -> o-    outof <- gets state_outof_prefix-    into <- gets state_into--    let alternates = resolvePrefix $ outof ++ [sources src]-    let destination = into </> dst--    addDeployment $ Put alternates destination resultKind---compileDec (SparkOff cr) = addCardRef cr---compileDec (IntoDir dir) = do-    ip <- gets state_into-    if null ip-    then modify (\s -> s {state_into = dir} )-    else modify (\s -> s {state_into = ip </> dir} )---compileDec (OutofDir dir) = do-    op <- gets state_outof_prefix-    modify (\s -> s {state_outof_prefix = op ++ [Literal dir]})---compileDec (DeployKindOverride kind) = do-    modify (\s -> s { state_deployment_kind_override = Just kind })---compileDec (Block ds) = do-    before <- get-    compileDecs ds-    put before---compileDec (Alternatives ds) = do-    op <- gets state_outof_prefix-    modify (\s -> s { state_outof_prefix = op ++ [Alts ds] })-
− src/Compiler/Types.hs
@@ -1,63 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-module Compiler.Types where--import           Constants-import           Control.Monad  (mzero)-import           CoreTypes-import           Data.Aeson     (FromJSON (..), ToJSON (..), Value (..), object,-                                 (.:), (.=))-import           Language.Types-import           Monad-import           Types--data Deployment = Put-    { deployment_srcs :: [FilePath]-    , deployment_dst  :: FilePath-    , deployment_kind :: DeploymentKind-    } deriving Eq--instance Show Deployment where-    show dep = unwords $ srcs ++ [k,dst]-      where-        srcs = map quote $ deployment_srcs dep-        k = case deployment_kind dep of-                LinkDeployment -> linkKindSymbol-                CopyDeployment -> copyKindSymbol-        dst = quote $ deployment_dst dep-        quote = (\s -> "\"" ++ s ++ "\"")--instance FromJSON Deployment where-    parseJSON (Object o)-        = Put <$> o .: "sources"-              <*> o .: "destination"-              <*> o .: "deployment kind"-    parseJSON _ = mzero--instance ToJSON Deployment where-    toJSON depl-        = object-        [ "sources" .= deployment_srcs depl-        , "destination" .= deployment_dst depl-        , "deployment kind" .= deployment_kind depl-        ]--type CompilerPrefix = [PrefixPart]--data PrefixPart-    = Literal String-    | Alts [String]-    deriving (Show, Eq)--data CompilerState = CompilerState-    { state_deployment_kind_override :: Maybe DeploymentKind-    , state_into                     :: Directory-    , state_outof_prefix             :: CompilerPrefix-    } deriving (Show, Eq)--type PrecompileError = String--type ImpureCompiler = ExceptT CompileError (ReaderT SparkConfig IO)-type PureCompiler = ExceptT CompileError (ReaderT SparkConfig Identity)-type Precompiler = WriterT [PrecompileError] Identity-type InternalCompiler = StateT CompilerState (WriterT ([Deployment], [CardReference]) PureCompiler)-
− src/Compiler/Utils.hs
@@ -1,39 +0,0 @@-module Compiler.Utils where--import           Compiler.Types-import           Language.Types-import           System.FilePath ((</>))-import           Types---initialState :: PureCompiler CompilerState-initialState = do-    override <- asks conf_compile_kind-    return $ CompilerState-        { state_deployment_kind_override = override-        , state_into = ""-        , state_outof_prefix = []-        }--addDeployment :: Deployment -> InternalCompiler ()-addDeployment d = tell ([d], [])--addCardRef :: CardReference -> InternalCompiler ()-addCardRef c = tell ([], [c])--sources :: FilePath -> PrefixPart-sources fp@('.':f) = Alts [fp, f]-sources fp = Literal fp--resolvePrefix :: CompilerPrefix -> [FilePath]-resolvePrefix [] = []-resolvePrefix [Literal s] = [s]-resolvePrefix [Alts ds] = ds-resolvePrefix ((Literal s):ps) = do-    rest <- resolvePrefix ps-    return $ s </> rest-resolvePrefix ((Alts as):ps) = do-    a <- as-    rest <- resolvePrefix ps-    return $ a </> rest-
− src/Config.hs
@@ -1,14 +0,0 @@-module Config where--import           Config.Types--defaultConfig :: SparkConfig-defaultConfig = Config-    { conf_compile_output             = Nothing-    , conf_compile_kind               = Nothing-    , conf_compile_override           = Nothing-    , conf_deploy_replace_links       = False-    , conf_deploy_replace_files       = False-    , conf_deploy_replace_directories = False-    , conf_debug                      = False-    }
− src/Config/Types.hs
@@ -1,13 +0,0 @@-module Config.Types where--import           CoreTypes--data SparkConfig = Config-    { conf_compile_output             :: Maybe FilePath-    , conf_compile_kind               :: Maybe DeploymentKind-    , conf_compile_override           :: Maybe DeploymentKind-    , conf_deploy_replace_links       :: Bool-    , conf_deploy_replace_files       :: Bool-    , conf_deploy_replace_directories :: Bool-    , conf_debug                      :: Bool-    } deriving (Show, Eq)
− src/Constants.hs
@@ -1,67 +0,0 @@-module Constants where--keywordCard :: String-keywordCard = "card"--keywordSpark :: String-keywordSpark = "spark"--keywordFile :: String-keywordFile = "file"--keywordInto :: String-keywordInto = "into"--keywordOutof :: String-keywordOutof = "outof"--keywordKindOverride :: String-keywordKindOverride = "kind"--keywordLink :: String-keywordLink = "link"--keywordCopy :: String-keywordCopy = "copy"--keywordAlternatives :: String-keywordAlternatives = "alternatives"--linkKindSymbol :: String-linkKindSymbol = "l->"--copyKindSymbol :: String-copyKindSymbol = "c->"--unspecifiedKindSymbol :: String-unspecifiedKindSymbol = "->"--bracesChars :: [Char]-bracesChars = ['{','}']--linespaceChars :: [Char]-linespaceChars = [' ', '\t']--endOfLineChars :: [Char]-endOfLineChars = ['\n','\r']--whitespaceChars :: [Char]-whitespaceChars = linespaceChars ++ endOfLineChars--lineDelimiter :: String-lineDelimiter = ";"--branchDelimiter :: String-branchDelimiter = ":"--quotesChar :: Char-quotesChar = '"'--lineCommentStr :: String-lineCommentStr = "#"--blockCommentStrs :: (String, String)-blockCommentStrs = ("[[", "]]")--sparkExtension :: String-sparkExtension = "sus"
− src/CoreTypes.hs
@@ -1,27 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-module CoreTypes where--import           Control.Monad (mzero)-import           Data.Aeson    (FromJSON (..), ToJSON (..), Value (..))--type Directory = FilePath---- | The kind of a deployment-data DeploymentKind = LinkDeployment-                    | CopyDeployment-    deriving (Show, Eq)--instance Read DeploymentKind where-    readsPrec _ "link" = [(LinkDeployment,"")]-    readsPrec _ "copy" = [(CopyDeployment,"")]-    readsPrec _ _ = []--instance FromJSON DeploymentKind where-    parseJSON (String "link") = return LinkDeployment-    parseJSON (String "copy") = return CopyDeployment-    parseJSON _ = mzero--instance ToJSON DeploymentKind where-    toJSON LinkDeployment = String "link"-    toJSON CopyDeployment = String "copy"-
− src/Deployer.hs
@@ -1,48 +0,0 @@-module Deployer where--import           Check-import           Check.Internal-import           Check.Types-import           Compiler.Types-import           Control.Monad     (forM_, when)-import           Deployer.Internal-import           Monad-import           Types-import           Utils---deploy :: [(Deployment, DeploymentCheckResult)] -> Sparker ()-deploy dcrs = do-    let crs = map snd dcrs-    -- Check for impossible deployments-    when (any impossibleDeployment crs) $-        err dcrs "Deployment is impossible."--    -- Clean up the situation-    forM_ crs $ \d -> do-        case d of-            DirtySituation _ _ cis -> performClean cis-            _ -> return ()--    -- Check again-    let ds = map fst dcrs-    dcrs2 <- liftIO $ check ds--    -- Error if the cleaning is not done now.-    when (any (impossibleDeployment ||| dirtyDeployment) dcrs2) $-        err (zip ds dcrs2) "Situation was not entirely clean after attemted cleanup. Maybe you forgot to enable cleanups (--replace-all)?"--    -- Perform deployments-    liftIO $ mapM_ performDeployment $ map (\(ReadyToDeploy i) -> i) $ filter deploymentReadyToDeploy dcrs2--    -- Check one last time.-    dcrsf <- liftIO $ check ds-    when (any (not . deploymentIsDone) dcrsf) $ do-        err (zip ds dcrsf) "Something went wrong during deployment. It's not done yet."-  where-    err :: [(Deployment, DeploymentCheckResult)] -> String -> Sparker ()-    err dcrs text = do-        liftIO $ putStrLn $ formatDeploymentChecks dcrs-        throwError $ DeployError text--
− src/Deployer/Internal.hs
@@ -1,94 +0,0 @@-module Deployer.Internal where--import           Check.Types-import           Compiler.Types-import           Data.List             (isPrefixOf)-import           Data.Text             (pack)-import           Deployer.Types-import           Shelly                (cp_r, fromText, shelly)-import           System.Directory      (getHomeDirectory,-                                        removeDirectoryRecursive, removeFile)-import           System.Environment    (getEnvironment)-import           System.FilePath       (normalise, (</>))-import           System.FilePath.Posix (dropFileName)-import           System.Posix.Files    (createSymbolicLink, removeLink)-import           Types-import           Utils--completeDeployments :: [Deployment] -> IO [Deployment]-completeDeployments ds = do-    home <- getHomeDirectory-    env <- getEnvironment-    case mapM (completeDeployment home env) ds of-        Left err -> die err-        Right fp -> return fp--type Environment = [(String, String)]-type HomeDir = FilePath--completeDeployment :: HomeDir -> Environment -> Deployment -> Either String Deployment-completeDeployment home env (Put srcs dst kind) = do-    csrcs <- mapM (complete home env) srcs-    cdst <- complete home env dst-    return $ Put csrcs cdst kind--complete :: HomeDir -> Environment -> FilePath -> Either String FilePath-complete home env fp = do-    let ids = parseId fp-    strs <- mapM (replaceId env) ids-    let completed = map (replaceHome home) strs-    return $ normalise $ concat completed--parseId :: FilePath -> [ID]-parseId [] = []-parseId ('$':'(':rest) = (Var id):(parseId next)-  where (id, (')':next)) = break (\c -> c == ')') rest-parseId (s:ss)-    = case parseId ss of-        (Plain str):r -> (Plain (s:str)):r-        r             -> (Plain [s]):r--replaceId :: Environment -> ID -> Either String FilePath-replaceId _ (Plain str) = return str-replaceId e (Var str) = do-    case lookup str e of-        Nothing -> Left $ unwords ["variable", str, "could not be resolved from environment."]-        Just fp -> Right fp--replaceHome :: HomeDir -> FilePath -> FilePath-replaceHome home path-    = if "~" `isPrefixOf` path-        then home </> drop 2 path-        else path--performClean :: CleanupInstruction -> Sparker ()-performClean (CleanFile fp)         = incase conf_deploy_replace_files       $ rmFile fp-performClean (CleanDirectory fp)    = incase conf_deploy_replace_directories $ rmDir fp-performClean (CleanLink fp)         = incase conf_deploy_replace_links       $ unlink fp--unlink :: FilePath -> Sparker ()-unlink fp = liftIO $ removeLink fp--rmFile :: FilePath -> Sparker ()-rmFile fp = liftIO $ removeFile fp--rmDir :: FilePath -> Sparker ()-rmDir fp  = liftIO $ removeDirectoryRecursive fp---performDeployment :: Instruction -> IO ()-performDeployment (Instruction source destination LinkDeployment) = link source destination-performDeployment (Instruction source destination CopyDeployment) = copy source destination--copy :: FilePath -> FilePath -> IO ()-copy src dst = do-    createDirectoryIfMissing upperDir-    shelly $ cp_r (fromText $ pack src) (fromText $ pack dst)-  where upperDir = dropFileName dst--link :: FilePath -> FilePath -> IO ()-link src dst = do-    createDirectoryIfMissing upperDir-    createSymbolicLink src dst-  where upperDir = dropFileName dst-
− src/Deployer/Types.hs
@@ -1,38 +0,0 @@-module Deployer.Types where--import           Language.Types-import           Monad-import           System.FilePath.Posix (takeExtension)-import           Types--data DeployerCardReference-    = DeployerCardCompiled FilePath-    | DeployerCardUncompiled CardFileReference-    deriving (Show, Eq)--instance Read DeployerCardReference where-    readsPrec _ fp-        = case length (words fp) of-            0 -> []-            1 -> if takeExtension fp == ".sus"-                  then [(DeployerCardUncompiled (CardFileReference fp Nothing) ,"")]-                  else [(DeployerCardCompiled fp, "")]-            2 -> let [f, c] = words fp-                  in [(DeployerCardUncompiled (CardFileReference f (Just $ CardNameReference c)), "")]-            _ -> []--type SparkDeployer = StateT DeployerState Sparker-data DeployerState = DeployerState--data PreDeployment-    = Ready FilePath FilePath DeploymentKind-    | AlreadyDone-    | Error String-    deriving (Show, Eq)--data ID-    = Plain String-    | Var String-    deriving (Show, Eq)--
− src/Dispatch.hs
@@ -1,36 +0,0 @@-module Dispatch (dispatch) where--import           Check-import           Compiler-import           Compiler.Types-import           Control.Monad  (void)-import           Deployer-import           Deployer.Types-import           Dispatch.Types-import           Parser-import           Seed-import           Types--dispatch :: Dispatch -> Sparker ()-dispatch (DispatchParse fp) = do-    void $ parseFile fp  -- Just parse, throw away the results.--dispatch (DispatchCompile cfr) = do-    deployments <- compileJob cfr-    outputCompiled deployments--dispatch (DispatchCheck dcr) = do-    deps <- compileDeployerCardRef dcr-    seeded <- seedByCompiledCardRef dcr deps-    dcrs <- liftIO $ check seeded-    liftIO $ putStrLn $ formatDeploymentChecks $ zip seeded dcrs--dispatch (DispatchDeploy dcr) = do-    deps <- compileDeployerCardRef dcr-    seeded <- seedByCompiledCardRef dcr deps-    dcrs <- liftIO $ check seeded-    deploy $ zip seeded dcrs--compileDeployerCardRef :: DeployerCardReference -> Sparker [Deployment]-compileDeployerCardRef (DeployerCardCompiled fp) = inputCompiled fp-compileDeployerCardRef (DeployerCardUncompiled cfr) = compileJob cfr
− src/Dispatch/Types.hs
@@ -1,19 +0,0 @@-module Dispatch.Types where--import           Types--import           Deployer.Types-import           Language.Types---type Instructions = (Dispatch, SparkConfig)--data Dispatch-    = DispatchParse FilePath-    | DispatchCompile CardFileReference-    | DispatchCheck DeployerCardReference-    | DispatchDeploy DeployerCardReference-    deriving (Show, Eq)--type Options = (Dispatch, GlobalOptions)-
+ src/Import.hs view
@@ -0,0 +1,26 @@+module Import+    ( module X+    ) where++import Prelude as X hiding (readFile, appendFile, writeFile, putStr, putStrLn)++import Control.Arrow as X+import Data.Maybe as X+import GHC.Generics as X+import System.Exit as X+import Text.Read as X (readEither)++import Data.IOString as X+import Path as X+import Path.IO as X++import Control.Monad as X+import Control.Monad.Except as X+import Control.Monad.IO.Class as X (MonadIO(..))+import Control.Monad.Identity as X+import Control.Monad.Reader as X+import Control.Monad.State as X+import Control.Monad.Writer as X++import Data.Validity as X+import Data.Validity.Path as X ()
− src/Language/Types.hs
@@ -1,61 +0,0 @@-module Language.Types where--import           CoreTypes---- * Cards--type CardName = String-type Source = FilePath-type Destination = FilePath--data Card = Card-    { cardName    :: CardName-    , cardContent :: Declaration-    } deriving (Show, Eq)---- ** Declarations---- | A declaration in a card-data Declaration-    = SparkOff CardReference -- ^ Spark off another card-    | Deploy Source Destination (Maybe DeploymentKind) -- ^ Deploy from source to destination-    | IntoDir Directory -- ^ Deploy into a directory-    | OutofDir Directory -- ^ Deploy outof a directory-    | DeployKindOverride DeploymentKind -- ^ Override the deployment kind-    | Alternatives [Directory] -- ^ Provide a list of alternative sources-    | Block [Declaration] -- ^ A scoped block of declarations-    deriving (Show, Eq)---- * Card references---- | Reference a card by name (inside a file)-data CardNameReference = CardNameReference CardName-    deriving (Show, Eq)---- | Reference a card by the file it is in and therein potentially by a name reference-data CardFileReference = CardFileReference FilePath (Maybe CardNameReference)-    deriving (Show, Eq)---instance Read CardFileReference where-    readsPrec _ fp-        = case length (words fp) of-            1 -> [(CardFileReference fp Nothing ,"")]-            2 -> let [f, c] = words fp-                  in [(CardFileReference f (Just $ CardNameReference c), "")]-            _ -> []----- | Union card reference-data CardReference-    = CardFile CardFileReference-    | CardName CardNameReference-    deriving (Show, Eq)----- * Card files-data SparkFile = SparkFile-    { sparkFilePath  :: FilePath-    , sparkFileCards :: [Card]-    } deriving (Show, Eq)-
− src/Monad.hs
@@ -1,25 +0,0 @@-module Monad where--import           Control.Monad.Except (ExceptT, runExceptT)-import           Control.Monad.Reader (ReaderT, runReaderT)-import           Text.Parsec          (ParseError)--import           Config.Types---- | The base monad-type Sparker = ExceptT SparkError (ReaderT SparkConfig IO)--runSparker :: SparkConfig -> Sparker a -> IO (Either SparkError a)-runSparker conf func = runReaderT (runExceptT func) conf--data SparkError = ParseError ParseError-                | PreCompileError [PreCompileError]-                | CompileError CompileError-                | DeployError DeployError-                | UnpredictedError String-    deriving (Show, Eq)--type CompileError = String-type PreCompileError = String-type DeployError = String-
− src/Parser.hs
@@ -1,20 +0,0 @@-module Parser where--import           Control.Exception (try)-import           Language.Types-import           Parser.Internal-import           Types---parseFile :: FilePath -> Sparker SparkFile-parseFile file = do-    esf <- liftIO $ try $ parseFileIO file-    case esf of-        Left ioe -> throwError $ UnpredictedError $ show (ioe :: IOError)-        Right sf -> do-            case sf of-                Left pe -> throwError $ ParseError pe-                Right cs -> return cs--parseFileIO :: FilePath -> IO (Either ParseError SparkFile)-parseFileIO file = (liftIO $ readFile file) >>= return . parseCardFile file
− src/Parser/Internal.hs
@@ -1,282 +0,0 @@-module Parser.Internal where--import           Constants-import           Data.List          (isSuffixOf)-import           Deployer.Types-import           Language.Types-import           Text.Parsec-import           Text.Parsec.String-import           Types---parseCardFile :: FilePath -> String -> Either ParseError SparkFile-parseCardFile f s = do-    cs <- parseFromSource sparkFile f s-    return $ SparkFile f cs--parseFromSource :: Parser a -> FilePath -> String -> Either ParseError a-parseFromSource = parse-----[ Language ]----sparkFile :: Parser [Card]-sparkFile = do-    clean <- eatComments-    setInput clean-    resetPosition-    cards--cards :: Parser [Card]-cards = card `sepEndBy1` whitespace--resetPosition :: Parser ()-resetPosition = do-    pos <- getPosition-    setPosition $ setSourceColumn (setSourceLine pos 1) 1--card :: Parser Card-card = do-    whitespace-    skip $ string keywordCard-    whitespace-    name <- cardNameP-    whitespace-    b <- block-    whitespace-    return $ Card name b--declarations :: Parser [Declaration]-declarations = (inLineSpace declaration) `sepEndBy` delim--declaration :: Parser Declaration-declaration = choice $ map try-    [ block-    , alternatives-    , sparkOff-    , intoDir-    , outOfDir-    , deploymentKindOverride-    , deployment-    ]--block :: Parser Declaration-block = do-    ds <- inBraces $ inWhiteSpace declarations-    return $ Block ds-    <?> "block"--sparkOff :: Parser Declaration-sparkOff = do-    skip $ string keywordSpark-    linespace-    ref <- cardReference-    return $ SparkOff ref-    <?> "sparkoff"--compilerCardReference :: Parser CardFileReference-compilerCardReference = unprefixedCardFileReference--deployerCardReference :: Parser DeployerCardReference-deployerCardReference = goComp <|> goUncomp-  where-    goComp = compiledCardReference >>= return . DeployerCardCompiled-    goUncomp = unprefixedCardFileReference >>= return . DeployerCardUncompiled--compiledCardReference :: Parser FilePath-compiledCardReference = do-    skip $ string "compiled"-    skip linespace-    filepath--cardReference :: Parser CardReference-cardReference = try goName <|> try goFile <?> "card reference"-  where-    goName = cardNameReference >>= return . CardName-    goFile = cardFileReference >>= return . CardFile--cardNameReference :: Parser CardNameReference-cardNameReference = do-    skip $ string keywordCard-    linespace-    name <- cardNameP-    return $ CardNameReference name-    <?> "card name reference"--cardNameP :: Parser CardName-cardNameP = identifier <?> "card name"--cardFileReference :: Parser CardFileReference-cardFileReference = do-    skip $ string keywordFile-    skip linespace-    unprefixedCardFileReference--unprefixedCardFileReference :: Parser CardFileReference-unprefixedCardFileReference = do-    fp <- filepath-    linespace-    mn <- optionMaybe $ try cardNameP-    return $ case mn of-        Nothing -> CardFileReference fp Nothing-        Just cn  -> CardFileReference fp (Just $ CardNameReference cn)-    <?> "card file reference"--intoDir :: Parser Declaration-intoDir = do-    skip $ string keywordInto-    linespace-    dir <- directory-    return $ IntoDir dir-    <?> "into directory declaration"--outOfDir :: Parser Declaration-outOfDir = do-    skip $ string keywordOutof-    linespace-    dir <- directory-    return $ OutofDir dir-    <?> "outof directory declaration"--deploymentKindOverride :: Parser Declaration-deploymentKindOverride = do-    skip $ string keywordKindOverride-    linespace-    kind <- try copy <|> link-    return $ DeployKindOverride kind-    <?> "deployment kind override"-  where-    copy = string keywordCopy >> return CopyDeployment-    link = string keywordLink >> return LinkDeployment--shortDeployment :: Parser Declaration-shortDeployment = do-    source <- try directory <|> filepath-    return $ Deploy source source Nothing--longDeployment :: Parser Declaration-longDeployment = do-    source <- filepath-    linespace-    kind <- deploymentKind-    linespace-    dest <- filepath-    return $ Deploy source dest kind--deployment :: Parser Declaration-deployment = try longDeployment <|> shortDeployment-    <?> "deployment"--deploymentKind :: Parser (Maybe DeploymentKind)-deploymentKind = try link <|> try copy <|> def-    <?> "deployment kind"-    where-        link = string linkKindSymbol >> return (Just LinkDeployment)-        copy = string copyKindSymbol >> return (Just CopyDeployment)-        def  = string unspecifiedKindSymbol >> return Nothing--alternatives :: Parser Declaration-alternatives = do-    skip $ string keywordAlternatives-    linespace-    ds <- directory `sepBy1` linespace-    return $ Alternatives ds---- [ FilePaths ]----filepath :: Parser FilePath-filepath = do-    i <- identifier <?> "Filepath"-    if "/" `isSuffixOf` i-    then unexpected "slash at the end"-    else return i--directory :: Parser Directory-directory = filepath <?> "Directory"-----[ Comments ]----comment :: Parser String-comment = try lineComment <|> try blockComment <?> "Comment"--lineComment :: Parser String-lineComment = (<?> "Line comment") $ do-    skip $ try $ string lineCommentStr-    anyChar `manyTill` eol--blockComment :: Parser String-blockComment = (<?> "Block comment") $ do-    skip $ try $ string start-    anyChar `manyTill` (try $ string stop)-  where (start, stop) = blockCommentStrs----notComment :: Parser String-notComment = manyTill anyChar (lookAhead ((skip comment) <|> eof))--eatComments :: Parser String-eatComments = do-    optional comment-    xs <- notComment `sepBy` comment-    optional comment-    let withoutComments = concat xs-    return withoutComments-----[ Identifiers ]---identifier :: Parser String-identifier = try quotedIdentifier <|> plainIdentifier--plainIdentifier :: Parser String-plainIdentifier = many1 $ noneOf $ quotesChar : lineDelimiter ++ whitespaceChars ++ bracesChars--quotedIdentifier :: Parser String-quotedIdentifier = inQuotes $ many $ noneOf $ quotesChar:endOfLineChars-----[ Delimiters ]----inBraces :: Parser a -> Parser a-inBraces = between (char '{') (char '}')--inQuotes :: Parser a -> Parser a-inQuotes = between (char quotesChar) (char quotesChar)--delim :: Parser ()-delim = try (skip $ string lineDelimiter) <|> go-  where-    go = do-        eol-        whitespace-----[ Whitespace ]----inLineSpace :: Parser a -> Parser a-inLineSpace = between linespace linespace--inWhiteSpace :: Parser a -> Parser a-inWhiteSpace = between whitespace whitespace--linespace :: Parser ()-linespace = skip $ many $ oneOf linespaceChars--whitespace :: Parser ()-whitespace = skip $ many $ oneOf whitespaceChars--eol :: Parser ()-eol =  skip newline-  where-    newline-        =   try (string "\r\n")-        <|> try (string "\n")-        <|> string "\r"-        <?> "end of line"-----[ Utils ]----skip :: Parser a -> Parser ()-skip p = p >> return ()
− src/PreCompiler.hs
@@ -1,57 +0,0 @@-module PreCompiler where--import           Compiler.Types-import           Language.Types-import           Types-import           Utils--preCompileChecks :: Card -> [PreCompileError]-preCompileChecks c = runIdentity $ execWriterT $ cleanCard c--dirty :: String -> Precompiler ()-dirty s = tell ["Precompilation check failed: " ++ s]--cleanCard :: Card -> Precompiler ()-cleanCard (Card name d) = do-    cleanCardName name-    cleanDeclaration d--cleanDeclaration :: Declaration -> Precompiler ()-cleanDeclaration (Deploy src dst _) = do-    cleanFilePath src-    cleanFilePath dst--cleanDeclaration (SparkOff cr) = cleanCardReference cr-cleanDeclaration (IntoDir dir) = cleanFilePath dir-cleanDeclaration (OutofDir dir) = cleanFilePath dir-cleanDeclaration (DeployKindOverride _) = return () -- Nothing can go wrong.-cleanDeclaration (Alternatives fs) = mapM_ cleanFilePath fs-cleanDeclaration (Block ds) = mapM_ cleanDeclaration ds--cleanCardReference :: CardReference -> Precompiler ()-cleanCardReference (CardFile cfr) = cleanCardFileReference cfr-cleanCardReference (CardName cnr) = cleanCardNameReference cnr--cleanCardFileReference :: CardFileReference -> Precompiler ()-cleanCardFileReference (CardFileReference fp mcnr) = do-    cleanFilePath fp-    case mcnr of-        Nothing -> return ()-        Just cnr -> cleanCardNameReference cnr--cleanCardNameReference :: CardNameReference -> Precompiler ()-cleanCardNameReference (CardNameReference cn) = cleanCardName cn--cleanCardName :: CardName -> Precompiler ()-cleanCardName n-    | containsNewline n = dirty $ "Card name contains newline character(s): " ++ n-    | otherwise = return ()--cleanFilePath :: FilePath -> Precompiler ()-cleanFilePath [] = dirty "Empty filepath"-cleanFilePath fp-    | containsNewline fp =-        dirty $ "Filepath contains newline character(s): " ++ fp-    | containsMultipleConsequtiveSlashes fp =-        dirty $ "Filepath contains multiple consequtive slashes: " ++ fp-    | otherwise = return ()
− src/Seed.hs
@@ -1,25 +0,0 @@-module Seed where--import           Compiler.Types-import           Deployer.Types-import           Language.Types-import           Monad-import           System.Directory (getCurrentDirectory)-import           System.FilePath  (takeDirectory, (</>))-import           Types--seedByCompiledCardRef :: DeployerCardReference -> [Deployment] -> Sparker [Deployment]-seedByCompiledCardRef (DeployerCardCompiled fp) = seedByRel fp-seedByCompiledCardRef (DeployerCardUncompiled (CardFileReference fp _)) = seedByRel fp--seedByRel :: FilePath -> [Deployment] -> Sparker [Deployment]-seedByRel file ds = do-    cur <- liftIO $ getCurrentDirectory-    let reldir = takeDirectory file-    return $ seed (cur </> reldir) ds--seed :: FilePath -> [Deployment] -> [Deployment]-seed fp = map (\d -> d { deployment_srcs = map seedsrc $ deployment_srcs d })-  where-    seedsrc :: FilePath -> FilePath-    seedsrc = (fp </>)
− src/Spark.hs
@@ -1,22 +0,0 @@-module Spark (spark) where--import           Arguments-import           Dispatch-import           Types-import           Utils--spark :: IO ()-spark = do-    (di, config) <- getInstructions--    er <- runSparker config $ dispatch di-    case er of-        Left err -> die $ showError err-        Right _ -> return ()--showError :: SparkError -> String-showError (ParseError err) = show err-showError (PreCompileError errs) = unlines errs-showError (CompileError err) = err-showError (UnpredictedError err) = "Panic: " ++ err-showError (DeployError err) = err
+ src/SuperUserSpark.hs view
@@ -0,0 +1,24 @@+module SuperUserSpark+    ( spark+    ) where++import Import++import SuperUserSpark.Bake+import SuperUserSpark.Check+import SuperUserSpark.Compiler+import SuperUserSpark.Deployer+import SuperUserSpark.Diagnose+import SuperUserSpark.OptParse+import SuperUserSpark.Parser++spark :: IO ()+spark = getDispatch >>= dispatch++dispatch :: Dispatch -> IO ()+dispatch (DispatchParse pas) = parseFromArgs pas+dispatch (DispatchCompile cas) = compileFromArgs cas+dispatch (DispatchBake bas) = bakeFromArgs bas+dispatch (DispatchDiagnose bas) = diagnoseFromArgs bas+dispatch (DispatchCheck cas) = checkFromArgs cas+dispatch (DispatchDeploy das) = deployFromArgs das
+ src/SuperUserSpark/Bake.hs view
@@ -0,0 +1,98 @@+{-# LANGUAGE RecordWildCards #-}++{-+    The responsibility of the baker is to turn raw deployments into baked+    deployments. This takes care of everything that couldn't happen during+    compilation yet. The differences between raw deployments and baked+    deployments are:+    - Baked deployments only deal with absolute filepaths so as to be+      working-directory-independent.+    - Baked deployments are aware of the kind of things the checker/deployer+      will be operating on (files versus directories).++    The baker is not responsible for checking any existences.+-}+module SuperUserSpark.Bake where++import Import++import qualified Data.Aeson.Encode.Pretty as JSON+import System.Environment (getEnvironment)+import System.FilePath (takeExtension)++import SuperUserSpark.Bake.Internal+import SuperUserSpark.Bake.Types+import SuperUserSpark.Compiler+import SuperUserSpark.Compiler.Types+import SuperUserSpark.Language.Types+import SuperUserSpark.OptParse.Types+import SuperUserSpark.Utils++bakeFromArgs :: BakeArgs -> IO ()+bakeFromArgs ba = do+    errOrAss <- bakeAssignment ba+    case errOrAss of+        Left be -> die $ unwords ["Failed to build bake assignment:", be]+        Right ass -> bake ass++bakeAssignment :: BakeArgs -> IO (Either String BakeAssignment)+bakeAssignment BakeArgs {..} = do+    errOrCardRef <- parseBakeCardReference bakeCardRef+    case errOrCardRef of+        Left err -> pure $ Left err+        Right cardRef ->+            BakeAssignment cardRef <$$> deriveBakeSettings cardRef bakeFlags++parseBakeCardReference :: String -> IO (Either String BakeCardReference)+parseBakeCardReference s =+    case words s of+        [fp] ->+            if takeExtension fp == ".sus"+                then BakeCardUncompiled <$$> parseStrongCardFileReference fp+                else BakeCardCompiled <$$> resolveFile'Either fp+        [f, c] ->+            BakeCardUncompiled <$$>+            ((\(StrongCardFileReference p _) ->+                  StrongCardFileReference p (Just $ CardNameReference c)) <$$>+             parseStrongCardFileReference f)+        _ -> pure $ Left $ unwords ["Could not parse card reference from:", s]++deriveBakeSettings :: BakeCardReference+                   -> BakeFlags+                   -> IO (Either String BakeSettings)+deriveBakeSettings bcr BakeFlags {..} =+    BakeSettings (rootOf bcr) <$$> (Right <$> getEnvironment) <**>+    deriveCompileSettings bakeCompileFlags++rootOf :: BakeCardReference -> Path Abs Dir+rootOf bcr =+    parent $+    case bcr of+        (BakeCardCompiled fp) -> fp+        (BakeCardUncompiled (StrongCardFileReference fp _)) -> fp++bake :: BakeAssignment -> IO ()+bake BakeAssignment {..} = do+    errOrDone <-+        runReaderT (runExceptT $ bakeByCardRef bakeCardReference) bakeSettings+    case errOrDone of+        Left err -> die $ formatBakeError err+        Right () -> pure ()++formatBakeError :: BakeError -> String+formatBakeError (BakeCompileError ce) = formatCompileError ce+formatBakeError (BakeError s) = unwords ["Bake failed:", s]++bakeByCardRef :: BakeCardReference -> SparkBaker ()+bakeByCardRef bakeCardReference = do+    deps <- compileBakeCardRef bakeCardReference+    bdeps <- bakeDeployments deps+    putStrLn $ JSON.encodePretty bdeps++compileBakeCardRef :: BakeCardReference -> SparkBaker [RawDeployment]+compileBakeCardRef (BakeCardCompiled fp) = bakerCompile $ inputCompiled fp+compileBakeCardRef (BakeCardUncompiled bcf) = bakerCompile $ compileJob bcf++bakerCompile :: ImpureCompiler a -> SparkBaker a+bakerCompile =+    withExceptT BakeCompileError . mapExceptT (withReaderT bakeCompileSettings)
+ src/SuperUserSpark/Bake/Internal.hs view
@@ -0,0 +1,90 @@+{-# LANGUAGE RecordWildCards #-}++module SuperUserSpark.Bake.Internal where++import Import++import Control.Exception (try)+import System.FilePath+       (isAbsolute, replaceDirectory, takeDirectory)++import SuperUserSpark.Bake.Types+import SuperUserSpark.Compiler.Types++bakeDeployments :: [RawDeployment] -> SparkBaker [BakedDeployment]+bakeDeployments = mapM bakeDeployment++bakeDeployment :: RawDeployment -> SparkBaker BakedDeployment+bakeDeployment Deployment {..} = do+    d <- bakeDirections deploymentDirections+    pure $+        Deployment {deploymentDirections = d, deploymentKind = deploymentKind}++bakeDirections :: DeploymentDirections FilePath+               -> SparkBaker (DeploymentDirections AbsP)+bakeDirections (Directions srcs dst) =+    Directions <$> mapM bakeFilePath srcs <*> bakeFilePath dst++-- | Bake asingle 'FilePath'+--+-- The result should:+--+-- * ... not contain any more variables.+-- * ... not contain any reference to the home directory: @~@.+-- * ... be absolute.+bakeFilePath :: FilePath -> SparkBaker AbsP+bakeFilePath fp = do+    env <- asks bakeEnvironment+    root <- asks bakeRoot+    case complete env fp of+        Left err -> throwError $ BakeError $ err+        Right cp -> do+            if isAbsolute cp+                then case parseAbsFile cp of+                         Left err -> throwError $ BakeError $ show err+                         Right af -> pure $ AbsP af+                else do+                    let dir = takeDirectory cp+                    errOrAp <-+                        liftIO $+                        try $ do+                            d <- resolveFile root dir+                            parseAbsFile $ replaceDirectory cp $ toFilePath d+                    case errOrAp of+                        Left err ->+                            throwError $+                            BakeError $ show (err :: PathParseException)+                        Right absp -> pure $ AbsP absp++type Environment = [(String, String)]++complete :: Environment -> FilePath -> Either String FilePath+complete env fp = do+    let ids = parseId fp+    strs <- mapM (replaceId env) ids+    return $ concat strs++parseId :: FilePath -> [ID]+parseId fp =+    case fp of+        ('~':rest) -> Var "HOME" : go rest+        _ -> go fp+  where+    go :: FilePath -> [ID]+    go [] = []+    go ('$':'(':rest) = (Var id_) : (go next)+      where+        (id_, (')':next)) = break (\c -> c == ')') rest+    go (s:ss) =+        case go ss of+            (Plain str):r -> (Plain (s : str)) : r+            r -> (Plain [s]) : r++replaceId :: Environment -> ID -> Either String FilePath+replaceId _ (Plain str) = return str+replaceId e (Var str) = do+    case lookup str e of+        Nothing ->+            Left $+            unwords ["variable", str, "could not be resolved from environment."]+        Just fp -> Right fp
+ src/SuperUserSpark/Bake/Types.hs view
@@ -0,0 +1,79 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE DeriveGeneric #-}++module SuperUserSpark.Bake.Types where++import Import++import Data.Aeson++import SuperUserSpark.Compiler.Types++data BakeAssignment = BakeAssignment+    { bakeCardReference :: BakeCardReference+    , bakeSettings :: BakeSettings+    } deriving (Show, Eq, Generic)++instance Validity BakeAssignment++data BakeCardReference+    = BakeCardCompiled (Path Abs File)+    | BakeCardUncompiled StrongCardFileReference+    deriving (Show, Eq, Generic)++instance Validity BakeCardReference++data BakeSettings = BakeSettings+    { bakeRoot :: Path Abs Dir+    , bakeEnvironment :: [(String, String)]+    , bakeCompileSettings :: CompileSettings+    } deriving (Show, Eq, Generic)++instance Validity BakeSettings++defaultBakeSettings :: BakeSettings+defaultBakeSettings =+    BakeSettings+    { bakeRoot = $(mkAbsDir "/")+    , bakeEnvironment = []+    , bakeCompileSettings = defaultCompileSettings+    }++type SparkBaker = ExceptT BakeError (ReaderT BakeSettings IO)++data BakeError+    = BakeCompileError CompileError+    | BakeError String+    deriving (Show, Eq, Generic)++instance Validity BakeError++type BakedDeployment = Deployment AbsP++-- | An absolute path.+--+-- This is kept as a 'Path Abs File' to avoid existential quantification, but+-- that is an implementation detail and should not be exposed as functionality.+newtype AbsP = AbsP+    { unAbsP :: Path Abs File+    } deriving (Show, Eq, Generic)++instance Validity AbsP++instance ToJSON AbsP where+    toJSON (AbsP p) = toJSON p++instance FromJSON AbsP where+    parseJSON v = AbsP <$> parseJSON v++toPath :: AbsP -> FilePath+toPath = toFilePath . unAbsP++data ID+    = Plain String+    | Var String+    deriving (Show, Eq, Generic)++instance Validity ID
+ src/SuperUserSpark/Check.hs view
@@ -0,0 +1,75 @@+{-# LANGUAGE RecordWildCards #-}++module SuperUserSpark.Check+    ( checkFromArgs+    , checkAssignment+    , deriveCheckSettings+    , check+    , formatCheckError+    , formatDeploymentChecks+    , checkDeployments+    ) where++import Import++import SuperUserSpark.Bake+import SuperUserSpark.Bake.Internal+import SuperUserSpark.Bake.Types+import SuperUserSpark.Check.Internal+import SuperUserSpark.Check.Types+import SuperUserSpark.Diagnose+import SuperUserSpark.Diagnose.Types+import SuperUserSpark.OptParse.Types+import SuperUserSpark.Utils++checkFromArgs :: CheckArgs -> IO ()+checkFromArgs cas = do+    errOrAss <- checkAssignment cas+    case errOrAss of+        Left err -> die $ unwords ["Failed to build Check assignment:", err]+        Right ass -> check ass++checkAssignment :: CheckArgs -> IO (Either String CheckAssignment)+checkAssignment CheckArgs {..} = do+    errOrCardRef <- parseBakeCardReference checkArgCardRef+    case errOrCardRef of+        Left err -> pure $ Left err+        Right cardRef ->+            CheckAssignment cardRef <$$> deriveCheckSettings cardRef checkFlags++deriveCheckSettings :: BakeCardReference+                    -> CheckFlags+                    -> IO (Either String CheckSettings)+deriveCheckSettings bcr CheckFlags {..} =+    CheckSettings <$$> deriveDiagnoseSettings bcr checkDiagnoseFlags++check :: CheckAssignment -> IO ()+check CheckAssignment {..} = do+    errOrDone <-+        runReaderT+            (runExceptT $ checkByCardRef checkCardReference)+            checkSettings+    case errOrDone of+        Left err -> die $ formatCheckError err+        Right () -> pure ()++formatCheckError :: CheckError -> String+formatCheckError (CheckDiagnoseError ce) = formatDiagnoseError ce+formatCheckError (CheckError s) = unwords ["Check failed:", s]++checkByCardRef :: BakeCardReference -> SparkChecker ()+checkByCardRef checkCardReference = do+    ddeps <-+        checkerDiagnose $+        diagnoserBake+            (compileBakeCardRef checkCardReference >>= bakeDeployments) >>=+        (liftIO . diagnoseDeployments)+    liftIO $ putStrLn $ formatDeploymentChecks $ zip ddeps $ checkDeployments ddeps++checkerDiagnose :: SparkDiagnoser a -> SparkChecker a+checkerDiagnose =+    withExceptT CheckDiagnoseError .+    mapExceptT (withReaderT checkDiagnoseSettings)++checkDeployments :: [DiagnosedDeployment] -> [DeploymentCheckResult]+checkDeployments = map checkDeployment
+ src/SuperUserSpark/Check/Internal.hs view
@@ -0,0 +1,277 @@+module SuperUserSpark.Check.Internal where++import Import++import Data.Maybe (catMaybes)++import SuperUserSpark.Bake.Types+import SuperUserSpark.Check.Types+import SuperUserSpark.Compiler.Types+import SuperUserSpark.Constants+import SuperUserSpark.CoreTypes+import SuperUserSpark.Diagnose.Types++checkDeployment :: DiagnosedDeployment -> DeploymentCheckResult+checkDeployment (Deployment (Directions [] (D dst _ _)) _) =+    ImpossibleDeployment+        [unwords ["No source for deployment with destination", toPath dst]]+checkDeployment (Deployment (Directions srcs dst) kind) =+    bestResult $ map (\src -> checkSingle src dst kind) srcs++bestResult :: [CheckResult] -> DeploymentCheckResult+bestResult cs+    | all impossible cs = ImpossibleDeployment $ map (\(Impossible s) -> s) cs+    | otherwise+        -- Will not be empty as per line above+     =+        case head $ dropWhile impossible cs of+            AlreadyDone -> DeploymentDone+            Ready i -> ReadyToDeploy i+            Dirty s i c -> DirtySituation s i c+            Impossible _ -> error "Cannot be the case"++impossible :: CheckResult -> Bool+impossible (Impossible _) = True+impossible _ = False++impossibleDeployment :: DeploymentCheckResult -> Bool+impossibleDeployment (ImpossibleDeployment _) = True+impossibleDeployment _ = False++dirtyDeployment :: DeploymentCheckResult -> Bool+dirtyDeployment (DirtySituation _ _ _) = True+dirtyDeployment _ = False++deploymentReadyToDeploy :: DeploymentCheckResult -> Bool+deploymentReadyToDeploy (ReadyToDeploy _) = True+deploymentReadyToDeploy _ = False++deploymentIsDone :: DeploymentCheckResult -> Bool+deploymentIsDone DeploymentDone = True+deploymentIsDone _ = False++-- | Check a single (@source@, @destination@, @kind@) triple.+checkSingle :: DiagnosedFp -> DiagnosedFp -> DeploymentKind -> CheckResult+checkSingle (D src srcd srch) (D dst dstd dsth) kind =+    let parseBoth cons p =+            case (p $ toPath src, p $ toPath dst) of+                (Left err1, Left err2) ->+                    Impossible $ unwords [show err1, show err2]+                (Left err, _) -> Impossible $ show err+                (_, Left err) -> Impossible $ show err+                (Right s, Right d) -> Ready $ cons s d+        readyCopyFile = parseBoth CopyFile parseAbsFile+        readyCopyDir = parseBoth CopyDir parseAbsDir+        readyLinkFile = parseBoth LinkFile parseAbsFile+        readyLinkDir = parseBoth LinkDir parseAbsDir+    in case (srcd, dstd, kind) of+           (IsFile, Nonexistent, CopyDeployment) -> readyCopyFile+           (IsFile, Nonexistent, LinkDeployment) -> readyLinkFile+           (IsFile, IsFile, LinkDeployment) ->+               e+                   readyLinkFile+                   [ "Both the source:"+                   , toPath src+                   , "and the destination:"+                   , toPath dst+                   , "are files for a link deployment."+                   ]+           (IsFile, IsFile, CopyDeployment) ->+               if srch == dsth+                   then AlreadyDone+                   else e+                            readyCopyFile+                            [ "Both the source:"+                            , toPath src+                            , "and the destination:"+                            , toPath dst+                            , "are files for a copy deployment, but they are not equal."+                            ]+           (IsFile, IsDirectory, LinkDeployment) ->+               e+                   readyLinkFile+                   [ "The source: "+                   , toPath src+                   , "is a file but the destination:"+                   , toPath dst+                   , "is a directory for a link deployment."+                   ]+           (IsFile, IsDirectory, CopyDeployment) ->+               e+                   readyCopyFile+                   [ "The source: "+                   , toPath src+                   , "is a file but the destination:"+                   , toPath dst+                   , "is a directory for a copy deployment."+                   ]+           (IsFile, IsLinkTo l, LinkDeployment) ->+               if l == src+                   then AlreadyDone+                   else e+                            readyLinkFile+                            [ "The source:"+                            , toPath src+                            , "is a file and the destination:"+                            , toPath dst+                            , "is a link for a link deployment but the destination does not point to the source. Instead it points to:"+                            , toPath l ++ "."+                            ]+           (IsFile, IsLinkTo _, CopyDeployment) ->+               e+                   readyCopyFile+                   [ "The source:"+                   , toPath src+                   , "is a file and the destination:"+                   , toPath dst+                   , "is a link for a copy deployment."+                   ]+           (IsDirectory, Nonexistent, LinkDeployment) -> readyLinkDir+           (IsDirectory, Nonexistent, CopyDeployment) -> readyCopyDir+           (IsDirectory, IsFile, LinkDeployment) ->+               e+                   readyLinkDir+                   [ "The source:"+                   , toPath src+                   , "is a directory and the destination:"+                   , toPath dst+                   , "is a file for a link deployment"+                   ]+           (IsDirectory, IsFile, CopyDeployment) ->+               e+                   readyCopyDir+                   [ "The source:"+                   , toPath src+                   , "is a directory and the destination:"+                   , toPath dst+                   , "is a file for a copy deployment"+                   ]+           (IsDirectory, IsDirectory, LinkDeployment) ->+               e+                   readyLinkDir+                   [ "The source:"+                   , toPath src+                   , "and the destination:"+                   , toPath dst+                   , "are directories for a link deployment."+                   ]+           (IsDirectory, IsDirectory, CopyDeployment) ->+               if srch == dsth+                   then AlreadyDone+                   else e+                            readyCopyDir+                            [ "The source:"+                            , toPath src+                            , "and destination:"+                            , toPath dst+                            , "are directories for a copy deployment, but they are not equal."+                            ]+           (IsDirectory, IsLinkTo l, LinkDeployment) ->+               if l == src+                   then AlreadyDone+                   else e+                            readyLinkDir+                            [ "The source:"+                            , toPath src+                            , "is a directory and the destination:"+                            , toPath dst+                            , "is a link for a link deployment but the destination does not point to the source. Instead it points to:"+                            , toPath l ++ "."+                            ]+           (IsDirectory, IsLinkTo _, CopyDeployment) ->+               e+                   readyCopyDir+                   [ "The source:"+                   , toPath src+                   , "is a directory and the destination:"+                   , toPath dst+                   , "is a link for a copy deployment."+                   ]+           (Nonexistent, _, _) ->+               i ["The source:", toPath src, "does not exist."]+           (IsLinkTo _, _, _) -> i ["The source:", toPath src, "is a link."]+           (IsWeird, IsWeird, _) ->+               i+                   [ "Both the source:"+                   , toPath src+                   , "and the destination:"+                   , toPath dst+                   , "are weird."+                   ]+           (IsWeird, _, _) -> i ["The source:", toPath src, "is weird."]+           (_, IsWeird, _) -> i ["The destination:", toPath dst, "is weird."]+  where+    i = Impossible . unlines+    e mins s =+        case mins of+            (Impossible _) -> mins+            (Ready ins) ->+                case dstd of+                    IsFile -> Dirty (unlines s) ins $ CleanFile $ unAbsP dst+                    IsLinkTo _ -> Dirty (unlines s) ins $ CleanLink $ unAbsP dst+                    IsDirectory ->+                        case parseAbsDir $ toPath dst of+                            Left err -> Impossible $ show err -- Should not happen, but just in case.+                            Right dir ->+                                Dirty (unlines s) ins $ CleanDirectory dir+                    _ -> Impossible "should not occur"+            _ -> Impossible "should not occur."++formatDeploymentChecks :: [(DiagnosedDeployment, DeploymentCheckResult)]+                       -> String+formatDeploymentChecks dss =+    if null output+        then "Deployment is done already."+        else unlines output +++             if all (impossibleDeployment . snd) dss+                 then "Deployment is impossible."+                 else "Deployment is possible."+  where+    output = catMaybes $ map formatDeploymentCheck dss++formatDeploymentCheck :: (DiagnosedDeployment, DeploymentCheckResult)+                      -> Maybe String+formatDeploymentCheck (_, (ReadyToDeploy is)) =+    Just $ "READY: " ++ formatInstruction is+formatDeploymentCheck (_, DeploymentDone) = Nothing+formatDeploymentCheck (d, ImpossibleDeployment ds) =+    Just $+    concat+        [ "IMPOSSIBLE: "+        , toPath $+          diagnosedFilePath $ directionDestination $ deploymentDirections d+        , " cannot be deployed:\n"+        , unlines ds+        , "\n"+        ]+formatDeploymentCheck (d, (DirtySituation str is c)) =+    Just $+    concat+        [ "DIRTY: "+        , toPath $+          diagnosedFilePath $ directionDestination $ deploymentDirections d+        , "\n"+        , str+        , "planned: "+        , formatInstruction is+        , "\n"+        , "cleanup needed:\n"+        , formatCleanupInstruction c+        , "\n"+        ]++formatInstruction :: Instruction -> String+formatInstruction (CopyFile from to) =+    unwords $ [toFilePath from, "c->", toFilePath to]+formatInstruction (CopyDir from to) =+    unwords $ [toFilePath from, "c->", toFilePath to]+formatInstruction (LinkFile from to) =+    unwords $ [toFilePath from, "l->", toFilePath to]+formatInstruction (LinkDir from to) =+    unwords $ [toFilePath from, "l->", toFilePath to]++formatCleanupInstruction :: CleanupInstruction -> String+formatCleanupInstruction (CleanFile fp) = "remove file " ++ toFilePath fp+formatCleanupInstruction (CleanDirectory dir) =+    "remove directory " ++ toFilePath dir+formatCleanupInstruction (CleanLink link) = "remove link " ++ toFilePath link
+ src/SuperUserSpark/Check/Types.hs view
@@ -0,0 +1,78 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE RecordWildCards #-}++module SuperUserSpark.Check.Types where++import Import++import SuperUserSpark.Bake.Types+import SuperUserSpark.Diagnose.Types++data CheckAssignment = CheckAssignment+    { checkCardReference :: BakeCardReference+    , checkSettings :: CheckSettings+    } deriving (Show, Eq, Generic)++instance Validity CheckAssignment++data CheckSettings = CheckSettings+    { checkDiagnoseSettings :: DiagnoseSettings+    } deriving (Show, Eq, Generic)++instance Validity CheckSettings++defaultCheckSettings :: CheckSettings+defaultCheckSettings =+    CheckSettings {checkDiagnoseSettings = defaultDiagnoseSettings}++type SparkChecker = ExceptT CheckError (ReaderT CheckSettings IO)++data CheckError+    = CheckDiagnoseError DiagnoseError+    | CheckError String+    deriving (Show, Eq, Generic)++instance Validity CheckError++data Instruction+    = CopyFile (Path Abs File)+               (Path Abs File)+    | CopyDir (Path Abs Dir)+              (Path Abs Dir)+    | LinkFile (Path Abs File)+               (Path Abs File)+    | LinkDir (Path Abs Dir)+              (Path Abs Dir)+    deriving (Show, Eq, Generic)++instance Validity Instruction++data CleanupInstruction+    = CleanFile (Path Abs File)+    | CleanDirectory (Path Abs Dir)+    | CleanLink (Path Abs File)+    deriving (Show, Eq, Generic)++instance Validity CleanupInstruction++data DeploymentCheckResult+    = DeploymentDone -- ^ Done already+    | ReadyToDeploy Instruction -- ^ Immediately possible+    | DirtySituation String+                     Instruction+                     CleanupInstruction -- ^ Possible after cleanup of destination+    | ImpossibleDeployment [String] -- ^ Entirely impossible+    deriving (Show, Eq, Generic)++instance Validity DeploymentCheckResult++data CheckResult+    = AlreadyDone -- ^ Done already+    | Ready Instruction -- ^ Immediately possible+    | Dirty String+            Instruction+            CleanupInstruction -- ^ Possible after cleanup+    | Impossible String -- ^ Entirely impossible+    deriving (Show, Eq, Generic)++instance Validity CheckResult
+ src/SuperUserSpark/Compiler.hs view
@@ -0,0 +1,201 @@+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}++{-+    The Compiler is responsible for transforming an AST into a list of+    deployments. A deployment knows about the possible sources, the+    destination, and how to deploy a source to a destination.++    Everything that the compiler does needs to be independent of the host+    system because compilation could have happened independently of deployment.++    As such, raw deployments still contain references to variables such as:+    - Environment variables+    - The home directory: @~@+-}+module SuperUserSpark.Compiler where++import Import hiding ((</>))++import Control.Exception (try)+import Data.Aeson (eitherDecode)+import Data.Aeson.Encode.Pretty (encodePretty)+import Data.List (find)++import SuperUserSpark.Compiler.Internal+import SuperUserSpark.Compiler.Types+import SuperUserSpark.CoreTypes+import SuperUserSpark.Language.Types+import SuperUserSpark.OptParse.Types+import SuperUserSpark.Parser+import SuperUserSpark.PreCompiler+import SuperUserSpark.Utils++compileFromArgs :: CompileArgs -> IO ()+compileFromArgs ca = do+    errOrrAss <- compileAssignment ca+    case errOrrAss of+        Left ce -> die $ unwords ["Failed to build compile assignment:", ce]+        Right ass -> compile ass++compileAssignment :: CompileArgs -> IO (Either String CompileAssignment)+compileAssignment CompileArgs {..} =+    CompileAssignment <$$> (parseStrongCardFileReference compileArgCardRef) <**>+    (case compileArgOutput of+         Nothing -> pure $ pure Nothing+         Just f -> do+             af <-+                 left (show :: PathParseException -> String) <$>+                 try (resolveFile' f)+             pure $ Just <$> af) <**>+    deriveCompileSettings compileFlags++resolveFile'Either :: FilePath -> IO (Either String (Path Abs File))+resolveFile'Either fp = do+    errOrSp <- try $ resolveFile' fp+    pure $ left (show :: PathParseException -> String) $ errOrSp++parseStrongCardFileReference :: FilePath+                             -> IO (Either String StrongCardFileReference)+parseStrongCardFileReference fp =+    (\sfp -> StrongCardFileReference sfp Nothing) <$$> resolveFile'Either fp++deriveCompileSettings :: CompileFlags -> IO (Either String CompileSettings)+deriveCompileSettings CompileFlags {..} =+    CompileSettings <$$>+    (pure $+     case compileDefaultKind of+         Nothing -> Right LinkDeployment+         Just s -> readEither s) <**>+    (pure $+     case compileKindOverride of+         Nothing -> Right Nothing+         Just s -> readEither s)++compile :: CompileAssignment -> IO ()+compile CompileAssignment {..} = do+    errOrDone <-+        runReaderT+            (runExceptT $+             compileJob compileCardReference >>= outputCompiled compileOutput)+            compileSettings+    case errOrDone of+        Left ce -> die $ formatCompileError ce+        Right () -> pure ()++formatCompileError :: CompileError -> String+formatCompileError (CompileParseError s) = unlines ["Parse failed:", show s]+formatCompileError (PreCompileErrors ss) =+    unlines $ "Precompilation checks failed:" : map show ss+formatCompileError (DuringCompilationError s) =+    unlines ["Compilation failed:", s]++decideCardToCompile :: StrongCardFileReference+                    -> [Card]+                    -> Either CompileError Card+decideCardToCompile (StrongCardFileReference fp mcn) scope =+    case mcn of+        Nothing ->+            case scope of+                [] ->+                    Left $+                    DuringCompilationError $+                    unwords+                        [ "No cards found for compilation in file:"+                        , toFilePath fp+                        ]+                            -- TODO more detailed error here+                (fst_:_) -> pure fst_+        Just (CardNameReference name) -> do+            case find (\c -> cardName c == name) scope of+                Nothing ->+                    Left $+                    DuringCompilationError $+                    unwords ["Card", name, "not found for compilation."] -- TODO more detailed error here+                Just cu -> return cu++throwEither :: Either CompileError a -> ImpureCompiler a+throwEither (Left e) = throwError e+throwEither (Right a) = pure a++injectBase :: Maybe (Path Rel Dir) -> Card -> Card+injectBase Nothing c = c+injectBase (Just base) (Card name s) =+    Card name $ Block [OutofDir $ toFilePath base, s]++compileJob :: StrongCardFileReference -> ImpureCompiler [RawDeployment]+compileJob cr@(StrongCardFileReference root _) =+    compileJobWithRoot root Nothing cr++compileJobWithRoot+    :: Path Abs File+    -> Maybe (Path Rel Dir)+    -> StrongCardFileReference+    -> ImpureCompiler [RawDeployment]+compileJobWithRoot root base cfr@(StrongCardFileReference fp _) = do+    sf <- compilerParse fp+    unit <- throwEither $ decideCardToCompile cfr $ sparkFileCards sf+    -- Inject base outofDir+    let injected = injectBase base unit+    -- Precompile checks+    let pces = preCompileChecks injected+    when (not . null $ pces) $ throwError $ PreCompileErrors pces+    -- Compile this unit+    (deps, crfs) <- embedPureCompiler $ compileUnit injected+    -- Compile the rest of the units+    rcrfs <- mapM (resolveCardReferenceRelativeTo fp) crfs+    restDeps <-+        fmap concat $+        forM rcrfs $ \rcrf ->+            case rcrf of+                (StrongCardFile cfr_@(StrongCardFileReference base2 _)) -> do+                    let (newRoot, newBase) =+                            case stripDir (parent root) (parent base2) of+                                Nothing -> (base2, Nothing)+                                Just d -> (root, Just d)+                    compileJobWithRoot newRoot newBase cfr_+                (StrongCardName cnr) ->+                    compileJobWithRoot+                        root+                        base+                        (StrongCardFileReference fp $ Just cnr)+    return $ deps ++ restDeps++resolveCardReferenceRelativeTo :: Path Abs File+                               -> CardReference+                               -> ImpureCompiler StrongCardReference+resolveCardReferenceRelativeTo fp (CardFile (CardFileReference cfp mcn)) = do+    nfp <- liftIO $ resolveFile (parent fp) cfp+    pure $ StrongCardFile $ StrongCardFileReference nfp mcn+resolveCardReferenceRelativeTo _ (CardName cnr) = pure $ StrongCardName cnr++compilerParse :: Path Abs File -> ImpureCompiler SparkFile+compilerParse fp = do+    esf <- liftIO $ parseFile fp+    case esf of+        Left pe -> throwError $ CompileParseError pe+        Right sf_ -> pure sf_++embedPureCompiler :: PureCompiler a -> ImpureCompiler a+embedPureCompiler = withExceptT id . mapExceptT (mapReaderT idToIO)+  where+    idToIO :: Identity a -> IO a+    idToIO = return . runIdentity++outputCompiled :: Maybe (Path Abs File) -> [RawDeployment] -> ImpureCompiler ()+outputCompiled out deps =+    liftIO $ do+        let bs = encodePretty deps+        case out of+            Nothing -> putStrLn bs+            Just fp -> writeFile fp bs++inputCompiled :: Path Abs File -> ImpureCompiler [RawDeployment]+inputCompiled fp = do+    bs <- readFile fp+    case eitherDecode bs of+        Left err ->+            throwError $+            DuringCompilationError $+            "Something went wrong while deserialising json data: " ++ err+        Right ds -> return ds
+ src/SuperUserSpark/Compiler/Internal.hs view
@@ -0,0 +1,52 @@+module SuperUserSpark.Compiler.Internal where++import Import hiding ((</>))++import System.FilePath ((</>))++import SuperUserSpark.Compiler.Types+import SuperUserSpark.Compiler.Utils+import SuperUserSpark.Language.Types++compileUnit :: Card -> PureCompiler ([RawDeployment], [CardReference])+compileUnit card =+    execWriterT $ evalStateT (compileDecs [cardContent card]) initialState++compileDecs :: [Declaration] -> InternalCompiler ()+compileDecs = mapM_ compileDec++compileDec :: Declaration -> InternalCompiler ()+compileDec (Deploy src dst kind) = do+    defaultKind <- asks compileDefaultKind+    localOverride <- gets stateDeploymentKindLocalOverride+    superOverride <- asks compileKindOverride+    let resultKind =+            case msum [superOverride, localOverride, kind] of+                Nothing -> defaultKind+                Just k -> k+    outof <- gets stateOutof_prefix+    into <- gets stateInto+    let directions =+            Directions+            { directionSources = resolvePrefix $ outof ++ [sources src]+            , directionDestination = into </> dst+            }+    addDeployment $ Deployment directions resultKind+compileDec (SparkOff cr) = addCardRef cr+compileDec (IntoDir dir) = do+    ip <- gets stateInto+    if null ip+        then modify (\s -> s {stateInto = dir})+        else modify (\s -> s {stateInto = ip </> dir})+compileDec (OutofDir dir) = do+    op <- gets stateOutof_prefix+    modify (\s -> s {stateOutof_prefix = op ++ [Literal dir]})+compileDec (DeployKindOverride kind) = do+    modify (\s -> s {stateDeploymentKindLocalOverride = Just kind})+compileDec (Block ds) = do+    before <- get+    compileDecs ds+    put before+compileDec (Alternatives ds) = do+    op <- gets stateOutof_prefix+    modify (\s -> s {stateOutof_prefix = op ++ [Alts ds]})
+ src/SuperUserSpark/Compiler/Types.hs view
@@ -0,0 +1,128 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DeriveGeneric #-}++module SuperUserSpark.Compiler.Types where++import Import++import Data.Aeson+       (FromJSON(..), ToJSON(..), object, (.:), (.=), withObject)++import SuperUserSpark.CoreTypes+import SuperUserSpark.Language.Types+import SuperUserSpark.Parser.Types+import SuperUserSpark.PreCompiler.Types++data CompileAssignment = CompileAssignment+    { compileCardReference :: StrongCardFileReference+    , compileOutput :: Maybe (Path Abs File)+    , compileSettings :: CompileSettings+    } deriving (Show, Eq, Generic)++instance Validity CompileAssignment++data StrongCardFileReference =+    StrongCardFileReference (Path Abs File)+                            (Maybe CardNameReference)+    deriving (Show, Eq, Generic)++instance Validity StrongCardFileReference++data StrongCardReference+    = StrongCardFile StrongCardFileReference+    | StrongCardName CardNameReference+    deriving (Show, Eq, Generic)++instance Validity StrongCardReference++data CompileSettings = CompileSettings+    { compileDefaultKind :: DeploymentKind+    , compileKindOverride :: Maybe DeploymentKind+    } deriving (Show, Eq, Generic)++instance Validity CompileSettings++defaultCompileSettings :: CompileSettings+defaultCompileSettings =+    CompileSettings+    {compileDefaultKind = LinkDeployment, compileKindOverride = Nothing}++type RawDeployment = Deployment FilePath++data Deployment a = Deployment+    { deploymentDirections :: DeploymentDirections a+    , deploymentKind :: DeploymentKind+    } deriving (Show, Eq, Generic)++instance Validity a =>+         Validity (Deployment a)++instance FromJSON a =>+         FromJSON (Deployment a) where+    parseJSON =+        withObject "Deployment" $ \o ->+            Deployment <$> o .: "directions" <*> o .: "deployment kind"++instance ToJSON a =>+         ToJSON (Deployment a) where+    toJSON depl =+        object+            [ "directions" .= deploymentDirections depl+            , "deployment kind" .= deploymentKind depl+            ]++instance Functor Deployment where+    fmap f (Deployment dis dk) = Deployment (fmap f dis) dk++data DeploymentDirections a = Directions+    { directionSources :: [a]+    , directionDestination :: a+    } deriving (Show, Eq, Generic)++instance Validity a =>+         Validity (DeploymentDirections a)++instance ToJSON a =>+         ToJSON (DeploymentDirections a) where+    toJSON (Directions srcs dst) =+        object ["sources" .= srcs, "destination" .= dst]++instance FromJSON a =>+         FromJSON (DeploymentDirections a) where+    parseJSON =+        withObject "Deployment Directions" $ \o ->+            Directions <$> o .: "sources" <*> o .: "destination"++instance Functor DeploymentDirections where+    fmap f (Directions srcs dst) = Directions (map f srcs) (f dst)++type CompilerPrefix = [PrefixPart]++data PrefixPart+    = Literal String+    | Alts [String]+    deriving (Show, Eq, Generic)++instance Validity PrefixPart++data CompilerState = CompilerState+    { stateDeploymentKindLocalOverride :: Maybe DeploymentKind+    , stateInto :: Directory+    , stateOutof_prefix :: CompilerPrefix+    } deriving (Show, Eq, Generic)++instance Validity CompilerState++type ImpureCompiler = ExceptT CompileError (ReaderT CompileSettings IO)++type PureCompiler = ExceptT CompileError (ReaderT CompileSettings Identity)++type InternalCompiler = StateT CompilerState (WriterT ([RawDeployment], [CardReference]) PureCompiler)++data CompileError+    = CompileParseError ParseError+    | PreCompileErrors [PreCompileError]+    | DuringCompilationError String+    deriving (Show, Eq, Generic)++instance Validity CompileError
+ src/SuperUserSpark/Compiler/Utils.hs view
@@ -0,0 +1,37 @@+module SuperUserSpark.Compiler.Utils where++import Import hiding ((</>))++import SuperUserSpark.Compiler.Types+import SuperUserSpark.Language.Types+import System.FilePath ((</>))++initialState :: CompilerState+initialState =+    CompilerState+    { stateDeploymentKindLocalOverride = Nothing+    , stateInto = ""+    , stateOutof_prefix = []+    }++addDeployment :: RawDeployment -> InternalCompiler ()+addDeployment d = tell ([d], [])++addCardRef :: CardReference -> InternalCompiler ()+addCardRef c = tell ([], [c])++sources :: FilePath -> PrefixPart+sources fp@('.':f) = Alts [fp, f]+sources fp = Literal fp++resolvePrefix :: CompilerPrefix -> [FilePath]+resolvePrefix [] = []+resolvePrefix [Literal s] = [s]+resolvePrefix [Alts ds] = ds+resolvePrefix ((Literal s):ps) = do+    rest <- resolvePrefix ps+    return $ s </> rest+resolvePrefix ((Alts as):ps) = do+    a <- as+    rest <- resolvePrefix ps+    return $ a </> rest
+ src/SuperUserSpark/Constants.hs view
@@ -0,0 +1,69 @@+module SuperUserSpark.Constants where++import Import++keywordCard :: String+keywordCard = "card"++keywordSpark :: String+keywordSpark = "spark"++keywordFile :: String+keywordFile = "file"++keywordInto :: String+keywordInto = "into"++keywordOutof :: String+keywordOutof = "outof"++keywordKindOverride :: String+keywordKindOverride = "kind"++keywordLink :: String+keywordLink = "link"++keywordCopy :: String+keywordCopy = "copy"++keywordAlternatives :: String+keywordAlternatives = "alternatives"++linkKindSymbol :: String+linkKindSymbol = "l->"++copyKindSymbol :: String+copyKindSymbol = "c->"++unspecifiedKindSymbol :: String+unspecifiedKindSymbol = "->"++bracesChars :: [Char]+bracesChars = ['{', '}']++linespaceChars :: [Char]+linespaceChars = [' ', '\t']++endOfLineChars :: [Char]+endOfLineChars = ['\n', '\r']++whitespaceChars :: [Char]+whitespaceChars = linespaceChars ++ endOfLineChars++lineDelimiter :: String+lineDelimiter = ";"++branchDelimiter :: String+branchDelimiter = ":"++quotesChar :: Char+quotesChar = '"'++lineCommentStr :: String+lineCommentStr = "#"++blockCommentStrs :: (String, String)+blockCommentStrs = ("[[", "]]")++sparkExtension :: String+sparkExtension = "sus"
+ src/SuperUserSpark/CoreTypes.hs view
@@ -0,0 +1,33 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DeriveGeneric #-}++module SuperUserSpark.CoreTypes where++import Import++import Control.Monad (mzero)+import Data.Aeson (FromJSON(..), ToJSON(..), Value(..))++type Directory = FilePath++-- | The kind of a deployment+data DeploymentKind+    = LinkDeployment+    | CopyDeployment+    deriving (Show, Eq, Generic)++instance Validity DeploymentKind++instance Read DeploymentKind where+    readsPrec _ "link" = [(LinkDeployment, "")]+    readsPrec _ "copy" = [(CopyDeployment, "")]+    readsPrec _ _ = []++instance FromJSON DeploymentKind where+    parseJSON (String "link") = return LinkDeployment+    parseJSON (String "copy") = return CopyDeployment+    parseJSON _ = mzero++instance ToJSON DeploymentKind where+    toJSON LinkDeployment = String "link"+    toJSON CopyDeployment = String "copy"
+ src/SuperUserSpark/Deployer.hs view
@@ -0,0 +1,127 @@+{-# LANGUAGE RecordWildCards #-}++module SuperUserSpark.Deployer where++import Import++import SuperUserSpark.Bake+import SuperUserSpark.Bake.Internal+import SuperUserSpark.Bake.Types+import SuperUserSpark.Check+import SuperUserSpark.Check.Internal+import SuperUserSpark.Check.Types+import SuperUserSpark.Deployer.Internal+import SuperUserSpark.Deployer.Types+import SuperUserSpark.Diagnose+import SuperUserSpark.Diagnose.Types+import SuperUserSpark.OptParse.Types+import SuperUserSpark.Utils++deployFromArgs :: DeployArgs -> IO ()+deployFromArgs das = do+    errOrAss <- deployAssignment das+    case errOrAss of+        Left err -> die $ unwords ["Failed to make Deployment assignment:", err]+        Right ass -> deploy ass++deployAssignment :: DeployArgs -> IO (Either String DeployAssignment)+deployAssignment DeployArgs {..} = do+    errOrCardRef <- parseBakeCardReference deployArgCardRef+    case errOrCardRef of+        Left err -> pure $ Left err+        Right cardRef ->+            DeployAssignment cardRef <$$>+            deriveDeploySettings cardRef deployFlags++deriveDeploySettings :: BakeCardReference+                     -> DeployFlags+                     -> IO (Either String DeploySettings)+deriveDeploySettings bcr DeployFlags {..} = do+    ecs <- deriveCheckSettings bcr deployCheckFlags+    pure $ do+        cs <- ecs+        pure+            DeploySettings+            { deploySetsReplaceLinks =+                  deployFlagReplaceLinks || deployFlagReplaceAll+            , deploySetsReplaceFiles =+                  deployFlagReplaceFiles || deployFlagReplaceAll+            , deploySetsReplaceDirectories =+                  deployFlagReplaceDirectories || deployFlagReplaceAll+            , deployCheckSettings = cs+            }++deploy :: DeployAssignment -> IO ()+deploy DeployAssignment {..} = do+    errOrDone <-+        runReaderT+            (runExceptT $ deployByCardRef deployCardReference)+            deploySettings+    case errOrDone of+        Left err -> die $ formatDeployError err+        Right () -> pure ()++formatDeployError :: DeployError -> String+formatDeployError (DeployCheckError e) = formatCheckError e+formatDeployError (DeployError s) = unwords ["Deployment failed:", s]++deployByCardRef :: BakeCardReference -> SparkDeployer ()+deployByCardRef dcr = do+    deps <- deployerBake $ compileBakeCardRef dcr >>= bakeDeployments+    deployAbss deps++deployerBake :: SparkBaker a -> SparkDeployer a+deployerBake =+    withExceptT (DeployCheckError . CheckDiagnoseError . DiagnoseBakeError) .+    mapExceptT+        (withReaderT $+         diagnoseBakeSettings . checkDiagnoseSettings . deployCheckSettings)++deployAbss :: [BakedDeployment] -> SparkDeployer ()+deployAbss ds = do+    stage1+    stage2+    stage3+  where+    stage1 = do+        ddeps <- liftIO $ diagnoseDeployments ds+        let dcrs = checkDeployments ddeps+        -- Check for impossible deployments+        when (any impossibleDeployment dcrs) $+            err (zip ddeps dcrs) "Deployment is impossible."+        -- Clean up the situation+        forM_ dcrs $ \d -> do+            case d of+                DirtySituation _ _ cis -> performClean cis+                _ -> return ()+    stage2+      -- Check again+     = do+        ddeps2 <- liftIO $ diagnoseDeployments ds+        let dcrs2 = checkDeployments ddeps2+        -- Error if the cleaning is not done now.+        when (any (\d -> impossibleDeployment d || dirtyDeployment d) dcrs2) $+            err (zip ddeps2 dcrs2) $+            unlines+                [ "Situation was not entirely clean after attemted cleanup."+                , "Maybe you forgot to enable cleanups (--replace-all)?"+                ]+        -- Perform deployments+        liftIO $+            mapM_ performDeployment $+            map (\(ReadyToDeploy i) -> i) $ filter deploymentReadyToDeploy dcrs2+    stage3+        -- Check one last time.+     = do+        do ddeps3 <- liftIO $ diagnoseDeployments ds+           let dcrsf3 = checkDeployments ddeps3+           when (any (not . deploymentIsDone) dcrsf3) $ do+               err+                   (zip ddeps3 dcrsf3)+                   "Something went wrong during deployment. It's not done yet."+    err :: [(DiagnosedDeployment, DeploymentCheckResult)]+        -> String+        -> SparkDeployer ()+    err dcrs_ text = do+        liftIO $ putStrLn $ formatDeploymentChecks dcrs_+        throwError $ DeployError text
+ src/SuperUserSpark/Deployer/Internal.hs view
@@ -0,0 +1,61 @@+module SuperUserSpark.Deployer.Internal where++import Import hiding ((</>))++import Data.Text (pack)+import System.FilePath.Posix+       (dropFileName, dropTrailingPathSeparator)+import System.Posix.Files (createSymbolicLink, removeLink)++import SuperUserSpark.Bake.Types+import SuperUserSpark.Check.Types+import SuperUserSpark.CoreTypes+import SuperUserSpark.Deployer.Types+import SuperUserSpark.Utils++performClean :: CleanupInstruction -> SparkDeployer ()+performClean (CleanFile fp) = incase deploySetsReplaceFiles $ rmFile fp+performClean (CleanDirectory fp) =+    incase deploySetsReplaceDirectories $ rmDir fp+performClean (CleanLink fp) = incase deploySetsReplaceLinks $ unlink fp++unlink :: Path Abs File -> SparkDeployer ()+unlink = liftIO . removeLink . dropTrailingPathSeparator . toFilePath++rmFile :: Path Abs File -> SparkDeployer ()+rmFile = liftIO . removeFile++rmDir :: Path Abs Dir -> SparkDeployer ()+rmDir = liftIO . removeDirRecur++performDeployment :: Instruction -> IO ()+performDeployment (CopyFile source destination) =+    performCopyFile source destination+performDeployment (CopyDir source destination) =+    performCopyDir source destination+performDeployment (LinkFile source destination) =+    performLinkFile source destination+performDeployment (LinkDir source destination) =+    performLinkDir source destination++performCopyFile :: Path Abs File -> Path Abs File -> IO ()+performCopyFile src dst = do+    ensureDir $ parent dst+    copyFile src dst++performCopyDir :: Path Abs Dir -> Path Abs Dir -> IO ()+performCopyDir src dst = do+    ensureDir $ parent dst+    copyDirRecur src dst++performLinkFile :: Path Abs File -> Path Abs File -> IO ()+performLinkFile src dst = do+    ensureDir $ parent dst+    createSymbolicLink (toFilePath src) (toFilePath dst)++performLinkDir :: Path Abs Dir -> Path Abs Dir -> IO ()+performLinkDir src dst = do+    ensureDir $ parent dst+    createSymbolicLink+        (dropTrailingPathSeparator $ toFilePath src)+        (dropTrailingPathSeparator $ toFilePath dst)
+ src/SuperUserSpark/Deployer/Types.hs view
@@ -0,0 +1,53 @@+{-# LANGUAGE DeriveGeneric #-}++module SuperUserSpark.Deployer.Types where++import Import++import SuperUserSpark.Bake.Types+import SuperUserSpark.Check.Types+import SuperUserSpark.CoreTypes++data DeployAssignment = DeployAssignment+    { deployCardReference :: BakeCardReference+    , deploySettings :: DeploySettings+    } deriving (Show, Eq, Generic)++instance Validity DeployAssignment++data DeploySettings = DeploySettings+    { deploySetsReplaceLinks :: Bool+    , deploySetsReplaceFiles :: Bool+    , deploySetsReplaceDirectories :: Bool+    , deployCheckSettings :: CheckSettings+    } deriving (Show, Eq, Generic)++instance Validity DeploySettings++defaultDeploySettings :: DeploySettings+defaultDeploySettings =+    DeploySettings+    { deploySetsReplaceLinks = False+    , deploySetsReplaceFiles = False+    , deploySetsReplaceDirectories = False+    , deployCheckSettings = defaultCheckSettings+    }++type SparkDeployer = ExceptT DeployError (ReaderT DeploySettings IO)++data DeployError+    = DeployCheckError CheckError+    | DeployError String+    deriving (Show, Eq, Generic)++instance Validity DeployError++data PreDeployment+    = Ready FilePath+            FilePath+            DeploymentKind+    | AlreadyDone+    | Error String+    deriving (Show, Eq, Generic)++instance Validity PreDeployment
+ src/SuperUserSpark/Diagnose.hs view
@@ -0,0 +1,80 @@+{-# LANGUAGE RecordWildCards #-}++{-+    The responsibility of the diagnoser is to turn baked deployments into+    diagnosed deployments. This is purely a read-only operation that looks+    at the current state of the file system.+-}+module SuperUserSpark.Diagnose+    ( diagnoseFromArgs+    , diagnoseAssignment+    , deriveDiagnoseSettings+    , diagnose+    , formatDiagnoseError+    , diagnoserBake+    , diagnoseDeployments+    ) where++import Import++import qualified Data.Aeson.Encode.Pretty as JSON++import SuperUserSpark.Bake+import SuperUserSpark.Bake.Internal+import SuperUserSpark.Bake.Types+import SuperUserSpark.Diagnose.Internal+import SuperUserSpark.Diagnose.Types+import SuperUserSpark.OptParse.Types+import SuperUserSpark.Utils++diagnoseFromArgs :: DiagnoseArgs -> IO ()+diagnoseFromArgs cas = do+    errOrAss <- diagnoseAssignment cas+    case errOrAss of+        Left err -> die $ unwords ["Failed to build Diagnose assignment:", err]+        Right ass -> diagnose ass++diagnoseAssignment :: DiagnoseArgs -> IO (Either String DiagnoseAssignment)+diagnoseAssignment DiagnoseArgs {..} = do+    errOrCardRef <- parseBakeCardReference diagnoseArgCardRef+    case errOrCardRef of+        Left err -> pure $ Left err+        Right cardRef ->+            DiagnoseAssignment cardRef <$$>+            deriveDiagnoseSettings cardRef diagnoseFlags++deriveDiagnoseSettings :: BakeCardReference+                       -> DiagnoseFlags+                       -> IO (Either String DiagnoseSettings)+deriveDiagnoseSettings bcr DiagnoseFlags {..} =+    DiagnoseSettings <$$> deriveBakeSettings bcr diagnoseBakeFlags++diagnose :: DiagnoseAssignment -> IO ()+diagnose DiagnoseAssignment {..} = do+    errOrDone <-+        runReaderT+            (runExceptT $ diagnoseByCardRef diagnoseCardReference)+            diagnoseSettings+    case errOrDone of+        Left err -> die $ formatDiagnoseError err+        Right () -> pure ()++formatDiagnoseError :: DiagnoseError -> String+formatDiagnoseError (DiagnoseBakeError ce) = formatBakeError ce+formatDiagnoseError (DiagnoseError s) = unwords ["Diagnose failed:", s]++diagnoseByCardRef :: BakeCardReference -> SparkDiagnoser ()+diagnoseByCardRef checkCardReference = do+    deps <-+        diagnoserBake $+        compileBakeCardRef checkCardReference >>= bakeDeployments+    ddeps <- liftIO $ diagnoseDeployments deps+    putStrLn $ JSON.encodePretty ddeps++diagnoserBake :: SparkBaker a -> SparkDiagnoser a+diagnoserBake =+    withExceptT DiagnoseBakeError .+    mapExceptT (withReaderT diagnoseBakeSettings)++diagnoseDeployments :: [BakedDeployment] -> IO [DiagnosedDeployment]+diagnoseDeployments = mapM diagnoseDeployment
+ src/SuperUserSpark/Diagnose/Internal.hs view
@@ -0,0 +1,77 @@+module SuperUserSpark.Diagnose.Internal where++import Import++import qualified Data.ByteString as SB+import Data.Hashable+import System.Posix.Files+       (getSymbolicLinkStatus, isBlockDevice, isCharacterDevice,+        isDirectory, isNamedPipe, isRegularFile, isSocket, isSymbolicLink,+        readSymbolicLink)++import SuperUserSpark.Bake.Types+import SuperUserSpark.Compiler.Types+import SuperUserSpark.Diagnose.Types++diagnoseDeployment :: BakedDeployment -> IO DiagnosedDeployment+diagnoseDeployment (Deployment bds kind) = do+    ddirs <- diagnoseDirs bds+    return $ Deployment ddirs kind++diagnoseDirs :: DeploymentDirections AbsP+             -> IO (DeploymentDirections DiagnosedFp)+diagnoseDirs (Directions srcs dst) =+    Directions <$> mapM diagnoseAbsP srcs <*> diagnoseAbsP dst++diagnoseAbsP :: AbsP -> IO DiagnosedFp+diagnoseAbsP fp = do+    d <- diagnoseFp fp+    hash_ <- hashFilePath fp+    return $ D fp d hash_++diagnoseFp :: AbsP -> IO Diagnostics+diagnoseFp absp = do+    let fp = toPath absp+    ms <- forgivingAbsence $ getSymbolicLinkStatus fp+    case ms of+        Nothing -> pure Nonexistent+        Just s ->+            if isBlockDevice s ||+               isCharacterDevice s || isSocket s || isNamedPipe s+                then return IsWeird+                else do+                    if isSymbolicLink s+                        then do+                            point <- readSymbolicLink fp+                            -- TODO check what happens with relative links.+                            apoint <- AbsP <$> parseAbsFile point+                            return $ IsLinkTo apoint+                        else pure $+                             if isDirectory s+                                 then IsDirectory+                                 else if isRegularFile s+                                          then IsFile+                                          else IsWeird++-- | Hash a filepath so that two filepaths with the same contents have the same hash+hashFilePath :: AbsP -> IO HashDigest+hashFilePath fp = do+    d <- diagnoseFp fp+    case d of+        IsFile -> hashFile fp+        IsDirectory -> hashDirectory fp+        IsLinkTo _ -> return $ HashDigest $ hash ()+        IsWeird -> return $ HashDigest $ hash ()+        Nonexistent -> return $ HashDigest $ hash ()++hashFile :: AbsP -> IO HashDigest+hashFile fp = (HashDigest . hash) <$> SB.readFile (toPath fp)++hashDirectory :: AbsP -> IO HashDigest+hashDirectory fp = do+    tdir <- parseAbsDir (toPath fp)+    walkDirAccum Nothing writer_ tdir+  where+    writer_ _ _ files = do+        hashes <- mapM (hashFile . AbsP) files+        pure $ HashDigest $ hash hashes
+ src/SuperUserSpark/Diagnose/Types.hs view
@@ -0,0 +1,91 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++module SuperUserSpark.Diagnose.Types where++import Import++import Data.Aeson+import Data.Hashable+import Text.Printf++import SuperUserSpark.Bake.Types+import SuperUserSpark.Compiler.Types++data DiagnoseAssignment = DiagnoseAssignment+    { diagnoseCardReference :: BakeCardReference+    , diagnoseSettings :: DiagnoseSettings+    } deriving (Show, Eq, Generic)++instance Validity DiagnoseAssignment++data DiagnoseSettings = DiagnoseSettings+    { diagnoseBakeSettings :: BakeSettings+    } deriving (Show, Eq, Generic)++instance Validity DiagnoseSettings++defaultDiagnoseSettings :: DiagnoseSettings+defaultDiagnoseSettings =+    DiagnoseSettings {diagnoseBakeSettings = defaultBakeSettings}++type SparkDiagnoser = ExceptT DiagnoseError (ReaderT DiagnoseSettings IO)++data DiagnoseError+    = DiagnoseBakeError BakeError+    | DiagnoseError String+    deriving (Show, Eq, Generic)++instance Validity DiagnoseError++newtype HashDigest =+    HashDigest Int+    deriving (Show, Eq, Generic)++instance Validity HashDigest++instance Monoid HashDigest where+    mempty = HashDigest (hash ())+    (HashDigest h1) `mappend` (HashDigest h2) = HashDigest $ h1 * 31 + h2++instance Hashable HashDigest++instance ToJSON HashDigest where+    toJSON (HashDigest i) = toJSON $ (printf "%016x" i :: String)++data Diagnostics+    = Nonexistent+    | IsFile+    | IsDirectory+    | IsLinkTo AbsP -- Could point to directory too.+    | IsWeird+    deriving (Show, Eq, Generic)++instance Validity Diagnostics++instance ToJSON Diagnostics where+    toJSON Nonexistent = String "nonexistent"+    toJSON IsFile = String "file"+    toJSON IsDirectory = String "directory"+    toJSON (IsLinkTo ap) =+        object ["kind" .= String "link", "link destination" .= ap]+    toJSON IsWeird = String "weird"++data DiagnosedFp = D+    { diagnosedFilePath :: AbsP+    , diagnosedDiagnostics :: Diagnostics+    , diagnosedHashDigest :: HashDigest+    } deriving (Show, Eq, Generic)++instance Validity DiagnosedFp++instance ToJSON DiagnosedFp where+    toJSON D {..} =+        object $+        ["path" .= diagnosedFilePath, "diagnostics" .= diagnosedDiagnostics] +++        if diagnosedHashDigest == mempty+            then []+            else ["hash" .= diagnosedHashDigest]++type DiagnosedDeployment = Deployment DiagnosedFp
+ src/SuperUserSpark/Language/Types.hs view
@@ -0,0 +1,78 @@+{-# LANGUAGE DeriveGeneric #-}++module SuperUserSpark.Language.Types where++import Import++import SuperUserSpark.CoreTypes++-- * Cards+type CardName = String++type Source = FilePath++type Destination = FilePath++data Card = Card+    { cardName :: CardName+    , cardContent :: Declaration+    } deriving (Show, Eq, Generic)++instance Validity Card++-- ** Declarations+-- | A declaration in a card+data Declaration+    = SparkOff CardReference -- ^ Spark off another card+    | Deploy Source+             Destination+             (Maybe DeploymentKind) -- ^ Deploy from source to destination+    | IntoDir Directory -- ^ Deploy into a directory+    | OutofDir Directory -- ^ Deploy outof a directory+    | DeployKindOverride DeploymentKind -- ^ Override the deployment kind+    | Alternatives [Directory] -- ^ Provide a list of alternative sources+    | Block [Declaration] -- ^ A scoped block of declarations+    deriving (Show, Eq, Generic)++instance Validity Declaration++-- * Card references+-- | Reference a card by name (inside a file)+newtype CardNameReference =+    CardNameReference CardName+    deriving (Show, Eq, Generic)++instance Validity CardNameReference++-- | Reference a card by the file it is in and therein potentially by a name reference+data CardFileReference =+    CardFileReference FilePath+                      (Maybe CardNameReference)+    deriving (Show, Eq, Generic)++instance Validity CardFileReference++instance Read CardFileReference where+    readsPrec _ fp =+        case length (words fp) of+            1 -> [(CardFileReference fp Nothing, "")]+            2 ->+                let [f, c] = words fp+                in [(CardFileReference f (Just $ CardNameReference c), "")]+            _ -> []++-- | Union card reference+data CardReference+    = CardFile CardFileReference+    | CardName CardNameReference+    deriving (Show, Eq, Generic)++instance Validity CardReference++-- * Card files+data SparkFile = SparkFile+    { sparkFilePath :: Path Abs File+    , sparkFileCards :: [Card]+    } deriving (Show, Eq, Generic)++instance Validity SparkFile
+ src/SuperUserSpark/OptParse.hs view
@@ -0,0 +1,187 @@+module SuperUserSpark.OptParse+    ( getDispatch+    , Dispatch(..)+    ) where++import Import++import Options.Applicative+import System.Environment (getArgs)++import SuperUserSpark.OptParse.Types++getDispatch :: IO Dispatch+getDispatch = do+    args <- getArgs+    let result = runOptionsParser args+    handleParseResult result++runOptionsParser :: [String] -> ParserResult Dispatch+runOptionsParser strs = execParserPure prefs_ optionsParser strs+  where+    prefs_ =+        ParserPrefs+        { prefMultiSuffix = ""+        , prefDisambiguate = True+        , prefShowHelpOnError = True+        , prefShowHelpOnEmpty = True+        , prefBacktrack = True+        , prefColumns = 80+        }++optionsParser :: ParserInfo Dispatch+optionsParser =+    info+        (helper <*> parseDispatch)+        (fullDesc <> progDesc "Super User Spark, author: Tom Sydney Kerckhove")++parseDispatch :: Parser Dispatch+parseDispatch =+    hsubparser $+    mconcat+        [ command "parse" parseParse+        , command "compile" parseCompile+        , command "bake" parseBake+        , command "diagnose" parseDiagnose+        , command "check" parseCheck+        , command "deploy" parseDeploy+        ]++parseParse :: ParserInfo Dispatch+parseParse = info parser modifier+  where+    parser = DispatchParse <$> parseParseArgs+    modifier =+        fullDesc <>+        progDesc "Parse a spark file and check for syntactic errors."++parseParseArgs :: Parser ParseArgs+parseParseArgs =+    ParseArgs <$>+    strArgument (mconcat [metavar "FILE", help "the file to parse"])++parseCompile :: ParserInfo Dispatch+parseCompile = info parser modifier+  where+    parser = DispatchCompile <$> parseCompileArgs+    modifier = fullDesc <> progDesc "Compile a spark card."++parseCompileArgs :: Parser CompileArgs+parseCompileArgs =+    CompileArgs <$>+    strArgument (metavar "CARDREF" <> help "the card file to compile") <*>+    option+        (Just <$> str)+        (mconcat+             [ long "output"+             , short 'o'+             , value Nothing+             , metavar "FILE"+             , help "The output file for compilation"+             ]) <*>+    parseCompileFlags++parseCompileFlags :: Parser CompileFlags+parseCompileFlags =+    CompileFlags <$>+    option+        (Just <$> str)+        (mconcat+             [ long "kind"+             , short 'k'+             , value Nothing+             , metavar "KIND"+             , help+                   "The kind specification for unspecified deployments (default: link)"+             ]) <*>+    option+        (Just <$> str)+        (mconcat+             [ long "override"+             , short 'O'+             , value Nothing+             , metavar "KIND"+             , help "Override every deployment to be of the given kind"+             ])++parseBake :: ParserInfo Dispatch+parseBake = info parser modifier+  where+    parser = DispatchBake <$> parseBakeArgs+    modifier = fullDesc <> progDesc "Bake the raw deployment of a spark card."++parseBakeArgs :: Parser BakeArgs+parseBakeArgs =+    BakeArgs <$> strArgument (metavar "CARDREF" <> help "the card to bake") <*>+    parseBakeFlags++parseBakeFlags :: Parser BakeFlags+parseBakeFlags = BakeFlags <$> parseCompileFlags++parseDiagnose :: ParserInfo Dispatch+parseDiagnose = info parser modifier+  where+    parser = DispatchDiagnose <$> parseDiagnoseArgs+    modifier =+        fullDesc <> progDesc "Diagnose the baked deployment of a spark card."++parseDiagnoseArgs :: Parser DiagnoseArgs+parseDiagnoseArgs =+    DiagnoseArgs <$>+    strArgument (metavar "CARDREF" <> help "the card to diagnose") <*>+    parseDiagnoseFlags++parseDiagnoseFlags :: Parser DiagnoseFlags+parseDiagnoseFlags = DiagnoseFlags <$> parseBakeFlags++parseCheck :: ParserInfo Dispatch+parseCheck = info parser modifier+  where+    parser = DispatchCheck <$> parseCheckArgs+    modifier = fullDesc <> progDesc "Check the deployment of a spark card."++parseCheckArgs :: Parser CheckArgs+parseCheckArgs =+    CheckArgs <$> strArgument (metavar "CARDREF" <> help "the card to check") <*>+    parseCheckFlags++parseCheckFlags :: Parser CheckFlags+parseCheckFlags = CheckFlags <$> parseDiagnoseFlags++parseDeploy :: ParserInfo Dispatch+parseDeploy = info parser modifier+  where+    parser = DispatchDeploy <$> parseDeployArgs+    modifier = fullDesc <> progDesc "Deploy a spark card."++parseDeployArgs :: Parser DeployArgs+parseDeployArgs =+    DeployArgs <$> strArgument (metavar "CARDREF" <> help "the card to deploy") <*>+    parseDeployFlags++parseDeployFlags :: Parser DeployFlags+parseDeployFlags =+    DeployFlags <$>+    switch+        (mconcat+             [ long "replace-links"+             , help "Replace links at deploy destinations."+             ]) <*>+    switch+        (mconcat+             [ long "replace-files"+             , help "Replace existing files at deploy destinations."+             ]) <*>+    switch+        (mconcat+             [ long "replace-Directories"+             , help "Replace existing directories at deploy destinations."+             ]) <*>+    switch+        (mconcat+             [ long "replace-all"+             , short 'r'+             , help+                   "Equivalent to --replace-files --replace-directories --replace-links"+             ]) <*>+    parseCheckFlags
+ src/SuperUserSpark/OptParse/Types.hs view
@@ -0,0 +1,93 @@+{-# LANGUAGE DeriveGeneric #-}++module SuperUserSpark.OptParse.Types where++import Import++data Dispatch+    = DispatchParse ParseArgs+    | DispatchCompile CompileArgs+    | DispatchBake BakeArgs+    | DispatchDiagnose DiagnoseArgs+    | DispatchCheck CheckArgs+    | DispatchDeploy DeployArgs+    deriving (Show, Eq, Generic)++instance Validity Dispatch++data ParseArgs = ParseArgs+    { parseFilePath :: FilePath+    } deriving (Show, Eq, Generic)++instance Validity ParseArgs++data CompileArgs = CompileArgs+    { compileArgCardRef :: String+    , compileArgOutput :: Maybe FilePath+    , compileFlags :: CompileFlags+    } deriving (Show, Eq, Generic)++instance Validity CompileArgs++data CompileFlags = CompileFlags+    { compileDefaultKind :: Maybe String+    , compileKindOverride :: Maybe String+    } deriving (Show, Eq, Generic)++instance Validity CompileFlags++data BakeArgs = BakeArgs+    { bakeCardRef :: String+    , bakeFlags :: BakeFlags+    } deriving (Show, Eq, Generic)++instance Validity BakeArgs++data BakeFlags = BakeFlags+    { bakeCompileFlags :: CompileFlags+    } deriving (Show, Eq, Generic)++instance Validity BakeFlags++data DiagnoseArgs = DiagnoseArgs+    { diagnoseArgCardRef :: String+    , diagnoseFlags :: DiagnoseFlags+    } deriving (Show, Eq, Generic)++instance Validity DiagnoseArgs++data DiagnoseFlags = DiagnoseFlags+    { diagnoseBakeFlags :: BakeFlags+    } deriving (Show, Eq, Generic)++instance Validity DiagnoseFlags++data CheckArgs = CheckArgs+    { checkArgCardRef :: String+    , checkFlags :: CheckFlags+    } deriving (Show, Eq, Generic)++instance Validity CheckArgs++data CheckFlags = CheckFlags+    { checkDiagnoseFlags ::DiagnoseFlags+    } deriving (Show, Eq, Generic)++instance Validity CheckFlags++data DeployArgs = DeployArgs+    { deployArgCardRef :: String+    , deployFlags :: DeployFlags+    } deriving (Show, Eq, Generic)++instance Validity DeployArgs++data DeployFlags = DeployFlags+    { deployFlagReplaceLinks :: Bool+    , deployFlagReplaceFiles :: Bool+    , deployFlagReplaceDirectories :: Bool+    , deployFlagReplaceAll :: Bool+    , deployCheckFlags :: CheckFlags+    } deriving (Show, Eq, Generic)++instance Validity DeployFlags
+ src/SuperUserSpark/Parser.hs view
@@ -0,0 +1,42 @@+{-# LANGUAGE RecordWildCards #-}++{-+    The Parser is responsible for transforming a 'String' into an AST.+-}+module SuperUserSpark.Parser where++import Import++import Control.Exception (try)++import SuperUserSpark.Language.Types+import SuperUserSpark.OptParse.Types+import SuperUserSpark.Parser.Internal+import SuperUserSpark.Parser.Types+import SuperUserSpark.Utils++parseFromArgs :: ParseArgs -> IO ()+parseFromArgs pa = do+    errOrAss <- parseAssignment pa+    case errOrAss of+        Left err -> die $ unwords ["Unable to build parse assignment:", err]+        Right ass -> parse ass++parseAssignment :: ParseArgs -> IO (Either String ParseAssignment)+parseAssignment ParseArgs {..} =+    ParseAssignment <$$>+    ((left (show :: PathParseException -> String)) <$>+     try (resolveFile' parseFilePath))++parse :: ParseAssignment -> IO ()+parse ParseAssignment {..} = do+    errOrFile <- parseFile fileToParse+    case errOrFile of+        Left err -> die $ formatParseError err+        Right _ -> pure ()++formatParseError :: ParseError -> String+formatParseError (ParseError pe) = show pe++parseFile :: Path Abs File -> IO (Either ParseError SparkFile)+parseFile file = (left ParseError . parseCardFile file) <$> readFile file
+ src/SuperUserSpark/Parser/Internal.hs view
@@ -0,0 +1,267 @@+module SuperUserSpark.Parser.Internal where++import Import++import Data.List (isSuffixOf)+import SuperUserSpark.Constants+import SuperUserSpark.CoreTypes+import SuperUserSpark.Language.Types+import Text.Parsec+import Text.Parsec.String++parseCardFile :: Path Abs File -> String -> Either ParseError SparkFile+parseCardFile f s = do+    cs <- parseFromSource sparkFile f s+    return $ SparkFile f cs++parseFromSource :: Parser a -> Path Abs File -> String -> Either ParseError a+parseFromSource parser file = parse parser $ toFilePath file++--[ Language ]--+sparkFile :: Parser [Card]+sparkFile = do+    clean <- eatComments+    setInput clean+    resetPosition+    cards++cards :: Parser [Card]+cards = card `sepEndBy1` whitespace++resetPosition :: Parser ()+resetPosition = do+    pos <- getPosition+    setPosition $ setSourceColumn (setSourceLine pos 1) 1++card :: Parser Card+card = do+    whitespace+    skip $ string keywordCard+    whitespace+    name <- cardNameP+    whitespace+    b <- block+    whitespace+    return $ Card name b++declarations :: Parser [Declaration]+declarations = (inLineSpace declaration) `sepEndBy` delim++declaration :: Parser Declaration+declaration =+    choice $+    map+        try+        [ block+        , alternatives+        , sparkOff+        , intoDir+        , outOfDir+        , deploymentKindOverride+        , deployment+        ]++block :: Parser Declaration+block =+    do ds <- inBraces $ inWhiteSpace declarations+       return $ Block ds+       <?> "block"++sparkOff :: Parser Declaration+sparkOff =+    do skip $ string keywordSpark+       linespace+       ref <- cardReference+       return $ SparkOff ref+       <?> "sparkoff"++compilerCardReference :: Parser CardFileReference+compilerCardReference = unprefixedCardFileReference++compiledCardReference :: Parser FilePath+compiledCardReference = do+    skip $ string "compiled"+    skip linespace+    filepath++cardReference :: Parser CardReference+cardReference = try goName <|> try goFile <?> "card reference"+  where+    goName = cardNameReference >>= return . CardName+    goFile = cardFileReference >>= return . CardFile++cardNameReference :: Parser CardNameReference+cardNameReference =+    do skip $ string keywordCard+       linespace+       name <- cardNameP+       return $ CardNameReference name+       <?> "card name reference"++cardNameP :: Parser CardName+cardNameP = identifier <?> "card name"++cardFileReference :: Parser CardFileReference+cardFileReference = do+    skip $ string keywordFile+    skip linespace+    unprefixedCardFileReference++unprefixedCardFileReference :: Parser CardFileReference+unprefixedCardFileReference =+    do fp <- filepath+       linespace+       mn <- optionMaybe $ try cardNameP+       return $+           case mn of+               Nothing -> CardFileReference fp Nothing+               Just cn -> CardFileReference fp (Just $ CardNameReference cn)+       <?> "card file reference"++intoDir :: Parser Declaration+intoDir =+    do skip $ string keywordInto+       linespace+       dir <- directory+       return $ IntoDir dir+       <?> "into directory declaration"++outOfDir :: Parser Declaration+outOfDir =+    do skip $ string keywordOutof+       linespace+       dir <- directory+       return $ OutofDir dir+       <?> "outof directory declaration"++deploymentKindOverride :: Parser Declaration+deploymentKindOverride =+    do skip $ string keywordKindOverride+       linespace+       kind <- try copy <|> link+       return $ DeployKindOverride kind+       <?> "deployment kind override"+  where+    copy = string keywordCopy >> return CopyDeployment+    link = string keywordLink >> return LinkDeployment++shortDeployment :: Parser Declaration+shortDeployment = do+    source <- try directory <|> filepath+    return $ Deploy source source Nothing++longDeployment :: Parser Declaration+longDeployment = do+    source <- filepath+    linespace+    kind <- deploymentKind+    linespace+    dest <- filepath+    return $ Deploy source dest kind++deployment :: Parser Declaration+deployment = try longDeployment <|> shortDeployment <?> "deployment"++deploymentKind :: Parser (Maybe DeploymentKind)+deploymentKind = try link <|> try copy <|> def <?> "deployment kind"+  where+    link = string linkKindSymbol >> return (Just LinkDeployment)+    copy = string copyKindSymbol >> return (Just CopyDeployment)+    def = string unspecifiedKindSymbol >> return Nothing++alternatives :: Parser Declaration+alternatives = do+    skip $ string keywordAlternatives+    linespace+    ds <- directory `sepBy1` linespace+    return $ Alternatives ds++-- [ FilePaths ]--+filepath :: Parser FilePath+filepath = do+    i <- identifier <?> "Filepath"+    if "/" `isSuffixOf` i+        then unexpected "slash at the end"+        else return i++directory :: Parser Directory+directory = filepath <?> "Directory"++--[ Comments ]--+comment :: Parser String+comment = try lineComment <|> try blockComment <?> "Comment"++lineComment :: Parser String+lineComment =+    (<?> "Line comment") $ do+        skip $ try $ string lineCommentStr+        anyChar `manyTill` eol++blockComment :: Parser String+blockComment =+    (<?> "Block comment") $ do+        skip $ try $ string start+        anyChar `manyTill` (try $ string stop)+  where+    (start, stop) = blockCommentStrs++notComment :: Parser String+notComment = manyTill anyChar (lookAhead ((skip comment) <|> eof))++eatComments :: Parser String+eatComments = do+    optional comment+    xs <- notComment `sepBy` comment+    optional comment+    let withoutComments = concat xs+    return withoutComments++--[ Identifiers ]--+identifier :: Parser String+identifier = try quotedIdentifier <|> plainIdentifier++plainIdentifier :: Parser String+plainIdentifier =+    many1 $+    noneOf $ quotesChar : lineDelimiter ++ whitespaceChars ++ bracesChars++quotedIdentifier :: Parser String+quotedIdentifier = inQuotes $ many $ noneOf $ quotesChar : endOfLineChars++--[ Delimiters ]--+inBraces :: Parser a -> Parser a+inBraces = between (char '{') (char '}')++inQuotes :: Parser a -> Parser a+inQuotes = between (char quotesChar) (char quotesChar)++delim :: Parser ()+delim = try (skip $ string lineDelimiter) <|> go+  where+    go = do+        eol+        whitespace++--[ Whitespace ]--+inLineSpace :: Parser a -> Parser a+inLineSpace = between linespace linespace++inWhiteSpace :: Parser a -> Parser a+inWhiteSpace = between whitespace whitespace++linespace :: Parser ()+linespace = skip $ many $ oneOf linespaceChars++whitespace :: Parser ()+whitespace = skip $ many $ oneOf whitespaceChars++eol :: Parser ()+eol = skip newline_+  where+    newline_ =+        try (string "\r\n") <|> try (string "\n") <|>+        string "\r" <?> "end of line"++--[ Utils ]--+skip :: Parser a -> Parser ()+skip p = p >> return ()
+ src/SuperUserSpark/Parser/Types.hs view
@@ -0,0 +1,27 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DeriveGeneric #-}++module SuperUserSpark.Parser.Types where++import Import++import qualified Text.Parsec as Parsec++data ParseAssignment = ParseAssignment+    { fileToParse :: Path Abs File+    } deriving (Show, Eq, Generic)++instance Validity ParseAssignment++data ParseSettings =+    ParseSettings+    deriving (Show, Eq, Generic)++instance Validity ParseSettings++newtype ParseError =+    ParseError Parsec.ParseError+    deriving (Show, Eq, Generic)++instance Validity ParseError where+    isValid (ParseError _) = True
+ src/SuperUserSpark/PreCompiler.hs view
@@ -0,0 +1,58 @@+module SuperUserSpark.PreCompiler where++import Import++import SuperUserSpark.Language.Types+import SuperUserSpark.PreCompiler.Types+import SuperUserSpark.Utils++preCompileChecks :: Card -> [PreCompileError]+preCompileChecks c = runIdentity $ execWriterT $ cleanCard c++dirty :: String -> Precompiler ()+dirty s = tell [PreCompileError $ "Precompilation check failed: " ++ s]++cleanCard :: Card -> Precompiler ()+cleanCard (Card name d) = do+    cleanCardName name+    cleanDeclaration d++cleanDeclaration :: Declaration -> Precompiler ()+cleanDeclaration (Deploy src dst _) = do+    cleanFilePath src+    cleanFilePath dst+cleanDeclaration (SparkOff cr) = cleanCardReference cr+cleanDeclaration (IntoDir dir) = cleanFilePath dir+cleanDeclaration (OutofDir dir) = cleanFilePath dir+cleanDeclaration (DeployKindOverride _) = return () -- Nothing can go wrong.+cleanDeclaration (Alternatives fs) = mapM_ cleanFilePath fs+cleanDeclaration (Block ds) = mapM_ cleanDeclaration ds++cleanCardReference :: CardReference -> Precompiler ()+cleanCardReference (CardFile cfr) = cleanCardFileReference cfr+cleanCardReference (CardName cnr) = cleanCardNameReference cnr++cleanCardFileReference :: CardFileReference -> Precompiler ()+cleanCardFileReference (CardFileReference fp mcnr) = do+    cleanFilePath fp+    case mcnr of+        Nothing -> return ()+        Just cnr -> cleanCardNameReference cnr++cleanCardNameReference :: CardNameReference -> Precompiler ()+cleanCardNameReference (CardNameReference cn) = cleanCardName cn++cleanCardName :: CardName -> Precompiler ()+cleanCardName n+    | containsNewline n =+        dirty $ "Card name contains newline character(s): " ++ n+    | otherwise = return ()++cleanFilePath :: FilePath -> Precompiler ()+cleanFilePath [] = dirty "Empty filepath"+cleanFilePath fp+    | containsNewline fp =+        dirty $ "Filepath contains newline character(s): " ++ fp+    | containsMultipleConsequtiveSlashes fp =+        dirty $ "Filepath contains multiple consequtive slashes: " ++ fp+    | otherwise = return ()
+ src/SuperUserSpark/PreCompiler/Types.hs view
@@ -0,0 +1,13 @@+{-# LANGUAGE DeriveGeneric #-}++module SuperUserSpark.PreCompiler.Types where++import Import++newtype PreCompileError =+    PreCompileError String+    deriving (Show, Eq, Generic)++instance Validity PreCompileError++type Precompiler = WriterT [PreCompileError] Identity
+ src/SuperUserSpark/Utils.hs view
@@ -0,0 +1,45 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE MultiParamTypeClasses #-}++module SuperUserSpark.Utils where++import Import++import Data.List (isInfixOf)+import qualified System.Directory as D (createDirectoryIfMissing)++incase+    :: MonadReader c m+    => (c -> Bool) -> m () -> m ()+incase bf func = do+    b <- asks bf+    when b func++incaseElse+    :: MonadReader c m+    => (c -> Bool) -> m a -> m a -> m a+incaseElse bf funcif funcelse = do+    b <- asks bf+    if b+        then funcif+        else funcelse++containsNewline :: String -> Bool+containsNewline f = any (\c -> elem c f) ['\n', '\r']++containsMultipleConsequtiveSlashes :: String -> Bool+containsMultipleConsequtiveSlashes = isInfixOf "//"++(&&&) :: (a -> Bool) -> (a -> Bool) -> a -> Bool+(&&&) f g = \a -> f a && g a++(<$$>) :: (a -> b) -> IO (Either e a) -> IO (Either e b)+(<$$>) f fa = do+    a <- fa+    pure $ f <$> a++(<**>) :: IO (Either e (a -> b)) -> IO (Either e a) -> IO (Either e b)+(<**>) fa fb = do+    e1 <- fa+    e2 <- fb+    pure $ e1 <*> e2
− src/Types.hs
@@ -1,49 +0,0 @@-module Types-    ( module Types-    , module CoreTypes-    , module Monad-    , module Config.Types--    , module Control.Monad.Except-    , module Control.Monad.IO.Class-    , module Control.Monad.Reader-    , module Control.Monad.State-    , module Control.Monad.Writer-    , module Control.Monad.Trans-    , module Control.Monad.Identity-    , module Text.Parsec--    , module Debug.Trace-    ) where--import           Control.Monad.Except   (ExceptT, mapExceptT, runExceptT,-                                         throwError, withExceptT)-import           Control.Monad.Identity (Identity, runIdentity)-import           Control.Monad.IO.Class (MonadIO (..), liftIO)-import           Control.Monad.Reader   (MonadReader (..), ReaderT, ask, asks,-                                         mapReaderT, runReaderT)-import           Control.Monad.State    (MonadState (..), StateT, evalStateT,-                                         execStateT, get, gets, modify, put,-                                         runStateT)-import           Control.Monad.Trans    (lift)-import           Control.Monad.Writer   (MonadWriter (..), WriterT, execWriterT,-                                         runWriterT, tell)-import           Debug.Trace--import           Text.Parsec            (ParseError)--import           Config.Types-import           CoreTypes-import           Monad-----[ Options ]----data GlobalOptions = GlobalOptions-    { opt_output              :: Maybe FilePath-    , opt_kind                :: Maybe DeploymentKind-    , opt_overrride           :: Maybe DeploymentKind-    , opt_replace_links       :: Bool-    , opt_replace_files       :: Bool-    , opt_replace_directories :: Bool-    , opt_replace             :: Bool-    , opt_debug               :: Bool-    } deriving (Show, Eq)
− src/Utils.hs
@@ -1,44 +0,0 @@-{-# LANGUAGE FlexibleContexts      #-}-{-# LANGUAGE MultiParamTypeClasses #-}-module Utils where--import           Control.Monad    (when)-import           Data.List        (isInfixOf)-import qualified System.Directory as D (createDirectoryIfMissing)-import           System.Exit      (exitFailure)-import           System.IO        (hPutStrLn, stderr)-import           Types--debug :: (MonadReader SparkConfig m, MonadIO m) => String -> m ()-debug str = incase (asks conf_debug) $ liftIO $ putStrLn str--incase :: MonadReader SparkConfig m => (SparkConfig -> Bool) -> m () -> m ()-incase bf func = do-    b <- asks bf-    when b func--incaseElse :: MonadReader SparkConfig m => (SparkConfig -> Bool) -> m a -> m a -> m a-incaseElse bf funcif funcelse = do-    b <- asks bf-    if b-    then funcif-    else funcelse--die :: String -> IO a-die err = hPutStrLn stderr err >> exitFailure--containsNewline :: String -> Bool-containsNewline f = any (\c -> elem c f) ['\n', '\r']--containsMultipleConsequtiveSlashes :: String -> Bool-containsMultipleConsequtiveSlashes = isInfixOf "//"--(&&&) :: (a -> Bool) -> (a -> Bool) -> a -> Bool-(&&&) f g = \a -> f a && g a--(|||) :: (a -> Bool) -> (a -> Bool) -> a -> Bool-(|||) f g = \a -> f a || g a--createDirectoryIfMissing :: FilePath -> IO ()-createDirectoryIfMissing = D.createDirectoryIfMissing True-
super-user-spark.cabal view
@@ -1,5 +1,5 @@ name:                super-user-spark-version:             0.3.2.0+version:             0.4.0.0 license:             MIT license-file:        LICENSE description:         Configure your dotfile deployment with a DSL.@@ -14,32 +14,36 @@ library   hs-source-dirs:   src   exposed-modules: -        Arguments-      , Check-      , Check.Internal-      , Check.Types-      , Compiler-      , Compiler.Internal-      , Compiler.Types-      , Compiler.Utils-      , Config.Types-      , Config-      , Constants-      , CoreTypes-      , Deployer-      , Deployer.Types-      , Deployer.Internal-      , Dispatch-      , Dispatch.Types-      , Language.Types-      , Monad-      , Parser-      , Parser.Internal-      , PreCompiler-      , Seed-      , Spark-      , Types-      , Utils+        SuperUserSpark+      , SuperUserSpark.Bake+      , SuperUserSpark.Bake.Internal+      , SuperUserSpark.Bake.Types+      , SuperUserSpark.Check+      , SuperUserSpark.Check.Internal+      , SuperUserSpark.Check.Types+      , SuperUserSpark.Compiler+      , SuperUserSpark.Compiler.Internal+      , SuperUserSpark.Compiler.Types+      , SuperUserSpark.Compiler.Utils+      , SuperUserSpark.Constants+      , SuperUserSpark.CoreTypes+      , SuperUserSpark.Deployer+      , SuperUserSpark.Deployer.Internal+      , SuperUserSpark.Deployer.Types+      , SuperUserSpark.Diagnose+      , SuperUserSpark.Diagnose.Internal+      , SuperUserSpark.Diagnose.Types+      , SuperUserSpark.Language.Types+      , SuperUserSpark.OptParse+      , SuperUserSpark.OptParse.Types+      , SuperUserSpark.Parser+      , SuperUserSpark.Parser.Internal+      , SuperUserSpark.Parser.Types+      , SuperUserSpark.PreCompiler+      , SuperUserSpark.PreCompiler.Types+      , SuperUserSpark.Utils+  other-modules:+        Import    ghc-options:              -Wall@@ -49,24 +53,28 @@        -fno-warn-name-shadowing    build-depends:   -        base                  >= 4.6   && < 5-      , aeson                 >= 0.8   && < 1.1-      , aeson-pretty          >= 0.7   && < 0.9+        base                  >= 4.9   && < 5+      , aeson                 >= 0.11  && < 1.2+      , aeson-pretty          >= 0.8   && < 0.9       , bytestring            >= 0.10  && < 0.11-      , directory             >= 1.2.5 && < 1.3+      , directory             >= 1.3   && < 1.4       , filepath              >= 1.4   && < 1.5       , mtl                   >= 2.2   && < 2.3-      , optparse-applicative  >= 0.11  && < 0.14+      , optparse-applicative  >= 0.13  && < 0.14       , parsec                >= 3.1.9 && < 3.2-      , process               >= 1.2   && < 1.5-      , pureMD5               >= 2.1   && < 2.2-      , shelly                >= 1.6   && < 1.7+      , process               >= 1.4   && < 1.6+      , hashable              >= 1.2   && < 1.3       , text                  >= 1.2   && < 1.3-      , transformers          >= 0.4   && < 0.6       , unix                  >= 2.7   && < 2.8+      , validity              >= 0.3.2 && < 0.4+      , validity-path >= 0.1 && < 0.2+      , path >= 0.5 && < 0.6+      , path-io >= 1.2 && < 1.3+      , iostring >= 0.0 && < 0.1   default-language: Haskell2010+  default-extensions: NoImplicitPrelude -executable spark+executable super-user-spark   main-is:          Main.hs   hs-source-dirs:   app   ghc-options:      -Wall@@ -76,7 +84,7 @@                     -fno-warn-name-shadowing                     -threaded -rtsopts -with-rtsopts=-N   build-depends:   -        base                  >= 4.6   && < 5+        base                  >= 4.9   && < 5       , super-user-spark   default-language: Haskell2010   @@ -86,17 +94,34 @@   main-is:          MainTest.hs   hs-source-dirs:   test   other-modules:    -        CheckSpec-      , ParserSpec-      , CompilerSpec-      , DeployerSpec-      , SeedSpec-      , EndToEndSpec+        SuperUserSpark.BakeSpec+      , SuperUserSpark.Bake.Gen+      , SuperUserSpark.Check.Gen+      , SuperUserSpark.Check.TestUtils+      , SuperUserSpark.CheckSpec+      , SuperUserSpark.Compiler.Gen+      , SuperUserSpark.Compiler.TestUtils+      , SuperUserSpark.CompilerSpec+      , SuperUserSpark.Deployer.Gen+      , SuperUserSpark.DeployerSpec+      , SuperUserSpark.Diagnose.Gen+      , SuperUserSpark.Diagnose.TestUtils+      , SuperUserSpark.DiagnoseSpec+      , SuperUserSpark.EndToEnd.RegressionSpec+      , SuperUserSpark.EndToEndSpec+      , SuperUserSpark.Language.Gen+      , SuperUserSpark.OptParse.Gen+      , SuperUserSpark.Parser.Gen+      , SuperUserSpark.Parser.TestUtils+      , SuperUserSpark.ParserSpec+      , SuperUserSpark.PreCompiler.Gen+      , TestImport+      , TestUtils   build-depends:    -        base                  >= 4.6   && < 5+        base                  >= 4.9   && < 5       , super-user-spark-      , hspec                 >= 2.2   && < 2.4-      , hspec-core            >= 2.2   && < 2.4+      , hspec                 >= 2.2   && < 2.5+      , hspec-core            >= 2.2   && < 2.5       , HUnit                 >= 1.2   && < 1.6       , QuickCheck            >= 2.8   && < 2.10       , aeson                 @@ -108,10 +133,19 @@       , optparse-applicative         , parsec                       , process               -      , pureMD5               -      , shelly                       , text                         , transformers                 , unix                  +      , genvalidity >= 0.3.1 && < 0.4+      , genvalidity-path >= 0.1 && < 0.2+      , genvalidity-hspec >= 0.3.1 && < 0.4+      , genvalidity-hspec-aeson >= 0.0 && < 0.1+      , validity+      , validity-path+      , path+      , path-io+      , iostring+      , hashable   default-language:  Haskell2010+  default-extensions: NoImplicitPrelude   
− test/CheckSpec.hs
@@ -1,298 +0,0 @@-module CheckSpec where--import           Check.Gen          ()-import           Check.Internal-import           Check.TestUtils-import           Check.Types-import           Compiler.Types-import           Control.Monad      (forM_)-import           CoreTypes-import qualified Data.ByteString    as SB-import           Parser.Gen-import           System.Directory   (removeDirectoryRecursive, removeFile,-                                     withCurrentDirectory)-import           System.Posix.Files-import           Test.Hspec-import           Test.QuickCheck-import           TestUtils-import           Utils--spec :: Spec-spec = do-    diagnoseSpec-    checkSpec-    hashSpec--diagnoseSpec :: Spec-diagnoseSpec = do-    let sandbox = "test_sandbox"-    let setup = createDirectoryIfMissing sandbox-    let teardown = removeDirectoryRecursive sandbox--    beforeAll_ setup $ afterAll_ teardown $ do-        describe "diagnoseDeployment" $ do-            it "retains the filepaths and deploymentkind that it diagnoses for very simple filepaths" $ do-                once $ forAll (resize 5 $ listOf generateWord) $ \srcs -> do-                    forAll generateWord $ \dst -> do-                        forAll arbitrary $ \kind -> do-                            (Diagnosed dsrcs ddst dkind) <- diagnoseDeployment $ Put srcs dst kind-                            map diagnosedFilePath dsrcs `shouldBe` srcs-                            diagnosedFilePath ddst `shouldBe` dst-                            dkind `shouldBe` kind--            pend---        describe "diagnose" $ do-            it "retains the filepath that it diagnoses for very simple filepaths" $ do-                once $ forAll generateWord $ \fp -> do-                    (D dfp _ _) <- diagnose fp-                    dfp `shouldBe` fp--            pend---        describe "diagnoseFp" $ do-            it "figures out this test file" $ do-                withCurrentDirectory sandbox $ do-                    let file = "test.txt"-                    writeFile file "This is a test"-                    diagnoseFp file `shouldReturn` IsFile-                    removeFile file-                    diagnoseFp file `shouldReturn` Nonexistent--            it "figures out this test directory" $ do-                withCurrentDirectory sandbox $ do-                    let dir = "testdir"-                    createDirectoryIfMissing dir-                    diagnoseFp dir `shouldReturn` IsDirectory-                    removeDirectoryRecursive dir-                    diagnoseFp dir `shouldReturn` Nonexistent--            it "figures out this test symbolic link with a destination" $ do-                withCurrentDirectory sandbox $ do-                    let link = "testlink"-                    let file = "testfile"-                    writeFile file "This is a test"-                    createSymbolicLink file link--                    diagnoseFp file `shouldReturn` IsFile-                    diagnoseFp link `shouldReturn` IsLinkTo file--                    removeLink link-                    removeFile file--                    diagnoseFp link `shouldReturn` Nonexistent--            it "figures out this test symbolic link without a destination" $ do-                withCurrentDirectory sandbox $ do-                    let link = "testlink"-                    let file = "testfile"-                    createSymbolicLink file link--                    diagnoseFp file `shouldReturn` Nonexistent-                    diagnoseFp link `shouldReturn` IsLinkTo file--                    removeLink link--                    diagnoseFp file `shouldReturn` Nonexistent-                    diagnoseFp link `shouldReturn` Nonexistent--            it "figures out that /dev/null is weird" $ do-                diagnoseFp "/dev/null" `shouldReturn` IsWeird--            it "figures out that /dev/random is weird" $ do-                diagnoseFp "/dev/random" `shouldReturn` IsWeird--checkSpec :: Spec-checkSpec = parallel $ do-    checkSingleSpec-    checkDeploymentSpec--checkDeploymentSpec :: Spec-checkDeploymentSpec = do-    describe "checkDeployment" $ do-        it "says 'impossible' for deployments with an empty list of sources" $ do-            forAll arbitrary $ \dst ->-                forAll arbitrary $ \kind ->-                    shouldBeImpossible' $ Diagnosed [] dst kind--        it "says 'impossible' for deployments where all singles are impossible" $ do-            forAll (arbitrary `suchThat` (\(Diagnosed srcs dst kind) -> all (\src -> isImpossible $ checkSingle src dst kind) srcs)) $ \dd ->-                shouldBeImpossible' dd---        it "gives the same result as bestResult (just with a better error for empty lists)" $ do-            property $ \dd@(Diagnosed srcs dst kind) ->-                case (bestResult (map (\src -> checkSingle src dst kind) srcs), checkDeployment dd) of-                    (ImpossibleDeployment r1, ImpossibleDeployment r2) -> length r1 `shouldSatisfy` (<= (length r2))-                    (r1, r2) -> r1 `shouldBe` r2--    describe "bestResult" $ do-        it "says 'impossible' if all checkresults are impossible" $ do-            forAll (arbitrary `suchThat` all isImpossible) shouldBeImpossibleDeployment--        it "says 'done' if the first non-impossible in 'done'" $ do-            forAll (arbitrary `suchThat` (any (not . isImpossible)-                                     &&& (isDone . head . dropWhile isImpossible)))-              $ \dd -> bestResult dd `shouldSatisfy` deploymentIsDone--        it "says 'dirty' if the first non-impossible in 'dirty'" $ do-            forAll (arbitrary `suchThat` (any (not . isImpossible)-                                     &&& (isDirty . head . dropWhile isImpossible)))-              $ \dd -> bestResult dd `shouldSatisfy` dirtyDeployment--        it "says 'ready' if the first non-impossible in 'ready'" $ do-            forAll (arbitrary `suchThat` (any (not . isImpossible)-                                     &&& (isReady . head . dropWhile isImpossible)))-              $ \dd -> bestResult dd `shouldSatisfy` deploymentReadyToDeploy---checkSingleSpec :: Spec-checkSingleSpec = describe "checkSingle" $ do-    it "says 'impossible' if the source does not exist" $ do-        forAll (arbitraryWith Nonexistent) $ \src ->-            forAll arbitrary $ \dst ->-                forAll arbitrary $ \kind ->-                    shouldBeImpossible src dst kind--    it "says 'ready' if the source is a file and the destination does not exist" $ do-        forAll (arbitraryWith IsFile) $ \src ->-            forAll (arbitraryWith Nonexistent) $ \dst ->-                forAll arbitrary $ \kind ->-                    shouldBeReady src dst kind--    it "says 'dirty' if both the source and destination are files and it's a link deployment" $ do-        forAll (arbitraryWith IsFile) $ \src ->-            forAll (arbitraryWith IsFile) $ \dst ->-                shouldBeDirty src dst LinkDeployment $ CleanFile $ diagnosedFilePath dst--    it "says 'done' if both the source and destination are files and it's a copy deployment and the files are equal" $ do-        forAll arbitrary $ \src ->-            forAll arbitrary $ \dst ->-                forAll arbitrary $ \h1 ->-                    shouldBeDone (D src IsFile h1) (D dst IsFile h1) CopyDeployment--    it "says 'dirty' if both the source and destination are files and it's a copy deployment but the files are unequal" $ do-        forAll arbitrary $ \src ->-            forAll arbitrary $ \dst ->-                forAll arbitrary $ \h1 ->-                    forAll (arbitrary `suchThat` (/= h1)) $ \h2 ->-                        shouldBeDirty (D src IsFile h1) (D dst IsFile h2) CopyDeployment $ CleanFile dst--    it "says 'dirty' if the source is a file and the destination is a directory" $ do-        forAll (arbitraryWith IsFile) $ \src ->-            forAll (arbitraryWith IsDirectory) $ \dst ->-                forAll arbitrary $ \kind ->-                    shouldBeDirty src dst kind $ CleanDirectory $ diagnosedFilePath dst--    it "says 'dirty' if the source is a file and the destination is a link for a link deployment but the destination doesn't point to the source" $ do-        forAll (arbitraryWith IsFile) $ \src@(D srcp _ _) ->-            forAll (arbitrary `suchThat` (/= srcp)) $ \l ->-                forAll (arbitraryWith $ IsLinkTo l) $ \dst ->-                    shouldBeDirty src dst LinkDeployment $ CleanLink $ diagnosedFilePath dst--    it "says 'done' if the source is a file and the destination is a link for a link deployment and the destination points to the source" $ do-        forAll (arbitraryWith IsFile) $ \src@(D srcp _ _) ->-            forAll (arbitraryWith $ IsLinkTo srcp) $ \dst ->-                shouldBeDone src dst LinkDeployment--    it "says 'dirty' if the source is a file and the destination is a link for a copy deployment" $ do-        forAll (arbitraryWith IsFile)  $ \src ->-            forAll arbitrary $ \l ->-                forAll (arbitraryWith $ IsLinkTo l) $ \dst ->-                    shouldBeDirty src dst CopyDeployment $ CleanLink $ diagnosedFilePath dst--    it "says 'ready' if the source is a directory and the destination does not exist" $ do-        forAll (arbitraryWith IsDirectory) $ \src ->-            forAll (arbitraryWith Nonexistent) $ \dst ->-                forAll arbitrary $ \kind ->-                    shouldBeReady src dst kind--    it "says 'dirty' if the source is a directory and the destination is a file" $ do-        forAll (arbitraryWith IsDirectory) $ \src ->-            forAll (arbitraryWith IsFile) $ \dst ->-                forAll arbitrary $ \kind ->-                    shouldBeDirty src dst kind $ CleanFile $ diagnosedFilePath dst--    it "says 'dirty' if both the source and destination are directories for a link deployment" $ do-        forAll (arbitraryWith IsDirectory) $ \src ->-            forAll (arbitraryWith IsDirectory) $ \dst ->-                shouldBeDirty src dst LinkDeployment $ CleanDirectory $ diagnosedFilePath dst--    it "says 'done' if both the source and destination are directories and it's a copy deployment and the directories are equal" $ do-        forAll arbitrary $ \src ->-            forAll arbitrary $ \dst ->-                forAll arbitrary $ \h1 ->-                        shouldBeDone (D src IsDirectory h1) (D dst IsDirectory h1) CopyDeployment--    it "says 'dirty' if both the source and destination are directories and it's a copy deployment but the directories are unequal" $ do-        forAll arbitrary $ \src ->-            forAll arbitrary $ \dst ->-                forAll arbitrary $ \h1 ->-                    forAll (arbitrary `suchThat` (/= h1)) $ \h2 ->-                        shouldBeDirty (D src IsDirectory h1) (D dst IsDirectory h2) CopyDeployment $ CleanDirectory dst--    it "says 'dirty' if the source is a directory and the destination is a link for a link deployment but the destination doesn't point to the source" $ do-        forAll (arbitraryWith IsDirectory) $ \src@(D srcp _ _) ->-            forAll (arbitrary `suchThat` (/= srcp)) $ \l ->-                forAll (arbitraryWith $ IsLinkTo l) $ \dst ->-                    shouldBeDirty src dst LinkDeployment $ CleanLink $ diagnosedFilePath dst--    it "says 'done' if the source is a directory and the destination is a link for a link deployment and the destination points to the source" $ do-        forAll (arbitraryWith IsDirectory) $ \src@(D srcp _ _) ->-            forAll (arbitraryWith $ IsLinkTo srcp) $ \dst ->-                shouldBeDone src dst LinkDeployment--    it "says 'dirty' if the source is a directory and the destination is a link for a copy deployment" $ do-        forAll (arbitraryWith IsDirectory) $ \src ->-            forAll arbitrary $ \l ->-                forAll (arbitraryWith $ IsLinkTo l) $ \dst ->-                    shouldBeDirty src dst CopyDeployment $ CleanLink $ diagnosedFilePath dst--    it "says 'dirty' if the source is a link" $ do-        forAll arbitrary $ \l ->-            forAll (arbitraryWith $ IsLinkTo l) $ \src ->-                forAll arbitrary $ \dst ->-                    forAll arbitrary $ \kind ->-                        shouldBeImpossible src dst kind---    it "says 'dirty' for a weird source" $ do-        forAll (arbitraryWith IsWeird) $ \src ->-            forAll arbitrary $ \dst ->-                forAll arbitrary $ \kind ->-                    shouldBeImpossible src dst kind--    it "says 'dirty' for a weird destination" $ do-        forAll arbitrary $ \src ->-            forAll (arbitraryWith IsWeird) $ \dst ->-                forAll arbitrary $ \kind ->-                    shouldBeImpossible src dst kind--    it "works for these unit tests" $ do-        pending----hashSpec :: Spec-hashSpec = do-    tooManyFilesTest--tooManyFilesTest :: Spec-tooManyFilesTest = do-    let sandbox = "test_sandbox"-    let setup = createDirectoryIfMissing sandbox-    let teardown = removeDirectoryRecursive sandbox-    let aLot = 20000 :: Int-    let setupALotOfFiles = do-          forM_ [1..aLot] $ \i ->-            writeFile (sandbox ++ "/file" ++ show i) $ "This is file " ++ show i ++ ".\n"--    beforeAll_ setup $ afterAll_ teardown $ do-        describe "hashFilePath" $ do-            beforeAll_ setupALotOfFiles $ do-                it ("has no problem with hashing a directory of " ++ show aLot ++ " files") $ do-                    hashFilePath "test_sandbox" `shouldNotReturn` md5 SB.empty--
− test/CompilerSpec.hs
@@ -1,364 +0,0 @@-module CompilerSpec where--import           Compiler-import           Compiler.TestUtils-import           Compiler.Types-import           Config-import           CoreTypes-import           Data.Either           (isLeft, isRight)-import           Data.List             (isPrefixOf)-import           Language.Gen          ()-import           Language.Types-import           PreCompiler-import           System.FilePath.Posix (takeExtension, (<.>), (</>))-import           Test.Hspec-import           Test.QuickCheck-import           TestUtils-import           Types-import           Utils--spec :: Spec-spec = parallel $ do-    singleCompileDecSpec-    precompileSpec-    hopTests-    exactTests-    compilerBlackBoxTests---precompileSpec :: Spec-precompileSpec = describe "pre-compilation" $ do-    cleanContentSpec--cleanContentSpec :: Spec-cleanContentSpec = do-    let validFp = arbitrary `suchThat` cleanBy cleanFilePath--    describe "cleanCard" $ do-        it "doesn't report any card with valid content and a valid name" $ do-            forAll (arbitrary `suchThat` cleanBy cleanCardName) $ \cn ->-              forAll (arbitrary `suchThat` cleanBy cleanDeclaration) $ \cc ->-                Card cn cc `shouldSatisfy` cleanBy cleanCard--    describe "cleanCardName" $ do-        pend--        it "doesn't report an emty card name" $ do-            "" `shouldSatisfy` cleanBy cleanCardName--        it "reports card names with newlines" $ do-            forAll (arbitrary `suchThat` containsNewlineCharacter) $ \s ->-                s `shouldNotSatisfy` cleanBy cleanCardName--    describe "cleanDeclaration" $ do-        describe "Deploy" $ do-            it "doesn't report Deploy declarations with valid filepaths" $ do-                forAll validFp $ \src ->-                  forAll validFp $ \dst ->-                    forAll arbitrary $ \kind ->-                      Deploy src dst kind `shouldSatisfy` cleanBy cleanDeclaration--            it "reports Deploy declarations with an invalid source" $ do-                forAll (arbitrary `suchThat` (not . cleanBy cleanFilePath)) $ \src ->-                  forAll validFp $ \dst ->-                    forAll arbitrary $ \kind ->-                      Deploy src dst kind `shouldNotSatisfy` cleanBy cleanDeclaration--            it "reports Deploy declarations with an invalid destination" $ do-                forAll validFp $ \src ->-                  forAll (arbitrary `suchThat` (not . cleanBy cleanFilePath)) $ \dst ->-                    forAll arbitrary $ \kind ->-                      Deploy src dst kind `shouldNotSatisfy` cleanBy cleanDeclaration--            pend--        describe "SparkOff" $ do-            it "reports SparkOff declarations with an invalid card reference" $ do-                forAll (arbitrary `suchThat` (not . cleanBy cleanCardReference)) $ \cr ->-                  SparkOff cr `shouldNotSatisfy` cleanBy cleanDeclaration--            it "doesn't report SparkOff declarations with a valid card reference" $ do-                forAll (arbitrary `suchThat` cleanBy cleanCardReference) $ \cr ->-                  SparkOff cr `shouldSatisfy` cleanBy cleanDeclaration--            pend--        describe "IntoDir" $ do-            it "reports IntoDir declarations with an invalid filepath" $ do-                forAll (arbitrary `suchThat` (not . cleanBy cleanFilePath)) $ \fp ->-                  IntoDir fp `shouldNotSatisfy` cleanBy cleanDeclaration--            it "doesn't report IntoDir declarations with a valid filepath" $ do-                forAll (arbitrary `suchThat` cleanBy cleanFilePath) $ \fp ->-                  IntoDir fp `shouldSatisfy` cleanBy cleanDeclaration--            pend--        describe "OutofDir" $ do-            it "reports OutofDir declarations with an invalid filepath" $ do-                forAll (arbitrary `suchThat` (not . cleanBy cleanFilePath)) $ \fp ->-                  OutofDir fp `shouldNotSatisfy` cleanBy cleanDeclaration--            it "doesn't report OutofDir declarations with a valid filepath" $ do-                forAll (arbitrary `suchThat` cleanBy cleanFilePath) $ \fp ->-                  OutofDir fp `shouldSatisfy` cleanBy cleanDeclaration--            pend--        describe "DeployKindOverride" $ do-            it "doesn't report any deployment kind override declarations" $ do-                forAll arbitrary $ \kind ->-                    DeployKindOverride kind `shouldSatisfy` cleanBy cleanDeclaration--            pend--        describe "Alternatives" $ do-            it "reports alternatives declarations with as much as a single invalid filepath" $ do-                forAll (arbitrary `suchThat` (any $ not . cleanBy cleanFilePath)) $ \fs ->-                    Alternatives fs `shouldNotSatisfy` cleanBy cleanDeclaration--            it "doesn't report alternatives declarations with valid filepaths" $ do-                forAll (arbitrary `suchThat` (all $ cleanBy cleanFilePath)) $ \fs ->-                    Alternatives fs `shouldSatisfy` cleanBy cleanDeclaration--            pend--        describe "Block" $ do-            it "reports block declarations with as much as a single invalid declaration inside" $ do-                forAll (arbitrary `suchThat` (any $ not . cleanBy cleanDeclaration)) $ \ds ->-                    Block ds `shouldNotSatisfy` cleanBy cleanDeclaration--            it "doesn't report any block declarations with valid declarations inside" $ do-                forAll (arbitrary `suchThat` (all $ cleanBy cleanDeclaration)) $ \ds ->-                    Block ds `shouldSatisfy` cleanBy cleanDeclaration---            pend--    describe "cleanCardReference" $ do-        it "works the same as cleanCardName separately" $ do-            forAll arbitrary $ \cnr ->-                cleanBy cleanCardNameReference cnr === cleanBy cleanCardReference (CardName cnr)--        it "works the same as cleanCardFile separately" $ do-            forAll arbitrary $ \cfr ->-                cleanBy cleanCardFileReference cfr === cleanBy cleanCardReference (CardFile cfr)--        pend--    describe "cleanCardNameReference" $ do-        it "reports card name references with an invalid card name" $ do-            forAll (arbitrary `suchThat` (not . cleanBy cleanCardName)) $ \cn ->-                CardNameReference cn `shouldNotSatisfy` cleanBy cleanCardNameReference--        it "doesn't report card name references with a valid card name" $ do-            forAll (arbitrary `suchThat` cleanBy cleanCardName) $ \cn ->-                CardNameReference cn `shouldSatisfy` cleanBy cleanCardNameReference--        pend--    describe "cleanCardFileReference" $ do-        it "reports card file references with an invalid filepath" $ do-            forAll (arbitrary `suchThat` (not . cleanBy cleanFilePath)) $ \fp ->-                forAll arbitrary $ \cn ->-                    CardFileReference fp cn `shouldNotSatisfy` cleanBy cleanCardFileReference--        it "reports card file references with an invalid card name" $ do-            forAll arbitrary $ \fp ->-                forAll (arbitrary `suchThat` (not . cleanBy cleanCardNameReference)) $ \cn ->-                    CardFileReference fp (Just cn) `shouldNotSatisfy` cleanBy cleanCardFileReference--        it "doesn't report card file references with a valid card name reference and valid filepath" $ do-            forAll (arbitrary `suchThat` cleanBy cleanFilePath) $ \fp ->-                forAll (arbitrary `suchThat` cleanBy cleanCardNameReference) $ \cn ->-                    CardFileReference fp (Just cn) `shouldSatisfy` cleanBy cleanCardFileReference--        pend---    describe "cleanFilePath" $ do-        it "reports empty an filepath" $ do-            filePathDirty []--        let nonNull = arbitrary `suchThat` (not . null)-        it "reports filepaths with newlines" $ do-            forAll (nonNull `suchThat` containsNewlineCharacter) filePathDirty--        let withoutNewlines = nonNull `suchThat` (not . containsNewlineCharacter)-        it "reports filepaths with multiple consequtive slashes" $ do-            once $ forAll (withoutNewlines `suchThat` containsMultipleConsequtiveSlashes) filePathDirty--        let c = filePathClean-        it "doesn't report these valid filepaths" $ do-            c "noextension"-            c ".bashrc"-            c "file.txt"-            c "Some file with spaces.doc"-            c "some/relative/filepath.file"----- TODO(syd) Use the default config to generate this!-defaultCompilerState :: CompilerState-defaultCompilerState = CompilerState-    { state_deployment_kind_override = Nothing-    , state_into = ""-    , state_outof_prefix = []-    }--singleCompileDecSpec :: Spec-singleCompileDecSpec = describe "compileDec" $ do-    let s = defaultCompilerState-    let c = defaultConfig-    let sc = singleShouldCompileTo c s--    let nonNull = arbitrary `suchThat` (not . null)-    let validFilePath = nonNull `suchThat` (not . containsNewlineCharacter)-    let easyFilePath = validFilePath `suchThat` (not . isPrefixOf ".")-    let validFp = arbitrary `suchThat` cleanBy cleanFilePath--    describe "Deploy" $ do-        it "uses the exact right text in source and destination when given valid filepaths without a leading dot" $ do-            forAll easyFilePath $ \from ->-                forAll easyFilePath $ \to ->-                    sc (Deploy from to Nothing) (Put [from] to LinkDeployment)--        it "handles filepaths with a leading dot correctly" $ do-            pending--        it "figures out the correct paths in these cases with default config and initial state" $ do-            let d = (Deploy "from" "to" $ Just LinkDeployment)-            sc d (Put ["from"] "to" LinkDeployment)--        it "uses the alternates correctly" $ do-            pending--        it "uses the into's correctly" $ do-            pending--        it "uses the outof's correctly" $ do-            pending--        pend--    describe "SparkOff" $ do-        it "adds a single card file reference to the list of cards to spark later" $ do-            forAll validFilePath $ \f ->-                let cr = CardFile $ CardFileReference f Nothing-                    d = SparkOff cr-                in compileSingleDec d s c `shouldBe` Right (s, ([], [cr]))--        it "adds any card reference to the list" $ do-            pending--        pend--    let shouldState = shouldResultInState c s-    describe "IntoDir" $ do-        it "adds the given directory to the into state" $ do-            forAll validFp $ \fp ->-                shouldState (IntoDir fp) $ s { state_into = fp }--        it "compounds with the input state" $ do-            pendingWith "Change the input state to an explicit list first"--        pend--    describe "OutofDir" $ do-        it "adds the given directory to the outof state" $ do-            forAll validFp $ \fp ->-                shouldState (OutofDir fp) $ s { state_outof_prefix = [Literal fp] }--        pend--    describe "DeployKindOverride" $ do-        it "modifies the internal deployment kind override" $ do-            property $ \kind -> shouldState (DeployKindOverride kind) $ s { state_deployment_kind_override = Just kind }--        pend--    describe "Block" $ do-        it "uses a separate scope for its sub-compilation" $ do-            property $ \ds -> shouldState (Block ds) s--        pend--    describe "Alternatives" $ do-        it "adds an alternatives prefix to the outof prefix in the compiler state" $ do-            forAll (listOf validFilePath) $ \fps ->-                shouldState (Alternatives fps) $ s { state_outof_prefix = [Alts fps] }--        pend--runDefaultSparker :: Sparker a -> IO (Either SparkError a)-runDefaultSparker func = flip runReaderT defaultConfig $ runExceptT $ func--hopTests :: Spec-hopTests = do-    describe "hop test" $ do-        let dir = "test_resources/hop_test"-        let root = dir </> "root.sus"-        let hop1 = dir </> "hop1dir" </> "hop1.sus"-        let hop2 = dir </> "hop1dir" </> "hop2dir" </> "hop2.sus"-        let hop3 = dir </> "hop1dir" </> "hop2dir" </> "hop3dir" </> "hop3.sus"-        it "compiles hop3 correctly" $ do-            r <- runDefaultSparker $ compileJob $ CardFileReference hop3 Nothing-            r `shouldBe` Right [Put ["z/delta"] "d/three" LinkDeployment]--        it "compiles hop2 correctly" $ do-            r <- runDefaultSparker $ compileJob $ CardFileReference hop2 Nothing-            r `shouldBe` Right-                [ Put ["y/gamma"] "c/two" LinkDeployment-                , Put ["hop3dir/z/delta"] "d/three" LinkDeployment-                ]--        it "compiles hop1 correctly" $ do-            r <- runDefaultSparker $ compileJob $ CardFileReference hop1 Nothing-            r `shouldBe` Right-                [ Put ["x/beta"] "b/one" LinkDeployment-                , Put ["hop2dir/y/gamma"] "c/two" LinkDeployment-                , Put ["hop2dir/hop3dir/z/delta"] "d/three" LinkDeployment-                ]--        it "compiles root correctly" $ do-            r <- runDefaultSparker $ compileJob $ CardFileReference root Nothing-            r `shouldBe` Right-                [ Put ["u/alpha"] "a/zero" LinkDeployment-                , Put ["hop1dir/x/beta"] "b/one" LinkDeployment-                , Put ["hop1dir/hop2dir/y/gamma"] "c/two" LinkDeployment-                , Put ["hop1dir/hop2dir/hop3dir/z/delta"] "d/three" LinkDeployment-                ]--exactTests :: Spec-exactTests = do-    describe "exact tests" $ do-        let dir = "test_resources/exact_compile_test_src"-        forFileInDirss [dir] $ \fp ->-            if takeExtension fp == ".res"-            then return ()-            else do-                it fp $ do-                    let orig = fp-                    let result = fp <.> "res"-                    ads <- runDefaultSparker $ compileJob $ CardFileReference orig Nothing-                    eds <- runDefaultSparker $ inputCompiled result-                    ads `shouldBe` eds----compilerBlackBoxTests :: Spec-compilerBlackBoxTests = do-    let tr = "test_resources"-    describe "Correct succesful compile examples" $ do-        let dirs = map (tr </>) ["shouldCompile", "hop_test"]-        forFileInDirss dirs $ \f -> do-            it f $ do-                r <- runDefaultSparker $ compileJob $ CardFileReference f Nothing-                r `shouldSatisfy` isRight--    describe "Correct unsuccesfull compile examples" $ do-        let dirs = map (tr </>) ["shouldNotParse", "shouldNotCompile"]-        forFileInDirss dirs $ \f -> do-            it f $ do-                r <- runDefaultSparker $ compileJob $ CardFileReference f Nothing-                r `shouldSatisfy` isLeft-
− test/DeployerSpec.hs
@@ -1,263 +0,0 @@-module DeployerSpec where--import           Check.Internal-import           Check.Types-import           Config-import           Config.Types-import           Data.Either           (isLeft)-import           Data.Maybe            (isNothing)-import           Deployer.Internal-import           Deployer.Types-import           Monad-import           Parser.Gen-import           System.Directory      hiding (createDirectoryIfMissing)-import           System.FilePath.Posix ((</>))-import           System.Posix.Files-import           Test.Hspec-import           Test.QuickCheck-import           Utils--spec :: Spec-spec = do-    cleanSpec-    deploymentSpec-    completionSpec--cleanSpec :: Spec-cleanSpec = do-    let sandbox = "test_sandbox"-    let setup = createDirectoryIfMissing sandbox-    let teardown = removeDirectoryRecursive sandbox--    let clean :: SparkConfig -> CleanupInstruction -> IO ()-        clean conf ci = runSparker conf (performClean ci) `shouldReturn` Right ()--    beforeAll_ setup $ afterAll_ teardown $ do-        describe "performClean" $ do--            it "doesn't remove this file if that's not in the config" $ do-                let c = defaultConfig { conf_deploy_replace_files = False }-                withCurrentDirectory sandbox $ do-                    let file = "test.txt"-                    writeFile file "This is a test"-                    diagnoseFp file `shouldReturn` IsFile-                    clean c $ CleanFile file-                    diagnoseFp file `shouldReturn` IsFile-                    removeFile file-                    diagnoseFp file `shouldReturn` Nonexistent--            it "removes this file if that's in the config" $ do-                let c = defaultConfig { conf_deploy_replace_files = True }-                withCurrentDirectory sandbox $ do-                    let file = "test.txt"-                    writeFile file "This is a test"-                    diagnoseFp file `shouldReturn` IsFile-                    clean c $ CleanFile file-                    diagnoseFp file `shouldReturn` Nonexistent--            it "doesn't remove this directory if that's not in the config" $ do-                let c = defaultConfig { conf_deploy_replace_directories = False }-                withCurrentDirectory sandbox $ do-                    let dir = "testdirectory"-                    createDirectoryIfMissing dir-                    diagnoseFp dir `shouldReturn` IsDirectory-                    clean c $ CleanDirectory dir-                    diagnoseFp dir `shouldReturn` IsDirectory-                    removeDirectoryRecursive dir-                    diagnoseFp dir `shouldReturn` Nonexistent--            it "removes this directory if that's in the config" $ do-                let c = defaultConfig { conf_deploy_replace_directories = True }-                withCurrentDirectory sandbox $ do-                    let dir = "testdirectory"-                    createDirectoryIfMissing dir-                    diagnoseFp dir `shouldReturn` IsDirectory-                    clean c $ CleanDirectory dir-                    diagnoseFp dir `shouldReturn` Nonexistent--            it "doesn't remove this link if that's not in the config" $ do-                let c = defaultConfig { conf_deploy_replace_links = False }-                withCurrentDirectory sandbox $ do-                    let link = "testlink"-                    let file = "testfile"-                    writeFile file "This is a test"-                    createSymbolicLink file link-                    diagnoseFp link `shouldReturn` IsLinkTo file-                    clean c $ CleanLink link-                    diagnoseFp link `shouldReturn` IsLinkTo file-                    removeLink link-                    diagnoseFp link `shouldReturn` Nonexistent-                    removeFile file-                    diagnoseFp file `shouldReturn` Nonexistent--            it "removes this link with an existent source if that's in the config" $ do-                let c = defaultConfig { conf_deploy_replace_links = True }-                withCurrentDirectory sandbox $ do-                    let link = "testlink"-                    let file = "testfile"-                    writeFile file "This is a test"-                    createSymbolicLink file link-                    diagnoseFp link `shouldReturn` IsLinkTo file-                    clean c $ CleanLink link-                    diagnoseFp link `shouldReturn` Nonexistent-                    removeFile file-                    diagnoseFp file `shouldReturn` Nonexistent--            it "removes this link with a nonexistent source if that's in the config" $ do-                let c = defaultConfig { conf_deploy_replace_links = True }-                withCurrentDirectory sandbox $ do-                    let link = "testlink"-                    let file = "testfile"-                    createSymbolicLink file link-                    diagnoseFp link `shouldReturn` IsLinkTo file-                    diagnoseFp file `shouldReturn` Nonexistent-                    clean c $ CleanLink link-                    diagnoseFp link `shouldReturn` Nonexistent-                    diagnoseFp file `shouldReturn` Nonexistent--deploymentSpec :: Spec-deploymentSpec = do-    let sandbox = "test_sandbox"-    let setup = createDirectoryIfMissing sandbox-    let teardown = removeDirectoryRecursive sandbox--    beforeAll_ setup $ afterAll_ teardown $ do-        describe "copy" $ do-            it "succcesfully copies this file" $ do-                withCurrentDirectory sandbox $ do-                    let src = "testfile"-                    let dst = "testcopy"-                    writeFile src "This is a file."--                    diagnoseFp src `shouldReturn` IsFile-                    diagnoseFp dst `shouldReturn` Nonexistent--                    -- Under test-                    copy src dst--                    diagnoseFp src `shouldReturn` IsFile-                    diagnoseFp dst `shouldReturn` IsFile--                    dsrc <- diagnose src-                    ddst <- diagnose dst-                    diagnosedHashDigest ddst `shouldBe` diagnosedHashDigest dsrc--                    removeFile src-                    removeFile dst--                    diagnoseFp src `shouldReturn` Nonexistent-                    diagnoseFp dst `shouldReturn` Nonexistent--            it "succcesfully copies this directory" $ do-                withCurrentDirectory sandbox $ do-                    let src = "testdir"-                    let dst = "testcopy"-                    createDirectoryIfMissing src--                    diagnoseFp src `shouldReturn` IsDirectory-                    diagnoseFp dst `shouldReturn` Nonexistent--                    -- Under test-                    copy src dst--                    diagnoseFp src `shouldReturn` IsDirectory-                    diagnoseFp dst `shouldReturn` IsDirectory--                    dsrc <- diagnose src-                    ddst <- diagnose dst-                    diagnosedHashDigest ddst `shouldBe` diagnosedHashDigest dsrc--                    removeDirectoryRecursive src-                    removeDirectoryRecursive dst--                    diagnoseFp src `shouldReturn` Nonexistent-                    diagnoseFp dst `shouldReturn` Nonexistent--        describe "link" $ do-            it "successfully links this file" $ do-                withCurrentDirectory sandbox $ do-                    let src = "testfile"-                    let dst = "testlink"--                    diagnoseFp src `shouldReturn` Nonexistent-                    diagnoseFp dst `shouldReturn` Nonexistent--                    writeFile src "This is a test."--                    diagnoseFp src `shouldReturn` IsFile-                    diagnoseFp dst `shouldReturn` Nonexistent--                    -- Under test-                    link src dst--                    diagnoseFp src `shouldReturn` IsFile-                    diagnoseFp dst `shouldReturn` IsLinkTo src--                    removeFile src-                    removeLink dst--                    diagnoseFp src `shouldReturn` Nonexistent-                    diagnoseFp dst `shouldReturn` Nonexistent--            it "successfully links this directory" $ do-                withCurrentDirectory sandbox $ do-                    let src = "testdir"-                    let dst = "testlink"--                    diagnoseFp src `shouldReturn` Nonexistent-                    diagnoseFp dst `shouldReturn` Nonexistent--                    createDirectoryIfMissing src--                    diagnoseFp src `shouldReturn` IsDirectory-                    diagnoseFp dst `shouldReturn` Nonexistent--                    -- Under test-                    link src dst--                    diagnoseFp src `shouldReturn` IsDirectory-                    diagnoseFp dst `shouldReturn` IsLinkTo src--                    removeDirectoryRecursive src-                    removeLink dst--                    diagnoseFp src `shouldReturn` Nonexistent-                    diagnoseFp dst `shouldReturn` Nonexistent---completionSpec :: Spec-completionSpec = do-    describe "complete" $ do-        -- Necessary because not all utilities can handle paths with '~' in them.-        it "replaces the home directory as specified for simple home directories and simple paths" $ do-            forAll arbitrary $ \env ->-                forAll generateWord $ \home ->-                    forAll generateWord $ \fp ->-                        complete home env ("~" </> fp) `shouldBe` Right (home </> fp)--    describe "parseId" $ do-        it "Works for these cases" $ do-            parseId "" `shouldBe` []-            parseId "file" `shouldBe` [Plain "file"]-            parseId "something$(with)variable" `shouldBe` [Plain "something", Var "with", Plain "variable"]-            parseId "$(one)$(two)$(three)" `shouldBe` [Var "one", Var "two", Var "three"]--    describe "replaceId" $ do-        it "leaves plain ID's unchanged in any environment" $ do-            forAll arbitrary $ \env ->-                forAll arbitrary $ \s ->-                    replaceId env (Plain s) `shouldBe` Right s--        it "returns Left if a variable is not in the environment" $ do-            forAll arbitrary $ \var ->-                forAll (arbitrary `suchThat` (isNothing . lookup var)) $ \env ->-                    replaceId env (Var var) `shouldSatisfy` isLeft--        it "replaces a variable if it's in the environment" $ do-            forAll arbitrary $ \var ->-                forAll arbitrary $ \val ->-                    forAll (arbitrary `suchThat` (isNothing . lookup var)) $ \env1 ->-                        forAll (arbitrary `suchThat` (isNothing . lookup var)) $ \env2 ->-                            replaceId (env1 ++ [(var, val)] ++ env2) (Var var) `shouldBe` Right val--
− test/EndToEndSpec.hs
@@ -1,82 +0,0 @@-module EndToEndSpec (spec) where--import           Spark-import           System.Directory      hiding (createDirectoryIfMissing)-import           System.Environment    (withArgs)-import           System.Exit           (ExitCode (ExitSuccess))-import           System.FilePath.Posix ((</>))-import           System.Posix.Files-import           Test.Hspec-import           Utils--spec :: Spec-spec = do-    helpTextSpec-    regularWorkflowSpec--helpTextSpec :: Spec-helpTextSpec = describe "help text test" $ do-    it "shows the help text without crashing" $ do-        withArgs ["--help"] spark `shouldThrow` (== ExitSuccess)--regularWorkflowSpec :: Spec-regularWorkflowSpec = do-    let sandbox = "test_sandbox"-    let setup = createDirectoryIfMissing sandbox-    let teardown = removeDirectoryRecursive sandbox--    let rsc = "test_resources" </> "end-to-end"--    beforeAll_ setup $ afterAll_ teardown $ do-        describe "standard bash card test" $ do-            let bashrsc = rsc </> "bash.sus"-            let bashrscres = rsc </> "bash.sus.res"-            let cardfile = sandbox </> "bash.sus"--            let up = do-                    copyFile bashrsc cardfile-                    withCurrentDirectory sandbox $ do-                        createDirectoryIfMissing "bash"-                        withCurrentDirectory "bash" $ do-                            writeFile "bash_aliases" "bash_aliases"-                            writeFile "bashrc" "bashrc"-                            writeFile "bash_profile" "bash_profile"--            let down = do-                    removeFile cardfile-                    withCurrentDirectory sandbox $ do-                        removeDirectoryRecursive "bash"---            beforeAll_ up $ afterAll_ down $ do-                it "parses correcty" $ do-                    withArgs ["parse", cardfile] spark `shouldReturn` ()--                it "compiles correctly" $ do-                    let outfile = sandbox </> "bash.sus.res"--                    withArgs ["compile", cardfile, "--output", outfile] spark `shouldReturn` ()--                    actual <- readFile outfile-                    expected <- readFile bashrscres-                    actual `shouldBe` expected--                it "checks without exceptions" $ do-                    withArgs ["check", cardfile] spark `shouldReturn` ()--                it "deploys correctly" $ do-                    withArgs ["deploy", cardfile] spark `shouldReturn` ()--                    let f1 = "subdir" </> ".bashrc"-                        f2 = "subdir" </> ".bash_aliases"-                        f3 = "subdir" </> ".bash_profile"--                    readFile f1 `shouldReturn` "bashrc"-                    readFile f2 `shouldReturn` "bash_aliases"-                    readFile f3 `shouldReturn` "bash_profile"--                    removeLink f1-                    removeLink f2-                    removeLink f3--
test/MainTest.hs view
@@ -1,11 +1,10 @@ {-# OPTIONS_GHC -F -pgmF hspec-discover #-}+ module MainTest where  import qualified Spec-import           Test.Hspec.Formatters-import           Test.Hspec.Runner+import Test.Hspec.Formatters+import Test.Hspec.Runner  main :: IO () main = hspecWith defaultConfig {configFormatter = Just progress} Spec.spec--
− test/ParserSpec.hs
@@ -1,498 +0,0 @@-module ParserSpec where--import           CoreTypes-import           Data.Either           (isLeft, isRight)-import           Data.List             (intercalate)-import           Language.Types-import           Parser.Gen-import           Parser.Internal-import           Parser.TestUtils-import           System.FilePath.Posix ((</>))-import           Test.Hspec-import           Test.QuickCheck-import           TestUtils-import           Text.Parsec--spec :: Spec-spec = parallel $ do-    blankspaceParserTests-    enclosingCharacterTests-    delimiterTests-    identifierParserTests-    commentParserTests-    pathParserTests-    declarationParserTests-    parserBlackBoxTests---enclosingCharacterTests :: Spec-enclosingCharacterTests = do-    describe "inBraces" $ do-        it "succeeds for cases where we enclose braces around a string without braces" $ do-            forAll (listOf1 arbitrary `suchThat` (\c -> c /= "{" && c /= "}")) (\word ->-                parseShouldSucceedAs (inBraces $ string word) ("{" ++ word ++ "}") word)--    describe "inQuotes" $ do-        it "succeeds for cases where we enclose quotes around a string without quotes" $ do-            forAll (listOf1 arbitrary `suchThat` (/= "\"")) (\word ->-                parseShouldSucceedAs (inQuotes $ string word) ("\"" ++ word ++ "\"") word)--blankspaceParserTests :: Spec-blankspaceParserTests = do-    describe "eol" $ do-        let s = shouldSucceed eol-        it "succeeds for Linux line endings" $ do-            s "\n"-        it "succeeds for Windows line endings" $ do-            s "\r\n"-        it "succeeds for Mac line endings" $ do-            s "\r"-        let f = shouldFail eol-        it "fails for the empty string" $ do-            f ""-        it "fails for spaces" $ do-            forAll (listOf generateSpace) (\ss -> f ss)-        it "fails for tabs" $ do-            forAll (listOf generateTab) (\ss -> f ss)-        it "fails for linespace" $ do-            forAll generateLineSpace (\ls -> f ls)--    describe "linespace" $ do-        let s = shouldSucceed linespace-        it "succeeds for spaces" $ do-            forAll (listOf generateSpace) s-        it "succeeds for tabs" $ do-            forAll (listOf generateTab) s-        it "succeeds for mixtures of spaces and tabs" $ do-            forAll generateLineSpace s--        let f = shouldSucceed whitespace-        it "fails for line ending characters" $ do-            forAll (listOf $ oneof [generateCarriageReturn, generateLineFeed]) f-        it "fails for any non-linespace, even if there's linespace in it" $  do-            forAll (listOf1 generateNormalCharacter) (\ls ->-             forAll generateLineSpace (\ls1 ->-              forAll generateLineSpace (\ls2 ->-               shouldFail linespace (ls1 ++ ls ++ ls2))))---    describe "whitespace" $ do-        let s = shouldSucceed whitespace-        it "succeeds for spaces" $ do-            forAll (listOf generateSpace) s-        it "succeeds for tabs" $ do-            forAll (listOf generateTab) s-        it "succeeds carriage returns" $ do-            forAll (listOf generateCarriageReturn) s-        it "succeeds line feeds" $ do-            forAll (listOf generateLineFeed) s-        it "succeeds for mixtures of spaces, tabs, carriage returns and line feeds" $ do-            forAll generateWhiteSpace s--        it "fails for any non-whitespace, even if there's whitespace in it" $  do-            forAll (listOf1 generateNormalCharacter) (\ls ->-                        forAll generateWhiteSpace (\ws1 ->-                         forAll generateWhiteSpace (\ws2 ->-                            shouldFail whitespace (ws1 ++ ls ++ ws2))))---    describe "inLineSpace" $ do-        it "succeeds for cases where we append whitespace to the front and back of non-whitespace" $ do-            forAll generateLineSpace (\ws1 ->-              forAll generateLineSpace (\ws2 ->-                forAll (listOf1 generateNormalCharacter) (\ls ->-                    parseShouldSucceedAs (inLineSpace $ string ls) (ws1 ++ ls ++ ws2) ls)))--    describe "inWhiteSpace" $ do-        it "succeeds for cases where we append whitespace to the front and back of non-whitespace" $ do-            forAll generateWhiteSpace (\ws1 ->-              forAll generateWhiteSpace (\ws2 ->-                forAll (listOf1 generateNormalCharacter) (\ls ->-                    parseShouldSucceedAs (inWhiteSpace $ string ls) (ws1 ++ ls ++ ws2) ls)))--delimiterTests :: Spec-delimiterTests = do-    describe "delim" $ do-        it "succeeds on a semicolon" $ do-            shouldSucceed delim ";"-        it "succeeds on an eol" $ do-            once $ forAll (arbitrary `suchThat` succeeds eol) (shouldSucceed delim)--identifierParserTests :: Spec-identifierParserTests = do-    describe "plainIdentifier" $ do-        it "succeeds for generated plain identifiers" $ do-            forAll generatePlainIdentifier $ \(e, a) ->-                parseShouldSucceedAs plainIdentifier e a--        let pi = shouldSucceed plainIdentifier-        it "succeeds for these examples" $ do-            pi "bash"-            pi "card"-            pi ".bashrc"-            pi "xmonad.hs"--        it "fails for generated quoted identifiers" $ do-            forAll generateQuotedIdentifier $ \(e, _) ->-                shouldFail plainIdentifier e--    describe "quotedIdentifier" $ do-        it "succeeds for generated plain identifiers surrounded in quotes" $ do-            forAll generatePlainIdentifier $ \(e, a) ->-                parseShouldSucceedAs quotedIdentifier ("\"" ++ e ++ "\"") a-        it "succeeds for generated quoted identifiers" $ do-            forAll generateQuotedIdentifier $ \(e, a) ->-                parseShouldSucceedAs quotedIdentifier e a--        let pi i = parseShouldSucceedAs quotedIdentifier ("\"" ++ i ++ "\"") i-        it "succeeds for these examples" $ do-            pi "bashrc"-            pi "with spaces"--        it "fails for generated plain identifiers" $ do-            forAll generatePlainIdentifier $ \(e, _) ->-                shouldFail quotedIdentifier e--        it "fails for generated identifiers with just one quote" $ do-            forAll generatePlainIdentifier $ \(e, _) ->-                shouldFail quotedIdentifier ("\"" ++ e) .&&. shouldFail quotedIdentifier (e ++ "\"")--    describe "identifier" $ do-        it "succeeds for generated identifiers" $ do-            forAll generateIdentifier $ \(e, a) ->-                parseShouldSucceedAs identifier e a--commentParserTests :: Spec-commentParserTests = do-    describe "eatComments" $ do-        it "should succeed unchanged on anything without comments" $ do-            property $ \s -> (not $ succeedsAnywhere comment s) ==> parseShouldSucceedAs eatComments s s--        let (-=>) e a = parseShouldSucceedAs eatComments e a-        it "successfully removes comments in these strings" $ do-            "abc#def\nghi"                          -=> "abcghi"-            "abc# This is a bigger comment \r\nghi" -=> "abcghi"-            "abc[[def]]ghi"                         -=> "abcghi"-            "abc[[ This is a bigger comment ]]ghi"  -=> "abcghi"-            "Heavy[[use]]of#comments\n."            -=> "Heavyof."--    describe "notComment" $ do-        it "should succeed for any string that doesn't contain comments" $ do-            property $ \s -> (not $ succeedsAnywhere comment s) ==> parseShouldSucceedAs notComment s s--    describe "lineComment" $ do-        it "succeeds for generated line comments" $ do-            forAll generateLineComment $ \(e, a) -> parseShouldSucceedAs lineComment e a-        it "succeeds for these test cases" $ do-            parseShouldSucceedAs lineComment "#a\n" "a"-            parseShouldSucceedAs lineComment "# This is a comment\n" " This is a comment"-            parseShouldSucceedAs lineComment "## This is a comment with two comment signs.\n" "# This is a comment with two comment signs."-            parseShouldSucceedAs lineComment "# with other eol\r\n" " with other eol"--    describe "blockComment" $ do-        it "succeeds for generated block comments" $ do-            forAll generateBlockComment $ \(e, a) -> parseShouldSucceedAs blockComment e a-        it "succeeds of these test cases" $ do-            parseShouldSucceedAs blockComment "[[a]]" "a"-            parseShouldSucceedAs blockComment "[[ This is a block comment. ]]" " This is a block comment. "-            parseShouldSucceedAs blockComment "[[ [This is a [block] comment containing brackets.] ]]" " [This is a [block] comment containing brackets.] "--    describe "comment" $ do-        it "succeeds for generated line comments" $ do-            forAll generateLineComment $ \(e, a) -> parseShouldSucceedAs comment e a--        it "succeeds for generated block comments" $ do-            forAll generateBlockComment $ \(e, a) -> parseShouldSucceedAs comment e a--pathParserTests :: Spec-pathParserTests = do-    describe "filepath" $ do-        it "succeeds for generated filepaths" $ do-            forAll generateFilePath $ \(e, a) -> parseShouldSucceedAs filepath e a-        it "succeeds for this quoted filepath with a space in it" $ do-            parseShouldSucceedAs filepath "\"/home/user/with spaces\"" "/home/user/with spaces"--        let s = shouldSucceed filepath-        it "succeeds for this file without an extension" $ do-            s "withoutExtension"-        it "succeeds for this simple file" $ do-            s "test.txt"-        it "succeeds for this simple file with a long extension" $ do-            s "file.somelongextension"-        it "succeeds for this absolute filepath" $ do-            s "/home/user/test.txt"-        it "succeeds for this absolute filepath with a long extension" $ do-            s "/home/user/file.somelongextension"-        it "succeeds for this absolute filepath with multiple extensions" $ do-            s "/home/user/test.multiple.extensions"-        it "succeeds for this relative filepath with a double dot" $ do-            s "/home/user/../user/test.txt"--        let f = shouldFail filepath-        it "fails for just a slash" $ do-            f "/"-        it "fails for strings ending in a slash" $ do-            property $ \s -> f (s ++ "/")--    describe "directory" $ do-        it "succeeds for generated directories" $ do-            forAll generateDirectory $ \(e, a) -> parseShouldSucceedAs directory e a--        let s = shouldSucceed directory-        it "succeeds for the home directory" $ do-            s "~"-        it "succeeds for this relative directory" $ do-            s "directory"-        it "succeeds for this absolute directory" $ do-            s "/home/user"-        it "succeeds for these directories in the home directory" $ do-            s "~/.vim"-            s "~/Dropbox"-            s "~/.xmonad"--        let f = shouldFail directory-        it "fails for just a slash" $ do-            f "/"-        it "fails for strings ending in a slash" $ do-            property $ \s -> f (s ++ "/")---declarationParserTests :: Spec-declarationParserTests = do-    describe "cardName" $ do-        it "succeeds on every card name that we generate" $ do-            forAll generateCardName $ \(a, e) -> parseShouldSucceedAs cardNameP a e--    describe "card" $ do-        let pc = parseShouldSucceedAs card-        it "succeeds on this card with an empty name correctly" $ do-            pc "card \"\" {}" $ Card "" (Block [])--        it "succeeds on this compressed empty cards" $ do-            forAll generateCardName $ \(a, e) ->-                parseShouldSucceedAs card ("card" ++ a ++ "{}") $ Card e (Block [])--        it "succeeds on empty cards with whitespace around the name" $ do-            forAll generateCardName $ \(a, e) ->-                forAll (twice generateWhiteSpace) $ \(ws1, ws2) ->-                    parseShouldSucceedAs card ("card" ++ ws1 ++ a ++ ws2 ++ "{}") $ Card e (Block [])--        it "succeeds on empty cards with whitespace between the brackets" $ do-            forAll generateCardName $ \(a, e) ->-                forAll generateWhiteSpace $ \ws ->-                    parseShouldSucceedAs card ("card" ++ a ++ "{" ++ ws ++ "}") $ Card e (Block [])--        it "fails on any card with an empty body" $ do-            forAll generateCardName $ \(a, _) ->-                forAll generateWhiteSpace $ \ws ->-                    shouldFail card ("card" ++ a ++ ws)--        it "succeeds on this complicated example" $ do-            parseShouldSucceedAs card ("card complicated {\n  alternatives $(HOST) shared\n  hello l-> goodbye\n into $(HOME)\n  outof depot\n  spark card othercard\n  kind link\n  {\n    one c-> more\n    source -> destination\n    file\n  }\n}")-                $ Card "complicated" $ Block-                    [-                      Alternatives ["$(HOST)", "shared"]-                    , Deploy "hello" "goodbye" (Just LinkDeployment)-                    , IntoDir "$(HOME)"-                    , OutofDir "depot"-                    , SparkOff (CardName (CardNameReference "othercard"))-                    , DeployKindOverride LinkDeployment-                    , Block [-                              Deploy "one" "more" (Just CopyDeployment)-                            , Deploy "source" "destination" Nothing-                            , Deploy "file" "file" Nothing-                            ]-                    ]--    describe "declarations" $ do-        it "succeeds for generated declarations'" $ do-            pending-        let s = parseShouldSucceedAs declarations-        it "succeeds for these cases" $ do-            s "into dir;outof dir" [IntoDir "dir", OutofDir "dir"]--    describe "declaration" $ do-        it "succeeds for generated declarations" $ do-            pending-        let s = parseShouldSucceedAs declaration-        it "succeeds for these cases" $ do-            s "into directory" (IntoDir "directory")-            s "outof \"other directory\"" (OutofDir "other directory")-            s "{}" (Block [])-            s "{{{};{};{}}}" (Block [Block [Block [], Block [], Block[]]])-            s "\"hi i'm a file\"c->iamthedestination" (Deploy "hi i'm a file" "iamthedestination" (Just CopyDeployment))--    describe "block" $ do-        it "succeeds for empty blocks" $ do-            parseShouldSucceedAs block "{}" (Block [])-        it "succeeds for a doubly nested empty block" $ do-            parseShouldSucceedAs block "{{}}" (Block [Block []])-        it "succeeds for a triply nested empty block" $ do-            parseShouldSucceedAs block "{{{}}}" (Block [Block [Block []]])--        let s = parseShouldSucceedAs block-        it "succeeds for these cases" $ do-            s "{into ~;bashrc -> .bashrc}" (Block [IntoDir "~", Deploy "bashrc" ".bashrc" Nothing])-            s "{\n    into \"~\"\n    \"xmonad\" -> \".xmonad\"\n}" (Block [IntoDir "~", Deploy "xmonad" ".xmonad" Nothing])--    describe "sparkOff" $ do-        it "succeeds for generated sparkOff declarations" $ do-            pending--        let s f g = parseShouldSucceedAs sparkOff f (SparkOff $ CardName $ CardNameReference g)-        it "succeeds for these cases" $ do-           s "spark card name" "name"-           s "sparkcardname" "name"-           s "spark card \"name with spaces\"" "name with spaces"--    describe "intoDir" $ do-        it "succeeds for generated into declarations" $ do-            forAll generateLineSpace $ \ls ->-              forAll generateDirectory $ \(d, da) ->-                parseShouldSucceedAs intoDir ("into" ++ ls ++ d) (IntoDir da)--        let s f g = parseShouldSucceedAs intoDir f (IntoDir g)-        it "succeeds for these cases" $ do-            s "into \"bash\"" "bash"-            s "into\t.xmonad" ".xmonad"-            s "into ~" "~"--    describe "outOfDir" $ do-        it "succeeds for generated outof declarations" $ do-            forAll generateLineSpace $ \ls ->-              forAll generateDirectory $ \(d, da) ->-                parseShouldSucceedAs outOfDir ("outof" ++ ls ++ d) (OutofDir da)--        let s f g = parseShouldSucceedAs outOfDir f (OutofDir g)-        it "succeeds for these cases" $ do-            s "outof bash" "bash"-            s "outof\t.xmonad" ".xmonad"--    describe "alternatives" $ do-        it "succeeds for generated alternatives declarations with single spaces" $ do-            forAll (listOf1 generateDirectory) $ \ds ->-              let (des, das) = unzip ds in-              parseShouldSucceedAs alternatives ("alternatives" ++ " " ++ intercalate " " des) (Alternatives das)--    describe "deployment" $ do-        it "succeeds for short deployments"-            pending-        it "succeeds for long deployments"-            pending--    describe "shortDeployment" $ do-        it "succeeds for any filepath with an identity deployment" $ do-            property $ \f -> succeeds filepath f-                ==> parseShouldSucceedAs shortDeployment f (Deploy f f Nothing)--        it "succeeds for generated filepaths with an identity deployment" $ do-            forAll generateFilePath $ \(f,g) -> parseShouldSucceedAs shortDeployment f (Deploy g g Nothing)--        it "succeeds for any directory with an identity deployment" $ do-            property $ \f -> succeeds directory f-                ==> parseShouldSucceedAs shortDeployment f (Deploy f f Nothing)--        it "succeeds for generated directories with an identity deployment" $ do-            forAll generateDirectory $ \(f,g) -> parseShouldSucceedAs shortDeployment f (Deploy g g Nothing)--        let s f = parseShouldSucceedAs shortDeployment f (Deploy f f Nothing)-        it "succeeds as-is for these cases" $ do-            s "file.txt"-            s "xmonad.hs"-            s "/home/user/.bashrc"--    describe "longDeployment" $ do-        it "succeeds for generated long deployments with quoted identifiers" $ do-            forAll generateDeploymentKindSymbol $ \dks ->-              forAll generateLineSpace $ \ls1 ->-                forAll generateLineSpace $ \ls2 ->-                  forAll generateQuotedIdentifier $ \(fp1,fp1a) ->-                    forAll generateQuotedIdentifier $ \(fp2,fp2a) ->-                      case parseWithoutSource deploymentKind dks of-                        Left _   -> fail "There was a problem with parsing the deployment kind"-                        Right dk -> parseShouldSucceedAs longDeployment (fp1 ++ ls1 ++ dks ++ ls2 ++ fp2) (Deploy fp1a fp2a dk)--        it "succeeds for single-space-separated long deployments with gerenated plain identifiers" $ do-            pendingWith "This would go wrong with plain identifiers they can end with \'l\' or \'c\'. Make sure to document this behaviour and write another test with plain identifiers."--        let s f g h i = parseShouldSucceedAs longDeployment f (Deploy g h i)-        it "succeeds for these cases" $ do-            s "\"something with spaces\"c->/home/user/test.txt"-                "something with spaces"-                "/home/user/test.txt"-                (Just CopyDeployment)-            s "\"xmonad.hs\"l-> /home/user/.xmonad/xmonad.hs"-                "xmonad.hs"-                "/home/user/.xmonad/xmonad.hs"-                (Just LinkDeployment)-            s "bashrc\t->\t/home/user/.bashrc"-                "bashrc"-                "/home/user/.bashrc"-                Nothing---    describe "deploymentKind" $ do-        let (-=>) = parseShouldSucceedAs deploymentKind-        it "succeeds for the link deployment kind" $ do-            "l->" -=> Just LinkDeployment-        it "succeeds for the copy deployment kind" $ do-            "c->" -=> Just CopyDeployment-        it "succeeds for the default deployment kind" $ do-            "->" -=> Nothing-        it "fails for anything else" $ do-            property $ \s -> (not $ any (== s) ["l->", "c->", "->"]) ==> shouldFail deploymentKind s--cardReferenceParserTests :: Spec-cardReferenceParserTests = do-    describe "compilerCardReference" $ do-        pend--    describe "deployerCardReference" $ do-        pend--    describe "compiledCardReference" $ do-        pend--    describe "cardReference" $ do-        pend--    describe "cardNameReference" $ do-        pend-        let s f g = parseShouldSucceedAs cardNameReference f (CardNameReference g)-        it "succeeds for these cases" $ do-           s "card name" "name"-           s "cardname" "name"-           s "card \"name with spaces\"" "name with spaces"--    describe "cardFileReference" $ do-        pend-        let s = parseShouldSucceedAs cardFileReference-        it "succeeds for these cases" $ do-            s "file card.sus" (CardFileReference "card.sus" Nothing)-            s "file card.sus name" (CardFileReference "card.sus" $ Just $ CardNameReference "name")--    describe "unprefixedCardFileReference" $ do-        pend---parserBlackBoxTests :: Spec-parserBlackBoxTests = do-    let tr = "test_resources"-    describe "Correct succesful parse examples" $ do-        let dirs = map (tr </>) ["shouldParse", "shouldCompile", "shouldNotCompile"]-        forFileInDirss dirs $ concerningContents $ \f contents -> do-            it f $ parseFromSource sparkFile f contents `shouldSatisfy` isRight--    describe "Correct unsuccesfull parse examples" $ do-        let dirs = map (tr </>) ["shouldNotParse"]-        forFileInDirss dirs $ concerningContents $ \f contents -> do-            it f $ parseFromSource sparkFile f contents `shouldSatisfy` isLeft---toplevelParserTests :: Spec-toplevelParserTests = do-    describe "sparkFile" $ do-        pend--    describe "resetPosition" $ do-        pend
− test/SeedSpec.hs
@@ -1,24 +0,0 @@-module SeedSpec where--import           Compiler.Gen          ()-import           Compiler.TestUtils-import           Compiler.Types-import           PreCompiler-import           Seed-import           System.FilePath.Posix (isAbsolute)-import           Test.Hspec-import           Test.QuickCheck-import           TestUtils--spec :: Spec-spec = parallel $ do-    pureSeedSpec--pureSeedSpec :: Spec-pureSeedSpec = describe "seed" $ do-    it "ensures that sources are absolute if the seed is an absolute path" $ do-        once $ forAll (arbitrary `suchThat` cleanBy cleanFilePath `suchThat` isAbsolute) $ \fp ->-            forAll arbitrary $ \ds ->-                all (\d -> all (isAbsolute) $ deployment_srcs d) $ seed fp ds--    pend
+ test/SuperUserSpark/Bake/Gen.hs view
@@ -0,0 +1,34 @@+{-# OPTIONS_GHC -Wno-orphans #-}++module SuperUserSpark.Bake.Gen where++import TestImport++import SuperUserSpark.Bake.Types+import SuperUserSpark.Compiler.Gen ()++instance GenUnchecked BakeAssignment++instance GenValid BakeAssignment where+    genValid = BakeAssignment <$> genValid <*> genValid++instance GenUnchecked BakeCardReference++instance GenValid BakeCardReference++instance GenUnchecked BakeSettings++instance GenValid BakeSettings where+    genValid = BakeSettings <$> genValid <*> genValid <*> genValid++instance GenUnchecked BakeError++instance GenValid BakeError++instance GenUnchecked AbsP++instance GenValid AbsP++instance GenUnchecked ID++instance GenValid ID
+ test/SuperUserSpark/BakeSpec.hs view
@@ -0,0 +1,172 @@+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TemplateHaskell #-}++module SuperUserSpark.BakeSpec where++import TestImport++import Data.Either (isLeft)+import Data.Maybe (isNothing)++import qualified System.FilePath as FP+import System.Posix.Files (createSymbolicLink)++import SuperUserSpark.Bake+import SuperUserSpark.Bake.Gen ()+import SuperUserSpark.Bake.Internal+import SuperUserSpark.Bake.Types+import SuperUserSpark.Compiler.Types+import SuperUserSpark.OptParse.Gen ()+import SuperUserSpark.Parser.Gen++spec :: Spec+spec = do+    instanceSpec+    bakeSpec++instanceSpec :: Spec+instanceSpec =+    parallel $ do+        eqSpec @BakeAssignment+        genValidSpec @BakeAssignment+        eqSpec @BakeCardReference+        genValidSpec @BakeCardReference+        eqSpec @BakeSettings+        genValidSpec @BakeSettings+        eqSpec @BakeError+        genValidSpec @BakeError+        eqSpec @BakedDeployment+        genValidSpec @BakedDeployment+        jsonSpecOnValid @BakedDeployment+        eqSpec @AbsP+        genValidSpec @AbsP+        eqSpec @(DeploymentDirections AbsP)+        genValidSpec @(DeploymentDirections AbsP)+        jsonSpecOnValid @(DeploymentDirections AbsP)+        eqSpec @ID+        genValidSpec @ID++bakeSpec :: Spec+bakeSpec =+    parallel $ do+        describe "bakeFilePath" $ do+            it "works for these unit test cases without variables" $ do+                let b root fp s = do+                        absp <- AbsP <$> parseAbsFile s+                        rp <- parseAbsDir root+                        runReaderT+                            (runExceptT (bakeFilePath fp))+                            defaultBakeSettings {bakeRoot = rp} `shouldReturn`+                            Right absp+                b "/home/user/hello" "a/b/c" "/home/user/hello/a/b/c"+                b "/home/user/hello" "/home/user/.files/c" "/home/user/.files/c"+            it "works for a simple home-only variable situation" $ do+                forAll genValid $ \root -> do+                    let b home fp s = do+                            absp <- AbsP <$> parseAbsFile s+                            runReaderT+                                (runExceptT (bakeFilePath fp))+                                defaultBakeSettings+                                { bakeRoot = root+                                , bakeEnvironment = [("HOME", home)]+                                } `shouldReturn`+                                Right absp+                    b "/home/user" "~/a/b/c" "/home/user/a/b/c"+                    b "/home" "~/c" "/home/c"+            here <- runIO getCurrentDir+            let sandbox = here </> $(mkRelDir "test_sandbox")+            before_ (ensureDir sandbox) $+                after_ (removeDirRecur sandbox) $ do+                    it+                        "does not follow toplevel links when the completed path is relative" $ do+                        let file = $(mkRelFile "file")+                        let from = $(mkRelDir "from") </> file+                        let to = $(mkRelDir "to") </> file+                        withCurrentDir sandbox $ do+                            ensureDir $ parent $ sandbox </> from+                            writeFile (sandbox </> from) "contents"+                            ensureDir $ parent $ sandbox </> to+                            createSymbolicLink+                                (toFilePath $ sandbox </> from)+                                (toFilePath $ sandbox </> to)+                        let runBake f =+                                runReaderT+                                    (runExceptT f)+                                    (defaultBakeSettings+                                     {bakeRoot = sandbox, bakeEnvironment = []})+                        runBake (bakeFilePath (toFilePath to)) `shouldReturn`+                            (Right $ AbsP $ sandbox </> to)+                        runBake (bakeFilePath (toFilePath from)) `shouldReturn`+                            (Right $ AbsP $ sandbox </> from)+                    it+                        "follows directory-level links when the completed path is relative" $ do+                        let file = $(mkRelFile "file")+                        let fromdir = $(mkRelDir "from")+                        let from = fromdir </> file+                        let todir = $(mkRelDir "to")+                        let to = todir </> file+                        withCurrentDir sandbox $ do+                            ensureDir $ parent $ sandbox </> from+                            writeFile (sandbox </> from) "from contents"+                            ensureDir $ parent $ sandbox </> todir+                            createSymbolicLink+                                (FP.dropTrailingPathSeparator $+                                 toFilePath $ sandbox </> fromdir)+                                (FP.dropTrailingPathSeparator $+                                 toFilePath $ sandbox </> todir)+                            writeFile (sandbox </> to) "to contents"+                        let runBake f =+                                runReaderT+                                    (runExceptT f)+                                    (defaultBakeSettings+                                     {bakeRoot = sandbox, bakeEnvironment = []})+                        runBake (bakeFilePath (toFilePath to)) `shouldReturn`+                            (Right $ AbsP $ sandbox </> from)+                        runBake (bakeFilePath (toFilePath from)) `shouldReturn`+                            (Right $ AbsP $ sandbox </> from)+        describe "defaultBakeSettings" $+            it "is valid" $ isValid defaultBakeSettings+        describe "formatBakeError" $ do+            it "only ever produces valid strings" $+                producesValid formatBakeError+        describe "complete" $ do+            it "only ever produces a valid filepath" $ validIfSucceeds2 complete+            it+                "replaces the home directory as specified for simple home directories and simple paths" $ do+                forAll arbitrary $ \env ->+                    forAll generateWord $ \home ->+                        forAll generateWord $ \fp ->+                            complete (("HOME", home) : env) ("~" FP.</> fp) `shouldBe`+                            Right (home FP.</> fp)+        describe "parseId" $ do+            it "only ever produces valid IDs" $ producesValid parseId+            it "Figures out the home directory in these cases" $ do+                parseId "~" `shouldBe` [Var "HOME"]+                parseId "~/ab" `shouldBe` [Var "HOME", Plain "/ab"]+            it "Works for these cases" $ do+                parseId "" `shouldBe` []+                parseId "file" `shouldBe` [Plain "file"]+                parseId "something$(with)variable" `shouldBe`+                    [Plain "something", Var "with", Plain "variable"]+                parseId "$(one)$(two)$(three)" `shouldBe`+                    [Var "one", Var "two", Var "three"]+        describe "replaceId" $ do+            it "only ever produces valid FilePaths" $ validIfSucceeds2 replaceId+            it "leaves plain ID's unchanged in any environment" $+                forAll arbitrary $ \env ->+                    forAll arbitrary $ \s ->+                        replaceId env (Plain s) `shouldBe` Right s+            it "returns Left if a variable is not in the environment" $+                forAll arbitrary $ \var ->+                    forAll (arbitrary `suchThat` (isNothing . lookup var)) $ \env ->+                        replaceId env (Var var) `shouldSatisfy` isLeft+            it "replaces a variable if it's in the environment" $+                forAll arbitrary $ \var ->+                    forAll arbitrary $ \val ->+                        forAll (arbitrary `suchThat` (isNothing . lookup var)) $ \env1 ->+                            forAll+                                (arbitrary `suchThat` (isNothing . lookup var)) $ \env2 ->+                                replaceId+                                    (env1 ++ [(var, val)] ++ env2)+                                    (Var var) `shouldBe`+                                Right val
+ test/SuperUserSpark/Check/Gen.hs view
@@ -0,0 +1,69 @@+{-# OPTIONS_GHC -Wno-orphans #-}++module SuperUserSpark.Check.Gen where++import TestImport++import SuperUserSpark.Bake.Gen ()+import SuperUserSpark.Check.Types+import SuperUserSpark.Compiler.Gen ()+import SuperUserSpark.Diagnose.Gen ()+import SuperUserSpark.Language.Gen ()++instance GenUnchecked CheckAssignment++instance GenValid CheckAssignment where+    genValid = CheckAssignment <$> genValid <*> genValid++instance GenUnchecked CheckSettings++instance GenValid CheckSettings where+    genValid = CheckSettings <$> genValid++instance GenUnchecked CheckError++instance GenValid CheckError++instance GenUnchecked CheckResult where+    genUnchecked =+        oneof+            [ pure AlreadyDone+            , Ready <$> genUnchecked+            , Dirty <$> genUnchecked <*> genUnchecked <*> genUnchecked+            , Impossible <$> genUnchecked+            ]++instance GenValid CheckResult where+    genValid =+        oneof+            [ pure AlreadyDone+            , Ready <$> genValid+            , Dirty <$> genValid <*> genValid <*> genValid+            , Impossible <$> genValid+            ]++instance GenUnchecked Instruction++instance GenValid Instruction where+    genValid =+        oneof+            [ CopyFile <$> genValid <*> genValid+            , CopyDir <$> genValid <*> genValid+            , LinkFile <$> genValid <*> genValid+            , LinkDir <$> genValid <*> genValid+            ]++instance GenUnchecked CleanupInstruction++instance GenValid CleanupInstruction++instance GenUnchecked DeploymentCheckResult where+    genUnchecked =+        oneof+            [ pure DeploymentDone+            , ReadyToDeploy <$> genUnchecked+            , DirtySituation <$> genUnchecked <*> genUnchecked <*> genUnchecked+            , ImpossibleDeployment <$> genUnchecked+            ]++instance GenValid DeploymentCheckResult
+ test/SuperUserSpark/Check/TestUtils.hs view
@@ -0,0 +1,93 @@+{-# LANGUAGE TemplateHaskell #-}++module SuperUserSpark.Check.TestUtils where++import TestImport++import SuperUserSpark.Bake.Gen ()+import SuperUserSpark.Bake.Types+import SuperUserSpark.Check.Gen ()+import SuperUserSpark.Check.Internal+import SuperUserSpark.Check.Types+import SuperUserSpark.CoreTypes+import SuperUserSpark.Diagnose.Types++-- * Test utils for checkDeployment+shouldBeImpossible' :: DiagnosedDeployment -> Expectation+shouldBeImpossible' dd = checkDeployment dd `shouldSatisfy` impossibleDeployment++shouldBeImpossibleDeployment :: [CheckResult] -> Expectation+shouldBeImpossibleDeployment dd =+    bestResult dd `shouldSatisfy` impossibleDeployment++-- * Test utils for checkSingle+isDirty :: CheckResult -> Bool+isDirty (Dirty _ _ _) = True+isDirty _ = False++isReady :: CheckResult -> Bool+isReady (Ready _) = True+isReady _ = False++isDone :: CheckResult -> Bool+isDone AlreadyDone = True+isDone _ = False++isImpossible :: CheckResult -> Bool+isImpossible (Impossible _) = True+isImpossible _ = False++shouldBeDirty+    :: DiagnosedFp+    -> DiagnosedFp+    -> DeploymentKind+    -> CleanupInstruction+    -> Expectation+shouldBeDirty src dst kind eci =+    case checkSingle src dst kind of+        Dirty _ ins ci -> do+            ci `shouldBe` eci+            let tp = dropTrailingPathSeparator . toFilePath+            case ins of+                CopyFile isrc idst -> do+                    tp isrc `shouldBe` toPath (diagnosedFilePath src)+                    tp idst `shouldBe` toPath (diagnosedFilePath dst)+                    CopyDeployment `shouldBe` kind+                CopyDir isrc idst -> do+                    tp isrc `shouldBe` toPath (diagnosedFilePath src)+                    tp idst `shouldBe` toPath (diagnosedFilePath dst)+                    CopyDeployment `shouldBe` kind+                LinkFile isrc idst -> do+                    tp isrc `shouldBe` toPath (diagnosedFilePath src)+                    tp idst `shouldBe` toPath (diagnosedFilePath dst)+                    LinkDeployment `shouldBe` kind+                LinkDir isrc idst -> do+                    tp isrc `shouldBe` toPath (diagnosedFilePath src)+                    tp idst `shouldBe` toPath (diagnosedFilePath dst)+                    LinkDeployment `shouldBe` kind+        t ->+            expectationFailure $+            unlines+                [ "checkSingle"+                , show src+                , show dst+                , show kind+                , "should be dirty but is"+                , show t+                ]++shouldBeReady :: DiagnosedFp -> DiagnosedFp -> DeploymentKind -> Expectation+shouldBeReady src dst kind = checkSingle src dst kind `shouldSatisfy` isReady++shouldBeDone :: DiagnosedFp -> DiagnosedFp -> DeploymentKind -> Expectation+shouldBeDone src dst kind = checkSingle src dst kind `shouldSatisfy` isDone++shouldBeImpossible :: DiagnosedFp+                   -> DiagnosedFp+                   -> DeploymentKind+                   -> Expectation+shouldBeImpossible src dst kind =+    checkSingle src dst kind `shouldSatisfy` isImpossible++validWith :: Diagnostics -> Gen DiagnosedFp+validWith d = D <$> genValid <*> pure d <*> genValid
+ test/SuperUserSpark/CheckSpec.hs view
@@ -0,0 +1,253 @@+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TemplateHaskell #-}++module SuperUserSpark.CheckSpec where++import TestImport++import Control.Monad (forM_)+import Data.Hashable+import System.FilePath (dropTrailingPathSeparator)+import System.Posix.Files++import SuperUserSpark.Bake.Types+import SuperUserSpark.Check+import SuperUserSpark.Check.Gen ()+import SuperUserSpark.Check.Internal+import SuperUserSpark.Check.TestUtils+import SuperUserSpark.Check.Types+import SuperUserSpark.Compiler.Types+import SuperUserSpark.CoreTypes+import SuperUserSpark.Diagnose.Internal+import SuperUserSpark.Diagnose.TestUtils+import SuperUserSpark.Diagnose.Types+import SuperUserSpark.OptParse.Gen ()+import SuperUserSpark.Parser.Gen ()+import SuperUserSpark.Utils+import TestUtils++spec :: Spec+spec = do+    instanceSpec+    checkSpec++instanceSpec :: Spec+instanceSpec = do+    eqSpec @CheckSettings+    genValidSpec @CheckSettings+    eqSpec @Instruction+    genValidSpec @Instruction+    eqSpec @CleanupInstruction+    genValidSpec @CleanupInstruction+    eqSpec @DeploymentCheckResult+    genValidSpec @DeploymentCheckResult+    eqSpec @CheckResult+    genValidSpec @CheckResult++checkSpec :: Spec+checkSpec =+    parallel $ do+        describe "formatCheckError" $+            it "always produces valid strings" $ producesValid formatCheckError+        checkSingleSpec+        checkDeploymentSpec+        describe "formatDeploymentChecks" $+            it "always produces valid strings" $+            producesValid formatDeploymentChecks+        describe "formatDeploymentCheck" $+            it "always produces valid strings" $+            producesValid formatDeploymentCheck+        describe "formatInstruction" $+            it "always produces valid strings" $ producesValid formatInstruction+        describe "formatCleanupInstruction" $+            it "always produces valid strings" $+            producesValid formatCleanupInstruction++checkDeploymentSpec :: Spec+checkDeploymentSpec = do+    describe "checkDeployment" $ do+        it "always produces valid check results" $+            producesValidsOnValids checkDeployment+        it "says 'impossible' for deployments with an empty list of sources" $ do+            forAll genUnchecked $ \dst ->+                forAll genUnchecked $ \kind ->+                    shouldBeImpossible' $ Deployment (Directions [] dst) kind+        it "says 'impossible' for deployments where all singles are impossible" $ do+            forAll+                (genValid `suchThat`+                 (\(Deployment (Directions srcs dst) kind) ->+                      all (\src -> isImpossible $ checkSingle src dst kind) srcs)) $ \dd ->+                shouldBeImpossible' dd+        it+            "gives the same result as bestResult (just with a better error for empty lists)" $ do+            forAll genValid $ \dd@(Deployment (Directions srcs dst) kind) ->+                case ( bestResult (map (\src -> checkSingle src dst kind) srcs)+                     , checkDeployment dd) of+                    (ImpossibleDeployment r1, ImpossibleDeployment r2) ->+                        length r1 `shouldSatisfy` (<= (length r2))+                    (r1, r2) -> r1 `shouldBe` r2+    describe "bestResult" $ do+        it "always produces valid check results" $+            producesValidsOnValids bestResult+        it "says 'impossible' if all checkresults are impossible" $ do+            forAll+                (genValid `suchThat` all isImpossible)+                shouldBeImpossibleDeployment+        it "says 'done' if the first non-impossible in 'done'" $ do+            forAll+                (genValid `suchThat`+                 (any (not . isImpossible) &&&+                  (isDone . head . dropWhile isImpossible))) $ \dd ->+                bestResult dd `shouldSatisfy` deploymentIsDone+        it "says 'dirty' if the first non-impossible in 'dirty'" $ do+            forAll+                (genValid `suchThat`+                 (any (not . isImpossible) &&&+                  (isDirty . head . dropWhile isImpossible))) $ \dd ->+                bestResult dd `shouldSatisfy` dirtyDeployment+        it "says 'ready' if the first non-impossible in 'ready'" $ do+            forAll+                (genValid `suchThat`+                 (any (not . isImpossible) &&&+                  (isReady . head . dropWhile isImpossible))) $ \dd ->+                bestResult dd `shouldSatisfy` deploymentReadyToDeploy++checkSingleSpec :: Spec+checkSingleSpec =+    describe "checkSingle" $ do+        it "always produces valid CheckResults" $+            producesValidsOnValids3 checkSingle+        it "says 'impossible' if the source does not exist" $ do+            forAll (validWith Nonexistent) $ \src ->+                forAll genValid $ \dst ->+                    forAll genValid $ \kind -> shouldBeImpossible src dst kind+        it+            "says 'ready' if the source is a file and the destination does not exist" $ do+            forAll (validWith IsFile) $ \src ->+                forAll (validWith Nonexistent) $ \dst ->+                    forAll genValid $ \kind -> shouldBeReady src dst kind+        it+            "says 'dirty' if both the source and destination are files and it's a link deployment" $ do+            forAll (validWith IsFile) $ \src ->+                forAll (validWith IsFile) $ \dst ->+                    shouldBeDirty src dst LinkDeployment $+                    CleanFile $ unAbsP $ diagnosedFilePath dst+        it+            "says 'done' if both the source and destination are files and it's a copy deployment and the files are equal" $ do+            forAll genValid $ \src ->+                forAll genValid $ \dst ->+                    forAll genValid $ \h1 ->+                        shouldBeDone+                            (D src IsFile h1)+                            (D dst IsFile h1)+                            CopyDeployment+        it+            "says 'dirty' if both the source and destination are files and it's a copy deployment but the files are unequal" $ do+            forAll genValid $ \src ->+                forAll genValid $ \dst ->+                    forAll genValid $ \h1 ->+                        forAll (genValid `suchThat` (/= h1)) $ \h2 ->+                            shouldBeDirty+                                (D src IsFile h1)+                                (D dst IsFile h2)+                                CopyDeployment $+                            CleanFile $ unAbsP dst+        it+            "says 'dirty' if the source is a file and the destination is a directory" $ do+            forAll (validWith IsFile) $ \src ->+                forAll (validWith IsDirectory) $ \dst ->+                    forAll genValid $ \kind -> do+                        d <- parseAbsDir (toPath $ diagnosedFilePath dst)+                        shouldBeDirty src dst kind $ CleanDirectory d+        it+            "says 'dirty' if the source is a file and the destination is a link for a link deployment but the destination doesn't point to the source" $ do+            forAll (validWith IsFile) $ \src@(D srcp _ _) ->+                forAll (genValid `suchThat` (/= srcp)) $ \l ->+                    forAll (validWith $ IsLinkTo l) $ \dst ->+                        shouldBeDirty src dst LinkDeployment $+                        CleanLink $ unAbsP $ diagnosedFilePath dst+        it+            "says 'done' if the source is a file and the destination is a link for a link deployment and the destination points to the source" $ do+            forAll (validWith IsFile) $ \src@(D srcp _ _) ->+                forAll (validWith $ IsLinkTo srcp) $ \dst ->+                    shouldBeDone src dst LinkDeployment+        it+            "says 'dirty' if the source is a file and the destination is a link for a copy deployment" $ do+            forAll (validWith IsFile) $ \src ->+                forAll genValid $ \l ->+                    forAll (validWith $ IsLinkTo l) $ \dst ->+                        shouldBeDirty src dst CopyDeployment $+                        CleanLink $ unAbsP $ diagnosedFilePath dst+        it+            "says 'ready' if the source is a directory and the destination does not exist" $ do+            forAll (validWith IsDirectory) $ \src ->+                forAll (validWith Nonexistent) $ \dst ->+                    forAll genValid $ \kind -> shouldBeReady src dst kind+        it+            "says 'dirty' if the source is a directory and the destination is a file" $ do+            forAll (validWith IsDirectory) $ \src ->+                forAll (validWith IsFile) $ \dst ->+                    forAll genValid $ \kind ->+                        shouldBeDirty src dst kind $+                        CleanFile $ unAbsP $ diagnosedFilePath dst+        it+            "says 'dirty' if both the source and destination are directories for a link deployment" $ do+            forAll (validWith IsDirectory) $ \src ->+                forAll (validWith IsDirectory) $ \dst -> do+                    d <- parseAbsDir (toPath $ diagnosedFilePath dst)+                    shouldBeDirty src dst LinkDeployment $ CleanDirectory d+        it+            "says 'done' if both the source and destination are directories and it's a copy deployment and the directories are equal" $ do+            forAll genValid $ \src ->+                forAll genValid $ \dst ->+                    forAll genValid $ \h1 ->+                        shouldBeDone+                            (D src IsDirectory h1)+                            (D dst IsDirectory h1)+                            CopyDeployment+        it+            "says 'dirty' if both the source and destination are directories and it's a copy deployment but the directories are unequal" $ do+            forAll genValid $ \src ->+                forAll genValid $ \dst ->+                    forAll genValid $ \h1 ->+                        forAll (genValid `suchThat` (/= h1)) $ \h2 -> do+                            d <- parseAbsDir $ toPath dst+                            shouldBeDirty+                                (D src IsDirectory h1)+                                (D dst IsDirectory h2)+                                CopyDeployment $+                                CleanDirectory d+        it+            "says 'dirty' if the source is a directory and the destination is a link for a link deployment but the destination doesn't point to the source" $ do+            forAll (validWith IsDirectory) $ \src@(D srcp _ _) ->+                forAll (genValid `suchThat` (/= srcp)) $ \l ->+                    forAll (validWith $ IsLinkTo l) $ \dst ->+                        shouldBeDirty src dst LinkDeployment $+                        CleanLink $ unAbsP $ diagnosedFilePath dst+        it+            "says 'done' if the source is a directory and the destination is a link for a link deployment and the destination points to the source" $ do+            forAll (validWith IsDirectory) $ \src@(D srcp _ _) ->+                forAll (validWith $ IsLinkTo srcp) $ \dst ->+                    shouldBeDone src dst LinkDeployment+        it+            "says 'dirty' if the source is a directory and the destination is a link for a copy deployment" $ do+            forAll (validWith IsDirectory) $ \src ->+                forAll genValid $ \l ->+                    forAll (validWith $ IsLinkTo l) $ \dst ->+                        shouldBeDirty src dst CopyDeployment $+                        CleanLink $ unAbsP $ diagnosedFilePath dst+        it "says 'dirty' if the source is a link" $ do+            forAll genValid $ \l ->+                forAll (validWith $ IsLinkTo l) $ \src ->+                    forAll genValid $ \dst ->+                        forAll genValid $ \kind ->+                            shouldBeImpossible src dst kind+        it "says 'dirty' for a weird source" $ do+            forAll (validWith IsWeird) $ \src ->+                forAll genValid $ \dst ->+                    forAll genValid $ \kind -> shouldBeImpossible src dst kind+        it "says 'dirty' for a weird destination" $ do+            forAll genValid $ \src ->+                forAll (validWith IsWeird) $ \dst ->+                    forAll genValid $ \kind -> shouldBeImpossible src dst kind+        it "works for these unit tests" $ do pending
+ test/SuperUserSpark/Compiler/Gen.hs view
@@ -0,0 +1,60 @@+{-# OPTIONS_GHC -Wno-orphans #-}++module SuperUserSpark.Compiler.Gen where++import TestImport++import SuperUserSpark.Compiler.Types+import SuperUserSpark.Language.Gen ()+import SuperUserSpark.PreCompiler.Gen ()++instance GenUnchecked CompileAssignment++instance GenValid CompileAssignment where+    genValid = CompileAssignment <$> genValid <*> genValid <*> genValid+++instance GenUnchecked StrongCardFileReference++instance GenValid StrongCardFileReference+++instance GenUnchecked CompileSettings++instance GenValid CompileSettings where+    genValid = CompileSettings <$> genValid <*> genValid+++instance GenUnchecked a =>+         GenUnchecked (Deployment a) where+    genUnchecked = Deployment <$> genUnchecked <*> genUnchecked++instance GenValid a =>+         GenValid (Deployment a) where+    genValid = Deployment <$> genValid <*> genValid++instance GenUnchecked a =>+         GenUnchecked (DeploymentDirections a)++instance GenValid a =>+         GenValid (DeploymentDirections a) where+    genValid = Directions <$> genValid <*> genValid++instance GenUnchecked PrefixPart++instance GenValid PrefixPart+++instance GenUnchecked CompilerState++instance GenValid CompilerState++instance GenUnchecked CompileError where+    genUnchecked =+        oneof+            [ PreCompileErrors <$> genUnchecked+            , DuringCompilationError <$> genUnchecked+            ]++instance GenValid CompileError+
+ test/SuperUserSpark/Compiler/TestUtils.hs view
@@ -0,0 +1,95 @@+module SuperUserSpark.Compiler.TestUtils where++import TestImport++import Data.Either (isLeft, isRight)++import SuperUserSpark.Compiler.Internal+import SuperUserSpark.Compiler.Types+import SuperUserSpark.Language.Types+import SuperUserSpark.PreCompiler+import SuperUserSpark.PreCompiler.Types++runPreCompiler :: Precompiler () -> [PreCompileError]+runPreCompiler pc = runIdentity $ execWriterT pc++cleanBy :: (a -> Precompiler ()) -> a -> Bool+cleanBy func a = null $ runPreCompiler $ func a++declarationClean :: Declaration -> IO ()+declarationClean d = d `shouldSatisfy` cleanBy cleanDeclaration++declarationDirty :: Declaration -> IO ()+declarationDirty d = d `shouldNotSatisfy` cleanBy cleanDeclaration++filePathDirty :: FilePath -> IO ()+filePathDirty fp = fp `shouldNotSatisfy` cleanBy cleanFilePath++filePathClean :: FilePath -> IO ()+filePathClean fp = fp `shouldSatisfy` cleanBy cleanFilePath++runPureCompiler :: CompileSettings -> PureCompiler a -> Either CompileError a+runPureCompiler c func = runIdentity $ runReaderT (runExceptT func) c++runInternalCompiler+    :: [Declaration]+    -> CompilerState+    -> CompileSettings+    -> Either CompileError (CompilerState, ([RawDeployment], [CardReference]))+runInternalCompiler ds s c =+    runPureCompiler c $ runWriterT $ execStateT (compileDecs ds) s++compileSingleDec+    :: Declaration+    -> CompilerState+    -> CompileSettings+    -> Either CompileError (CompilerState, ([RawDeployment], [CardReference]))+compileSingleDec d = runInternalCompiler [d]++compilationShouldSucceed :: [Declaration]+                         -> CompilerState+                         -> CompileSettings+                         -> IO ()+compilationShouldSucceed ds s c =+    runInternalCompiler ds s c `shouldSatisfy` isRight++compilationShouldFail :: [Declaration]+                      -> CompilerState+                      -> CompileSettings+                      -> IO ()+compilationShouldFail ds s c = runInternalCompiler ds s c `shouldSatisfy` isLeft++singleShouldFail :: CompileSettings -> CompilerState -> Declaration -> IO ()+singleShouldFail c s d = compilationShouldFail [d] s c++shouldCompileTo :: CompileSettings+                -> CompilerState+                -> [Declaration]+                -> [RawDeployment]+                -> IO ()+shouldCompileTo c s ds eds = do+    compilationShouldSucceed ds s c+    let Right (_, (ads, crs)) = runInternalCompiler ds s c+    ads `shouldBe` eds+    crs `shouldSatisfy` null++singleShouldCompileTo :: CompileSettings+                      -> CompilerState+                      -> Declaration+                      -> RawDeployment+                      -> IO ()+singleShouldCompileTo c s d eds = shouldCompileTo c s [d] [eds]++shouldResultInState :: CompileSettings+                    -> CompilerState+                    -> Declaration+                    -> CompilerState+                    -> IO ()+shouldResultInState c s d es = do+    compilationShouldSucceed [d] s c+    let Right (as, _) = runInternalCompiler [d] s c+    as `shouldBe` es++-- Filepath utils+containsNewlineCharacter :: String -> Bool+containsNewlineCharacter f = any (\c -> elem c f) ['\n', '\r']
+ test/SuperUserSpark/CompilerSpec.hs view
@@ -0,0 +1,422 @@+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TemplateHaskell #-}++module SuperUserSpark.CompilerSpec where++import TestImport++import Data.Either (isLeft, isRight)+import Data.List (isPrefixOf)+import System.FilePath.Posix ((<.>))++import SuperUserSpark.Compiler+import SuperUserSpark.Compiler.Gen ()+import SuperUserSpark.Compiler.Internal+import SuperUserSpark.Compiler.TestUtils+import SuperUserSpark.Compiler.Types+import SuperUserSpark.Compiler.Utils+import SuperUserSpark.CoreTypes+import SuperUserSpark.Language.Gen ()+import SuperUserSpark.Language.Types+import SuperUserSpark.OptParse.Gen ()+import SuperUserSpark.PreCompiler+import SuperUserSpark.Utils+import TestUtils++spec :: Spec+spec = do+    parallel $ do+        instanceSpec+        compileSpec+        singleCompileDecSpec+        precompileSpec+        compileUnitSpec+        utilsSpec+    hopTests+    exactTests+    compilerBlackBoxTests++precompileSpec :: Spec+precompileSpec = describe "pre-compilation" $ do cleanContentSpec++cleanContentSpec :: Spec+cleanContentSpec = do+    let validFp = genValid `suchThat` cleanBy cleanFilePath+    describe "cleanCard" $ do+        it "doesn't report any card with valid content and a valid name" $ do+            forAll (genValid `suchThat` cleanBy cleanCardName) $ \cn ->+                forAll (genValid `suchThat` cleanBy cleanDeclaration) $ \cc ->+                    Card cn cc `shouldSatisfy` cleanBy cleanCard+    describe "cleanCardName" $ do+        pend+        it "doesn't report an emty card name" $ do+            "" `shouldSatisfy` cleanBy cleanCardName+        it "reports card names with newlines" $ do+            forAll (genValid `suchThat` containsNewlineCharacter) $ \s ->+                s `shouldNotSatisfy` cleanBy cleanCardName+    describe "cleanDeclaration" $ do+        describe "Deploy" $ do+            it "doesn't report Deploy declarations with valid filepaths" $ do+                forAll validFp $ \src ->+                    forAll validFp $ \dst ->+                        forAll genValid $ \kind ->+                            Deploy src dst kind `shouldSatisfy`+                            cleanBy cleanDeclaration+            it "reports Deploy declarations with an invalid source" $ do+                forAll (genValid `suchThat` (not . cleanBy cleanFilePath)) $ \src ->+                    forAll validFp $ \dst ->+                        forAll genValid $ \kind ->+                            Deploy src dst kind `shouldNotSatisfy`+                            cleanBy cleanDeclaration+            it "reports Deploy declarations with an invalid destination" $ do+                forAll validFp $ \src ->+                    forAll (genValid `suchThat` (not . cleanBy cleanFilePath)) $ \dst ->+                        forAll genValid $ \kind ->+                            Deploy src dst kind `shouldNotSatisfy`+                            cleanBy cleanDeclaration+            pend+        describe "SparkOff" $ do+            it "reports SparkOff declarations with an invalid card reference" $ do+                forAll (genValid `suchThat` (not . cleanBy cleanCardReference)) $ \cr ->+                    SparkOff cr `shouldNotSatisfy` cleanBy cleanDeclaration+            it+                "doesn't report SparkOff declarations with a valid card reference" $ do+                forAll (genValid `suchThat` cleanBy cleanCardReference) $ \cr ->+                    SparkOff cr `shouldSatisfy` cleanBy cleanDeclaration+            pend+        describe "IntoDir" $ do+            it "reports IntoDir declarations with an invalid filepath" $ do+                forAll (genValid `suchThat` (not . cleanBy cleanFilePath)) $ \fp ->+                    IntoDir fp `shouldNotSatisfy` cleanBy cleanDeclaration+            it "doesn't report IntoDir declarations with a valid filepath" $ do+                forAll (genValid `suchThat` cleanBy cleanFilePath) $ \fp ->+                    IntoDir fp `shouldSatisfy` cleanBy cleanDeclaration+            pend+        describe "OutofDir" $ do+            it "reports OutofDir declarations with an invalid filepath" $ do+                forAll (genValid `suchThat` (not . cleanBy cleanFilePath)) $ \fp ->+                    OutofDir fp `shouldNotSatisfy` cleanBy cleanDeclaration+            it "doesn't report OutofDir declarations with a valid filepath" $ do+                forAll (genValid `suchThat` cleanBy cleanFilePath) $ \fp ->+                    OutofDir fp `shouldSatisfy` cleanBy cleanDeclaration+            pend+        describe "DeployKindOverride" $ do+            it "doesn't report any deployment kind override declarations" $ do+                forAll genValid $ \kind ->+                    DeployKindOverride kind `shouldSatisfy`+                    cleanBy cleanDeclaration+            pend+        describe "Alternatives" $ do+            it+                "reports alternatives declarations with as much as a single invalid filepath" $ do+                forAll (genValid `suchThat` (any $ not . cleanBy cleanFilePath)) $ \fs ->+                    Alternatives fs `shouldNotSatisfy` cleanBy cleanDeclaration+            it "doesn't report alternatives declarations with valid filepaths" $ do+                forAll (genValid `suchThat` (all $ cleanBy cleanFilePath)) $ \fs ->+                    Alternatives fs `shouldSatisfy` cleanBy cleanDeclaration+            pend+        describe "Block" $ do+            it+                "reports block declarations with as much as a single invalid declaration inside" $ do+                forAll+                    (genValid `suchThat` (any $ not . cleanBy cleanDeclaration)) $ \ds ->+                    Block ds `shouldNotSatisfy` cleanBy cleanDeclaration+            it+                "doesn't report any block declarations with valid declarations inside" $ do+                forAll (genValid `suchThat` (all $ cleanBy cleanDeclaration)) $ \ds ->+                    Block ds `shouldSatisfy` cleanBy cleanDeclaration+            pend+    describe "cleanCardReference" $ do+        it "works the same as cleanCardName separately" $ do+            forAll genValid $ \cnr ->+                cleanBy cleanCardNameReference cnr ===+                cleanBy cleanCardReference (CardName cnr)+        it "works the same as cleanCardFile separately" $ do+            forAll genValid $ \cfr ->+                cleanBy cleanCardFileReference cfr ===+                cleanBy cleanCardReference (CardFile cfr)+        pend+    describe "cleanCardNameReference" $ do+        it "reports card name references with an invalid card name" $ do+            forAll (genValid `suchThat` (not . cleanBy cleanCardName)) $ \cn ->+                CardNameReference cn `shouldNotSatisfy`+                cleanBy cleanCardNameReference+        it "doesn't report card name references with a valid card name" $ do+            forAll (genValid `suchThat` cleanBy cleanCardName) $ \cn ->+                CardNameReference cn `shouldSatisfy`+                cleanBy cleanCardNameReference+        pend+    describe "cleanCardFileReference" $ do+        it "reports card file references with an invalid filepath" $ do+            forAll (genValid `suchThat` (not . cleanBy cleanFilePath)) $ \fp ->+                forAll genValid $ \cn ->+                    CardFileReference fp cn `shouldNotSatisfy`+                    cleanBy cleanCardFileReference+        it "reports card file references with an invalid card name" $ do+            forAll genValid $ \fp ->+                forAll+                    (genValid `suchThat` (not . cleanBy cleanCardNameReference)) $ \cn ->+                    CardFileReference fp (Just cn) `shouldNotSatisfy`+                    cleanBy cleanCardFileReference+        it+            "doesn't report card file references with a valid card name reference and valid filepath" $ do+            forAll (genValid `suchThat` cleanBy cleanFilePath) $ \fp ->+                forAll (genValid `suchThat` cleanBy cleanCardNameReference) $ \cn ->+                    CardFileReference fp (Just cn) `shouldSatisfy`+                    cleanBy cleanCardFileReference+        pend+    describe "cleanFilePath" $ do+        it "reports empty an filepath" $ do filePathDirty []+        let nonNull = genValid `suchThat` (not . null)+        it "reports filepaths with newlines" $ do+            forAll (nonNull `suchThat` containsNewlineCharacter) filePathDirty+        let withoutNewlines =+                nonNull `suchThat` (not . containsNewlineCharacter)+        it "reports filepaths with multiple consequtive slashes" $ do+            once $+                forAll+                    (withoutNewlines `suchThat`+                     containsMultipleConsequtiveSlashes)+                    filePathDirty+        let c = filePathClean+        it "doesn't report these valid filepaths" $ do+            c "noextension"+            c ".bashrc"+            c "file.txt"+            c "Some file with spaces.doc"+            c "some/relative/filepath.file"++-- TODO(syd) Use the default config to generate this!+defaultCompilerState :: CompilerState+defaultCompilerState =+    CompilerState+    { stateDeploymentKindLocalOverride = Nothing+    , stateInto = ""+    , stateOutof_prefix = []+    }++instanceSpec :: Spec+instanceSpec =+    parallel $ do+        eqSpec @CompileAssignment+        genValidSpec @CompileAssignment+        eqSpec @CompileSettings+        genValidSpec @CompileSettings+        eqSpec @(Deployment FilePath)+        genValidSpec @(Deployment FilePath)+        jsonSpecOnValid @(Deployment FilePath)+        functorSpec @Deployment+        eqSpec @(DeploymentDirections FilePath)+        genValidSpec @(DeploymentDirections FilePath)+        jsonSpecOnValid @(DeploymentDirections FilePath)+        functorSpec @DeploymentDirections+        eqSpec @PrefixPart+        genValidSpec @PrefixPart+        eqSpec @CompilerState+        genValidSpec @CompilerState+        eqSpec @CompileError+        genValidSpec @CompileError++compileSpec :: Spec+compileSpec = do+    describe "formatCompileError" $+        it "only produces valid strings" $ producesValid formatCompileError++singleCompileDecSpec :: Spec+singleCompileDecSpec =+    describe "compileDec" $ do+        let s = defaultCompilerState+        let c = defaultCompileSettings+        let sc = singleShouldCompileTo c s+        let nonNull = genValid `suchThat` (not . null)+        let validFilePath = nonNull `suchThat` (not . containsNewlineCharacter)+        let easyFilePath = validFilePath `suchThat` (not . isPrefixOf ".")+        let validFp = genValid `suchThat` cleanBy cleanFilePath+        describe "Deploy" $ do+            it+                "uses the exact right text in source and destination when given valid filepaths without a leading dot" $ do+                forAll easyFilePath $ \from ->+                    forAll easyFilePath $ \to ->+                        sc+                            (Deploy from to Nothing)+                            (Deployment (Directions [from] to) LinkDeployment)+            it "handles filepaths with a leading dot correctly" $ do pending+            it+                "figures out the correct paths in these cases with default config and initial state" $ do+                let d = (Deploy "from" "to" $ Just LinkDeployment)+                sc d (Deployment (Directions ["from"] "to") LinkDeployment)+            it "uses the alternates correctly" $ do pending+            it "uses the into's correctly" $ do pending+            it "uses the outof's correctly" $ do pending+            pend+        describe "SparkOff" $ do+            it+                "adds a single card file reference to the list of cards to spark later" $ do+                forAll validFilePath $ \f ->+                    let cr = CardFile $ CardFileReference f Nothing+                        d = SparkOff cr+                    in compileSingleDec d s c `shouldBe` Right (s, ([], [cr]))+            it "adds any card reference to the list" $ do pending+            pend+        let shouldState = shouldResultInState c s+        describe "IntoDir" $ do+            it "adds the given directory to the into state" $ do+                forAll validFp $ \fp ->+                    shouldState (IntoDir fp) $ s {stateInto = fp}+            it "compounds with the input state" $ do+                pendingWith "Change the input state to an explicit list first"+            pend+        describe "OutofDir" $ do+            it "adds the given directory to the outof state" $ do+                forAll validFp $ \fp ->+                    shouldState (OutofDir fp) $+                    s {stateOutof_prefix = [Literal fp]}+            pend+        describe "DeployKindOverride" $ do+            it "modifies the internal deployment kind override" $ do+                forAll genValid $ \kind ->+                    shouldState (DeployKindOverride kind) $+                    s {stateDeploymentKindLocalOverride = Just kind}+            pend+        describe "Block" $ do+            it "uses a separate scope for its sub-compilation" $ do+                forAll genValid $ \ds -> shouldState (Block ds) s+            pend+        describe "Alternatives" $ do+            it+                "adds an alternatives prefix to the outof prefix in the compiler state" $ do+                forAll (listOf validFilePath) $ \fps ->+                    shouldState (Alternatives fps) $+                    s {stateOutof_prefix = [Alts fps]}+            pend++runDefaultImpureCompiler :: ImpureCompiler a -> IO (Either CompileError a)+runDefaultImpureCompiler = flip runReaderT defaultCompileSettings . runExceptT++compileUnitSpec :: Spec+compileUnitSpec =+    describe "compileUnit" $ do+        it "Only ever produces valid results" $+            forAll genValid $ \sets ->+                validIfSucceeds+                    (runIdentity .+                     flip runReaderT sets . runExceptT . compileUnit)++utilsSpec :: Spec+utilsSpec =+    parallel $ do+        describe "initialState" $ it "is valid" $ isValid initialState+        describe "sources" $+            it "only produces valid prefix parts" $ producesValid sources+        describe "resolvePrefix" $+            it "only produces valid paths" $ producesValid resolvePrefix++hopTests :: Spec+hopTests = do+    describe "hop test" $ do+        dir <- runIO $ resolveDir' "test_resources/hop_test"+        let root = dir </> $(mkRelFile "root.sus")+        let hop1 = dir </> $(mkRelDir "hop1dir") </> $(mkRelFile "hop1.sus")+        let hop2 =+                dir </> $(mkRelDir "hop1dir") </> $(mkRelDir "hop2dir") </>+                $(mkRelFile "hop2.sus")+        let hop3 =+                dir </> $(mkRelDir "hop1dir") </> $(mkRelDir "hop2dir") </>+                $(mkRelDir "hop3dir") </>+                $(mkRelFile "hop3.sus")+        it "compiles hop3 correctly" $ do+            r <-+                runDefaultImpureCompiler $+                compileJob $ StrongCardFileReference hop3 Nothing+            r `shouldBe`+                Right+                    [ Deployment+                          (Directions ["z/delta"] "d/three")+                          LinkDeployment+                    ]+        it "compiles hop2 correctly" $ do+            r <-+                runDefaultImpureCompiler $+                compileJob $ StrongCardFileReference hop2 Nothing+            r `shouldBe`+                Right+                    [ Deployment (Directions ["y/gamma"] "c/two") LinkDeployment+                    , Deployment+                          (Directions ["hop3dir/z/delta"] "d/three")+                          LinkDeployment+                    ]+        it "compiles hop1 correctly" $ do+            r <-+                runDefaultImpureCompiler $+                compileJob $ StrongCardFileReference hop1 Nothing+            r `shouldBe`+                Right+                    [ Deployment (Directions ["x/beta"] "b/one") LinkDeployment+                    , Deployment+                          (Directions ["hop2dir/y/gamma"] "c/two")+                          LinkDeployment+                    , Deployment+                          (Directions ["hop2dir/hop3dir/z/delta"] "d/three")+                          LinkDeployment+                    ]+        it "compiles root correctly" $ do+            r <-+                runDefaultImpureCompiler $+                compileJob $ StrongCardFileReference root Nothing+            r `shouldBe`+                Right+                    [ Deployment+                          (Directions ["u/alpha"] "a/zero")+                          LinkDeployment+                    , Deployment+                          (Directions ["hop1dir/x/beta"] "b/one")+                          LinkDeployment+                    , Deployment+                          (Directions ["hop1dir/hop2dir/y/gamma"] "c/two")+                          LinkDeployment+                    , Deployment+                          (Directions+                               ["hop1dir/hop2dir/hop3dir/z/delta"]+                               "d/three")+                          LinkDeployment+                    ]++exactTests :: Spec+exactTests = do+    describe "exact tests" $ do+        dir <- runIO $ resolveDir' "test_resources/exact_compile_test_src"+        forFileInDirss [dir] $ \fp ->+            if fileExtension fp == ".res"+                then return ()+                else do+                    it (toFilePath fp) $ do+                        let orig = fp+                        result <- parseAbsFile $ toFilePath fp <.> "res"+                        ads <-+                            runDefaultImpureCompiler $+                            compileJob $ StrongCardFileReference orig Nothing+                        eds <- runDefaultImpureCompiler $ inputCompiled result+                        ads `shouldBe` eds++hopTestDir :: Path Rel Dir+hopTestDir = $(mkRelDir "hop_test")++compilerBlackBoxTests :: Spec+compilerBlackBoxTests = do+    tr <- runIO $ resolveDir' "test_resources"+    describe "Succesful compile examples" $ do+        let dirs = map (tr </>) [shouldCompileDir, hopTestDir]+        forFileInDirss dirs $ \f -> do+            it (toFilePath f) $ do+                r <-+                    runDefaultImpureCompiler $+                    compileJob $ StrongCardFileReference f Nothing+                r `shouldSatisfy` isRight+    describe "Unsuccesfull compile examples" $ do+        let dirs = map (tr </>) [shouldNotParseDir, shouldNotCompileDir]+        forFileInDirss dirs $ \f -> do+            it (toFilePath f) $ do+                r <-+                    runDefaultImpureCompiler $+                    compileJob $ StrongCardFileReference f Nothing+                r `shouldSatisfy` isLeft
+ test/SuperUserSpark/Deployer/Gen.hs view
@@ -0,0 +1,30 @@+{-# OPTIONS_GHC -Wno-orphans #-}++module SuperUserSpark.Deployer.Gen where++import TestImport++import SuperUserSpark.Check.Gen ()+import SuperUserSpark.Deployer.Types++instance GenUnchecked DeployAssignment++instance GenValid DeployAssignment where+    genValid = DeployAssignment <$> genValid <*> genValid++instance GenUnchecked DeploySettings++instance GenValid DeploySettings where+    genValid =+        DeploySettings <$> genValid <*> genValid <*> genValid <*> genValid+++instance GenUnchecked DeployError++instance GenValid DeployError+++instance GenUnchecked PreDeployment++instance GenValid PreDeployment+
+ test/SuperUserSpark/DeployerSpec.hs view
@@ -0,0 +1,257 @@+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TemplateHaskell #-}++module SuperUserSpark.DeployerSpec where++import TestImport++import System.Posix.Files++import SuperUserSpark.Bake.Types+import SuperUserSpark.Check.Internal+import SuperUserSpark.Check.Types+import SuperUserSpark.Deployer+import SuperUserSpark.Deployer.Gen ()+import SuperUserSpark.Deployer.Internal+import SuperUserSpark.Deployer.Types+import SuperUserSpark.Diagnose.Internal+import SuperUserSpark.Diagnose.Types+import SuperUserSpark.OptParse.Gen ()+import SuperUserSpark.Parser.Gen ()++spec :: Spec+spec = do+    instanceSpec+    deployerSpec+    cleanSpec+    deploymentSpec++instanceSpec :: Spec+instanceSpec =+    parallel $ do+        eqSpec @DeployAssignment+        genValidSpec @DeployAssignment+        eqSpec @DeploySettings+        genValidSpec @DeploySettings+        eqSpec @DeployError+        genValidSpec @DeployError+        eqSpec @PreDeployment+        genValidSpec @PreDeployment++deployerSpec :: Spec+deployerSpec =+    parallel $ do+        describe "defaultDeploySettings" $+            it "is valid" $ isValid defaultDeploySettings+        describe "formatDeployError" $ do+            it "only ever produces valid strings" $+                producesValid formatDeployError++cleanSpec :: Spec+cleanSpec = do+    here <- runIO getCurrentDir+    let sandbox = here </> $(mkRelDir "test_sandbox")+    let setup = ensureDir sandbox+    let teardown = removeDirRecur sandbox+    let clean :: DeploySettings -> CleanupInstruction -> IO ()+        clean sets ci =+            runReaderT (runExceptT $ performClean ci) sets `shouldReturn`+            Right ()+    beforeAll_ setup $+        afterAll_ teardown $ do+            describe "performClean" $ do+                it "doesn't remove this file if that's not in the config" $ do+                    let c =+                            defaultDeploySettings+                            {deploySetsReplaceFiles = False}+                    withCurrentDir sandbox $ do+                        let file = sandbox </> $(mkRelFile "test.txt")+                        let fp = AbsP file+                        writeFile file "This is a test"+                        diagnoseFp fp `shouldReturn` IsFile+                        clean c $ CleanFile file+                        diagnoseFp fp `shouldReturn` IsFile+                        removeFile file+                        diagnoseFp fp `shouldReturn` Nonexistent+                it "removes this file if that's in the config" $ do+                    let c =+                            defaultDeploySettings+                            {deploySetsReplaceFiles = True}+                    withCurrentDir sandbox $ do+                        let file = sandbox </> $(mkRelFile "test.txt")+                        let fp = AbsP file+                        writeFile file "This is a test"+                        diagnoseFp fp `shouldReturn` IsFile+                        clean c $ CleanFile file+                        diagnoseFp fp `shouldReturn` Nonexistent+                it "doesn't remove this directory if that's not in the config" $ do+                    let c =+                            defaultDeploySettings+                            {deploySetsReplaceDirectories = False}+                    withCurrentDir sandbox $ do+                        let dir = sandbox </> $(mkRelDir "testdirectory")+                        let dirf =+                                AbsP $ sandbox </> $(mkRelFile "testdirectory")+                        ensureDir dir+                        diagnoseFp dirf `shouldReturn` IsDirectory+                        clean c $ CleanDirectory dir+                        diagnoseFp dirf `shouldReturn` IsDirectory+                        removeDirRecur dir+                        diagnoseFp dirf `shouldReturn` Nonexistent+                it "removes this directory if that's in the config" $ do+                    let c =+                            defaultDeploySettings+                            {deploySetsReplaceDirectories = True}+                    withCurrentDir sandbox $ do+                        let dir = sandbox </> $(mkRelDir "testdirectory")+                        let dirf =+                                AbsP $ sandbox </> $(mkRelFile "testdirectory")+                        ensureDir dir+                        diagnoseFp dirf `shouldReturn` IsDirectory+                        clean c $ CleanDirectory dir+                        diagnoseFp dirf `shouldReturn` Nonexistent+                it "doesn't remove this link if that's not in the config" $ do+                    let c =+                            defaultDeploySettings+                            {deploySetsReplaceLinks = False}+                    withCurrentDir sandbox $ do+                        let link_ = sandbox </> $(mkRelFile "testlink")+                        let link_' = AbsP link_+                        let file_ = sandbox </> $(mkRelFile "testfile")+                        let file_' = AbsP file_+                        writeFile file_ "This is a test"+                        createSymbolicLink (toFilePath file_) (toFilePath link_)+                        diagnoseFp link_' `shouldReturn` IsLinkTo file_'+                        clean c $ CleanLink link_+                        diagnoseFp link_' `shouldReturn` IsLinkTo file_'+                        removeLink $ toFilePath link_+                        diagnoseFp link_' `shouldReturn` Nonexistent+                        removeFile file_+                        diagnoseFp file_' `shouldReturn` Nonexistent+                it+                    "removes this link with an existent source if that's in the config" $ do+                    let c =+                            defaultDeploySettings+                            {deploySetsReplaceLinks = True}+                    withCurrentDir sandbox $ do+                        let link_ = sandbox </> $(mkRelFile "testlink")+                        let link_' = AbsP link_+                        let file_ = sandbox </> $(mkRelFile "testfile")+                        let file_' = AbsP file_+                        writeFile file_ "This is a test"+                        createSymbolicLink (toFilePath file_) (toFilePath link_)+                        diagnoseFp link_' `shouldReturn` IsLinkTo file_'+                        clean c $ CleanLink link_+                        diagnoseFp link_' `shouldReturn` Nonexistent+                        diagnoseFp file_' `shouldReturn` IsFile+                        removeFile file_+                        diagnoseFp file_' `shouldReturn` Nonexistent+                it+                    "removes this link with a nonexistent source if that's in the config" $ do+                    let c =+                            defaultDeploySettings+                            {deploySetsReplaceLinks = True}+                    withCurrentDir sandbox $ do+                        let link_ = sandbox </> $(mkRelFile "testlink")+                        let link_' = AbsP link_+                        let file_ = sandbox </> $(mkRelFile "testfile")+                        let file_' = AbsP file_+                        createSymbolicLink (toFilePath file_) (toFilePath link_)+                        diagnoseFp link_' `shouldReturn` IsLinkTo file_'+                        diagnoseFp file_' `shouldReturn` Nonexistent+                        clean c $ CleanLink link_+                        diagnoseFp link_' `shouldReturn` Nonexistent+                        diagnoseFp file_' `shouldReturn` Nonexistent++deploymentSpec :: Spec+deploymentSpec = do+    here <- runIO $ getCurrentDir+    let sandbox = here </> $(mkRelDir "test_sandbox")+    let setup = ensureDir sandbox+    let teardown = removeDirRecur sandbox+    beforeAll_ setup $+        afterAll_ teardown $ do+            describe "performCopyFile" $ do+                it "succcesfully copies this file" $ do+                    withCurrentDir sandbox $ do+                        let src = sandbox </> $(mkRelFile "testfile")+                        let src' = AbsP src+                        let dst = sandbox </> $(mkRelFile "testcopy")+                        let dst' = AbsP dst+                        writeFile src "This is a file."+                        diagnoseFp src' `shouldReturn` IsFile+                        diagnoseFp dst' `shouldReturn` Nonexistent+                        -- Under test+                        performCopyFile src dst+                        diagnoseFp src' `shouldReturn` IsFile+                        diagnoseFp dst' `shouldReturn` IsFile+                        dsrc <- diagnoseAbsP src'+                        ddst <- diagnoseAbsP dst'+                        diagnosedHashDigest ddst `shouldBe`+                            diagnosedHashDigest dsrc+                        removeFile src+                        removeFile dst+                        diagnoseFp src' `shouldReturn` Nonexistent+                        diagnoseFp dst' `shouldReturn` Nonexistent+            describe "performCopyDir" $ do+                it "succcesfully copies this directory" $ do+                    withCurrentDir sandbox $ do+                        let src = sandbox </> $(mkRelDir "testdir")+                        let src' = AbsP $ sandbox </> $(mkRelFile "testdir")+                        let dst = sandbox </> $(mkRelDir "testcopy")+                        let dst' = AbsP $ sandbox </> $(mkRelFile "testcopy")+                        ensureDir src+                        diagnoseFp src' `shouldReturn` IsDirectory+                        diagnoseFp dst' `shouldReturn` Nonexistent+                        -- Under test+                        performCopyDir src dst+                        diagnoseFp src' `shouldReturn` IsDirectory+                        diagnoseFp dst' `shouldReturn` IsDirectory+                        dsrc <- diagnoseAbsP src'+                        ddst <- diagnoseAbsP dst'+                        diagnosedHashDigest ddst `shouldBe`+                            diagnosedHashDigest dsrc+                        removeDirRecur src+                        removeDirRecur dst+                        diagnoseFp src' `shouldReturn` Nonexistent+                        diagnoseFp dst' `shouldReturn` Nonexistent+            describe "performLinkFile" $ do+                it "successfully links this file" $ do+                    withCurrentDir sandbox $ do+                        let src = sandbox </> $(mkRelFile "testfile")+                        let src' = AbsP src+                        let dst = sandbox </> $(mkRelFile "testlink")+                        let dst' = AbsP dst+                        diagnoseFp src' `shouldReturn` Nonexistent+                        diagnoseFp dst' `shouldReturn` Nonexistent+                        writeFile src "This is a test."+                        diagnoseFp src' `shouldReturn` IsFile+                        diagnoseFp dst' `shouldReturn` Nonexistent+                        -- Under test+                        performLinkFile src dst+                        diagnoseFp src' `shouldReturn` IsFile+                        diagnoseFp dst' `shouldReturn` IsLinkTo src'+                        removeFile src+                        removeLink $ toFilePath dst+                        diagnoseFp src' `shouldReturn` Nonexistent+                        diagnoseFp dst' `shouldReturn` Nonexistent+            describe "performLinkDir" $ do+                it "successfully links this directory" $ do+                    withCurrentDir sandbox $ do+                        let src = sandbox </> $(mkRelDir "testdir")+                        let src' = AbsP $ sandbox </> $(mkRelFile "testdir")+                        let dst = sandbox </> $(mkRelDir "testlink")+                        let dst' = AbsP $ sandbox </> $(mkRelFile "testlink")+                        diagnoseFp src' `shouldReturn` Nonexistent+                        diagnoseFp dst' `shouldReturn` Nonexistent+                        ensureDir src+                        diagnoseFp src' `shouldReturn` IsDirectory+                        diagnoseFp dst' `shouldReturn` Nonexistent+                        -- Under test+                        performLinkDir src dst+                        diagnoseFp src' `shouldReturn` IsDirectory+                        diagnoseFp dst' `shouldReturn` IsLinkTo src'+                        removeDirRecur src+                        removeLink $ dropTrailingPathSeparator $ toFilePath dst+                        diagnoseFp src' `shouldReturn` Nonexistent+                        diagnoseFp dst' `shouldReturn` Nonexistent
+ test/SuperUserSpark/Diagnose/Gen.hs view
@@ -0,0 +1,37 @@+{-# OPTIONS_GHC -Wno-orphans #-}++module SuperUserSpark.Diagnose.Gen where++import TestImport++import SuperUserSpark.Bake.Gen ()+import SuperUserSpark.Diagnose.Types+import SuperUserSpark.Compiler.Gen ()+import SuperUserSpark.Language.Gen ()++instance GenUnchecked DiagnoseAssignment++instance GenValid DiagnoseAssignment where+    genValid = DiagnoseAssignment <$> genValid <*> genValid++instance GenUnchecked DiagnoseSettings++instance GenValid DiagnoseSettings where+    genValid = DiagnoseSettings <$> genValid++instance GenUnchecked DiagnoseError++instance GenValid DiagnoseError++instance GenUnchecked HashDigest++instance GenValid HashDigest++instance GenUnchecked Diagnostics++instance GenValid Diagnostics++instance GenUnchecked DiagnosedFp++instance GenValid DiagnosedFp where+    genValid = D <$> genValid <*> genValid <*> genValid
+ test/SuperUserSpark/Diagnose/TestUtils.hs view
@@ -0,0 +1,36 @@+{-# LANGUAGE TemplateHaskell #-}++module SuperUserSpark.Diagnose.TestUtils where++import TestImport++import SuperUserSpark.Bake.Gen ()+import SuperUserSpark.Bake.Types+import SuperUserSpark.Check.Gen ()+import SuperUserSpark.Check.Internal+import SuperUserSpark.Check.Types+import SuperUserSpark.CoreTypes+import SuperUserSpark.Diagnose.Types+++-- TODO, the code should be able to handle 'genValid', but currenty it can't.+absPathIn :: Path Abs Dir -> Gen AbsP+absPathIn sandbox =+    scale (+ 5) $ do+        fp <- genListOf $ elements $ ['a' .. 'z'] ++ ['A' .. 'Z']+        case parseRelFile fp of+            Nothing -> absPathIn sandbox+            Just p -> pure $ AbsP $ sandbox </> p++absFileIn :: Path Abs Dir -> Gen (Path Abs File, AbsP)+absFileIn sandbox = do+    p <- absPathIn sandbox+    pure (unAbsP p, p)++absDirIn :: Path Abs Dir -> Gen (Path Abs Dir, AbsP)+absDirIn sandbox = do+    p <- absPathIn sandbox+    let u = toPath p+    case parseAbsDir u of+        Nothing -> absDirIn sandbox+        Just ad -> pure (ad, p)
+ test/SuperUserSpark/DiagnoseSpec.hs view
@@ -0,0 +1,270 @@+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TemplateHaskell #-}++module SuperUserSpark.DiagnoseSpec where++import TestImport++import Control.Monad (forM_)+import Data.Hashable+import System.FilePath (dropTrailingPathSeparator)+import System.Posix.Files++import SuperUserSpark.Bake.Types+import SuperUserSpark.Check+import SuperUserSpark.Check.Gen ()+import SuperUserSpark.Check.Internal+import SuperUserSpark.Check.TestUtils+import SuperUserSpark.Check.Types+import SuperUserSpark.Compiler.Types+import SuperUserSpark.CoreTypes+import SuperUserSpark.Diagnose+import SuperUserSpark.Diagnose.Gen ()+import SuperUserSpark.Diagnose.Internal+import SuperUserSpark.Diagnose.TestUtils+import SuperUserSpark.Diagnose.Types+import SuperUserSpark.OptParse.Gen ()+import SuperUserSpark.Parser.Gen ()+import SuperUserSpark.Utils+import TestUtils++spec :: Spec+spec = do+    instanceSpec+    diagnoseSpec+    hashSpec++instanceSpec :: Spec+instanceSpec = do+    eqSpec @CheckSettings+    genValidSpec @CheckSettings+    eqSpec @Diagnostics+    genValidSpec @Diagnostics+    eqSpec @DiagnosedFp+    genValidSpec @DiagnosedFp+    eqSpec @DiagnosedDeployment+    genValidSpec @DiagnosedDeployment++diagnoseSpec :: Spec+diagnoseSpec = do+    describe "formatDiagnoseError" $+        it "always produces valid strings" $ producesValid formatDiagnoseError+    sandbox <- runIO $ resolveDir' "test_sandbox"+    let setup = ensureDir sandbox+    let teardown = removeDirRecur sandbox+    beforeAll_ setup $+        afterAll_ teardown $ do+            describe "diagnoseDeployment" $ do+                it+                    "retains the filepaths and deploymentkind that it diagnoses for valid filepaths" $ do+                    forAll genValid $ \d@(Deployment (Directions srcs dst) kind) -> do+                        (Deployment (Directions dsrcs ddst) dkind) <-+                            diagnoseDeployment d+                        map diagnosedFilePath dsrcs `shouldBe` srcs+                        diagnosedFilePath ddst `shouldBe` dst+                        dkind `shouldBe` kind+                pend+            describe "diagnose" $ do+                it "retains the filepath that it diagnoses for valid AbsPath's" $ do+                    forAll genValid $ \fp -> do+                        (D dfp _ _) <- diagnoseAbsP fp+                        dfp `shouldBe` fp+                pend+            describe "diagnoseFp" $ do+                let expect s ls = do+                        rs <-+                            forM ls $ \(a, b) -> do+                                r <- diagnoseFp a+                                pure (a, r, b)+                        unless (all (\(a, r, b) -> r == b) rs) $+                            let nice (a, r, b) =+                                    unlines+                                        [ unwords+                                              [ "Unexpected results at stage"+                                              , show s+                                              ]+                                        , unwords ["path:", show $ toPath a]+                                        , unwords+                                              [ "stripped:"+                                              , show $+                                                (stripDir sandbox $ unAbsP a :: Maybe (Path Rel File))+                                              ]+                                        , unwords ["expected:", show b]+                                        , unwords ["real:", show r]+                                        ]+                            in expectationFailure $ unlines $ map nice rs+                it "figures out this link that points to something that exists" $ do+                    let file = sandbox </> $(mkRelFile "file")+                    let file' = AbsP file+                    let link = sandbox </> $(mkRelFile "link")+                    let link' = AbsP link+                    expect "before" [(file', Nonexistent), (link', Nonexistent)]+                    writeFile file "This is a test"+                    expect+                        "after file creation"+                        [(file', IsFile), (link', Nonexistent)]+                    createSymbolicLink (toFilePath file) (toFilePath link)+                    expect+                        "after link creation"+                        [(file', IsFile), (link', IsLinkTo file')]+                    removeLink $ toFilePath link+                    expect+                        "after link removal"+                        [(file', IsFile), (link', Nonexistent)]+                    removeFile file+                    expect+                        "after file removal"+                        [(file', Nonexistent), (link', Nonexistent)]+                it+                    "figures out this link that points to something that does not exist" $ do+                    let file = sandbox </> $(mkRelFile "file")+                    let file' = AbsP file+                    let link = sandbox </> $(mkRelFile "link")+                    let link' = AbsP link+                    expect "before" [(file', Nonexistent), (link', Nonexistent)]+                    createSymbolicLink (toFilePath file) (toFilePath link)+                    expect+                        "after link creation"+                        [(file', Nonexistent), (link', IsLinkTo file')]+                    removeLink $ toFilePath link+                    expect "after" [(file', Nonexistent), (link', Nonexistent)]+                it+                    "figures out that a thing in a nonexistent dir is nonexistent" $ do+                    let file = sandbox </> $(mkRelFile "nonexistent/and/file")+                    let file' = AbsP file+                    diagnoseFp file' `shouldReturn` Nonexistent+                it "figures out a file" $ do+                    forAll (absFileIn sandbox) $ \(file, file') -> do+                        diagnoseFp file' `shouldReturn` Nonexistent+                        ensureDir $ parent file+                        writeFile file "This is a test"+                        diagnoseFp file' `shouldReturn` IsFile+                        removeFile file+                        diagnoseFp file' `shouldReturn` Nonexistent+                it "figures out a directory" $ do+                    forAll (absDirIn sandbox) $ \(dir, dir') -> do+                        diagnoseFp dir' `shouldReturn` Nonexistent+                        ensureDir dir+                        diagnoseFp dir' `shouldReturn` IsDirectory+                        removeDirRecur dir+                        diagnoseFp dir' `shouldReturn` Nonexistent+                it "figures out a symbolic link with an existent destination" $ do+                    forAll (absFileIn sandbox) $ \f@(file, file') ->+                        forAll (absFileIn sandbox `suchThat` (/= f)) $ \(link, link') -> do+                            expect+                                "before"+                                [(file', Nonexistent), (link', Nonexistent)]+                            writeFile file "This is a test"+                            expect+                                "after file creation"+                                [(file', IsFile), (link', Nonexistent)]+                            createSymbolicLink+                                (toFilePath file)+                                (toFilePath link)+                            expect+                                "after link creation"+                                [(file', IsFile), (link', IsLinkTo file')]+                            removeLink $ toFilePath link+                            expect+                                "after link removal"+                                [(file', IsFile), (link', Nonexistent)]+                            removeFile file+                            expect+                                "after"+                                [(file', Nonexistent), (link', Nonexistent)]+                it "figures out a symbolic link with a nonexistent destination" $ do+                    forAll (absFileIn sandbox) $ \f@(file, file') ->+                        forAll (absFileIn sandbox `suchThat` (/= f)) $ \(link, link') -> do+                            expect+                                "before"+                                [(file', Nonexistent), (link', Nonexistent)]+                            createSymbolicLink+                                (toFilePath file)+                                (toFilePath link)+                            expect+                                "after link creation"+                                [(file', Nonexistent), (link', IsLinkTo file')]+                            removeLink $ toFilePath link+                            expect+                                "after"+                                [(file', Nonexistent), (link', Nonexistent)]+                it "figures out that /dev/null is weird" $ do+                    diagnoseFp (AbsP $(mkAbsFile "/dev/null")) `shouldReturn`+                        IsWeird+                it "figures out that /dev/random is weird" $ do+                    diagnoseFp (AbsP $(mkAbsFile "/dev/random")) `shouldReturn`+                        IsWeird++checkDeploymentSpec :: Spec+checkDeploymentSpec = do+    describe "checkDeployment" $ do+        it "always produces valid check results" $+            producesValidsOnValids checkDeployment+        it "says 'impossible' for deployments with an empty list of sources" $ do+            forAll genUnchecked $ \dst ->+                forAll genUnchecked $ \kind ->+                    shouldBeImpossible' $ Deployment (Directions [] dst) kind+        it "says 'impossible' for deployments where all singles are impossible" $ do+            forAll+                (genValid `suchThat`+                 (\(Deployment (Directions srcs dst) kind) ->+                      all (\src -> isImpossible $ checkSingle src dst kind) srcs)) $ \dd ->+                shouldBeImpossible' dd+        it+            "gives the same result as bestResult (just with a better error for empty lists)" $ do+            forAll genValid $ \dd@(Deployment (Directions srcs dst) kind) ->+                case ( bestResult (map (\src -> checkSingle src dst kind) srcs)+                     , checkDeployment dd) of+                    (ImpossibleDeployment r1, ImpossibleDeployment r2) ->+                        length r1 `shouldSatisfy` (<= (length r2))+                    (r1, r2) -> r1 `shouldBe` r2+    describe "bestResult" $ do+        it "always produces valid check results" $+            producesValidsOnValids bestResult+        it "says 'impossible' if all checkresults are impossible" $ do+            forAll+                (genValid `suchThat` all isImpossible)+                shouldBeImpossibleDeployment+        it "says 'done' if the first non-impossible in 'done'" $ do+            forAll+                (genValid `suchThat`+                 (any (not . isImpossible) &&&+                  (isDone . head . dropWhile isImpossible))) $ \dd ->+                bestResult dd `shouldSatisfy` deploymentIsDone+        it "says 'dirty' if the first non-impossible in 'dirty'" $ do+            forAll+                (genValid `suchThat`+                 (any (not . isImpossible) &&&+                  (isDirty . head . dropWhile isImpossible))) $ \dd ->+                bestResult dd `shouldSatisfy` dirtyDeployment+        it "says 'ready' if the first non-impossible in 'ready'" $ do+            forAll+                (genValid `suchThat`+                 (any (not . isImpossible) &&&+                  (isReady . head . dropWhile isImpossible))) $ \dd ->+                bestResult dd `shouldSatisfy` deploymentReadyToDeploy++hashSpec :: Spec+hashSpec = do+    tooManyFilesTest++tooManyFilesTest :: Spec+tooManyFilesTest = do+    sandbox <- runIO $ resolveDir' "test_sandbox"+    let setup = ensureDir sandbox+    let teardown = removeDirRecur sandbox+    let aLot = 20000 :: Int+    let setupALotOfFiles = do+            forM_ [1 .. aLot] $ \i -> do+                f <- parseRelFile $ "file" ++ show i+                writeFile (sandbox </> f) $ "This is file " ++ show i ++ ".\n"+    beforeAll_ setup $+        afterAll_ teardown $ do+            describe "hashFilePath" $ do+                beforeAll_ setupALotOfFiles $ do+                    it+                        ("has no problem with hashing a directory of " +++                         show aLot ++ " files") $ do+                        sb <- resolveFile' "test_sandbox"+                        let d = AbsP sb+                        hashFilePath d `shouldNotReturn` HashDigest (hash ())
+ test/SuperUserSpark/EndToEnd/RegressionSpec.hs view
@@ -0,0 +1,64 @@+{-# LANGUAGE TemplateHaskell #-}++module SuperUserSpark.EndToEnd.RegressionSpec+    ( spec+    ) where++import TestImport++import System.Directory+import System.Environment (withArgs)+import System.Exit (ExitCode(ExitFailure))+import System.Posix.Files++import SuperUserSpark+import SuperUserSpark.Utils++spec :: Spec+spec = do+    linkThenCopySpec++linkThenCopySpec :: Spec+linkThenCopySpec = do+    here <- runIO getCurrentDir+    let sandbox = here </> $(mkRelDir "test_sandbox")+    let setup = ensureDir sandbox+    let teardown = removeDirRecur sandbox+    beforeAll_ setup $+        afterAll_ teardown $ do+            describe "link then copy regression" $ do+                let runSpark args = do+                        putStrLn $ unwords $ "spark" : args+                        withArgs args spark+                it+                    "ensures that deploy fails when there is already a link that points to the file that is being copied." $+                    withCurrentDir sandbox $ do+                        let cf = sandbox </> $(mkRelFile "cardfile.sus")+                        let file = $(mkRelFile "file")+                        let from = sandbox </> $(mkRelDir "from") </> file+                        let to = sandbox </> $(mkRelDir "to") </> file+                        -- Set up the file+                        ensureDir (parent from)+                        writeFile from "contents"+                        -- Set up the first card file+                        writeFile+                            cf+                            "card link { kind link; into to; outof from; file }"+                        runSpark ["parse", toFilePath cf]+                        runSpark ["compile", toFilePath cf]+                        runSpark ["bake", toFilePath cf]+                        runSpark ["check", toFilePath cf]+                        runSpark ["deploy", toFilePath cf]+                        -- Set up the second card file+                        writeFile+                            cf+                            "card link { kind copy; into to; outof from; file }"+                        runSpark ["parse", toFilePath cf]+                        runSpark ["compile", toFilePath cf]+                        runSpark ["bake", toFilePath cf]+                        runSpark ["check", toFilePath cf]+                        runSpark ["deploy", toFilePath cf] `shouldThrow`+                            (\e ->+                                 case e of+                                     ExitFailure _ -> True+                                     _ -> False)
+ test/SuperUserSpark/EndToEndSpec.hs view
@@ -0,0 +1,85 @@+module SuperUserSpark.EndToEndSpec+    ( spec+    ) where++import TestImport hiding ((</>), removeFile, copyFile)++import qualified Prelude as P (writeFile, readFile)++import SuperUserSpark+import SuperUserSpark.Utils+import System.Directory+import System.Environment (withArgs)+import System.Exit (ExitCode(ExitSuccess))+import System.FilePath.Posix ((</>))+import System.Posix.Files++spec :: Spec+spec = do+    helpTextSpec+    regularWorkflowSpec++helpTextSpec :: Spec+helpTextSpec =+    describe "help text test" $ do+        it "shows the help text without crashing" $ do+            withArgs ["--help"] spark `shouldThrow` (== ExitSuccess)++regularWorkflowSpec :: Spec+regularWorkflowSpec = do+    here <- runIO getCurrentDirectory+    let sandbox = here </> "test_sandbox"+    let setup = createDirectoryIfMissing True sandbox+    let teardown = removeDirectoryRecursive sandbox+    let rsc = here </> "test_resources" </> "end-to-end"+    beforeAll_ setup $+        afterAll_ teardown $ do+            describe "standard bash card test" $ do+                let bashrsc = rsc </> "bash.sus"+                let bashrscres = rsc </> "bash.sus.res"+                let cardfile = sandbox </> "bash.sus"+                let up = do+                        copyFile bashrsc cardfile+                        withCurrentDirectory sandbox $ do+                            createDirectoryIfMissing True "bash"+                            withCurrentDirectory "bash" $ do+                                P.writeFile "bash_aliases" "bash_aliases"+                                P.writeFile "bashrc" "bashrc"+                                P.writeFile "bash_profile" "bash_profile"+                let down = do+                        removeFile cardfile+                        withCurrentDirectory sandbox $ do+                            removeDirectoryRecursive "bash"+                beforeAll_ up $+                    afterAll_ down $ do+                        it "parses correcty" $ do+                            withCurrentDirectory sandbox $+                                withArgs ["parse", cardfile] spark `shouldReturn`+                                ()+                        it "compiles correctly" $ do+                            let outfile = sandbox </> "bash.sus.res"+                            withCurrentDirectory sandbox $+                                withArgs+                                    ["compile", cardfile, "--output", outfile]+                                    spark `shouldReturn`+                                ()+                            actual <- P.readFile outfile+                            expected <- P.readFile bashrscres+                            unless (actual == expected) $ expectationFailure $ unlines ["Expected and actual differ:", expected, actual]+                        it "checks without exceptions" $ do+                            withCurrentDirectory sandbox $+                                withArgs ["check", cardfile] spark `shouldReturn`+                                ()+                        it "deploys correctly" $+                            withCurrentDirectory sandbox $ do+                                withArgs ["deploy", cardfile] spark `shouldReturn`+                                    ()+                                let f1 = "subdir" </> ".bashrc"+                                    f2 = "subdir" </> ".bash_aliases"+                                    f3 = "subdir" </> ".bash_profile"+                                P.readFile f1 `shouldReturn` "bashrc"+                                P.readFile f2 `shouldReturn` "bash_aliases"+                                P.readFile f3 `shouldReturn` "bash_profile"+                                removeLink f1+                                removeLink f2+                                removeLink f3
+ test/SuperUserSpark/Language/Gen.hs view
@@ -0,0 +1,64 @@+{-# OPTIONS_GHC -Wno-orphans #-}++module SuperUserSpark.Language.Gen where++import TestImport++import SuperUserSpark.CoreTypes+import SuperUserSpark.Language.Types++instance GenUnchecked SparkFile where+    genUnchecked = SparkFile <$> genUnchecked <*> genUnchecked++instance GenValid SparkFile++instance GenUnchecked Card where+    genUnchecked = Card <$> genUnchecked <*> genUnchecked++instance GenValid Card++instance GenUnchecked Declaration where+    genUnchecked = resize 5 $ sized go+      where+        go 0 =+            oneof+                [ SparkOff <$> genUnchecked+                , Deploy <$> genUnchecked <*> genUnchecked <*> genUnchecked+                , IntoDir <$> genUnchecked+                , OutofDir <$> genUnchecked+                , DeployKindOverride <$> genUnchecked+                , Alternatives <$> genUnchecked+                , Block <$> genUnchecked+                ]+        go n =+            oneof+                [ SparkOff <$> genUnchecked+                , Deploy <$> genUnchecked <*> genUnchecked <*> genUnchecked+                , IntoDir <$> genUnchecked+                , OutofDir <$> genUnchecked+                , DeployKindOverride <$> genUnchecked+                , Alternatives <$> genUnchecked+                , Block <$> listOf (go $ n - 1)+                ]++instance GenValid Declaration++instance GenUnchecked DeploymentKind where+    genUnchecked = elements [LinkDeployment, CopyDeployment]++instance GenValid DeploymentKind++instance GenUnchecked CardNameReference where+    genUnchecked = CardNameReference <$> genUnchecked++instance GenValid CardNameReference++instance GenUnchecked CardFileReference where+    genUnchecked = CardFileReference <$> genUnchecked <*> genUnchecked++instance GenValid CardFileReference++instance GenUnchecked CardReference where+    genUnchecked = oneof [CardFile <$> genUnchecked, CardName <$> genUnchecked]++instance GenValid CardReference
+ test/SuperUserSpark/OptParse/Gen.hs view
@@ -0,0 +1,55 @@+{-# OPTIONS_GHC -Wno-orphans #-}++module SuperUserSpark.OptParse.Gen where++import TestImport++import SuperUserSpark.OptParse.Types++instance GenUnchecked Dispatch++instance GenValid Dispatch++instance GenUnchecked ParseArgs++instance GenValid ParseArgs++instance GenUnchecked CompileArgs++instance GenValid CompileArgs++instance GenUnchecked CompileFlags++instance GenValid CompileFlags++instance GenUnchecked BakeArgs++instance GenValid BakeArgs++instance GenUnchecked BakeFlags++instance GenValid BakeFlags++instance GenUnchecked CheckArgs++instance GenValid CheckArgs++instance GenUnchecked CheckFlags++instance GenValid CheckFlags++instance GenUnchecked DiagnoseArgs++instance GenValid DiagnoseArgs++instance GenUnchecked DiagnoseFlags++instance GenValid DiagnoseFlags++instance GenUnchecked DeployArgs++instance GenValid DeployArgs++instance GenUnchecked DeployFlags++instance GenValid DeployFlags
+ test/SuperUserSpark/Parser/Gen.hs view
@@ -0,0 +1,85 @@+module SuperUserSpark.Parser.Gen where++import TestImport++import Data.List (isSuffixOf)++generateNormalCharacter :: Gen Char+generateNormalCharacter =+    elements $ ['a' .. 'z'] ++ ['A' .. 'Z'] ++ ['0' .. '1']++generateWord :: Gen String+generateWord = listOf1 generateNormalCharacter++generateTab :: Gen Char+generateTab = return '\t'++generateSpace :: Gen Char+generateSpace = return ' '++generateLineFeed :: Gen Char+generateLineFeed = return '\n'++generateCarriageReturn :: Gen Char+generateCarriageReturn = return '\r'++generateLineSpace :: Gen String+generateLineSpace = listOf $ oneof [generateTab, generateSpace]++generateWhiteSpace :: Gen String+generateWhiteSpace =+    listOf $+    oneof [generateTab, generateSpace, generateLineFeed, generateCarriageReturn]++generateWords :: Gen String+generateWords = fmap unwords $ listOf1 generateWord++generateEol :: Gen String+generateEol = elements ["\n", "\r", "\r\n"]++twice :: Gen a -> Gen (a, a)+twice gen = (,) <$> gen <*> gen++trice :: Gen a -> Gen (a, a, a)+trice gen = (,,) <$> gen <*> gen <*> gen++generateCardName :: Gen (String, String)+generateCardName = oneof [generateQuotedIdentifier, generatePlainIdentifier]++generateIdentifier :: Gen (String, String)+generateIdentifier = oneof [generatePlainIdentifier, generateQuotedIdentifier]++generateQuotedIdentifier :: Gen (String, String)+generateQuotedIdentifier = do+    w <- generateWord+    return $ ("\"" ++ w ++ "\"", w)++generatePlainIdentifier :: Gen (String, String)+generatePlainIdentifier = do+    w <- generateWord+    return $ (w, w)++generateFilePath :: Gen (FilePath, FilePath)+generateFilePath =+    generateIdentifier `suchThat` (\(_, f) -> not $ "/" `isSuffixOf` f)++generateDirectory :: Gen (FilePath, FilePath)+generateDirectory = generateFilePath++generateComment :: Gen (String, String)+generateComment = oneof [generateLineComment, generateBlockComment]++generateLineComment :: Gen (String, String)+generateLineComment = do+    ws <- generateWords+    let ws' = "#" ++ ws ++ "\n"+    return (ws', ws)++generateBlockComment :: Gen (String, String)+generateBlockComment = do+    ws <- generateWords+    let ws' = "[[" ++ ws ++ "]]"+    return (ws', ws)++generateDeploymentKindSymbol :: Gen String+generateDeploymentKindSymbol = elements ["l->", "c->", "->"]
+ test/SuperUserSpark/Parser/TestUtils.hs view
@@ -0,0 +1,54 @@+{-# LANGUAGE TemplateHaskell #-}++module SuperUserSpark.Parser.TestUtils where++import TestImport hiding (succeeds)++import Data.Either (isRight)+import SuperUserSpark.Parser.Internal+import Text.Parsec+import Text.Parsec.String++shouldSucceed+    :: (Show a, Eq a)+    => Parser a -> String -> IO ()+shouldSucceed parser input = input `shouldSatisfy` succeeds parser++shouldFail+    :: (Show a, Eq a)+    => Parser a -> String -> IO ()+shouldFail parser input = input `shouldNotSatisfy` succeeds parser++succeeds :: Parser a -> String -> Bool+succeeds parser = succeedsWithLeftover $ parser >> eof++succeedsWithLeftover :: Parser a -> String -> Bool+succeedsWithLeftover parser input = isRight $ parseWithoutSource parser input++succeedsAnywhere :: Parser a -> String -> Bool+succeedsAnywhere p s = or $ map (succeedsWithLeftover p) (tails s)+  where+    tails :: [a] -> [[a]]+    tails [] = [[]]+    tails ass@(_:as) = ass : tails as++fails :: Parser a -> String -> Bool+fails parser input = not $ succeeds parser input++testInputSource :: Path Abs File+testInputSource = $(mkAbsFile "/Test/input/file")++parseShouldSucceedAs+    :: (Show a, Eq a)+    => Parser a -> String -> a -> IO ()+parseShouldSucceedAs parser input a =+    parseFromSource parser testInputSource input `shouldBe` Right a++parseShouldBe+    :: (Show a, Eq a)+    => Parser a -> String -> Either ParseError a -> IO ()+parseShouldBe parser input result =+    parseFromSource parser testInputSource input `shouldBe` result++parseWithoutSource :: Parser a -> String -> Either ParseError a+parseWithoutSource parser input = parseFromSource parser testInputSource input
+ test/SuperUserSpark/ParserSpec.hs view
@@ -0,0 +1,528 @@+{-# LANGUAGE TypeApplications #-}++module SuperUserSpark.ParserSpec where++import TestImport hiding (succeeds)++import Data.Either (isLeft, isRight)+import Data.List (intercalate)+import Text.Parsec++import SuperUserSpark.CoreTypes+import SuperUserSpark.Language.Gen ()+import SuperUserSpark.Language.Types+import SuperUserSpark.Parser.Gen+import SuperUserSpark.Parser.Internal+import SuperUserSpark.Parser.TestUtils+import TestUtils++spec :: Spec+spec =+    parallel $ do+        instanceSpec+        blankspaceParserTests+        enclosingCharacterTests+        delimiterTests+        identifierParserTests+        commentParserTests+        pathParserTests+        declarationParserTests+        parserBlackBoxTests+        toplevelParserTests++enclosingCharacterTests :: Spec+enclosingCharacterTests = do+    describe "inBraces" $ do+        it+            "succeeds for cases where we enclose braces around a string without braces" $ do+            forAll+                (listOf1 arbitrary `suchThat` (\c -> c /= "{" && c /= "}"))+                (\word ->+                     parseShouldSucceedAs+                         (inBraces $ string word)+                         ("{" ++ word ++ "}")+                         word)+    describe "inQuotes" $ do+        it+            "succeeds for cases where we enclose quotes around a string without quotes" $ do+            forAll (listOf1 arbitrary `suchThat` (/= "\"")) $ \word ->+                parseShouldSucceedAs+                    (inQuotes $ string word)+                    ("\"" ++ word ++ "\"")+                    word++instanceSpec :: Spec+instanceSpec = do+    eqSpec @Card+    genValidSpec @Card+    eqSpec @Declaration+    genValidSpec @Declaration+    eqSpec @CardNameReference+    genValidSpec @CardNameReference+    eqSpec @CardFileReference+    genValidSpec @CardFileReference+    eqSpec @CardReference+    genValidSpec @CardReference+    eqSpec @SparkFile+    genValidSpec @SparkFile++blankspaceParserTests :: Spec+blankspaceParserTests = do+    describe "eol" $ do+        let s = shouldSucceed eol+        it "succeeds for Linux line endings" $ do s "\n"+        it "succeeds for Windows line endings" $ do s "\r\n"+        it "succeeds for Mac line endings" $ do s "\r"+        let f = shouldFail eol+        it "fails for the empty string" $ do f ""+        it "fails for spaces" $ do forAll (listOf generateSpace) (\ss -> f ss)+        it "fails for tabs" $ do forAll (listOf generateTab) (\ss -> f ss)+        it "fails for linespace" $ do forAll generateLineSpace (\ls -> f ls)+    describe "linespace" $ do+        let s = shouldSucceed linespace+        it "succeeds for spaces" $ do forAll (listOf generateSpace) s+        it "succeeds for tabs" $ do forAll (listOf generateTab) s+        it "succeeds for mixtures of spaces and tabs" $ do+            forAll generateLineSpace s+        let f = shouldSucceed whitespace+        it "fails for line ending characters" $ do+            forAll (listOf $ oneof [generateCarriageReturn, generateLineFeed]) f+        it "fails for any non-linespace, even if there's linespace in it" $+            forAll (listOf1 generateNormalCharacter) $ \ls ->+                forAll generateLineSpace $ \ls1 ->+                    forAll generateLineSpace $ \ls2 ->+                        shouldFail linespace (ls1 ++ ls ++ ls2)+    describe "whitespace" $ do+        let s = shouldSucceed whitespace+        it "succeeds for spaces" $ do forAll (listOf generateSpace) s+        it "succeeds for tabs" $ do forAll (listOf generateTab) s+        it "succeeds carriage returns" $ do+            forAll (listOf generateCarriageReturn) s+        it "succeeds line feeds" $ do forAll (listOf generateLineFeed) s+        it+            "succeeds for mixtures of spaces, tabs, carriage returns and line feeds" $ do+            forAll generateWhiteSpace s+        it "fails for any non-whitespace, even if there's whitespace in it" $ do+            forAll (listOf1 generateNormalCharacter) $ \ls ->+                forAll generateWhiteSpace $ \ws1 ->+                    forAll generateWhiteSpace $ \ws2 ->+                        shouldFail whitespace (ws1 ++ ls ++ ws2)+    describe "inLineSpace" $ do+        it+            "succeeds for cases where we append whitespace to the front and back of non-whitespace" $ do+            forAll generateLineSpace $ \ws1 ->+                forAll generateLineSpace $ \ws2 ->+                    forAll (listOf1 generateNormalCharacter) $ \ls ->+                        parseShouldSucceedAs+                            (inLineSpace $ string ls)+                            (ws1 ++ ls ++ ws2)+                            ls+    describe "inWhiteSpace" $ do+        it+            "succeeds for cases where we append whitespace to the front and back of non-whitespace" $ do+            forAll generateWhiteSpace $ \ws1 ->+                forAll generateWhiteSpace $ \ws2 ->+                    forAll (listOf1 generateNormalCharacter) $ \ls ->+                        parseShouldSucceedAs+                            (inWhiteSpace $ string ls)+                            (ws1 ++ ls ++ ws2)+                            ls++delimiterTests :: Spec+delimiterTests = do+    describe "delim" $ do+        it "succeeds on a semicolon" $ do shouldSucceed delim ";"+        it "succeeds on an eol" $ do+            once $+                forAll (arbitrary `suchThat` succeeds eol) (shouldSucceed delim)++identifierParserTests :: Spec+identifierParserTests = do+    describe "plainIdentifier" $ do+        it "succeeds for generated plain identifiers" $ do+            forAll generatePlainIdentifier $ \(e, a) ->+                parseShouldSucceedAs plainIdentifier e a+        let pti = shouldSucceed plainIdentifier+        it "succeeds for these examples" $ do+            pti "bash"+            pti "card"+            pti ".bashrc"+            pti "xmonad.hs"+        it "fails for generated quoted identifiers" $ do+            forAll generateQuotedIdentifier $ \(e, _) ->+                shouldFail plainIdentifier e+    describe "quotedIdentifier" $ do+        it "succeeds for generated plain identifiers surrounded in quotes" $ do+            forAll generatePlainIdentifier $ \(e, a) ->+                parseShouldSucceedAs quotedIdentifier ("\"" ++ e ++ "\"") a+        it "succeeds for generated quoted identifiers" $ do+            forAll generateQuotedIdentifier $ \(e, a) ->+                parseShouldSucceedAs quotedIdentifier e a+        let pti i = parseShouldSucceedAs quotedIdentifier ("\"" ++ i ++ "\"") i+        it "succeeds for these examples" $ do+            pti "bashrc"+            pti "with spaces"+        it "fails for generated plain identifiers" $ do+            forAll generatePlainIdentifier $ \(e, _) ->+                shouldFail quotedIdentifier e+        it "fails for generated identifiers with just one quote" $ do+            forAll generatePlainIdentifier $ \(e, _) ->+                shouldFail quotedIdentifier ("\"" ++ e) .&&.+                shouldFail quotedIdentifier (e ++ "\"")+    describe "identifier" $ do+        it "succeeds for generated identifiers" $ do+            forAll generateIdentifier $ \(e, a) ->+                parseShouldSucceedAs identifier e a++commentParserTests :: Spec+commentParserTests = do+    describe "eatComments" $ do+        it "should succeed unchanged on anything without comments" $ do+            property $ \s ->+                (not $ succeedsAnywhere comment s) ==>+                parseShouldSucceedAs eatComments s s+        let (-=>) e a = parseShouldSucceedAs eatComments e a+        it "successfully removes comments in these strings" $ do+            "abc#def\nghi" -=> "abcghi"+            "abc# This is a bigger comment \r\nghi" -=> "abcghi"+            "abc[[def]]ghi" -=> "abcghi"+            "abc[[ This is a bigger comment ]]ghi" -=> "abcghi"+            "Heavy[[use]]of#comments\n." -=> "Heavyof."+    describe "notComment" $ do+        it "should succeed for any string that doesn't contain comments" $ do+            property $ \s ->+                (not $ succeedsAnywhere comment s) ==>+                parseShouldSucceedAs notComment s s+    describe "lineComment" $ do+        it "succeeds for generated line comments" $ do+            forAll generateLineComment $ \(e, a) ->+                parseShouldSucceedAs lineComment e a+        it "succeeds for these test cases" $ do+            parseShouldSucceedAs lineComment "#a\n" "a"+            parseShouldSucceedAs+                lineComment+                "# This is a comment\n"+                " This is a comment"+            parseShouldSucceedAs+                lineComment+                "## This is a comment with two comment signs.\n"+                "# This is a comment with two comment signs."+            parseShouldSucceedAs+                lineComment+                "# with other eol\r\n"+                " with other eol"+    describe "blockComment" $ do+        it "succeeds for generated block comments" $ do+            forAll generateBlockComment $ \(e, a) ->+                parseShouldSucceedAs blockComment e a+        it "succeeds of these test cases" $ do+            parseShouldSucceedAs blockComment "[[a]]" "a"+            parseShouldSucceedAs+                blockComment+                "[[ This is a block comment. ]]"+                " This is a block comment. "+            parseShouldSucceedAs+                blockComment+                "[[ [This is a [block] comment containing brackets.] ]]"+                " [This is a [block] comment containing brackets.] "+    describe "comment" $ do+        it "succeeds for generated line comments" $ do+            forAll generateLineComment $ \(e, a) ->+                parseShouldSucceedAs comment e a+        it "succeeds for generated block comments" $ do+            forAll generateBlockComment $ \(e, a) ->+                parseShouldSucceedAs comment e a++pathParserTests :: Spec+pathParserTests = do+    describe "filepath" $ do+        it "succeeds for generated filepaths" $ do+            forAll generateFilePath $ \(e, a) ->+                parseShouldSucceedAs filepath e a+        it "succeeds for this quoted filepath with a space in it" $ do+            parseShouldSucceedAs+                filepath+                "\"/home/user/with spaces\""+                "/home/user/with spaces"+        let s = shouldSucceed filepath+        it "succeeds for this file without an extension" $ do+            s "withoutExtension"+        it "succeeds for this simple file" $ do s "test.txt"+        it "succeeds for this simple file with a long extension" $ do+            s "file.somelongextension"+        it "succeeds for this absolute filepath" $ do s "/home/user/test.txt"+        it "succeeds for this absolute filepath with a long extension" $ do+            s "/home/user/file.somelongextension"+        it "succeeds for this absolute filepath with multiple extensions" $ do+            s "/home/user/test.multiple.extensions"+        it "succeeds for this relative filepath with a double dot" $ do+            s "/home/user/../user/test.txt"+        let f = shouldFail filepath+        it "fails for just a slash" $ do f "/"+        it "fails for strings ending in a slash" $ do+            property $ \s_ -> f (s_ ++ "/")+    describe "directory" $ do+        it "succeeds for generated directories" $ do+            forAll generateDirectory $ \(e, a) ->+                parseShouldSucceedAs directory e a+        let s = shouldSucceed directory+        it "succeeds for the home directory" $ do s "~"+        it "succeeds for this relative directory" $ do s "directory"+        it "succeeds for this absolute directory" $ do s "/home/user"+        it "succeeds for these directories in the home directory" $ do+            s "~/.vim"+            s "~/Dropbox"+            s "~/.xmonad"+        let f = shouldFail directory+        it "fails for just a slash" $ do f "/"+        it "fails for strings ending in a slash" $ do+            property $ \s_ -> f (s_ ++ "/")++declarationParserTests :: Spec+declarationParserTests = do+    describe "cardName" $ do+        it "succeeds on every card name that we generate" $ do+            forAll generateCardName $ \(a, e) ->+                parseShouldSucceedAs cardNameP a e+    describe "card" $ do+        let pc = parseShouldSucceedAs card+        it "succeeds on this card with an empty name correctly" $ do+            pc "card \"\" {}" $ Card "" (Block [])+        it "succeeds on this compressed empty cards" $ do+            forAll generateCardName $ \(a, e) ->+                parseShouldSucceedAs card ("card" ++ a ++ "{}") $+                Card e (Block [])+        it "succeeds on empty cards with whitespace around the name" $ do+            forAll generateCardName $ \(a, e) ->+                forAll (twice generateWhiteSpace) $ \(ws1, ws2) ->+                    parseShouldSucceedAs+                        card+                        ("card" ++ ws1 ++ a ++ ws2 ++ "{}") $+                    Card e (Block [])+        it "succeeds on empty cards with whitespace between the brackets" $ do+            forAll generateCardName $ \(a, e) ->+                forAll generateWhiteSpace $ \ws ->+                    parseShouldSucceedAs card ("card" ++ a ++ "{" ++ ws ++ "}") $+                    Card e (Block [])+        it "fails on any card with an empty body" $ do+            forAll generateCardName $ \(a, _) ->+                forAll generateWhiteSpace $ \ws ->+                    shouldFail card ("card" ++ a ++ ws)+        it "succeeds on this complicated example" $ do+            parseShouldSucceedAs+                card+                ("card complicated {\n  alternatives $(HOST) shared\n  hello l-> goodbye\n into $(HOME)\n  outof depot\n  spark card othercard\n  kind link\n  {\n    one c-> more\n    source -> destination\n    file\n  }\n}") $+                Card "complicated" $+                Block+                    [ Alternatives ["$(HOST)", "shared"]+                    , Deploy "hello" "goodbye" (Just LinkDeployment)+                    , IntoDir "$(HOME)"+                    , OutofDir "depot"+                    , SparkOff (CardName (CardNameReference "othercard"))+                    , DeployKindOverride LinkDeployment+                    , Block+                          [ Deploy "one" "more" (Just CopyDeployment)+                          , Deploy "source" "destination" Nothing+                          , Deploy "file" "file" Nothing+                          ]+                    ]+    describe "declarations" $ do+        it "succeeds for generated declarations'" $ do pending+        let s = parseShouldSucceedAs declarations+        it "succeeds for these cases" $ do+            s "into dir;outof dir" [IntoDir "dir", OutofDir "dir"]+    describe "declaration" $ do+        it "succeeds for generated declarations" $ do pending+        let s = parseShouldSucceedAs declaration+        it "succeeds for these cases" $ do+            s "into directory" (IntoDir "directory")+            s "outof \"other directory\"" (OutofDir "other directory")+            s "{}" (Block [])+            s "{{{};{};{}}}" (Block [Block [Block [], Block [], Block []]])+            s+                "\"hi i'm a file\"c->iamthedestination"+                (Deploy+                     "hi i'm a file"+                     "iamthedestination"+                     (Just CopyDeployment))+    describe "block" $ do+        it "succeeds for empty blocks" $ do+            parseShouldSucceedAs block "{}" (Block [])+        it "succeeds for a doubly nested empty block" $ do+            parseShouldSucceedAs block "{{}}" (Block [Block []])+        it "succeeds for a triply nested empty block" $ do+            parseShouldSucceedAs block "{{{}}}" (Block [Block [Block []]])+        let s = parseShouldSucceedAs block+        it "succeeds for these cases" $ do+            s+                "{into ~;bashrc -> .bashrc}"+                (Block [IntoDir "~", Deploy "bashrc" ".bashrc" Nothing])+            s+                "{\n    into \"~\"\n    \"xmonad\" -> \".xmonad\"\n}"+                (Block [IntoDir "~", Deploy "xmonad" ".xmonad" Nothing])+    describe "sparkOff" $ do+        it "succeeds for generated sparkOff declarations" $ do pending+        let s f g =+                parseShouldSucceedAs+                    sparkOff+                    f+                    (SparkOff $ CardName $ CardNameReference g)+        it "succeeds for these cases" $ do+            s "spark card name" "name"+            s "sparkcardname" "name"+            s "spark card \"name with spaces\"" "name with spaces"+    describe "intoDir" $ do+        it "succeeds for generated into declarations" $ do+            forAll generateLineSpace $ \ls ->+                forAll generateDirectory $ \(d, da) ->+                    parseShouldSucceedAs+                        intoDir+                        ("into" ++ ls ++ d)+                        (IntoDir da)+        let s f g = parseShouldSucceedAs intoDir f (IntoDir g)+        it "succeeds for these cases" $ do+            s "into \"bash\"" "bash"+            s "into\t.xmonad" ".xmonad"+            s "into ~" "~"+    describe "outOfDir" $ do+        it "succeeds for generated outof declarations" $ do+            forAll generateLineSpace $ \ls ->+                forAll generateDirectory $ \(d, da) ->+                    parseShouldSucceedAs+                        outOfDir+                        ("outof" ++ ls ++ d)+                        (OutofDir da)+        let s f g = parseShouldSucceedAs outOfDir f (OutofDir g)+        it "succeeds for these cases" $ do+            s "outof bash" "bash"+            s "outof\t.xmonad" ".xmonad"+    describe "alternatives" $ do+        it "succeeds for generated alternatives declarations with single spaces" $ do+            forAll (listOf1 generateDirectory) $ \ds ->+                let (des, das) = unzip ds+                in parseShouldSucceedAs+                       alternatives+                       ("alternatives" ++ " " ++ intercalate " " des)+                       (Alternatives das)+    describe "deployment" $ do+        it "succeeds for short deployments" pending+        it "succeeds for long deployments" pending+    describe "shortDeployment" $ do+        it "succeeds for any filepath with an identity deployment" $ do+            property $ \f ->+                succeeds filepath f ==>+                parseShouldSucceedAs shortDeployment f (Deploy f f Nothing)+        it "succeeds for generated filepaths with an identity deployment" $ do+            forAll generateFilePath $ \(f, g) ->+                parseShouldSucceedAs shortDeployment f (Deploy g g Nothing)+        it "succeeds for any directory with an identity deployment" $ do+            property $ \f ->+                succeeds directory f ==>+                parseShouldSucceedAs shortDeployment f (Deploy f f Nothing)+        it "succeeds for generated directories with an identity deployment" $ do+            forAll generateDirectory $ \(f, g) ->+                parseShouldSucceedAs shortDeployment f (Deploy g g Nothing)+        let s f = parseShouldSucceedAs shortDeployment f (Deploy f f Nothing)+        it "succeeds as-is for these cases" $ do+            s "file.txt"+            s "xmonad.hs"+            s "/home/user/.bashrc"+    describe "longDeployment" $ do+        it "succeeds for generated long deployments with quoted identifiers" $ do+            forAll generateDeploymentKindSymbol $ \dks ->+                forAll generateLineSpace $ \ls1 ->+                    forAll generateLineSpace $ \ls2 ->+                        forAll generateQuotedIdentifier $ \(fp1, fp1a) ->+                            forAll generateQuotedIdentifier $ \(fp2, fp2a) ->+                                case parseWithoutSource deploymentKind dks of+                                    Left _ ->+                                        fail+                                            "There was a problem with parsing the deployment kind"+                                    Right dk ->+                                        parseShouldSucceedAs+                                            longDeployment+                                            (fp1 ++ ls1 ++ dks ++ ls2 ++ fp2)+                                            (Deploy fp1a fp2a dk)+        it+            "succeeds for single-space-separated long deployments with gerenated plain identifiers" $ do+            pendingWith+                "This would go wrong with plain identifiers they can end with \'l\' or \'c\'. Make sure to document this behaviour and write another test with plain identifiers."+        let s f g h i = parseShouldSucceedAs longDeployment f (Deploy g h i)+        it "succeeds for these cases" $ do+            s+                "\"something with spaces\"c->/home/user/test.txt"+                "something with spaces"+                "/home/user/test.txt"+                (Just CopyDeployment)+            s+                "\"xmonad.hs\"l-> /home/user/.xmonad/xmonad.hs"+                "xmonad.hs"+                "/home/user/.xmonad/xmonad.hs"+                (Just LinkDeployment)+            s+                "bashrc\t->\t/home/user/.bashrc"+                "bashrc"+                "/home/user/.bashrc"+                Nothing+    describe "deploymentKind" $ do+        let (-=>) = parseShouldSucceedAs deploymentKind+        it "succeeds for the link deployment kind" $ do+            "l->" -=> Just LinkDeployment+        it "succeeds for the copy deployment kind" $ do+            "c->" -=> Just CopyDeployment+        it "succeeds for the default deployment kind" $ do "->" -=> Nothing+        it "fails for anything else" $ do+            property $ \s ->+                (not $ any (== s) ["l->", "c->", "->"]) ==>+                shouldFail deploymentKind s++cardReferenceParserTests :: Spec+cardReferenceParserTests = do+    describe "compilerCardReference" $ do pend+    describe "deployerCardReference" $ do pend+    describe "compiledCardReference" $ do pend+    describe "cardReference" $ do pend+    describe "cardNameReference" $ do+        pend+        let s f g =+                parseShouldSucceedAs cardNameReference f (CardNameReference g)+        it "succeeds for these cases" $ do+            s "card name" "name"+            s "cardname" "name"+            s "card \"name with spaces\"" "name with spaces"+    describe "cardFileReference" $ do+        pend+        let s = parseShouldSucceedAs cardFileReference+        it "succeeds for these cases" $ do+            s "file card.sus" (CardFileReference "card.sus" Nothing)+            s+                "file card.sus name"+                (CardFileReference "card.sus" $ Just $ CardNameReference "name")+    describe "unprefixedCardFileReference" $ do pend++parserBlackBoxTests :: Spec+parserBlackBoxTests = do+    testRecoursesDir <- runIO $ resolveDir' "test_resources"+    describe "Correct succesful parse examples" $ do+        let dirs =+                map+                    (testRecoursesDir </>)+                    [shouldParseDir, shouldCompileDir, shouldNotCompileDir]+        forFileInDirss dirs $+            concerningContents $ \f contents -> do+                it (toFilePath f) $+                    parseCardFile f contents `shouldSatisfy` isRight+    describe "Correct unsuccesfull parse examples" $ do+        let dirs = map (testRecoursesDir </>) [shouldNotParseDir]+        forFileInDirss dirs $+            concerningContents $ \f contents -> do+                it (toFilePath f) $+                    parseCardFile f contents `shouldSatisfy` isLeft++toplevelParserTests :: Spec+toplevelParserTests = do+    describe "sparkFile" $ do+        it "Only ever produces valid SparkFile's" $+            validIfSucceeds2 parseCardFile+        pend+    describe "resetPosition" $ do pend
+ test/SuperUserSpark/PreCompiler/Gen.hs view
@@ -0,0 +1,12 @@+{-# OPTIONS_GHC -Wno-orphans #-}++module SuperUserSpark.PreCompiler.Gen where++import TestImport++import SuperUserSpark.Language.Gen ()+import SuperUserSpark.PreCompiler.Types++instance GenUnchecked PreCompileError++instance GenValid PreCompileError
+ test/TestImport.hs view
@@ -0,0 +1,32 @@+module TestImport+    ( module X+    ) where++import Prelude as X hiding (writeFile, readFile, putStr,putStrLn, appendFile)++import Path as X+import Path.IO as X++import Control.Monad as X+import Control.Monad.Except as X+import Control.Monad.IO.Class as X (MonadIO(..))+import Control.Monad.Identity as X+import Control.Monad.Reader as X+import Control.Monad.State as X+import Control.Monad.Writer as X++import Debug.Trace as X++import Data.IOString as X++import Test.Hspec as X+import Test.QuickCheck as X++import Data.GenValidity as X+import Data.GenValidity.Path as X ()+import Data.Validity as X ()+import Data.Validity.Path as X ()+import Test.Validity as X+import Test.Validity.Aeson as X++import System.FilePath as X (dropTrailingPathSeparator)
+ test/TestUtils.hs view
@@ -0,0 +1,33 @@+{-# LANGUAGE TemplateHaskell #-}++module TestUtils where++import TestImport++shouldParseDir :: Path Rel Dir+shouldParseDir = $(mkRelDir "shouldParseDir")++shouldNotParseDir :: Path Rel Dir+shouldNotParseDir = $(mkRelDir "shouldNotParseDir")++shouldCompileDir :: Path Rel Dir+shouldCompileDir = $(mkRelDir "shouldCompileDir")++shouldNotCompileDir :: Path Rel Dir+shouldNotCompileDir = $(mkRelDir "shouldNotCompileDir")++concerningContents :: (Path Abs File -> String -> SpecWith a)+                   -> (Path Abs File -> SpecWith a)+concerningContents func file = (runIO $ readFile file) >>= func file++forFileInDirss :: [Path Abs Dir] -> (Path Abs File -> SpecWith a) -> SpecWith a+forFileInDirss [] _ = return ()+forFileInDirss dirs func =+    forM_ dirs $ \dir -> do+        exists <- runIO $ doesDirExist dir+        when exists $ do+            files <- runIO $ snd <$> listDirRecur dir+            forM_ files func++pend :: SpecWith ()+pend = it "is still missing some tests" pending