packages feed

sydtest-0.15.1.0: src/Test/Syd/SVG.hs

{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE OverloadedStrings #-}

module Test.Syd.SVG (writeSvgReport) where

import qualified Data.ByteString.Lazy as LB
import Data.Maybe
import Data.String
import Data.Text (Text)
import qualified Data.Text as T
import Data.Word
import Graphics.Svg as Svg
import Test.Syd.Run
import Test.Syd.SpecDef
import Test.Syd.SpecForest

writeSvgReport :: FilePath -> Timed ResultForest -> IO ()
writeSvgReport fp trf = do
  let svgLBs = Svg.renderBS $ timedResultForestElement trf
  let completeFile =
        mconcat
          [ "<html><head><style>",
            style,
            "</style></head><body><div id=\"container\">",
            svgLBs,
            "</div></body></html>"
          ]
  LB.writeFile fp completeFile

timedResultForestElement :: Timed ResultForest -> Svg.Element
timedResultForestElement trf =
  let tests = flattenSpecForest (timedValue trf)
      runBegin = timedBegin trf
      runEnd = timedEnd trf
      totalDuration = runEnd - runBegin
      maximumMay [] = Nothing
      maximumMay l = Just $ maximum l
      maxWorker :: Int
      maxWorker = fromMaybe 0 $ maximumMay $ map (timedWorker . testDefVal . snd) tests
      nanosPerSecond = 1_000_000_000
      maximumTime = ceiling (fromIntegral totalDuration / fromIntegral nanosPerSecond :: Double) * nanosPerSecond
   in ( \e ->
          with
            (svg11_ e)
            [ Height_ <<- fromString (show (workerY (maxWorker + 1)) <> "px"),
              Width_ <<- fromString (show (timeX maximumTime maximumTime) <> "px")
            ]
      )
        $ mconcat
          [ -- Thread labels
            g_ [] $
              mconcat $
                flip map [0 .. maxWorker] $ \workerIx ->
                  text_
                    [ X_ <<- "0",
                      Y_ <<- fromString (show (workerY workerIx))
                    ]
                    (toElement (show workerIx)),
            -- Timing labels
            g_ []
              $ mconcat
              $ flip
                map
                [ 0,
                  nanosPerSecond -- In steps of 1 second
                  .. totalDuration
                ]
              $ \t ->
                mconcat
                  [ -- Label
                    text_
                      [ X_ <<- fromString (show (timeX maximumTime t)),
                        Y_ <<- fromString (show (topBarHeight - fontSize))
                      ]
                      (toElement (show (t `div` 1_000_000_000) <> " s")),
                    -- Line
                    line_
                      [ X1_ <<- fromString (show (timeX maximumTime t)),
                        Y1_ <<- fromString (show topBarHeight),
                        X2_ <<- fromString (show (timeX maximumTime t)),
                        Y2_ <<- fromString (show (workerY (maxWorker + 1))),
                        Class_ <<- "time"
                      ]
                      ""
                  ],
            g_ [] $
              mconcat $
                flip map tests $ \(path, TDef timed _) ->
                  let begin = timedBegin timed - runBegin
                      end = timedEnd timed - runBegin
                      duration = end - begin
                      workerIx = timedWorker timed
                      title =
                        T.pack $
                          unlines
                            [ show $ T.intercalate "." path,
                              show (duration `div` 1_000_000) <> "ms"
                            ]
                   in mconcat
                        [ rect_
                            [ X_ <<- fromString (show (timeX maximumTime begin)),
                              Y_ <<- fromString (show (workerY workerIx - barHeight `div` 2)),
                              Font_size_ <<- fromString (show fontSize),
                              Width_ <<- fromString (show (nanosToX totalDuration duration)),
                              Height_ <<- fromString (show barHeight),
                              Class_ <<- "test ",
                              Style_ <<- testStyle duration
                            ]
                            ( title_
                                []
                                ( text_
                                    []
                                    (toElement title)
                                )
                            )
                        ]
          ]

fontSize :: Int
fontSize = 20

timeX :: Word64 -> Word64 -> Int
timeX maximumTime time = leftBarWidth + nanosToX maximumTime time

workerY :: Int -> Int
workerY workerIx = topBarHeight + (workerIx + 1) * barHeight + workerIx * barSpacing

topBarHeight :: Int
topBarHeight = 50

leftBarWidth :: Int
leftBarWidth = 50

barHeight :: Int
barHeight = 40

barSpacing :: Int
barSpacing = 5

nanosToX :: Word64 -> Word64 -> Int
nanosToX totalDuration n =
  round $
    fromIntegral n / (fromIntegral totalDuration / (1_800 :: Double))

testStyle :: Word64 -> Text
testStyle runtime =
  let (fill, stroke) = testColours runtime
   in T.pack $
        concat
          [ "fill: ",
            renderRedGreen fill,
            ";",
            "stroke:",
            renderRedGreen stroke,
            ";"
          ]

data RedGreen
  = RedGreen
      !Word8 -- Red
      !Word8 -- Green

renderRedGreen :: RedGreen -> String
renderRedGreen (RedGreen r g) = concat ["rgb(", show r, ",", show g, ",0)"]

testColours :: Word64 -> (RedGreen, RedGreen)
testColours duration =
  let fill = testFill duration
      stroke = testStroke fill
   in (fill, stroke)

-- Red to green are the colours
-- (ff, 00, 00) -> (ff, ff, 00), (00, ff, ff)
testFill :: Word64 -> RedGreen
testFill duration =
  let t :: Double
      t =
        max
          1 -- We don't care about any differences below 1 ms, and they could
          -- cause trouble with the logarithm.
          (fromIntegral duration / 1_000_000)
      midway :: Double
      midway = 500 -- ms
      -- This means that tlog will be between
      -- 0(1ms) and 1(500ms): green
      -- 1(500ms) and 2(around 200sec): red
      tlog = min 2 $ logBase midway t
   in if tlog <= 1
        then -- Faster than 500ms
        -- Between green and yellow
        -- So definitely maximum green.
        --
        -- The faster the test, the darker the colour should be,
        -- The faster the test, the smaller tlog, the smaller the red component.
          RedGreen (round (tlog * 200)) 255
        else -- Slower than 500 ms
        -- Between yellow and red.
        -- So definitely maximum red.
        -- The slower the test, the darker the colour should be.
          RedGreen 255 (round ((2 - tlog) * 255))

-- Make the stroke colour based on the fill colour
testStroke :: RedGreen -> RedGreen
testStroke (RedGreen r g) =
  let darken c = round (fromIntegral c * 0.75 :: Double)
   in RedGreen (darken r) (darken g)

style :: LB.ByteString
style =
  LB.intercalate
    "\n"
    [ "div#container {",
      "  height: 100%;",
      "  width: 100%;",
      "  overflow: scroll;",
      "}",
      "svg {",
      "  border: 1px dotted grey;",
      "}",
      ".test {",
      "  pointer-events: all;",
      "  stroke-width: 3;",
      "}",
      ".test:hover {",
      "  stroke: magenta !important;",
      "}",
      ".time {",
      "  stroke: black;",
      "  stroke-width: 1;",
      "  stroke-dasharray: 10,10;",
      "  opacity: 0.5",
      "}"
    ]