aboutsummaryrefslogtreecommitdiff
path: root/server/test/Thermoprint/ServerSpec.hs
blob: d1dadbaaecfe90b1150aea9b6364c1ad2f95a9bc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
{-# LANGUAGE OverloadedStrings  #-}
{-# LANGUAGE OverloadedLists  #-}
{-# LANGUAGE ImpredicativeTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE StandaloneDeriving #-}

module Thermoprint.ServerSpec (spec) where

import           Test.Hspec
import           Test.Hspec.QuickCheck
import qualified Test.Hspec as Hspec

import           Test.QuickCheck

import           Thermoprint.API
import qualified Thermoprint.Server as S
import           Thermoprint.Client

import           Data.Monoid
import           Data.Function
import           Control.Monad
import           Control.Monad.Logger
import           Control.Monad.Reader
import           Control.Monad.State
import           Control.Monad.Trans.Identity
import           Control.Monad.Trans.Resource
import           Control.Monad.Catch (finally)

import           Database.Persist.Sqlite

import           Control.Concurrent
import           Control.Concurrent.STM
import           Control.Concurrent.STM.TSem
import           Control.Concurrent.Async

import           Control.Exception
import           System.IO.Error

import           System.IO
import           System.IO.Temp

import qualified Data.Text as T

import           Network.Wai.Handler.Warp (defaultSettings, setBeforeMainLoop)
import           Network.HTTP.Types.Status (Status(..))

import qualified Data.Map as Map

deriving instance Eq PrintingError
deriving instance Eq JobStatus
deriving instance Eq PrinterStatus

-- Equality via cotext on Block
instance Eq Block where
  (==) = (==) `on` cotext
-- Structural equality for Chunk 
deriving instance Eq Chunk

data TestPrinter = TestPrinter
  { outputChan :: TMVar Printout
  , failSwitch :: TMVar PrintingError
  }

data TestManager = TestManager
  { manage :: TMVar (S.QueueManager IdentityT)
  , ran :: TSem
  }

data RunningServer = RunningServer
  { thread :: ThreadId
  , termination :: MVar (Either SomeException ())
  , startup :: QSem
  , printer :: TestPrinter
  , manager :: TestManager
  }

setup :: QSem -> IO RunningServer
setup startup = withSystemTempFile "thermoprint.sqlite" $ \fp h -> hClose h >> do
  tPrinter <- TestPrinter <$> newEmptyTMVarIO <*> newEmptyTMVarIO
  tManager <- TestManager <$> newEmptyTMVarIO <*> atomically (newTSem 0)
  term <- newEmptyMVar
  startSem <- newQSem 0
  let
    runSqlite :: ReaderT ConnectionPool (LoggingT IO) a -> IO a
    runSqlite = runStderrLoggingT . filterLogger (const (> LevelDebug)) . withSqlitePool (T.pack fp) 1 . runReaderT

    printers = [ ( pure $ S.PM tPM
                 , S.QMConfig ((join . lift $ takeTMVar (manage tManager) <* signalTSem (ran tManager)) >> return 0) (NT $ liftIO . runIdentityT)
                 )
               ]

    tPM :: MonadIO m => Printout -> m (Maybe PrintingError)
    tPM printout = liftIO . atomically $ putTMVar (outputChan tPrinter) printout >> tryTakeTMVar (failSwitch tPrinter)
  RunningServer <$> forkFinally (S.thermoprintServer False (NT runSqlite) $ def' `S.withPrinters` printers) (putMVar term) <*> pure term <*> pure startSem <*> pure tPrinter <*> pure tManager
  where
    def' :: MonadIO m => S.Config m
    def' = S.def { S.warpSettings = setBeforeMainLoop (signalQSem startup) $ defaultSettings }

withSetup :: SpecWith RunningServer -> Spec
withSetup = beforeAll setup' . afterAll teardown
  where
    setup' = do
      startup <- newQSem 0
      r <- setup startup
      waitStartup <- async $ waitQSem startup
      waitTermination <- async . readMVar $ termination r
      wait <- waitEitherCancel waitTermination waitStartup
      case wait of
        Left (Left err) -> throwIO err
        Left (Right _) -> throwIO $ userError "Server thread terminated early"
        Right _ -> return r
    teardown RunningServer{..} = killThread thread >> void (readMVar termination)

spec :: Spec
spec = withSetup $ do
  it "Reports initial server state" $ \RunningServer{..} -> do
    printers                     `shouldReturn` [(0, Available)]
    jobs Nothing Nothing Nothing `shouldReturn` []
    drafts                       `shouldReturn` []
  it "Reports printing errors" $ \RunningServer{..} -> do
    let
      err = IOError "test"
    atomically $ putTMVar (failSwitch printer) err
    jId <- jobCreate Nothing mempty
    atomically . takeTMVar $ outputChan printer
    jobStatus jId `shouldReturn` (Failed err)
  -- it "Queues any Printout" $ \RunningServer{..} -> property $ \p -> do
  --   jId <- jobCreate (Just 0) p
  --   (atomically . takeTMVar . outputChan $ printer) `shouldReturn` p
  it "Reports qualitative queue position" $ \RunningServer{..} -> do
    jids <- replicateM 3 $ jobCreate (Just 0) mempty
    zipWithM_ (\jid ret -> jobStatus jid `shouldReturn` ret) jids
      [ Done
      , Printing 0
      , Queued 0
      ]
    replicateM_ (length jids) . atomically . takeTMVar . outputChan $ printer
    zipWithM_ (\jid ret -> jobStatus jid `shouldReturn` ret) jids (replicate (length jids) Done)
  it "Keeps history" $ \RunningServer{..} -> do
    jobs Nothing Nothing Nothing `shouldNotReturn` []
  it "Runs queue managers" $ \RunningServer{..} -> do
    atomically $ putTMVar (manage manager) S.nullQM
    atomically $ waitTSem (ran manager)
    jobs Nothing Nothing Nothing `shouldReturn` []
  it "Handles drafts" $ \RunningServer{..} -> do
    drafts `shouldReturn` []
    dId <- draftCreate Nothing mempty
    draft dId `shouldReturn` (Nothing, mempty)
    drafts `shouldReturn` [(dId, Nothing :: Maybe DraftTitle)]
    p <- generate arbitrary
    draftReplace dId (Just "Title") p
    draft dId `shouldReturn` (Just "Title", p)
    jId <- draftPrint dId Nothing
    (atomically . takeTMVar $ outputChan printer) `shouldReturn` p
    draftDelete dId
    drafts `shouldReturn` []
    draftReplace dId Nothing p `shouldThrow` is404
  where
    Client{..} = mkClient' $ BaseUrl Http "localhost" 3000 ""
    is404 :: ServantError -> Bool
    is404 (FailureResponse e) = statusCode (responseStatusCode e) == 404
    is404 _ = False