packages feed

pandoc-plot-0.7.0.0: tests/Common.hs

{-# LANGUAGE FlexibleContexts  #-}
{-# LANGUAGE OverloadedStrings #-}

module Common where

import           Control.Monad                    (unless, when)

import           Data.List                        (isInfixOf, isSuffixOf)
import           Data.Monoid                      ((<>))
import           Data.String                      (fromString)
import           Data.Text                        (Text, pack, unpack)

import           Test.Tasty
import           Test.Tasty.HUnit

import           Text.Pandoc.Filter.Plot
import           Text.Pandoc.Filter.Plot.Internal

import qualified Text.Pandoc.Builder              as B
import qualified Text.Pandoc.Definition           as B
import           Text.Pandoc.JSON

import           System.Directory                 (createDirectory,
                                                   createDirectoryIfMissing,
                                                   doesDirectoryExist,
                                                   doesFileExist, listDirectory,
                                                   removeDirectoryRecursive,
                                                   removePathForcibly, 
                                                   getTemporaryDirectory)
import           System.FilePath                  (takeExtensions, (</>))


defaultTestConfig :: Configuration
defaultTestConfig = defaultConfiguration {logVerbosity=Silent, logSink=StdErr}

-------------------------------------------------------------------------------
-- Test that plot files and source files are created when the filter is run
testFileCreation :: Toolkit -> TestTree
testFileCreation tk =
    testCase "writes output files in appropriate directory" $ do
        let postfix = unpack . cls $ tk
        tempDir <- (</> "test-file-creation-" <> postfix) <$> getTemporaryDirectory
        ensureDirectoryExistsAndEmpty tempDir

        let cb = (addDirectory tempDir $ codeBlock tk (trivialContent tk))
        _ <- make defaultTestConfig cb
        filesCreated <- length <$> listDirectory tempDir
        assertEqual "" 2 filesCreated

-------------------------------------------------------------------------------
-- Test that included files are found within the source
testFileInclusion :: Toolkit -> TestTree
testFileInclusion tk =
    testCase "includes plot inclusions" $ do
        let postfix = unpack . cls $ tk
        tempDir <- (</> "test-file-inclusion-" <> postfix) <$> getTemporaryDirectory
        ensureDirectoryExistsAndEmpty tempDir

        let cb = (addPreamble (include tk) $
                    addDirectory tempDir $ codeBlock tk (trivialContent tk))
        _ <- make defaultTestConfig cb
        inclusion <- readFile (include tk)
        sourcePath <- head . filter (isExtensionOf "txt") <$> listDirectory tempDir
        src <- readFile (tempDir </> sourcePath)
        assertIsInfix inclusion src
    where
        include Matplotlib   = "tests/includes/matplotlib.py"
        include PlotlyPython = "tests/includes/plotly-python.py"
        include Matlab       = "tests/includes/matlabplot.m"
        include Mathematica  = "tests/includes/mathplot.m"
        include Octave       = "tests/includes/octave.m"
        include GGPlot2      = "tests/includes/ggplot2.r"
        include GNUPlot      = "tests/includes/gnuplot.gp"
        include Graphviz     = "tests/includes/graphviz.dot"

-------------------------------------------------------------------------------
-- Test that the files are saved in the appropriate format
testSaveFormat :: Toolkit -> TestTree
testSaveFormat tk =
    testCase "saves in the appropriate format" $ do
        let postfix = unpack . cls $ tk
        tempDir <- (</> "test-safe-format-" <> postfix) <$> getTemporaryDirectory
        ensureDirectoryExistsAndEmpty tempDir
        let fmt = head (supportedSaveFormats tk)
            cb = (addSaveFormat fmt $
                 addDirectory tempDir $ codeBlock tk (trivialContent tk))
        _ <- make defaultTestConfig cb
        numberjpgFiles <-
            length <$> filter (isExtensionOf (extension fmt)) <$>
            listDirectory tempDir
        assertEqual "" numberjpgFiles 1

-------------------------------------------------------------------------------
-- Test that it is possible to not render source links in captions
testWithSource :: Toolkit -> TestTree
testWithSource tk =
    testCase "appropriately omits links to source code" $ do
        let postfix = unpack . cls $ tk
        tempDir <- (</> "test-caption-links-" <> postfix) <$> getTemporaryDirectory
        ensureDirectoryExistsAndEmpty tempDir

        let expected = "caption content"
            noSource = addWithSource False 
                      $ addDirectory tempDir 
                      $ addCaption expected 
                      $ codeBlock tk (trivialContent tk)
            withSource = addWithSource True 
                      $ addDirectory tempDir 
                      $ addCaption expected 
                      $ codeBlock tk (trivialContent tk)
        blockNoSource   <- make defaultTestConfig noSource
        blockWithSource <- make defaultTestConfig withSource

        -- In the case where source=false, the caption is used verbatim.
        -- Otherwise, links will be appended to the caption; hence, the caption
        -- is no longer equal to the initial value
        assertEqual    "" (B.toList $ fromString expected) (extractCaption blockNoSource)
        assertNotEqual "" (B.toList $ fromString expected) (extractCaption blockWithSource)

    where
        extractCaption (B.Para blocks) = extractImageCaption . head $ blocks
        extractCaption _               = mempty

        extractImageCaption (Image _ c _) = c
        extractImageCaption _             = mempty

-------------------------------------------------------------------------------
-- Test that parameters in code blocks will override the defaults in configuration
testOverrideConfiguration :: Toolkit -> TestTree
testOverrideConfiguration tk = 
    -- We set the default save format to JPG via the configuration, 
    -- but set the code block parameter to PNG.
    -- Therefore, after the filter has been used, there should be one PNG file and
    -- no JPG files.
    testCase "code block attributes override configuration defaults" $ do
        let postfix = unpack . cls $ tk
        tempDir <- (</> "test-caption-links-" <> postfix) <$> getTemporaryDirectory
        ensureDirectoryExistsAndEmpty tempDir

        let config = defaultTestConfig { defaultDirectory = tempDir
                                       , defaultSaveFormat = JPG}

        -- Not all toolkits support both save formats
        when (  JPG `elem` supportedSaveFormats tk 
             && PNG `elem` supportedSaveFormats tk
             ) $ do

            let cb = addDirectory tempDir 
                        $ addSaveFormat PNG
                        $ codeBlock tk (trivialContent tk)
            _ <- make config cb

            numberPngFiles <-
                length <$> filter (isExtensionOf (extension PNG)) <$>
                listDirectory (defaultDirectory config)
            numberJpgFiles <-
                length <$> filter (isExtensionOf (extension JPG)) <$>
                listDirectory (defaultDirectory config)
            assertEqual "" numberPngFiles 1
            assertEqual "" numberJpgFiles 0

-------------------------------------------------------------------------------
-- Test that Markdown bold formatting in captions is correctly rendered
testMarkdownFormattingCaption1 :: Toolkit -> TestTree
testMarkdownFormattingCaption1 tk =
    testCase "appropriately parses captions 1" $ do
        let postfix = unpack . cls $ tk
        tempDir <- (</> "test-caption-parsing1-" <> postfix) <$> getTemporaryDirectory
        ensureDirectoryExistsAndEmpty tempDir

        -- Note that this test is fragile, in the sense that the expected result must be carefully
        -- constructed
        let expected = [B.Strong [B.Str "caption"]]
            cb = addDirectory tempDir 
                    $ addCaption "**caption**" 
                    $ codeBlock tk (trivialContent tk)
            fmt = B.Format "markdown"
        result <- make (defaultTestConfig {captionFormat=fmt}) cb
        assertIsInfix expected (extractCaption result)
    where
        extractCaption (B.Para blocks) = extractImageCaption . head $ blocks
        extractCaption _               = mempty

        extractImageCaption (Image _ c _) = c
        extractImageCaption _             = mempty

-------------------------------------------------------------------------------
-- Test that Markdown bold formatting in captions is correctly rendered
testMarkdownFormattingCaption2 :: Toolkit -> TestTree
testMarkdownFormattingCaption2 tk =
    testCase "appropriately parses captions 2" $ do
        let postfix = unpack . cls $ tk
        tempDir <- (</> "test-caption-parsing2-" <> postfix) <$> getTemporaryDirectory
        ensureDirectoryExistsAndEmpty tempDir

        -- Note that this test is fragile, in the sense that the expected result must be carefully
        -- constructed
        let expected =  [Link ("",[],[]) [Str "title"] ("https://google.com","")]
            cb = addDirectory tempDir 
                    $ addCaption "[title](https://google.com)" 
                    $ codeBlock tk (trivialContent tk)
            fmt = B.Format "markdown"
        result <- make (defaultTestConfig {captionFormat=fmt}) cb
        assertIsInfix expected (extractCaption result)
    where
        extractCaption (B.Para blocks) = extractImageCaption . head $ blocks
        extractCaption _               = mempty

        extractImageCaption (Image _ c _) = c
        extractImageCaption _             = mempty


-------------------------------------------------------------------------------
-- Test that cleanOutpuDirs correctly cleans the output directory specified in a block.
testCleanOutputDirs :: Toolkit -> TestTree
testCleanOutputDirs tk = 
    testCase "correctly cleans output directories" $ do
        let postfix = unpack . cls $ tk
        tempDir <- (</> "test-clean-output-dir" <> postfix) <$> getTemporaryDirectory
        ensureDirectoryExistsAndEmpty tempDir

        let cb = addDirectory tempDir
                    $ codeBlock tk (trivialContent tk)
        
        result <- make defaultTestConfig cb
        cleanedDirs <- cleanOutputDirs defaultTestConfig cb

        assertEqual "" [tempDir] cleanedDirs

        outputDirExists <- doesDirectoryExist tempDir
        assertEqual "" outputDirExists False


-------------------------------------------------------------------------------
-- Test that toolkit checks failed when appropriate.
testChecksFail :: Toolkit -> TestTree
testChecksFail tk = 
    testCase "script checks fail when appropriate" $ do
        assertChecksFail tk
    where
        assertChecksFail Matplotlib = do
            let postfix = unpack . cls $ tk
            tempDir <- (</> "test-checks" <> postfix) <$> getTemporaryDirectory
            ensureDirectoryExistsAndEmpty tempDir

            let cb = addDirectory tempDir $ codeBlock Matplotlib "plt.show()"
            result <- makeEither defaultTestConfig cb
            let expectedCheck :: Either PandocPlotError a -> Bool
                expectedCheck (Left (ScriptChecksFailedError _)) = True
                expectedCheck _ = False
            assertBool "" (expectedCheck result)

        assertChecksFail _          = assertEqual "Test skipped" True True


codeBlock :: Toolkit -> Script -> Block
codeBlock tk script = CodeBlock (mempty, [cls tk], mempty) script


trivialContent :: Toolkit -> Script
trivialContent Matplotlib   = "import matplotlib.pyplot as plt\n"
trivialContent PlotlyPython = "import plotly.graph_objects as go; fit = go.Figure()\n"
trivialContent Matlab       = "figure('visible', 'off')\n"
trivialContent Mathematica  = "\n"
trivialContent Octave       = "figure('visible', 'off')\nplot (-10:0.1:10);"
trivialContent GGPlot2      = "library(ggplot2)\nggplot()\n"
trivialContent GNUPlot      = "plot sin(x)"
trivialContent Graphviz     = "digraph {A -> B [label=\"test\"];}"


addCaption :: String -> Block -> Block
addCaption caption (CodeBlock (id', cls, attrs) script) =
    CodeBlock (id', cls, attrs ++ [(tshow CaptionK, pack caption)]) script


addDirectory :: FilePath -> Block -> Block
addDirectory dir (CodeBlock (id', cls, attrs) script) =
    CodeBlock (id', cls, attrs ++ [(tshow DirectoryK, pack dir)]) script


addPreamble :: FilePath -> Block -> Block
addPreamble inclusionPath (CodeBlock (id', cls, attrs) script) =
    CodeBlock (id', cls, attrs ++ [(tshow PreambleK, pack inclusionPath)]) script


addSaveFormat :: SaveFormat -> Block -> Block
addSaveFormat saveFormat (CodeBlock (id', cls, attrs) script) =
    CodeBlock (id', cls, attrs ++ [(tshow SaveFormatK, pack . extension $ saveFormat)]) script


addDPI :: Int -> Block -> Block
addDPI dpi (CodeBlock (id', cls, attrs) script) =
    CodeBlock (id', cls, attrs ++ [(tshow DpiK, pack . show $ dpi)]) script


addWithSource :: Bool -> Block -> Block
addWithSource yn (CodeBlock (id', cls, attrs) script) =
    CodeBlock (id', cls, attrs ++ [(tshow WithSourceK, pack . show $ yn)]) script


-- | Assert that a file exists
assertFileExists :: HasCallStack => FilePath -> Assertion
assertFileExists filepath = do
    fileExists <- doesFileExist filepath
    unless fileExists (assertFailure msg)
  where
    msg = mconcat ["File ", filepath, " does not exist."]


-- | Assert not equal
assertNotEqual :: (HasCallStack, Eq a, Show a) => String -> a -> a -> Assertion
assertNotEqual msg expected actual = 
    unless (expected /= actual) 
        (assertFailure $ mconcat [msg, ": expected ", show expected, " but got ", show actual])


-- | Not available with GHC < 8.4
-- since this function was added in filepath-1.4.2
-- but GHC 8.2.2 comes with filepath-1.4.1.2
isExtensionOf :: String -> FilePath -> Bool
isExtensionOf ext@('.':_) = isSuffixOf ext . takeExtensions
isExtensionOf ext         = isSuffixOf ('.':ext) . takeExtensions


-- | Assert that the first list is contained,
-- wholly and intact, anywhere within the second.
assertIsInfix :: (Eq a, Show a, HasCallStack) => [a] -> [a] -> Assertion
assertIsInfix xs ys = unless (xs `isInfixOf` ys) (assertFailure msg)
  where
    msg = mconcat ["Expected ", show xs, " to be an infix of ", show ys]


-- Ensure a directory is empty but exists.
ensureDirectoryExistsAndEmpty :: FilePath -> IO ()
ensureDirectoryExistsAndEmpty dir = do
    exists <- doesDirectoryExist dir
    if exists
        then removePathForcibly dir
        else return ()
    createDirectory dir


tshow :: Show a => a -> Text
tshow = pack . show