packages feed

looper 0.0.0.2 → 0.2.0.1

raw patch · 9 files changed

+286/−249 lines, 9 filesdep +autodocodecdep +autodocodec-yamldep +envparsedep −hspecsetup-changedPVP ok

version bump matches the API change (PVP)

Dependencies added: autodocodec, autodocodec-yaml, envparse, sydtest

Dependencies removed: hspec

API changes (from Hackage documentation)

+ Looper: instance Autodocodec.Class.HasCodec Looper.LooperConfiguration
+ Looper: instance Data.Aeson.Types.ToJSON.ToJSON Looper.LooperConfiguration
+ Looper: looperEnvironmentParser :: String -> Parser Error LooperEnvironment
- Looper: LooperConfiguration :: Maybe Bool -> Maybe Int -> Maybe Int -> LooperConfiguration
+ Looper: LooperConfiguration :: Maybe Bool -> Maybe Word -> Maybe Word -> LooperConfiguration
- Looper: LooperEnvironment :: Maybe Bool -> Maybe Int -> Maybe Int -> LooperEnvironment
+ Looper: LooperEnvironment :: Maybe Bool -> Maybe Word -> Maybe Word -> LooperEnvironment
- Looper: LooperFlags :: Maybe Bool -> Maybe Int -> Maybe Int -> LooperFlags
+ Looper: LooperFlags :: Maybe Bool -> Maybe Word -> Maybe Word -> LooperFlags
- Looper: [looperConfPeriod] :: LooperConfiguration -> Maybe Int
+ Looper: [looperConfPeriod] :: LooperConfiguration -> Maybe Word
- Looper: [looperConfPhase] :: LooperConfiguration -> Maybe Int
+ Looper: [looperConfPhase] :: LooperConfiguration -> Maybe Word
- Looper: [looperEnvPeriod] :: LooperEnvironment -> Maybe Int
+ Looper: [looperEnvPeriod] :: LooperEnvironment -> Maybe Word
- Looper: [looperEnvPhase] :: LooperEnvironment -> Maybe Int
+ Looper: [looperEnvPhase] :: LooperEnvironment -> Maybe Word
- Looper: [looperFlagPeriod] :: LooperFlags -> Maybe Int
+ Looper: [looperFlagPeriod] :: LooperFlags -> Maybe Word
- Looper: [looperFlagPhase] :: LooperFlags -> Maybe Int
+ Looper: [looperFlagPhase] :: LooperFlags -> Maybe Word
- Looper: runLoopersIgnoreOverrun :: MonadUnliftIO m => (LooperDef m -> m ()) -> [LooperDef m] -> m ()
+ Looper: runLoopersIgnoreOverrun :: (MonadUnliftIO m, MonadUnliftIO n) => (LooperDef m -> n ()) -> [LooperDef m] -> n ()
- Looper: runLoopersRaw :: MonadUnliftIO m => m () -> (LooperDef m -> m ()) -> [LooperDef m] -> m ()
+ Looper: runLoopersRaw :: (MonadUnliftIO m, MonadUnliftIO n) => (LooperDef m -> n ()) -> (LooperDef m -> n ()) -> [LooperDef m] -> n ()

Files

− ChangeLog.md
@@ -1,20 +0,0 @@-# Changelog-  -## [Unreleased]--## [0.0.0.2] - 2019-05-15--### Changed--* Exposed 'readLooperEnvironment'-* Doc fix--## [0.0.0.1] - 2019-05-15--### Changed--* Better docs--## [0.0.0.0] - 2019-05-15--Initial version
LICENSE view
@@ -1,4 +1,4 @@-Copyright Tom Sydney Kerckhove (c) 2019+Copyright Tom Sydney Kerckhove (c) 2019-2022  All rights reserved. 
− README.md
@@ -1,1 +0,0 @@-# looper
− Setup.hs
@@ -1,2 +0,0 @@-import Distribution.Simple-main = defaultMain
looper.cabal view
@@ -1,25 +1,22 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.31.1.+-- This file has been generated from package.yaml by hpack version 0.34.7. -- -- see: https://github.com/sol/hpack------ hash: 89647c3d09f7fa4d71e44a70470be2d4892fa2d52a50236d39c23d195fe84eae  name:           looper-version:        0.0.0.2-description:    Please see the README on GitHub at <https://github.com/NorfairKing/looper#readme>+version:        0.2.0.1+description:    Configure and run recurring jobs indefinitely homepage:       https://github.com/NorfairKing/looper#readme bug-reports:    https://github.com/NorfairKing/looper/issues author:         Tom Sydney Kerckhove maintainer:     syd@cs-syd.eu-copyright:      Copyright: (c) 2019 Tom Sydney Kerckhove-license:        BSD3+copyright:      Copyright: (c) 2019-2022 Tom Sydney Kerckhove+license:        MIT license-file:   LICENSE build-type:     Simple extra-source-files:-    README.md-    ChangeLog.md+    test_resources/configuration.txt  source-repository head   type: git@@ -34,7 +31,9 @@       src   build-depends:       aeson+    , autodocodec     , base >=4.7 && <5+    , envparse     , optparse-applicative     , text     , time@@ -50,13 +49,13 @@   hs-source-dirs:       test   ghc-options: -threaded -rtsopts -with-rtsopts=-N+  build-tool-depends:+      sydtest-discover:sydtest-discover   build-depends:-      aeson+      autodocodec-yaml     , base >=4.7 && <5-    , hspec     , looper     , optparse-applicative-    , text-    , time+    , sydtest     , unliftio   default-language: Haskell2010
src/Looper.hs view
@@ -1,55 +1,57 @@-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingVia #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}  module Looper-  ( LooperDef(..)-  , seconds-  , minutes-  , hours-  , LooperFlags(..)-  , getLooperFlags-  , LooperEnvironment(..)-  , getLooperEnvironment-  , readLooperEnvironment-  , LooperConfiguration(..)-  , LooperSettings(..)-  , deriveLooperSettings-  , mkLooperDef-  , runLoopers-  , runLoopersIgnoreOverrun-  , runLoopersRaw-  , waitNominalDiffTime-  ) where--import GHC.Generics (Generic)+  ( LooperDef (..),+    seconds,+    minutes,+    hours,+    LooperFlags (..),+    getLooperFlags,+    LooperEnvironment (..),+    getLooperEnvironment,+    readLooperEnvironment,+    looperEnvironmentParser,+    LooperConfiguration (..),+    LooperSettings (..),+    deriveLooperSettings,+    mkLooperDef,+    runLoopers,+    runLoopersIgnoreOverrun,+    runLoopersRaw,+    waitNominalDiffTime,+  )+where +import Autodocodec import Control.Applicative import Control.Monad--import qualified System.Environment as System (getEnvironment)--import Data.Aeson+import Data.Aeson (FromJSON, ToJSON) import Data.Maybe-import Data.String import Data.Text (Text) import Data.Time--import Options.Applicative-import Text.Read-+import qualified Env+import GHC.Generics (Generic)+import Options.Applicative as OptParse+import qualified System.Environment as System (getEnvironment) import UnliftIO import UnliftIO.Concurrent  -- | A looper definition-data LooperDef m =-  LooperDef-    { looperDefName :: Text -- ^ The name of the looper, can be useful for logging-    , looperDefEnabled :: Bool -- ^ Whether this looper is enabled-    , looperDefPeriod :: NominalDiffTime -- ^ The time between the start of each run-    , looperDefPhase :: NominalDiffTime -- ^ The time before the first run-    , looperDefFunc :: m () -- ^ The function to run-    }+data LooperDef m = LooperDef+  { -- | The name of the looper, can be useful for logging+    looperDefName :: Text,+    -- | Whether this looper is enabled+    looperDefEnabled :: Bool,+    -- | The time between the start of each run+    looperDefPeriod :: NominalDiffTime,+    -- | The time before the first run+    looperDefPhase :: NominalDiffTime,+    -- | The function to run+    looperDefFunc :: m ()+  }   deriving (Generic)  -- | Construct a 'NominalDiffTime' from a number of seconds@@ -65,147 +67,167 @@ hours = minutes . (* 60)  -- | A structure to parse command-line flags for a looper into-data LooperFlags =-  LooperFlags-    { looperFlagEnabled :: Maybe Bool-    , looperFlagPhase :: Maybe Int -- Seconds-    , looperFlagPeriod :: Maybe Int -- Seconds-    }+data LooperFlags = LooperFlags+  { looperFlagEnabled :: Maybe Bool,+    looperFlagPhase :: Maybe Word, -- Seconds+    looperFlagPeriod :: Maybe Word -- Seconds+  }   deriving (Show, Eq, Generic)  -- | An optparse applicative parser for 'LooperFlags' getLooperFlags ::-     String -- ^ The name of the looper (best to make this all-lowercase)-  -> Parser LooperFlags+  -- | The name of the looper (best to make this all-lowercase)+  String ->+  OptParse.Parser LooperFlags getLooperFlags name =-  LooperFlags <$> doubleSwitch name (unwords ["enable the", name, "looper"]) mempty <*>-  option-    (Just <$> auto)-    (mconcat-       [ long $ name <> "-phase"-       , metavar "SECONDS"-       , value Nothing-       , help $ unwords ["the phase for the", name, "looper in seconsd"]-       ]) <*>-  option-    (Just <$> auto)-    (mconcat-       [ long $ name <> "-period"-       , metavar "SECONDS"-       , value Nothing-       , help $ unwords ["the period for the", name, "looper in seconds"]-       ])+  LooperFlags <$> doubleSwitch name (unwords ["enable the", name, "looper"]) mempty+    <*> option+      (Just <$> auto)+      ( mconcat+          [ long $ name <> "-phase",+            metavar "SECONDS",+            value Nothing,+            help $ unwords ["the phase for the", name, "looper in seconsd"]+          ]+      )+    <*> option+      (Just <$> auto)+      ( mconcat+          [ long $ name <> "-period",+            metavar "SECONDS",+            value Nothing,+            help $ unwords ["the period for the", name, "looper in seconds"]+          ]+      ) -doubleSwitch :: String -> String -> Mod FlagFields Bool -> Parser (Maybe Bool)+doubleSwitch :: String -> String -> Mod FlagFields Bool -> OptParse.Parser (Maybe Bool) doubleSwitch name helpText mods =   let enabledValue = True       disabledValue = False       defaultValue = True-   in ((last . map Just) <$>-       some-         ((flag'-             enabledValue-             (hidden <> internal <> long ("enable-" ++ name) <> help helpText <> mods) <|>-           flag'-             disabledValue-             (hidden <> internal <> long ("disable-" ++ name) <> help helpText <> mods)) <|>-          flag'-            disabledValue-            (long ("(enable|disable)-" ++ name) <>-             help ("Enable/disable " ++ helpText ++ " (default: " ++ show defaultValue ++ ")") <>-             mods))) <|>-      pure Nothing+   in ( last . map Just+          <$> some+            ( ( flag'+                  enabledValue+                  (hidden <> internal <> long ("enable-" ++ name) <> help helpText <> mods)+                  <|> flag'+                    disabledValue+                    (hidden <> internal <> long ("disable-" ++ name) <> help helpText <> mods)+              )+                <|> flag'+                  disabledValue+                  ( long ("(enable|disable)-" ++ name)+                      <> help ("Enable/disable " ++ helpText ++ " (default: " ++ show defaultValue ++ ")")+                      <> mods+                  )+            )+      )+        <|> pure Nothing  -- | A structure to parse environment variables for a looper into-data LooperEnvironment =-  LooperEnvironment-    { looperEnvEnabled :: Maybe Bool-    , looperEnvPhase :: Maybe Int -- Seconds-    , looperEnvPeriod :: Maybe Int -- Seconds-    }+data LooperEnvironment = LooperEnvironment+  { looperEnvEnabled :: Maybe Bool,+    looperEnvPhase :: Maybe Word, -- Seconds+    looperEnvPeriod :: Maybe Word -- Seconds+  }   deriving (Show, Eq, Generic)  -- | Get a 'LooperEnvironment' from the environment getLooperEnvironment ::-     String -- ^ Prefix for each variable (best to make this all-caps)-  -> String -- ^ Name of the looper (best to make this all-caps too)-  -> IO LooperEnvironment+  -- | Prefix for each variable (best to make this all-caps)+  String ->+  -- | Name of the looper (best to make this all-caps too)+  String ->+  IO LooperEnvironment getLooperEnvironment prefix name = readLooperEnvironment prefix name <$> System.getEnvironment  -- | Get a 'LooperEnvironment' from a pure environment readLooperEnvironment ::-     String -- ^ Prefix for each variable (best to make this all-caps)-  -> String -- ^ Name of the looper (best to make this all-caps too)-  -> [(String, String)]-  -> LooperEnvironment-readLooperEnvironment prefix name env =-  let v :: IsString s => String -> Maybe s-      v k = fromString <$> lookup (prefix <> k) env-      r :: Read a => String -> Maybe a-      r k = v k >>= readMaybe-      lr :: Read a => String -> Maybe a-      lr k = r $ name <> "_" <> k-   in LooperEnvironment-        { looperEnvEnabled = lr "ENABLED"-        , looperEnvPhase = lr "PHASE"-        , looperEnvPeriod = lr "PERIOD"-        }+  -- | Prefix for each variable (best to make this all-caps)+  String ->+  -- | Name of the looper (best to make this all-caps too)+  String ->+  [(String, String)] ->+  LooperEnvironment+readLooperEnvironment prefix name env = case Env.parsePure (Env.prefixed prefix $ looperEnvironmentParser name) env of+  Left _ -> error "This indicates a bug in looper because all environment variables are optional."+  Right r -> r +-- | An 'envparse' parser for a 'LooperEnvironment'+looperEnvironmentParser ::+  -- | Name of the looper (best to make this all-caps)+  String ->+  Env.Parser Env.Error LooperEnvironment+looperEnvironmentParser name =+  Env.prefixed (name <> "_") $+    LooperEnvironment+      <$> Env.var (fmap Just . Env.auto) "ENABLED" (Env.def Nothing <> Env.help "Whether to enable this looper")+      <*> Env.var (fmap Just . Env.auto) "PHASE" (Env.def Nothing <> Env.help "The amount of time to wait before starting the looper the first time, in seconds")+      <*> Env.var (fmap Just . Env.auto) "PERIOD" (Env.def Nothing <> Env.help "The amount of time to wait between runs of the looper, in seconds")+ -- | A structure to configuration for a looper into-data LooperConfiguration =-  LooperConfiguration-    { looperConfEnabled :: Maybe Bool-    , looperConfPhase :: Maybe Int-    , looperConfPeriod :: Maybe Int-    }-  deriving (Show, Eq, Generic)+data LooperConfiguration = LooperConfiguration+  { looperConfEnabled :: Maybe Bool,+    looperConfPhase :: Maybe Word,+    looperConfPeriod :: Maybe Word+  }+  deriving stock (Show, Eq, Generic)+  deriving (FromJSON, ToJSON) via (Autodocodec LooperConfiguration) --- | You can parse Data.Aeson's JSON or Data.Yaml's YAML to parse a 'LooperConfiguration'.--- You can also use Data.Yaml.Config.-instance FromJSON LooperConfiguration where-  parseJSON =-    withObject "LooperConfiguration" $ \o ->-      LooperConfiguration <$> o .:? "enabled" <*> o .:? "phase" <*> o .: "period"+instance HasCodec LooperConfiguration where+  codec =+    named "LooperConfiguration" $+      object "LooperConfiguration" $+        LooperConfiguration+          <$> parseAlternative+            (optionalFieldOrNull "enable" "Enable this looper")+            (optionalFieldOrNull "enabled" "Enable this looper")+            .= looperConfEnabled+          <*> optionalFieldOrNull "phase" "The amount of time to wait before starting the looper the first time, in seconds" .= looperConfPhase+          <*> optionalFieldOrNull "period" "The amount of time to wait between runs of the looper, in seconds" .= looperConfPeriod  -- | Settings that you might want to pass into a looper using 'mkLooperDef'-data LooperSettings =-  LooperSettings-    { looperSetEnabled :: Bool-    , looperSetPhase :: NominalDiffTime-    , looperSetPeriod :: NominalDiffTime-    }+data LooperSettings = LooperSettings+  { looperSetEnabled :: Bool,+    looperSetPhase :: NominalDiffTime,+    looperSetPeriod :: NominalDiffTime+  }   deriving (Show, Eq, Generic)  deriveLooperSettings ::-     NominalDiffTime -- ^ Default phase-  -> NominalDiffTime -- ^ Default period-  -> LooperFlags-  -> LooperEnvironment-  -> Maybe LooperConfiguration-  -> LooperSettings+  -- | Default phase+  NominalDiffTime ->+  -- | Default period+  NominalDiffTime ->+  LooperFlags ->+  LooperEnvironment ->+  Maybe LooperConfiguration ->+  LooperSettings deriveLooperSettings defaultPhase defaultPeriod LooperFlags {..} LooperEnvironment {..} mlc =   let looperSetEnabled =         fromMaybe True $ looperFlagEnabled <|> looperEnvEnabled <|> (mlc >>= looperConfEnabled)       looperSetPhase =         maybe defaultPhase fromIntegral $-        looperFlagPhase <|> looperEnvPhase <|> (mlc >>= looperConfPhase)+          looperFlagPhase <|> looperEnvPhase <|> (mlc >>= looperConfPhase)       looperSetPeriod =         maybe defaultPeriod fromIntegral $-        looperFlagPeriod <|> looperEnvPeriod <|> (mlc >>= looperConfPeriod)+          looperFlagPeriod <|> looperEnvPeriod <|> (mlc >>= looperConfPeriod)    in LooperSettings {..}  mkLooperDef ::-     Text -- ^ Name-  -> LooperSettings-  -> m () -- ^ The function to loop-  -> LooperDef m+  -- | Name+  Text ->+  LooperSettings ->+  -- | The function to loop+  m () ->+  LooperDef m mkLooperDef name LooperSettings {..} func =   LooperDef-    { looperDefName = name-    , looperDefEnabled = looperSetEnabled-    , looperDefPeriod = looperSetPeriod-    , looperDefPhase = looperSetPhase-    , looperDefFunc = func+    { looperDefName = name,+      looperDefEnabled = looperSetEnabled,+      looperDefPeriod = looperSetPeriod,+      looperDefPhase = looperSetPhase,+      looperDefFunc = func     }  -- | Simply run loopers@@ -226,11 +248,13 @@ -- -- Note that this function will loop forever, you need to wrap it using 'async' yourself. runLoopersIgnoreOverrun ::-     MonadUnliftIO m-  => (LooperDef m -> m ()) -- ^ Custom runner-  -> [LooperDef m] -- ^ Loopers-  -> m ()-runLoopersIgnoreOverrun = runLoopersRaw (pure ())+  (MonadUnliftIO m, MonadUnliftIO n) =>+  -- | Custom runner+  (LooperDef m -> n ()) ->+  -- | Loopers+  [LooperDef m] ->+  n ()+runLoopersIgnoreOverrun = runLoopersRaw (const $ pure ())  -- | Run loopers, with a custom runner and overrun handler --@@ -244,23 +268,26 @@ -- -- Note that this function will loop forever, you need to wrap it using 'async' yourself. runLoopersRaw ::-     MonadUnliftIO m-  => m () -- ^ Overrun handler-  -> (LooperDef m -> m ()) -- ^ Runner-  -> [LooperDef m] -- ^ Loopers-  -> m ()+  (MonadUnliftIO m, MonadUnliftIO n) =>+  -- | Overrun handler+  (LooperDef m -> n ()) ->+  -- | Runner+  (LooperDef m -> n ()) ->+  -- | Loopers+  [LooperDef m] ->+  n () runLoopersRaw onOverrun runLooper =   mapConcurrently_ $ \ld@LooperDef {..} ->     when looperDefEnabled $ do       waitNominalDiffTime looperDefPhase       let loop = do-            start <- liftIO $ getCurrentTime+            start <- liftIO getCurrentTime             runLooper ld-            end <- liftIO $ getCurrentTime+            end <- liftIO getCurrentTime             let elapsed = diffUTCTime end start             let nextWait = looperDefPeriod - elapsed-            if (nextWait < 0)-              then onOverrun+            if nextWait < 0+              then onOverrun ld               else waitNominalDiffTime nextWait             loop       loop
test/LooperSpec.hs view
@@ -1,16 +1,17 @@ {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}  module LooperSpec-  ( spec-  ) where--import Test.Hspec+  ( spec,+  )+where +import Autodocodec.Yaml+import Looper import Options.Applicative as AP+import Test.Syd import UnliftIO -import Looper- spec :: Spec spec = do   describe "getLooperFlags" $ do@@ -18,33 +19,50 @@       parserSucceedsWith         (getLooperFlags "test")         []-        (LooperFlags-           {looperFlagEnabled = Nothing, looperFlagPhase = Nothing, looperFlagPeriod = Nothing})+        ( LooperFlags+            { looperFlagEnabled = Nothing,+              looperFlagPhase = Nothing,+              looperFlagPeriod = Nothing+            }+        )     it "parses an enable flag correctly" $ do       parserSucceedsWith         (getLooperFlags "test")         ["--enable-test"]-        (LooperFlags-           {looperFlagEnabled = Just True, looperFlagPhase = Nothing, looperFlagPeriod = Nothing})+        ( LooperFlags+            { looperFlagEnabled = Just True,+              looperFlagPhase = Nothing,+              looperFlagPeriod = Nothing+            }+        )     it "parses an disable flag correctly" $ do       parserSucceedsWith         (getLooperFlags "test")         ["--disable-test"]-        (LooperFlags-           {looperFlagEnabled = Just False, looperFlagPhase = Nothing, looperFlagPeriod = Nothing})+        ( LooperFlags+            { looperFlagEnabled = Just False,+              looperFlagPhase = Nothing,+              looperFlagPeriod = Nothing+            }+        )++  describe "Configuration" $ do+    it "has the same schema as before" $+      pureGoldenByteStringFile "test_resources/configuration.txt" (renderColouredSchemaViaCodec @LooperConfiguration)+   describe "runLoopers" $ do     it "runs one looper as intended" $ do       v <- newTVarIO (0 :: Int)       let l =             LooperDef-              { looperDefName = "l1"-              , looperDefEnabled = True-              , looperDefPeriod = seconds 0.01-              , looperDefPhase = seconds 0-              , looperDefFunc = atomically $ modifyTVar' v succ+              { looperDefName = "l1",+                looperDefEnabled = True,+                looperDefPeriod = seconds 1,+                looperDefPhase = seconds 0,+                looperDefFunc = atomically $ modifyTVar' v succ               }       a <- async $ runLoopers [l]-      waitNominalDiffTime $ seconds 0.015+      waitNominalDiffTime $ seconds 1.5       cancel a       r <- readTVarIO v       r `shouldBe` 2@@ -52,14 +70,14 @@       v <- newTVarIO (0 :: Int)       let l =             LooperDef-              { looperDefName = "l"-              , looperDefEnabled = True-              , looperDefPeriod = seconds 0.01-              , looperDefPhase = seconds 0.02-              , looperDefFunc = atomically $ modifyTVar' v succ+              { looperDefName = "l",+                looperDefEnabled = True,+                looperDefPeriod = seconds 1,+                looperDefPhase = seconds 2,+                looperDefFunc = atomically $ modifyTVar' v succ               }       a <- async $ runLoopers [l]-      waitNominalDiffTime $ seconds 0.015+      waitNominalDiffTime $ seconds 1.5       cancel a       r <- readTVarIO v       r `shouldBe` 0@@ -69,28 +87,28 @@       v3 <- newTVarIO (0 :: Int)       let l1 =             LooperDef-              { looperDefName = "l1"-              , looperDefEnabled = True-              , looperDefPeriod = seconds 0.01-              , looperDefPhase = seconds 0-              , looperDefFunc =+              { looperDefName = "l1",+                looperDefEnabled = True,+                looperDefPeriod = seconds 1,+                looperDefPhase = seconds 0,+                looperDefFunc =                   atomically $ do                     modifyTVar' v1 succ                     modifyTVar' v2 succ               }       let l2 =             LooperDef-              { looperDefName = "l2"-              , looperDefEnabled = True-              , looperDefPeriod = seconds 0.005-              , looperDefPhase = seconds 0.005-              , looperDefFunc =+              { looperDefName = "l2",+                looperDefEnabled = True,+                looperDefPeriod = seconds 0.5,+                looperDefPhase = seconds 0.5,+                looperDefFunc =                   atomically $ do                     modifyTVar' v2 succ                     modifyTVar' v3 succ               }       a <- async $ runLoopers [l1, l2]-      waitNominalDiffTime $ seconds 0.0225+      waitNominalDiffTime $ seconds 2.25       cancel a       r1 <- readTVarIO v1       r2 <- readTVarIO v2@@ -104,14 +122,14 @@           inc2 = atomically $ modifyTVar' v2 succ       let l =             LooperDef-              { looperDefName = "l"-              , looperDefEnabled = True-              , looperDefPeriod = seconds 0.01-              , looperDefPhase = seconds 0-              , looperDefFunc = inc1+              { looperDefName = "l",+                looperDefEnabled = True,+                looperDefPeriod = seconds 1,+                looperDefPhase = seconds 0,+                looperDefFunc = inc1               }-      a <- async $ runLoopersRaw (pure ()) (\ld -> looperDefFunc ld >> inc2) [l]-      waitNominalDiffTime $ seconds 0.015+      a <- async $ runLoopersRaw (const $ pure ()) (\ld -> looperDefFunc ld >> inc2) [l]+      waitNominalDiffTime $ seconds 1.5       cancel a       r1 <- readTVarIO v1       r2 <- readTVarIO v2@@ -123,14 +141,14 @@           inc2 = atomically $ modifyTVar' v2 succ       let l =             LooperDef-              { looperDefName = "l"-              , looperDefEnabled = True-              , looperDefPeriod = seconds 0.01-              , looperDefPhase = seconds 0-              , looperDefFunc = inc1 >> waitNominalDiffTime (seconds 0.015)+              { looperDefName = "l",+                looperDefEnabled = True,+                looperDefPeriod = seconds 1,+                looperDefPhase = seconds 0,+                looperDefFunc = inc1 >> waitNominalDiffTime (seconds 1.5)               }-      a <- async $ runLoopersRaw inc2 looperDefFunc [l]-      waitNominalDiffTime $ seconds 0.035+      a <- async $ runLoopersRaw (const inc2) looperDefFunc [l]+      waitNominalDiffTime $ seconds 3.5       cancel a       r1 <- readTVarIO v1       r2 <- readTVarIO v2@@ -143,16 +161,12 @@     AP.Failure fp ->       let (err, ec) = renderFailure fp "test"        in expectationFailure $-          unlines ["Failed to parse:", err, "would have resulted in exit code", show ec]+            unlines ["Failed to parse:", err, "would have resulted in exit code", show ec]     AP.CompletionInvoked _ -> expectationFailure "Tried to invoke a completion, should not happen"   where     parserPrefs :: ParserPrefs     parserPrefs =-      ParserPrefs-        { prefMultiSuffix = ""-        , prefDisambiguate = True-        , prefShowHelpOnError = True-        , prefShowHelpOnEmpty = True-        , prefBacktrack = True-        , prefColumns = 80+      defaultPrefs+        { prefShowHelpOnError = True,+          prefShowHelpOnEmpty = True         }
test/Spec.hs view
@@ -1,1 +1,1 @@-{-# OPTIONS_GHC -F -pgmF hspec-discover #-}+{-# OPTIONS_GHC -F -pgmF sydtest-discover #-}
+ test_resources/configuration.txt view
@@ -0,0 +1,20 @@+[36mdef: LooperConfiguration[m+# LooperConfiguration+# [32many of[m+[ [37menable[m: # [34moptional[m+    # Enable this looper+    # [32mor null[m+    [33m<boolean>[m+, [37menabled[m: # [34moptional[m+    # Enable this looper+    # [32mor null[m+    [33m<boolean>[m+]+[37mphase[m: # [34moptional[m+  # The amount of time to wait before starting the looper the first time, in seconds+  # [32mor null[m+  [33m<number>[m # between [32m0[m and [32m18446744073709551615[m+[37mperiod[m: # [34moptional[m+  # The amount of time to wait between runs of the looper, in seconds+  # [32mor null[m+  [33m<number>[m # between [32m0[m and [32m18446744073709551615[m