|
1 | 1 | {-# LANGUAGE AllowAmbiguousTypes #-} |
| 2 | +{-# LANGUAGE DeriveAnyClass #-} |
2 | 3 | {-# LANGUAGE ExistentialQuantification #-} |
3 | 4 | {-# LANGUAGE FlexibleContexts #-} |
| 5 | +{-# LANGUAGE FlexibleInstances #-} |
| 6 | +{-# LANGUAGE RankNTypes #-} |
4 | 7 | {-# LANGUAGE ScopedTypeVariables #-} |
5 | 8 | {-# LANGUAGE TupleSections #-} |
6 | 9 | {-# LANGUAGE TypeApplications #-} |
| 10 | +{-# LANGUAGE TypeOperators #-} |
7 | 11 | module ObsHelper where |
8 | 12 |
|
9 | | -import qualified Data.ByteString.Base64 as B64 (decodeLenient) |
10 | | -import qualified Data.ByteString.Char8 as BS |
11 | | -import qualified Data.ByteString.Lazy as BL |
12 | | -import qualified Jose.Jwa as JWT |
13 | | -import qualified Jose.Jws as JWT |
14 | | -import qualified Jose.Jwt as JWT |
15 | | - |
16 | | -import PostgREST.Config (AppConfig (..), JSPathExp (..), |
17 | | - LogLevel (..), OpenAPIMode (..), |
18 | | - Verbosity (..), parseSecret) |
19 | | - |
20 | | -import Data.List.NonEmpty (fromList) |
21 | | -import Data.String (String) |
22 | | -import Prometheus (Counter, getCounter) |
23 | | -import Test.Hspec.Expectations.Contrib (annotate) |
24 | | - |
25 | | -import Network.HTTP.Types |
26 | | -import Protolude |
27 | | -import Test.Hspec |
28 | | -import Test.Hspec.Wai |
| 13 | +import qualified Data.ByteString as BS |
| 14 | +import qualified Data.ByteString.Base64 as B64 |
| 15 | +import qualified Data.ByteString.Lazy as BL |
| 16 | +import qualified Data.List as DL |
| 17 | +import Data.List.NonEmpty (fromList) |
| 18 | +import Data.String (String) |
| 19 | +import qualified Data.Text as T |
| 20 | +import qualified Jose.Jwa as JWT |
| 21 | +import qualified Jose.Jws as JWT |
| 22 | +import qualified Jose.Jwt as JWT |
| 23 | +import Network.HTTP.Types |
| 24 | +import qualified PostgREST.AppState as AppState |
| 25 | +import PostgREST.Config (AppConfig (..), |
| 26 | + JSPathExp (..), |
| 27 | + LogLevel (..), |
| 28 | + OpenAPIMode (..), |
| 29 | + Verbosity (..), |
| 30 | + parseSecret) |
| 31 | +import qualified PostgREST.Metrics as Metrics |
| 32 | +import PostgREST.Observation (Observation (..)) |
| 33 | +import Prometheus (Counter, getCounter) |
| 34 | +import Protolude hiding (get, toS) |
| 35 | +import System.Timeout (timeout) |
| 36 | +import Test.Hspec |
| 37 | +import Test.Hspec.Expectations.Contrib (annotate) |
| 38 | +import Test.Hspec.Wai |
29 | 39 |
|
30 | 40 |
|
31 | 41 | baseCfg :: AppConfig |
@@ -133,3 +143,72 @@ expectCounter :: forall s st m. (KnownSymbol s, HasField s st Counter, MonadIO m |
133 | 143 | expectCounter = expectField @s intCounter |
134 | 144 | where |
135 | 145 | intCounter = ((round @Double @Int) <$>) . getCounter |
| 146 | + |
| 147 | +data TimeoutException = TimeoutException deriving (Show, Exception) |
| 148 | + |
| 149 | +accumulateUntilTimeout :: Int -> (s -> a -> s) -> s -> IO a -> IO s |
| 150 | +accumulateUntilTimeout t f start act = do |
| 151 | + tid <- myThreadId |
| 152 | + -- mask to make sure TimeoutException is not thrown before starting the loop |
| 153 | + mask $ \unmask -> do |
| 154 | + -- start timeout thread unmasking exceptions |
| 155 | + ttid <- forkIOWithUnmask ($ (threadDelay t *> throwTo tid TimeoutException)) |
| 156 | + -- unmask effect |
| 157 | + unmask (fix (\loop accum -> (act >>= loop . f accum) `onTimeout` pure accum) start) |
| 158 | + -- make sure we catch timeout if happens before entering the loop |
| 159 | + `onTimeout` pure start |
| 160 | + -- make sure timer thread is killed on other exceptions |
| 161 | + -- so that it won't throw TimeoutException later |
| 162 | + `onException` killThread ttid |
| 163 | + where |
| 164 | + onTimeout m a = m `catch` \TimeoutException -> a |
| 165 | + |
| 166 | +data ObsChan = ObsChan (Chan Observation) (Chan Observation) |
| 167 | + |
| 168 | +newObsChan :: Chan Observation -> IO ObsChan |
| 169 | +newObsChan = fmap <$> ObsChan <*> dupChan |
| 170 | + |
| 171 | +-- read messages from copy chan and once condition is met drain original to the same point |
| 172 | +-- upon timeout report error and messages remaining in the original chan |
| 173 | +-- that way we report messages since last successful read |
| 174 | +waitForObs :: HasCallStack => ObsChan -> Int -> Text -> (Observation -> Maybe a) -> IO () |
| 175 | +waitForObs (ObsChan orig copy) t msg f = |
| 176 | + timeout t (readUntil copy *> readUntil orig) >>= maybe failTimeout mempty |
| 177 | + where |
| 178 | + failTimeout = takeUntilTimeout decisecond (readChan orig) |
| 179 | + >>= expectationFailure . DL.unlines . fmap show . (failureMessageHeader :) . fmap obsDiagMessage |
| 180 | + failureMessageHeader = "Timeout waiting for " <> msg <> " at " <> loc <> ". Remaining observations:" |
| 181 | + readUntil = void . untilM (pure . not . null . f) . readChan |
| 182 | + loc = fromMaybe "(unknown)" . head $ (T.pack . prettySrcLoc . snd <$> getCallStack callStack) |
| 183 | + -- execute effectful computation until result meets provided condition |
| 184 | + untilM cond m = fix $ \loop -> m >>= \a -> ifM (cond a) (pure a) loop |
| 185 | + -- duplicate the provided channel and construct wairFor function binding both channels |
| 186 | + -- accumulate effecful computation results into a list for specified time |
| 187 | + takeUntilTimeout t' = fmap reverse . accumulateUntilTimeout t' (flip (:)) [] |
| 188 | + obsDiagMessage (HasqlPoolObs o) = show o |
| 189 | + obsDiagMessage o@(DBListenStart host port name channel) = constrName o <> show (host, port, name, channel) |
| 190 | + obsDiagMessage o = constrName o |
| 191 | + decisecond = 100000 |
| 192 | + |
| 193 | +data SpecState = SpecState { |
| 194 | + specAppState :: AppState.AppState, |
| 195 | + specMetrics :: Metrics.MetricsState, |
| 196 | + specObsChan :: ObsChan |
| 197 | +} |
| 198 | + |
| 199 | +-- helpers used to produce observation diagnostics in waitForObs |
| 200 | +constrName :: (HasConstructor (Rep a), Generic a)=> a -> Text |
| 201 | +constrName = genericConstrName . from |
| 202 | + |
| 203 | +class HasConstructor f where |
| 204 | + genericConstrName :: f x -> Text |
| 205 | + |
| 206 | +instance HasConstructor f => HasConstructor (D1 c f) where |
| 207 | + genericConstrName (M1 x) = genericConstrName x |
| 208 | + |
| 209 | +instance (HasConstructor x, HasConstructor y) => HasConstructor (x :+: y) where |
| 210 | + genericConstrName (L1 l) = genericConstrName l |
| 211 | + genericConstrName (R1 r) = genericConstrName r |
| 212 | + |
| 213 | +instance Constructor c => HasConstructor (C1 c f) where |
| 214 | + genericConstrName = T.pack . conName |
0 commit comments