88 "os/exec"
99 "path/filepath"
1010 "runtime"
11+ "slices"
1112 "strings"
1213 "testing"
1314 "time"
@@ -21,6 +22,20 @@ func TestMain(m *testing.M) {
2122 os .Exit (m .Run ())
2223}
2324
25+ type downloadResult struct {
26+ r * Result
27+ err error
28+ }
29+
30+ // We expect only few parallel downloads. Testing with larger number to find
31+ // races quicker. 20 parallel downloads take about 0.5 seocnds on M1 Pro.
32+ const parallelDownloads = 20
33+
34+ // When downloading in parallel usually all downloads completed with
35+ // StatusDownload, but some may be delayed and find the data file when they
36+ // start. Can be reproduced locally using 100 parallel downloads.
37+ var parallelStatus []Status = []Status {StatusDownloaded , StatusUsedCache }
38+
2439func TestDownloadRemote (t * testing.T ) {
2540 ts := httptest .NewServer (http .FileServer (http .Dir ("testdata" )))
2641 t .Cleanup (ts .Close )
@@ -57,38 +72,90 @@ func TestDownloadRemote(t *testing.T) {
5772 })
5873 })
5974 t .Run ("with cache" , func (t * testing.T ) {
60- cacheDir := filepath .Join (t .TempDir (), "cache" )
61- localPath := filepath .Join (t .TempDir (), t .Name ())
62- r , err := Download (context .Background (), localPath , dummyRemoteFileURL , WithExpectedDigest (dummyRemoteFileDigest ), WithCacheDir (cacheDir ))
63- assert .NilError (t , err )
64- assert .Equal (t , StatusDownloaded , r .Status )
75+ t .Run ("serial" , func (t * testing.T ) {
76+ cacheDir := filepath .Join (t .TempDir (), "cache" )
77+ localPath := filepath .Join (t .TempDir (), t .Name ())
78+ r , err := Download (context .Background (), localPath , dummyRemoteFileURL ,
79+ WithExpectedDigest (dummyRemoteFileDigest ), WithCacheDir (cacheDir ))
80+ assert .NilError (t , err )
81+ assert .Equal (t , StatusDownloaded , r .Status )
6582
66- r , err = Download (context .Background (), localPath , dummyRemoteFileURL , WithExpectedDigest (dummyRemoteFileDigest ), WithCacheDir (cacheDir ))
67- assert .NilError (t , err )
68- assert .Equal (t , StatusSkipped , r .Status )
83+ r , err = Download (context .Background (), localPath , dummyRemoteFileURL ,
84+ WithExpectedDigest (dummyRemoteFileDigest ), WithCacheDir (cacheDir ))
85+ assert .NilError (t , err )
86+ assert .Equal (t , StatusSkipped , r .Status )
6987
70- localPath2 := localPath + "-2"
71- r , err = Download (context .Background (), localPath2 , dummyRemoteFileURL , WithExpectedDigest (dummyRemoteFileDigest ), WithCacheDir (cacheDir ))
72- assert .NilError (t , err )
73- assert .Equal (t , StatusUsedCache , r .Status )
88+ localPath2 := localPath + "-2"
89+ r , err = Download (context .Background (), localPath2 , dummyRemoteFileURL ,
90+ WithExpectedDigest (dummyRemoteFileDigest ), WithCacheDir (cacheDir ))
91+ assert .NilError (t , err )
92+ assert .Equal (t , StatusUsedCache , r .Status )
93+ })
94+ t .Run ("parallel" , func (t * testing.T ) {
95+ cacheDir := filepath .Join (t .TempDir (), "cache" )
96+ results := make (chan downloadResult , parallelDownloads )
97+ for i := 0 ; i < parallelDownloads ; i ++ {
98+ go func () {
99+ // Parallel download is supported only for different instances with unique localPath.
100+ localPath := filepath .Join (t .TempDir (), t .Name ())
101+ r , err := Download (context .Background (), localPath , dummyRemoteFileURL ,
102+ WithExpectedDigest (dummyRemoteFileDigest ), WithCacheDir (cacheDir ))
103+ results <- downloadResult {r , err }
104+ }()
105+ }
106+ // We must process all results before cleanup.
107+ for i := 0 ; i < parallelDownloads ; i ++ {
108+ result := <- results
109+ if result .err != nil {
110+ t .Errorf ("Download failed: %s" , result .err )
111+ } else if ! slices .Contains (parallelStatus , result .r .Status ) {
112+ t .Errorf ("Expected download status %s, got %s" , parallelStatus , result .r .Status )
113+ }
114+ }
115+ })
74116 })
75117 t .Run ("caching-only mode" , func (t * testing.T ) {
76- _ , err := Download (context .Background (), "" , dummyRemoteFileURL , WithExpectedDigest (dummyRemoteFileDigest ))
77- assert .ErrorContains (t , err , "cache directory to be specified" )
118+ t .Run ("serial" , func (t * testing.T ) {
119+ _ , err := Download (context .Background (), "" , dummyRemoteFileURL , WithExpectedDigest (dummyRemoteFileDigest ))
120+ assert .ErrorContains (t , err , "cache directory to be specified" )
78121
79- cacheDir := filepath .Join (t .TempDir (), "cache" )
80- r , err := Download (context .Background (), "" , dummyRemoteFileURL , WithExpectedDigest (dummyRemoteFileDigest ), WithCacheDir (cacheDir ))
81- assert .NilError (t , err )
82- assert .Equal (t , StatusDownloaded , r .Status )
122+ cacheDir := filepath .Join (t .TempDir (), "cache" )
123+ r , err := Download (context .Background (), "" , dummyRemoteFileURL , WithExpectedDigest (dummyRemoteFileDigest ),
124+ WithCacheDir (cacheDir ))
125+ assert .NilError (t , err )
126+ assert .Equal (t , StatusDownloaded , r .Status )
83127
84- r , err = Download (context .Background (), "" , dummyRemoteFileURL , WithExpectedDigest (dummyRemoteFileDigest ), WithCacheDir (cacheDir ))
85- assert .NilError (t , err )
86- assert .Equal (t , StatusUsedCache , r .Status )
128+ r , err = Download (context .Background (), "" , dummyRemoteFileURL , WithExpectedDigest (dummyRemoteFileDigest ),
129+ WithCacheDir (cacheDir ))
130+ assert .NilError (t , err )
131+ assert .Equal (t , StatusUsedCache , r .Status )
87132
88- localPath := filepath .Join (t .TempDir (), t .Name ())
89- r , err = Download (context .Background (), localPath , dummyRemoteFileURL , WithExpectedDigest (dummyRemoteFileDigest ), WithCacheDir (cacheDir ))
90- assert .NilError (t , err )
91- assert .Equal (t , StatusUsedCache , r .Status )
133+ localPath := filepath .Join (t .TempDir (), t .Name ())
134+ r , err = Download (context .Background (), localPath , dummyRemoteFileURL ,
135+ WithExpectedDigest (dummyRemoteFileDigest ), WithCacheDir (cacheDir ))
136+ assert .NilError (t , err )
137+ assert .Equal (t , StatusUsedCache , r .Status )
138+ })
139+ t .Run ("parallel" , func (t * testing.T ) {
140+ cacheDir := filepath .Join (t .TempDir (), "cache" )
141+ results := make (chan downloadResult , parallelDownloads )
142+ for i := 0 ; i < parallelDownloads ; i ++ {
143+ go func () {
144+ r , err := Download (context .Background (), "" , dummyRemoteFileURL ,
145+ WithExpectedDigest (dummyRemoteFileDigest ), WithCacheDir (cacheDir ))
146+ results <- downloadResult {r , err }
147+ }()
148+ }
149+ // We must process all results before cleanup.
150+ for i := 0 ; i < parallelDownloads ; i ++ {
151+ result := <- results
152+ if result .err != nil {
153+ t .Errorf ("Download failed: %s" , result .err )
154+ } else if ! slices .Contains (parallelStatus , result .r .Status ) {
155+ t .Errorf ("Expected download status %s, got %s" , parallelStatus , result .r .Status )
156+ }
157+ }
158+ })
92159 })
93160 t .Run ("cached" , func (t * testing.T ) {
94161 _ , err := Cached (dummyRemoteFileURL , WithExpectedDigest (dummyRemoteFileDigest ))
0 commit comments