packages feed

testloop (empty) → 0.1.0.0

raw patch · 9 files changed

+465/−0 lines, 9 filesdep +Cabaldep +basedep +directorysetup-changed

Dependencies added: Cabal, base, directory, filepath, fsnotify, hint, mtl, system-filepath, time, unix

Files

+ LICENSE view
@@ -0,0 +1,7 @@+Copyright (c) 2013 Roman Gonzalez++Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ src/System/TestLoop.hs view
@@ -0,0 +1,104 @@+{-# LANGUAGE OverloadedStrings #-}+-- |+-- Module: TestLoop.Main+-- Copyright: 2013 Roman Gonzalez+-- License: MIT+--+-- Maintainer: romanandreg@gmail.com+-- Portability: unix+--+module System.TestLoop (+  -- * Main function+  setupTestLoop+  ) where++--------------------++import           Control.Concurrent               (forkIO, threadDelay)+import           Control.Monad                    (forM_, forever)+import           Data.Maybe                       (catMaybes)+import           Data.Monoid                      (mconcat)+import           System.Directory                 (doesFileExist)+import           System.FilePath                  (joinPath)+import           System.IO                        (hPutStrLn, stderr)++--------------------++import qualified Filesystem.Path.CurrentOS        as FS++--------------------++import           System.FSNotify                  (withManager)+import           System.FSNotify.Devel            (treeExtExists)+--------------------++import           System.TestLoop.Internal.Cabal+import           System.TestLoop.Internal.Types+import           System.TestLoop.Internal.Watcher+import           System.TestLoop.Util++--------------------------------------------------------------------------------++startTestLoop :: MainModuleName -> MainModulePath -> HsSourcePaths -> IO ()+startTestLoop moduleName modulePath paths =+   withManager $ \manager -> do+     forM_ paths $ \path -> do+       treeExtExists manager+                     (FS.decodeString path)+                     "hs"+                     (reloadTestSuite moduleName modulePath paths)+     forever $ threadDelay 100++--------------------------------------------------------------------------------++getTestMainFilePath :: HsSourcePaths -> MainModulePath -> IO (Either String FilePath)+getTestMainFilePath sourcePaths modulePath = do+    mainPaths <- mapM getPossiblePath sourcePaths+    case (catMaybes mainPaths) of+      [completeModulePath] -> return $ Right completeModulePath+      [] -> return . Left $ mconcat [ "Could not find `",+                                      modulePath,+                                      "' in ",+                                      show sourcePaths]+      multipleMatches -> return . Left $ mconcat [ "Multiple matches for test `Main' module"+                                                , "on source-paths: \n",+                                                show mainPaths ]++  where+    getPossiblePath sourcePath = do+      let completeModulePath = joinPath [sourcePath, modulePath]+      fileExists <- doesFileExist completeModulePath+      if fileExists+        then return $ Just completeModulePath+        else return Nothing++--------------------------------------------------------------------------------++-- | Parses your project's cabal file to find possible test-suites you+--   may have on your project, then it will start a file modification+--   tracking and once a file is changed it will run the testsuite+--   automatically.  in the test-suite's hs-source-dirs setting.+--+-- Use this function as the main of you testloop executable.+-- e.g+--+-- > module Main where+-- >+-- > import System.TestLoop+-- >+-- > main :: IO ()+-- > main = setupTestLoop+--+setupTestLoop :: IO ()+setupTestLoop = do+ (testsuite, moduleFile, sourcePaths) <- parseCabalFile+ result <- getTestMainFilePath sourcePaths moduleFile+ case result of+   Left e -> hPutStrLn stderr e+   Right fullModuleFilePath -> do+     putStrLn $ "Found test-suite main function on `" ++ fullModuleFilePath ++ "'"+     putStrLn $ "Listening files on source paths: " ++ (join ", " sourcePaths)+     _ <- forkIO $ startTestLoop "Main"+                                 fullModuleFilePath+                                 sourcePaths+     forever $ threadDelay 100
+ src/System/TestLoop/Internal/Cabal.hs view
@@ -0,0 +1,88 @@+module System.TestLoop.Internal.Cabal (parseCabalFile) where++--------------------++import           Control.Applicative                   ((<$>), (<*>))+import           Data.List                             (isInfixOf)+import           Data.Monoid                           (First (..), mconcat)+import           System.Directory                      (getCurrentDirectory,+                                                        getDirectoryContents,+                                                        getHomeDirectory)+import           System.Environment                    (getArgs)+import           System.FilePath                       (joinPath, takeDirectory)++--------------------++import           Distribution.PackageDescription       (CondTree (..), GenericPackageDescription (..),+                                                        TestSuite (..),+                                                        TestSuiteInterface (..),+                                                        condTreeData,+                                                        hsSourceDirs,+                                                        testBuildInfo)+import           Distribution.PackageDescription.Parse (readPackageDescription)+import           Distribution.Verbosity                (normal)++--------------------++import           System.TestLoop.Internal.Types++--------------------------------------------------------------------------------++getTestSuiteToRun :: IO (Maybe String)+getTestSuiteToRun = do+    args <- getArgs+    case args of+      (x:_) -> return (Just x)+      _ -> return Nothing++parseTestSuiteInfo :: Maybe String+                   -> (String, CondTree a b TestSuite)+                   -> Maybe (String, String, [String])+parseTestSuiteInfo (Just inputName) (name, CondNode { condTreeData=testSuite })+    | inputName == name =+      case testInterface testSuite of+        TestSuiteExeV10 _ file -> Just (name, file, hsSourceDirs $ testBuildInfo testSuite)+        _ -> Nothing+    | otherwise = Nothing+parseTestSuiteInfo Nothing input@(name, _) = parseTestSuiteInfo (Just name) input++getCabalFilePathFrom :: FilePath -> IO (Maybe FilePath)+getCabalFilePathFrom originalPath =+    getHomeDirectory >>= loop originalPath+  where+    loop currentPath finalPath = do+      if currentPath == finalPath+        then return Nothing+        else do+          contents <- getDirectoryContents currentPath+          case dropWhile (not . (".cabal" `isInfixOf`)) contents of+            [] -> loop (takeDirectory currentPath) finalPath+            (result:_) -> return $ Just (joinPath [currentPath, result])+++getCabalFilePath :: IO (Maybe FilePath)+getCabalFilePath = do+    getCurrentDirectory >>= getCabalFilePathFrom++parseCabalFile_ :: Maybe String+                -> GenericPackageDescription+                -> Maybe (String, String, [String])+parseCabalFile_ testSuiteName genericPackDesc =+    getFirst . mconcat $ map (First . parseTestSuiteInfo testSuiteName)+                             (condTestSuites genericPackDesc)++parseCabalFile :: IO (TestSuiteName, MainModuleName, HsSourcePaths)+parseCabalFile = do+    mcabalFilePath <- getCabalFilePath+    case mcabalFilePath of+      Just cabalFilePath -> do+        result <- parseCabalFile_ <$> getTestSuiteToRun+                                  <*> readPackageDescription normal cabalFilePath+        maybe (error $ msg ++ cabalFilePath)+              return+              result+      Nothing ->+          error "Couldn't find a cabal file in this directory"+  where+    msg = mconcat [ "You need to have at least one test-suite "+                  , "with type == exitcode-stdio-1.0 on "]
+ src/System/TestLoop/Internal/Signal.hs view
@@ -0,0 +1,58 @@+{-# LANGUAGE CPP #-}+-- Shamelessly stolen from snap-loader-dynamic+module System.TestLoop.Internal.Signal (protectHandlers) where++------------------------------------------------------------------------------+import Control.Exception (bracket)+++#ifdef mingw32_HOST_OS++                                 -------------+                                 -- windows --+                                 -------------+------------------------------------------------------------------------------+import GHC.ConsoleHandler as C++saveHandlers :: IO C.Handler+saveHandlers = C.installHandler Ignore++restoreHandlers :: C.Handler -> IO C.Handler+restoreHandlers = C.installHandler+------------------------------------------------------------------------------+++#else++                                  -----------+                                  -- posix --+                                  -----------+------------------------------------------------------------------------------+import qualified System.Posix.Signals as S++helper :: S.Handler -> S.Signal -> IO S.Handler+helper handler signal = S.installHandler signal handler Nothing++signals :: [S.Signal]+signals = [ S.sigQUIT+          , S.sigINT+          , S.sigHUP+          , S.sigTERM+          ]++saveHandlers :: IO [S.Handler]+saveHandlers = mapM (helper S.Ignore) signals++restoreHandlers :: [S.Handler] -> IO [S.Handler]+restoreHandlers h = sequence $ zipWith helper h signals+------------------------------------------------------------------------------++#endif++                                  ----------+                                  -- both --+                                  ----------+------------------------------------------------------------------------------+protectHandlers :: IO a -> IO a+protectHandlers a = bracket saveHandlers restoreHandlers (const a)+------------------------------------------------------------------------------
+ src/System/TestLoop/Internal/Types.hs view
@@ -0,0 +1,6 @@+module System.TestLoop.Internal.Types where++type TestSuiteName = String+type MainModulePath = String+type MainModuleName = String+type HsSourcePaths = [String]
+ src/System/TestLoop/Internal/Watcher.hs view
@@ -0,0 +1,102 @@+module System.TestLoop.Internal.Watcher (reloadTestSuite) where++--------------------++import           Control.Monad.Trans                 (MonadIO (..))+import           Data.List                           (intercalate, isPrefixOf,+                                                      nub)+import           Data.Monoid                         (mconcat)+import qualified Filesystem.Path                     as FS+import qualified Filesystem.Path.CurrentOS           as FS++--------------------++import           Language.Haskell.Interpreter        (InterpreterError (..), as,+                                                      errMsg, interpret,+                                                      loadModules, setImportsQ,+                                                      setTopLevelModules)+import           Language.Haskell.Interpreter.Unsafe (unsafeRunInterpreterWithArgs)++--------------------++import           Data.Time.LocalTime                 (getZonedTime)++--------------------++import           System.Directory                    (doesDirectoryExist,+                                                      getCurrentDirectory,+                                                      getDirectoryContents)+import           System.FilePath                     (joinPath)++--------------------++import           System.TestLoop.Internal.Signal+import           System.TestLoop.Internal.Types+import           System.TestLoop.Util++--------------------------------------------------------------------------------++getPackageDatabaseFile :: IO (Maybe FilePath)+getPackageDatabaseFile = do+  cabalDevExists <- doesDirectoryExist "cabal-dev"+  if (not cabalDevExists)+     then return Nothing+     else do+       dir <- getCurrentDirectory+       let cabalDevDir = joinPath [dir, "cabal-dev"]+       packages <- getDirectoryContents cabalDevDir+       case filter ("packages-" `isPrefixOf`) packages of+         (packagesFile:_) -> return $ Just (joinPath [cabalDevDir, packagesFile])+         _ -> return Nothing+++reloadTestSuite :: MainModuleName+                -> MainModulePath+                -> HsSourcePaths+                -> FS.FilePath+                -> IO ()+reloadTestSuite moduleName modulePath sourcePaths modifiedFile+  | isNotEmacsFile = do+    reloadTestSuite_+  | otherwise = return ()+  where+    isNotEmacsFile = not ('#' `elem` (FS.encodeString $ FS.filename modifiedFile))+    reloadTestSuite_ = do+      printTimeHeader+      result <- protectHandlers runInterpreter+      case result of+        Left err -> putStrLn "" >> putStrLn (format err)+        Right _ -> return ()++    printTimeHeader = do+      time <- getZonedTime+      putStrLn ""+      putStrLn $ replicate 80 '-'+      putStr "-- "+      putStr (show time)+      putStr " "+      let remaindingWidth = 76 - (length (show time))+      putStrLn $ replicate remaindingWidth '-'++    -- shamelessly stolen from snap-loader-dynamic+    format :: InterpreterError -> String+    format (UnknownError e)   = "Unknown interpreter error:\r\n\r\n" ++ e+    format (NotAllowed e)     = "Interpreter action not allowed:\r\n\r\n" ++ e+    format (GhcException e)   = "GHC error:\r\n\r\n" ++ e+    format (WontCompile errs) = concat ["Compile errors:\r\n\r\n",+                                        intercalate "\r\n" $ nub $ map errMsg errs]++    runInterpreter = do+      mPackageDatabaseFile <- getPackageDatabaseFile+      let args = case mPackageDatabaseFile of+                   Just file -> [ "-i " ++ join ":" sourcePaths+                                , "-package-conf " ++ file]+                   Nothing -> [ "-i " ++ join ":" sourcePaths]+      unsafeRunInterpreterWithArgs args interpreterAction++    interpreterAction = do+      loadModules [modulePath]+      setTopLevelModules [moduleName]+      setImportsQ [("Prelude", Nothing)]+      execution <- interpret "main" (as :: IO ())+      liftIO $ execution
+ src/System/TestLoop/Util.hs view
@@ -0,0 +1,10 @@+module System.TestLoop.Util where++--------------------++import           Data.List (intersperse)++--------------------------------------------------------------------------------++join :: [a] -> [[a]] -> [a]+join delim l = concat (intersperse delim l)
+ testloop.cabal view
@@ -0,0 +1,88 @@+-- Initial testloop.cabal generated by cabal init.  For further+-- documentation, see http://haskell.org/cabal/users-guide/++-- The name of the package.+name:                testloop++-- The package version.  See the Haskell package versioning policy (PVP)+-- for standards guiding when and how versions should be incremented.+-- http://www.haskell.org/haskellwiki/Package_versioning_policy+-- PVP summary:      +-+------- breaking API changes+--                   | | +----- non-breaking API additions+--                   | | | +--- code changes with no API change+version:             0.1.0.0++-- A short (one-line) description of the package.+synopsis:            Quick feedback loop for test suites++-- A longer description of the package.+description:+  TestLoop provides an automated execution and code reloading of+  your project's test-suites whenever a haskell source file is+  modified.++  To get started check out http://github.com/roman/testloop++-- URL for the project homepage or repository.+homepage:            http://github.com/roman/testloop++-- The license under which the package is released.+license:             MIT++-- The file containing the license text.+license-file:        LICENSE++-- The package author(s).+author:              Roman Gonzalez++-- An email address to which users can send suggestions, bug reports, and+-- patches.+maintainer:          romanandreg@gmail.com++-- A copyright notice.+-- copyright:+++category:            Testing, Development++build-type:          Simple++-- Constraint on the version of Cabal needed to build this package.+cabal-version:       >=1.8+++library+  -- Modules exported by the library.+  hs-source-dirs: src+  exposed-modules:+    System.TestLoop++  -- Modules included in this library but not exported.+  other-modules:+    System.TestLoop.Util,+    System.TestLoop.Internal.Cabal,+    System.TestLoop.Internal.Types,+    System.TestLoop.Internal.Signal,+    System.TestLoop.Internal.Watcher+++  -- Other library packages from which modules are imported.+  build-depends:+    base            >= 4.5 && <= 4.6,+    Cabal           >= 1.14,+    directory       >= 1.1,+    filepath        == 1.3.*,+    fsnotify        >= 0.0.8,+    hint            == 0.3.*,+    mtl             == 2.1.*,+    system-filepath == 0.4.*,+    time            == 1.4.*++  if !os(windows)+    build-depends:+      unix >= 2.2.0.0 && < 2.7+++source-repository head+  type:     git+  location: https://github.com/roman/testloop