@@ -9,14 +9,16 @@ local path = mods.path
99local isdir = mods .is .dir
1010local make_tmp_dir = helpers .make_tmp_dir
1111local tmpname = helpers .tmpname
12- local join = mods .path .join
12+ local join = path .join
13+ local dirname = path .dirname
14+
1315local fmt = string.format
1416
1517describe (" mods.fs" , function ()
1618 local is_unix = not mods .runtime .is_windows
1719 local cwd = path .cwd () --[[ @as string]]
18- local readme_file = path . join (cwd , " README.md" )
19- local spec_file = path . join (cwd , " spec" , " fs_spec.lua" )
20+ local readme_file = join (cwd , " README.md" )
21+ local spec_file = join (cwd , " spec" , " fs_spec.lua" )
2022
2123 for _ , fname in ipairs ({ " getsize" , " getatime" , " getmtime" , " getctime" }) do
2224 it (fmt (" %s() returns a number for an existing path" , fname ), function ()
@@ -285,6 +287,181 @@ describe("mods.fs", function()
285287 end )
286288 end )
287289
290+ describe (" cp()" , function ()
291+ it (" copies binary files" , function ()
292+ local root = make_tmp_dir ()
293+ local src = join (root , " src.bin" )
294+ local dst = join (root , " dst.bin" )
295+ local body = " a\0 b\1 c\255 z"
296+
297+ assert .is_true (fs .write_bytes (src , body ))
298+ assert .is_true (fs .cp (src , dst ))
299+ assert .are_equal (body , fs .read_bytes (dst ))
300+
301+ assert .is_true (fs .rm (root , true ))
302+ end )
303+
304+ it (" copies files" , function ()
305+ local root = make_tmp_dir ()
306+ local src = join (root , " src.txt" )
307+ local dst = join (root , " dst.txt" )
308+
309+ assert .is_true (fs .write_text (src , " abc" ))
310+ assert .is_true (fs .cp (src , dst ))
311+ assert .are_equal (" abc" , fs .read_text (dst ))
312+
313+ assert .is_true (fs .rm (root , true ))
314+ end )
315+
316+ it (" overwrites an existing destination file" , function ()
317+ local root = make_tmp_dir ()
318+ local src = join (root , " src.txt" )
319+ local dst = join (root , " dst.txt" )
320+
321+ assert .is_true (fs .write_text (src , " new" ))
322+ assert .is_true (fs .write_text (dst , " old" ))
323+
324+ assert .is_true (fs .cp (src , dst ))
325+ assert .are_equal (" new" , fs .read_text (dst ))
326+
327+ assert .is_true (fs .rm (root , true ))
328+ end )
329+
330+ it (" copies empty directories" , function ()
331+ local root = make_tmp_dir ()
332+ local src = join (root , " src" )
333+ local empty = join (src , " empty" )
334+ local dst = join (root , " copy" )
335+
336+ assert .is_true (fs .mkdir (empty , true ))
337+
338+ assert .is_true (fs .cp (src , dst ))
339+ assert .is_true (isdir (join (dst , " empty" )))
340+
341+ assert .is_true (fs .rm (root , true ))
342+ end )
343+
344+ it (" copies directories recursively" , function ()
345+ local root = make_tmp_dir ()
346+ local src = join (root , " src" )
347+ local top_level = join (src , " top.txt" )
348+ local nested_dir = join (src , " deep" )
349+ local nested = join (nested_dir , " nested.txt" )
350+ local dst = join (root , " copy" )
351+
352+ assert .is_true (fs .mkdir (nested_dir , true ))
353+ assert .is_true (fs .write_text (top_level , " abc" ))
354+ assert .is_true (fs .write_text (nested , " xyz" ))
355+
356+ assert .is_true (fs .cp (src , dst ))
357+ assert .are_equal (" abc" , fs .read_text (join (dst , " top.txt" )))
358+ assert .is_true (fs .exists (join (dst , " deep" )))
359+ assert .are_equal (" xyz" , fs .read_text (join (dst , " deep" , " nested.txt" )))
360+
361+ assert .is_true (fs .rm (root , true ))
362+ end )
363+
364+ it (" merges into an existing destination directory" , function ()
365+ local root = make_tmp_dir ()
366+ local src = join (root , " src" )
367+ local nested_dir = join (src , " deep" )
368+ local src_file = join (src , " top.txt" )
369+ local nested = join (nested_dir , " nested.txt" )
370+ local dst = join (root , " copy" )
371+ local keep = join (dst , " keep.txt" )
372+
373+ assert .is_true (fs .mkdir (nested_dir , true ))
374+ assert .is_true (fs .mkdir (dst , true ))
375+ assert .is_true (fs .write_text (src_file , " abc" ))
376+ assert .is_true (fs .write_text (nested , " xyz" ))
377+ assert .is_true (fs .write_text (keep , " keep" ))
378+
379+ assert .is_true (fs .cp (src , dst ))
380+ assert .are_equal (" keep" , fs .read_text (keep ))
381+ assert .are_equal (" abc" , fs .read_text (join (dst , " top.txt" )))
382+ assert .are_equal (" xyz" , fs .read_text (join (dst , " deep" , " nested.txt" )))
383+
384+ assert .is_true (fs .rm (root , true ))
385+ end )
386+
387+ it (" fails when the source is missing" , function ()
388+ local root = make_tmp_dir ()
389+ local missing = join (root , " missing.txt" )
390+ local dst = join (root , " dst.txt" )
391+
392+ local ok , errmsg , errcode = fs .cp (missing , dst )
393+ assert .are_same ({ " nil" , " string" , " number" }, { type (ok ), type (errmsg ), type (errcode ) })
394+
395+ assert .is_true (fs .rm (root , true ))
396+ end )
397+
398+ it (" fails when the destination parent is missing" , function ()
399+ local root = make_tmp_dir ()
400+ local src = join (root , " src.txt" )
401+ local dst = join (root , " missing" , " dst.txt" )
402+
403+ assert .is_true (fs .write_text (src , " abc" ))
404+
405+ local ok , errmsg , errcode = fs .cp (src , dst )
406+ assert .are_same ({ " nil" , " string" , " number" }, { type (ok ), type (errmsg ), type (errcode ) })
407+
408+ assert .is_true (fs .rm (root , true ))
409+ end )
410+
411+ it (" fails when a nested destination path conflicts with a file" , function ()
412+ local root = make_tmp_dir ()
413+ local src = join (root , " src" )
414+ local nested_dir = join (src , " deep" )
415+ local nested = join (nested_dir , " nested.txt" )
416+ local dst = join (root , " copy" )
417+ local conflicting = join (dst , " deep" )
418+
419+ assert .is_true (fs .mkdir (nested_dir , true ))
420+ assert .is_true (fs .mkdir (dst , true ))
421+ assert .is_true (fs .write_text (nested , " xyz" ))
422+ assert .is_true (fs .write_text (conflicting , " not a dir" ))
423+
424+ local ok , errmsg , errcode = fs .cp (src , dst )
425+ assert .are_same ({ " nil" , " string" , " number" }, { type (ok ), type (errmsg ), type (errcode ) })
426+
427+ assert .is_true (fs .rm (root , true ))
428+ end )
429+
430+ it (" fails when copying a directory onto itself" , function ()
431+ local root = make_tmp_dir ()
432+ local src = join (root , " src" )
433+ local nested = join (src , " nested.txt" )
434+
435+ assert .is_true (fs .mkdir (src , true ))
436+ assert .is_true (fs .write_text (nested , " xyz" ))
437+
438+ local ok , errmsg , errcode = fs .cp (src , src )
439+ local msg = " cannot copy a directory into itself or its descendant"
440+ assert .are_same ({ " nil" , msg , " nil" }, { type (ok ), errmsg , type (errcode ) })
441+
442+ assert .are_equal (" xyz" , fs .read_text (nested ))
443+ assert .is_true (fs .rm (root , true ))
444+ end )
445+
446+ it (" fails when copying a directory into its descendant" , function ()
447+ local root = make_tmp_dir ()
448+ local src = join (root , " src" )
449+ local nested = join (src , " nested.txt" )
450+ local dst = join (src , " child" , " copy" )
451+
452+ assert .is_true (fs .mkdir (src , true ))
453+ assert .is_true (fs .write_text (nested , " xyz" ))
454+
455+ local ok , errmsg , errcode = fs .cp (src , dst )
456+ local msg = " cannot copy a directory into itself or its descendant"
457+ assert .are_same ({ " nil" , msg , " nil" }, { type (ok ), errmsg , type (errcode ) })
458+
459+ assert .is_false (fs .exists (dst ))
460+ assert .are_equal (" xyz" , fs .read_text (nested ))
461+ assert .is_true (fs .rm (root , true ))
462+ end )
463+ end )
464+
288465 describe (" rm()" , function ()
289466 it (" removes a file without recursive mode" , function ()
290467 local target = tmpname ()
@@ -485,6 +662,7 @@ describe("mods.fs", function()
485662 --- @diagnostic disable : param-type-mismatch , discard-returns , missing-parameter , assign-type-mismatch
486663 it (" errors on invalid argument types" , function ()
487664 -- Argument #1 validation.
665+ assert .has_error (function () fs .cp (false ) end , " bad argument #1 to 'cp' (string expected, got boolean)" )
488666 assert .has_error (function () fs .exists (true ) end , " bad argument #1 to 'exists' (string expected, got boolean)" )
489667 assert .has_error (function () fs .getatime (false ) end , " bad argument #1 to 'getatime' (string expected, got boolean)" )
490668 assert .has_error (function () fs .getctime (0 ) end , " bad argument #1 to 'getctime' (string expected, got number)" )
@@ -500,6 +678,7 @@ describe("mods.fs", function()
500678 assert .has_error (function () fs .write_text ({}) end , " bad argument #1 to 'write_text' (string expected, got table)" )
501679
502680 -- Argument #2 validation.
681+ assert .has_error (function () fs .cp (" a" ) end , " bad argument #2 to 'cp' (string expected, got no value)" )
503682 assert .has_error (function () fs .mkdir (" tmp" , 1 ) end , " bad argument #2 to 'mkdir' (boolean expected, got number)" )
504683 assert .has_error (function () fs .rm (" tmp" , 1 ) end , " bad argument #2 to 'rm' (boolean expected, got number)" )
505684 assert .has_error (function () fs .samefile (readme_file , 123 ) end , " bad argument #2 to 'samefile' (string expected, got number)" )
0 commit comments