File: FetchUtils.hs

package info (click to toggle)
haskell-cabal-install 3.10.3.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, trixie
  • size: 3,400 kB
  • sloc: haskell: 52,202; sh: 80; makefile: 9
file content (209 lines) | stat: -rw-r--r-- 8,086 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
{-# 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
        }