packages feed

hydra-0.15.0: src/test/haskell/Hydra/TestSuiteSpec.hs

{-
stack ghci hydra:lib hydra:hydra-test

Test.Hspec.hspec Hydra.TestSuiteSpec.spec
-}

module Hydra.TestSuiteSpec where

import Hydra.Testing
import Hydra.Test.TestSuite
import qualified Hydra.Dsl.Meta.Testing as Testing

import qualified Control.Monad as CM
import qualified Test.Hspec as H
import qualified Data.IORef as IORef
import qualified Data.List as L
import qualified Data.Map as M
import qualified Data.Maybe as Y
import qualified Data.Time.Clock.POSIX as POSIX
import qualified System.Environment as Env


type TestRunner = String -> TestCaseWithMetadata -> Y.Maybe (H.SpecWith ())

defaultTestRunner :: TestRunner
defaultTestRunner desc tcase = if Testing.isDisabled tcase
  then Nothing
  else Just $ case testCaseWithMetadataCase tcase of
    TestCaseUniversal (UniversalTestCase actual expected) ->
      H.it "universal" $ H.shouldBe actual expected

runTestCase :: String -> TestRunner -> TestCaseWithMetadata -> H.SpecWith ()
runTestCase pdesc runner tcase@(TestCaseWithMetadata name _ mdesc _) =
  case runner cdesc tcase of
    Nothing -> return ()
    Just spec -> H.describe desc spec
  where
    desc = name ++ Y.maybe ("") (\d -> ": " ++ d) mdesc
    cdesc = if L.null pdesc then desc else pdesc ++ ", " ++ desc

runTestGroup :: String -> TestRunner -> TestGroup -> H.SpecWith ()
runTestGroup pdesc runner tg = H.describe desc $ do
    CM.mapM (runTestCase cdesc runner) $ testGroupCases tg
    CM.sequence [runTestGroup cdesc runner sub | sub <- testGroupSubgroups tg]
    return ()
  where
    desc = testGroupName tg ++ descSuffix
    cdesc = if L.null pdesc then desc else pdesc ++ ", " ++ desc
    descSuffix = case testGroupDescription tg of
      Nothing -> ""
      Just d -> " (" ++ d ++ ")"

-- | Timed variant for benchmark output
runTestGroupTimed :: IORef.IORef (M.Map String Double) -> String -> TestRunner -> TestGroup -> H.SpecWith ()
runTestGroupTimed timingsRef hydraPath runner tg = do
    H.describe desc $ do
      H.runIO $ IORef.modifyIORef' timingsRef (M.insert hydraPath 0)
      startRef <- H.runIO $ IORef.newIORef (0 :: Double)
      H.beforeAll_ (recordStart startRef) $ H.afterAll_ (recordStop startRef) $ do
        CM.mapM (runTestCase cdesc runner) $ testGroupCases tg
        CM.sequence [runTestGroupTimed timingsRef subPath runner sub
          | sub <- testGroupSubgroups tg
          , let subPath = hydraPath ++ "/" ++ testGroupName sub]
        return ()
  where
    desc = testGroupName tg ++ descSuffix
    cdesc = ""
    descSuffix = case testGroupDescription tg of
      Nothing -> ""
      Just d -> " (" ++ d ++ ")"
    recordStart startRef = do
      now <- POSIX.getPOSIXTime
      IORef.writeIORef startRef (realToFrac now :: Double)
    recordStop startRef = do
      startTime <- IORef.readIORef startRef
      now <- POSIX.getPOSIXTime
      let elapsedMs = (realToFrac now - startTime) * 1000.0
      IORef.modifyIORef' timingsRef (M.insert hydraPath elapsedMs)

spec :: H.Spec
spec = do
  benchmarkOutput <- H.runIO $ Env.lookupEnv "HYDRA_BENCHMARK_OUTPUT"
  case benchmarkOutput of
    Nothing -> runTestGroup "" defaultTestRunner allTests
    Just outputPath -> do
      timingsRef <- H.runIO $ IORef.newIORef M.empty
      let rootPath = testGroupName allTests
      runTestGroupTimed timingsRef rootPath defaultTestRunner allTests
      H.afterAll_ (writeBenchmarkJson outputPath timingsRef allTests) $ do
        H.it "benchmark finalize" $ True `H.shouldBe` True

-- | Write benchmark JSON
writeBenchmarkJson :: String -> IORef.IORef (M.Map String Double) -> TestGroup -> IO ()
writeBenchmarkJson outputPath timingsRef tg = do
    timings <- IORef.readIORef timingsRef
    let json = renderBenchmarkJson timings tg
    writeFile outputPath json

renderBenchmarkJson :: M.Map String Double -> TestGroup -> String
renderBenchmarkJson timings tg =
    "{\n" ++
    "  \"metadata\": {\n" ++
    "    \"language\": \"haskell\"\n" ++
    "  },\n" ++
    "  \"groups\": [\n" ++
    renderGroup "    " (testGroupName tg) tg ++ "\n" ++
    "  ],\n" ++
    "  \"summary\": {\n" ++
    "    \"totalPassed\": " ++ show totalPassed ++ ",\n" ++
    "    \"totalFailed\": 0,\n" ++
    "    \"totalSkipped\": " ++ show totalSkipped ++ ",\n" ++
    "    \"totalTimeMs\": " ++ show totalTime ++ "\n" ++
    "  }\n" ++
    "}"
  where
    totalTime = Y.fromMaybe 0 $ M.lookup (testGroupName tg) timings
    totalPassed = countCases False tg
    totalSkipped = countCases True tg
    countCases wantSkipped g =
      length [() | c <- testGroupCases g, Testing.isDisabled c == wantSkipped] +
      sum [countCases wantSkipped sub | sub <- testGroupSubgroups g]
    renderGroup indent path g =
      indent ++ "{\n" ++
      indent ++ "  \"name\": " ++ show (testGroupName g) ++ ",\n" ++
      indent ++ "  \"time_ms\": " ++ show (Y.fromMaybe 0 $ M.lookup path timings) ++ ",\n" ++
      indent ++ "  \"subgroups\": [" ++
      (if null (testGroupSubgroups g) then "]" else
        "\n" ++ L.intercalate ",\n" [renderGroup (indent ++ "    ") (path ++ "/" ++ testGroupName sub) sub | sub <- testGroupSubgroups g] ++
        "\n" ++ indent ++ "  ]") ++
      "\n" ++ indent ++ "}"