{-# LANGUAGE ScopedTypeVariables #-} module UnitTests.Distribution.Client.FetchUtils ( tests, ) where import Control.Concurrent (threadDelay) import Control.Exception import Data.Time.Clock (NominalDiffTime, UTCTime, diffUTCTime, getCurrentTime) import Distribution.Client.FetchUtils import Distribution.Client.GlobalFlags (RepoContext (..)) import Distribution.Client.HttpUtils (HttpCode, HttpTransport (..)) import Distribution.Client.Types.PackageLocation (PackageLocation (..), ResolvedPkgLoc) import Distribution.Client.Types.Repo (Repo (..), emptyRemoteRepo) import Distribution.Client.Types.RepoName (RepoName (..)) import Distribution.Types.PackageId (PackageIdentifier (..)) import Distribution.Types.PackageName (mkPackageName) import qualified Distribution.Verbosity as Verbosity import Distribution.Version (mkVersion) import Network.URI (URI, uriPath) import Test.Tasty import Test.Tasty.HUnit import UnitTests.TempTestDir (withTestDir) tests :: [TestTree] tests = [ testGroup "asyncFetchPackages" [ testCase "handles an empty package list" testEmpty, testCase "passes an unpacked local package through" testPassLocalPackage, testCase "handles http" testHttp, testCase "aborts on interrupt in GET" $ testGetInterrupt, testCase "aborts on other exception in GET" $ testGetException, testCase "aborts on interrupt in GET (uncollected download)" $ testUncollectedInterrupt, testCase "continues on other exception in GET (uncollected download)" $ testUncollectedException ] ] verbosity :: Verbosity.Verbosity verbosity = Verbosity.silent -- | An interval that we use to assert that something happens "immediately". -- Must be shorter than 'longSleep' to ensure those are interrupted. -- 1s would be a reasonable value, but failed tempfile cleanup on Windows CI -- takes ~1s. shortDelta :: NominalDiffTime shortDelta = 5 -- 5s longSleep :: IO () longSleep = threadDelay 10000000 -- 10s testEmpty :: Assertion testEmpty = do let repoCtxt = undefined pkgLocs = [] res <- asyncFetchPackages verbosity repoCtxt pkgLocs $ \_ -> return () res @?= () testPassLocalPackage :: Assertion testPassLocalPackage = do let repoCtxt = error "repoCtxt undefined" loc = LocalUnpackedPackage "a" res <- asyncFetchPackages verbosity repoCtxt [loc] $ \downloadMap -> waitAsyncFetchPackage verbosity downloadMap loc res @?= LocalUnpackedPackage "a" testHttp :: Assertion testHttp = withFakeRepoCtxt get200 $ \repoCtxt repo -> do let pkgId = mkPkgId "foo" loc = RepoTarballPackage repo pkgId Nothing res <- asyncFetchPackages verbosity repoCtxt [loc] $ \downloadMap -> waitAsyncFetchPackage verbosity downloadMap loc case res of RepoTarballPackage repo' pkgId' _ -> do repo' @?= repo pkgId' @?= pkgId _ -> assertFailure $ "expected RepoTarballPackage, got " ++ show res where get200 = \_uri -> return 200 testGetInterrupt :: Assertion testGetInterrupt = testGetAny UserInterrupt testGetException :: Assertion testGetException = testGetAny $ userError "some error" -- | Test that if a GET request fails with the given exception, -- we exit promptly. We queue two slow downloads after the failing -- download to cover a buggy scenario where -- 1. first download throws -- 2. second download is cancelled, but swallows AsyncCancelled -- 3. third download keeps running testGetAny :: Exception e => e -> Assertion testGetAny exc = withFakeRepoCtxt get $ \repoCtxt repo -> do let loc pkgId = RepoTarballPackage repo pkgId Nothing pkgLocs = [loc throws, loc slowA, loc slowB] start <- getCurrentTime res :: Either SomeException ResolvedPkgLoc <- try $ asyncFetchPackages verbosity repoCtxt pkgLocs $ \downloadMap -> do waitAsyncFetchPackage verbosity downloadMap (loc throws) assertFaster start shortDelta case res of Left _ -> pure () Right _ -> assertFailure $ "expected an exception, got " ++ show res where throws = mkPkgId "throws" slowA = mkPkgId "slowA" slowB = mkPkgId "slowB" get uri = case uriPath uri of "package/throws-1.0.tar.gz" -> throwIO exc "package/slowA-1.0.tar.gz" -> longSleep >> return 200 "package/slowB-1.0.tar.gz" -> longSleep >> return 200 _ -> assertFailure $ "unexpected URI: " ++ show uri -- | Test that when an undemanded download is interrupted (Ctrl-C), -- we still abort directly. testUncollectedInterrupt :: Assertion testUncollectedInterrupt = withFakeRepoCtxt get $ \repoCtxt repo -> do let loc pkgId = RepoTarballPackage repo pkgId Nothing pkgLocs = [loc throws, loc slowA, loc slowB] start <- getCurrentTime res :: Either SomeException ResolvedPkgLoc <- try $ asyncFetchPackages verbosity repoCtxt pkgLocs $ \downloadMap -> do waitAsyncFetchPackage verbosity downloadMap (loc slowA) assertFaster start shortDelta case res of Left _ -> pure () Right _ -> assertFailure $ "expected an exception, got " ++ show res where throws = mkPkgId "throws" slowA = mkPkgId "slowA" slowB = mkPkgId "slowB" get uri = case uriPath uri of "package/throws-1.0.tar.gz" -> throwIO UserInterrupt "package/slowA-1.0.tar.gz" -> longSleep >> return 200 "package/slowB-1.0.tar.gz" -> longSleep >> return 200 _ -> assertFailure $ "unexpected URI: " ++ show uri -- | Test that a download failure doesn't automatically abort things, -- e.g. if we don't collect the download. (In practice, we might collect -- the download and handle its exception.) testUncollectedException :: Assertion testUncollectedException = withFakeRepoCtxt get $ \repoCtxt repo -> do let loc pkgId = RepoTarballPackage repo pkgId Nothing pkgLocs = [loc throws, loc foo] start <- getCurrentTime res <- asyncFetchPackages verbosity repoCtxt pkgLocs $ \downloadMap -> do waitAsyncFetchPackage verbosity downloadMap (loc foo) assertFaster start shortDelta case res of RepoTarballPackage repo' pkgId' _ -> do repo' @?= repo pkgId' @?= foo _ -> assertFailure $ "expected RepoTarballPackage, got " ++ show res where throws = mkPkgId "throws" foo = mkPkgId "foo" get uri = case uriPath uri of "package/throws-1.0.tar.gz" -> throwIO $ userError "failed download" "package/foo-1.0.tar.gz" -> return 200 _ -> assertFailure $ "unexpected URI: " ++ show uri assertFaster :: UTCTime -> NominalDiffTime -> Assertion assertFaster start delta = do t <- getCurrentTime assertBool ("took longer than " ++ show delta) (diffUTCTime t start < delta) mkPkgId :: String -> PackageIdentifier mkPkgId name = PackageIdentifier (mkPackageName name) (mkVersion [1, 0]) -- | Provide a repo and a repo context with the given GET handler. withFakeRepoCtxt :: (URI -> IO HttpCode) -> (RepoContext -> Repo -> IO a) -> IO a withFakeRepoCtxt handleGet action = withTestDir verbosity "fake repo" $ \tmpDir -> let repo = RepoRemote { repoRemote = emptyRemoteRepo $ RepoName "fake", repoLocalDir = tmpDir } repoCtxt = RepoContext { repoContextRepos = [repo], repoContextGetTransport = return httpTransport, repoContextWithSecureRepo = \_ _ -> error "fake repo ctxt: repoContextWithSecureRepo not implemented", repoContextIgnoreExpiry = error "fake repo ctxt: repoContextIgnoreExpiry not implemented" } in action repoCtxt repo where httpTransport = HttpTransport { getHttp = \_verbosity uri _etag _filepath _headers -> do code <- handleGet uri return (code, Nothing), postHttp = error "fake transport: postHttp not implemented", postHttpFile = error "fake transport: postHttpFile not implemented", putHttpFile = error "fake transport: putHttp not implemented", transportSupportsHttps = error "fake transport: transportSupportsHttps not implemented", transportManuallySelected = True }