phino-0.0.115: test/BytesSpec.hs
{-# LANGUAGE OverloadedStrings #-}
-- SPDX-FileCopyrightText: Copyright (c) 2025 Objectionary.com
-- SPDX-License-Identifier: MIT
module BytesSpec where
import AST
import Bytes
( NonFinite (..)
, btsAnd
, btsConcat
, btsEqual
, btsNot
, btsOr
, btsShift
, btsSize
, btsSlice
, btsToNonFinite
, btsToNum
, btsToStr
, btsToUnescapedStr
, bytesToBts
, nonFiniteBts
, nonFiniteName
, nonFiniteOf
, nonFinites
, numToBts
, strToBts
, unescapeStr
)
import Control.Exception (evaluate)
import Control.Monad (forM_)
import Data.Text qualified as T
import Test.Hspec (Spec, anyErrorCall, describe, it, shouldBe, shouldSatisfy, shouldThrow)
spec :: Spec
spec = do
describe "numToBts" $
forM_
[ ("0.0", 0.0 :: Double, BtMany ["00", "00", "00", "00", "00", "00", "00", "00"])
, ("42", 42, BtMany ["40", "45", "00", "00", "00", "00", "00", "00"])
, ("-0.25", -0.25, BtMany ["BF", "D0", "00", "00", "00", "00", "00", "00"])
, ("5", 5, BtMany ["40", "14", "00", "00", "00", "00", "00", "00"])
]
( \(desc, num, bts) ->
it desc $ numToBts num `shouldBe` bts
)
describe "numToBts/btsToNum round trip" $
forM_
[ ("normal integer", 42, (== Left 42))
, ("normal fraction", 3.5, (== Right 3.5))
, ("zero", 0.0, (== Left 0))
, ("negative integer", -2, (== Left (-2)))
, ("negative fraction", -0.25, (== Right (-0.25)))
, ("NaN", 0 / 0, either (const False) isNaN)
, ("positive infinity", 1 / 0, either (const False) (\num -> isInfinite num && num > 0))
, ("negative infinity", -(1 / 0), either (const False) (\num -> isInfinite num && num < 0))
, ("negative zero", -0.0, either (const False) isNegativeZero)
]
(\(desc, num, predicate) -> it desc (btsToNum (numToBts num) `shouldSatisfy` predicate))
describe "nonFiniteBts and btsToNonFinite round trip" $
forM_
nonFinites
(\value -> it (T.unpack (nonFiniteName value)) (btsToNonFinite (nonFiniteBts value) `shouldBe` Just value))
describe "btsToNonFinite tells the named patterns from every other one" $
forM_
[ ("a finite integral value", BtMany ["40", "45", "00", "00", "00", "00", "00", "00"], Nothing)
, ("a NaN carrying a payload", BtMany ["7F", "F8", "00", "00", "00", "00", "00", "01"], Nothing)
, ("the negative quiet NaN", BtMany ["FF", "F8", "00", "00", "00", "00", "00", "00"], Nothing)
, ("a byte array of the wrong size", BtOne "7F", Nothing)
, ("meta bytes", BtMeta "d", Nothing)
, ("NaN", BtMany ["7F", "F8", "00", "00", "00", "00", "00", "00"], Just NfNan)
, ("positive infinity", BtMany ["7F", "F0", "00", "00", "00", "00", "00", "00"], Just NfPinf)
, ("negative infinity", BtMany ["FF", "F0", "00", "00", "00", "00", "00", "00"], Just NfNinf)
]
(\(desc, bts, expected) -> it desc (btsToNonFinite bts `shouldBe` expected))
describe "nonFiniteOf reads back the name the printer gives" $
forM_
[ ("nan", Just NfNan)
, ("pinf", Just NfPinf)
, ("ninf", Just NfNinf)
, ("number", Nothing)
, ("", Nothing)
]
(\(name, expected) -> it (T.unpack name) (nonFiniteOf name `shouldBe` expected))
describe "btsToNum on the named non-finite patterns" $
forM_
[ ("NaN", NfNan, either (const False) isNaN)
, ("positive infinity", NfPinf, either (const False) (\num -> isInfinite num && num > 0))
, ("negative infinity", NfNinf, either (const False) (\num -> isInfinite num && num < 0))
]
(\(desc, value, predicate) -> it desc (btsToNum (nonFiniteBts value) `shouldSatisfy` predicate))
describe "btsToNum with a byte array that is not 8 bytes long" $
it "errors out" $
evaluate (btsToNum (BtMany ["40", "45"])) `shouldThrow` anyErrorCall
describe "strToBts" $
forM_
[ ("", BtEmpty)
, ("h", BtOne "68")
, ("hello", BtMany ["68", "65", "6C", "6C", "6F"])
, ("\"", BtOne "22")
, ("\\", BtOne "5C")
, ("\n", BtOne "0A")
, ("\t", BtOne "09")
, ("\x01", BtOne "01")
]
( \(str, bts) ->
it (show str) $ strToBts str `shouldBe` bts
)
describe "btsToStr" $
forM_
[ ("empty", BtEmpty, "")
, ("single char", BtOne "68", "h")
, ("multi byte", BtMany ["68", "65", "6C", "6C", "6F"], "hello")
, ("escapes double quote", BtOne "22", "\\\"")
, ("escapes backslash", BtOne "5C", "\\\\")
, ("escapes newline", BtOne "0A", "\\n")
, ("escapes tab", BtOne "09", "\\t")
, ("escapes non-printable", BtOne "01", "\\x01")
, ("mixed printable and quote", BtMany ["61", "22", "62"], "a\\\"b")
]
( \(desc, bts, str) ->
it desc $ btsToStr bts `shouldBe` str
)
describe "unescapeStr" $
forM_
[ ("empty", "", "")
, ("nothing to unescape", "hello", "hello")
, ("double quote", "h\\\"", "h\"")
, ("backslash", "\\\\", "\\")
, ("newline", "e\\ne", "e\ne")
, ("tab", "\\t", "\t")
, ("hex escape", "\\x01", "\SOH")
, ("uppercase hex escape", "\\xFF", "\255")
, ("backslash before an escape", "\\\\n", "\\n")
, ("unknown escape is kept as it stands", "\\q", "\\q")
, ("trailing backslash is kept", "a\\", "a\\")
, ("truncated hex escape is kept", "\\x0", "\\x0")
]
( \(desc, escaped, unescaped) ->
it desc $ unescapeStr escaped `shouldBe` unescaped
)
describe "btsToStr/unescapeStr round trip" $
forM_
[ ("empty", BtEmpty)
, ("plain word", BtMany ["68", "65", "6C", "6C", "6F"])
, ("double quote", BtOne "22")
, ("backslash", BtOne "5C")
, ("newline", BtOne "0A")
, ("tab", BtOne "09")
, ("non-printable", BtMany ["01", "02"])
, ("text around a newline", BtMany ["65", "0A", "65"])
]
( \(desc, bts) ->
it desc $ strToBts (unescapeStr (btsToStr bts)) `shouldBe` bts
)
describe "btsToUnescapedStr" $
forM_
[ ("non-printable", BtMany ["01", "02"], "\SOH\STX")
, ("multi byte word", BtMany ["77", "6F", "72", "6C", "64"], "world")
, ("double quote", BtMany ["68", "22"], "h\"")
, ("single hex digit padded", BtOne "35", "5")
]
( \(desc, bts, str) ->
it desc $ btsToUnescapedStr bts `shouldBe` str
)
describe "bytesToBts" $
forM_
[ ("empty", "--", BtEmpty)
, ("single byte with trailing dash", "01-", BtOne "01")
, ("multi byte", "77-6F", BtMany ["77", "6F"])
]
( \(desc, str, bts) ->
it desc $ bytesToBts str `shouldBe` bts
)
describe "btsAnd and btsOr" $
forM_
[
( "btsAnd: matching lengths"
, btsAnd (BtMany ["02", "EF"]) (BtMany ["12", "33"])
, Just (BtMany ["02", "23"])
)
, ("btsAnd: mismatched lengths", btsAnd (BtOne "20") (BtMany ["CA", "FE"]), Nothing)
,
( "btsOr: matching lengths"
, btsOr (BtMany ["02", "EF"]) (BtMany ["12", "33"])
, Just (BtMany ["12", "FF"])
)
, ("btsOr: mismatched lengths", btsOr (BtOne "20") (BtMany ["CA", "FE"]), Nothing)
]
(\(desc, actual, expected) -> it desc (actual `shouldBe` expected))
describe "btsNot" $
it "negates every byte" $
btsNot (BtMany ["CA", "FE", "BE", "BE"]) `shouldBe` BtMany ["35", "01", "41", "41"]
describe "btsConcat" $
forM_
[ ("with BtEmpty on the right", BtMany ["05", "5E"], BtEmpty, BtMany ["05", "5E"])
, ("two BtEmpty", BtEmpty, BtEmpty, BtEmpty)
, ("two single bytes", BtOne "01", BtOne "02", BtMany ["01", "02"])
]
( \(desc, left, right, result) ->
it desc $ btsConcat left right `shouldBe` result
)
describe "btsEqual" $ do
it "same value via different constructors" $
btsEqual (BtOne "01") (BtMany ["01"]) `shouldBe` True
it "different values" $
btsEqual (BtMany ["01", "02"]) (BtMany ["01", "03"]) `shouldBe` False
describe "btsSize" $
forM_
[ ("empty", BtEmpty, 0)
, ("three bytes", BtMany ["F1", "20", "5F"], 3)
]
( \(desc, bts, size) ->
it desc $ btsSize bts `shouldBe` size
)
describe "btsSize on meta bytes" $
it "errors out since meta bytes cannot be converted to actual bytes" $
evaluate (btsSize (BtMeta "alpha")) `shouldThrow` anyErrorCall
describe "hex byte decoding" $
forM_
[
( "accepts lowercase hex digits the same way as uppercase ones"
, btsEqual (BtOne "bf") (BtOne "BF") `shouldBe` True
)
,
( "decodes a single hex character via the fallback numeric reader"
, btsEqual (BtOne "5") (BtOne "05") `shouldBe` True
)
,
( "errors out on a hex digit that isn't 0-9, a-f or A-F"
, evaluate (btsToUnescapedStr (BtOne "G1")) `shouldThrow` anyErrorCall
)
,
( "errors out when the fallback numeric reader can't parse the byte at all"
, evaluate (btsToUnescapedStr (BtOne "")) `shouldThrow` anyErrorCall
)
]
(uncurry it)
describe "btsSlice" $
forM_
[
( "in range"
, btsSlice 1 3 (BtMany ["20", "1F", "EE", "B5", "90"])
, Just (BtMany ["1F", "EE", "B5"])
)
, ("out of range", btsSlice 3 10 (BtMany ["20", "1F", "EE", "B5", "90"]), Nothing)
, ("negative start", btsSlice (-1) 2 (BtMany ["20", "1F", "EE"]), Nothing)
, ("zero length slice", btsSlice 0 0 (BtMany ["20", "1F"]), Just BtEmpty)
]
(\(desc, actual, expected) -> it desc (actual `shouldBe` expected))
describe "btsShift" $
forM_
[
( "positive shift crossing a bit boundary"
, btsShift 1 (BtMany ["C0", "43", "00"])
, BtMany ["60", "21", "80"]
)
, ("negative shift crossing a bit boundary", btsShift (-1) (BtMany ["01", "80"]), BtMany ["03", "00"])
, ("shift by zero is identity", btsShift 0 (BtMany ["FF", "00"]), BtMany ["FF", "00"])
,
( "positive shift crossing a byte boundary"
, btsShift 8 (BtMany ["01", "02", "03"])
, BtMany ["00", "01", "02"]
)
,
( "negative shift crossing a byte boundary"
, btsShift (-8) (BtMany ["01", "02", "03"])
, BtMany ["02", "03", "00"]
)
, ("large negative shift empties out", btsShift (-2147483648) (BtMany ["BF", "F0"]), BtMany ["00", "00"])
]
(\(desc, actual, expected) -> it desc (actual `shouldBe` expected))