diff --git a/app/Main.hs b/app/Main.hs index 92f35f0..c2ba8fb 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -1,7 +1,8 @@ module Main where -import Data.Map (empty) -import Data.Tuple.Extra (uncurry3) +import Control.Monad.Reader +import Data.Map (empty) +import Data.Tuple.Extra (uncurry3) import System.Environment import System.IO @@ -27,20 +28,20 @@ main = do Left NoStackPackageDbError -> putStrLn "Couldn't find an appropriate stack package DB!" Right paths -> do let config = uncurry3 ProgramConfig paths mainProjectExercisesDir stdin stdout stderr empty - runCommand config (tail args) (head args) + runReaderT (runCommand (tail args) (head args)) config -runCommand :: ProgramConfig -> [String] -> String -> IO () -runCommand config restArgs command = case command of +runCommand :: [String] -> String -> ReaderT ProgramConfig IO () +runCommand restArgs command = case command of "run" -> if null restArgs - then progPutStrLn config "Run command requires an exercise name!" - else runExercise config (head restArgs) - "watch" -> watchExercises config + then progPutStrLn "Run command requires an exercise name!" + else runExercise (head restArgs) + "watch" -> watchExercises "exec" -> if null restArgs - then progPutStrLn config "Exec command requires an exercise name!" - else execExercise config (head restArgs) - "version" -> putStrLn haskellingsVersion - "list" -> listExercises config + then progPutStrLn "Exec command requires an exercise name!" + else execExercise (head restArgs) + "version" -> progPutStrLn haskellingsVersion + "list" -> listExercises "hint" -> if null restArgs - then progPutStrLn config "Hint command requires an exercise name!" - else hintExercise config (head restArgs) - _ -> runHelp + then progPutStrLn "Hint command requires an exercise name!" + else hintExercise (head restArgs) + _ -> lift runHelp diff --git a/haskellings.cabal b/haskellings.cabal index b52cbf8..902d8a8 100644 --- a/haskellings.cabal +++ b/haskellings.cabal @@ -44,6 +44,7 @@ library , extra , filepath , fsnotify + , mtl , tasty , tasty-hunit , process @@ -65,6 +66,7 @@ executable haskellings , containers , extra , haskellings + , mtl default-language: Haskell2010 test-suite haskellings-tests @@ -81,6 +83,7 @@ test-suite haskellings-tests , haskellings , hspec , HUnit + , mtl , time default-language: Haskell2010 diff --git a/src/DirectoryUtils.hs b/src/DirectoryUtils.hs index 7bc197a..30628fb 100644 --- a/src/DirectoryUtils.hs +++ b/src/DirectoryUtils.hs @@ -1,17 +1,21 @@ -{- Utility functions for manipulating filepaths and directories. --} +{- Utility functions for manipulating filepaths and directories. -} + +{-# LANGUAGE FlexibleContexts #-} + module DirectoryUtils where import Control.Concurrent -import Control.Exception (catch) +import Control.Exception (catch) +import Control.Monad.IO.Class +import Control.Monad.Reader.Class import Data.Char -import Data.List (isSuffixOf) -import Data.List.Extra (upper) -import qualified Data.Map as M -import qualified Data.Sequence as S +import Data.List (isSuffixOf) +import Data.List.Extra (upper) +import qualified Data.Map as M +import qualified Data.Sequence as S import System.Directory -import System.FilePath (takeBaseName, takeFileName, ()) -import System.Info (os) +import System.FilePath (takeBaseName, takeFileName, ()) +import System.Info (os) import Types @@ -40,22 +44,24 @@ fullExerciseFp :: FilePath -> FilePath -> ExerciseInfo -> FilePath fullExerciseFp projectRoot exercisesExt (ExerciseInfo exName exDir _ _) = projectRoot exercisesExt exDir haskellFileName exName -withFileLock :: FilePath -> ProgramConfig -> IO a -> IO a -withFileLock fp config action = case M.lookup fp (fileLocks config) of - Nothing -> action - Just lock -> do - putMVar lock () - result <- action - takeMVar lock - return result +withFileLock :: (MonadIO m, MonadReader ProgramConfig m) => FilePath -> m a -> m a +withFileLock fp action = do + maybeLock <- M.lookup fp <$> asks fileLocks + case maybeLock of + Nothing -> action + Just lock -> do + liftIO $ putMVar lock () + result <- action + liftIO $ takeMVar lock + return result -- Create a directory. Run the action depending on that directory, -- and then clean the directory up. -withDirectory :: FilePath -> IO a -> IO a +withDirectory :: (MonadIO m) => FilePath -> m a -> m a withDirectory dirPath action = do - createDirectoryIfMissing True dirPath + liftIO $ createDirectoryIfMissing True dirPath res <- action - removeDirectoryRecursive dirPath + liftIO $ removeDirectoryRecursive dirPath return res returnIfDirExists :: FilePath -> IO (Maybe FilePath) diff --git a/src/Processor.hs b/src/Processor.hs index b8e71d2..2ba9e0b 100644 --- a/src/Processor.hs +++ b/src/Processor.hs @@ -5,10 +5,10 @@ -} module Processor where -import Control.Monad (forM_, void, when) -import Data.Maybe (fromJust, isJust) +import Control.Monad.Reader +import Data.Maybe (fromJust, isJust) import System.Exit -import System.FilePath (()) +import System.FilePath (()) import System.IO import System.Process @@ -16,29 +16,29 @@ import DirectoryUtils import TerminalUtils import Types -executeExercise :: ProgramConfig -> ExerciseInfo -> IO () -executeExercise config exInfo@(ExerciseInfo exerciseName _ _ _) = do - let (processSpec, genDirPath, genExecutablePath) = createExerciseProcess config exInfo - let exFilename = haskellFileName exerciseName +executeExercise :: ExerciseInfo -> ReaderT ProgramConfig IO () +executeExercise exInfo@(ExerciseInfo exerciseName _ _ _) = do + config <- ask + let (processSpec, genDirPath, genExecutablePath, exFilename) = createExerciseProcess config exInfo withDirectory genDirPath $ do - (_, _, procStdErr, procHandle) <- createProcess (processSpec { std_out = CreatePipe, std_err = CreatePipe }) - exitCode <- waitForProcess procHandle + (_, _, procStdErr, procHandle) <- lift $ createProcess (processSpec { std_out = CreatePipe, std_err = CreatePipe }) + exitCode <- lift $ waitForProcess procHandle case exitCode of - ExitFailure code -> void $ onCompileFailure config exFilename procStdErr + ExitFailure code -> void $ onCompileFailure exFilename procStdErr ExitSuccess -> do - progPutStrLnSuccess config $ "Successfully compiled: " ++ exFilename - progPutStrLn config $ "----- Executing file: " ++ exFilename ++ " -----" + progPutStrLnSuccess $ "Successfully compiled: " ++ exFilename + progPutStrLn $ "----- Executing file: " ++ exFilename ++ " -----" let execSpec = shell genExecutablePath - (_, _, _, execProcHandle) <- createProcess execSpec - void $ waitForProcess execProcHandle + (_, _, _, execProcHandle) <- lift $ createProcess execSpec + void $ lift $ waitForProcess execProcHandle -- Produces 3 Elements for running our exercise: -- 1. The 'CreateProcess' that we can run for the compilation. -- 2. The directory path for the generated files -- 3. The path of the executable we would run (assuming the exercise is executable). -createExerciseProcess :: ProgramConfig -> ExerciseInfo -> (CreateProcess, FilePath, FilePath) +createExerciseProcess :: ProgramConfig -> ExerciseInfo -> (CreateProcess, FilePath, FilePath, FilePath) createExerciseProcess config (ExerciseInfo exerciseName exDirectory exType _) = - (processSpec, genDirPath, genExecutablePath) + (processSpec, genDirPath, genExecutablePath, haskellFileName exerciseName) where exIsRunnable = exType /= CompileOnly exFilename = haskellFileName exerciseName @@ -51,88 +51,87 @@ createExerciseProcess config (ExerciseInfo exerciseName exDirectory exType _) = finalArgs = execArgs ++ ["-package-db", packageDb config] processSpec = proc (ghcPath config) finalArgs -onCompileFailure :: ProgramConfig -> String -> Maybe Handle -> IO RunResult -onCompileFailure config exFilename errHandle = withTerminalFailure $ do - progPutStrLn config $ "Couldn't compile : " ++ exFilename +onCompileFailure :: String -> Maybe Handle -> ReaderT ProgramConfig IO RunResult +onCompileFailure exFilename errHandle = withTerminalFailure $ do + progPutStrLn $ "Couldn't compile : " ++ exFilename case errHandle of Nothing -> return () - Just h -> hGetContents h >>= progPutStrLn config + Just h -> lift (hGetContents h) >>= progPutStrLn return CompileError -runUnitTestExercise :: ProgramConfig -> FilePath -> String -> IO RunResult -runUnitTestExercise config genExecutablePath exFilename = do +runUnitTestExercise :: FilePath -> String -> ReaderT ProgramConfig IO RunResult +runUnitTestExercise genExecutablePath exFilename = do let execSpec = shell genExecutablePath - (_, execStdOut, execStdErr, execProcHandle) <- createProcess (execSpec { std_out = CreatePipe, std_err = CreatePipe }) - execExit <- waitForProcess execProcHandle + (_, execStdOut, execStdErr, execProcHandle) <- lift $ createProcess (execSpec { std_out = CreatePipe, std_err = CreatePipe }) + execExit <- lift $ waitForProcess execProcHandle case execExit of ExitFailure code -> withTerminalFailure $ do - progPutStrLn config $ "Tests failed on exercise : " ++ exFilename + progPutStrLn $ "Tests failed on exercise : " ++ exFilename case execStdErr of Nothing -> return () - Just h -> hGetContents h >>= progPutStrLn config + Just h -> lift (hGetContents h) >>= progPutStrLn case execStdOut of Nothing -> return () - Just h -> hGetContents h >>= progPutStrLn config + Just h -> lift (hGetContents h) >>= progPutStrLn return TestFailed ExitSuccess -> do - progPutStrLnSuccess config $ "Successfully ran : " ++ exFilename + progPutStrLnSuccess $ "Successfully ran : " ++ exFilename return RunSuccess runExecutableExercise - :: ProgramConfig - -> FilePath + :: FilePath -> String -> [String] -> ([String] -> Bool) - -> IO RunResult -runExecutableExercise config genExecutablePath exFilename inputs outputPred = do + -> ReaderT ProgramConfig IO RunResult +runExecutableExercise genExecutablePath exFilename inputs outputPred = do let execSpec = shell genExecutablePath - (execStdIn, execStdOut, execStdErr, execProcHandle) <- createProcess + (execStdIn, execStdOut, execStdErr, execProcHandle) <- lift $ createProcess (execSpec { std_out = CreatePipe, std_err = CreatePipe, std_in = CreatePipe }) - when (isJust execStdIn) $ forM_ inputs $ \i -> hPutStrLn (fromJust execStdIn) i - execExit <- waitForProcess execProcHandle + when (isJust execStdIn) $ forM_ inputs $ \i -> lift $ hPutStrLn (fromJust execStdIn) i + execExit <- lift $ waitForProcess execProcHandle case execExit of ExitFailure code -> withTerminalFailure $ do - progPutStrLn config $ "Encountered error running exercise: " ++ exFilename + progPutStrLn $ "Encountered error running exercise: " ++ exFilename case execStdOut of Nothing -> return () - Just h -> hGetContents h >>= progPutStrLn config + Just h -> lift (hGetContents h) >>= progPutStrLn case execStdErr of Nothing -> return () - Just h -> hGetContents h >>= progPutStrLn config - progPutStrLn config "Check the Sample Input and Sample Output in the file." - progPutStrLn config $ "Then try running it for yourself with 'haskellings exec" ++ haskellModuleName exFilename ++ "'." + Just h -> lift (hGetContents h) >>= progPutStrLn + progPutStrLn "Check the Sample Input and Sample Output in the file." + progPutStrLn $ "Then try running it for yourself with 'haskellings exec" ++ haskellModuleName exFilename ++ "'." return TestFailed ExitSuccess -> do passes <- case execStdOut of Nothing -> return (outputPred []) - Just h -> (lines <$> hGetContents h) >>= (return . outputPred) + Just h -> (lines <$> lift (hGetContents h)) >>= (return . outputPred) if passes then withTerminalSuccess $ do - progPutStrLn config $ "Successfully ran : " ++ exFilename - progPutStrLn config $ "You can run this code for yourself with 'haskellings exec " ++ haskellModuleName exFilename ++ "'." + progPutStrLn $ "Successfully ran : " ++ exFilename + progPutStrLn $ "You can run this code for yourself with 'haskellings exec " ++ haskellModuleName exFilename ++ "'." return RunSuccess else withTerminalFailure $ do - progPutStrLn config $ "Unexpected output for exercise: " ++ exFilename - progPutStrLn config "Check the Sample Input and Sample Output in the file." - progPutStrLn config $ "Then try running it for yourself with 'haskellings exec " ++ haskellModuleName exFilename ++ "'." + progPutStrLn $ "Unexpected output for exercise: " ++ exFilename + progPutStrLn "Check the Sample Input and Sample Output in the file." + progPutStrLn $ "Then try running it for yourself with 'haskellings exec " ++ haskellModuleName exFilename ++ "'." return TestFailed -compileAndRunExercise :: ProgramConfig -> ExerciseInfo -> IO RunResult -compileAndRunExercise config exInfo@(ExerciseInfo exerciseName exDirectory exType _) = do - let (processSpec, genDirPath, genExecutablePath) = createExerciseProcess config exInfo - let exFilename = haskellFileName exerciseName +compileAndRunExercise :: ExerciseInfo -> ReaderT ProgramConfig IO RunResult +compileAndRunExercise exInfo@(ExerciseInfo exerciseName exDirectory exType _) = do + config <- ask + let (processSpec, genDirPath, genExecutablePath, exFilename) = createExerciseProcess config exInfo withDirectory genDirPath $ do - (_, _, procStdErr, procHandle) <- createProcess (processSpec { std_out = CreatePipe, std_err = CreatePipe }) - exitCode <- waitForProcess procHandle + (_, _, procStdErr, procHandle) <- lift $ createProcess (processSpec { std_out = CreatePipe, std_err = CreatePipe }) + exitCode <- lift $ waitForProcess procHandle case exitCode of - ExitFailure code -> onCompileFailure config exFilename procStdErr + ExitFailure code -> onCompileFailure exFilename procStdErr ExitSuccess -> do - progPutStrLnSuccess config $ "Successfully compiled : " ++ exFilename + progPutStrLnSuccess $ "Successfully compiled : " ++ exFilename case exType of CompileOnly -> return RunSuccess - UnitTests -> runUnitTestExercise config genExecutablePath exFilename - Executable inputs outputPred -> runExecutableExercise config genExecutablePath exFilename inputs outputPred + UnitTests -> runUnitTestExercise genExecutablePath exFilename + Executable inputs outputPred -> runExecutableExercise genExecutablePath exFilename inputs outputPred -compileAndRunExercise_ :: ProgramConfig -> ExerciseInfo -> IO () -compileAndRunExercise_ config ex = void $ compileAndRunExercise config ex +compileAndRunExercise_ :: ExerciseInfo -> ReaderT ProgramConfig IO () +compileAndRunExercise_ ex = void $ compileAndRunExercise ex diff --git a/src/RunCommands.hs b/src/RunCommands.hs index 35248e0..2b9150b 100644 --- a/src/RunCommands.hs +++ b/src/RunCommands.hs @@ -3,12 +3,12 @@ -} module RunCommands where -import Control.Concurrent (threadDelay) -import Control.Monad (forM_, when) -import qualified Data.Map as M -import Data.Yaml (encodeFile) +import Control.Concurrent (threadDelay) +import Control.Monad.Reader +import qualified Data.Map as M +import Data.Yaml (encodeFile) import System.Directory -import System.FilePath (()) +import System.FilePath (()) import Constants import DirectoryUtils @@ -18,45 +18,46 @@ import Processor import TerminalUtils import Types -runExercise :: ProgramConfig -> String -> IO () -runExercise config exerciseName = case M.lookup exerciseName allExercisesMap of - Nothing -> progPutStrLn config $ "Could not find exercise: " ++ exerciseName ++ "!" - Just exInfo -> compileAndRunExercise_ config exInfo +runExercise :: String -> ReaderT ProgramConfig IO () +runExercise exerciseName = case M.lookup exerciseName allExercisesMap of + Nothing -> progPutStrLn $ "Could not find exercise: " ++ exerciseName ++ "!" + Just exInfo -> compileAndRunExercise_ exInfo -execExercise :: ProgramConfig -> String -> IO () -execExercise config exerciseName = case M.lookup exerciseName allExercisesMap of - Nothing -> progPutStrLn config $ "Could not find exercise: " ++ exerciseName ++ "!" - Just exInfo@(ExerciseInfo _ _ (Executable _ _) _) -> executeExercise config exInfo - _ -> progPutStrLn config $ "Exercise " ++ exerciseName ++ " is not executable!" +execExercise :: String -> ReaderT ProgramConfig IO () +execExercise exerciseName = case M.lookup exerciseName allExercisesMap of + Nothing -> progPutStrLn $ "Could not find exercise: " ++ exerciseName ++ "!" + Just exInfo@(ExerciseInfo _ _ (Executable _ _) _) -> executeExercise exInfo + _ -> progPutStrLn $ "Exercise " ++ exerciseName ++ " is not executable!" -hintExercise :: ProgramConfig -> String -> IO () -hintExercise config exerciseName = case M.lookup exerciseName allExercisesMap of - Nothing -> progPutStrLn config $ "Could not find exercise: " ++ exerciseName ++ "!" - Just exInfo -> progPutStrLn config (exerciseHint exInfo) +hintExercise :: String -> ReaderT ProgramConfig IO () +hintExercise exerciseName = case M.lookup exerciseName allExercisesMap of + Nothing -> progPutStrLn $ "Could not find exercise: " ++ exerciseName ++ "!" + Just exInfo -> progPutStrLn (exerciseHint exInfo) -listExercises :: ProgramConfig -> IO () +listExercises :: ReaderT ProgramConfig IO () listExercises = listExercises' allExercises -- Separated for testability -listExercises' :: [ExerciseInfo] -> ProgramConfig -> IO () -listExercises' [] config = progPutStrLn config "No exercises!" -listExercises' exercises config = do - progPutStrLn config "Listing exercises...(must remove \"I AM NOT DONE\" comment to indicate as done)" - threadDelay 2000000 +listExercises' :: [ExerciseInfo] -> ReaderT ProgramConfig IO () +listExercises' [] = progPutStrLn "No exercises!" +listExercises' exercises = do + config <- ask + progPutStrLn "Listing exercises...(must remove \"I AM NOT DONE\" comment to indicate as done)" + lift $ threadDelay 2000000 let maxNameSize = maximum (length . exerciseName <$> exercises) forM_ (zip [1..] exercises) $ \(i, exInfo) -> do let fullFp = fullExerciseFp (projectRoot config) (exercisesExt config) exInfo let name = exerciseName exInfo - isNotDone <- fileContainsNotDone fullFp + isNotDone <- lift $ fileContainsNotDone fullFp let printNameAndDots = do - when (i < 10) (progPutStr config " ") - progPutStr config (show i) - progPutStr config ": " - progPutStr config name - progPutStr config $ replicate (maxNameSize - length name) '.' + when (i < 10) (progPutStr " ") + progPutStr (show i) + progPutStr ": " + progPutStr name + progPutStr $ replicate (maxNameSize - length name) '.' if isNotDone - then withTerminalFailure $ printNameAndDots >> progPutStrLn config "...NOT DONE" - else withTerminalSuccess $ printNameAndDots >> progPutStrLn config ".......DONE" + then withTerminalFailure $ printNameAndDots >> progPutStrLn "...NOT DONE" + else withTerminalSuccess $ printNameAndDots >> progPutStrLn ".......DONE" runHelp :: IO () runHelp = mapM_ putStrLn diff --git a/src/TerminalUtils.hs b/src/TerminalUtils.hs index 2335129..d434c60 100644 --- a/src/TerminalUtils.hs +++ b/src/TerminalUtils.hs @@ -3,49 +3,61 @@ -} module TerminalUtils where +import Control.Monad.IO.Class +import Control.Monad.Reader import System.Console.ANSI import System.IO import Types -progPutStr :: ProgramConfig -> String -> IO () -progPutStr pc = hPutStr (outHandle pc) +progPutStr :: String -> ReaderT ProgramConfig IO () +progPutStr str = do + handle <- asks outHandle + lift $ hPutStr handle str -progPutStrLn :: ProgramConfig -> String -> IO () -progPutStrLn pc = hPutStrLn (outHandle pc) +progPutStrLn :: String -> ReaderT ProgramConfig IO () +progPutStrLn str = do + handle <- asks outHandle + lift $ hPutStrLn handle str -progPrint :: (Show a) => ProgramConfig -> a -> IO () -progPrint pc = hPrint (outHandle pc) +progPrint :: (Show a) => a -> ReaderT ProgramConfig IO () +progPrint val = do + handle <- asks outHandle + lift $ hPrint handle val -progPutStrErr :: ProgramConfig -> String -> IO () -progPutStrErr pc = hPutStrLn (errHandle pc) +progPutStrErr :: String -> ReaderT ProgramConfig IO () +progPutStrErr str = do + handle <- asks errHandle + lift $ hPutStrLn handle str -progPrintErr :: (Show a) => ProgramConfig -> a -> IO () -progPrintErr pc = hPrint (errHandle pc) +progPrintErr :: (Show a) => a -> ReaderT ProgramConfig IO () +progPrintErr val = do + handle <- asks errHandle + lift $ hPrint handle val -progReadLine :: ProgramConfig -> IO String -progReadLine pc = hGetLine (inHandle pc) +progReadLine :: ReaderT ProgramConfig IO String +progReadLine = asks inHandle >>= (lift . hGetLine) -- Perform an action with 'Green' Terminal Text -withTerminalSuccess :: IO a -> IO a +withTerminalSuccess :: (MonadIO m) => m a -> m a withTerminalSuccess = withTerminalColor Green -- Perform an action with 'Red' Terminal Text -withTerminalFailure :: IO a -> IO a +withTerminalFailure :: (MonadIO m) => m a -> m a withTerminalFailure = withTerminalColor Red -- Perform an action with printed output given a color. -withTerminalColor :: Color -> IO a -> IO a +withTerminalColor :: (MonadIO m) => Color -> m a -> m a withTerminalColor color action = do - setSGR [SetColor Foreground Vivid color] + liftIO $ setSGR [SetColor Foreground Vivid color] res <- action - setSGR [Reset] + liftIO $ setSGR [Reset] return res -- Print a line, but in Green -progPutStrLnSuccess :: ProgramConfig -> String -> IO () -progPutStrLnSuccess pc output = withTerminalSuccess (progPutStrLn pc output) +progPutStrLnSuccess :: String -> ReaderT ProgramConfig IO () +progPutStrLnSuccess output = withTerminalSuccess (progPutStrLn output) -- Print a line, but in Red -progPutStrLnFailure :: ProgramConfig -> String -> IO () -progPutStrLnFailure pc output = withTerminalFailure (progPutStrLn pc output) +progPutStrLnFailure :: String -> ReaderT ProgramConfig IO () +progPutStrLnFailure output = withTerminalFailure (progPutStrLn output) diff --git a/src/Watcher.hs b/src/Watcher.hs index c73bb69..87f9637 100644 --- a/src/Watcher.hs +++ b/src/Watcher.hs @@ -4,10 +4,10 @@ module Watcher where import Control.Concurrent -import Control.Monad (forever, void, when) -import System.FilePath (takeFileName, ()) +import Control.Monad.Reader +import System.FilePath (takeFileName, ()) import System.FSNotify -import System.IO (hIsEOF) +import System.IO (hIsEOF) import DirectoryUtils import ExerciseList @@ -15,8 +15,8 @@ import Processor import TerminalUtils import Types -watchExercises :: ProgramConfig -> IO () -watchExercises config = runExerciseWatch config allExercises +watchExercises :: ReaderT ProgramConfig IO () +watchExercises = runExerciseWatch allExercises shouldCheckFile :: ExerciseInfo -> Event -> Bool shouldCheckFile (ExerciseInfo exName _ _ _) (Added fp _ _) = takeFileName fp == haskellFileName exName @@ -24,51 +24,52 @@ shouldCheckFile (ExerciseInfo exName _ _ _) (Modified fp _ _) = takeFileName fp shouldCheckFile _ _ = False -- This event should be a modification of one of our exercise files -processEvent :: ProgramConfig -> ExerciseInfo -> MVar () -> Event -> IO () -processEvent config exerciseInfo signalMVar _ = do - progPutStrLn config $ "Running exercise: " ++ exerciseName exerciseInfo - withFileLock fullFp config $ do - runResult <- compileAndRunExercise config exerciseInfo +processEvent :: ExerciseInfo -> MVar () -> Event -> ReaderT ProgramConfig IO () +processEvent exerciseInfo signalMVar _ = do + config <- ask + let fullFp = fullExerciseFp (projectRoot config) (exercisesExt config) exerciseInfo + progPutStrLn $ "Running exercise: " ++ exerciseName exerciseInfo + withFileLock fullFp $ do + runResult <- compileAndRunExercise exerciseInfo case runResult of RunSuccess -> do - isNotDone <- fileContainsNotDone fullFp + isNotDone <- lift $ fileContainsNotDone fullFp if isNotDone - then progPutStrLn config "This exercise succeeds! Remove 'I AM NOT DONE' to proceed!" - else putMVar signalMVar () + then progPutStrLn "This exercise succeeds! Remove 'I AM NOT DONE' to proceed!" + else lift $ putMVar signalMVar () _ -> return () - where - fullFp = fullExerciseFp (projectRoot config) (exercisesExt config) exerciseInfo -runExerciseWatch :: ProgramConfig -> [ExerciseInfo] -> IO () -runExerciseWatch config [] = progPutStrLn config "Congratulations, you've completed all the exercises!" -runExerciseWatch config (firstEx : restExs) = do - (runResult, isDone) <- withFileLock fullFp config $ do - runResult <- compileAndRunExercise config firstEx - isDone <- not <$> fileContainsNotDone fullFp +runExerciseWatch :: [ExerciseInfo] -> ReaderT ProgramConfig IO () +runExerciseWatch [] = progPutStrLn "Congratulations, you've completed all the exercises!" +runExerciseWatch (firstEx : restExs) = do + config <- ask + let fullFp = fullExerciseFp (projectRoot config) (exercisesExt config) firstEx + (runResult, isDone) <- withFileLock fullFp $ do + runResult <- compileAndRunExercise firstEx + isDone <- lift (not <$> fileContainsNotDone fullFp) return (runResult, isDone) if runResult == RunSuccess && isDone - then runExerciseWatch config restExs + then runExerciseWatch restExs else do - when (runResult == RunSuccess) $ progPutStrLn config "This exercise succeeds! Remove 'I AM NOT DONE' to proceed!" + when (runResult == RunSuccess) $ progPutStrLn "This exercise succeeds! Remove 'I AM NOT DONE' to proceed!" let conf = defaultConfig { confDebounce = Debounce 1 } - withManagerConf conf $ \mgr -> do + liftIO $ withManagerConf conf $ \mgr -> do signalMVar <- newEmptyMVar stopAction <- watchTree mgr (projectRoot config exercisesExt config) (shouldCheckFile firstEx) - (processEvent config firstEx signalMVar) + (\event -> runReaderT (processEvent firstEx signalMVar event) config) userInputThread <- forkIO $ forever (watchForUserInput config firstEx) takeMVar signalMVar stopAction forkIO $ killThread userInputThread - runExerciseWatch config restExs - where - fullFp = fullExerciseFp (projectRoot config) (exercisesExt config) firstEx + runExerciseWatch restExs +-- Must be IO because it is called through forkIO watchForUserInput :: ProgramConfig -> ExerciseInfo -> IO () watchForUserInput config exInfo = do inIsEnd <- hIsEOF (inHandle config) if inIsEnd then void (threadDelay 1000000) - else do - userInput <- progReadLine config + else flip runReaderT config $ do + userInput <- progReadLine when (userInput == "hint") $ - progPutStrLn config (exerciseHint exInfo) + progPutStrLn (exerciseHint exInfo) diff --git a/tests/Main.hs b/tests/Main.hs index d24b691..3e3930b 100644 --- a/tests/Main.hs +++ b/tests/Main.hs @@ -1,9 +1,10 @@ import Control.Concurrent +import Control.Monad.Reader import Data.List -import qualified Data.Map as M +import qualified Data.Map as M import Data.Time import System.Directory -import System.FilePath (()) +import System.FilePath (()) import System.IO import Test.Hspec import Test.HUnit @@ -41,7 +42,7 @@ compileBeforeHook (projectRoot, ghcPath, packageDb) exInfo outFile = do let fullFp = projectRoot "tests" "test_gen" outFile outHandle <- openFile fullFp WriteMode let conf = ProgramConfig projectRoot ghcPath packageDb ("tests" "exercises") stdin outHandle stderr M.empty - resultExit <- compileAndRunExercise conf exInfo + resultExit <- runReaderT (compileAndRunExercise exInfo) conf hClose outHandle programOutput <- readFile fullFp return (programOutput, resultExit) @@ -224,7 +225,7 @@ makeModifications :: ProgramConfig -> [(FilePath, FilePath)] -> IO () makeModifications _ [] = return () makeModifications conf ((src, dst) : rest) = do threadDelay 1000000 - withFileLock dst conf $ do + flip runReaderT conf $ withFileLock dst $ lift $ do removeFile dst copyFile src dst getCurrentTime >>= setModificationTime dst @@ -245,7 +246,7 @@ beforeWatchHook (projectRoot, ghcPath, stackPackageDb) outFile = do lock2 <- newEmptyMVar let locks = M.fromList [(fullDest1, lock1), (fullDest2, lock2)] let conf = ProgramConfig projectRoot ghcPath stackPackageDb testExercisesDir inHandle outHandle stderr locks - watchTid <- forkIO (runExerciseWatch conf watchTestExercises) + watchTid <- forkIO (runReaderT (runExerciseWatch watchTestExercises) conf) -- Modify Files makeModifications conf modifications killThread watchTid @@ -284,7 +285,7 @@ listBeforeHook (projectRoot, ghcPath, stackPackageDb) outFile = do let fullFp = projectRoot "tests" "test_gen" outFile outHandle <- openFile fullFp WriteMode let conf = ProgramConfig projectRoot ghcPath stackPackageDb ("tests" "exercises") stdin outHandle stderr M.empty - listExercises' listTestExercises conf + runReaderT (listExercises' listTestExercises) conf hClose outHandle readFile fullFp