diff --git a/Cargo.lock b/Cargo.lock index 7cd6ecb8..eef2af7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -218,9 +218,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "bytemuck" @@ -250,9 +250,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" -version = "1.2.49" +version = "1.2.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +checksum = "9f50d563227a1c37cc0a263f64eca3334388c01c5e4c4861a9def205c614383c" dependencies = [ "find-msvc-tools", "shlex", @@ -1033,13 +1033,13 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libredox" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +checksum = "df15f6eac291ed1cf25865b1ee60399f57e7c227e7f51bdbd4c5270396a9ed50" dependencies = [ "bitflags 2.10.0", "libc", - "redox_syscall", + "redox_syscall 0.6.0", ] [[package]] @@ -1185,9 +1185,9 @@ dependencies = [ [[package]] name = "ntapi" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +checksum = "c70f219e21142367c70c0b30c6a9e3a14d55b4d12a204d897fbec83a0363f081" dependencies = [ "winapi", ] @@ -1297,7 +1297,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link", ] @@ -1424,9 +1424,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "f59e70c4aef1e55797c2e8fd94a4f2a973fc972cfde0e0b05f683667b0cd39dd" [[package]] name = "portable-atomic-util" @@ -1994,6 +1994,15 @@ dependencies = [ "bitflags 2.10.0", ] +[[package]] +name = "redox_syscall" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec96166dafa0886eb81fe1c0a388bece180fbef2135f97c1e2cf8302e74b43b5" +dependencies = [ + "bitflags 2.10.0", +] + [[package]] name = "redox_users" version = "0.4.6" @@ -2508,18 +2517,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.3" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.23.9" +version = "0.23.10+spec-1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d7cbc3b4b49633d57a0509303158ca50de80ae32c265093b24c414705807832" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" dependencies = [ "indexmap", "toml_datetime", @@ -2529,9 +2538,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.4" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" dependencies = [ "winnow", ] @@ -2544,9 +2553,9 @@ checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" [[package]] name = "tracing" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-core", @@ -2554,9 +2563,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.35" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", diff --git a/tree/du.rs b/tree/du.rs index b94eed20..29ba1a3a 100644 --- a/tree/du.rs +++ b/tree/du.rs @@ -6,15 +6,14 @@ // file in the root directory of this project. // SPDX-License-Identifier: MIT // -// TODO: -// - implement -H, -L, -x -// use clap::Parser; use gettextrs::{bind_textdomain_codeset, gettext, setlocale, textdomain, LocaleCategory}; -use std::os::unix::fs::MetadataExt; -use std::path::Path; -use std::{fs, io}; +use std::{ + cell::RefCell, + collections::{HashSet, LinkedList}, + os::unix::fs::MetadataExt, +}; /// du - estimate file space usage #[derive(Parser)] @@ -56,50 +55,125 @@ fn calc_size(kilo: bool, size: u64) -> u64 { } } -fn print_pathinfo(args: &Args, filename: &str, size: u64, toplevel: bool) { - if args.sum && !toplevel { - return; - } - - // print the file size - println!("{}\t{}", size, filename); +struct Node { + total_blocks: u64, } -fn du_cli_arg( - args: &Args, - filename: &str, - total: &mut u64, - toplevel: bool, -) -> Result<(), io::Error> { - let path = Path::new(filename); - let metadata = fs::metadata(path)?; - - // recursively process directories - if metadata.is_dir() { - let mut sub_total = 0; - for entry in fs::read_dir(path)? { - let entry = entry?; - let path = entry.path(); - let filename = path.to_str().unwrap(); - if let Err(e) = du_cli_arg(args, filename, &mut sub_total, false) { - eprintln!("{}: {}", filename, e); +fn du_impl(args: &Args, filename: &str) -> bool { + let terminate = RefCell::new(false); + let stack: RefCell> = RefCell::new(LinkedList::new()); + // Track seen (dev, ino) pairs for hard link deduplication + let seen: RefCell> = RefCell::new(HashSet::new()); + // Track initial device for -x option + let initial_dev: RefCell> = RefCell::new(None); + + let path = std::path::Path::new(filename); + + ftw::traverse_directory( + path, + |entry| { + if *terminate.borrow() { + return Ok(false); } - } - print_pathinfo(args, filename, sub_total, toplevel); - - *total += sub_total; - return Ok(()); - } - // print the file size - let size = calc_size(args.kilo, metadata.blocks()); - *total += size; + let md = entry + .metadata() + .expect("ftw::traverse_directory yielded an entry without metadata"); + + // -x: skip files on different filesystems + if args.one_fs { + let dev = md.dev(); + let mut init_dev = initial_dev.borrow_mut(); + if init_dev.is_none() { + *init_dev = Some(dev); + } else if Some(dev) != *init_dev { + // Different filesystem, skip this entry + return Ok(false); + } + } - if args.all { - print_pathinfo(args, filename, size, toplevel); - } + let is_dir = md.is_dir(); + + // Handle hard link deduplication: files with nlink > 1 should only be counted once + let size = if !is_dir && md.nlink() > 1 { + let key = (md.dev(), md.ino()); + let mut seen_set = seen.borrow_mut(); + if seen_set.contains(&key) { + // Already counted this file, use size 0 + 0 + } else { + seen_set.insert(key); + calc_size(args.kilo, md.blocks()) + } + } else { + calc_size(args.kilo, md.blocks()) + }; + + let mut stack = stack.borrow_mut(); + + // Check if this is the original file operand (root of traversal) + let is_root = entry.path().as_inner() == path; + + if is_dir { + // For directories, push onto stack. Don't add to parent here - + // the directory's total will be added when we exit the directory. + stack.push_back(Node { total_blocks: size }); + } else { + // For files, add size to parent directory's total + if let Some(back) = stack.back_mut() { + back.total_blocks += size; + } + // For non-directories: + // - Always print if it's the original file operand (POSIX BSD behavior) + // - Print if -a is specified + // - Don't print with -s (handled separately) + let display_size = size; + if is_root { + // File operands are always listed + println!("{}\t{}", display_size, entry.path()); + } else if args.all && !args.sum { + // -a: report all files within directories + println!("{}\t{}", display_size, entry.path()); + } + } - Ok(()) + Ok(is_dir) + }, + |entry| { + let mut stack = stack.borrow_mut(); + if let Some(node) = stack.pop_back() { + let size = node.total_blocks; + + // Recursively sum the block size + if let Some(back) = stack.back_mut() { + back.total_blocks += size; + } + + if args.sum { + // -s: report only the total sum for the file operand + let entry_path = entry.path(); + if entry_path.as_inner() == path { + println!("{}\t{}", size, entry_path); + } + } else { + println!("{}\t{}", size, entry.path()); + } + } + Ok(()) + }, + |_entry, error| { + *terminate.borrow_mut() = true; + eprintln!("du: {}", error.inner()); + }, + ftw::TraverseDirectoryOpts { + follow_symlinks_on_args: args.follow_cli, + follow_symlinks: args.dereference, + ..Default::default() + }, + ); + + let failed = *terminate.borrow(); + !failed } fn main() -> Result<(), Box> { @@ -114,13 +188,11 @@ fn main() -> Result<(), Box> { args.files.push(".".to_string()); } let mut exit_code = 0; - let mut total = 0; // apply the group to each file for filename in &args.files { - if let Err(e) = du_cli_arg(&args, filename, &mut total, true) { + if !du_impl(&args, filename) { exit_code = 1; - eprintln!("{}: {}", filename, e); } } diff --git a/tree/tests/du/mod.rs b/tree/tests/du/mod.rs new file mode 100644 index 00000000..598584ba --- /dev/null +++ b/tree/tests/du/mod.rs @@ -0,0 +1,391 @@ +// +// Copyright (c) 2024 Hemi Labs, Inc. +// +// This file is part of the posixutils-rs project covered under +// the MIT License. For the full license text, please see the LICENSE +// file in the root directory of this project. +// SPDX-License-Identifier: MIT +// + +use plib::testing::{run_test, run_test_with_checker, TestPlan}; +use std::{fs, io::Write, os::unix::fs::MetadataExt, process::Output}; + +fn du_test(args: &[&str], expected_output: &str, expected_error: &str, expected_exit_code: i32) { + let str_args: Vec = args.iter().map(|s| String::from(*s)).collect(); + + run_test(TestPlan { + cmd: String::from("du"), + args: str_args, + stdin_data: String::new(), + expected_out: String::from(expected_output), + expected_err: String::from(expected_error), + expected_exit_code, + }); +} + +fn du_test_with_checker(args: &[&str], checker: impl FnMut(&TestPlan, &Output)) { + let str_args: Vec = args.iter().map(|s| String::from(*s)).collect(); + + run_test_with_checker( + TestPlan { + cmd: String::from("du"), + args: str_args, + stdin_data: String::new(), + expected_out: String::new(), + expected_err: String::new(), + expected_exit_code: 0, + }, + checker, + ); +} + +fn du_test_with_checker_and_exit( + args: &[&str], + expected_exit_code: i32, + checker: impl FnMut(&TestPlan, &Output), +) { + let str_args: Vec = args.iter().map(|s| String::from(*s)).collect(); + + run_test_with_checker( + TestPlan { + cmd: String::from("du"), + args: str_args, + stdin_data: String::new(), + expected_out: String::new(), + expected_err: String::new(), + expected_exit_code, + }, + checker, + ); +} + +// Helper to sort output lines for comparison (order may vary) +fn sort_lines(s: &str) -> Vec { + let mut lines: Vec = s.lines().map(|s| s.to_string()).collect(); + lines.sort(); + lines +} + +// Partial port of coreutils/tests/du/basic.sh +// Omits the nonstandard --block-size and -S options +#[test] +fn test_du_basic() { + let test_dir = &format!("{}/test_du_basic", env!("CARGO_TARGET_TMPDIR")); + + let _ = fs::remove_dir_all(test_dir); + + let a = &format!("{test_dir}/a"); + let a_b = &format!("{test_dir}/a/b"); + let d = &format!("{test_dir}/d"); + let d_sub = &format!("{test_dir}/d/sub"); + + let a_b_f = &format!("{a_b}/F"); + let d_1 = &format!("{d}/1"); + let d_sub_2 = &format!("{d_sub}/2"); + + fs::create_dir(test_dir).unwrap(); + for dir in [a_b, d, d_sub] { + fs::create_dir_all(dir).unwrap(); + } + + { + // Create a > 257 bytes file + let mut file1 = fs::File::create(a_b_f).unwrap(); + write!(file1, "{:>257}", "x").unwrap(); + + // Create a 4 KiB file + let mut file2 = fs::File::create(d_1).unwrap(); + write!(file2, "{:>4096}", "x").unwrap(); + + fs::copy(d_1, d_sub_2).unwrap(); + } + + // Manually calculate the block sizes for directory a + let [size_abf, mut size_ab, mut size_a] = + [a_b_f, a_b, a].map(|filename| fs::metadata(filename).unwrap().blocks()); + size_ab += size_abf; + size_a += size_ab; + + // Test -a: should print files AND directories + du_test( + &["-a", a], + &format!( + "{size_abf}\t{a_b_f}\ + \n{size_ab}\t{a_b}\ + \n{size_a}\t{a}\n" + ), + "", + 0, + ); + + // Test -s: should only print the total + du_test(&["-s", a], &format!("{size_a}\t{a}\n"), "", 0); + + // Manually calculate the block sizes for directory d + let [size_dsub2, mut size_dsub, size_d1, mut size_d] = + [d_sub_2, d_sub, d_1, d].map(|filename| fs::metadata(filename).unwrap().blocks()); + size_dsub += size_dsub2; + size_d += size_d1 + size_dsub; + + du_test_with_checker(&["-a", d], |_, output| { + // Order of d/1 vs d/sub is not guaranteed, so compare sorted lines + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + let expected = format!( + "{size_dsub2}\t{d_sub_2}\ + \n{size_dsub}\t{d_sub}\ + \n{size_d1}\t{d_1}\ + \n{size_d}\t{d}\n" + ); + assert_eq!(sort_lines(&stdout), sort_lines(&expected)); + }); + + fs::remove_dir_all(test_dir).unwrap(); +} + +// Test default behavior (no -a): should only print directories +#[test] +fn test_du_default_no_files() { + let test_dir = &format!("{}/test_du_default", env!("CARGO_TARGET_TMPDIR")); + + let _ = fs::remove_dir_all(test_dir); + + let subdir = &format!("{test_dir}/subdir"); + let file1 = &format!("{test_dir}/file1"); + let file2 = &format!("{subdir}/file2"); + + fs::create_dir_all(subdir).unwrap(); + fs::write(file1, "hello").unwrap(); + fs::write(file2, "world").unwrap(); + + // Without -a, should only print directories (subdir and test_dir) + du_test_with_checker(&[test_dir], |_, output| { + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + // Should NOT contain file1 or file2 + assert!( + !stdout.contains("file1"), + "Default du should not list files" + ); + assert!( + !stdout.contains("file2"), + "Default du should not list files" + ); + // Should contain subdir and test_dir + assert!(stdout.contains("subdir"), "Default du should list subdir"); + assert!( + stdout.contains("test_du_default\n"), + "Default du should list root" + ); + }); + + fs::remove_dir_all(test_dir).unwrap(); +} + +// Test -k option (1024-byte units) +#[test] +fn test_du_kilo() { + let test_dir = &format!("{}/test_du_kilo", env!("CARGO_TARGET_TMPDIR")); + + let _ = fs::remove_dir_all(test_dir); + fs::create_dir(test_dir).unwrap(); + + let file1 = &format!("{test_dir}/file1"); + fs::write(file1, "x".repeat(2048)).unwrap(); + + let blocks = fs::metadata(file1).unwrap().blocks(); + let size_512 = blocks; + let size_1024 = blocks / 2; + + // Without -k: 512-byte units + du_test_with_checker(&["-a", test_dir], |_, output| { + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + assert!( + stdout.contains(&format!("{size_512}\t")), + "Expected 512-byte units" + ); + }); + + // With -k: 1024-byte units + du_test_with_checker(&["-ak", test_dir], |_, output| { + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + assert!( + stdout.contains(&format!("{size_1024}\t")), + "Expected 1024-byte units" + ); + }); + + fs::remove_dir_all(test_dir).unwrap(); +} + +// Test error handling for non-existent file +#[test] +fn test_du_nonexistent() { + let nonexistent = "/tmp/posixutils_du_nonexistent_file_xyz123"; + let _ = fs::remove_file(nonexistent); // Ensure it doesn't exist + + du_test_with_checker_and_exit(&[nonexistent], 1, |_, output| { + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + assert!( + stderr.contains("du:"), + "Should print error message for nonexistent file" + ); + assert_eq!(output.status.code(), Some(1), "Should exit with code 1"); + }); +} + +// Test non-directory file operand (should always be listed per POSIX BSD behavior) +#[test] +fn test_du_file_operand() { + let test_dir = &format!("{}/test_du_file_op", env!("CARGO_TARGET_TMPDIR")); + + let _ = fs::remove_dir_all(test_dir); + fs::create_dir(test_dir).unwrap(); + + let file1 = &format!("{test_dir}/file1"); + fs::write(file1, "test content").unwrap(); + + let size = fs::metadata(file1).unwrap().blocks(); + + // File operand should always be listed (even without -a) + du_test(&[file1], &format!("{size}\t{file1}\n"), "", 0); + + fs::remove_dir_all(test_dir).unwrap(); +} + +// Test symlink handling with -H (follow cmdline symlinks only) +#[test] +fn test_du_symlink_h() { + let test_dir = &format!("{}/test_du_symlink_h", env!("CARGO_TARGET_TMPDIR")); + + let _ = fs::remove_dir_all(test_dir); + fs::create_dir(test_dir).unwrap(); + + let target_dir = &format!("{test_dir}/target"); + let link = &format!("{test_dir}/link"); + let target_file = &format!("{target_dir}/file"); + + fs::create_dir(target_dir).unwrap(); + fs::write(target_file, "x".repeat(1024)).unwrap(); + std::os::unix::fs::symlink(target_dir, link).unwrap(); + + let target_size = + fs::metadata(target_dir).unwrap().blocks() + fs::metadata(target_file).unwrap().blocks(); + + // With -H, following the symlink on cmdline should report target's size + du_test_with_checker(&["-sH", link], |_, output| { + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + // Should contain the target directory's size, not just symlink size + let reported_size: u64 = stdout.split('\t').next().unwrap().trim().parse().unwrap(); + assert_eq!( + reported_size, target_size, + "With -H, should report target directory size" + ); + }); + + // Without -H, should report symlink's own size (just the link, not followed) + du_test_with_checker(&["-s", link], |_, output| { + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + let reported_size: u64 = stdout.split('\t').next().unwrap().trim().parse().unwrap(); + // Symlink size is typically very small (just the link itself) + let link_size = fs::symlink_metadata(link).unwrap().blocks(); + assert_eq!( + reported_size, link_size, + "Without -H, should report symlink size only" + ); + }); + + fs::remove_dir_all(test_dir).unwrap(); +} + +// Test symlink handling with -L (follow all symlinks) +#[test] +fn test_du_symlink_l() { + let test_dir = &format!("{}/test_du_symlink_l", env!("CARGO_TARGET_TMPDIR")); + + let _ = fs::remove_dir_all(test_dir); + fs::create_dir(test_dir).unwrap(); + + let target_file = &format!("{test_dir}/target"); + let subdir = &format!("{test_dir}/subdir"); + let link_in_dir = &format!("{subdir}/link"); + + fs::create_dir(subdir).unwrap(); + fs::write(target_file, "x".repeat(2048)).unwrap(); + std::os::unix::fs::symlink(target_file, link_in_dir).unwrap(); + + let target_size = fs::metadata(target_file).unwrap().blocks(); + let link_size = fs::symlink_metadata(link_in_dir).unwrap().blocks(); + + // With -L, symlinks inside directories should be followed + du_test_with_checker(&["-aL", subdir], |_, output| { + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + // The link should report target file's size + for line in stdout.lines() { + if line.ends_with("/link") { + let reported_size: u64 = line.split('\t').next().unwrap().trim().parse().unwrap(); + assert_eq!( + reported_size, target_size, + "With -L, should report target file size for symlink" + ); + } + } + }); + + // Without -L, symlinks inside directories should NOT be followed + du_test_with_checker(&["-a", subdir], |_, output| { + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + for line in stdout.lines() { + if line.ends_with("/link") { + let reported_size: u64 = line.split('\t').next().unwrap().trim().parse().unwrap(); + assert_eq!( + reported_size, link_size, + "Without -L, should report symlink size only" + ); + } + } + }); + + fs::remove_dir_all(test_dir).unwrap(); +} + +// Test hard link deduplication +#[test] +fn test_du_hardlink() { + let test_dir = &format!("{}/test_du_hardlink", env!("CARGO_TARGET_TMPDIR")); + + let _ = fs::remove_dir_all(test_dir); + fs::create_dir(test_dir).unwrap(); + + let file1 = &format!("{test_dir}/file1"); + let file2 = &format!("{test_dir}/file2"); + + // Create a file and a hard link to it + fs::write(file1, "x".repeat(4096)).unwrap(); + fs::hard_link(file1, file2).unwrap(); + + let file_size = fs::metadata(file1).unwrap().blocks(); + let dir_size = fs::metadata(test_dir).unwrap().blocks(); + + // With hard links, the file should only be counted once + // Total should be: dir_size + file_size (not dir_size + 2*file_size) + let expected_total = dir_size + file_size; + + du_test_with_checker(&["-s", test_dir], |_, output| { + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + let reported_size: u64 = stdout.split('\t').next().unwrap().trim().parse().unwrap(); + assert_eq!( + reported_size, expected_total, + "Hard links should only be counted once. Expected {}, got {}", + expected_total, reported_size + ); + }); + + // With -a, both files should still be listed (but counted only once) + du_test_with_checker(&["-a", test_dir], |_, output| { + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + // Both file1 and file2 should appear in output + assert!(stdout.contains("file1"), "file1 should be listed with -a"); + assert!(stdout.contains("file2"), "file2 should be listed with -a"); + }); + + fs::remove_dir_all(test_dir).unwrap(); +} diff --git a/tree/tests/tree-tests.rs b/tree/tests/tree-tests.rs index 8c8491ec..4f628285 100644 --- a/tree/tests/tree-tests.rs +++ b/tree/tests/tree-tests.rs @@ -11,6 +11,7 @@ mod chgrp; mod chmod; mod chown; mod cp; +mod du; mod link; mod ls; mod mkdir;