ipynb-0.3: test/roundtrip.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
import Control.Monad (unless)
import Data.Aeson (Value (..), eitherDecode, encode, object, (.=))
import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString as B
import Data.Ipynb
import qualified Data.Map as M
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Encoding as TE
import qualified Data.Text.Lazy.Encoding as TLE
import Lens.Micro
import Lens.Micro.Aeson
import System.Directory
import System.FilePath
import Test.Tasty
import Test.Tasty.HUnit
import Data.Monoid
main :: IO ()
main = do
let rtdir = "test" </> "rt-files"
createDirectoryIfMissing False rtdir
fs <- map (rtdir </>) . filter isIpynb <$> getDirectoryContents rtdir
defaultMain $ testGroup "tests"
[ testGroup "round-trip tests" $ map rtTest fs
, testGroup "mime data tests" mimeTests
, testCase "execute_result with null execution_count" $ do
let inRaw =
"{\"nbformat\":4,\"nbformat_minor\":5,\"metadata\":{}," <>
"\"cells\":[{\"cell_type\":\"code\",\"metadata\":{}," <>
"\"source\":\"\",\"execution_count\":null,\"outputs\":" <>
"[{\"output_type\":\"execute_result\",\"execution_count\":null," <>
"\"data\":{\"text/plain\":\"1\"},\"metadata\":{}}]}]}"
(nb :: Notebook NbV4) <- either error return $ eitherDecode inRaw
case map cellType (notebookCells nb) of
[Code{ codeOutputs = [ExecuteResult{ executeCount = ec }] }] ->
ec @?= Nothing
_ -> assertFailure "unexpected notebook structure"
(nb' :: Notebook NbV4) <- either error return $ eitherDecode (encode nb)
nb' @?= nb
]
-- Parse a v4 notebook with a single code cell whose only output
-- is display_data with the given data object, returning the MimeBundle.
parseV4Data :: BL.ByteString -> Either String MimeBundle
parseV4Data dat = do
(nb :: Notebook NbV4) <- eitherDecode $
"{\"nbformat\":4,\"nbformat_minor\":5,\"metadata\":{},\"cells\":" <>
"[{\"cell_type\":\"code\",\"metadata\":{},\"source\":\"\"," <>
"\"execution_count\":1,\"outputs\":[{\"output_type\":\"display_data\"," <>
"\"metadata\":{},\"data\":" <> dat <> "}]}]}"
case map cellType (notebookCells nb) of
[Code{ codeOutputs = [DisplayData{ displayData = d }] }] -> Right d
_ -> Left "unexpected notebook structure"
mimeTests :: [TestTree]
mimeTests =
[ testCase "textual data that looks like base64 stays textual" $
parseV4Data "{\"application/javascript\":\"test\"}" @?=
Right (MimeBundle (M.fromList
[("application/javascript", TextualData "test")]))
, testCase "image data is decoded from base64" $
parseV4Data "{\"image/png\":\"dGVzdA==\\n\"}" @?=
Right (MimeBundle (M.fromList [("image/png", BinaryData "test")]))
, testCase "svg data stays textual" $
parseV4Data "{\"image/svg+xml\":[\"<svg>\\n\",\"</svg>\"]}" @?=
Right (MimeBundle (M.fromList
[("image/svg+xml", TextualData "<svg>\n</svg>")]))
, testCase "non-textual data under a non-json mime type is kept as json" $
parseV4Data "{\"application/vnd.foo\":{\"a\":1}}" @?=
Right (MimeBundle (M.fromList
[("application/vnd.foo", JsonData (object ["a" .= (1 :: Int)]))]))
, testCase "v3 svg, json, and pdf data is read" $
parseV3Data
"\"svg\":\"<svg/>\",\"json\":{\"a\":1},\"pdf\":\"dGVzdA==\"" @?=
Right (MimeBundle (M.fromList
[ ("image/svg+xml", TextualData "<svg/>")
, ("application/json", JsonData (object ["a" .= (1 :: Int)]))
, ("application/pdf", BinaryData "test") ]))
, testCase "v3 mime data survives write and read" $ do
let bundle = MimeBundle (M.fromList
[ ("text/plain", TextualData "hi")
, ("image/svg+xml", TextualData "<svg/>")
, ("application/json", JsonData (object ["a" .= (1 :: Int)]))
, ("application/pdf", BinaryData "test")
, ("application/vnd.foo", TextualData "foo") ])
nb :: Notebook NbV3
nb = Notebook mempty (3, 0)
[Cell (Code (Just 1) [DisplayData bundle mempty])
Nothing (Source ["x"]) mempty Nothing]
fmap (map cellType . notebookCells)
(eitherDecode (encode nb) :: Either String (Notebook NbV3)) @?=
Right (map cellType (notebookCells nb))
]
-- Like parseV4Data, but for a v3 notebook; the argument is a
-- comma-separated sequence of JSON fields to add to the display_data
-- output.
parseV3Data :: BL.ByteString -> Either String MimeBundle
parseV3Data fields = do
(nb :: Notebook NbV3) <- eitherDecode $
"{\"nbformat\":3,\"nbformat_minor\":0,\"metadata\":{},\"worksheets\":" <>
"[{\"cells\":[{\"cell_type\":\"code\",\"input\":\"\",\"metadata\":{}," <>
"\"outputs\":[{\"output_type\":\"display_data\",\"metadata\":{}," <>
fields <> "}]}]}]}"
case map cellType (notebookCells nb) of
[Code{ codeOutputs = [DisplayData{ displayData = d }] }] -> Right d
_ -> Left "unexpected notebook structure"
isIpynb :: FilePath -> Bool
isIpynb fp = takeExtension fp == ".ipynb"
rtTest :: FilePath -> TestTree
rtTest fp = testCase fp $ do
inRaw <- BL.readFile fp
let format = inRaw ^? key "nbformat" . _Number
case format of
Just 4 -> rtTest4 inRaw
_ -> rtTest3 inRaw
rtTest3 :: BL.ByteString -> IO ()
rtTest3 inRaw = do
(nb :: Notebook NbV3) <- either error return $ eitherDecode inRaw
let outRaw = encode nb
(nb' :: Notebook NbV3) <- either error return $ eitherDecode outRaw
-- test that (write . read) == id
assertEqual "write . read != read" nb nb'
rtTest4 :: BL.ByteString -> IO ()
rtTest4 inRaw = do
(nb :: Notebook NbV4) <- either error return $ eitherDecode inRaw
let outRaw = encode nb
(nb' :: Notebook NbV4) <- either error return $ eitherDecode outRaw
-- test that (write . read) == id
assertEqual "write . read != read" nb nb'