-- |
-- Tests for variable scope and names
module TST.Run(tests) where
import Control.Concurrent
import Control.Concurrent.STM
import Control.Exception
import Control.Monad
import Control.Monad.IO.Class
import Data.Map.Strict qualified as Map
import Data.Text qualified as T
import Test.Tasty
import Test.Tasty.HUnit
import Python.Inline
import Python.Inline.QQ
import Python.Inline.Eval
import Python.Inline.Async
import TST.Util
tests :: TestTree
tests = testGroup "Run python"
[ testCase "Empty QQ" $ runPy [py_| |]
, testCase "Second init is noop" $ initializePython
, testRunPy
, testScope
, testEvalExec
, testMonadic
, testAsync
]
-- Test that python evaluation works in general and its interaction
-- with exceptions
testRunPy :: TestTree
testRunPy = testGroup "Running python"
[ testCase "Nested runPy" $ runPy $ liftIO $ runPy $ pure ()
, testCase "Nested runPyInMain" $ runPyInMain $ liftIO $ runPyInMain $ pure ()
, testCase "runPyInMain" $ runPyInMain $ [py_|
import threading
tid_main = threading.main_thread()
tid_our = threading.current_thread()
assert tid_main == tid_our, f"TID[main]={tid_main}, TID[our]={tid_our}"
|]
, testCase "Python exceptions are converted (py)" $ runPy $ throwsPy [py_| 1 / 0 |]
, testCase "Python exceptions are converted (std)" $ throwsPyIO $ runPy [py_| 1 / 0 |]
, testCase "Python exceptions are converted (main)" $ throwsPyIO $ runPyInMain [py_| 1 / 0 |]
, testCase "Main doesn't deadlock after exception" $ do
throwsPyIO $ runPyInMain [py_| 1 / 0 |]
runPyInMain [py_| assert True |]
-- Here we test that exceptions are really passed to python's thread without running python
, testCase "Exception in runPyInMain works hask" $ do
lock <- newEmptyMVar
tid <- myThreadId
_ <- forkIO $ takeMVar lock >> threadDelay 1000 >> throwTo tid Stop
handle (\Stop -> pure ())
$ runPyInMain
$ do liftIO $ putMVar lock ()
liftIO $ threadDelay 10_000_000
error "Should be interrupted"
]
-- Test that different quasiquotes correctly handle scoping
testScope :: TestTree
testScope = testGroup "Python scope"
[ testCase "Scope pymain->any" $ runPy $ do
[pymain|
x = 12
x
|]
-- Visible
[py_| x |]
_ <- [pye| x |]
[pymain|
x
del x
|]
-- Disappears
[pymain|
try:
x
assert False, "x shouln't be visible"
except NameError:
pass
|]
[py_|
try:
x
assert False, "x shouln't be visible"
except NameError:
pass
|]
, testCase "Scope py_->any" $ runPy $ do
[py_|
x = 12
x
|]
-- Not visible
throwsPy $ void [pye| x |]
[py_|
try:
x
assert False, "x shouln't be visible (1)"
except NameError:
pass
|]
[pymain|
try:
x
assert False, "x shouln't be visible (2)"
except NameError:
pass
|]
, testCase "Import py_->any" $ runPy $ do
[py_|
import sys
sys
|]
-- Not visible
throwsPy $ void [pye| sys |]
[py_|
try:
sys
assert False, "sys shouln't be visible (1)"
except NameError:
pass
|]
[pymain|
try:
sys
assert False, "sys shouln't be visible (2)"
except NameError:
pass
|]
, testCase "Scope pyf->any" $ runPy $ do
_ <- [pyf|
x = 12
x
return 12
|]
-- Not visible
throwsPy $ void [pye| x |]
[py_|
try:
x
assert False, "x shouln't be visible (1)"
except NameError:
pass
|]
[pymain|
try:
x
assert False, "x shouln't be visible (2)"
except NameError:
pass
|]
]
testEvalExec :: TestTree
testEvalExec = testGroup "eval/exec"
[ testCase "pyf works" $ do
let x = 12 :: Int
eq (Just (482412::Int)) [pyf|
xs = [i*x_hs for i in [1, 200, 40000]]
return sum(xs)
|]
, testCase "exec with Dict" $ runPy $ do
dct <- [pye| {} |]
exec Main (Dict dct) [pycode|
a = 12
b = 13
|]
throwsPy $ exec Main (Module dct) [pycode| |]
d <- fromPy dct
liftIO $ assertEqual "dict" (Just (Map.fromList [("a",12::Int),("b",13)])) d
, testCase "exec with Module" $ runPy $ do
m <- [pyf|
import importlib.util
spec = importlib.util.spec_from_loader("dyn", loader=None)
return importlib.util.module_from_spec(spec)
|]
exec Main (Module m) [pycode|
a = 12
b = 'asd'
|]
[py_|
import types
isinstance(m_hs, types.ModuleType)
assert m_hs.a == 12
assert m_hs.b == 'asd'
|]
, testCase "evalPyFunction" $ runPy $ do
let n = 2 :: Int
x <- fromPy =<< evalPyFunction Temp [pyfun| return 100 * n_hs |]
liftIO $ Just (200::Int) @=? x
, testCase "createModule" $ runPy $ do
m <- createModule (T.pack "foo") [pycode||]
[py_|
import types
assert isinstance(m_hs, types.ModuleType)
|]
]
testAsync :: TestTree
testAsync = testGroup "async" $ guardThreaded
[ -- We can run async computation at all
testCase "runPyAsync" $ do
runPy [pymain| dct = {} |]
a <- runPyAsync $ [py_| dct[1] = 100 |]
_ <- atomically $ waitPy a
n <- runPy $ fromPy =<< [pye| dct[1] |]
assertEqual "x" (Just (100::Int)) n
runPy [pymain| del dct |]
, -- Cancellation of python code
testCase "cancelPy [python]" $ do
a <- runPyAsync $ forever $ [py_|
import time
while True:
time.sleep(1e-3)
|]
d <- registerDelay 100_000
threadDelay 100 -- Wait to make sure execution actually started
cancelPy a
_ <- atomically $ waitPyCatch a `orElse` do readTVar d >>= \case
True -> error "Timeout"
False -> retry
return ()
, -- Cancellation of haskell code
testCase "cancelPy [haskell]" $ do
a <- runPyAsync $ do
liftIO $ forever $ threadDelay 1_000_000
d <- registerDelay 100_000
threadDelay 100
cancelPy a
_ <- atomically $ waitPyCatch a `orElse` do readTVar d >>= \case
True -> error "Timeout"
False -> retry
return ()
, -- Cancellation of haskell code
testCase "cancelPy [callback]" $ do
a <- runPyAsync $ do
let loop = forever $ threadDelay 1_000_000 :: IO ()
forever [py_| loop_hs() |]
d <- registerDelay 100_000
threadDelay 100
_ <- forkIO $ cancelPy a
_ <- atomically $ waitPyCatch a `orElse` do readTVar d >>= \case
True -> error "Timeout"
False -> retry
return ()
, testCase "Exception in runPyInMain works py" $ do
lock <- newEmptyMVar
tid <- myThreadId
_ <- forkIO $ takeMVar lock >> threadDelay 1000 >> throwTo tid Stop
handle (\Stop -> pure ())
$ runPyInMain
$ do liftIO $ putMVar lock ()
[py_|
import time
while True:
time.sleep(1e-3)
|]
error "Should be interrupted"
]
-- Here we only test that quasiquotes produce correct code
testMonadic :: TestTree
testMonadic = testGroup "Monadic"
[ testCase "pymain" $ runPy [pymain| assert True |]
, testCase "py_" $ runPy [py_| assert True |]
, testCase "pye" $ runPy $ do
n <- fromPy =<< [pye| 42 |]
liftIO $ Just (42::Int) @=? n
, testCase "pyf" $ runPy $ do
n <- fromPy =<< [pyf| return 42 |]
liftIO $ Just (42::Int) @=? n
]
data Stop = Stop
deriving stock Show
deriving anyclass Exception
guardThreaded :: [TestTree] -> [TestTree]
guardThreaded ts
| rtsSupportsBoundThreads = ts
| otherwise = []