packages feed

simple-smt-1.0.1: test/Finalizer.hs

{-# OPTIONS_GHC -O2 #-}

module Main (main) where

import Control.Concurrent.MVar
  ( MVar, newEmptyMVar, putMVar, takeMVar )
import Control.Exception (evaluate)
import Control.Monad (unless, when)
import System.Environment (getArgs, getExecutablePath)
import System.Exit (ExitCode)
import System.IO (hFlush, hPutStrLn, isEOF, stderr, stdout)
import System.Mem (performGC)
import System.Timeout (timeout)

import SimpleSMT


eofMessage :: String
eofMessage = "fake solver: stdin closed"


prematureFinalizerWindow :: Int
prematureFinalizerWindow = 50000


operationTimeout :: Int
operationTimeout = 2000000


main :: IO ()
main =
  do args <- getArgs
     case args of
       ["--solver"] -> fakeSolver
       _ -> testFinalizer


testFinalizer :: IO ()
testFinalizer =
  do exe <- getExecutablePath

     -- Keeping projected Solver operations alive must keep the external
     -- process alive too.
     progress "Test 1: live solver operations prevent finalization"
     exited <- newEmptyMVar
     solver <- within "starting the first solver" $
       newSolverNotify exe ["--solver"] Nothing
         (Just (\ec ->
                  do progress ("First solver exited: " ++ show ec)
                     putMVar exited ec))
     run <- evaluate (command solver)
     finish <- evaluate (stop solver)

     progress "  Forcing GC while projected operations are still live"
     performGC

     prematureExit <- timeout prematureFinalizerWindow (takeMVar exited)
     case prematureExit of
       Just _ -> fail "Solver finalizer ran while its operations were live"
       Nothing -> progress "  Solver remained alive"

     progress "  Sending check-sat after GC"
     result <- within "waiting for check-sat" $
       run (List [Atom "check-sat"])
     unless (result == Atom "sat") $
       fail ("Unexpected check-sat response: " ++ show result)
     progress "  Stopping the first solver explicitly"
     _ <- within "stopping the first solver" finish

     -- Once the Solver and its operations are unreachable, its finalizer
     -- must close stdin and allow the external process to exit.
     progress "Test 2: unreachable solver is finalized"
     finalized <- newEmptyMVar
     sawEOF <- newEmptyMVar
     within "using the second solver" $
       useAndForgetSolver exe sawEOF finalized
     progress "  Dropped the solver; forcing GC"
     performGC

     finalResult <- timeout operationTimeout $
       do takeMVar sawEOF
          takeMVar finalized
     case finalResult of
       Just _ -> progress "  Solver observed EOF and exited"
       Nothing ->
         fail "Solver finalizer did not close stdin and terminate the process"

     progress "Finalizer tests passed"


{-# NOINLINE useAndForgetSolver #-}
useAndForgetSolver :: FilePath -> MVar () -> MVar ExitCode -> IO ()
useAndForgetSolver exe sawEOF exited =
  do let logger =
           noSolverLogger
             { solverLogStdErr = \message ->
                 when (message == eofMessage) $
                   do progress "  Fake solver observed EOF"
                      putMVar sawEOF ()
             }
         config =
           (defaultConfig exe ["--solver"])
             { solverOnExit =
                 Just (\ec ->
                         do progress ("  Second solver exited: " ++ show ec)
                            putMVar exited ec)
             , solverLogger = logger
             }
     solver <- newSolverWithConfig config
     result <- command solver (List [Atom "check-sat"])
     unless (result == Atom "sat") $
       fail ("Unexpected check-sat response: " ++ show result)


fakeSolver :: IO ()
fakeSolver =
  do eof <- isEOF
     if eof
       then hPutStrLn stderr eofMessage
       else do request <- getLine
               case request of
                 "(exit)" -> pure ()
                 "(check-sat)" -> respond "sat" >> fakeSolver
                 _ -> respond "success" >> fakeSolver
  where
  respond response =
    do putStrLn response
       hFlush stdout


within :: String -> IO a -> IO a
within description action =
  do result <- timeout operationTimeout action
     case result of
       Just value -> pure value
       Nothing -> fail ("Timed out " ++ description)


progress :: String -> IO ()
progress message =
  do putStrLn message
     hFlush stdout