packages feed

sunlight (empty) → 0.2.0.0

raw patch · 7 files changed

+917/−0 lines, 7 filesdep +Cabaldep +basedep +bytestringsetup-changed

Dependencies added: Cabal, base, bytestring, directory, old-locale, process, random, time, tuple

Files

+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2014, Omari Norman++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Omari Norman nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,98 @@+# sunlight++sunlight helps you test your Cabalized package against multiple+dependency versions.++## Why?++I wrote sunlight because of minor quibbles I have with the Haskell+[Package Versioning+Policy](http://www.haskell.org/haskellwiki/Package_versioning_policy).+The PVP encourages you to record top bounds on each of the+dependencies in your package.  This often keeps packages from+building when dependencies are updated, even if the dependencies+have changed in harmless ways.++I'm not the only one with quibbles here: [Haskell Cafe+discussion](http://www.haskell.org/pipermail/haskell-cafe/2012-August/102885.html).+My only quibble is with upper bounds; specifying the release of a+package by using the PVP is an excellent practice.++I figure it would be much more useful for the Cabal file to serve as+documentation of software versions that the package will build with.+The problem is that there was no easy way to make sure that a+package builds with the minimum versions specified in the Cabal+file.  For example, cabal install does not have an option to fetch+the lowest version of each dependency; instead, it eagerly fetches+the most recent dependencies--which is what you want most of the+time, but this makes it hard to ensure that the package builds with+the lowest possible dependencies.++sunlight automatically builds the package using multiple GHC+versions, so you can verify that the package builds even with+different dependencies or compiler versions.  This also makes it+easier to maintain a package that builds with older GHC versions+that you may not use regularly.++## Usage++To use sunlight, you need to specify a minimum bound for each+dependency.  That minimum bound must be resolvable to an actual+version of the package.  It's OK to specify an exact bound, or an+optional maximum bound, but there must be a minimum.++For example, these are all acceptable:++    bytestring ##0.9.2.1+    bytestring ># 0.9.2.1 && < 0.10+    bytestring ># 0.9.2.1++This is not acceptable because there is no bound at all:++    bytestring++This is not acceptable because there is no lower bound:++    bytestring <# 0.10++Then you build a test executable.  An example is in the+`sunlight-test.hs` file in the sunlight git tree.  This program+tests sunlight itself.++The executable will output a number of reports in a `sunlight`+directory, and it will also output a `current-versions.txt` report+to show you the most recent versions of dependencies that work with+your package, as well as a `minimum-versions.txt` file showing you+that your package builds and tests with the minimum package+versions.++Also, see the Haddocks on the Test.Sunlight module.++## Disclaimer++sunlight works, but I'm not sure it is a good solution to the+problem of managing bounds and dependencies in a Cabal file.  In+particular, it might be a better long-term idea to change+cabal-install so that it can fetch the lowest versions rather than+the most recent ones.++sunlight works on simple Cabal files but I have not tested it on any+exotic files.++## See also++cabal-bounds - program to automatically manage the bounds and+versions in a Cabal file.+[Github](https://github.com/dan-t/cabal-bounds)+[Hackage](http://hackage.haskell.org/package/cabal-bounds)++## License++sunlight is licensed under the BSD license.++## Why the name?++The word "cabal" has shadowy connotations.  This package helps+ensure your cabal is doing what you say it is doing...so it helps+shine sunlight on your cabal which is, as they say, the best+disinfectant.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ lib/Test/Sunlight.hs view
@@ -0,0 +1,564 @@+{-# LANGUAGE OverloadedStrings #-}+module Test.Sunlight where++import Distribution.Package+import Distribution.Text+import Test.Sunlight.Shell+import System.Directory+import Distribution.PackageDescription+import Distribution.PackageDescription.Parse+import Distribution.Simple.Utils+import Distribution.Verbosity+import Distribution.Version+import Data.Tuple.Select+import qualified Data.ByteString.Char8 as BS8+import Data.Monoid+import Data.Time+import System.Locale (defaultTimeLocale)+import Data.List (intersperse)+import System.Random+import Control.Monad+import System.Exit+++-- | Result from installing the package's dependencies.+data InstallResult = InstallResult+  { drOutput :: CmdResult+  , drGhcPkg :: CmdResult+  } deriving Show++instance CheckOk InstallResult where+  isOk r = all isOk . map ($ r) $ [drOutput, drGhcPkg]++-- | Install a package's dependencies.++-- Preconditions:+--+-- * current directory has the unpacked tarball.+--+-- Side effects:+--+-- * dependencies are fully or partially installed.  If partially+-- installed, the ExitCode should be non-zero.+installDeps+  :: [PackageIdentifier]+  -- ^ Optional constraints.  Currently constraints may only be tied+  -- to specific versions (for instance, flag constraints or+  -- constraints tied to a range of versions are not allowed.)+  -> FilePath+  -- ^ Install using this compiler (full path to compiler).+  -> FilePath+  -- ^ Full path to ghc-pkg.+  -> FilePath+  -- ^ Path to cabal executable+  -> FilePath+  -- ^ Path to directory to use for user package DB.+  -> FilePath+  -- ^ Directory to use for installation prefix+  -> IO InstallResult+installDeps cs ghc pkg cabal db dir = do+  let opts =+        [ "install"+        , "--verbose=2"+        , "--with-compiler=" ++ ghc+        , "--with-hc-pkg=" ++ pkg+        , "--prefix=" ++ dir+        , "--disable-library-profiling"+        , "--disable-executable-profiling"+        , "--package-db=clear"+        , "--package-db=global"+        , "--package-db=" ++ db+        , "--enable-tests"+        , "--disable-documentation"+        , "--only-dependencies"+        ] ++ map constraint cs+  out <- tee cabal opts+  let pkgOpts = [ "list", "--global", "--package-conf=" ++ db ]+  pkgOut <- tee pkg pkgOpts+  return $ InstallResult out pkgOut++-- | Makes a PackageIdentifier into a version constraint option.+constraint :: PackageIdentifier -> String+constraint i = "--constraint=" ++ name ++ "==" ++ ver+  where+    name = display . pkgName $ i+    ver = display . pkgVersion $ i++data PackageInstResult = PackageInstResult+  { piSetup :: CmdResult+  , piConfigure :: CmdResult+  , piBuild :: CmdResult+  , piInst :: CmdResult+  , piGhcPkg :: CmdResult+  } deriving Show++instance CheckOk PackageInstResult where+  isOk r = all isOk . map ($ r) $+    [ piSetup, piConfigure, piBuild, piInst, piGhcPkg ]++-- | Install a package.+--+-- Preconditions:+--+-- * dependencies have already been installed at the given prefix+-- and registered in the given db+--+-- * current directory has the unpacked tarball.+--+-- Side effects:+--+-- * package should be installed, or exit code is non-zero+installPackage+  :: FilePath+  -- ^ Install using this compiler (full path to compiler)+  -> FilePath+  -- ^ Full path to ghc-pkg+  -> FilePath+  -- ^ User DB+  -> FilePath+  -- ^ Installation prefix+  -> IO PackageInstResult+installPackage ghc pkg db pfx = do+  let opts =+        [ "configure"+        , "--verbose=2"+        , "--with-compiler=" ++ ghc+        , "--with-hc-pkg=" ++ pkg+        , "--prefix=" ++ pfx+        , "--package-db=clear"+        , "--package-db=global"+        , "--package-db=" ++ db+        , "--enable-tests"+        ]+  rBuildSetup <- tee "ghc" ["--make", "Setup.hs"]+  rConf <- tee "./Setup" opts+  let bOpts =+        [ "build", "--verbose=2" ]+  rBuild <- tee "./Setup" bOpts+  rInst <- tee "./Setup" ["install", "--verbose=2"]+  rPkg <- tee pkg+    [ "list"+    , "--global"+    , "--package-conf=" ++ db+    ]+  return $ PackageInstResult rBuildSetup rConf rBuild rInst rPkg++-- | Test a package.+--+-- Preconditions:+--+-- * Package has already been built.+testPackage+  :: [(String, [String])]+  -- ^ From the package root directory, these are the commands and+  -- arguments to run to test the package.+  -> IO [CmdResult]+testPackage = mapM (uncurry tee)++data InstallAndTestResult = InstallAndTestResult+  { itDate :: UTCTime+  -- ^ Current time+  , itGhcVersion :: CmdResult+  -- ^ Result of ghc --version+  , itPkgVersion :: CmdResult+  -- ^ Result of ghc-pkg --version+  , itCabalVersion :: CmdResult+  -- ^ Result of cabal --version+  , itCompileSetup :: CmdResult+  -- ^ Result from compiling Setup.hs+  , itSdistDeps :: CmdResult+  -- ^ Result from running sdist to create tree from which to+  -- install dependencies+  , itSdistPkg :: CmdResult+  -- ^ Result from running sdist to create tree from which to+  -- install package+  , itInit :: CmdResult+  -- ^ Result from initializing user package DB+  , itDeps :: InstallResult+  , itPackage :: PackageInstResult+  , itTest :: [CmdResult]+  } deriving Show++instance CheckOk InstallAndTestResult where+  isOk r = and [ rsltsOk, testsOk,+    isOk (itDeps r), isOk (itPackage r) ]+    where+      rsltsOk = all isOk . map ($ r) $+        [ itGhcVersion, itPkgVersion, itCabalVersion, itCompileSetup,+          itSdistDeps, itSdistPkg, itInit ]+      testsOk = all isOk . itTest $ r++-- | Performs test installation.+--+-- Preconditions:+--+-- * is run from root package directory.  A temporary directory is+-- created inside this directory.  All work is done within the+-- temporary directory.+--+-- Postconditions:+--+-- * cleans up after itself; no temporary files should remain.+installAndTest+  :: [PackageIdentifier]+  -- ^ Constraints+  -> UTCTime+  -> FilePath+  -- ^ Path to compiler+  -> FilePath+  -- ^ Path to ghc-pkg+  -> FilePath+  -- ^ Path to cabal executable+  -> [(String, [String])]+  -- ^ How to test the package+  -> IO InstallAndTestResult+installAndTest cs date ghc pkg cabal test =+  withTempDirectory verbose "." "sunlight" $ \relDir -> do+    dir <- canonicalizePath relDir+    let setup = dir ++ "/Setup"+        distDeps = dir ++ "/distDeps"+        distPkg = dir ++ "/distPkg"+        db = dir ++ "/db"+        pfx = dir ++ "/prefix"+    ghcVer <- tee ghc ["--version"]+    pkgVer <- tee pkg ["--version"]+    cblVer <- tee "cabal" ["--version"]+    rSetup <- tee ghc ["--make", "-outputdir", dir,+      "-o", setup, "Setup.hs"]+    rDistDeps <- tee setup+      ["sdist", "--output-directory=" ++ distDeps ]+    rDistPkg <- tee setup+      ["sdist", "--output-directory=" ++ distPkg ]+    createDirectory pfx+    rInit <- tee pkg ["init", db]+    rDeps <- inDirectory distDeps $+      installDeps cs ghc pkg cabal db pfx+    rInst <- inDirectory distPkg+      $ installPackage ghc pkg db pfx+    rTest <- inDirectory distPkg $ testPackage test+    return $ InstallAndTestResult date ghcVer pkgVer cblVer+      rSetup rDistDeps rDistPkg+      rInit rDeps rInst rTest++-- | Gets a list of PackageIdentifier with the lowest possible+-- versions.  Fails if a package has a dependency range with no+-- minimum.+lowestVersions+  :: GenericPackageDescription+  -> Either Dependency [PackageIdentifier]+  -- ^ Left with the bad dependency if there is one; Right+  -- otherwise.+lowestVersions pd = mapM lowestVersion ls+  where+    ls = depsLib ++ depsExe ++ depsTest ++ depsBench+    depsLib = case condLibrary pd of+      Nothing -> []+      Just deps -> getDependencies deps+    getDeps = getDepsList pd+    depsExe = getDeps condExecutables+    depsTest = getDeps condTestSuites+    depsBench = getDeps condBenchmarks++getDepsList+  :: GenericPackageDescription+  -> (GenericPackageDescription -> [(a, CondTree b [Dependency] c)])+  -> [Dependency]+getDepsList d f = concatMap getDependencies . map snd . f $ d++getDependencies+  :: CondTree v [Dependency] a+  -> [Dependency]+getDependencies t =+  let this = condTreeConstraints t+      rest = concatMap getDependencies . map sel2+        . condTreeComponents $ t+  in this ++ rest++lowestVersion :: Dependency -> Either Dependency PackageIdentifier+lowestVersion d@(Dependency n r) = case asVersionIntervals r of+  [] -> Left d+  (LowerBound v b, _):_+    | b == ExclusiveBound -> Left d+    | otherwise -> Right $ PackageIdentifier n v++testLowestVersions+  :: UTCTime+  -> FilePath+  -- ^ Path to compiler+  -> FilePath+  -- ^ Path to ghc-pkg+  -> FilePath+  -- ^ Path to cabal executable+  -> [(String, [String])]+  -- ^ How to test the package+  -> GenericPackageDescription+  -> Either Dependency (IO InstallAndTestResult)+testLowestVersions date ghc pkg cabal test d = case lowestVersions d of+  Left e -> Left e+  Right ps -> Right $ installAndTest ps date ghc pkg cabal test++testDefaultVersions+  :: UTCTime+  -> FilePath+  -- ^ Path to compiler+  -> FilePath+  -- ^ Path to ghc-pkg+  -> FilePath+  -- ^ Path to cabal executable+  -> [(String, [String])]+  -- ^ How to test the package+  -> IO InstallAndTestResult+testDefaultVersions date ghc pkg cabal test =+  installAndTest [] date ghc pkg cabal test+++testMultipleVersions+  :: UTCTime+  -> (FilePath, FilePath)+  -- ^ Compiler and ghc-pkg to use when testing lowest version+  -> [(FilePath, FilePath)]+  -- ^ Compilers and ghc-pkg to use to test default versions+  -> FilePath+  -- ^ Path to cabal executable+  -> [(String, [String])]+  -- ^ How to test package+  -> GenericPackageDescription+  -> Either Dependency+            (IO (InstallAndTestResult, [InstallAndTestResult]))+testMultipleVersions date (lowestGhc, lowestPkg) rest cabal test pd =+  case testLowestVersions date lowestGhc lowestPkg cabal test pd of+    Left d -> Left d+    Right g -> Right $ do+      r1 <- g+      let testRest (c, p) = testDefaultVersions date c p cabal test+      rs <- mapM testRest rest+      return (r1, rs)++-- Sample test directory structure:+-- minimum-versions.txt+-- current-versions.txt+-- sunlight+--  - 2014+--    - 02+--      - 22+--        - UTCtime+--          - [random string]+--            - lowest-7.4.2-passed.txt+--            - current-7.4.2-passed.txt+--            - current-7.6.1-passed.txt++versionsReport+  :: BS8.ByteString+  -- ^ Description of dependencies+  -> Compiler+  -> Description+  -> UTCTime+  -> InstallAndTestResult+  -> BS8.ByteString+versionsReport desc c d t r = BS8.concat+  [ "This package was tested to work with these dependency\n"+  , "versions and compiler version.\n"+  , desc+  , "Tested as of: " <> (BS8.pack . show $ t) <> "\n"+  , "Path to compiler: " <> BS8.pack c <> "\n"+  , "Compiler description: " <> BS8.pack d <> "\n"+  , "\n"+  ] <>+  (crStdOut . piGhcPkg . itPackage $ r)++minimumVersionsReport+  :: Compiler+  -> Description+  -> UTCTime+  -> InstallAndTestResult+  -> BS8.ByteString+minimumVersionsReport = versionsReport+  "These are the minimum versions given in the .cabal file.\n"++writeMinimumVersionsReport :: BS8.ByteString -> IO ()+writeMinimumVersionsReport = BS8.writeFile "minimum-versions.txt"++currentVersionsReport+  :: UTCTime+  -> [(Description, Compiler, a)]+  -> [InstallAndTestResult]+  -> Maybe BS8.ByteString+currentVersionsReport t ds ts+  | null ds || null ts = Nothing+  | otherwise = Just $ versionsReport dep comp desc t r+  where+    dep = "These are the default versions fetched by cabal install.\n"+    (desc, comp, _) = last ds+    r = last ts++writeCurrentVersionsReport :: BS8.ByteString -> IO ()+writeCurrentVersionsReport = BS8.writeFile "current-versions.txt"+++instance ShowBS InstallResult where+  showBS r = showBS (drOutput r)+    <> showBS (drGhcPkg r)++instance ShowBS PackageInstResult where+  showBS r =+    showBS (piSetup r)+    <> showBS (piConfigure r)+    <> showBS (piBuild r)+    <> showBS (piInst r)+    <> showBS (piGhcPkg r)++instance ShowBS InstallAndTestResult where+  showBS i =+    "Current time: " <> showBS (itDate i) <> "\n"+    <> showBS (itGhcVersion i)+    <> showBS (itPkgVersion i)+    <> showBS (itCabalVersion i)+    <> showBS (itCompileSetup i)+    <> showBS (itSdistDeps i)+    <> showBS (itSdistPkg i)+    <> showBS (itInit i)+    <> showBS (itDeps i)+    <> showBS (itPackage i)+    <> (BS8.concat . map showBS . itTest $ i)++-- | Gets four-character random string.+randomString :: IO String+randomString =+  fmap (map toEnum)+  $ replicateM 4 (getStdRandom (randomR (fromEnum 'a', fromEnum 'z')))++makeResultFile+  :: UTCTime+  -> String+  -- ^ Random string+  -> String+  -- ^ Description string, e.g. "lowest" or "current"+  -> String+  -- ^ Compiler version description+  -> InstallAndTestResult+  -> IO ()+makeResultFile utct rand desc comp res = do+  let dir = resultDirectory utct rand+  createDirectoryIfMissing True dir+  let fn = dir ++ "/" ++ desc ++ "-" ++ comp ++ "-"+        ++ passFail ++ ".txt"+      passFail+        | isOk res = "passed"+        | otherwise = "FAILED"+  BS8.writeFile fn . showBS $ res+++resultDirectory+  :: UTCTime+  -> String+  -- ^ Random string+  -> String+resultDirectory t s = concat . intersperse "/" $+  ["sunlight", yr, mo, dy, ti, s ]+  where+    (y, m, d) = toGregorian . utctDay $ t+    yr = show y+    mo = pad . show $ m+    dy = pad . show $ d+    pad str+      | length str > 1 = str+      | otherwise = '0':str+    ti = formatTime defaultTimeLocale "%H%M%S" t++-- | A description for this compiler version.  This will be used in+-- the directory name in the tree that is written to disk.  I simply+-- use a compiler version, such as @7.4@.+type Description = String++-- | Path to GHC compiler.  You can use a full path name or, if you use+-- just an executable name, the PATH will be searched.+type Compiler = String++-- | Path to ghc-pkg.  If you use just an executable name, the PATH+-- will be searched.+type GhcPkg = String++-- | Path to cabal executable.  Used to install package+-- dependencies.  If you use just an executable name, the PATH will+-- be searched.+type Cabal = String++data TestInputs = TestInputs+  { tiDescription :: Maybe GenericPackageDescription+  -- ^ If Just, use this package description.  Handy if you write+  -- your own package description in Haskell rather than in the+  -- cabal format.  Otherwise, use Nothing and sunlight will look+  -- for the default cabal file and parse that.++  , tiCabal :: Cabal+  -- ^ Which @cabal@ executable to use.++  , tiLowest :: (Description, Compiler, GhcPkg)+  -- ^ Test the minimum dependency bounds using this compiler.  A+  -- report is left in the main package directory showing which+  -- package versions worked with this compiler and with the minimum+  -- bounds.++  , tiDefault :: [(Description, Compiler, GhcPkg)]+  -- ^ Test the default dependencies using these compilers.  Since+  -- cabal-install will eagerly get the most recent dependencies it+  -- can find, this will test the highest possible versions.  The+  -- compiler specified in 'tiLowest' is not automatically retried+  -- here, so if you want to use that compiler specify it as well.+  --+  -- The last compiler in this list is assumed to be the most recent+  -- compiler.  A report is left in the main package directory+  -- showing the dependencies that worked with this compiler+  -- version.++  , tiTest :: [(String, [String])]+  --  ^ How to test the package.  Each pair is a command to run,+  --  and the arguments to that command.  The command is run from+  --  the resulting package root directory after the package is+  --  unpacked and compiled.  So, if your test suite is an+  --  executable called @myTest@ and it requires the argument+  --  @--verbose@, you would use something like this:+  --+  -- > [("dist/build/myTest/myTest", ["--verbose"])]+  --+  -- The tests are considered to pass if all these commands exit+  -- with a zero exit status.+  --+  -- You can also abuse this option to get output in the record of+  -- the test; for instance, maybe you want to record the current+  -- git HEAD:+  --+  -- > [("git", ["rev-parse", "HEAD"])]+  } deriving Show++runTests :: TestInputs -> IO ()+runTests i = do+  desc <- case tiDescription i of+    Just pd -> return pd+    Nothing -> do+      path <- defaultPackageDesc verbose+      readPackageDescription verbose path+  date <- getCurrentTime+  randStr <- randomString+  let last2 (_, b, c) = (b, c)+      eiRes = testMultipleVersions date+        (last2 . tiLowest $ i) (map last2 . tiDefault $ i)+        (tiCabal i) (tiTest i) desc+  (r1, rs) <- case eiRes of+    Left e -> dependencyError e+    Right g -> g+  makeResultFile date randStr "lowest" (sel1 . tiLowest $ i) r1+  let makeRest r (d, _, _) = makeResultFile date randStr "current" d r+  _ <- zipWithM makeRest rs (tiDefault i)+  when (not $ isOk r1 && all isOk rs) exitFailure+  writeMinimumVersionsReport $ minimumVersionsReport+    (sel2 . tiLowest $ i) (sel1 . tiLowest $ i) date r1+  maybe (return ()) writeCurrentVersionsReport $ currentVersionsReport+    date (tiDefault i) rs+  exitSuccess++dependencyError :: Dependency -> IO a+dependencyError d = putStrLn s >> exitFailure+  where+    s = "dependency invalid: " ++ show d ++ " sunlight requires "+      ++ "that you specify a fixed lower bound for each dependency."
+ lib/Test/Sunlight/Shell.hs view
@@ -0,0 +1,151 @@+{-# LANGUAGE OverloadedStrings #-}+module Test.Sunlight.Shell where+++import qualified Data.ByteString as BS+import qualified Data.ByteString.Char8 as BS8+import Control.Exception+import Data.Monoid+import Foreign.Safe+import System.Directory+import System.Exit+import System.IO+import System.Process+import Control.Concurrent+import Data.Time++-- | Runs an IO action within the given directory, and then returns+-- to the original directory.+inDirectory+  :: FilePath+  -> IO a+  -> IO a+inDirectory inside a = bracket get rel run+  where+    get = do+      d <- getCurrentDirectory+      putStrLn $ "current directory is: " ++ d+      putStrLn $ "changing to directory: " ++ inside+      setCurrentDirectory inside+      return d+    rel d = do+      putStrLn $ "returning to directory: " ++ d+      setCurrentDirectory d+    run = const a++-- | Creates a process.  Its stdin is closed.  Returns handles to+-- stdout and stderr, and the ProcessHandle.+makeProcess+  :: FilePath+  -- ^ Run this command+  -> [String]+  -- ^ Arguments+  -> IO (Handle, Handle, ProcessHandle)+makeProcess c as = do+  d <- getCurrentDirectory+  putStrLn $ "current directory is: " ++ d+  putStrLn $ "running: " ++ c ++ " " ++ show as+  (Just hin, Just out, Just err, h) <- createProcess cp+  hClose hin+  return (out, err, h)+  where+    cp = CreateProcess { cmdspec = RawCommand c as+                       , cwd = Nothing+                       , env = Nothing+                       , std_in = CreatePipe+                       , std_out = CreatePipe+                       , std_err = CreatePipe+                       , close_fds = True+                       , create_group = False }++bufsize :: Int+bufsize = 512++data CmdResult = CmdResult+  { crCode :: ExitCode+  , crStdOut :: BS.ByteString+  , crStdErr :: BS.ByteString+  , crCommand :: String+  , crArgs :: [String]+  } deriving Show++class ShowBS a where+  showBS :: a -> BS.ByteString++instance ShowBS CmdResult where+  showBS r =+    BS8.pack (crCommand r) <> " " <> (BS8.pack . show . crArgs $ r)+    <> " "+    <> case crCode r of+        ExitSuccess -> "succeeded with exit code 0"+        ExitFailure e -> "FAILED with exit code " <> BS8.pack (show e)+    <> "\n"+    <> "Standard output:\n"+    <> shower (crStdOut r)+    <> "\nStandard error:\n"+    <> shower (crStdErr r)+    <> "\n"+    where+      shower bs+        | BS8.null bs = "<empty>\n"+        | otherwise = bs++instance ShowBS UTCTime where+  showBS = BS8.pack . show+++resultOk :: CmdResult -> Bool+resultOk c = case crCode c of+  ExitSuccess -> True+  _ -> False++class CheckOk a where+  isOk :: a -> Bool++instance CheckOk CmdResult where+  isOk c = case crCode c of+    ExitSuccess -> True+    _ -> False++-- | Runs a command, sending standard output and standard error to+-- the terminal, while also returning the standard output.+tee+  :: FilePath+  -- ^ Run this command+  -> [String]+  -- ^ Arguments+  -> IO CmdResult+tee cmd as = bracket (makeProcess cmd as) rel go+  where+    rel (out, err, _) = hClose out >> hClose err++    go (out, err, hdl) = do+      mvOut <- newEmptyMVar+      mvErr <- newEmptyMVar+      _ <- forkIO (readToEOF out stdout >>= putMVar mvOut)+      _ <- forkIO (readToEOF err stderr >>= putMVar mvErr)+      bsOut <- takeMVar mvOut+      bsErr <- takeMVar mvErr+      cd <- waitForProcess hdl+      return $ CmdResult cd bsOut bsErr cmd as+++-- | Reads from the given handle until EOF.  Sends results to the+-- given handle and also stores them in the returned ByteString.+readToEOF+  :: Handle+  -- ^ Read from this handle+  -> Handle+  -- ^ Send results to this handle+  -> IO BS.ByteString+readToEOF inp out = allocaBytes bufsize $ \ptr ->+  let go bs = do+        n <- hGetBufSome inp ptr bufsize+        case n of+          0 -> return bs+          _ -> do+            hPutBuf out ptr n+            bs' <- BS.packCStringLen (ptr, n)+            go (bs `BS.append` bs')+  in go BS.empty+
+ sunlight-test.hs view
@@ -0,0 +1,19 @@+-- | This file is an example of how you can use sunlight.  I+-- actually use it to test sunlight itself.+--+-- I keep multiple versions of GHC installed.  Each one can be found+-- in the PATH so there is no need to give exact paths to each one.+module Main where++import Test.Sunlight++inputs = TestInputs+  { tiDescription = Nothing+  , tiCabal = "cabal"+  , tiLowest = ("7.4", "ghc-7.4", "ghc-pkg-7.4")+  , tiDefault = [ ("7.4", "ghc-7.4", "ghc-pkg-7.4")+                , ("7.6", "ghc-7.6", "ghc-pkg-7.6") ]+  , tiTest = []+  }++main = runTests inputs
+ sunlight.cabal view
@@ -0,0 +1,53 @@+-- Initial sunlight.cabal generated by cabal init.  For further +-- documentation, see http://haskell.org/cabal/users-guide/++name:                sunlight+version:             0.2.0.0+synopsis:            Test Cabalized package against multiple dependency versions++description:         +  sunlight checks your Cabalized package and ensures that it actually+  builds and passes its tests, even against mulitple dependency+  versions specified in your Cabal file.  For example, your Cabal file+  might say that your package builds against base-4.5.1.0+  and base-4.6.0.1.  With+  sunlight you can automatically verify that this is actually the case.+  .+  For more information, please see+  .+  <http://www.github.com/massysett/sunlight>++homepage:            http://www.github.com/massysett/sunlight+license:             BSD3+license-file:        LICENSE+author:              Omari Norman+maintainer:          omari@smileystation.com+stability:           experimental+category:            Testing+build-type:          Simple+cabal-version:       >=1.10+extra-source-files: sunlight-test.hs, README.md+source-repository head+  type: git+  location: https://github.com/massysett/sunlight.git+  ++library+  exposed-modules:+      Test.Sunlight+    , Test.Sunlight.Shell++  build-depends:+      base >=4.5.1.0 && < 5+    , directory >= 1.1.0.2+    , bytestring >= 0.9.2.1+    , Cabal >= 1.14.0+    , process >= 1.1.0.1+    , tuple >= 0.2.0.1+    , time >= 1.4+    , old-locale >= 1.0.0.4+    , random >= 1.0.0.0++  hs-source-dirs:      lib+  ghc-options: -Wall+  default-language:    Haskell2010