bake-0.2: src/Development/Bake/Type.hs
{-# LANGUAGE RecordWildCards, GeneralizedNewtypeDeriving #-}
-- | Define a continuous integration system.
module Development.Bake.Type(
Host, Port,
Stringy(..), readShowStringy,
Oven(..), TestInfo(..), defaultOven, ovenTest, ovenNotifyStdout,
threads, threadsAll, require, run, suitable,
State(..), Patch(..), Test(..), Client(..), concrete, validate,
Author
) where
import Development.Bake.Format
import Control.Monad.Extra
import Data.Monoid
import Data.Aeson
import Data.Hashable
import Control.Arrow
type Author = String
type Host = String
type Port = Int
-- | The central type defining a continuous integration system.
-- Usually constructed with 'defaultOven' then filled out with other
-- 'Oven' modifiers such as 'ovenGit' and 'ovenTest'.
--
-- The types are: @state@ is the base state of the system (think HEAD on the master branch);
-- @patch@ is a change that is proposed (think a diff); @test@ is the type of tests that
-- are run.
--
-- All IO operations will be called in a direct subdirectory of the directory you start
-- 'bake' from. In particular:
-- 'ovenUpdateState' will always be called single-threaded from @bake-server@;
-- 'ovenPatchExtra' will always be called from @bake-patch-/hash/@;
-- 'ovenPrepare' and 'run' will always be called from @bake-test-/hash/@.
data Oven state patch test = Oven
{ovenUpdateState :: Maybe (state, [patch]) -> IO state
-- ^ Given a state, and a set of candiates that have passed,
-- merge to create a new state.
,ovenPrepare :: state -> [patch] -> IO [test]
-- ^ Prepare a candidate to be run, produces the tests that must pass
,ovenTestInfo :: test -> TestInfo test
-- ^ Produce information about a test
,ovenNotify :: [Author] -> String -> IO ()
-- ^ Tell an author some information contained in the string (usually an email)
,ovenPatchExtra :: state -> Maybe patch -> IO (String, String)
-- ^ Extra information about a patch, a single line (HTML span),
-- and a longer chunk (HTML block)
,ovenServer :: (Host, Port)
-- ^ Default server to use
,ovenStringyState :: Stringy state
,ovenStringyPatch :: Stringy patch
,ovenStringyTest :: Stringy test
}
-- | Given a 'Stringy' for @test@, and a function that when run on a code base
-- returns the list of tests that need running, and a function to populate
-- a 'TestInfo', modify the 'Oven' with a test type.
ovenTest :: Stringy test -> IO [test] -> (test -> TestInfo test)
-> Oven state patch () -> Oven state patch test
ovenTest stringy prepare info o = o{ovenStringyTest=stringy, ovenPrepare= \_ _ -> prepare, ovenTestInfo=info}
-- | Produce notifications on 'stdout' when users should be notified about success/failure.
ovenNotifyStdout :: Oven state patch test -> Oven state patch test
ovenNotifyStdout o = o{ovenNotify = \a s -> f a s >> ovenNotify o a s}
where f a s = putStr $ unlines
[replicate 70 '-'
,"To: " ++ commas a
,s
,replicate 70 '-'
]
-- | A type representing a translation between a value and a string, which can be
-- produced by 'readShowStringy' if the type has both 'Read' and 'Show' instances.
-- The functions 'stringyTo' and 'stringyFrom' should be inverses of each other.
-- The function 'stringyPretty' shows a value in a way suitable for humans, and can
-- discard uninteresting information.
data Stringy s = Stringy
{stringyTo :: s -> String
,stringyFrom :: String -> s
,stringyPretty :: s -> String
}
-- | Produce a 'Stringy' for a type with 'Read' and 'Show'.
readShowStringy :: (Show s, Read s) => Stringy s
readShowStringy = Stringy show read show
-- | The default oven, which doesn't do anything interesting. Usually the starting point.
defaultOven :: Oven () () ()
defaultOven = Oven
{ovenUpdateState = \_ -> return ()
,ovenNotify = \_ _ -> return ()
,ovenPrepare = \_ _ -> return []
,ovenTestInfo = \_ -> mempty
,ovenPatchExtra = \_ _ -> return ("","")
,ovenServer = ("127.0.0.1",80)
,ovenStringyState = readShowStringy
,ovenStringyPatch = readShowStringy
,ovenStringyTest = readShowStringy
}
-- | Information about a test.
data TestInfo test = TestInfo
{testThreads :: Maybe Int -- number of threads, defaults to 1, Nothing for use all
,testAction :: IO ()
,testSuitable :: IO Bool -- can this test be run on this machine (e.g. Linux only tests)
,testRequire :: [test]
}
instance Functor TestInfo where
fmap f t = t{testRequire = map f $ testRequire t}
instance Monoid (TestInfo test) where
mempty = TestInfo (Just 1) (return ()) (return True) []
mappend (TestInfo x1 x2 x3 x4) (TestInfo y1 y2 y3 y4) =
TestInfo (liftM2 (+) x1 y1) (x2 >> y2) (x3 &&^ y3) (x4 ++ y4)
-- | Change the number of threads a test requires, defaults to 1.
threads :: Int -> TestInfo test -> TestInfo test
threads j t = t{testThreads=Just j}
-- | Record that a test requires all available threads on a machine,
-- typically used for the build step.
-- Use 'getNumCapabilities' to find out how many threads you were allocated.
threadsAll :: TestInfo test -> TestInfo test
threadsAll t = t{testThreads=Nothing}
-- | Require the following tests have been evaluated on this machine
-- before this test is run. Typically used to require compilation
-- before running most tests.
require :: [test] -> TestInfo test -> TestInfo test
require xs t = t{testRequire=testRequire t++xs}
-- | The action associated with a @test@.
run :: IO () -> TestInfo test
run act = mempty{testAction=act}
-- | Is a particular client capable of running a test.
-- Usually an OS check.
suitable :: IO Bool -> TestInfo test -> TestInfo test
suitable query t = t{testSuitable = query &&^ testSuitable t}
newtype State = State {fromState :: String} deriving (Show,Eq,Ord,ToJSON,FromJSON,Hashable)
newtype Patch = Patch {fromPatch :: String} deriving (Show,Eq,Ord,ToJSON,FromJSON,Hashable)
newtype Test = Test {fromTest :: String} deriving (Show,Eq,Ord,ToJSON,FromJSON,Hashable)
newtype Client = Client {fromClient :: String} deriving (Show,Eq,Ord,ToJSON,FromJSON,Hashable)
concrete :: Oven state patch test -> Oven State Patch Test
concrete o@Oven{..} = o
{ovenUpdateState = fmap restate . ovenUpdateState . fmap (unstate *** map unpatch)
,ovenPrepare = \s ps -> fmap (map retest) $ ovenPrepare (unstate s) (map unpatch ps)
,ovenTestInfo = fmap retest . ovenTestInfo . untest
,ovenPatchExtra = \s p -> ovenPatchExtra (unstate s) (fmap unpatch p)
,ovenStringyState = state
,ovenStringyPatch = patch
,ovenStringyTest = test
}
where
(patch,unpatch,_ ) = f Patch fromPatch ovenStringyPatch
(state,unstate,restate) = f State fromState ovenStringyState
(test ,untest ,retest ) = f Test fromTest ovenStringyTest
f :: (String -> s) -> (s -> String) -> Stringy o -> (Stringy s, s -> o, o -> s)
f inj proj Stringy{..} =
(Stringy proj inj (stringyPretty . stringyFrom . proj)
,stringyFrom . proj
,inj . stringyTo)
validate :: Oven state patch test -> Oven state patch test
validate o@Oven{..} = o
{ovenStringyState = f ovenStringyState
,ovenStringyPatch = f ovenStringyPatch
,ovenStringyTest = f ovenStringyTest
}
where
f :: Stringy a -> Stringy a
f s@Stringy{..} = s
{stringyTo = check . stringyTo
,stringyFrom = stringyFrom . check
}
where check s | s == stringyTo (stringyFrom s) = s
| otherwise = error $ "Problem with stringyTo/stringyFrom on " ++ show s