packages feed

symbol-0.3.0: tests/Main.hs

module Main (main) where

import           Control.Concurrent    (forkFinally, killThread, newEmptyMVar,
                                        putMVar, readMVar, takeMVar)
import           Control.Exception     (ErrorCall, evaluate, finally, throwIO,
                                        try)
import           Control.Monad         (forM, forM_)
import           Data.Data             (Data, fromConstrB, gmapM, gmapT,
                                        toConstr)
import           Data.Maybe            (fromMaybe)
import           Data.String           (fromString)
import           Data.Symbol           (Symbol, intern, unintern)
import           Data.Typeable         (Typeable, cast)
import           Test.Tasty            (TestTree, defaultMain, localOption,
                                        mkTimeout, testGroup)
import           Test.Tasty.HUnit      (Assertion, assertFailure, testCase,
                                        (@?=))
import           Test.Tasty.QuickCheck (UnicodeString (..), testProperty)
import           Text.Read             (readMaybe)

main :: IO ()
main = defaultMain tests

tests :: TestTree
tests = testGroup "symbol"
    [ testGroup "interning"
        [ testProperty "preserves strings" $ \(UnicodeString s) ->
            unintern (intern s) == s
        , testProperty "equality agrees with string equality" $
            \(UnicodeString s) (UnicodeString t) ->
                (intern s == intern t) == (s == t)
        , testProperty "repeated interning preserves identity" $ \(UnicodeString s) ->
            intern s == intern (reverse (reverse s))
        , testProperty "IsString agrees with intern" $ \(UnicodeString s) ->
            fromString s == intern s
        ]
    , testGroup "ordering laws"
        [ testProperty "compare agrees with equality" $
            \(UnicodeString s) (UnicodeString t) ->
                (compare (intern s) (intern t) == EQ) == (s == t)
        , testProperty "comparison is antisymmetric" $
            \(UnicodeString s) (UnicodeString t) ->
                compare (intern s) (intern t) == invert (compare (intern t) (intern s))
        , testProperty "ordering is transitive" $
            \(UnicodeString s) (UnicodeString t) (UnicodeString u) ->
                let x = intern s
                    y = intern t
                    z = intern u
                in not (x <= y && y <= z) || x <= z
        ]
    , testGroup "Read and Show"
        [ testProperty "Show agrees with String" $ \(UnicodeString s) ->
            show (intern s) == show s
        , testProperty "symbols round-trip" $ \(UnicodeString s) ->
            readMaybe (show (intern s)) == Just (intern s)
        , testProperty "lists of symbols round-trip" $ \strings ->
            let symbols = map (intern . getUnicodeString) strings
            in readMaybe (show symbols) == Just symbols
        , testGroup "edge cases"
            [ testCase (show s) $ do
                unintern (intern s) @?= s
                readMaybe (show (intern s)) @?= Just (intern s)
            | s <- ["", "\NUL", "\n\t", "quote\"slash\\", "\x3bb\x1f600"]
            ]
        , testCase "reads parenthesized strings" $
            readMaybe "((\"alpha\"))" @?= Just (intern "alpha")
        , testCase "reads character lists" $
            readMaybe "['a', 'b']" @?= Just (intern "ab")
        , testCase "leaves trailing input" $
            reads "\"alpha\" rest" @?= [(intern "alpha", " rest")]
        , testCase "rejects malformed input" $
            (readMaybe "\"unterminated" :: Maybe Symbol) @?= Nothing
        ]
    , testGroup "Data instance"
        [ testCase "generic string updates preserve interning" $ do
            let original = intern "generic:original"
                expected = intern "generic:changed"
                changed = gmapT (replace ("generic:changed" :: String)) original
            unintern changed @?= "generic:changed"
            changed @?= expected
            (changed == original) @?= False
            compare changed expected @?= EQ
        , testCase "generic integer updates cannot forge identifiers" $ do
            let original = intern "generic:original"
                changed = gmapT (replace (0 :: Int)) original
            changed @?= original
            unintern changed @?= unintern original
        , testProperty "generic identity traversal preserves symbols" $
            \(UnicodeString s) ->
                let sym = gmapT id (intern s)
                in sym == intern s && unintern sym == s
        , testCase "monadic generic updates preserve interning" $ do
            changed <- gmapM (return . replace ("generic:changed" :: String))
                (intern "generic:original")
            changed @?= intern "generic:changed"
            unintern changed @?= "generic:changed"
        , testCase "generic construction interns its argument" $ do
            let rebuilt = fromConstrB (stringField "generic:rebuilt")
                    (toConstr (intern "generic:original")) :: Symbol
            rebuilt @?= intern "generic:rebuilt"
            unintern rebuilt @?= "generic:rebuilt"
        ]
    , localOption (mkTimeout 10000000) $ testGroup "evaluation and concurrency"
        [ testCase "nested interning does not deadlock" $ do
            sym <- evaluate (intern ("outer:" ++ unintern (intern "inner")))
            unintern sym @?= "outer:inner"
        , testCase "input exceptions leave the table usable" $ do
            result <- try (evaluate (intern ('x' : error "bad input")))
                :: IO (Either ErrorCall Symbol)
            case result of
                Left _ -> return ()
                Right _ -> assertFailure "intern did not evaluate the entire input"
            sym <- evaluate (intern "after-exception")
            unintern sym @?= "after-exception"
        , testCase "concurrent interning preserves identity" concurrentInterning
        ]
    ]

replace :: (Typeable a, Typeable b) => a -> b -> b
replace replacement value = fromMaybe value (cast replacement)

stringField :: Data a => String -> a
stringField value = fromMaybe (error "Unexpected non-string Symbol field") (cast value)

invert :: Ordering -> Ordering
invert LT = GT
invert EQ = EQ
invert GT = LT

concurrentInterning :: Assertion
concurrentInterning = do
    gate <- newEmptyMVar
    workers <- forM [0 .. 7 :: Int] $ \offset -> do
        done <- newEmptyMVar
        tid <- forkFinally
            (do readMVar gate
                forM [0 .. 255 :: Int] $ \i -> do
                    let s = "concurrent:" ++ show ((i + offset) `mod` 128)
                    sym <- evaluate (intern s)
                    return (s, sym))
            (putMVar done)
        return (tid, done)
    flip finally (mapM_ (killThread . fst) workers) $ do
        putMVar gate ()
        results <- mapM (takeMVar . snd) workers
        symbols <- mapM (either throwIO return) results
        forM_ (concat symbols) $ \(s, sym) -> do
            unintern sym @?= s
            sym @?= intern s