packages feed

hmp3-ng-2.20.0: test/TextSpec.hs

module TextSpec (tests) where

import Test.Tasty
import Test.Tasty.HUnit

import Base
import Text
import Data.ByteString.UTF8 qualified as UTF8
import Data.ByteString.Unsafe qualified as P

-- These tests depend on wcwidth's behavior under a UTF-8 locale and on a
-- handful of codepoints whose canonical widths are well-known:
--   * ASCII chars are 1 column.
--   * Latin-Extended chars (é, ñ) are 1 column.
--   * Common CJK chars (中) are 2 columns.
--   * The horizontal ellipsis (…) is 1 column.
-- They are not deterministic under the C locale.

tests :: TestTree
tests = testGroup "Text"
    [ testGroup "match"
        [ m (Just True)  "exact"         "foo"       "fooBar"
        , m (Just True)  "caseless"      "FOO"       "fooBar"
        , m Nothing      "invalid"       "["         "any[thing"
        , m (Just True)  "alt"           "(foo|az)Q" "bazQux"
        , m (Just True)  "dot"           "o.a"       "foobar"
        , m (Just True)  "Unicode"       "jör"       "Björk"
        , m (Just True)  "dot Unicode"   "j.r"       "Björk"
        , m (Just True)  "Nordic case"   "bør"       "BØrnE"
        , m (Just True)  "Greek case"    "Λω"        "ΦλΩα"
        , m (Just True)  "dot CJK"       "中.人"     "中國人"
        , m (Just False) "byte dots"     "c..te"     "côte"
        ]
    , testGroup "dropLastUTF8" $ let u = UTF8.fromString in
        [ testCase "ASCII"    $ dropLastUTF8 "abc"          @?= "ab"
        , testCase "empty"    $ dropLastUTF8 ""             @?= ""
        , testCase "French"   $ dropLastUTF8 (u"été")       @?= u"ét"
        , testCase "CJK"      $ dropLastUTF8 (u"中國")      @?= u"中"
        , testCase "Emoji"    $ dropLastUTF8 (u"Yes👍")     @?= u"Yes"
        , testCase "invalid"  $ dropLastUTF8 "\x80"         @?= ""
        ]
    , testGroup "trim"
        [ testCase "empty"      $ trim ""                   @?= ""
        , testCase "whitespace" $ trim "  \tfoo bar \n"     @?= "foo bar"
        ]
    , testGroup "guessEncoding"
        [ testCase "ASCII"    $ guessEncoding "abc"      @?= "abc"
        , testCase "ISO-8859" $ guessEncoding "encöde"   @?= "encöde"
        , testCase "UTF-8"    $ guessEncoding "encöde"   @?= "encöde"
        , testCase "control"  $ guessEncoding "en\3öde"  @?= "en�öde"
        ]
    , testGroup "width"
        [ testCase "empty"             $ width ""           @?= 0
        , testCase "ascii"             $ width "hello"      @?= 5
        , testCase "latin-extended"    $ width "café"       @?= 4
        , testCase "cjk doubles each"  $ width "中文"       @?= 4
        , testCase "mixed"             $ width "中a文b"     @?= 6
        ]
    , testGroup "toMaxWidth"
        [ testCase "wider than input passes through"
            $ toMaxWidth 10 "hello"    @?= "hello"
        , testCase "exactly the width passes through"
            $ toMaxWidth 5 "hello"     @?= "hello"
        , testCase "truncate ascii with ellipsis"
            $ toMaxWidth 4 "hello"     @?= "hel" <> "…"
        , testCase "narrower truncate"
            $ toMaxWidth 2 "hello"     @?= "h"   <> "…"
        , testCase "width one becomes a lone ellipsis"
            $ toMaxWidth 1 "hello"     @?= "…"
        , testCase "width zero becomes empty"
            $ toMaxWidth 0 "hello"     @?= ""
        , testCase "wide char truncation respects boundaries"
            -- "中文hi" is 6 columns (2+2+1+1); toMaxWidth 4 keeps the first
            -- wide char plus two ellipses to fill the remaining columns.
            $ toMaxWidth 4 "中文hi"    @?= "中……"
        , testCase "wide char gives way to single ellipsis at the boundary"
            -- "中文" is 4 columns; toMaxWidth 3 keeps the first wide char
            -- (2 columns) plus one ellipsis (1 column).
            $ toMaxWidth 3 "中文"      @?= "中…"
        ]
    , testGroup "toWidth"
        [ testCase "pads short ascii"
            $ toWidth 10 "hello"       @?= "hello     "
        , testCase "pad with empty input"
            $ toWidth 4 ""             @?= "    "
        , testCase "exact width unchanged"
            $ toWidth 5 "hello"        @?= "hello"
        , testCase "truncate matches toMaxWidth when over-width"
            $ toWidth 4 "hello"        @?= "hel…"
        , testCase "pads after a wide-char content too"
            $ toWidth 5 "中a"          @?= "中a  "
        ]
    , testGroup "fromBS"
        [ testCase "Unicode"     $ fromBS (UTF8.fromString "encöde") @?= "encöde"
        , testCase "bad bytes"   $ fromBS "no\130b\8y"               @?= "no�b�y"
        , testCase "worse bytes" $ fromBS "no\130bsy"                @?= "no�bsy"
        , testCase "control"     $ fromBS "nob\8dy"                  @?= "nob�dy"
        , testCase "no dupe"     $
            let bs = UTF8.fromString "schőn" in eqRef bs (toBS $ fromBS bs)
        ]
    , testGroup "spaces"
        [ testCase "two"      $ spaces 2    @?= "  "
        , testCase "negative" $ spaces (-1) @?= ""
        ]
    ]


m :: Maybe Bool -> String -> SText -> SText -> TestTree
m b tag pat str = testCase tag $ ($ str) <$> matches pat @?= b

-- Test memory reuse.
eqRef :: ByteString -> ByteString -> Assertion
eqRef a b =
    P.unsafeUseAsCStringLen a \a' -> P.unsafeUseAsCStringLen b \b' -> a' @?= b'