telescope-0.3.0: test/Test/Asdf/EncodeSpec.hs
module Test.Asdf.EncodeSpec where
import Control.Monad (forM_)
import Data.ByteString qualified as BS
import Data.ByteString.Char8 qualified as BC
import Data.Massiv.Array (Array, Comp (Seq), D, Ix2, P)
import Data.Massiv.Array qualified as M
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.Encoding qualified as T
import GHC.Generics (Generic)
import GHC.Int (Int16, Int64)
import Skeletest
import Skeletest.Predicate qualified as P
import Telescope.Asdf.Class
import Telescope.Asdf.Core
import Telescope.Asdf.Encoding
import Telescope.Asdf.Encoding.File
import Telescope.Asdf.Error
import Telescope.Asdf.NDArray
import Telescope.Asdf.Node
import Telescope.Data.Parser (expected)
import Test.Asdf.ClassSpec (expectObject)
spec :: Spec
spec = do
describe "basic" basicSpec
describe "document" documentSpec
describe "blocks" blocksSpec
describe "roundtrip" roundSpec
describe "stream" streamSpec
describe "external verification" externalSpec
describe "references" referenceSpec
describe "anchors" anchorSpec
anchorSpec :: Spec
anchorSpec = do
it "should encode an anchor" $ do
(out, _) <- runAsdfM . encodeNode $ Node mempty (Just "woot") "hello"
out `shouldBe` "&woot 'hello'\n"
it "should encode an alias" $ do
let alias = toNode $ Alias $ Anchor "something" :: Node
let anc = Node mempty (Just "something") "something"
(out, _) <- runAsdfM . encodeNode $ toNode $ Array [anc, alias]
out `shouldBe` "[&something 'something', *something]\n"
it "should not encode anchors to mappings" $ do
(out, _) <- runAsdfM . encodeNode $ Node mempty (Just "thing") (Object [("hello", "world")])
let outt = T.decodeUtf8 out
length (T.splitOn "&thing" outt) `shouldBe` 2
out `shouldBe` "&thing {hello: world}\n"
it "should not encode anchors to array members" $ do
(out, _) <- runAsdfM . encodeNode $ Node mempty (Just "thing") (Array ["one", "two"])
let outt = T.decodeUtf8 out
length (T.splitOn "&thing" outt) `shouldBe` 2
out `shouldBe` "&thing [one, two]\n"
it "should throw if alias before anchor" $ do
let vals = [toNode $ Alias "two", Node mempty (Just "two") "two"]
runAsdfM (encodeNode $ toNode (Array vals)) `shouldSatisfy` P.throws @AsdfError P.anything
referenceSpec :: Spec
referenceSpec = do
it "should encode a pointer" $ do
let ref = JSONReference mempty (jsonPointer "#/users/1/name")
(out, _) <- runAsdfM . encodeNode $ toNode $ Reference ref
out `shouldBe` "{$ref: '#/users/1/name'}\n"
it "should encode a reference" $ do
let ref = JSONReference "https://woot.com/" (jsonPointer "#/users/1/name")
(out, _) <- runAsdfM . encodeNode $ toNode $ Reference ref
out `shouldBe` "{$ref: 'https://woot.com/#/users/1/name'}\n"
it "should roundtrip reference" $ do
let ref = Reference $ JSONReference mempty (jsonPointer "#/users/1/name")
let tree = Object [("username", toNode ref)]
out <- encodeM tree
obj <- decodeM @Value out >>= expectObject
lookup "username" obj `shouldBe` Just (Node mempty Nothing ref)
-- it "should rountrip and resolve references" $ do
-- let pn = PointyName "pip"
-- toValue pn `shouldBe` InternalRef (pointer "/names/2")
--
-- let pd = PointyData pn ["bob", "pip", "will"]
-- out <- encodeM pd
-- pd2 <- decodeM @PointyData out
--
-- pd2.other `shouldBe` PointyName "will"
basicSpec :: Spec
basicSpec = do
it "starts with required header lines" $ do
out <- encodeM (Object [])
(l1 : l2 : l3 : l4 : doc1 : _) <- pure $ BC.lines out
l1 `shouldBe` "#ASDF 1.0.0"
l2 `shouldBe` "#ASDF_STANDARD 1.5.0"
l3 `shouldBe` "%YAML 1.1"
l4 `shouldBe` "%TAG ! tag:stsci.edu:asdf/"
BS.take 4 doc1 `shouldBe` "--- "
it "should include history" $ do
out <- encodeM (Object [])
let (_, restL) = BS.breakSubstring "asdf_library:" out
BS.length restL `shouldNotBe` 0
let (_, restH) = BS.breakSubstring "history:" out
BS.length restH `shouldNotBe` 0
it "should throw if not an object" $ do
encodeM (Integer 100) `shouldSatisfy` P.throws @AsdfError P.anything
streamSpec :: Spec
streamSpec = do
it "should encode an empty string as empty single quotes" $ do
let unit = fromValue $ String ""
let obj = Object [("unit", unit)]
(out, _) <- runAsdfM . encodeNode $ toNode obj
out `shouldBe` "{unit: ''}\n"
documentSpec :: Spec
documentSpec = do
it "converts to document" $ do
asdf <- runAsdfM $ toAsdfDoc $ BasicData "henry"
asdf.library.name `shouldBe` "telescope.hs"
let Tree tree = asdf.tree
lookup "username" tree `shouldBe` Just "henry"
blocksSpec :: Spec
blocksSpec = do
it "includes blocks" $ do
let ns = [1 .. 100]
out <- encodeM (BasicArray ns)
af <- runAsdfM $ splitAsdfFile out
length af.blocks `shouldBe` 1
[BlockData bd] <- runAsdfM $ mapM decodeBlock af.blocks
bd `shouldBe` (toNDArray ns).bytes
describe "index" $ do
it "increeasing" $ do
let nd1 = toNDArray ([1 .. 10] :: [Int64])
let nd2 = toNDArray $ matrix @Int64 [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
let blks = fmap (encodeBlock . BlockData) [nd1.bytes, nd2.bytes, nd1.bytes]
let tree = "1234567890"
let BlockIndex ix = blockIndex tree blks
length ix `shouldBe` 3
[i1, i2, i3] <- pure ix
let start = BS.length tree.bytes
i1 `shouldBe` start
i2 `shouldSatisfy` P.gt (start + (10 * 8))
i3 `shouldSatisfy` P.gt i2
it "equivalent to example.asdf" $ do
inp <- BS.readFile "samples/example.asdf"
e <- decodeM @Value inp
e `shouldSatisfy` P.con (Object P.anything)
af <- runAsdfM $ toAsdfDoc e >>= encodeAsdf
length af.blocks `shouldBe` 3
let BlockIndex ix = blockIndex af.tree af.blocks
length ix `shouldBe` 3
(i1 : _) <- pure ix
i1 `shouldBe` BS.length af.tree.bytes
fmap (subtract i1) ix `shouldBe` fmap (subtract 897) [897, 1751, 2605]
it "addresses blocks" $ do
inp <- BS.readFile "samples/example.asdf"
e <- decodeM @Value inp
o <- encodeM e
BlockIndex ix <- runAsdfM $ do
a <- toAsdfDoc e
af <- encodeAsdf a
pure $ blockIndex af.tree af.blocks
forM_ ix $ \n -> do
BS.take 4 (BS.drop n o) `shouldBe` blockMagicToken
roundSpec :: Spec
roundSpec = do
it "decodes encoded file" $ do
out <- encodeM (Object [("hello", "world")])
tree <- decodeM @Value out >>= expectObject
lookup "hello" tree `shouldSatisfy` P.just (P.eq "world")
it "encodes data type fields" $ do
out <- encodeM $ BasicData "hello"
let (_, nameRest) = BS.breakSubstring "username: hello" out
nameRest `shouldNotBe` ""
it "encodes simple ndarray" $ do
out <- encodeM $ BasicArray [1 .. 100]
BasicArray ns <- decodeM out
ns `shouldBe` [1 .. 100]
it "encodes massiv array" $ do
let mx = matrix [[1.0 .. 5.0], [2.0 .. 6.0]]
out <- encodeM $ Matrix mx
-- TEST: throws an error if NDArrayData.shape doesn't match
Matrix ns <- decodeM out
ns `shouldBe` mx
it "decodes encoded data type" $ do
let sd = SomeData 24 ["one", "two"] $ matrix [[1, 2, 3], [4, 5, 6]]
out <- encodeM sd
sd2 <- decodeM @SomeData out
sd2.number `shouldBe` sd.number
sd2.tags `shouldBe` sd.tags
sd2.matrix `shouldBe` sd.matrix
externalSpec :: Spec
externalSpec = do
it "saves encoded document to an asdf for external verification in python" $ do
let sd = SomeData 24 ["one", "two"] $ matrix [[1, 2, 3], [4, 5, 6]]
out <- encodeM sd
BS.writeFile "samples/generated.asdf" out
matrix :: (M.Prim n) => [[n]] -> Array D Ix2 n
matrix ns = M.delay @Ix2 @P $ M.fromLists' Seq ns
newtype BasicArray = BasicArray [Int64]
instance ToAsdf BasicArray where
toValue (BasicArray ns) =
Object
[("values", fromValue $ NDArray $ toNDArray ns)]
instance FromAsdf BasicArray where
parseValue = \case
Object o -> do
nd <- o .: "values"
ns <- fromNDArray nd
pure $ BasicArray ns
val -> expected "BasicArray.values" val
data Matrix = Matrix
{ values :: Array D Ix2 Double
}
deriving (Generic, ToAsdf, FromAsdf)
data BasicData = BasicData
{ username :: Text
}
deriving (Generic, ToAsdf, FromAsdf)
data SomeData = SomeData
{ number :: Int
, tags :: [Text]
, matrix :: Array D Ix2 Int16
}
deriving (Generic, ToAsdf, FromAsdf)
-- TEST: round trip file parts
-- TEST: round trip is the only good way
-- data PointyData = PointyData
-- { other :: PointyName
-- , names :: [Text]
-- }
-- deriving (Generic, FromAsdf, ToAsdf, Show, Eq)
--
--
-- newtype PointyName = PointyName Text
-- deriving (Show, Eq)
-- instance ToAsdf PointyName where
-- toValue (PointyName _) = InternalRef (pointer "/names/2")
-- instance FromAsdf PointyName where
-- parseValue = \case
-- String s -> pure $ PointyName s
-- InternalRef p -> parsePointer p
-- other -> expected "PointyName Ref" other
--