-
Notifications
You must be signed in to change notification settings - Fork 4
Add test documentation and expand CLI test coverage #46
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
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.
|
@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). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A detail:
| 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/`. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A detail:
| .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 |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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.
| .no_stderr(); | |
| .no_output(); |
| .succeeds(); | ||
| ucmd.args(&["-cf", "archive.tar", "file1.txt", "file2.txt", "file3.txt"]) | ||
| .succeeds() | ||
| .no_stderr(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| .no_stderr(); | |
| .no_output(); |
|
|
||
| ucmd.args(&["-cf", "archive.tar", "dir1"]) | ||
| .succeeds() | ||
| .no_stderr(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| .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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| .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 |
| let (_at, mut ucmd) = at_and_ucmd!(); | ||
|
|
||
| ucmd.args(&["-cf", "archive.tar", "nonexistent.txt"]) |
There was a problem hiding this comment.
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.
| 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(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| .no_stderr(); | |
| .no_output(); |
| std::fs::remove_dir(at.plus("testdir/subdir")).unwrap(); | ||
| std::fs::remove_dir(at.plus("testdir")).unwrap(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An alternative is rmdir:
| std::fs::remove_dir(at.plus("testdir/subdir")).unwrap(); | |
| std::fs::remove_dir(at.plus("testdir")).unwrap(); | |
| at.rmdir("testdir/subdir"); | |
| at.rmdir("testdir"); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
| #[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"); | ||
| } |
There was a problem hiding this comment.
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
| #[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"); | |
| } |
| #[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"); | ||
| } |
There was a problem hiding this comment.
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.
| #[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() { |
There was a problem hiding this comment.
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.
| at.write("empty1.txt", ""); | ||
| at.write("empty2.txt", ""); |
There was a problem hiding this comment.
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:
| at.write("empty1.txt", ""); | |
| at.write("empty2.txt", ""); | |
| at.touch("empty1.txt"); | |
| at.touch("empty2.txt"); |
| } | ||
|
|
||
| #[test] | ||
| fn test_roundtrip_special_characters_in_names() { |
There was a problem hiding this comment.
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 ;-)
| // Make directory read-only | ||
| let perms = fs::Permissions::from_mode(0o444); | ||
| fs::set_permissions(at.plus("readonly"), perms).unwrap(); |
There was a problem hiding this comment.
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:
| // Make directory read-only | |
| let perms = fs::Permissions::from_mode(0o444); | |
| fs::set_permissions(at.plus("readonly"), perms).unwrap(); | |
| at.set_readonly("readonly"); |
| #[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")); | ||
| } |
There was a problem hiding this comment.
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.
| #[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")); | |
| } |
| #[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")); | ||
| } |
There was a problem hiding this comment.
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.
Document testing philosophy and expand test suite with better CLI-focused tests and improved error verification.
Changes:
All 31 tests pass. Test suite now has clear documentation explaining what should be tested at the application level vs library level.