tilia-0.0.1.0: tests/Tilia/Doc/PropertiesSpec.hs
{-# LANGUAGE OverloadedStrings #-}
-- | Properties that should hold of every document the engine renders.
module Tilia.Doc.PropertiesSpec (spec) where
import Data.Char (isSpace)
import Data.Text (Text)
import Data.Text qualified as T
import Test.Hspec
import Test.QuickCheck
import Tilia.Doc
import Tilia.Doc.Combinators
import Tilia.Gen
import Tilia.Span
spec :: Spec
spec = do
describe "output shape" $ do
it "never leaves trailing whitespace on a line" $
property $ \(AnyDoc d) ->
let ls = T.lines (out d)
in counterexample (show ls) (all (\l -> l == T.stripEnd l) ls)
it "is empty or ends in exactly one newline" $
property $ \(AnyDoc d) ->
let t = out d
in T.null t || (T.isSuffixOf "\n" t && not (T.isSuffixOf "\n\n" t))
it "never begins with a blank line" $
property $ \(AnyDoc d) ->
let t = out d
in not (T.isPrefixOf "\n" t)
it "never carries two blank lines in a row" $
property $ \(AnyDoc d) ->
let ls = T.lines (out d)
pairs = zip ls (drop 1 ls)
in counterexample (show ls) (not (any (\(a, b) -> T.null a && T.null b) pairs))
describe "content" $
it "emits exactly the text it was given, and nothing else" $
property $ \(PlainDoc d) ->
let expected = squash (T.concat (docTexts d))
actual = squash (out d)
in counterexample (show (expected, actual)) (expected == actual)
describe "layout" $ do
it "keeps a flat document on one line" $
property $ \(FlatSafeDoc d) ->
let t = out (flat d)
in counterexample (show t) (length (T.lines t) <= 1)
it "renders a group with a single-line span as flat" $
property $ \(FlatSafeDoc d) (SingleLineSpan s) ->
out (group s d) === out (flat d)
it "renders a group with a multi-line span as broken" $
property $ \(FlatSafeDoc d) (MultiLineSpan s) ->
out (group s d) === out (broken d)
describe "provenance" $
it "does not affect the output" $
property $ \(AnyDoc d) (AnySpan s) ->
out (located s d) === out d
describe "monoid" $ do
it "renders associatively" $
property $ \(AnyDoc a) (AnyDoc b) (AnyDoc c) ->
out (broken ((a <> b) <> c)) === out (broken (a <> (b <> c)))
it "has mempty as a rendering identity" $
property $ \(AnyDoc d) -> do
out (broken (mempty <> d)) === out (broken d)
.&&. out (broken (d <> mempty)) === out (broken d)
describe "Span" $ do
it "unions associatively" $
property $ \(AnySpan a) (AnySpan b) (AnySpan c) ->
(a <> b) <> c === a <> (b <> c)
it "unions to something covering both operands" $
property $ \(AnySpan a) (AnySpan b) ->
let u = a <> b
in counterexample (show u) (covers u a && covers u b)
it "is idempotent under union" $
property $
\(AnySpan a) -> a <> a === a
-- | Everything that is not whitespace, in order.
--
-- Whitespace is exactly what the engine is entitled to add, move and
-- remove; what remains is what it must not touch.
squash :: Text -> Text
squash = T.filter (not . isSpace)
out :: Doc -> Text
out = printDoc defaultRenderOptions