Skip to content

Commit

Permalink
First commit of testloop
Browse files Browse the repository at this point in the history
  • Loading branch information
roman committed Apr 7, 2013
0 parents commit 4502c7c
Show file tree
Hide file tree
Showing 10 changed files with 361 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
@@ -0,0 +1,4 @@
\.\#*
\#*
dist/
cabal-dev/
7 changes: 7 additions & 0 deletions LICENSE
@@ -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.
40 changes: 40 additions & 0 deletions README
@@ -0,0 +1,40 @@
# TestLoop

TestLoop is a library that provides an automated execution and
reloading of a cabal's test-suites whenever a file is modified on the
hs-source-dirs

## Usage

Once you have a test suite using a haskell test library (hspec, HUnit,
test-framework, etc), write the following in your project's cabal file:

```cabal
test-suite tests
type: exitcode-stdio-1.0
hs-source-dirs: src, test
main-is: TestSuite.hs
-- configuration for your testsuite here

executable testloop
main-is: TestLoop.hs
-- all your project sources (+tests) directores
hs-source-dirs: src, test
build-depends:
base == 4.6.*,
-- your project + test dependencies
testloop == 0.1.*
```

And in the `test/TestLoop.hs` file:

```haskell
import TestLoop.Main (setupTestLoop)

main :: IO ()
main = setupTestLoop
```

Install and run your testloop, as soon as you edit your source files
it will automatically compile and run your tests, instant feedback
FTW.
2 changes: 2 additions & 0 deletions Setup.hs
@@ -0,0 +1,2 @@
import Distribution.Simple
main = defaultMain
89 changes: 89 additions & 0 deletions src/TestLoop/Internal/Cabal.hs
@@ -0,0 +1,89 @@
module 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 (..),
PackageDescription (..),
TestSuite (..),
TestSuiteInterface (..),
condTreeData,
hsSourceDirs,
testBuildInfo)
import Distribution.PackageDescription.Parse (readPackageDescription)
import Distribution.Verbosity (normal)

--------------------

import 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 "]
6 changes: 6 additions & 0 deletions src/TestLoop/Internal/Types.hs
@@ -0,0 +1,6 @@
module TestLoop.Internal.Types where

type TestSuiteName = String
type MainModulePath = String
type MainModuleName = String
type HsSourcePaths = [String]
61 changes: 61 additions & 0 deletions src/TestLoop/Internal/Watcher.hs
@@ -0,0 +1,61 @@
module TestLoop.Internal.Watcher (reloadTestSuite) where

--------------------

import Control.Monad.Trans (MonadIO (..))
import Data.Monoid (mconcat)
import qualified Filesystem.Path as FS
import qualified Filesystem.Path.CurrentOS as FS

--------------------

import Language.Haskell.Interpreter (as, interpret,
loadModules, setImportsQ,
setTopLevelModules)
import Language.Haskell.Interpreter.Unsafe (unsafeRunInterpreterWithArgs)

--------------------

import Data.Time.LocalTime (getZonedTime)

--------------------

import TestLoop.Internal.Types
import TestLoop.Util

--------------------------------------------------------------------------------

reloadTestSuite :: MainModuleName
-> MainModulePath
-> HsSourcePaths
-> FS.FilePath
-> IO ()
reloadTestSuite moduleName modulePath sourcePaths modifiedFile
| isNotEmacsFile = reloadTestSuite_
| otherwise = return ()
where
isNotEmacsFile = not ('#' `elem` (FS.encodeString $ FS.filename modifiedFile))
reloadTestSuite_ = do
-- time <- getZonedTime
-- putStrLn ""
-- putStrLn $ replicate 80 '-'
-- putStrLn $ mconcat [ show time
-- , " | "
-- , FS.encodeString modifiedFile]
-- putStrLn $ replicate 80 '-'
r <- runInterpreter
case r of
Right () -> return ()
Left e -> print e

runInterpreter = unsafeRunInterpreterWithArgs
[ "-i " ++ join ":" sourcePaths
, "-package-conf cabal-dev/packages-7.6.1.conf"]
interpreterAction

interpreterAction = do
loadModules [modulePath]
setTopLevelModules [moduleName]
setImportsQ [("Prelude", Nothing)]
execution <- interpret "main" (as :: IO ())
liftIO $ execution
67 changes: 67 additions & 0 deletions src/TestLoop/Main.hs
@@ -0,0 +1,67 @@
{-# LANGUAGE OverloadedStrings #-}
-- |
-- Module: TestLoop.Main
-- Copyright: 2013 Roman Gonzalez
-- License: MIT
--
-- Maintainer: romanandreg@gmail.com
-- Portability: unix
--
module TestLoop.Main (setupTestLoop) where

--------------------

import Control.Concurrent (forkIO, threadDelay)
import Control.Monad (forM_, forever)
import Data.Monoid (mconcat)
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 TestLoop.Internal.Cabal
import TestLoop.Internal.Types
import TestLoop.Internal.Watcher
import 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

--------------------------------------------------------------------------------

-- | Use this function as the main of you testloop executable.
--
-- Parses your project's cabal file to figure out a test-suite
-- executable, then it will wait for modifications on files contained
-- in the test-suite's hs-source-dirs setting.

setupTestLoop :: IO ()
setupTestLoop = do
(testsuite, moduleFile, sourcePaths) <- parseCabalFile
if not ("test" `elem` sourcePaths)
then hPutStrLn stderr (mconcat [ "You must have a `test` folder in "
, "your cabal's test-suite hs-source-paths"])
else do
putStrLn $ "Test Loop starting on test-suite '" ++ testsuite ++ "'"
putStrLn $ "Listening files on source paths: " ++ (join ", " sourcePaths)
_ <- forkIO $ startTestLoop "Main"
(joinPath ["test", moduleFile])
sourcePaths
forever $ threadDelay 100
10 changes: 10 additions & 0 deletions src/TestLoop/Util.hs
@@ -0,0 +1,10 @@
module TestLoop.Util where

--------------------

import Data.List (intersperse)

--------------------------------------------------------------------------------

join :: [a] -> [[a]] -> [a]
join delim l = concat (intersperse delim l)
75 changes: 75 additions & 0 deletions testloop.cabal
@@ -0,0 +1,75 @@
-- 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 on test suites

-- A longer description of the package.
description:
TestLoop is a library that provides an automated
execution and reloading of a cabal's test-suites whenever
a file is modified on the hs-source-dirs

-- 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:
TestLoop.Main

-- Modules included in this library but not exported.
other-modules:
TestLoop.Util,
TestLoop.Internal.Cabal,
TestLoop.Internal.Types,
TestLoop.Internal.Watcher


-- Other library packages from which modules are imported.
build-depends:
base == 4.6.*,
Cabal == 1.16.*,
directory == 1.2.*,
filepath == 1.3.0.1,
fsnotify == 0.0.6,
hint == 0.3.3.6,
mtl == 2.1.*,
system-filepath == 0.4.*,
time == 1.4.*

0 comments on commit 4502c7c

Please sign in to comment.