{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeFamilies #-}
{-# OPTIONS_GHC -Wno-orphans #-}
module Test.Hspec.BenchGolden
(
benchGolden
, benchGoldenWith
, benchGoldenIO
, benchGoldenIOWith
, BenchConfig(..)
, defaultBenchConfig
, BenchGolden(..)
, GoldenStats(..)
, BenchResult(..)
, Warning(..)
, ArchConfig(..)
, runBenchGolden
, 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
benchGolden ::
String
-> IO ()
-> Spec
benchGolden :: String -> IO () -> Spec
benchGolden String
name IO ()
action = BenchConfig -> String -> IO () -> Spec
benchGoldenWith BenchConfig
defaultBenchConfig String
name IO ()
action
benchGoldenWith :: BenchConfig
-> String
-> IO ()
-> 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
}
benchGoldenIO :: String
-> IO ()
-> Spec
benchGoldenIO :: String -> IO () -> Spec
benchGoldenIO = String -> IO () -> Spec
benchGolden
benchGoldenIOWith :: BenchConfig
-> String
-> IO ()
-> Spec
benchGoldenIOWith :: BenchConfig -> String -> IO () -> Spec
benchGoldenIOWith = BenchConfig -> String -> IO () -> Spec
benchGoldenWith
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 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
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
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
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
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
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
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
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
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
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
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)
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