cachix-1.13.0: test/DeploySpec.hs
module DeploySpec where
import Cachix.API.WebSocketSubprotocol qualified as DeploymentDetails (DeploymentDetails (..))
import Cachix.API.WebSocketSubprotocol qualified as WSS
import Cachix.Client.Config qualified as Config
import Cachix.Deploy.Agent (Agent (..), launchDeploymentWith, mkAgent, registerAgent, waitForAgent)
import Cachix.Deploy.Deployment (lockContentionExitCode)
import Cachix.Deploy.Lock (withTryLock, withTryLockAndPid)
import Cachix.Deploy.Log qualified as Log
import Cachix.Deploy.OptionsParser qualified as CLI
import Cachix.Deploy.Websocket qualified as WebSocket
import Control.Concurrent.MVar qualified as MVar
import Control.Retry qualified as Retry
import Data.IORef
import Data.UUID qualified as UUID
import Protolude
import System.IO.Temp (withSystemTempDirectory)
import Test.Hspec
spec :: Spec
spec =
describe "deploy" $ do
describe "lock" $ do
it "returns Nothing if the lock is free" $
withSystemTempDirectory "cachix-deploy-test" $ \tempDir ->
withTestAgent tempDir $ \agent -> do
mpid <- waitForAgent retryPolicy agent
mpid `shouldBe` Nothing
it "returns Nothing if there's no PID" $
withSystemTempDirectory "cachix-deploy-test" $ \tempDir ->
withTestAgent tempDir $ \agent -> do
void $ withTryLock (lockFile agent) $ do
mpid <- waitForAgent retryPolicy agent
mpid `shouldBe` Nothing
it "returns the PID if the lock is taken" $
withSystemTempDirectory "cachix-deploy-test" $ \tempDir ->
withTestAgent tempDir $ \agent -> do
void $ withTryLockAndPid (lockFile agent) (pidFile agent) $ do
mpid <- waitForAgent retryPolicy agent
mpid `shouldSatisfy` isJust
describe "WebSocket connection timeout" $ do
it "interrupts stalled connection establishment" $ do
let stalledConnection = do
void (MVar.newEmptyMVar >>= MVar.takeMVar :: IO ())
pure ((), pure ())
WebSocket.withConnectionTimeout (10 * 1000) stalledConnection pure
`shouldThrow` (== WebSocket.WebSocketConnectionTimeout)
it "closes an established connection after the client exits" $ do
closed <- MVar.newEmptyMVar
WebSocket.withConnectionTimeout (10 * 1000) (pure ((), MVar.putMVar closed ())) pure
MVar.tryTakeMVar closed `shouldReturn` Just ()
describe "deployment commands" $ do
it "runs a deployment ID only once" $
withSystemTempDirectory "cachix-deploy-test" $ \tempDir ->
withTestAgent tempDir $ \agent -> do
registerTestAgent agent
launches <- newIORef (0 :: Int)
let runDeployment _ = modifyIORef' launches (+ 1) $> ExitSuccess
deployment = testDeployment UUID.nil
launchDeploymentWith runDeployment agent deployment
launchDeploymentWith runDeployment agent deployment
readIORef launches `shouldReturn` 1
it "runs a new deployment ID" $
withSystemTempDirectory "cachix-deploy-test" $ \tempDir ->
withTestAgent tempDir $ \agent -> do
registerTestAgent agent
launches <- newIORef (0 :: Int)
let runDeployment _ = modifyIORef' launches (+ 1) $> ExitSuccess
launchDeploymentWith runDeployment agent (testDeployment UUID.nil)
launchDeploymentWith runDeployment agent (testDeployment (UUID.fromWords 0 0 0 1))
readIORef launches `shouldReturn` 2
it "ignores an older deployment ID after a newer deployment" $
withSystemTempDirectory "cachix-deploy-test" $ \tempDir ->
withTestAgent tempDir $ \agent -> do
registerTestAgent agent
launches <- newIORef (0 :: Int)
let runDeployment _ = modifyIORef' launches (+ 1) $> ExitSuccess
firstDeployment = testDeployment UUID.nil
secondDeployment = testDeployment (UUID.fromWords 0 0 0 1)
launchDeploymentWith runDeployment agent firstDeployment
launchDeploymentWith runDeployment agent secondDeployment
launchDeploymentWith runDeployment agent firstDeployment
readIORef launches `shouldReturn` 2
it "retries a deployment after the child process skips on lock contention" $
withSystemTempDirectory "cachix-deploy-test" $ \tempDir ->
withTestAgent tempDir $ \agent -> do
registerTestAgent agent
results <- newIORef [lockContentionExitCode, ExitSuccess]
let runDeployment _ = atomicModifyIORef' results $ \case
result : remaining -> (remaining, result)
[] -> ([], ExitSuccess)
deployment = testDeployment UUID.nil
launchDeploymentWith runDeployment agent deployment
launchDeploymentWith runDeployment agent deployment
readIORef results `shouldReturn` []
withTestAgent :: FilePath -> (Agent -> IO ()) -> IO ()
withTestAgent tempDir action = do
let logOptions =
Log.Options
{ verbosity = Log.Verbose,
namespace = "agent",
environment = "Test"
}
agentOptions =
CLI.AgentOptions
{ name = "foo",
profile = Just "testing",
bootstrap = False
}
agentToken = ""
cachixOptions <- Config.defaultCachixOptions
Log.withLog logOptions $ \withLog -> do
agent <- mkAgent withLog logOptions (Just tempDir) cachixOptions agentOptions agentToken
action agent
retryPolicy :: Retry.RetryPolicyM IO
retryPolicy = Retry.limitRetries 1 <> Retry.constantDelay 10
registerTestAgent :: Agent -> IO ()
registerTestAgent agent =
registerAgent agent WSS.AgentInformation {WSS.cache = Nothing, WSS.id = UUID.nil}
testDeployment :: UUID.UUID -> WSS.DeploymentDetails
testDeployment deploymentID =
DeploymentDetails.DeploymentDetails
{ DeploymentDetails.storePath = "/nix/store/test",
DeploymentDetails.id = deploymentID,
DeploymentDetails.index = 1,
DeploymentDetails.rollbackScript = Nothing
}