From 1494d7afab09405aa6eb82ac9f82b973937f52e0 Mon Sep 17 00:00:00 2001 From: Siddharth Krishna Date: Mon, 20 Feb 2023 16:17:09 +0200 Subject: [PATCH 1/5] Concurrent tests for inferno-vc --- inferno-core/inferno-core.cabal | 45 +++++++++++++++++++++ inferno-core/test/VC/Spec.hs | 69 +++++++++++++++++++++++++++++++++ inferno-vc/inferno-vc.cabal | 4 +- 3 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 inferno-core/test/VC/Spec.hs diff --git a/inferno-core/inferno-core.cabal b/inferno-core/inferno-core.cabal index 37182e5..b40d680 100644 --- a/inferno-core/inferno-core.cabal +++ b/inferno-core/inferno-core.cabal @@ -125,6 +125,51 @@ test-suite inferno-tests , RecordWildCards ghc-options: -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates +test-suite inferno-vc-tests + type: exitcode-stdio-1.0 + hs-source-dirs: test/VC + main-is: Spec.hs + -- other-modules: + build-depends: + base >=4.7 && <5 + , aeson + -- , containers + -- , exceptions + , ghc + , hspec + -- , hspec-golden-aeson + -- , hspec-golden-cereal + , http-client + , http-types + , inferno-core + , inferno-types + , inferno-vc + -- , megaparsec + , mtl + -- , prettyprinter + -- , pretty-simple + , plow-log + , QuickCheck + , quickcheck-instances + , servant-quickcheck + , servant-server + -- , recursion-schemes + -- , text + , wai-logger + , warp + default-language: Haskell2010 + default-extensions: + DeriveDataTypeable + , DeriveFunctor + , DeriveGeneric + , FlexibleContexts + , FlexibleInstances + , LambdaCase + , OverloadedStrings + , TupleSections + , RecordWildCards + ghc-options: -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates + executable inferno main-is: Main.hs hs-source-dirs: diff --git a/inferno-core/test/VC/Spec.hs b/inferno-core/test/VC/Spec.hs new file mode 100644 index 0000000..2ffbfd6 --- /dev/null +++ b/inferno-core/test/VC/Spec.hs @@ -0,0 +1,69 @@ +{-# OPTIONS_GHC -fno-warn-orphans #-} + +module Main (main) where + +import qualified Data.Aeson as Aeson +import Control.Exception (throw) +import Control.Monad.Reader (runReaderT, when) +import Data.Functor.Contravariant (contramap) +import Inferno.Instances.Arbitrary () +import Inferno.VersionControl.Client (api) +import Inferno.VersionControl.Log (vcServerTraceToString) +import qualified Inferno.VersionControl.Operations as Ops +import Inferno.VersionControl.Server (VersionControlAPI, vcServer) +import Network.HTTP.Client (responseStatus, Response (..), httpLbs) +import Network.HTTP.Types (status500) +import Network.Wai.Logger (withStdoutLogger) +import Network.Wai.Handler.Warp (defaultSettings, setLogger) +import Plow.Logging (IOTracer (..), simpleStdOutTracer) +import Servant.Server (Server) +import Servant.QuickCheck +import Servant.QuickCheck.Internal.ErrorTypes (PredicateFailure (..)) +import Test.Hspec +import Inferno.VersionControl.Operations.Error (VCStoreError(..)) +import GHC.Utils.TmpFs (withSystemTempDirectory) + +noInternalError :: RequestPredicate +noInternalError = RequestPredicate $ \req mgr -> do + resp <- httpLbs req mgr + putStrLn $ show req ++ show resp + when (responseStatus resp == status500) $ do + case (Aeson.decode $ responseBody resp :: Maybe VCStoreError) of + -- HACK: these are the non-errors, which should be 404s or other codes: + Just (CouldNotFindObject _) -> pure () + Just (TryingToAppendToNonHead _) -> pure () + Just (TryingToDeleteNonAutosave _) -> pure () + _ -> do + putStrLn "BOO" + throw $ PredicateFailure "noInternalError" (Just req) resp + return [resp] + +spec :: Spec +spec = describe "inferno-vc server" $ do + -- let maxTries = 20 + -- let args = defaultArgs { maxSuccess = maxTries } + + it "no internal errors" $ withStdoutLogger $ \appLogger -> do + withSystemTempDirectory "vc_store_" $ \vcPath -> do + -- let settings = setLogger appLogger defaultSettings + let settings = defaultSettings + withServantServerAndSettings testApi settings (pserver vcPath) $ \url -> + serverSatisfies testApi url defaultArgs (noInternalError <%> mempty) + -- putStrLn "Done" + + where + tracer = contramap vcServerTraceToString $ IOTracer $ simpleStdOutTracer + + testApi :: Proxy (VersionControlAPI Int Int) + testApi = api + + pserver :: FilePath -> IO (Server (VersionControlAPI Int Int)) + pserver vcPath = do + putStrLn $ "Store is at: " ++ (show vcPath) + runReaderT Ops.initVCStore $ Ops.VCStorePath vcPath + print ("running..." :: String) + return $ vcServer vcPath tracer + +main :: IO () +main = + hspec spec diff --git a/inferno-vc/inferno-vc.cabal b/inferno-vc/inferno-vc.cabal index e521dd9..3c2462f 100644 --- a/inferno-vc/inferno-vc.cabal +++ b/inferno-vc/inferno-vc.cabal @@ -43,7 +43,9 @@ library , directory >= 1.3.6 && < 1.4 , filepath >= 1.4.2 && < 1.5 , generic-lens >= 2.2.1 && < 2.3 - , http-client >= 0.7.13 && < 0.8 + -- TODO version bounds + , ghc + , http-client >= 0.6.4 && < 0.7 , http-types >= 0.12.3 && < 0.13 , inferno-types >= 0.1.0 && < 0.2 , lens >= 5.2 && < 5.3 From c3bd5d0a3bea2985ecd91693f1920e412393b4ea Mon Sep 17 00:00:00 2001 From: Siddharth Krishna Date: Mon, 20 Feb 2023 16:18:26 +0200 Subject: [PATCH 2/5] Operations: throw better errors --- .../src/Inferno/VersionControl/Operations.hs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/inferno-vc/src/Inferno/VersionControl/Operations.hs b/inferno-vc/src/Inferno/VersionControl/Operations.hs index be249cc..ac3190b 100644 --- a/inferno-vc/src/Inferno/VersionControl/Operations.hs +++ b/inferno-vc/src/Inferno/VersionControl/Operations.hs @@ -38,6 +38,7 @@ import Data.Text (pack) import Data.Time.Clock.POSIX (getPOSIXTime) import Foreign.C.Types (CTime (..)) import GHC.Generics (Generic) +import GHC.Utils.Monad (unlessM) import Inferno.Types.Syntax (Dependencies (..)) import Inferno.VersionControl.Log (VCServerTrace (..)) import Inferno.VersionControl.Operations.Error (VCStoreError (..)) @@ -85,8 +86,8 @@ initVCCachedClient = (getTyped <$> ask) >>= \(VCStorePath storePath) -> liftIO $ do createDirectoryIfMissing True $ storePath "deps" -checkPathExists :: (VCStoreLogM env m, VCStoreErrM err m) => FilePath -> m () -checkPathExists fp = +assertPathExists :: (VCStoreLogM env m, VCStoreErrM err m) => FilePath -> m () +assertPathExists fp = liftIO (doesFileExist fp) >>= \case False -> throwError $ CouldNotFindPath fp True -> pure () @@ -94,7 +95,7 @@ checkPathExists fp = getDepsFromStore :: (VCStoreLogM env m, VCStoreErrM err m) => FilePath -> VCObjectHash -> m BL.ByteString getDepsFromStore path h = do let fp = path show h - checkPathExists fp + unlessM (liftIO $ doesFileExist fp) $ throwError $ CouldNotFindObject h liftIO $ BL.readFile $ path show h appendBS :: (VCStoreLogM env m) => FilePath -> BL.ByteString -> m () @@ -120,7 +121,7 @@ writeHashedJSON path o = do readVCObjectHashTxt :: (VCStoreLogM env m, VCStoreErrM err m) => FilePath -> m [VCObjectHash] readVCObjectHashTxt fp = do - checkPathExists fp + assertPathExists fp trace $ ReadTxt fp deps <- filter (not . B.null) . Char8.lines <$> (liftIO $ B.readFile fp) forM deps $ \dep -> do @@ -262,7 +263,7 @@ fetchVCObject' mprefix h = do let fp = case mprefix of Nothing -> storePath show h Just prefix -> storePath prefix show h - checkPathExists fp + unlessM (liftIO $ doesFileExist fp) $ throwError $ CouldNotFindObject h trace $ ReadJSON fp either (throwError . CouldNotDecodeObject h) pure =<< (liftIO $ eitherDecode <$> BL.readFile fp) @@ -274,6 +275,7 @@ fetchVCObjectClosureHashes :: (VCStoreLogM env m, VCStoreErrM err m, VCStoreEnvM fetchVCObjectClosureHashes h = do VCStorePath storePath <- getTyped <$> ask let fp = storePath "deps" show h + unlessM (liftIO $ doesFileExist fp) $ throwError $ CouldNotFindObject h readVCObjectHashTxt fp fetchVCObjectWithClosure :: (VCStoreLogM env m, VCStoreErrM err m, VCStoreEnvM env m, FromJSON a, FromJSON g) => VCObjectHash -> m (Map.Map VCObjectHash (VCMeta a g VCObject)) @@ -296,7 +298,7 @@ fetchCurrentHead h = do readVCObjectHashTxt fp >>= \case [h'] -> pure h' _ -> throwError $ CouldNotFindHead h - else throwError $ CouldNotFindHead h + else throwError $ CouldNotFindObject h fetchVCObjectHistory :: (VCStoreLogM env m, VCStoreErrM err m, VCStoreEnvM env m, FromJSON a, FromJSON g) => VCObjectHash -> m [VCMeta a g VCObjectHash] fetchVCObjectHistory h = do From b73078057c0bac770c1dd177f85cf975ce263868 Mon Sep 17 00:00:00 2001 From: Siddharth Krishna Date: Mon, 20 Feb 2023 18:36:50 +0000 Subject: [PATCH 3/5] Try to repro test failure --- cabal.project | 6 ++++++ inferno-core/test/VC/Spec.hs | 15 +++++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/cabal.project b/cabal.project index 2bbf6ee..8c3d3a3 100644 --- a/cabal.project +++ b/cabal.project @@ -15,3 +15,9 @@ source-repository-package location: https://github.com/plow-technologies/hspec-golden-cereal.git tag: f9e3e485409c5a1de1de99a8ec0f35226c79da79 --sha256: j+SZk5AZsNP674fb+1aiA7vrsk6Eq5BQM2losQSnaeE= + +source-repository-package + type: git + location: https://github.com/siddharth-krishna/servant-quickcheck.git + tag: 8da2061928d194637c6c99486366bd2f9f2a16f0 + --sha256: 07v8hizx0hq3bs609al5vwa4jai16fpl75pm1nz6i2h7v4m3f162 diff --git a/inferno-core/test/VC/Spec.hs b/inferno-core/test/VC/Spec.hs index 2ffbfd6..3a6db7e 100644 --- a/inferno-core/test/VC/Spec.hs +++ b/inferno-core/test/VC/Spec.hs @@ -40,16 +40,15 @@ noInternalError = RequestPredicate $ \req mgr -> do spec :: Spec spec = describe "inferno-vc server" $ do - -- let maxTries = 20 - -- let args = defaultArgs { maxSuccess = maxTries } + let maxTries = 35 + let args = defaultArgs { maxSuccess = maxTries } + -- let args = defaultArgs - it "no internal errors" $ withStdoutLogger $ \appLogger -> do + it "no internal errors" $ do withSystemTempDirectory "vc_store_" $ \vcPath -> do - -- let settings = setLogger appLogger defaultSettings - let settings = defaultSettings - withServantServerAndSettings testApi settings (pserver vcPath) $ \url -> - serverSatisfies testApi url defaultArgs (noInternalError <%> mempty) - -- putStrLn "Done" + withServantServer testApi (pserver vcPath) $ \url -> + serverSatisfies testApi url args (noInternalError <%> mempty) + putStrLn "Done" where tracer = contramap vcServerTraceToString $ IOTracer $ simpleStdOutTracer From 077eba9bab4405a6ccebc0f781e11a2a1d27ddef Mon Sep 17 00:00:00 2001 From: Siddharth Krishna Date: Mon, 27 Feb 2023 06:11:33 +0000 Subject: [PATCH 4/5] TEMP: try to generate realistic conc tests --- .gitignore | 1 + cabal.project | 11 ++-- inferno-core/inferno-core.cabal | 6 ++ inferno-core/test/VC/Spec.hs | 108 ++++++++++++++++++++++++++++---- 4 files changed, 110 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index 2dd7d5f..47c6825 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ cabal.project.local~ result* .vscode/ .venv/ +vc_store/ diff --git a/cabal.project b/cabal.project index 8c3d3a3..07654b9 100644 --- a/cabal.project +++ b/cabal.project @@ -3,6 +3,7 @@ packages: ./inferno-types ./inferno-lsp ./inferno-vc + ../servant-quickcheck source-repository-package type: git @@ -16,8 +17,8 @@ source-repository-package tag: f9e3e485409c5a1de1de99a8ec0f35226c79da79 --sha256: j+SZk5AZsNP674fb+1aiA7vrsk6Eq5BQM2losQSnaeE= -source-repository-package - type: git - location: https://github.com/siddharth-krishna/servant-quickcheck.git - tag: 8da2061928d194637c6c99486366bd2f9f2a16f0 - --sha256: 07v8hizx0hq3bs609al5vwa4jai16fpl75pm1nz6i2h7v4m3f162 +-- source-repository-package +-- type: git +-- location: https://github.com/siddharth-krishna/servant-quickcheck.git +-- tag: 8da2061928d194637c6c99486366bd2f9f2a16f0 +-- --sha256: PaaApUqHkBFVN7MWDm2oYBSBeRd/c6XmrF8Lat3Myks= diff --git a/inferno-core/inferno-core.cabal b/inferno-core/inferno-core.cabal index b40d680..37b0f68 100644 --- a/inferno-core/inferno-core.cabal +++ b/inferno-core/inferno-core.cabal @@ -133,7 +133,9 @@ test-suite inferno-vc-tests build-depends: base >=4.7 && <5 , aeson + , async -- , containers + , directory -- , exceptions , ghc , hspec @@ -141,6 +143,7 @@ test-suite inferno-vc-tests -- , hspec-golden-cereal , http-client , http-types + , http-media , inferno-core , inferno-types , inferno-vc @@ -151,8 +154,10 @@ test-suite inferno-vc-tests , plow-log , QuickCheck , quickcheck-instances + , servant , servant-quickcheck , servant-server + , string-conversions -- , recursion-schemes -- , text , wai-logger @@ -169,6 +174,7 @@ test-suite inferno-vc-tests , TupleSections , RecordWildCards ghc-options: -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates + -- -threaded -fprof-auto "-with-rtsopts=-N -p" executable inferno main-is: Main.hs diff --git a/inferno-core/test/VC/Spec.hs b/inferno-core/test/VC/Spec.hs index 3a6db7e..7d6abe5 100644 --- a/inferno-core/test/VC/Spec.hs +++ b/inferno-core/test/VC/Spec.hs @@ -1,4 +1,4 @@ -{-# OPTIONS_GHC -fno-warn-orphans #-} +{-# LANGUAGE DataKinds #-} module Main (main) where @@ -11,10 +11,9 @@ import Inferno.VersionControl.Client (api) import Inferno.VersionControl.Log (vcServerTraceToString) import qualified Inferno.VersionControl.Operations as Ops import Inferno.VersionControl.Server (VersionControlAPI, vcServer) -import Network.HTTP.Client (responseStatus, Response (..), httpLbs) +import Network.HTTP.Client (responseStatus, Response (..), httpLbs, Request, RequestBody (..), host, method, path, port, queryString, requestBody, requestHeaders, secure, defaultRequest) +import Data.String.Conversions (cs) import Network.HTTP.Types (status500) -import Network.Wai.Logger (withStdoutLogger) -import Network.Wai.Handler.Warp (defaultSettings, setLogger) import Plow.Logging (IOTracer (..), simpleStdOutTracer) import Servant.Server (Server) import Servant.QuickCheck @@ -22,13 +21,61 @@ import Servant.QuickCheck.Internal.ErrorTypes (PredicateFailure (..)) import Test.Hspec import Inferno.VersionControl.Operations.Error (VCStoreError(..)) import GHC.Utils.TmpFs (withSystemTempDirectory) +import System.Directory (removePathForcibly) +import Test.QuickCheck (Result(..), quickCheckWithResult, Gen, Arbitrary (..), elements) +import Control.Concurrent (newEmptyMVar, tryPutMVar, tryReadMVar) +import Servant.QuickCheck.Internal.HasGenRequest (runGenRequest, HasGenRequest (..)) +import Test.QuickCheck.Monadic (monadicIO, forAllM, run, assert) +import Servant.QuickCheck.Internal.Predicates (finishPredicates) +import Servant.QuickCheck.Internal.QuickCheck (noCheckStatus, defManager) +import Control.Concurrent.Async (concurrently) +import Data.Proxy (Proxy (..)) +import Inferno.Types.Type (TCScheme) +import Inferno.VersionControl.Types (VCObjectHash, Pinned, VCMeta) +import Inferno.Types.Syntax (Expr) +import Servant.API.ContentTypes (JSON (..), AllMimeRender (allMimeRender)) +import Network.HTTP.Media (renderHeader) + +newtype VCApi = VCApi (VersionControlAPI Int Int) + +-- Test set: store successive versions of the same script, run fetchHistory for arbitrary versions +-- Try to trigger CouldNotFindPath when fetchHistory reads wrong head +instance HasGenRequest (VCApi) where + genRequest _ = (1, genStore) + where + genStore :: Gen (BaseUrl -> Request) + genStore = do + new' <- new + (ct, bd) <- elements $ allMimeRender (Proxy :: Proxy '[JSON]) new' + return $ \burl -> defaultRequest + { host = cs $ baseUrlHost burl + , port = baseUrlPort burl + , secure = baseUrlScheme burl == Https + , method = "POST" + , path = "/push/function" + , requestBody = RequestBodyLBS bd + , requestHeaders = ("Content-Type", renderHeader ct) : [] + } + where + -- TODO keep a list and point to correct pred + -- Do we need MTL for this? See GenT + -- TODO limit size of Expr + new = arbitrary :: Gen (VCMeta Int Int (Expr (Pinned VCObjectHash) (), TCScheme)) + genFetchHist :: Gen (BaseUrl -> Request) + genFetchHist = return $ \burl -> defaultRequest + { host = cs $ baseUrlHost burl + , port = baseUrlPort burl + , secure = baseUrlScheme burl == Https + , method = "GET" + , path = "/fetch/X8-B10K4IF4blrwGl3oztxlw-0LAeiQvkx5bxvLrr4Y=" + } noInternalError :: RequestPredicate noInternalError = RequestPredicate $ \req mgr -> do resp <- httpLbs req mgr putStrLn $ show req ++ show resp when (responseStatus resp == status500) $ do - case (Aeson.decode $ responseBody resp :: Maybe VCStoreError) of + case ((Aeson.decode $ responseBody resp) :: Maybe VCStoreError) of -- HACK: these are the non-errors, which should be 404s or other codes: Just (CouldNotFindObject _) -> pure () Just (TryingToAppendToNonHead _) -> pure () @@ -40,15 +87,54 @@ noInternalError = RequestPredicate $ \req mgr -> do spec :: Spec spec = describe "inferno-vc server" $ do - let maxTries = 35 + let maxTries = 20 let args = defaultArgs { maxSuccess = maxTries } -- let args = defaultArgs - it "no internal errors" $ do - withSystemTempDirectory "vc_store_" $ \vcPath -> do - withServantServer testApi (pserver vcPath) $ \url -> - serverSatisfies testApi url args (noInternalError <%> mempty) - putStrLn "Done" + -- it "fixed store dir" $ do + -- let vcPath = "vc_store" + -- removePathForcibly vcPath + -- withServantServer testApi (pserver vcPath) $ \url -> + -- serverSatisfies testApi url args (noInternalError <%> mempty) + + -- TODO switch to using temp dir + -- it "no internal errors" $ do + -- withSystemTempDirectory "vc_store_" $ \vcPath -> do + -- withServantServer testApi (pserver vcPath) $ \url -> + -- serverSatisfies testApi url args (noInternalError <%> mempty) + -- -- TODO this works. Is the problem our predicate? + -- -- serverSatisfies testApi url args (not500 <%> mempty) + -- putStrLn "Done" + + it "concurrent test" $ do + let preds = noInternalError <%> mempty + let vcPath = "vc_store" + removePathForcibly vcPath + let proxyApi = Proxy :: Proxy VCApi + + withServantServer testApi (pserver vcPath) $ \burl -> do + deetsMVar <- newEmptyMVar + let reqs = ($ burl) <$> runGenRequest proxyApi + let prop = monadicIO $ forAllM reqs $ \req -> do + v <- run $ finishPredicates preds (noCheckStatus req) defManager + _ <- run $ tryPutMVar deetsMVar v + case v of + Just _ -> assert False + _ -> return () + -- TODO do both threads generate the exact same sequence of requests? + (res1, res2) <- concurrently + (quickCheckWithResult args {chatty = False} prop) + (quickCheckWithResult args {chatty = False} prop) + -- TODO need a way to kill other threads when property violated in one thread + case (res1, res2) of + (Success {}, Success {}) -> return () + _ -> do + mx <- tryReadMVar deetsMVar + case mx of + Just x -> + expectationFailure $ "Failed:\n" ++ show x + Nothing -> + expectationFailure $ "We failed to record a reason for failure: " <> show (res1, res2) where tracer = contramap vcServerTraceToString $ IOTracer $ simpleStdOutTracer From 2bf973656d7d3f277a30161343afdf4471f00824 Mon Sep 17 00:00:00 2001 From: Siddharth Krishna Date: Wed, 30 Aug 2023 15:42:17 +0000 Subject: [PATCH 5/5] TEMP: try to use servant-conctest --- cabal.project | 2 +- inferno-vc/inferno-vc.cabal | 21 +++++ .../src/Inferno/VersionControl/Operations.hs | 10 ++- inferno-vc/test/Spec.hs | 82 +++++++++++++++++++ 4 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 inferno-vc/test/Spec.hs diff --git a/cabal.project b/cabal.project index 07654b9..57ebd10 100644 --- a/cabal.project +++ b/cabal.project @@ -3,7 +3,7 @@ packages: ./inferno-types ./inferno-lsp ./inferno-vc - ../servant-quickcheck + ../servant-conctest source-repository-package type: git diff --git a/inferno-vc/inferno-vc.cabal b/inferno-vc/inferno-vc.cabal index 3c2462f..a3dfa82 100644 --- a/inferno-vc/inferno-vc.cabal +++ b/inferno-vc/inferno-vc.cabal @@ -76,6 +76,27 @@ library , TupleSections , RecordWildCards +test-suite inferno-vc-tests + type: exitcode-stdio-1.0 + hs-source-dirs: test + main-is: Spec.hs + build-depends: + base >=4.7 && <5 + , containers + , hspec + , http-client + , inferno-types + , inferno-vc + , QuickCheck + , servant-client + , servant-conctest + , servant-server + , servant-typed-error + , temporary + , time + default-language: Haskell2010 + ghc-options: -Wall -Wunused-packages -Wincomplete-uni-patterns -Wincomplete-record-updates -threaded -with-rtsopts=-N + -- An example executable definition, needs instantation of author/group types: -- executable inferno-vc-server -- main-is: Main.hs diff --git a/inferno-vc/src/Inferno/VersionControl/Operations.hs b/inferno-vc/src/Inferno/VersionControl/Operations.hs index ac3190b..a62ea6c 100644 --- a/inferno-vc/src/Inferno/VersionControl/Operations.hs +++ b/inferno-vc/src/Inferno/VersionControl/Operations.hs @@ -53,7 +53,7 @@ import Inferno.VersionControl.Types vcObjectHashToByteString, ) import Plow.Logging (IOTracer, traceWith) -import System.Directory (createDirectoryIfMissing, doesFileExist, getDirectoryContents, removeFile, renameFile) +import System.Directory (createDirectoryIfMissing, doesFileExist, getDirectoryContents, removeFile, renameFile, removePathForcibly) import System.FilePath.Posix (takeFileName, ()) newtype VCStorePath = VCStorePath FilePath deriving (Generic) @@ -128,6 +128,14 @@ readVCObjectHashTxt fp = do decoded <- either (const $ throwError $ InvalidHash $ Char8.unpack dep) pure $ Base64.decode dep maybe (throwError $ InvalidHash $ Char8.unpack dep) (pure . VCObjectHash) $ digestFromByteString decoded +-- | RESET the VC store: deletes all objects permanently and resets to initial state. +-- Intended to be used for testing only. +resetVC :: VCStoreEnvM env m => m () +resetVC = do + VCStorePath storePath <- getTyped <$> ask + liftIO $ removePathForcibly storePath + initVCStore + storeVCObject :: forall env err m a g. (VCStoreLogM env m, VCStoreErrM err m, VCStoreEnvM env m, VCHashUpdate a, VCHashUpdate g, ToJSON a, ToJSON g, FromJSON a, FromJSON g) => VCMeta a g VCObject -> m VCObjectHash storeVCObject obj@VCMeta {obj = ast, pred = p} = do VCStorePath storePath <- getTyped <$> ask diff --git a/inferno-vc/test/Spec.hs b/inferno-vc/test/Spec.hs new file mode 100644 index 0000000..ce28b13 --- /dev/null +++ b/inferno-vc/test/Spec.hs @@ -0,0 +1,82 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TupleSections #-} +{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE TypeApplications #-} + +module Main (main) where + +import qualified Control.Concurrent.MVar as MVar +import Control.Monad (when) +import Control.Monad.IO.Class (liftIO) +import Network.HTTP.Client (defaultManagerSettings, newManager) +import Servant +import Servant.Client (ClientM, client) +import Servant.ConcTest (Method (..), isLinearizable, withServantServer) +import Test.QuickCheck (elements, forAll, ioProperty, quickCheck, vectorOf, verbose) + +import Control.Concurrent (forkIO) +import Control.Monad (forM_) +import qualified Data.Map as Map +import Data.Proxy (Proxy (..)) +import qualified Data.Set as Set +import Data.Time.Clock (getCurrentTime) +import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds) +import Foreign.C (CTime (..)) +import Inferno.Types.Syntax (Expr (Lit), Lit (LDouble), TV (TV)) +import Inferno.Types.Type (ImplType (ImplType), TCScheme (ForallTC), typeDouble) +import Inferno.VersionControl.Client (ClientMWithVCStoreError, api, mkVCClientEnv) +import Inferno.VersionControl.Operations.Error (VCStoreError (..)) +import Inferno.VersionControl.Server (VCServerError (VCServerError), runServerConfig) +import Inferno.VersionControl.Server.Types (ServerConfig (..)) +import Inferno.VersionControl.Types (Pinned, VCMeta (..), VCObject (VCFunction), VCObjectHash, VCObjectPred (CloneOf, Init, MarkedBreakingWithPred), VCObjectVisibility (VCObjectPublic)) +import Network.HTTP.Client (defaultManagerSettings, newManager) +import Servant ((:<|>) (..)) +import Servant.Client (BaseUrl (..), ClientEnv, Scheme (..), client) +import Servant.Typed.Error (runTypedClientM, typedClient) +import System.IO.Temp (withSystemTempDirectory) +import Test.Hspec +import Test.QuickCheck (arbitrary, generate) + + +fetchFunction :: VCObjectHash -> ClientMWithVCStoreError (VCMeta Int Int (Expr (Pinned VCObjectHash) (), TCScheme)) +fetchFunctionsForGroups :: Set.Set Int -> ClientMWithVCStoreError [VCMeta Int Int VCObjectHash] +fetchVCObject :: VCObjectHash -> ClientMWithVCStoreError (VCMeta Int Int VCObject) +fetchVCObjectHistory :: VCObjectHash -> ClientMWithVCStoreError [VCMeta Int Int VCObjectHash] +fetchVCObjects :: [VCObjectHash] -> ClientMWithVCStoreError (Map.Map VCObjectHash (VCMeta Int Int VCObject)) +fetchVCObjectClosureHashes :: VCObjectHash -> ClientMWithVCStoreError [VCObjectHash] +pushFunction :: VCMeta Int Int (Expr (Pinned VCObjectHash) (), TCScheme) -> ClientMWithVCStoreError VCObjectHash +deleteAutosavedFunction :: VCObjectHash -> ClientMWithVCStoreError () +deleteVCObject :: VCObjectHash -> ClientMWithVCStoreError () +fetchFunction + :<|> fetchFunctionsForGroups + :<|> fetchVCObject + :<|> fetchVCObjectHistory + :<|> fetchVCObjects + :<|> fetchVCObjectClosureHashes + :<|> pushFunction + :<|> deleteAutosavedFunction + :<|> deleteVCObject = typedClient $ client $ api @Int @Int + +main :: IO () +main = do + manager <- newManager defaultManagerSettings + + -- Define the API to be tested + let putClientFn = Method {Servant.ConcTest.name = "put", clientFn = show <$> putClient} + let getClientFn = Method {Servant.ConcTest.name = "get", clientFn = show <$> getClient} + let fns = [getClientFn, putClientFn] + + -- Generate concurrent executions to be tested + let numThreads = 2 + let numCalls = 2 + let execGen = vectorOf numThreads $ vectorOf numCalls $ elements fns + + withServantServer api server $ \burl -> do + quickCheck $ + verbose $ + forAll execGen (ioProperty . isLinearizable manager burl resetClient)