@@ -11,6 +11,7 @@ import (
1111 "github.com/docker/cli/internal/test"
1212 "github.com/moby/go-archive"
1313 "github.com/moby/go-archive/compression"
14+ "github.com/moby/moby/api/types/container"
1415 "github.com/moby/moby/client"
1516 "gotest.tools/v3/assert"
1617 is "gotest.tools/v3/assert/cmp"
@@ -211,3 +212,237 @@ func TestRunCopyFromContainerToFilesystemIrregularDestination(t *testing.T) {
211212 expected := `"/dev/random" must be a directory or a regular file`
212213 assert .ErrorContains (t , err , expected )
213214}
215+
216+ func TestCopySummary (t * testing.T ) {
217+ tests := []struct {
218+ name string
219+ contentSize int64
220+ transferredSize int64
221+ dest string
222+ wantContains string
223+ wantNoContain string
224+ }{
225+ {
226+ name : "different sizes shows both" ,
227+ contentSize : 5 ,
228+ transferredSize : 2048 ,
229+ dest : "/dst" ,
230+ wantContains : "(transferred" ,
231+ },
232+ {
233+ name : "equal sizes shows single value" ,
234+ contentSize : 100 ,
235+ transferredSize : 100 ,
236+ dest : "/dst" ,
237+ wantNoContain : "(transferred" ,
238+ },
239+ {
240+ name : "both zero" ,
241+ contentSize : 0 ,
242+ transferredSize : 0 ,
243+ dest : "ctr:/dst" ,
244+ wantContains : "Successfully copied 0B to ctr:/dst" ,
245+ wantNoContain : "(transferred" ,
246+ },
247+ }
248+ for _ , tc := range tests {
249+ t .Run (tc .name , func (t * testing.T ) {
250+ got := copySummary (tc .contentSize , tc .transferredSize , tc .dest )
251+ if tc .wantContains != "" {
252+ assert .Check (t , is .Contains (got , tc .wantContains ))
253+ }
254+ if tc .wantNoContain != "" {
255+ assert .Check (t , ! strings .Contains (got , tc .wantNoContain ), "unexpected substring %q in %q" , tc .wantNoContain , got )
256+ }
257+ })
258+ }
259+ }
260+
261+ func TestCopyFromContainerReportsFileSize (t * testing.T ) {
262+ // The file content is "hello" (5 bytes), but the TAR archive wrapping
263+ // it is much larger due to headers and padding. The success message
264+ // should report the actual file size (5B), not the TAR stream size.
265+ srcDir := fs .NewDir (t , "cp-test-from" ,
266+ fs .WithFile ("file1" , "hello" ))
267+
268+ destDir := fs .NewDir (t , "cp-test-from-dest" )
269+
270+ const fileSize int64 = 5
271+ fakeCli := test .NewFakeCli (& fakeClient {
272+ containerCopyFromFunc : func (ctr , srcPath string ) (client.CopyFromContainerResult , error ) {
273+ readCloser , err := archive .Tar (srcDir .Path (), compression .None )
274+ return client.CopyFromContainerResult {
275+ Content : readCloser ,
276+ Stat : container.PathStat {
277+ Name : "file1" ,
278+ Size : fileSize ,
279+ },
280+ }, err
281+ },
282+ })
283+ err := runCopy (context .TODO (), fakeCli , copyOptions {
284+ source : "container:/file1" ,
285+ destination : destDir .Path (),
286+ })
287+ assert .NilError (t , err )
288+ errOut := fakeCli .ErrBuffer ().String ()
289+ assert .Check (t , is .Contains (errOut , "Successfully copied 5B" ))
290+ assert .Check (t , is .Contains (errOut , "(transferred" ))
291+ }
292+
293+ func TestCopyToContainerReportsFileSize (t * testing.T ) {
294+ // Create a temp file with known content ("hello" = 5 bytes).
295+ // The TAR archive sent to the container is larger, but the success
296+ // message should report the actual content size.
297+ srcFile := fs .NewFile (t , "cp-test-to" , fs .WithContent ("hello" ))
298+
299+ fakeCli := test .NewFakeCli (& fakeClient {
300+ containerStatPathFunc : func (containerID , path string ) (client.ContainerStatPathResult , error ) {
301+ return client.ContainerStatPathResult {
302+ Stat : container.PathStat {
303+ Name : "tmp" ,
304+ Mode : os .ModeDir | 0o755 ,
305+ },
306+ }, nil
307+ },
308+ containerCopyToFunc : func (containerID string , options client.CopyToContainerOptions ) (client.CopyToContainerResult , error ) {
309+ _ , _ = io .Copy (io .Discard , options .Content )
310+ return client.CopyToContainerResult {}, nil
311+ },
312+ })
313+ err := runCopy (context .TODO (), fakeCli , copyOptions {
314+ source : srcFile .Path (),
315+ destination : "container:/tmp" ,
316+ })
317+ assert .NilError (t , err )
318+ errOut := fakeCli .ErrBuffer ().String ()
319+ assert .Check (t , is .Contains (errOut , "Successfully copied 5B" ))
320+ assert .Check (t , is .Contains (errOut , "(transferred" ))
321+ }
322+
323+ func TestCopyToContainerReportsEmptyFileSize (t * testing.T ) {
324+ srcFile := fs .NewFile (t , "cp-test-empty" , fs .WithContent ("" ))
325+
326+ fakeCli := test .NewFakeCli (& fakeClient {
327+ containerStatPathFunc : func (containerID , path string ) (client.ContainerStatPathResult , error ) {
328+ return client.ContainerStatPathResult {
329+ Stat : container.PathStat {
330+ Name : "tmp" ,
331+ Mode : os .ModeDir | 0o755 ,
332+ },
333+ }, nil
334+ },
335+ containerCopyToFunc : func (containerID string , options client.CopyToContainerOptions ) (client.CopyToContainerResult , error ) {
336+ _ , _ = io .Copy (io .Discard , options .Content )
337+ return client.CopyToContainerResult {}, nil
338+ },
339+ })
340+ err := runCopy (context .TODO (), fakeCli , copyOptions {
341+ source : srcFile .Path (),
342+ destination : "container:/tmp" ,
343+ })
344+ assert .NilError (t , err )
345+ errOut := fakeCli .ErrBuffer ().String ()
346+ assert .Check (t , is .Contains (errOut , "Successfully copied 0B" ))
347+ assert .Check (t , is .Contains (errOut , "(transferred" ))
348+ }
349+
350+ func TestCopyToContainerReportsDirectorySize (t * testing.T ) {
351+ // Create a temp directory with files "aaa" (3 bytes) + "bbb" (3 bytes) = 6 bytes.
352+ // The TAR archive is much larger, but the success message should report 6B.
353+ srcDir := fs .NewDir (t , "cp-test-dir" ,
354+ fs .WithFile ("aaa" , "aaa" ),
355+ fs .WithFile ("bbb" , "bbb" ),
356+ )
357+
358+ fakeCli := test .NewFakeCli (& fakeClient {
359+ containerStatPathFunc : func (containerID , path string ) (client.ContainerStatPathResult , error ) {
360+ return client.ContainerStatPathResult {
361+ Stat : container.PathStat {
362+ Name : "tmp" ,
363+ Mode : os .ModeDir | 0o755 ,
364+ },
365+ }, nil
366+ },
367+ containerCopyToFunc : func (containerID string , options client.CopyToContainerOptions ) (client.CopyToContainerResult , error ) {
368+ _ , _ = io .Copy (io .Discard , options .Content )
369+ return client.CopyToContainerResult {}, nil
370+ },
371+ })
372+ err := runCopy (context .TODO (), fakeCli , copyOptions {
373+ source : srcDir .Path () + string (os .PathSeparator ),
374+ destination : "container:/tmp" ,
375+ })
376+ assert .NilError (t , err )
377+ errOut := fakeCli .ErrBuffer ().String ()
378+ assert .Check (t , is .Contains (errOut , "Successfully copied 6B" ))
379+ assert .Check (t , is .Contains (errOut , "(transferred" ))
380+ }
381+
382+ func TestCopyFromContainerReportsDirectorySize (t * testing.T ) {
383+ // When copying a directory from a container, cpRes.Stat.Mode.IsDir() is true,
384+ // so reportedSize falls back to copiedSize (the tar stream bytes).
385+ srcDir := fs .NewDir (t , "cp-test-fromdir" ,
386+ fs .WithFile ("file1" , "hello" ))
387+
388+ destDir := fs .NewDir (t , "cp-test-fromdir-dest" )
389+
390+ fakeCli := test .NewFakeCli (& fakeClient {
391+ containerCopyFromFunc : func (ctr , srcPath string ) (client.CopyFromContainerResult , error ) {
392+ readCloser , err := archive .Tar (srcDir .Path (), compression .None )
393+ return client.CopyFromContainerResult {
394+ Content : readCloser ,
395+ Stat : container.PathStat {
396+ Name : "mydir" ,
397+ Mode : os .ModeDir | 0o755 ,
398+ },
399+ }, err
400+ },
401+ })
402+ err := runCopy (context .TODO (), fakeCli , copyOptions {
403+ source : "container:/mydir" ,
404+ destination : destDir .Path (),
405+ })
406+ assert .NilError (t , err )
407+ errOut := fakeCli .ErrBuffer ().String ()
408+ assert .Check (t , is .Contains (errOut , "Successfully copied" ))
409+ // For directories from container, content size is unknown so
410+ // reportedSize == copiedSize and "(transferred ...)" is omitted.
411+ assert .Check (t , ! strings .Contains (errOut , "(transferred" ))
412+ }
413+
414+ func TestCopyToContainerStdinReportsTransferredSize (t * testing.T ) {
415+ // When copying from stdin, content size is unknown.
416+ // The message should report transferred bytes without "(transferred ...)".
417+ r , w , _ := os .Pipe ()
418+ _ , _ = w .WriteString ("some data from stdin" )
419+ w .Close ()
420+ oldStdin := os .Stdin
421+ os .Stdin = r
422+ t .Cleanup (func () { os .Stdin = oldStdin })
423+
424+ fakeCli := test .NewFakeCli (& fakeClient {
425+ containerStatPathFunc : func (containerID , path string ) (client.ContainerStatPathResult , error ) {
426+ return client.ContainerStatPathResult {
427+ Stat : container.PathStat {
428+ Name : "tmp" ,
429+ Mode : os .ModeDir | 0o755 ,
430+ },
431+ }, nil
432+ },
433+ containerCopyToFunc : func (containerID string , options client.CopyToContainerOptions ) (client.CopyToContainerResult , error ) {
434+ _ , _ = io .Copy (io .Discard , options .Content )
435+ return client.CopyToContainerResult {}, nil
436+ },
437+ })
438+ err := runCopy (context .TODO (), fakeCli , copyOptions {
439+ source : "-" ,
440+ destination : "container:/tmp" ,
441+ })
442+ assert .NilError (t , err )
443+ errOut := fakeCli .ErrBuffer ().String ()
444+ assert .Check (t , is .Contains (errOut , "Successfully copied" ))
445+ // stdin has no content size, so reportedSize == copiedSize and
446+ // "(transferred ...)" should not appear.
447+ assert .Check (t , ! strings .Contains (errOut , "(transferred" ))
448+ }
0 commit comments