@@ -4,6 +4,7 @@
4{-# LANGUAGE PatternGuards #-}
4{-# LANGUAGE PatternGuards #-}
5{-# LANGUAGE ScopedTypeVariables #-}
5{-# LANGUAGE ScopedTypeVariables #-}
6{-# LANGUAGE ViewPatterns #-}
6{-# LANGUAGE ViewPatterns #-}
7+{-# LANGUAGE TemplateHaskell #-}
7module Obelisk.SelfTest where
8module Obelisk.SelfTest where
9import Control.Exception (bracket, throw)
10import Control.Exception (bracket, throw)
@@ -11,7 +12,7 @@ import Control.Monad
11import Control.Monad.IO.Class
12import Control.Monad.IO.Class
12import Data.Bool (bool)
13import Data.Bool (bool)
13import Data.Function (fix)
14import Data.Function (fix)
14−import Data.Semigroup (Semigroup, (<>))
15+import Data.Semigroup ((<>))
15import qualified Data.Set as Set
16import qualified Data.Set as Set
16import Data.String
17import Data.String
17import Data.Text (Text)
18import Data.Text (Text)
@@ -21,10 +22,12 @@ import qualified Data.Text.IO as T
21import qualified Network.HTTP.Client as HTTP
22import qualified Network.HTTP.Client as HTTP
22import qualified Network.HTTP.Types as HTTP
23import qualified Network.HTTP.Types as HTTP
23import qualified Network.Socket as Socket
24import qualified Network.Socket as Socket
24−import Shelly
25+import Shelly hiding (cp)
25import System.Directory (withCurrentDirectory, getDirectoryContents)
26import System.Directory (withCurrentDirectory, getDirectoryContents)
26import System.Environment
27import System.Environment
27import System.Exit (ExitCode (..))
28import System.Exit (ExitCode (..))
29+import System.FilePath (replaceBaseName, takeBaseName)
30+import System.Which (staticWhich)
28import qualified System.Info
31import qualified System.Info
29import System.IO (Handle, hClose)
32import System.IO (Handle, hClose)
30import System.IO.Temp
33import System.IO.Temp
@@ -43,16 +46,21 @@ data ObRunState
43 | ObRunState_BackendStarted
46 | ObRunState_BackendStarted
44 deriving (Eq, Show)
47 deriving (Eq, Show)
46−doubleQuotes :: (IsString a, Semigroup a) => a -> a
49+cp :: FilePath
47−doubleQuotes s = "\"" <> s <> "\""
50+cp = $(staticWhich "cp")
53+cabalPath = $(staticWhich "cabal")
55+gitUserConfig :: [Text]
56+gitUserConfig = ["-c", "user.name=Obelisk Selftest", "-c", "user.email=noreply@example.com"]
49commit :: Text -> Sh ()
58commit :: Text -> Sh ()
50−commit msg = void $ run "git"
59+commit msg = void $ run "git" $ gitUserConfig <> [ "commit"
52 , "--no-gpg-sign"
60 , "--no-gpg-sign"
53 , "--allow-empty"
61 , "--allow-empty"
55− , doubleQuotes msg
63+ , msg
58-- TODO replace Void and stop using loosely typed synchronous exceptions
66-- TODO replace Void and stop using loosely typed synchronous exceptions
@@ -68,6 +76,7 @@ tshow = T.pack . show
68shellyOb :: MonadIO m => (Sh a -> Sh a) -> Sh a -> m a
76shellyOb :: MonadIO m => (Sh a -> Sh a) -> Sh a -> m a
69shellyOb f obTest = shelly $ f obTest
77shellyOb f obTest = shelly $ f obTest
71main :: IO ()
80main :: IO ()
73 -- Note: you can pass hspec arguments as well, eg: `-m <pattern>`
82 -- Note: you can pass hspec arguments as well, eg: `-m <pattern>`
@@ -82,33 +91,49 @@ main = do
82 withSystemTempDirectory "obelisk-impl-copy" $ \(fromString -> obeliskImpl) -> do
91 withSystemTempDirectory "obelisk-impl-copy" $ \(fromString -> obeliskImpl) -> do
83 void . shellyOb verbosity $ chdir obeliskImpl $ do
92 void . shellyOb verbosity $ chdir obeliskImpl $ do
84 user <- T.strip <$> run "whoami" []
93 user <- T.strip <$> run "whoami" []
85− run_ "cp" ["-rT", toTextIgnore obeliskImplRaw, toTextIgnore obeliskImpl]
94+ run_ cp ["-rT", toTextIgnore obeliskImplRaw, toTextIgnore obeliskImpl]
86 run_ "chown" ["-R", user, toTextIgnore obeliskImpl]
95 run_ "chown" ["-R", user, toTextIgnore obeliskImpl]
87 run_ "chmod" ["-R", "g-rw,o-rw", toTextIgnore obeliskImpl]
96 run_ "chmod" ["-R", "g-rw,o-rw", toTextIgnore obeliskImpl]
99+ withInitCache f obeliskImpl =
100+ withSystemTempDirectory "init Cache λ" $ \initCache -> do
101+ -- Setup the ob init cache
102+ void . shellyOb verbosity $ chdir initCache $ do
103+ run_ "ob" ["init", "--symlink", toTextIgnore obeliskImpl]
89− withSystemTempDirectory "init Cache λ" $ \initCache -> do
106+ f initCache
90− -- Setup the ob init cache
91− void . shellyOb verbosity $ chdir (fromString initCache) $ do
92− run_ "ob" ["init", "--symlink", toTextIgnore obeliskImpl]
95− f obeliskImpl initCache
108+ withObeliskImplAndInitCache f =
109+ withObeliskImpl $ \impl -> withInitCache (f impl) impl
97 httpManager <- HTTP.newManager HTTP.defaultManagerSettings
111 httpManager <- HTTP.newManager HTTP.defaultManagerSettings
98− [p0, p1, p2, p3] <- liftIO $ getFreePorts 4
100− withObeliskImpl $ \obeliskImpl initCache ->
113+ withObeliskImplAndInitCache $ \obeliskImpl initCache ->
101 hspec $ parallel $ do
114 hspec $ parallel $ do
102 let shelly_ = void . shellyOb verbosity
115 let shelly_ = void . shellyOb verbosity
104− inTmp :: (Shelly.FilePath -> Sh a) -> IO ()
117+ defaultTmpDirName = "test λ"
105− inTmp f = withTmp (chdir <*> f)
107− withTmp f = shelly_ . withSystemTempDirectory "test λ" $ f . fromString
119+ inTmp' :: FilePath -> (FilePath -> Sh a) -> IO ()
120+ inTmp' dirname f = withTmp' dirname (chdir <*> f)
121+ inTmp = inTmp' defaultTmpDirName
123+ withTmp' dirname f = shelly_ . withSystemTempDirectory dirname $ f . fromString
124+ withTmp = withTmp' defaultTmpDirName
109− inTmpObInit f = inTmp $ \dir -> do
126+ inTmpObInit' dirname f = inTmp' dirname $ \dir -> do
110− run_ "cp" ["-rT", fromString initCache, toTextIgnore dir]
127+ run_ cp ["-rT", fromString initCache, toTextIgnore dir]
129+ inTmpObInit = inTmpObInit' defaultTmpDirName
131+ -- To be used in tests that change the obelisk impl directory
132+ inTmpObInitWithImplCopy f = inTmpObInit $ \dir ->
133+ withObeliskImpl $ \(fromString -> implCopy) -> do
135+ run_ "ln" ["-s", implCopy, thunk]
113 assertRevEQ a b = liftIO . assertEqual "" "" =<< diff a b
138 assertRevEQ a b = liftIO . assertEqual "" "" =<< diff a b
114 assertRevNE a b = liftIO . assertBool "" . (/= "") =<< diff a b
139 assertRevNE a b = liftIO . assertBool "" . (/= "") =<< diff a b
@@ -149,9 +174,12 @@ main = do
150 -- These tests fail with "Could not find module 'Obelisk.Generated.Static'"
175 -- These tests fail with "Could not find module 'Obelisk.Generated.Static'"
151 -- when not run by 'nix-build --attr selftest'
176 -- when not run by 'nix-build --attr selftest'
152− describe "ob run" $ parallel $ do
177+ describe "ob run" $ {- NOT parallel $ -} do
153− it "works in root directory" $ inTmpObInit $ \_ -> testObRunInDir p0 p1 Nothing httpManager
178+ it "works in root directory" $ inTmpObInit $ \_ -> testObRunInDir Nothing httpManager
154− it "works in sub directory" $ inTmpObInit $ \_ -> testObRunInDir p2 p3 (Just "frontend") httpManager
179+ it "works in sub directory" $ inTmpObInit $ \_ -> testObRunInDir (Just "frontend") httpManager
180+ it "works with differently named cabal files" $ inTmpObInit $ \_ -> do
181+ changeCabalPackageName "backend/backend.cabal" "new-backend"
182+ testObRunInDir Nothing httpManager
156 describe "obelisk project" $ parallel $ do
184 describe "obelisk project" $ parallel $ do
157 it "can build obelisk command" $ inTmpObInit $ \_ -> nixBuild ["-A", "command" , toTextIgnore obeliskImpl]
185 it "can build obelisk command" $ inTmpObInit $ \_ -> nixBuild ["-A", "command" , toTextIgnore obeliskImpl]
@@ -167,7 +195,7 @@ main = do
167 if System.Info.os == "darwin"
195 if System.Info.os == "darwin"
168 then it "can build ios" $ inTmpObInit $ \_ -> nixBuild ["-A", "ios.frontend"]
196 then it "can build ios" $ inTmpObInit $ \_ -> nixBuild ["-A", "ios.frontend"]
169 else it "can build android after accepting license" $ inTmpObInit $ \dir -> do
197 else it "can build android after accepting license" $ inTmpObInit $ \dir -> do
170− let defaultNixPath = dir </> ("default.nix" :: Shelly.FilePath)
198+ let defaultNixPath = dir </> ("default.nix" :: FilePath)
171 writefile defaultNixPath
199 writefile defaultNixPath
172 =<< T.replace
200 =<< T.replace
173 "# config.android_sdk.accept_license = false;"
201 "# config.android_sdk.accept_license = false;"
@@ -178,18 +206,21 @@ main = do
178 forM_ ["ghc", "ghcjs"] $ \compiler -> do
206 forM_ ["ghc", "ghcjs"] $ \compiler -> do
180 shellName = "shells." <> compiler
208 shellName = "shells." <> compiler
181− inShell cmd' = run "nix-shell" ["default.nix", "-A", fromString shellName, "--run", cmd']
209+ inShell cmd' = run_ "nix-shell" ["default.nix", "-A", fromString shellName, "--run", cmd']
182 it ("can enter " <> shellName) $ inTmpObInit $ \_ -> inShell "exit"
210 it ("can enter " <> shellName) $ inTmpObInit $ \_ -> inShell "exit"
211+ -- NOTE: We override the temporary directory name because cabal has a bug preventing new-build from working
212+ -- in a path that has unicode characters.
183− it ("can build in " <> shellName) $ inTmpObInit $ \_ -> inShell $ "cabal new-build --" <> fromString compiler <> " all"
213+ it ("can build in " <> shellName) $ inTmpObInit' "test" $ \_ -> inShell $
214+ T.pack cabalPath <> " --version; " <> T.pack cabalPath <> " new-build --" <> T.pack compiler <> " all"
185− it "has idempotent thunk update" $ inTmpObInit $ \_ -> do
216+ it "has idempotent thunk update" $ inTmpObInitWithImplCopy $ \_ -> do
186 _ <- pack
217 _ <- pack
187 u <- update
218 u <- update
188 uu <- update
219 uu <- update
189 assertRevEQ u uu
220 assertRevEQ u uu
191 describe "ob thunk pack/unpack" $ parallel $ do
222 describe "ob thunk pack/unpack" $ parallel $ do
192− it "has thunk pack and unpack inverses" $ inTmpObInit $ \_ -> do
223+ it "has thunk pack and unpack inverses" $ inTmpObInitWithImplCopy $ \_ -> do
194 _ <- pack
225 _ <- pack
195 e <- commitAll
226 e <- commitAll
@@ -249,8 +280,9 @@ main = do
251-- | Run `ob run` in the given directory (maximum of one level deep)
282-- | Run `ob run` in the given directory (maximum of one level deep)
252−testObRunInDir :: Socket.PortNumber -> Socket.PortNumber -> Maybe Shelly.FilePath -> HTTP.Manager -> Sh ()
283+testObRunInDir :: Maybe FilePath -> HTTP.Manager -> Sh ()
253−testObRunInDir p0 p1 mdir httpManager = handle_sh (\case ExitSuccess -> pure (); e -> throw e) $ do
284+testObRunInDir mdir httpManager = handle_sh (\case ExitSuccess -> pure (); e -> throw e) $ do
285+ [p0, p1] <- liftIO $ getFreePorts 2
254 let uri p = "http://localhost:" <> T.pack (show p) <> "/" -- trailing slash required for comparison
286 let uri p = "http://localhost:" <> T.pack (show p) <> "/" -- trailing slash required for comparison
255 writefile "config/common/route" $ uri p0
287 writefile "config/common/route" $ uri p0
256 maybe id chdir mdir $ runHandle "ob" ["run", "-v"] $ \stdout -> do
288 maybe id chdir mdir $ runHandle "ob" ["run", "-v"] $ \stdout -> do
@@ -264,7 +296,7 @@ testObRunInDir p0 p1 mdir httpManager = handle_sh (\case ExitSuccess -> pure ();
264 then errorExit $ "Reloading failed: expected " <> newUri <> " but got " <> runningUri
296 then errorExit $ "Reloading failed: expected " <> newUri <> " but got " <> runningUri
265 else exit 0
297 else exit 0
267−testThunkPack :: Shelly.FilePath -> Sh ()
299+testThunkPack :: FilePath -> Sh ()
268testThunkPack path' = withTempFile (T.unpack $ toTextIgnore path') "test-file" $ \file handle -> do
300testThunkPack path' = withTempFile (T.unpack $ toTextIgnore path') "test-file" $ \file handle -> do
269 let pack' = readProcessWithExitCode "ob" ["thunk", "pack", T.unpack $ toTextIgnore path'] ""
301 let pack' = readProcessWithExitCode "ob" ["thunk", "pack", T.unpack $ toTextIgnore path'] ""
270 ensureThunkPackFails q = liftIO $ pack' >>= \case
302 ensureThunkPackFails q = liftIO $ pack' >>= \case
@@ -285,7 +317,7 @@ testThunkPack path' = withTempFile (T.unpack $ toTextIgnore path') "test-file" $
285 liftIO $ T.hPutStrLn handle "test file" >> hClose handle
317 liftIO $ T.hPutStrLn handle "test file" >> hClose handle
286 ensureThunkPackFails "modified"
318 ensureThunkPackFails "modified"
287 -- Existing stashes
319 -- Existing stashes
288− void $ git ["stash"]
320+ void $ git $ gitUserConfig <> [ "stash" ]
289 ensureThunkPackFails "has stashes"
321 ensureThunkPackFails "has stashes"
291-- | Blocks until a non-empty line is available
323-- | Blocks until a non-empty line is available
@@ -330,6 +362,14 @@ obRunCheck httpManager _stdout frontendUri = do
330 req frontendUri >>= \r -> when (HTTP.responseStatus r /= HTTP.ok200) $ errorExit $
362 req frontendUri >>= \r -> when (HTTP.responseStatus r /= HTTP.ok200) $ errorExit $
331 "Request to frontend server failed: " <> T.pack (show r)
363 "Request to frontend server failed: " <> T.pack (show r)
365+-- | Rename a cabal file and do a really dumb, brittle search/replace in its content to update the name.
366+changeCabalPackageName :: FilePath -> Text -> Sh ()
367+changeCabalPackageName cabalFile newName = do
368+ contents <- readfile cabalFile
369+ writefile (replaceBaseName cabalFile (T.unpack newName)) $
370+ T.replace (" " <> T.pack (takeBaseName cabalFile)) (" " <> newName) contents -- WARNING: Super brittle
333getFreePorts :: Int -> IO [Socket.PortNumber]
373getFreePorts :: Int -> IO [Socket.PortNumber]
334getFreePorts 0 = pure []
374getFreePorts 0 = pure []
335getFreePorts n = Socket.withSocketsDo $ do
375getFreePorts n = Socket.withSocketsDo $ do