{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeFamilies #-}
{-# OPTIONS_GHC -Wno-orphans #-}

-- |
-- Module      : Test.Hspec.BenchGolden
-- Description : Golden testing for performance benchmarks
-- Copyright   : (c) 2026
-- License     : MIT
-- Maintainer  : your.email@example.com
--
-- = Overview
--
-- @golds-gym@ is a framework for golden testing of performance benchmarks.
-- It integrates with hspec and uses benchpress for lightweight timing measurements.
--
-- Optionally, benchmarks can use robust statistics to mitigate the impact of outliers.
--
-- = Quick Start
--
-- @
-- import Test.Hspec
-- import Test.Hspec.BenchGolden
--
-- main :: IO ()
-- main = hspec $ do
--   describe \"Performance\" $ do
--     `benchGolden` "my algorithm" $
--       return $ myAlgorithm input
-- @
--
-- = How It Works
--
-- 1. On first run, the benchmark is executed and results are saved to a
--    golden file as the baseline.
--
-- 2. On subsequent runs, the benchmark is executed and compared against
--    the baseline using a configurable tolerance (default ±15%).
--
-- 3. If the mean time exceeds the tolerance, the test fails with a
--    regression or improvement notification.
--
-- = Architecture-Specific Baselines
--
-- Golden files are stored per-architecture to ensure benchmarks are only
-- compared against equivalent hardware. The architecture identifier includes
-- CPU type, OS, and CPU model.
--
-- = Configuration
--
-- Use 'benchGoldenWith' with a custom 'BenchConfig' to adjust:
--
-- * Number of iterations
-- * Warm-up iterations
-- * Tolerance percentage
-- * Absolute tolerance (hybrid tolerance strategy)
-- * Variance warnings
-- * Robust statistics mode (trimmed mean, MAD, outlier detection)
--
-- == Tolerance Configuration
--
-- The framework supports two tolerance mechanisms that work together:
--
-- 1. __Percentage tolerance__ ('tolerancePercent'): Checks if the mean time
--    change is within ±X% of the baseline. This is the traditional approach
--    and works well for operations that take more than a few milliseconds.
--
-- 2. __Absolute tolerance__ ('absoluteToleranceMs'): Checks if the absolute
--    time difference is within X milliseconds. This prevents false failures
--    for extremely fast operations (< 1ms) where measurement noise causes
--    large percentage variations despite negligible absolute differences.
--
-- By default, benchmarks pass if __EITHER__ tolerance is satisfied:
--
-- @
-- pass = (percentChange <= 15%) OR (absTimeDiff <= 0.01 ms)
-- @
--
-- This hybrid strategy combines the benefits of both approaches:
--
-- * For fast operations (< 1ms): Absolute tolerance dominates, preventing noise
-- * For slow operations (> 1ms): Percentage tolerance dominates, catching real regressions
--
-- To disable absolute tolerance and use percentage-only comparison:
--
-- @
-- benchGoldenWith defaultBenchConfig
--   { absoluteToleranceMs = Nothing
--   }
--   \"benchmark\" $ ...
-- @
--
-- To adjust the absolute tolerance threshold:
--
-- @
-- benchGoldenWith defaultBenchConfig
--   { absoluteToleranceMs = Just 0.001  -- 1 microsecond (very strict)
--   }
--   \"benchmark\" $ ...
-- @
--
-- = Environment Variables
--
-- * @GOLDS_GYM_ACCEPT=1@ - Regenerate all golden files
-- * @GOLDS_GYM_SKIP=1@ - Skip all benchmark tests
-- * @GOLDS_GYM_ARCH=custom-id@ - Override architecture detection

module Test.Hspec.BenchGolden
  ( -- * Spec Combinators
    benchGolden
  , benchGoldenWith
  , benchGoldenIO
  , benchGoldenIOWith

    -- * Configuration
  , BenchConfig(..)
  , defaultBenchConfig

    -- * Types
  , BenchGolden(..)
  , GoldenStats(..)
  , BenchResult(..)
  , Warning(..)
  , ArchConfig(..)

    -- * Low-Level API
  , runBenchGolden

    -- * Re-exports
  , module Test.Hspec.BenchGolden.Arch
  ) where

import Data.IORef
import qualified Data.Text as T
import System.Environment (lookupEnv)
import Text.Printf (printf)
import qualified Text.PrettyPrint.Boxes as Box

import Test.Hspec.Core.Spec

import Test.Hspec.BenchGolden.Arch
import Test.Hspec.BenchGolden.Runner (runBenchGolden, setAcceptGoldens, setSkipBenchmarks)
import Test.Hspec.BenchGolden.Types

-- | Create a benchmark golden test with default configuration.
--
-- This is the simplest way to add a benchmark test:
--
-- @
-- describe "Sorting" $ do
--   benchGolden "quicksort 1000 elements" $
--     return $ quicksort [1000, 999..1]
-- @
--
-- Default configuration:
--
-- * 100 iterations
-- * 5 warm-up iterations
-- * 15% tolerance
-- * Variance warnings enabled
-- * Standard statistics (not robust mode)
benchGolden :: 
    String  -- ^ Name of the benchmark
    -> IO () -- ^ The IO action to benchmark
    -> Spec
benchGolden :: String -> IO () -> Spec
benchGolden String
name IO ()
action = BenchConfig -> String -> IO () -> Spec
benchGoldenWith BenchConfig
defaultBenchConfig String
name IO ()
action

-- | Create a benchmark golden test with custom configuration.
--
-- Examples:
--
-- @
-- -- Tighter tolerance for critical code
-- benchGoldenWith defaultBenchConfig
--   { iterations = 500
--   , tolerancePercent = 5.0
--   , warmupIterations = 20
--   }
--   "hot loop" $
--   return $ criticalFunction input
--
-- -- Robust statistics mode for noisy environments
-- benchGoldenWith defaultBenchConfig
--   { useRobustStatistics = True
--   , trimPercent = 10.0
--   , outlierThreshold = 3.0
--   }
--   "benchmark with outliers" $
--   return $ computation input
-- @
benchGoldenWith :: BenchConfig  -- ^ Configuration parameters
    -> String -- ^ Name of the benchmark
    -> IO () -- ^ The IO action to benchmark
    -> Spec
benchGoldenWith :: BenchConfig -> String -> IO () -> Spec
benchGoldenWith BenchConfig
config String
name IO ()
action =
  String -> BenchGolden -> SpecWith (Arg BenchGolden)
forall a.
(HasCallStack, Example a) =>
String -> a -> SpecWith (Arg a)
it String
name (BenchGolden -> SpecWith (Arg BenchGolden))
-> BenchGolden -> SpecWith (Arg BenchGolden)
forall a b. (a -> b) -> a -> b
$ BenchGolden
    { benchName :: String
benchName   = String
name
    , benchAction :: IO ()
benchAction = IO ()
action
    , benchConfig :: BenchConfig
benchConfig = BenchConfig
config
    }

-- | Create a benchmark golden test for an IO action.
--
-- This is an alias for 'benchGolden' that makes it clear the action
-- involves IO (e.g., file operations, network calls).
--
-- @
-- benchGoldenIO "file read" $ do
--   contents <- readFile "large-file.txt"
--   evaluate (length contents)
-- @
--
-- Note: For IO actions in noisy environments (CI, shared systems),
-- consider using 'benchGoldenIOWith' with @useRobustStatistics = True@.
benchGoldenIO :: String -- ^ Name of the benchmark
    -> IO () -- ^ The IO action to benchmark
    -> Spec
benchGoldenIO :: String -> IO () -> Spec
benchGoldenIO = String -> IO () -> Spec
benchGolden

-- | Create an IO benchmark golden test with custom configuration.
benchGoldenIOWith :: BenchConfig -- ^ Configuration parameters
    -> String -- ^ Name of the benchmark
    -> IO () -- ^ The IO action to benchmark
    -> Spec
benchGoldenIOWith :: BenchConfig -> String -> IO () -> Spec
benchGoldenIOWith = BenchConfig -> String -> IO () -> Spec
benchGoldenWith

-- | Instance for BenchGolden without arguments.
instance Example BenchGolden where
  type Arg BenchGolden = ()
  evaluateExample :: BenchGolden
-> Params
-> (ActionWith (Arg BenchGolden) -> IO ())
-> ProgressCallback
-> IO Result
evaluateExample BenchGolden
bg Params
params ActionWith (Arg BenchGolden) -> IO ()
hook ProgressCallback
progress =
    (() -> BenchGolden)
-> Params
-> (ActionWith (Arg (() -> BenchGolden)) -> IO ())
-> ProgressCallback
-> IO Result
forall e.
Example e =>
e
-> Params
-> (ActionWith (Arg e) -> IO ())
-> ProgressCallback
-> IO Result
evaluateExample (\() -> BenchGolden
bg) Params
params ActionWith (Arg BenchGolden) -> IO ()
ActionWith (Arg (() -> BenchGolden)) -> IO ()
hook ProgressCallback
progress

-- | Instance for BenchGolden with an argument.
--
-- This allows benchmarks to receive setup data from @before@ or @around@ combinators.
instance Example (arg -> BenchGolden) where
  type Arg (arg -> BenchGolden) = arg
  evaluateExample :: (arg -> BenchGolden)
-> Params
-> (ActionWith (Arg (arg -> BenchGolden)) -> IO ())
-> ProgressCallback
-> IO Result
evaluateExample arg -> BenchGolden
bgFn Params
_params ActionWith (Arg (arg -> BenchGolden)) -> IO ()
hook ProgressCallback
_progress = do
    -- Read environment variables to determine accept/skip flags
    Maybe String
acceptEnv <- String -> IO (Maybe String)
lookupEnv String
"GOLDS_GYM_ACCEPT"
    Maybe String
skipEnv <- String -> IO (Maybe String)
lookupEnv String
"GOLDS_GYM_SKIP"
    
    let shouldAccept :: Bool
shouldAccept = case Maybe String
acceptEnv of
          Just String
"1"    -> Bool
True
          Just String
"true" -> Bool
True
          Just String
"yes"  -> Bool
True
          Maybe String
_           -> Bool
False
        shouldSkip :: Bool
shouldSkip = case Maybe String
skipEnv of
          Just String
"1"    -> Bool
True
          Just String
"true" -> Bool
True
          Just String
"yes"  -> Bool
True
          Maybe String
_           -> Bool
False
    
    -- Store the flags so Runner can access them
    Bool -> IO ()
setAcceptGoldens Bool
shouldAccept
    Bool -> IO ()
setSkipBenchmarks Bool
shouldSkip
    
    IORef Result
ref <- Result -> IO (IORef Result)
forall a. a -> IO (IORef a)
newIORef (String -> ResultStatus -> Result
Result String
"" ResultStatus
Success)
    ActionWith (Arg (arg -> BenchGolden)) -> IO ()
hook (ActionWith (Arg (arg -> BenchGolden)) -> IO ())
-> ActionWith (Arg (arg -> BenchGolden)) -> IO ()
forall a b. (a -> b) -> a -> b
$ \Arg (arg -> BenchGolden)
arg -> do
      let bg :: BenchGolden
bg = arg -> BenchGolden
bgFn arg
Arg (arg -> BenchGolden)
arg
      BenchResult
result <- BenchGolden -> IO BenchResult
runBenchGolden BenchGolden
bg
      IORef Result -> Result -> IO ()
forall a. IORef a -> a -> IO ()
writeIORef IORef Result
ref (BenchResult -> Result
fromBenchResult BenchResult
result)
    IORef Result -> IO Result
forall a. IORef a -> IO a
readIORef IORef Result
ref

-- | Convert a benchmark result to an hspec Result.
fromBenchResult :: BenchResult -> Result
fromBenchResult :: BenchResult -> Result
fromBenchResult BenchResult
result = case BenchResult
result of
  FirstRun GoldenStats
stats ->
    String -> ResultStatus -> Result
Result (GoldenStats -> String
formatFirstRun GoldenStats
stats) ResultStatus
Success

  Pass GoldenStats
golden GoldenStats
actual [Warning]
warnings ->
    let info :: String
info = GoldenStats -> GoldenStats -> String
formatPass GoldenStats
golden GoldenStats
actual
        warningInfo :: String
warningInfo = [Warning] -> String
formatWarnings [Warning]
warnings
    in String -> ResultStatus -> Result
Result (String
info String -> String -> String
forall a. [a] -> [a] -> [a]
++ String
warningInfo) ResultStatus
Success

  Regression GoldenStats
golden GoldenStats
actual Double
pctChange Double
tolerance Maybe Double
absToleranceMs ->
    let toleranceDesc :: String
        toleranceDesc :: String
toleranceDesc = case Maybe Double
absToleranceMs of
          Maybe Double
Nothing -> String -> Double -> String
forall r. PrintfType r => String -> r
printf String
"tolerance: %.1f%%" Double
tolerance
          Just Double
absMs -> String -> Double -> Double -> String
forall r. PrintfType r => String -> r
printf String
"tolerance: %.1f%% or %.3f ms" Double
tolerance Double
absMs
        message :: String
message = String -> Double -> String -> String -> String
forall r. PrintfType r => String -> r
printf String
"Mean time increased by %.1f%% (%s)\n\n%s"
                    Double
pctChange String
toleranceDesc (GoldenStats -> GoldenStats -> String
formatRegression GoldenStats
golden GoldenStats
actual)
    in String -> ResultStatus -> Result
Result String
message (Maybe Location -> FailureReason -> ResultStatus
Failure Maybe Location
forall a. Maybe a
Nothing (String -> FailureReason
Reason String
message))

  Improvement GoldenStats
golden GoldenStats
actual Double
pctChange Double
tolerance Maybe Double
absToleranceMs ->
    let toleranceDesc :: String
        toleranceDesc :: String
toleranceDesc = case Maybe Double
absToleranceMs of
          Maybe Double
Nothing -> String -> Double -> String
forall r. PrintfType r => String -> r
printf String
"tolerance: %.1f%%" Double
tolerance
          Just Double
absMs -> String -> Double -> Double -> String
forall r. PrintfType r => String -> r
printf String
"tolerance: %.1f%% or %.3f ms" Double
tolerance Double
absMs
    in String -> ResultStatus -> Result
Result (String -> Double -> String -> String -> String
forall r. PrintfType r => String -> r
printf String
"Performance improved by %.1f%% (%s)\n%s"
                Double
pctChange String
toleranceDesc (GoldenStats -> GoldenStats -> String
formatPass GoldenStats
golden GoldenStats
actual))
      ResultStatus
Success

-- | Format statistics for the first run.
formatFirstRun :: GoldenStats -> String
formatFirstRun :: GoldenStats -> String
formatFirstRun GoldenStats
stats = String
"First run - baseline created\n" String -> String -> String
forall a. [a] -> [a] -> [a]
++ GoldenStats -> String
formatStats GoldenStats
stats

-- | Format a regression comparison with full details.
formatRegression :: GoldenStats -> GoldenStats -> String
formatRegression :: GoldenStats -> GoldenStats -> String
formatRegression GoldenStats
golden GoldenStats
actual =
  let meanDiff :: Double
meanDiff = if GoldenStats -> Double
statsMean GoldenStats
golden Double -> Double -> Bool
forall a. Eq a => a -> a -> Bool
== Double
0
                 then Double
0
                 else ((GoldenStats -> Double
statsMean GoldenStats
actual Double -> Double -> Double
forall a. Num a => a -> a -> a
- GoldenStats -> Double
statsMean GoldenStats
golden) Double -> Double -> Double
forall a. Fractional a => a -> a -> a
/ GoldenStats -> Double
statsMean GoldenStats
golden) Double -> Double -> Double
forall a. Num a => a -> a -> a
* Double
100
      stddevDiff :: Double
stddevDiff = if GoldenStats -> Double
statsStddev GoldenStats
golden Double -> Double -> Bool
forall a. Eq a => a -> a -> Bool
== Double
0
                   then Double
0
                   else ((GoldenStats -> Double
statsStddev GoldenStats
actual Double -> Double -> Double
forall a. Num a => a -> a -> a
- GoldenStats -> Double
statsStddev GoldenStats
golden) Double -> Double -> Double
forall a. Fractional a => a -> a -> a
/ GoldenStats -> Double
statsStddev GoldenStats
golden) Double -> Double -> Double
forall a. Num a => a -> a -> a
* Double
100
      medianDiff :: Double
medianDiff = if GoldenStats -> Double
statsMedian GoldenStats
golden Double -> Double -> Bool
forall a. Eq a => a -> a -> Bool
== Double
0
                   then Double
0
                   else ((GoldenStats -> Double
statsMedian GoldenStats
actual Double -> Double -> Double
forall a. Num a => a -> a -> a
- GoldenStats -> Double
statsMedian GoldenStats
golden) Double -> Double -> Double
forall a. Fractional a => a -> a -> a
/ GoldenStats -> Double
statsMedian GoldenStats
golden) Double -> Double -> Double
forall a. Num a => a -> a -> a
* Double
100
      
      -- Create detailed comparison table
      metricCol :: Box
metricCol = Alignment -> [Box] -> Box
forall (f :: * -> *). Foldable f => Alignment -> f Box -> Box
Box.vcat Alignment
Box.left ([Box] -> Box) -> [Box] -> Box
forall a b. (a -> b) -> a -> b
$ (String -> Box) -> [String] -> [Box]
forall a b. (a -> b) -> [a] -> [b]
map String -> Box
Box.text 
        [String
"Metric", String
"------", String
"Mean", String
"Stddev", String
"Median", String
"Min", String
"Max"]
      actualCol :: Box
actualCol = Alignment -> [Box] -> Box
forall (f :: * -> *). Foldable f => Alignment -> f Box -> Box
Box.vcat Alignment
Box.right ([Box] -> Box) -> [Box] -> Box
forall a b. (a -> b) -> a -> b
$ (String -> Box) -> [String] -> [Box]
forall a b. (a -> b) -> [a] -> [b]
map String -> Box
Box.text 
        [ String
"Actual"
        , String
"------"
        , String -> Double -> String
forall r. PrintfType r => String -> r
printf String
"%.3f ms" (GoldenStats -> Double
statsMean GoldenStats
actual)
        , String -> Double -> String
forall r. PrintfType r => String -> r
printf String
"%.3f ms" (GoldenStats -> Double
statsStddev GoldenStats
actual)
        , String -> Double -> String
forall r. PrintfType r => String -> r
printf String
"%.3f ms" (GoldenStats -> Double
statsMedian GoldenStats
actual)
        , String -> Double -> String
forall r. PrintfType r => String -> r
printf String
"%.3f ms" (GoldenStats -> Double
statsMin GoldenStats
actual)
        , String -> Double -> String
forall r. PrintfType r => String -> r
printf String
"%.3f ms" (GoldenStats -> Double
statsMax GoldenStats
actual)
        ]
      baselineCol :: Box
baselineCol = Alignment -> [Box] -> Box
forall (f :: * -> *). Foldable f => Alignment -> f Box -> Box
Box.vcat Alignment
Box.right ([Box] -> Box) -> [Box] -> Box
forall a b. (a -> b) -> a -> b
$ (String -> Box) -> [String] -> [Box]
forall a b. (a -> b) -> [a] -> [b]
map String -> Box
Box.text
        [ String
"Baseline"
        , String
"--------"
        , String -> Double -> String
forall r. PrintfType r => String -> r
printf String
"%.3f ms" (GoldenStats -> Double
statsMean GoldenStats
golden)
        , String -> Double -> String
forall r. PrintfType r => String -> r
printf String
"%.3f ms" (GoldenStats -> Double
statsStddev GoldenStats
golden)
        , String -> Double -> String
forall r. PrintfType r => String -> r
printf String
"%.3f ms" (GoldenStats -> Double
statsMedian GoldenStats
golden)
        , String -> Double -> String
forall r. PrintfType r => String -> r
printf String
"%.3f ms" (GoldenStats -> Double
statsMin GoldenStats
golden)
        , String -> Double -> String
forall r. PrintfType r => String -> r
printf String
"%.3f ms" (GoldenStats -> Double
statsMax GoldenStats
golden)
        ]
      diffCol :: Box
diffCol = Alignment -> [Box] -> Box
forall (f :: * -> *). Foldable f => Alignment -> f Box -> Box
Box.vcat Alignment
Box.right ([Box] -> Box) -> [Box] -> Box
forall a b. (a -> b) -> a -> b
$ (String -> Box) -> [String] -> [Box]
forall a b. (a -> b) -> [a] -> [b]
map String -> Box
Box.text
        [ String
"Diff"
        , String
"----"
        , String -> Double -> String
forall r. PrintfType r => String -> r
printf String
"%+.1f%%" Double
meanDiff
        , String -> Double -> String
forall r. PrintfType r => String -> r
printf String
"%+.1f%%" Double
stddevDiff
        , String -> Double -> String
forall r. PrintfType r => String -> r
printf String
"%+.1f%%" Double
medianDiff
        , String
""
        , String
""
        ]
      
      table :: Box
table = Int -> Alignment -> [Box] -> Box
forall (f :: * -> *).
Foldable f =>
Int -> Alignment -> f Box -> Box
Box.hsep Int
2 Alignment
Box.top [Box
metricCol, Box
actualCol, Box
baselineCol, Box
diffCol]
  in Box -> String
Box.render Box
table

-- | Format a passing comparison.
formatPass :: GoldenStats -> GoldenStats -> String
formatPass :: GoldenStats -> GoldenStats -> String
formatPass GoldenStats
golden GoldenStats
actual =
  let meanDiff :: Double
meanDiff = if GoldenStats -> Double
statsMean GoldenStats
golden Double -> Double -> Bool
forall a. Eq a => a -> a -> Bool
== Double
0
                 then Double
0
                 else ((GoldenStats -> Double
statsMean GoldenStats
actual Double -> Double -> Double
forall a. Num a => a -> a -> a
- GoldenStats -> Double
statsMean GoldenStats
golden) Double -> Double -> Double
forall a. Fractional a => a -> a -> a
/ GoldenStats -> Double
statsMean GoldenStats
golden) Double -> Double -> Double
forall a. Num a => a -> a -> a
* Double
100
      stddevDiff :: Double
stddevDiff = if GoldenStats -> Double
statsStddev GoldenStats
golden Double -> Double -> Bool
forall a. Eq a => a -> a -> Bool
== Double
0
                   then Double
0
                   else ((GoldenStats -> Double
statsStddev GoldenStats
actual Double -> Double -> Double
forall a. Num a => a -> a -> a
- GoldenStats -> Double
statsStddev GoldenStats
golden) Double -> Double -> Double
forall a. Fractional a => a -> a -> a
/ GoldenStats -> Double
statsStddev GoldenStats
golden) Double -> Double -> Double
forall a. Num a => a -> a -> a
* Double
100
      
      -- Create table with metric, actual, baseline, and diff columns
      metricCol :: Box
metricCol = Alignment -> [Box] -> Box
forall (f :: * -> *). Foldable f => Alignment -> f Box -> Box
Box.vcat Alignment
Box.left ([Box] -> Box) -> [Box] -> Box
forall a b. (a -> b) -> a -> b
$ (String -> Box) -> [String] -> [Box]
forall a b. (a -> b) -> [a] -> [b]
map String -> Box
Box.text [String
"Metric", String
"------", String
"Mean", String
"Stddev"]
      actualCol :: Box
actualCol = Alignment -> [Box] -> Box
forall (f :: * -> *). Foldable f => Alignment -> f Box -> Box
Box.vcat Alignment
Box.right ([Box] -> Box) -> [Box] -> Box
forall a b. (a -> b) -> a -> b
$ (String -> Box) -> [String] -> [Box]
forall a b. (a -> b) -> [a] -> [b]
map String -> Box
Box.text 
        [ String
"Actual"
        , String
"------"
        , String -> Double -> String
forall r. PrintfType r => String -> r
printf String
"%.3f ms" (GoldenStats -> Double
statsMean GoldenStats
actual)
        , String -> Double -> String
forall r. PrintfType r => String -> r
printf String
"%.3f ms" (GoldenStats -> Double
statsStddev GoldenStats
actual)
        ]
      baselineCol :: Box
baselineCol = Alignment -> [Box] -> Box
forall (f :: * -> *). Foldable f => Alignment -> f Box -> Box
Box.vcat Alignment
Box.right ([Box] -> Box) -> [Box] -> Box
forall a b. (a -> b) -> a -> b
$ (String -> Box) -> [String] -> [Box]
forall a b. (a -> b) -> [a] -> [b]
map String -> Box
Box.text
        [ String
"Baseline"
        , String
"--------"
        , String -> Double -> String
forall r. PrintfType r => String -> r
printf String
"%.3f ms" (GoldenStats -> Double
statsMean GoldenStats
golden)
        , String -> Double -> String
forall r. PrintfType r => String -> r
printf String
"%.3f ms" (GoldenStats -> Double
statsStddev GoldenStats
golden)
        ]
      diffCol :: Box
diffCol = Alignment -> [Box] -> Box
forall (f :: * -> *). Foldable f => Alignment -> f Box -> Box
Box.vcat Alignment
Box.right ([Box] -> Box) -> [Box] -> Box
forall a b. (a -> b) -> a -> b
$ (String -> Box) -> [String] -> [Box]
forall a b. (a -> b) -> [a] -> [b]
map String -> Box
Box.text
        [ String
"Diff"
        , String
"----"
        , String -> Double -> String
forall r. PrintfType r => String -> r
printf String
"%+.1f%%" Double
meanDiff
        , String -> Double -> String
forall r. PrintfType r => String -> r
printf String
"%+.1f%%" Double
stddevDiff
        ]
      
      table :: Box
table = Int -> Alignment -> [Box] -> Box
forall (f :: * -> *).
Foldable f =>
Int -> Alignment -> f Box -> Box
Box.hsep Int
2 Alignment
Box.top [Box
metricCol, Box
actualCol, Box
baselineCol, Box
diffCol]
  in Box -> String
Box.render Box
table

-- | Format statistics for display.
formatStats :: GoldenStats -> String
formatStats :: GoldenStats -> String
formatStats GoldenStats{Double
[Double]
[(Int, Double)]
Text
UTCTime
statsMean :: GoldenStats -> Double
statsStddev :: GoldenStats -> Double
statsMedian :: GoldenStats -> Double
statsMin :: GoldenStats -> Double
statsMax :: GoldenStats -> Double
statsMean :: Double
statsStddev :: Double
statsMedian :: Double
statsMin :: Double
statsMax :: Double
statsPercentiles :: [(Int, Double)]
statsArch :: Text
statsTimestamp :: UTCTime
statsTrimmedMean :: Double
statsMAD :: Double
statsIQR :: Double
statsOutliers :: [Double]
statsOutliers :: GoldenStats -> [Double]
statsIQR :: GoldenStats -> Double
statsMAD :: GoldenStats -> Double
statsTrimmedMean :: GoldenStats -> Double
statsTimestamp :: GoldenStats -> UTCTime
statsArch :: GoldenStats -> Text
statsPercentiles :: GoldenStats -> [(Int, Double)]
..} =
  let metricCol :: Box
metricCol = Alignment -> [Box] -> Box
forall (f :: * -> *). Foldable f => Alignment -> f Box -> Box
Box.vcat Alignment
Box.left ([Box] -> Box) -> [Box] -> Box
forall a b. (a -> b) -> a -> b
$ (String -> Box) -> [String] -> [Box]
forall a b. (a -> b) -> [a] -> [b]
map String -> Box
Box.text
        [ String
"Metric", String
"------", String
"Mean", String
"Stddev", String
"Median", String
"Min", String
"Max", String
"Arch" ]
      valueCol :: Box
valueCol = Alignment -> [Box] -> Box
forall (f :: * -> *). Foldable f => Alignment -> f Box -> Box
Box.vcat Alignment
Box.right ([Box] -> Box) -> [Box] -> Box
forall a b. (a -> b) -> a -> b
$ (String -> Box) -> [String] -> [Box]
forall a b. (a -> b) -> [a] -> [b]
map String -> Box
Box.text
        [ String
"Value"
        , String
"-----"
        , String -> Double -> String
forall r. PrintfType r => String -> r
printf String
"%.3f ms" Double
statsMean
        , String -> Double -> String
forall r. PrintfType r => String -> r
printf String
"%.3f ms" Double
statsStddev
        , String -> Double -> String
forall r. PrintfType r => String -> r
printf String
"%.3f ms" Double
statsMedian
        , String -> Double -> String
forall r. PrintfType r => String -> r
printf String
"%.3f ms" Double
statsMin
        , String -> Double -> String
forall r. PrintfType r => String -> r
printf String
"%.3f ms" Double
statsMax
        , Text -> String
T.unpack Text
statsArch
        ]
      table :: Box
table = Int -> Alignment -> [Box] -> Box
forall (f :: * -> *).
Foldable f =>
Int -> Alignment -> f Box -> Box
Box.hsep Int
2 Alignment
Box.top [Box
metricCol, Box
valueCol]
  in Box -> String
Box.render Box
table

-- | Format warnings for display.
formatWarnings :: [Warning] -> String
formatWarnings :: [Warning] -> String
formatWarnings [] = String
""
formatWarnings [Warning]
ws = String
"\nWarnings:\n" String -> String -> String
forall a. [a] -> [a] -> [a]
++ [String] -> String
unlines ((Warning -> String) -> [Warning] -> [String]
forall a b. (a -> b) -> [a] -> [b]
map Warning -> String
formatWarning [Warning]
ws)

-- | Format a single warning.
formatWarning :: Warning -> String
formatWarning :: Warning -> String
formatWarning Warning
w = case Warning
w of
  VarianceIncreased Double
golden Double
actual Double
pct Double
tolerance ->
    String -> Double -> Double -> Double -> Double -> String
forall r. PrintfType r => String -> r
printf String
"  ⚠ Variance increased by %.1f%% (%.3f ms -> %.3f ms, tolerance: %.1f%%)"
      Double
pct Double
golden Double
actual Double
tolerance

  VarianceDecreased Double
golden Double
actual Double
pct Double
tolerance ->
    String -> Double -> Double -> Double -> Double -> String
forall r. PrintfType r => String -> r
printf String
"  ⚠ Variance decreased by %.1f%% (%.3f ms -> %.3f ms, tolerance: %.1f%%)"
      Double
pct Double
golden Double
actual Double
tolerance

  HighVariance Double
cv ->
    String -> Double -> String
forall r. PrintfType r => String -> r
printf String
"  ⚠ High variance detected (CV = %.1f%%)" (Double
cv Double -> Double -> Double
forall a. Num a => a -> a -> a
* Double
100)

  OutliersDetected Int
count [Double]
outliers ->
    let outlierStr :: String
outlierStr = if Int
count Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
<= Int
5
                     then [String] -> String
unwords ((Double -> String) -> [Double] -> [String]
forall a b. (a -> b) -> [a] -> [b]
map (String -> Double -> String
forall r. PrintfType r => String -> r
printf String
"%.3fms") [Double]
outliers)
                     else [String] -> String
unwords ((Double -> String) -> [Double] -> [String]
forall a b. (a -> b) -> [a] -> [b]
map (String -> Double -> String
forall r. PrintfType r => String -> r
printf String
"%.3fms") (Int -> [Double] -> [Double]
forall a. Int -> [a] -> [a]
take Int
5 [Double]
outliers)) String -> String -> String
forall a. [a] -> [a] -> [a]
++ String
"..."
    in String -> Int -> String -> String
forall r. PrintfType r => String -> r
printf String
"  ⚠ %d outlier(s) detected: %s" Int
count String
outlierStr