@@ -23,7 +23,7 @@ import qualified Data.Text.IO as T
2323import qualified Network.HTTP.Client as HTTP
2424import qualified Network.HTTP.Types as HTTP
2525import qualified Network.Socket as Socket
26−import Shelly hiding (cp)
26+import Shelly
2727import System.Directory (getCurrentDirectory, withCurrentDirectory, getDirectoryContents)
2828import System.Environment
2929import System.Exit (ExitCode (..))
@@ -31,7 +31,7 @@ import System.Which (staticWhich)
3131import qualified System.Info
3232import System.IO (Handle, hClose, hIsEOF, hGetContents)
3333import System.IO.Temp
34−import System.Process (readProcessWithExitCode, CreateProcess(cwd), readCreateProcessWithExitCode, proc)
34+import System.Process (readProcessWithExitCode)
3535import Test.Hspec
3636import Test.HUnit.Base
3737
@@ -46,8 +46,8 @@ data ObRunState
4646 | ObRunState_BackendStarted
4747 deriving (Eq, Show)
4848
49−cp :: FilePath
50−cp = $(staticWhich "cp")
49+cpPath :: FilePath
50+cpPath = $(staticWhich "cp")
5151
5252cabalPath :: FilePath
5353cabalPath = $(staticWhich "cabal")
@@ -112,25 +112,189 @@ main = do
112112 isVerbose <- elem "-v" <$> getArgs
113113 unless isVerbose $
114114 putStrLn "Tests may take longer to run if there are unbuilt derivations: use -v for verbose output"
115− let verbosity = bool silently verbosely isVerbose
116− nixBuild args = run nixBuildPath ("--no-out-link" : args)
115+
117116 obeliskImplDirtyReadOnly <- fromString <$> getCurrentDirectory
118− let runOb_ = augmentWithVerbosity run_ ob isVerbose
119− let runOb = augmentWithVerbosity run ob isVerbose
120− let testObRunInDir' = augmentWithVerbosity testObRunInDir ob isVerbose ["run"]
121− let testThunkPack' = augmentWithVerbosity testThunkPack ob isVerbose []
117+ httpManager <- HTTP.newManager HTTP.defaultManagerSettings
118+
119+ main' isVerbose httpManager obeliskImplDirtyReadOnly
120+
121+main' :: Bool -> HTTP.Manager -> FilePath -> IO ()
122+main' isVerbose httpManager obeliskRepoReadOnly = withObeliskImplDirtyAndInitCache $ \obeliskImplDirty initCache -> hspec $ parallel $ do
122123 let
124+ inTmpObInit' dirname f = inTmp' dirname $ \dir -> do
125+ run_ cpPath ["-rT", fromString initCache, toTextIgnore dir]
126+ f dir
127+ inTmpObInit = inTmpObInit' defaultTmpDirName
128+
129+ -- To be used in tests that change the obelisk impl directory
130+ inTmpObInitWithImplCopy f = inTmpObInit $ \dir ->
131+ withObeliskImplClean $ \(fromString -> implClean) -> do
132+ run_ rmPath [thunk]
133+ run_ lnPath ["-s", implClean, thunk]
134+ f dir
135+
136+ describe "ob init" $ parallel $ do
137+ it "works with default impl" $ inTmp $ \_ -> runOb ["init"]
138+ it "works with master branch impl" $ inTmp $ \_ -> runOb ["init", "--branch", "master"]
139+ it "works with symlink" $ inTmp $ \_ -> runOb ["init", "--symlink", toTextIgnore obeliskImplDirty]
140+ it "doesn't silently overwrite existing files" $ inTmp $ \_ -> do
141+ let p force = errExit False $ do
142+ run_ ob $ "--no-handoff" : "-v" : "init" : ["--force"|force]
143+ (== 0) <$> lastExitCode
144+
145+ True <- p False
146+ False <- p False
147+ True <- p True
148+ pure ()
149+
150+ it "doesn't create anything when given an invalid impl" $ inTmp $ \tmp -> do
151+ void $ errExit False $ runOb ["init", "--symlink", "/dev/null"]
152+ ls tmp >>= liftIO . assertEqual "" []
153+
154+ it "produces a valid route config" $ inTmpObInit $ \tmp -> liftIO $
155+ withCurrentDirectory (T.unpack $ toTextIgnore tmp) $ do
156+ configs <- getConfigs
157+ return (either (const Nothing) Just $ getConfigRoute configs) `shouldNotReturn` Nothing
158+
159+ -- These tests fail with "Could not find module 'Obelisk.Generated.Static'"
160+ -- when not run by 'nix-build --attr selftest'
161+ describe "ob run" $ {- NOT parallel $ -} do
162+ it "works in root directory" $ inTmpObInit $ \_ -> testObRunInDir' Nothing httpManager
163+ it "works in sub directory" $ inTmpObInit $ \_ -> testObRunInDir' (Just "frontend") httpManager
164+
165+ describe "obelisk project" $ parallel $ do
166+ it "can build obelisk command" $ inTmpObInit $ \_ -> nixBuild ["-A", "command" , toTextIgnore obeliskImplDirty]
167+ it "can build obelisk skeleton" $ inTmpObInit $ \_ -> nixBuild ["-A", "skeleton", toTextIgnore obeliskImplDirty]
168+ it "can build obelisk shell" $ inTmpObInit $ \_ -> nixBuild ["-A", "shell", toTextIgnore obeliskImplDirty]
169+ it "can build everything" $ inTmpObInit $ \_ -> nixBuild [toTextIgnore obeliskImplDirty]
170+
171+ describe "blank initialized project" $ parallel $ do
172+
173+ it "can build ghc.backend" $ inTmpObInit $ \_ -> nixBuild ["-A", "ghc.backend"]
174+ it "can build ghcjs.frontend" $ inTmpObInit $ \_ -> nixBuild ["-A", "ghcjs.frontend"]
175+
176+ if System.Info.os == "darwin"
177+ then it "can build ios" $ inTmpObInit $ \_ -> nixBuild ["-A", "ios.frontend"]
178+ else it "can build android after accepting license" $ inTmpObInit $ \dir -> do
179+ let defaultNixPath = dir </> ("default.nix" :: FilePath)
180+ writefile defaultNixPath
181+ =<< T.replace
182+ "# config.android_sdk.accept_license = false;"
183+ "config.android_sdk.accept_license = true;"
184+ <$> readfile defaultNixPath
185+ nixBuild ["-A", "android.frontend"]
186+
187+ forM_ ["ghc", "ghcjs"] $ \compiler -> do
188+ let
189+ shellName = "shells." <> compiler
190+ inShell cmd' = run_ "nix-shell" ["default.nix", "-A", fromString shellName, "--run", cmd']
191+ it ("can enter " <> shellName) $ inTmpObInit $ \_ -> inShell "exit"
192+ -- NOTE: We override the temporary directory name because cabal has a bug preventing new-build from working
193+ -- in a path that has unicode characters.
194+ it ("can build in " <> shellName) $ inTmpObInit' "test" $ \_ -> inShell $
195+ T.pack cabalPath <> " --version; " <> T.pack cabalPath <> " new-build --" <> T.pack compiler <> " all"
196+
197+ it "has idempotent thunk update" $ inTmpObInitWithImplCopy $ \_ -> do
198+ _ <- pack
199+ u <- update
200+ uu <- update
201+ assertRevEQ u uu
202+
203+ describe "ob thunk pack/unpack" $ parallel $ do
204+ it "has thunk pack and unpack inverses" $ inTmpObInitWithImplCopy $ \_ -> do
205+
206+ _ <- pack
207+ e <- commitAll
208+ eu <- unpack
209+ eup <- pack
210+ eupu <- unpack
211+ _ <- pack
212+
213+ assertRevEQ e eup
214+ assertRevEQ eu eupu
215+ assertRevNE e eu
216+
217+ it "unpacks the correct branch" $ withTmp $ \dir -> do
218+ let branch = "master"
219+ run_ gitPath ["clone", "https://github.com/reflex-frp/reflex.git", toTextIgnore dir, "--branch", branch]
220+ runOb_ ["thunk", "pack", toTextIgnore dir]
221+ runOb_ ["thunk", "unpack", toTextIgnore dir]
222+ branch' <- chdir dir $ run gitPath ["rev-parse", "--abbrev-ref", "HEAD"]
223+ liftIO $ assertEqual "" branch (T.strip branch')
224+
225+ it "can pack and unpack plain git repos" $
226+ shelly_ $ withSystemTempDirectory "git-repo" $ \dir -> do
227+ let repo = toTextIgnore $ dir </> ("repo" :: FilePath)
228+ run_ gitPath ["clone", "https://github.com/haskell/process.git", repo]
229+ origHash <- chdir (fromText repo) revParseHead
230+
231+ runOb_ ["thunk", "pack", repo]
232+ packedFiles <- Set.fromList <$> ls (fromText repo)
233+ liftIO $ assertEqual "" packedFiles $ Set.fromList $ (repo </>) <$>
234+ ["default.nix", "github.json", ".attr-cache" :: FilePath]
235+
236+ runOb_ ["thunk", "unpack", repo]
237+ chdir (fromText repo) $ do
238+ unpackHash <- revParseHead
239+ assertRevEQ origHash unpackHash
240+
241+ testThunkPack' $ fromText repo
242+
243+ it "aborts thunk pack when there are uncommitted files" $ inTmpObInitWithImplCopy $ \dir -> do
244+ testThunkPack' (dir </> thunk)
245+
246+ describe "ob thunk update --branch" $ parallel $ do
247+ it "can change a thunk to the latest version of a desired branch" $ withTmp $ \dir -> do
248+ let branch1 = "master"
249+ branch2 = "develop"
250+ run_ gitPath ["clone", "https://github.com/reflex-frp/reflex.git", toTextIgnore dir, "--branch", branch1]
251+ runOb_ ["thunk" , "pack", toTextIgnore dir]
252+ runOb_ ["thunk", "update", toTextIgnore dir, "--branch", branch2]
253+
254+ it "doesn't create anything when given an invalid branch" $ withTmp $ \dir -> do
255+ let checkDir dir' = liftIO $ getDirectoryContents $ T.unpack $ toTextIgnore dir'
256+ run_ gitPath ["clone", "https://github.com/reflex-frp/reflex.git", toTextIgnore dir, "--branch", "master"]
257+ runOb_ ["thunk" , "pack", toTextIgnore dir]
258+ startingContents <- checkDir dir
259+ void $ errExit False $ runOb ["thunk", "update", toTextIgnore dir, "--branch", "dumble-palooza"]
260+ checkDir dir >>= liftIO . assertEqual "" startingContents
261+
262+ describe "ob hoogle" $ {- NOT parallel -} do
263+ it "starts a hoogle server on the given port" $ inTmpObInit $ \_ -> do
264+ [p0] <- liftIO $ getFreePorts 1
265+ maskExitSuccess $ runHandle "ob" ["hoogle", "--port", T.pack (show p0)] $ \stdout -> fix $ \loop -> do
266+ ln <- liftIO $ T.hGetLine stdout
267+ echo ln
268+ let search = "Server starting on port " <> T.pack (show p0)
269+ case search `T.isInfixOf` ln of
270+ False -> loop -- keep waiting
271+ True -> do
272+ let req uri = liftIO $ HTTP.parseRequest uri >>= flip HTTP.httpLbs httpManager
273+ req ("http://127.0.0.1:" <> show p0) >>= \r -> case HTTP.responseStatus r == HTTP.ok200 of
274+ False -> errorExit $ "Request to hoogle server failed: " <> T.pack (show r)
275+ True -> exit 0
276+ where
277+ verbosity = bool silently verbosely isVerbose
278+ nixBuild args = run nixBuildPath ("--no-out-link" : args)
279+
280+ runOb_ = augmentWithVerbosity run_ ob isVerbose
281+ runOb = augmentWithVerbosity run ob isVerbose
282+ testObRunInDir' = augmentWithVerbosity testObRunInDir ob isVerbose ["run"]
283+ testThunkPack' = augmentWithVerbosity testThunkPack ob isVerbose []
284+
123285 withObeliskImplClean f =
124286 withSystemTempDirectory "obelisk-impl-clean" $ \(fromString -> obeliskImpl) -> do
125287 void . shellyOb verbosity $ chdir obeliskImpl $ do
126− run_ gitPath ["clone", "file://" <> toTextIgnore obeliskImplDirtyReadOnly, toTextIgnore obeliskImpl]
288+ dirtyFiles <- T.strip <$> run gitPath ["-C", toTextIgnore obeliskRepoReadOnly, "diff", "--stat"]
289+ () <- when (dirtyFiles /= "") $ error "SelfTest does not work correctly on dirty repo"
290+ run_ gitPath ["clone", "file://" <> toTextIgnore obeliskRepoReadOnly, toTextIgnore obeliskImpl]
127291 f obeliskImpl
128292
129293 withObeliskImplDirty f =
130294 withSystemTempDirectory "obelisk-impl-copy" $ \(fromString -> obeliskImpl) -> do
131295 void . shellyOb verbosity $ chdir obeliskImpl $ do
132296 user <- T.strip <$> run whoamiPath []
133− run_ cp ["-rT", "--no-preserve=mode", toTextIgnore obeliskImplDirtyReadOnly, toTextIgnore obeliskImpl]
297+ run_ cpPath ["-rT", "--no-preserve=mode", toTextIgnore obeliskRepoReadOnly, toTextIgnore obeliskImpl]
134298 run_ chownPath ["-R", user, toTextIgnore obeliskImpl]
135299 run_ chmodPath ["-R", "g-rw,o-rw", toTextIgnore obeliskImpl]
136300 f obeliskImpl
@@ -147,190 +311,34 @@ main = do
147311 withObeliskImplDirtyAndInitCache f =
148312 withObeliskImplDirty $ \impl -> withInitCache (f impl) impl
149313
150− httpManager <- HTTP.newManager HTTP.defaultManagerSettings
314+ shelly_ = void . shellyOb verbosity
315+
316+ defaultTmpDirName = "test λ"
317+
318+ inTmp' :: FilePath -> (FilePath -> Sh a) -> IO ()
319+ inTmp' dirname f = withTmp' dirname (chdir <*> f)
320+ inTmp = inTmp' defaultTmpDirName
321+
322+ withTmp' dirname f = shelly_ . withSystemTempDirectory dirname $ f . fromString
323+ withTmp = withTmp' defaultTmpDirName
324+
325+ assertRevEQ a b = liftIO . assertEqual "" "" =<< diff a b
326+ assertRevNE a b = liftIO . assertBool "" . (/= "") =<< diff a b
327+
328+ revParseHead = T.strip <$> run gitPath ["rev-parse", "HEAD"]
329+
330+ commitAll = do
331+ run_ gitPath ["add", "."]
332+ commit "checkpoint"
333+ revParseHead
334+
335+ thunk = ".obelisk/impl"
336+ update = runOb ["thunk", "update", thunk] *> commitAll
337+ pack = runOb ["thunk", "pack", thunk] *> commitAll
338+ unpack = runOb ["thunk", "unpack", thunk] *> commitAll
339+
340+ diff a b = run gitPath ["diff", a, b]
151341
152− withObeliskImplDirtyAndInitCache $ \obeliskImplDirty initCache ->
153− hspec $ parallel $ do
154− let shelly_ = void . shellyOb verbosity
155−
156− defaultTmpDirName = "test λ"
157−
158− inTmp' :: FilePath -> (FilePath -> Sh a) -> IO ()
159− inTmp' dirname f = withTmp' dirname (chdir <*> f)
160− inTmp = inTmp' defaultTmpDirName
161−
162− withTmp' dirname f = shelly_ . withSystemTempDirectory dirname $ f . fromString
163− withTmp = withTmp' defaultTmpDirName
164−
165− inTmpObInit' dirname f = inTmp' dirname $ \dir -> do
166− run_ cp ["-rT", fromString initCache, toTextIgnore dir]
167− f dir
168− inTmpObInit = inTmpObInit' defaultTmpDirName
169−
170− -- To be used in tests that change the obelisk impl directory
171− inTmpObInitWithImplCopy f = inTmpObInit $ \dir ->
172− withObeliskImplClean $ \(fromString -> implClean) -> do
173− run_ rmPath [thunk]
174− run_ lnPath ["-s", implClean, thunk]
175− f dir
176−
177− assertRevEQ a b = liftIO . assertEqual "" "" =<< diff a b
178− assertRevNE a b = liftIO . assertBool "" . (/= "") =<< diff a b
179−
180− revParseHead = T.strip <$> run gitPath ["rev-parse", "HEAD"]
181−
182− commitAll = do
183− run_ gitPath ["add", "."]
184− commit "checkpoint"
185− revParseHead
186−
187− thunk = ".obelisk/impl"
188− update = runOb ["thunk", "update", thunk] >> commitAll
189− pack = runOb ["thunk", "pack", thunk] >> commitAll
190− unpack = runOb ["thunk", "unpack", thunk] >> commitAll
191−
192− diff a b = run gitPath ["diff", a, b]
193−
194− describe "ob init" $ parallel $ do
195− it "works with default impl" $ inTmp $ \_ -> runOb ["init"]
196− it "works with master branch impl" $ inTmp $ \_ -> runOb ["init", "--branch", "master"]
197− it "works with symlink" $ inTmp $ \_ -> runOb ["init", "--symlink", toTextIgnore obeliskImplDirty]
198− it "doesn't silently overwrite existing files" $ inTmp $ \dir -> do
199− let p force = errExit False $ do
200− run_ ob $ "--no-handoff" : "-v" : "init" : ["--force"|force]
201− (== 0) <$> lastExitCode
202−
203− True <- p False
204− False <- p False
205− True <- p True
206− pure ()
207−
208− it "doesn't create anything when given an invalid impl" $ inTmp $ \tmp -> do
209− void $ errExit False $ runOb ["init", "--symlink", "/dev/null"]
210− ls tmp >>= liftIO . assertEqual "" []
211−
212− it "produces a valid route config" $ inTmpObInit $ \tmp -> liftIO $
213− withCurrentDirectory (T.unpack $ toTextIgnore tmp) $ do
214− configs <- getConfigs
215− return (either (const Nothing) Just $ getConfigRoute configs) `shouldNotReturn` Nothing
216−
217− -- These tests fail with "Could not find module 'Obelisk.Generated.Static'"
218− -- when not run by 'nix-build --attr selftest'
219− describe "ob run" $ {- NOT parallel $ -} do
220− it "works in root directory" $ inTmpObInit $ \_ -> testObRunInDir' Nothing httpManager
221− it "works in sub directory" $ inTmpObInit $ \_ -> testObRunInDir' (Just "frontend") httpManager
222−
223− describe "obelisk project" $ parallel $ do
224− it "can build obelisk command" $ inTmpObInit $ \_ -> nixBuild ["-A", "command" , toTextIgnore obeliskImplDirty]
225− it "can build obelisk skeleton" $ inTmpObInit $ \_ -> nixBuild ["-A", "skeleton", toTextIgnore obeliskImplDirty]
226− it "can build obelisk shell" $ inTmpObInit $ \_ -> nixBuild ["-A", "shell", toTextIgnore obeliskImplDirty]
227− it "can build everything" $ inTmpObInit $ \_ -> nixBuild [toTextIgnore obeliskImplDirty]
228−
229− describe "blank initialized project" $ parallel $ do
230−
231− it "can build ghc.backend" $ inTmpObInit $ \_ -> nixBuild ["-A", "ghc.backend"]
232− it "can build ghcjs.frontend" $ inTmpObInit $ \_ -> nixBuild ["-A", "ghcjs.frontend"]
233−
234− if System.Info.os == "darwin"
235− then it "can build ios" $ inTmpObInit $ \_ -> nixBuild ["-A", "ios.frontend"]
236− else it "can build android after accepting license" $ inTmpObInit $ \dir -> do
237− let defaultNixPath = dir </> ("default.nix" :: FilePath)
238− writefile defaultNixPath
239− =<< T.replace
240− "# config.android_sdk.accept_license = false;"
241− "config.android_sdk.accept_license = true;"
242− <$> readfile defaultNixPath
243− nixBuild ["-A", "android.frontend"]
244−
245− forM_ ["ghc", "ghcjs"] $ \compiler -> do
246− let
247− shellName = "shells." <> compiler
248− inShell cmd' = run_ "nix-shell" ["default.nix", "-A", fromString shellName, "--run", cmd']
249− it ("can enter " <> shellName) $ inTmpObInit $ \_ -> inShell "exit"
250− -- NOTE: We override the temporary directory name because cabal has a bug preventing new-build from working
251− -- in a path that has unicode characters.
252− it ("can build in " <> shellName) $ inTmpObInit' "test" $ \_ -> inShell $
253− T.pack cabalPath <> " --version; " <> T.pack cabalPath <> " new-build --" <> T.pack compiler <> " all"
254−
255− it "has idempotent thunk update" $ inTmpObInitWithImplCopy $ \_ -> do
256− _ <- pack
257− u <- update
258− uu <- update
259− assertRevEQ u uu
260−
261− describe "ob thunk pack/unpack" $ parallel $ do
262− it "has thunk pack and unpack inverses" $ inTmpObInitWithImplCopy $ \_ -> do
263−
264− _ <- pack
265− e <- commitAll
266− eu <- unpack
267− eup <- pack
268− eupu <- unpack
269− _ <- pack
270−
271− assertRevEQ e eup
272− assertRevEQ eu eupu
273− assertRevNE e eu
274−
275− it "unpacks the correct branch" $ withTmp $ \dir -> do
276− let branch = "master"
277− run_ gitPath ["clone", "https://github.com/reflex-frp/reflex.git", toTextIgnore dir, "--branch", branch]
278− runOb_ ["thunk", "pack", toTextIgnore dir]
279− runOb_ ["thunk", "unpack", toTextIgnore dir]
280− branch' <- chdir dir $ run gitPath ["rev-parse", "--abbrev-ref", "HEAD"]
281− liftIO $ assertEqual "" branch (T.strip branch')
282−
283− it "can pack and unpack plain git repos" $
284− shelly_ $ withSystemTempDirectory "git-repo" $ \dir -> do
285− let repo = toTextIgnore $ dir </> ("repo" :: FilePath)
286− run_ gitPath ["clone", "https://github.com/haskell/process.git", repo]
287− origHash <- chdir (fromText repo) revParseHead
288−
289− runOb_ ["thunk", "pack", repo]
290− packedFiles <- Set.fromList <$> ls (fromText repo)
291− liftIO $ assertEqual "" packedFiles $ Set.fromList $ (repo </>) <$>
292− ["default.nix", "github.json", ".attr-cache" :: String]
293−
294− runOb_ ["thunk", "unpack", repo]
295− chdir (fromText repo) $ do
296− unpackHash <- revParseHead
297− assertRevEQ origHash unpackHash
298−
299− testThunkPack' $ fromText repo
300−
301− it "aborts thunk pack when there are uncommitted files" $ inTmpObInitWithImplCopy $ \dir -> do
302− testThunkPack' (dir </> thunk)
303−
304− describe "ob thunk update --branch" $ parallel $ do
305− it "can change a thunk to the latest version of a desired branch" $ withTmp $ \dir -> do
306− let branch1 = "master"
307− branch2 = "develop"
308− run_ gitPath ["clone", "https://github.com/reflex-frp/reflex.git", toTextIgnore dir, "--branch", branch1]
309− runOb_ ["thunk" , "pack", toTextIgnore dir]
310− runOb_ ["thunk", "update", toTextIgnore dir, "--branch", branch2]
311−
312− it "doesn't create anything when given an invalid branch" $ withTmp $ \dir -> do
313− let checkDir dir' = liftIO $ getDirectoryContents $ T.unpack $ toTextIgnore dir'
314− run_ gitPath ["clone", "https://github.com/reflex-frp/reflex.git", toTextIgnore dir, "--branch", "master"]
315− runOb_ ["thunk" , "pack", toTextIgnore dir]
316− startingContents <- checkDir dir
317− void $ errExit False $ runOb ["thunk", "update", toTextIgnore dir, "--branch", "dumble-palooza"]
318− checkDir dir >>= liftIO . assertEqual "" startingContents
319−
320− describe "ob hoogle" $ {- NOT parallel -} do
321− it "starts a hoogle server on the given port" $ inTmpObInit $ \_ -> do
322− [p0] <- liftIO $ getFreePorts 1
323− maskExitSuccess $ runHandle "ob" ["hoogle", "--port", T.pack (show p0)] $ \stdout -> fix $ \loop -> do
324− ln <- liftIO $ T.hGetLine stdout
325− echo ln
326− let search = "Server starting on port " <> T.pack (show p0)
327− case search `T.isInfixOf` ln of
328− False -> loop -- keep waiting
329− True -> do
330− let req uri = liftIO $ HTTP.parseRequest uri >>= flip HTTP.httpLbs httpManager
331− req ("http://127.0.0.1:" <> show p0) >>= \r -> case HTTP.responseStatus r == HTTP.ok200 of
332− False -> errorExit $ "Request to hoogle server failed: " <> T.pack (show r)
333− True -> exit 0
334342
335343maskExitSuccess :: Sh () -> Sh ()
336344maskExitSuccess = handle_sh (\case ExitSuccess -> pure (); e -> throw e)