Skip to content

Commit 12090b4

Browse files
authored
Fix: DeleteCommand returns 0 for SuccessCount and FailCount when errors occur (jfrog#342)
1 parent 56a45e5 commit 12090b4

File tree

2 files changed

+322
-11
lines changed

2 files changed

+322
-11
lines changed

artifactory/commands/generic/delete.go

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package generic
22

33
import (
4+
"errors"
5+
46
ioutils "github.com/jfrog/gofrog/io"
57
"github.com/jfrog/jfrog-cli-core/v2/artifactory/utils"
68
"github.com/jfrog/jfrog-cli-core/v2/common/spec"
@@ -46,12 +48,13 @@ func (dc *DeleteCommand) Run() (err error) {
4648
}
4749
}
4850
if allowDelete {
49-
var successCount int
50-
var failedCount int
51+
var successCount, failedCount int
5152
successCount, failedCount, err = dc.DeleteFiles(reader)
53+
// Always set the result counts, even if an error occurred
54+
// This follows the pattern used by move/copy commands
5255
result := dc.Result()
53-
result.SetFailCount(failedCount)
5456
result.SetSuccessCount(successCount)
57+
result.SetFailCount(failedCount)
5558
}
5659
return
5760
}
@@ -108,15 +111,17 @@ func (dc *DeleteCommand) DeleteFiles(reader *content.ContentReader) (successCoun
108111
if err != nil {
109112
return 0, 0, err
110113
}
111-
deletedCount, err := servicesManager.DeleteFiles(reader)
112-
if err != nil {
113-
return 0, 0, err
114-
}
115-
length, err := reader.Length()
116-
if err != nil {
117-
return 0, 0, err
114+
// Perform deletion - capture both count and error separately
115+
deletedCount, deleteErr := servicesManager.DeleteFiles(reader)
116+
// Get total length to calculate failed count
117+
length, lengthErr := reader.Length()
118+
if lengthErr != nil {
119+
// If we can't get the length, return what we have with the original error
120+
return deletedCount, 0, errors.Join(deleteErr, lengthErr)
118121
}
119-
return deletedCount, length - deletedCount, err
122+
// Return actual counts even when there was an error during deletion
123+
// This follows the pattern used by move/copy commands
124+
return deletedCount, length - deletedCount, deleteErr
120125
}
121126

122127
func getDeleteParams(f *spec.File) (deleteParams services.DeleteParams, err error) {
Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
package generic
2+
3+
import (
4+
"errors"
5+
"testing"
6+
7+
"github.com/jfrog/jfrog-cli-core/v2/common/spec"
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestNewDeleteCommand(t *testing.T) {
12+
dc := NewDeleteCommand()
13+
assert.NotNil(t, dc)
14+
assert.Equal(t, "rt_delete", dc.CommandName())
15+
assert.NotNil(t, dc.Result())
16+
}
17+
18+
func TestDeleteCommand_SetThreads(t *testing.T) {
19+
dc := NewDeleteCommand()
20+
dc.SetThreads(5)
21+
assert.Equal(t, 5, dc.Threads())
22+
}
23+
24+
func TestDeleteCommand_Threads(t *testing.T) {
25+
dc := NewDeleteCommand()
26+
27+
// Default threads should be 0
28+
assert.Equal(t, 0, dc.Threads())
29+
30+
// Set and verify threads
31+
dc.SetThreads(10)
32+
assert.Equal(t, 10, dc.Threads())
33+
34+
// Chaining should work
35+
result := dc.SetThreads(3)
36+
assert.Equal(t, dc, result)
37+
assert.Equal(t, 3, dc.Threads())
38+
}
39+
40+
func TestGetDeleteParams(t *testing.T) {
41+
tests := []struct {
42+
name string
43+
file *spec.File
44+
expectedRecursive bool
45+
}{
46+
{
47+
name: "Basic pattern with default recursive",
48+
file: &spec.File{
49+
Pattern: "repo/path/*",
50+
},
51+
expectedRecursive: true,
52+
},
53+
{
54+
name: "Pattern with recursive true",
55+
file: &spec.File{
56+
Pattern: "repo/path/*",
57+
Recursive: "true",
58+
},
59+
expectedRecursive: true,
60+
},
61+
{
62+
name: "Pattern with recursive false",
63+
file: &spec.File{
64+
Pattern: "repo/path/*",
65+
Recursive: "false",
66+
},
67+
expectedRecursive: false,
68+
},
69+
}
70+
71+
for _, tt := range tests {
72+
t.Run(tt.name, func(t *testing.T) {
73+
params, err := getDeleteParams(tt.file)
74+
assert.NoError(t, err)
75+
assert.Equal(t, tt.file.Pattern, params.Pattern)
76+
assert.Equal(t, tt.expectedRecursive, params.Recursive)
77+
})
78+
}
79+
}
80+
81+
func TestGetDeleteParams_ExcludeArtifacts(t *testing.T) {
82+
tests := []struct {
83+
name string
84+
excludeArtifacts string
85+
expectedExcludeArtifacts bool
86+
}{
87+
{
88+
name: "Default exclude artifacts (false)",
89+
excludeArtifacts: "",
90+
expectedExcludeArtifacts: false,
91+
},
92+
{
93+
name: "Exclude artifacts true",
94+
excludeArtifacts: "true",
95+
expectedExcludeArtifacts: true,
96+
},
97+
{
98+
name: "Exclude artifacts false",
99+
excludeArtifacts: "false",
100+
expectedExcludeArtifacts: false,
101+
},
102+
}
103+
104+
for _, tt := range tests {
105+
t.Run(tt.name, func(t *testing.T) {
106+
file := &spec.File{
107+
Pattern: "repo/*",
108+
ExcludeArtifacts: tt.excludeArtifacts,
109+
}
110+
params, err := getDeleteParams(file)
111+
assert.NoError(t, err)
112+
assert.Equal(t, tt.expectedExcludeArtifacts, params.ExcludeArtifacts)
113+
})
114+
}
115+
}
116+
117+
func TestGetDeleteParams_IncludeDeps(t *testing.T) {
118+
tests := []struct {
119+
name string
120+
includeDeps string
121+
expectedIncludeDeps bool
122+
}{
123+
{
124+
name: "Default include deps (false)",
125+
includeDeps: "",
126+
expectedIncludeDeps: false,
127+
},
128+
{
129+
name: "Include deps true",
130+
includeDeps: "true",
131+
expectedIncludeDeps: true,
132+
},
133+
{
134+
name: "Include deps false",
135+
includeDeps: "false",
136+
expectedIncludeDeps: false,
137+
},
138+
}
139+
140+
for _, tt := range tests {
141+
t.Run(tt.name, func(t *testing.T) {
142+
file := &spec.File{
143+
Pattern: "repo/*",
144+
IncludeDeps: tt.includeDeps,
145+
}
146+
params, err := getDeleteParams(file)
147+
assert.NoError(t, err)
148+
assert.Equal(t, tt.expectedIncludeDeps, params.IncludeDeps)
149+
})
150+
}
151+
}
152+
153+
func TestGetDeleteParams_CombinedFlags(t *testing.T) {
154+
file := &spec.File{
155+
Pattern: "repo/path/**/*.jar",
156+
Recursive: "true",
157+
ExcludeArtifacts: "true",
158+
IncludeDeps: "true",
159+
Exclusions: []string{"*-sources.jar", "*-javadoc.jar"},
160+
}
161+
162+
params, err := getDeleteParams(file)
163+
assert.NoError(t, err)
164+
165+
assert.Equal(t, "repo/path/**/*.jar", params.Pattern)
166+
assert.True(t, params.Recursive)
167+
assert.True(t, params.ExcludeArtifacts)
168+
assert.True(t, params.IncludeDeps)
169+
assert.Len(t, params.Exclusions, 2)
170+
}
171+
172+
func TestGetDeleteParams_WithExclusions(t *testing.T) {
173+
file := &spec.File{
174+
Pattern: "repo/*.bin",
175+
Exclusions: []string{
176+
"*-test.bin",
177+
"*-debug.bin",
178+
"temp/*",
179+
},
180+
}
181+
182+
params, err := getDeleteParams(file)
183+
184+
assert.NoError(t, err)
185+
assert.Len(t, params.Exclusions, 3)
186+
assert.Contains(t, params.Exclusions, "*-test.bin")
187+
assert.Contains(t, params.Exclusions, "*-debug.bin")
188+
assert.Contains(t, params.Exclusions, "temp/*")
189+
}
190+
191+
func TestGetDeleteParams_EmptyPattern(t *testing.T) {
192+
file := &spec.File{
193+
Pattern: "",
194+
}
195+
196+
params, err := getDeleteParams(file)
197+
198+
assert.NoError(t, err)
199+
assert.Equal(t, "", params.Pattern)
200+
}
201+
202+
func TestDeleteCommand_ResultInitialized(t *testing.T) {
203+
dc := NewDeleteCommand()
204+
205+
// Result should be initialized (not nil)
206+
result := dc.Result()
207+
assert.NotNil(t, result)
208+
209+
// Default counts should be 0
210+
assert.Equal(t, 0, result.SuccessCount())
211+
assert.Equal(t, 0, result.FailCount())
212+
}
213+
214+
func TestDeleteCommand_GenericCommandEmbedded(t *testing.T) {
215+
dc := NewDeleteCommand()
216+
217+
// Test GenericCommand methods are accessible
218+
dc.SetDryRun(true)
219+
assert.True(t, dc.DryRun())
220+
221+
dc.SetQuiet(true)
222+
assert.True(t, dc.Quiet())
223+
224+
dc.SetRetries(3)
225+
assert.Equal(t, 3, dc.Retries())
226+
}
227+
228+
// TestErrorsJoin verifies that errors.Join works correctly for combining errors
229+
// This tests the behavior used in DeleteFiles when both delete and length errors occur
230+
func TestErrorsJoin(t *testing.T) {
231+
err1 := errors.New("delete error")
232+
err2 := errors.New("length error")
233+
234+
combined := errors.Join(err1, err2)
235+
assert.Error(t, combined)
236+
assert.Contains(t, combined.Error(), "delete error")
237+
assert.Contains(t, combined.Error(), "length error")
238+
239+
// Test with nil errors
240+
combinedWithNil := errors.Join(nil, err2)
241+
assert.Error(t, combinedWithNil)
242+
assert.Contains(t, combinedWithNil.Error(), "length error")
243+
244+
combinedBothNil := errors.Join(nil, nil)
245+
assert.NoError(t, combinedBothNil)
246+
}
247+
248+
// TestDeleteFilesCountBehavior documents the expected behavior of count calculations
249+
// deletedCount represents successfully deleted items
250+
// length - deletedCount represents failed items
251+
func TestDeleteFilesCountBehavior(t *testing.T) {
252+
tests := []struct {
253+
name string
254+
deletedCount int
255+
totalLength int
256+
expectedSuccessCount int
257+
expectedFailedCount int
258+
}{
259+
{
260+
name: "All items deleted successfully",
261+
deletedCount: 10,
262+
totalLength: 10,
263+
expectedSuccessCount: 10,
264+
expectedFailedCount: 0,
265+
},
266+
{
267+
name: "Some items failed",
268+
deletedCount: 7,
269+
totalLength: 10,
270+
expectedSuccessCount: 7,
271+
expectedFailedCount: 3,
272+
},
273+
{
274+
name: "All items failed",
275+
deletedCount: 0,
276+
totalLength: 10,
277+
expectedSuccessCount: 0,
278+
expectedFailedCount: 10,
279+
},
280+
{
281+
name: "No items to delete",
282+
deletedCount: 0,
283+
totalLength: 0,
284+
expectedSuccessCount: 0,
285+
expectedFailedCount: 0,
286+
},
287+
{
288+
name: "Partial success (404 scenario)",
289+
deletedCount: 5,
290+
totalLength: 8,
291+
expectedSuccessCount: 5,
292+
expectedFailedCount: 3,
293+
},
294+
}
295+
296+
for _, tt := range tests {
297+
t.Run(tt.name, func(t *testing.T) {
298+
// This simulates the count calculation in DeleteFiles
299+
successCount := tt.deletedCount
300+
failedCount := tt.totalLength - tt.deletedCount
301+
302+
assert.Equal(t, tt.expectedSuccessCount, successCount)
303+
assert.Equal(t, tt.expectedFailedCount, failedCount)
304+
})
305+
}
306+
}

0 commit comments

Comments
 (0)