Skip to content

Commit 3a81c00

Browse files
committed
✨ Improve artefact manager download methods by adding download options
1 parent c93702f commit 3a81c00

File tree

8 files changed

+342
-137
lines changed

8 files changed

+342
-137
lines changed

changes/20251024142639.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:sparkles: Improve artefact manager download methods by adding download options

utils/artefacts/artefacts.go

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -332,10 +332,16 @@ func (m *ArtefactManager[M, D, L, C]) DownloadAllJobArtefacts(ctx context.Contex
332332
}
333333

334334
func (m *ArtefactManager[M, D, L, C]) DownloadAllJobArtefactsWithTree(ctx context.Context, jobName string, maintainTreeStructure bool, outputDirectory string) (err error) {
335+
return m.DownloadAllJobArtefactsWithOptions(ctx, jobName, outputDirectory, WithMaintainStructure(maintainTreeStructure))
336+
}
337+
338+
func (m *ArtefactManager[M, D, L, C]) DownloadAllJobArtefactsWithOptions(ctx context.Context, jobName string, outputDirectory string, opts ...DownloadOption) (err error) {
335339
err = parallelisation.DetermineContextError(ctx)
336340
if err != nil {
337341
return
338342
}
343+
344+
dlOpts := NewDownloadOptions(opts...)
339345
err = filesystem.MkDir(outputDirectory)
340346
if err != nil {
341347
err = commonerrors.WrapErrorf(commonerrors.ErrUnexpected, err, "failed creating the output directory [%v] for job artefacts", outputDirectory)
@@ -347,37 +353,48 @@ func (m *ArtefactManager[M, D, L, C]) DownloadAllJobArtefactsWithTree(ctx contex
347353
}
348354
stop := paginator.Stop()
349355
defer stop()
356+
var collatedDownloadErrors []error
350357
for {
351358
if !paginator.HasNext() {
359+
if len(collatedDownloadErrors) > 0 {
360+
err = commonerrors.Join(collatedDownloadErrors...)
361+
}
352362
return
353363
}
354364
item, subErr := paginator.GetNext()
355365
if subErr != nil {
356366
err = commonerrors.WrapError(commonerrors.ErrUnexpected, subErr, "failed getting information about job artefacts")
357367
return
358368
}
369+
var artefactName string
370+
var downloadErr error
359371
artefactLink, ok := item.(D)
360372
if ok {
361-
subErr = m.DownloadJobArtefactFromLinkWithTree(ctx, jobName, maintainTreeStructure, outputDirectory, artefactLink)
362-
if subErr != nil {
363-
err = subErr
364-
return
365-
}
366-
373+
artefactName = artefactLink.GetName()
374+
downloadErr = m.DownloadJobArtefactFromLinkWithTree(ctx, jobName, dlOpts.MaintainTreeStructure, outputDirectory, artefactLink)
367375
} else {
368-
artefactManager, ok := item.(M)
369-
if ok {
370-
subErr = m.DownloadJobArtefactWithTree(ctx, jobName, maintainTreeStructure, outputDirectory, artefactManager)
371-
if subErr != nil {
372-
err = subErr
373-
return
374-
}
376+
artefactManager, isManager := item.(M)
377+
if isManager {
378+
artefactName = artefactManager.GetName()
379+
downloadErr = m.DownloadJobArtefactWithTree(ctx, jobName, dlOpts.MaintainTreeStructure, outputDirectory, artefactManager)
375380
} else {
376-
err = commonerrors.New(commonerrors.ErrMarshalling, "the type of the response from service cannot be interpreted")
377-
return
381+
downloadErr = commonerrors.New(commonerrors.ErrMarshalling, "the type of the response from service cannot be interpreted")
378382
}
379-
380383
}
381384

385+
if downloadErr != nil {
386+
if dlOpts.StopOnFirstError {
387+
err = downloadErr
388+
return
389+
}
390+
collatedDownloadErrors = append(collatedDownloadErrors, downloadErr)
391+
if dlOpts.Logger != nil {
392+
dlOpts.Logger.Log(fmt.Sprintf("could not download %s", artefactName))
393+
}
394+
} else if !reflection.IsEmpty(artefactName) {
395+
if dlOpts.Logger != nil {
396+
dlOpts.Logger.Log(fmt.Sprintf("downloading %s", artefactName))
397+
}
398+
}
382399
}
383400
}

utils/artefacts/artefacts_test.go

Lines changed: 187 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/stretchr/testify/require"
2121

2222
"github.com/ARM-software/embedded-development-services-client/client"
23+
"github.com/ARM-software/golang-utils/utils/collection"
2324
"github.com/ARM-software/golang-utils/utils/commonerrors"
2425
"github.com/ARM-software/golang-utils/utils/commonerrors/errortest"
2526
"github.com/ARM-software/golang-utils/utils/field"
@@ -28,14 +29,21 @@ import (
2829
"github.com/ARM-software/golang-utils/utils/safeio"
2930
)
3031

32+
type (
33+
testGetArtefactManagersFirstPageFunc = func(ctx context.Context, _ string) (a *client.ArtefactManagerCollection, resp *http.Response, err error)
34+
testGetArtefactManagerFunc = func(ctx context.Context, job, artefact string) (*client.ArtefactManagerItem, *http.Response, error)
35+
testGetArtefactContentFunc = func(ctx context.Context, job, artefactID string) (*os.File, *http.Response, error)
36+
)
37+
3138
type testArtefact struct {
3239
name string
3340
path string
3441
embeddedResource bool
42+
shouldFail bool
3543
}
3644

37-
func newTestArtefact(t *testing.T, tmpDir, artefactContent string, embeddedResource bool) *testArtefact {
38-
path, err := filesystem.TouchTempFile(tmpDir, "artefact")
45+
func newTestArtefact(t *testing.T, tmpDir, artefactContent string, embeddedResource bool, shouldFail bool) *testArtefact {
46+
path, err := filesystem.TouchTempFile(tmpDir, fmt.Sprintf("artefact-%s", faker.Word()))
3947
require.NoError(t, err)
4048
if len(artefactContent) > 0 {
4149
err = filesystem.WriteFile(path, []byte(artefactContent), 0777)
@@ -48,67 +56,10 @@ func newTestArtefact(t *testing.T, tmpDir, artefactContent string, embeddedResou
4856
name: filepath.Base(path),
4957
path: path,
5058
embeddedResource: embeddedResource,
59+
shouldFail: shouldFail,
5160
}
5261
}
5362

54-
func (t *testArtefact) testGetArtefactManagers(ctx context.Context, _ string) (a *client.ArtefactManagerCollection, resp *http.Response, err error) {
55-
item, err := t.fetchTestArtefact(ctx)
56-
if err != nil {
57-
return
58-
}
59-
if t.embeddedResource {
60-
a = &client.ArtefactManagerCollection{
61-
Embedded: &client.EmbeddedArtefactManagerItems{Item: []client.ArtefactManagerItem{*item}},
62-
Links: *client.NewNullableHalCollectionLinks(client.NewHalCollectionLinksWithDefaults()),
63-
Metadata: *client.NewNullablePagingMetadata(&client.PagingMetadata{
64-
Count: 1,
65-
Ctime: time.Now(),
66-
Etime: client.NullableTime{},
67-
Limit: 6,
68-
Mtime: time.Now(),
69-
Offset: 0,
70-
Total: 1,
71-
}),
72-
Name: "list of artefacts",
73-
Title: faker.Name(),
74-
}
75-
} else {
76-
links := client.NewHalCollectionLinksWithDefaults()
77-
links.Item = []client.HalLinkData{client.HalLinkData{
78-
Href: fmt.Sprintf("/test/%v", item.Name),
79-
Name: field.ToOptionalString(item.Name),
80-
Title: field.ToOptionalString(faker.Name()),
81-
}}
82-
a = &client.ArtefactManagerCollection{
83-
Embedded: nil,
84-
Links: *client.NewNullableHalCollectionLinks(links),
85-
Metadata: *client.NewNullablePagingMetadata(&client.PagingMetadata{
86-
Count: 1,
87-
Ctime: time.Now(),
88-
Etime: client.NullableTime{},
89-
Limit: 6,
90-
Mtime: time.Now(),
91-
Offset: 0,
92-
Total: 1,
93-
}),
94-
Name: "list of artefacts",
95-
Title: faker.Name(),
96-
}
97-
}
98-
resp = &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(safeio.NewByteReader(ctx, []byte("hello")))}
99-
return
100-
}
101-
102-
func (t *testArtefact) testGetArtefactManager(ctx context.Context, _, artefact string) (a *client.ArtefactManagerItem, resp *http.Response, err error) {
103-
if artefact == t.name {
104-
a, err = t.fetchTestArtefact(ctx)
105-
resp = &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(safeio.NewByteReader(ctx, []byte("hello")))}
106-
} else {
107-
err = commonerrors.ErrNotFound
108-
}
109-
return
110-
}
111-
11263
func (t *testArtefact) fetchTestArtefact(ctx context.Context) (a *client.ArtefactManagerItem, err error) {
11364
fileHasher, subErr := filesystem.NewFileHash(hashing.HashSha256)
11465
if subErr != nil {
@@ -143,28 +94,155 @@ func (t *testArtefact) fetchTestArtefact(ctx context.Context) (a *client.Artefac
14394
return
14495
}
14596

146-
func (t *testArtefact) testGetOutputArtefact(ctx context.Context, _, artefact string) (f *os.File, resp *http.Response, err error) {
147-
if artefact == t.name {
97+
func testGetArtefactManager(t *testing.T, artefacts []*testArtefact) testGetArtefactManagerFunc {
98+
t.Helper()
99+
if len(artefacts) == 0 {
100+
return nil
101+
}
102+
103+
names := collection.Map(artefacts, func(a *testArtefact) string {
104+
return a.name
105+
})
106+
107+
return func(ctx context.Context, _, artefact string) (a *client.ArtefactManagerItem, resp *http.Response, err error) {
108+
artefactIdx, found := collection.Find(&names, artefact)
109+
if !found {
110+
err = commonerrors.ErrNotFound
111+
return
112+
}
113+
t := artefacts[artefactIdx]
114+
a, err = t.fetchTestArtefact(ctx)
115+
resp = &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(safeio.NewByteReader(ctx, []byte("hello")))}
116+
return
117+
118+
}
119+
}
120+
121+
func testGetOutputArtefact(t *testing.T, artefacts []*testArtefact) testGetArtefactContentFunc {
122+
t.Helper()
123+
if len(artefacts) == 0 {
124+
return nil
125+
}
126+
127+
names := collection.Map(artefacts, func(a *testArtefact) string {
128+
return a.name
129+
})
130+
131+
return func(ctx context.Context, _, artefact string) (f *os.File, resp *http.Response, err error) {
132+
artefactIdx, found := collection.Find(&names, artefact)
133+
if !found {
134+
return nil, &http.Response{StatusCode: http.StatusNotFound, Body: io.NopCloser(safeio.NewByteReader(ctx, []byte("hello")))}, commonerrors.ErrNotFound
135+
}
136+
137+
t := artefacts[artefactIdx]
138+
139+
if t.shouldFail {
140+
return nil, &http.Response{StatusCode: http.StatusInternalServerError, Body: io.NopCloser(safeio.NewByteReader(ctx, []byte("hello")))}, commonerrors.ErrUnexpected
141+
}
142+
148143
f, err = os.Open(t.path)
149144
if err != nil {
150145
return
151146
}
152147

153148
return f, &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(safeio.NewByteReader(ctx, []byte("hello")))}, nil
154149
}
155-
156-
return nil, &http.Response{StatusCode: http.StatusNotFound, Body: io.NopCloser(safeio.NewByteReader(ctx, []byte("hello")))}, commonerrors.ErrNotFound
157150
}
158151

159-
func newTestArtefactManager(t *testing.T, tmpDir, artefactContent string, linksOnly bool) (IArtefactManager[*client.ArtefactManagerItem, *client.HalLinkData], *testArtefact) {
160-
testArtefact := newTestArtefact(t, tmpDir, artefactContent, !linksOnly)
161-
return NewArtefactManager(testArtefact.testGetArtefactManagers, nil, testArtefact.testGetArtefactManager, testArtefact.testGetOutputArtefact), testArtefact
152+
func testGetArtefactManagers(t *testing.T, artefacts []*testArtefact, embeddedResource bool) testGetArtefactManagersFirstPageFunc {
153+
t.Helper()
154+
if len(artefacts) == 0 {
155+
return nil
156+
}
157+
158+
return func(ctx context.Context, _ string) (a *client.ArtefactManagerCollection, resp *http.Response, err error) {
159+
count := int32(len(artefacts))
160+
if embeddedResource {
161+
items, mapErr := collection.MapWithError(artefacts, func(artefact *testArtefact) (client.ArtefactManagerItem, error) {
162+
item, err := artefact.fetchTestArtefact(ctx)
163+
if err != nil {
164+
return client.ArtefactManagerItem{}, err
165+
}
166+
167+
return *item, nil
168+
})
169+
170+
if mapErr != nil {
171+
err = mapErr
172+
return
173+
}
174+
175+
a = &client.ArtefactManagerCollection{
176+
Embedded: &client.EmbeddedArtefactManagerItems{Item: items},
177+
Links: *client.NewNullableHalCollectionLinks(client.NewHalCollectionLinksWithDefaults()),
178+
Metadata: *client.NewNullablePagingMetadata(&client.PagingMetadata{
179+
Count: count,
180+
Ctime: time.Now(),
181+
Etime: client.NullableTime{},
182+
Limit: 6,
183+
Mtime: time.Now(),
184+
Offset: 0,
185+
Total: count,
186+
}),
187+
Name: "list of artefacts",
188+
Title: faker.Name(),
189+
}
190+
} else {
191+
items, mapErr := collection.MapWithError(artefacts, func(artefact *testArtefact) (client.HalLinkData, error) {
192+
item, err := artefact.fetchTestArtefact(ctx)
193+
if err != nil {
194+
return client.HalLinkData{}, err
195+
}
196+
197+
return client.HalLinkData{
198+
Href: fmt.Sprintf("/test/%v", item.Name),
199+
Name: field.ToOptionalString(item.Name),
200+
Title: field.ToOptionalString(faker.Name()),
201+
}, nil
202+
})
203+
204+
if mapErr != nil {
205+
err = mapErr
206+
return
207+
}
208+
209+
links := client.NewHalCollectionLinksWithDefaults()
210+
links.Item = items
211+
a = &client.ArtefactManagerCollection{
212+
Embedded: nil,
213+
Links: *client.NewNullableHalCollectionLinks(links),
214+
Metadata: *client.NewNullablePagingMetadata(&client.PagingMetadata{
215+
Count: count,
216+
Ctime: time.Now(),
217+
Etime: client.NullableTime{},
218+
Limit: 6,
219+
Mtime: time.Now(),
220+
Offset: 0,
221+
Total: count,
222+
}),
223+
Name: "list of artefacts",
224+
Title: faker.Name(),
225+
}
226+
}
227+
resp = &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(safeio.NewByteReader(ctx, []byte("hello")))}
228+
return
229+
}
162230
}
163231

164232
func newTestArtefactManagerWithEmbeddedResources(t *testing.T, tmpDir, artefactContent string) (IArtefactManager[*client.ArtefactManagerItem, *client.HalLinkData], *testArtefact) {
165233
return newTestArtefactManager(t, tmpDir, artefactContent, false)
166234
}
167235

236+
func newTestArtefactManager(t *testing.T, tmpDir, artefactContent string, linksOnly bool) (IArtefactManager[*client.ArtefactManagerItem, *client.HalLinkData], *testArtefact) {
237+
artefact := newTestArtefact(t, tmpDir, artefactContent, !linksOnly, false)
238+
artefacts := []*testArtefact{artefact}
239+
return newTestArtefactsManager(t, artefacts, linksOnly), artefact
240+
}
241+
242+
func newTestArtefactsManager(t *testing.T, artefacts []*testArtefact, linksOnly bool) IArtefactManager[*client.ArtefactManagerItem, *client.HalLinkData] {
243+
return NewArtefactManager(testGetArtefactManagers(t, artefacts, !linksOnly), nil, testGetArtefactManager(t, artefacts), testGetOutputArtefact(t, artefacts))
244+
}
245+
168246
func TestDetermineDestination(t *testing.T) {
169247
outputDir := strings.ReplaceAll(faker.Sentence(), " ", "//") + " "
170248
cleanedOutputDir := filepath.Clean(outputDir)
@@ -292,6 +370,48 @@ func TestArtefactDownload(t *testing.T) {
292370
require.NoError(t, err)
293371
assert.Equal(t, expectedContents, actualContents)
294372
})
373+
t.Run("Stop on first download error", func(t *testing.T) {
374+
tmpDir, err := filesystem.TempDirInTempDir("test-artefact-")
375+
require.NoError(t, err)
376+
defer func() { _ = filesystem.Rm(tmpDir) }()
377+
artefacts := []*testArtefact{
378+
newTestArtefact(t, tmpDir, faker.Sentence(), true, false),
379+
newTestArtefact(t, tmpDir, faker.Sentence(), true, true),
380+
newTestArtefact(t, tmpDir, faker.Sentence(), true, false),
381+
}
382+
manager := newTestArtefactsManager(t, artefacts, false)
383+
384+
out := t.TempDir()
385+
err = manager.DownloadAllJobArtefactsWithOptions(context.Background(), faker.Word(), out, WithStopOnFirstError(true))
386+
require.Error(t, err)
387+
assert.ErrorIs(t, err, commonerrors.ErrUnexpected)
388+
389+
require.FileExists(t, filepath.Join(out, artefacts[0].name))
390+
assert.NoFileExists(t, filepath.Join(out, artefacts[1].name))
391+
assert.NoFileExists(t, filepath.Join(out, artefacts[2].name))
392+
})
393+
394+
t.Run("Continue on download error", func(t *testing.T) {
395+
tmpDir, err := filesystem.TempDirInTempDir("test-artefact-")
396+
require.NoError(t, err)
397+
defer func() { _ = filesystem.Rm(tmpDir) }()
398+
artefacts := []*testArtefact{
399+
newTestArtefact(t, tmpDir, faker.Sentence(), true, false),
400+
newTestArtefact(t, tmpDir, faker.Sentence(), true, true),
401+
newTestArtefact(t, tmpDir, faker.Sentence(), true, false),
402+
}
403+
manager := newTestArtefactsManager(t, artefacts, false)
404+
405+
out := t.TempDir()
406+
err = manager.DownloadAllJobArtefactsWithOptions(context.Background(), faker.Word(), out, WithStopOnFirstError(false))
407+
require.Error(t, err)
408+
assert.ErrorIs(t, err, commonerrors.ErrUnexpected)
409+
410+
require.FileExists(t, filepath.Join(out, artefacts[0].name))
411+
assert.NoFileExists(t, filepath.Join(out, artefacts[1].name))
412+
assert.FileExists(t, filepath.Join(out, artefacts[2].name))
413+
414+
})
295415
t.Run("Happy download artefact and keep tree", func(t *testing.T) {
296416
tmpDir, err := filesystem.TempDirInTempDir("test-artefact-with-tree-")
297417
require.NoError(t, err)

0 commit comments

Comments
 (0)