Skip to content

Conversation

@kaladron
Copy link
Contributor

Document testing philosophy and expand test suite with better CLI-focused tests and improved error verification.

Changes:

  • Add tests/README.md documenting testing philosophy separating application (CLI interface) from library (archive format) testing concerns
  • Update README.md with Testing section linking to test documentation
  • Add 23 new CLI-focused tests covering:
    • Basic CLI: conflicting operations, missing operation specification
    • Create operations: directory archiving, verbose output, empty archive handling, nonexistent file errors
    • Extract operations: verbose output, directory structure preservation
    • Round-trip tests: single/multiple files, directories, empty files, special characters in filenames
    • Error handling: permission denied, corrupted archives, dash-prefixed filenames with -- separator
    • Verbose output: format verification for create/extract operations
    • CLI parsing: mixed short/long options, option order variations, default overwrite behavior
    • Edge cases: filenames with spaces, large file counts (100 files)
  • Improve all 8 existing tests with better error verification:
    • Add exit code checks and stderr pattern matching
    • Add no_stderr() assertions for successful operations
    • Add sanity checks for archive file sizes
    • Add TODO comments noting exit code mismatches (usage errors currently return 1 instead of documented 64)
  • Reorganize tests with clear section headers:
    1. Basic CLI Tests
    2. Create Operation Tests
    3. Extract Operation Tests
    4. Round-trip Tests
    5. Error Handling and Exit Code Tests
    6. Verbose Output Format Tests
    7. CLI Argument Handling Tests
    8. Edge Case Tests

All 31 tests pass. Test suite now has clear documentation explaining what should be tested at the application level vs library level.

Document testing philosophy and expand test suite with better CLI-focused
tests and improved error verification.

Changes:
- Add tests/README.md documenting testing philosophy separating application
  (CLI interface) from library (archive format) testing concerns
- Update README.md with Testing section linking to test documentation
- Add 23 new CLI-focused tests covering:
  * Basic CLI: conflicting operations, missing operation specification
  * Create operations: directory archiving, verbose output, empty archive
    handling, nonexistent file errors
  * Extract operations: verbose output, directory structure preservation
  * Round-trip tests: single/multiple files, directories, empty files,
    special characters in filenames
  * Error handling: permission denied, corrupted archives, dash-prefixed
    filenames with -- separator
  * Verbose output: format verification for create/extract operations
  * CLI parsing: mixed short/long options, option order variations,
    default overwrite behavior
  * Edge cases: filenames with spaces, large file counts (100 files)
- Improve all 8 existing tests with better error verification:
  * Add exit code checks and stderr pattern matching
  * Add no_stderr() assertions for successful operations
  * Add sanity checks for archive file sizes
  * Add TODO comments noting exit code mismatches (usage errors currently
    return 1 instead of documented 64)
- Reorganize tests with clear section headers:
  1. Basic CLI Tests
  2. Create Operation Tests
  3. Extract Operation Tests
  4. Round-trip Tests
  5. Error Handling and Exit Code Tests
  6. Verbose Output Format Tests
  7. CLI Argument Handling Tests
  8. Edge Case Tests

All 31 tests pass. Test suite now has clear documentation explaining what
should be tested at the application level vs library level.
@kaladron
Copy link
Contributor Author

@sylvestre Here's the rest of the tests that I had held back from the initial commit since it would've been a huge review.

One thing in particular to note: Some of the tests confirm an exit code of 1, which we know is incorrect. I'd like to leave those at the moment (the tests are marked with a TODO) because this is the current behaviour of the code and I'm trying to keep the reviews to a manageable size.

After that, I want to modify the test harness to test against the system tar. That way mistakes like these will be caught. It will be convenient to have a mismatch to prove that the tests are working. Then I will fix any gaps between them.

Thanks!


## Testing

The tar application has a focused testing philosophy that separates concerns between the application (CLI interface, error handling, user experience) and the underlying tar-rs library (archive format correctness, encoding, permissions).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A detail:

Suggested change
The tar application has a focused testing philosophy that separates concerns between the application (CLI interface, error handling, user experience) and the underlying tar-rs library (archive format correctness, encoding, permissions).
The tar application has a focused testing philosophy that separates concerns between the application (CLI interface, error handling, user experience) and the underlying `tar-rs` library (archive format correctness, encoding, permissions).


The `tar` utility is built on top of the `tar-rs` library. Because of this, we split our testing into two distinct areas:

1. **The Library (`tar-rs`)**: This is where we test the nitty-gritty details of the tar format. If you want to verify that permissions are preserved correctly, that long paths are handled according to the UStar spec, or that unicode filenames are encoded properly, those tests belong in `tar-rs/tests/`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if it's a planned refactoring, but currently there is no tar-rs/tests folder.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, sorry. I have too many checkouts, and this was probably be getting confused. Thanks, I will update.

new_ucmd!()
.args(&["-f", "archive.tar"])
.fails()
.code_is(1) // TODO: align with GNU tar by returning exit code 64
Copy link
Contributor

@cakebaker cakebaker Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A detail:

Suggested change
.code_is(1) // TODO: align with GNU tar by returning exit code 64
.code_is(1) // TODO: align with GNU tar by returning exit code 2

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll double check this (and below) but I had tested these by hand for comparison and I think these were the ones returning 64. The GNU tar documentation only document exit 0,1, and 2.

ucmd.args(&["-cf", "archive.tar", "file1.txt"]).succeeds();
ucmd.args(&["-cf", "archive.tar", "file1.txt"])
.succeeds()
.no_stderr();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would use no_output() to indicate there is no output at all.

Suggested change
.no_stderr();
.no_output();

.succeeds();
ucmd.args(&["-cf", "archive.tar", "file1.txt", "file2.txt", "file3.txt"])
.succeeds()
.no_stderr();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.no_stderr();
.no_output();


ucmd.args(&["-cf", "archive.tar", "dir1"])
.succeeds()
.no_stderr();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.no_stderr();
.no_output();

new_ucmd!()
.args(&["-cf", "archive.tar"])
.fails()
.code_is(1) // TODO: propagate usage exit code 64 once empty archive handling is fixed
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.code_is(1) // TODO: propagate usage exit code 64 once empty archive handling is fixed
.code_is(1) // TODO: propagate usage exit code 2 once empty archive handling is fixed

Comment on lines +132 to +134
let (_at, mut ucmd) = at_and_ucmd!();

ucmd.args(&["-cf", "archive.tar", "nonexistent.txt"])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would use new_ucmd!() here.

Suggested change
let (_at, mut ucmd) = at_and_ucmd!();
ucmd.args(&["-cf", "archive.tar", "nonexistent.txt"])
new_ucmd!()
.args(&["-cf", "archive.tar", "nonexistent.txt"])

.current_dir(at.as_string())
.succeeds();
.succeeds()
.no_stderr();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.no_stderr();
.no_output();

Comment on lines +240 to +241
std::fs::remove_dir(at.plus("testdir/subdir")).unwrap();
std::fs::remove_dir(at.plus("testdir")).unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An alternative is rmdir:

Suggested change
std::fs::remove_dir(at.plus("testdir/subdir")).unwrap();
std::fs::remove_dir(at.plus("testdir")).unwrap();
at.rmdir("testdir/subdir");
at.rmdir("testdir");

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still learning, so sorry for the silly question. Why do we prefer the at. method instead of remove_dir?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's nothing wrong with using remove_dir, so feel free to leave it as it is ;-) I mentioned rmdir to show the testing framework provides a convenience function for the same functionality that is a bit shorter.

Comment on lines +263 to +286
#[test]
fn test_roundtrip_single_file() {
let (at, mut ucmd) = at_and_ucmd!();

// Create a file
at.write("file.txt", "test content");

// Create archive
ucmd.args(&["-cf", "archive.tar", "file.txt"]).succeeds();

// Remove original
at.remove("file.txt");

// Extract
new_ucmd!()
.arg("-xf")
.arg(at.plus("archive.tar"))
.current_dir(at.as_string())
.succeeds();

// Verify content is identical
assert!(at.file_exists("file.txt"));
assert_eq!(at.read("file.txt"), "test content");
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is a duplicate of test_extract_single_file

Suggested change
#[test]
fn test_roundtrip_single_file() {
let (at, mut ucmd) = at_and_ucmd!();
// Create a file
at.write("file.txt", "test content");
// Create archive
ucmd.args(&["-cf", "archive.tar", "file.txt"]).succeeds();
// Remove original
at.remove("file.txt");
// Extract
new_ucmd!()
.arg("-xf")
.arg(at.plus("archive.tar"))
.current_dir(at.as_string())
.succeeds();
// Verify content is identical
assert!(at.file_exists("file.txt"));
assert_eq!(at.read("file.txt"), "test content");
}

Comment on lines +288 to +317
#[test]
fn test_roundtrip_multiple_files() {
let (at, mut ucmd) = at_and_ucmd!();

// Create multiple files with different content
at.write("file1.txt", "content one");
at.write("file2.txt", "content two");
at.write("file3.txt", "content three");

// Create archive
ucmd.args(&["-cf", "archive.tar", "file1.txt", "file2.txt", "file3.txt"])
.succeeds();

// Remove originals
at.remove("file1.txt");
at.remove("file2.txt");
at.remove("file3.txt");

// Extract
new_ucmd!()
.arg("-xf")
.arg(at.plus("archive.tar"))
.current_dir(at.as_string())
.succeeds();

// Verify all contents are identical
assert_eq!(at.read("file1.txt"), "content one");
assert_eq!(at.read("file2.txt"), "content two");
assert_eq!(at.read("file3.txt"), "content three");
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is more or less a duplicate of test_extract_multiple_files, with the only difference that this test uses three files.

Suggested change
#[test]
fn test_roundtrip_multiple_files() {
let (at, mut ucmd) = at_and_ucmd!();
// Create multiple files with different content
at.write("file1.txt", "content one");
at.write("file2.txt", "content two");
at.write("file3.txt", "content three");
// Create archive
ucmd.args(&["-cf", "archive.tar", "file1.txt", "file2.txt", "file3.txt"])
.succeeds();
// Remove originals
at.remove("file1.txt");
at.remove("file2.txt");
at.remove("file3.txt");
// Extract
new_ucmd!()
.arg("-xf")
.arg(at.plus("archive.tar"))
.current_dir(at.as_string())
.succeeds();
// Verify all contents are identical
assert_eq!(at.read("file1.txt"), "content one");
assert_eq!(at.read("file2.txt"), "content two");
assert_eq!(at.read("file3.txt"), "content three");
}

}

#[test]
fn test_roundtrip_directory_structure() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, this test is more or less the same as test_extract_directory_structure, with more files and directories.

Comment on lines +371 to +372
at.write("empty1.txt", "");
at.write("empty2.txt", "");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For empty files you can use touch:

Suggested change
at.write("empty1.txt", "");
at.write("empty2.txt", "");
at.touch("empty1.txt");
at.touch("empty2.txt");

}

#[test]
fn test_roundtrip_special_characters_in_names() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about this test, the chosen "special characters" look rather "normal" to me. I would have expected some file names with spaces or non-UTF-8 characters ;-)

Comment on lines +448 to +450
// Make directory read-only
let perms = fs::Permissions::from_mode(0o444);
fs::set_permissions(at.plus("readonly"), perms).unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An alternative is set_readonly:

Suggested change
// Make directory read-only
let perms = fs::Permissions::from_mode(0o444);
fs::set_permissions(at.plus("readonly"), perms).unwrap();
at.set_readonly("readonly");

Comment on lines +503 to +519
#[test]
fn test_verbose_output_format_matches_gnu() {
let (at, mut ucmd) = at_and_ucmd!();

at.write("file1.txt", "content");
at.write("file2.txt", "content");

let result = ucmd
.args(&["-cvf", "archive.tar", "file1.txt", "file2.txt"])
.succeeds();

let stdout = result.stdout_str();

// Verify verbose output contains filenames
assert!(stdout.contains("file1.txt"));
assert!(stdout.contains("file2.txt"));
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is more or less the same as test_create_verbose, with the only difference that this test uses two files.

Suggested change
#[test]
fn test_verbose_output_format_matches_gnu() {
let (at, mut ucmd) = at_and_ucmd!();
at.write("file1.txt", "content");
at.write("file2.txt", "content");
let result = ucmd
.args(&["-cvf", "archive.tar", "file1.txt", "file2.txt"])
.succeeds();
let stdout = result.stdout_str();
// Verify verbose output contains filenames
assert!(stdout.contains("file1.txt"));
assert!(stdout.contains("file2.txt"));
}

Comment on lines +521 to +550
#[test]
fn test_extract_verbose_shows_all_files() {
let (at, mut ucmd) = at_and_ucmd!();

// Create archive with multiple files
at.write("file1.txt", "content1");
at.write("file2.txt", "content2");
at.write("file3.txt", "content3");

ucmd.args(&["-cf", "archive.tar", "file1.txt", "file2.txt", "file3.txt"])
.succeeds();

at.remove("file1.txt");
at.remove("file2.txt");
at.remove("file3.txt");

// Extract with verbose
let result = new_ucmd!()
.arg("-xvf")
.arg(at.plus("archive.tar"))
.current_dir(at.as_string())
.succeeds();

let stdout = result.stdout_str();

// Verify all files are listed in output
assert!(stdout.contains("file1.txt"));
assert!(stdout.contains("file2.txt"));
assert!(stdout.contains("file3.txt"));
}
Copy link
Contributor

@cakebaker cakebaker Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is more or less the same as test_extract_verbose, but with more files. I would merge the tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants