packages feed

inline-r-1.0.0: tests/tests.hs

-- |
-- Copyright: (C) 2013 Amgen, Inc.
--
-- Main test suite for inline-r. Pass --torture on command-line or set
-- R_GCTORTURE environment variable to perform memory tests. They will be
-- ignored otherwise. Only pass --torture when linking against a version of
-- R compiled with the --enable-strict-barrier configure flag turned on.

{-# LANGUAGE GADTs #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Main where

import qualified Test.Constraints
import qualified Test.Event
import qualified Test.FunPtr
import qualified Test.GC
import qualified Test.Regions
import qualified Test.Vector
import qualified Test.Matcher

import H.Prelude
import qualified Foreign.R as R
import qualified Language.R.Instance as R
    ( initialize
    , defaultConfig )

import Test.Tasty
import Test.Tasty.HUnit
import Test.Tasty.ExpectedFailure

import Control.Applicative
import Control.Concurrent
import Control.Exception (handle)
import Control.Memory.Region
import Control.Monad (void)
import Data.List
import Data.Singletons (sing)
import Data.Vector.SEXP (indexM)
import Foreign hiding (void)
import System.Environment (getArgs, lookupEnv, withArgs)
import qualified Text.Heredoc as Heredoc
import Prelude -- Silence AMP warning

inVoid :: R V z -> R V z
inVoid = id

tests :: Bool -> TestTree
tests torture = testGroup "Unit tests"
  [ testCase "fromSEXP . mkSEXP" $ do
      z <- fromSEXP <$> mkSEXPIO (2 :: Double)
      (2 :: Double) @=? z
  , testCase "HEq HExp" $ do
      -- XXX ideally randomly generate input.
      let x = 2 :: Double
      R.withProtected (mkSEXPIO x) $ \z ->
        assertBool "reflexive" $
          let s = hexp z in s === s
      R.withProtected (mkSEXPIO x) $ \z ->
        assertBool "symmetric" $
          let s1 = hexp z
              s2 = hexp z
          in s1 === s2 && s2 === s1
      R.withProtected (mkSEXPIO x) $ \z ->
        assertBool "transitive" $
          let s1 = hexp z
              s2 = hexp z
              s3 = hexp z
          in s1 === s2 && s2 === s3 && s1 === s3
  , testCase "Haskell function from R" $ do
      (((3::Double) @=?) =<<) $ fmap fromSEXP $
          alloca $ \p -> do
            e <- peek R.globalEnv
            R.withProtected (mkSEXPIO $ \x -> return $ x + 1 :: R s Double) $
              \sf -> R.withProtected (mkSEXPIO (2::Double)) $ \d ->
                       R.withProtected (R.lang2 sf d) (unsafeRunRegion . eval)
                       >>= \(R.SomeSEXP s) ->
                              R.cast (sing :: R.SSEXPTYPE 'R.Real) <$>
                              R.tryEval s (R.release e) p
  , testCase "Weak Ptr test" $ runRegion $ do
      R.SomeSEXP key  <- [r| new.env() |]
      R.SomeSEXP val  <- [r| new.env() |]
      True <- return $ R.typeOf val == R.Env
      n    <- io $ R.release <$> peek R.nilValue
      rf   <- io $ R.mkWeakRef key val n True
      True <- case hexp rf of
                WeakRef a b c _ -> do
                  True <- return $ (R.unsexp a) == (R.unsexp key)
                  True <- return $ (R.unsexp b) == (R.unsexp val)
                  return $ (R.unsexp c) == (R.unsexp n)
      return ()
  , testCase "Hexp works" $
      (((42::Double) @=?) =<<) $ runRegion $ do
         y <- R.cast (sing :: R.SSEXPTYPE 'R.Real) . R.SomeSEXP
                     <$> mkSEXP (42::Double)
         case hexp y of
           Real s -> s `indexM` 0
  , Test.Constraints.tests
  , Test.FunPtr.tests
  , (if torture then id else ignoreTest) Test.GC.tests
  , (if torture then id else ignoreTest) Test.Regions.tests
  , Test.Vector.tests
  , Test.Event.tests
  , Test.Matcher.tests
    -- This test helps compiling quasiquoters concurrently from
    -- multiple modules. This in turns helps testing for race
    -- conditions when initializing R from multiple threads.
  , testCase "qq/concurrent-initialization" $ runRegion $ [r| 1 |] >> return ()
  , testCase "sanity check " $ return ()
  , testCase "cancel works" $ do
      void $ forkIO $ cancel
      handle (\RError{} -> return ())
             (runRegion $ void $ [r| while(1){}; |])
  , testGroup "collectAntis"
    [ testCase "Succeeds on a simple expression" $ do
        result <- collectAntis "foo_hs + bar + baz_hs"
        case result of
          Left err -> assertFailure err
          Right ids -> sort ids @?=
            sort ["foo_hs", "baz_hs"]
    , testCase "Returns an error on invalid R code" $ do
        result <- collectAntis "foo_hs bar"
        case result of
          Left err -> assertBool "Unrecognized error message" $
            "unexpected symbol" `isInfixOf` err
          Right _ -> assertFailure "Unexpected success"
    , testCase "Succeeds on a more complicated expression and ignores symbols inside strings" $ do
        result <- collectAntis [Heredoc.here|
            function () {
              r <- fft(foo_hs) + bar_hs + "baz_hs"
              f(r, quux_hs)
            }
          |]
        case result of
          Left err -> assertFailure err
          Right ids -> sort ids @?=
            sort ["foo_hs", "bar_hs", "quux_hs"]
    ]
  ]

main :: IO ()
main = do
    _ <- R.initialize R.defaultConfig
    argv <- getArgs
    -- Assume gctorture() step size of 1.
    let tortureCLI = "1" <$ find (== "--torture") argv
    tortureEnv <- lookupEnv "R_GCTORTURE"
    torture <- case tortureCLI <|> tortureEnv of
      Nothing -> return False
      Just x -> do
        -- gctorture turned on. So assume moreover --enable-strict-barrier.
        let step = read x :: Int32
        putStrLn "WARNING: gctorture() turned on.\n\
                 \    Tests will fail if R not compiled with --enable-strict-barrier."
        runRegion $ void [r| gctorture2(step = step_hs, inhibit_release = TRUE) |]
        return True
    withArgs (delete "--torture" argv) $
      defaultMain (tests torture)