tadka-2.0.0.0: test/props/TermColor.hs
{-# LANGUAGE GADTs #-}
{-# LANGUAGE OverloadedStrings #-}
-- | Terminal-capability resolution (pure) and ANSI colour application
-- (post-v1 hardening). The resolver proofs pin exactly how @Auto@ modes become
-- concrete; the colour proofs pin that colour adds only ANSI (and a uniform
-- caret glyph), never structural change — so plain output stays plain.
module TermColor (group) where
import Data.Text (Text)
import qualified Data.Text as T
import Prettyprinter (LayoutOptions (..),
PageWidth (Unbounded), layoutPretty)
import Prettyprinter.Render.Text (renderStrict)
import Hedgehog
import qualified Hedgehog.Gen as Gen
import qualified Hedgehog.Range as Range
import GenDiag (genGD)
import Tadka
import Tadka.Internal (buildContext)
import Tadka.Internal.Config (configPalette, configRelatedDepth,
configTabWidth, configTarget,
configColorMode, configUnicodeMode,
defaultPalette)
import Tadka.Internal.Terminal (TerminalCaps (..), resolveColor,
resolveConfig, resolveUnicode)
group :: Group
group = Group "Terminal detection & colour"
[ ("explicit colour modes pass through", prop_colorPassthrough)
, ("explicit Unicode modes pass through", prop_unicodePassthrough)
, ("auto colour: NO_COLOR always wins", prop_noColorWins)
, ("auto colour: force beats tty", prop_forceColor)
, ("auto colour: else follows tty", prop_ttyColor)
, ("auto Unicode follows the locale", prop_autoUnicode)
, ("resolveConfig eliminates every Auto", prop_noAutoAfter)
, ("resolveConfig is idempotent", prop_idempotent)
, ("resolveConfig touches only the two modes", prop_preservesRest)
, ("ColorNever emits no ANSI", prop_neverNoEsc)
, ("ColorAlways emits ANSI for labels", prop_alwaysHasEsc)
, ("colour changes only ANSI + caret glyph", prop_structureInvariant)
]
-- === generators ===========================================================
genCaps :: Gen TerminalCaps
genCaps = TerminalCaps <$> Gen.bool <*> Gen.bool <*> Gen.bool <*> Gen.bool <*> Gen.bool <*> Gen.bool
genColor :: Gen ColorMode
genColor = Gen.element [ColorAuto, ColorAlways, ColorNever]
genUnicode :: Gen UnicodeMode
genUnicode = Gen.element [UnicodeAuto, UnicodeAlways, UnicodeAscii]
-- === pure resolution proofs ===============================================
prop_colorPassthrough :: Property
prop_colorPassthrough = property $ do
caps <- forAll genCaps
resolveColor caps ColorAlways === ColorAlways
resolveColor caps ColorNever === ColorNever
prop_unicodePassthrough :: Property
prop_unicodePassthrough = property $ do
caps <- forAll genCaps
resolveUnicode caps UnicodeAlways === UnicodeAlways
resolveUnicode caps UnicodeAscii === UnicodeAscii
prop_noColorWins :: Property
prop_noColorWins = property $ do
caps <- forAll (fmap (\c -> c { capNoColor = True }) genCaps)
resolveColor caps ColorAuto === ColorNever
prop_forceColor :: Property
prop_forceColor = property $ do
caps <- forAll (fmap (\c -> c { capNoColor = False, capForceColor = True }) genCaps)
resolveColor caps ColorAuto === ColorAlways
prop_ttyColor :: Property
prop_ttyColor = property $ do
caps0 <- forAll genCaps
let caps = caps0 { capNoColor = False, capForceColor = False }
resolveColor caps ColorAuto === (if capIsTerminal caps then ColorAlways else ColorNever)
prop_autoUnicode :: Property
prop_autoUnicode = property $ do
caps <- forAll genCaps
resolveUnicode caps UnicodeAuto === (if capUnicode caps then UnicodeAlways else UnicodeAscii)
cfgWith :: ColorMode -> UnicodeMode -> Config
cfgWith cm um =
withRelatedDepthLimit 3 . withTabWidth 7 . withTarget TJson
. withColorMode cm . withUnicodeMode um $ defaultConfig
prop_noAutoAfter :: Property
prop_noAutoAfter = property $ do
caps <- forAll genCaps
cm <- forAll genColor
um <- forAll genUnicode
let r = resolveConfig caps (cfgWith cm um)
assert (configColorMode r `elem` [ColorAlways, ColorNever])
assert (configUnicodeMode r `elem` [UnicodeAlways, UnicodeAscii])
prop_idempotent :: Property
prop_idempotent = property $ do
caps <- forAll genCaps
cm <- forAll genColor
um <- forAll genUnicode
let c1 = resolveConfig caps (cfgWith cm um)
c2 = resolveConfig caps c1
configColorMode c2 === configColorMode c1
configUnicodeMode c2 === configUnicodeMode c1
prop_preservesRest :: Property
prop_preservesRest = property $ do
caps <- forAll genCaps
cm <- forAll genColor
um <- forAll genUnicode
let r = resolveConfig caps (cfgWith cm um)
configRelatedDepth r === 3
configTabWidth r === 7
configTarget r === Just TJson
assert (configPalette r == defaultPalette)
-- === colour application proofs =============================================
renderGfx :: Diagnostic e => ColorMode -> e -> Text
renderGfx cm e =
case selectRenderer (withColorMode cm (withUnicodeMode UnicodeAlways (withTarget TGraphical defaultConfig))) of
SomeRenderer r@(Graphical _) -> renderStrict (layoutPretty (LayoutOptions Unbounded) (render r e))
_ -> ""
esc :: Char
esc = '\ESC'
-- Drop ANSI CSI (…m) sequences.
stripEsc :: Text -> Text
stripEsc = T.pack . go . T.unpack
where
go [] = []
go (c : '[' : rest)
| c == esc = go (drop 1 (dropWhile (/= 'm') rest))
go (c : rest) = c : go rest
-- Normalise the cycling underline glyphs to '^' (safe only where content has no
-- '~'/'-'; the fixture below is chosen that way).
normHats :: Text -> Text
normHats = T.map (\c -> if c == '~' || c == '-' then '^' else c)
rightOrErr :: Show a => Either a b -> b
rightOrErr = either (error . show) id
-- Two labels on one line; label text/source deliberately free of '~'/'-'.
data ColorFix = ColorFix
instance Diagnostic ColorFix where
message _ = "type mismatch"
context _ = buildContext (rightOrErr (mkNamedSource "f.hs" "let a = bb"))
[ (rightOrErr (mkSpan 4 1), Just "first thing")
, (rightOrErr (mkSpan 8 2), Just "second thing")
]
prop_neverNoEsc :: Property
prop_neverNoEsc = property $ do
d <- forAllWith (const "<generated diagnostic>") (genGD =<< Gen.int (Range.linear 0 3))
assert (not (T.any (== esc) (renderGfx ColorNever d)))
prop_alwaysHasEsc :: Property
prop_alwaysHasEsc = withTests 1 . property $
assert (T.any (== esc) (renderGfx ColorAlways ColorFix))
prop_structureInvariant :: Property
prop_structureInvariant = withTests 1 . property $
stripEsc (renderGfx ColorAlways ColorFix) === normHats (renderGfx ColorNever ColorFix)