From 459b695af06faeff00904ad4a97a3ec614e214fe Mon Sep 17 00:00:00 2001 From: Xi Xu Date: Sun, 1 Jun 2025 17:01:27 +0800 Subject: [PATCH] Add compression level validation to CLI Introduced a `validate_compression_level` function to ensure compression levels are within the valid range (1-22). Updated CLI argument parsing to use this validation and added tests to verify valid, invalid, and non-numeric compression levels. --- src/tzst/cli.py | 36 ++++++++++++++++++++++-- tests/test_cli.py | 71 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 3 deletions(-) diff --git a/src/tzst/cli.py b/src/tzst/cli.py index 4b8a36a..ca034ce 100644 --- a/src/tzst/cli.py +++ b/src/tzst/cli.py @@ -50,6 +50,31 @@ def format_size(size: int) -> str: return f"{size_float:6.1f} PB" +def validate_compression_level(value: str) -> int: + """Validate and return compression level. + + Args: + value: String value from command line + + Returns: + int: Valid compression level (1-22) + + Raises: + argparse.ArgumentTypeError: If value is not a valid compression level + """ + try: + level = int(value) + if not 1 <= level <= 22: + raise argparse.ArgumentTypeError( + f"Invalid compression level: {level}. Must be between 1 and 22." + ) + return level + except ValueError: + raise argparse.ArgumentTypeError( + f"Invalid compression level: '{value}'. Must be an integer between 1 and 22." + ) + + def cmd_add(args) -> int: """Command handler for creating/adding to archives. @@ -511,9 +536,8 @@ Documentation: "-l", "--level", dest="compression_level", - type=int, + type=validate_compression_level, default=3, - choices=range(1, 23), metavar="LEVEL", help="Compression level (1-22, default: 3)", ) @@ -628,6 +652,7 @@ def main(argv: list[str] | None = None) -> int: int: Exit code for the program - 0: Success - 1: No command specified (help displayed) + - 2: Argument parsing error (invalid arguments) - Other codes: Specific to individual command handlers Note: @@ -642,7 +667,12 @@ def main(argv: list[str] | None = None) -> int: print() parser = create_parser() - args = parser.parse_args(argv) + + try: + args = parser.parse_args(argv) + except SystemExit as e: + # Handle argparse errors (invalid arguments, help, etc.) + return e.code if e.code is not None else 1 if not hasattr(args, "func"): parser.print_help() diff --git a/tests/test_cli.py b/tests/test_cli.py index db20f7c..14ee48d 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1033,3 +1033,74 @@ class TestCLIBoundaryConditions: except OSError: # Skip if filesystem doesn't support such long names pytest.skip("Filesystem doesn't support long filenames") + + +class TestCompressionLevelValidation: + """Test compression level validation in CLI.""" + + def test_valid_compression_levels(self, temp_dir): + """Test that valid compression levels (1-22) work correctly.""" + test_file = temp_dir / "test.txt" + test_file.write_text("Test content") + + # Test boundary values and some middle values + valid_levels = [1, 2, 10, 22] + + for level in valid_levels: + archive_path = temp_dir / f"test_level_{level}.tzst" + result = main(["a", str(archive_path), str(test_file), "-l", str(level)]) + assert result == 0, f"Compression level {level} should be valid" + + def test_invalid_compression_levels(self, temp_dir): + """Test that invalid compression levels return proper error codes.""" + test_file = temp_dir / "test.txt" + test_file.write_text("Test content") + archive_path = temp_dir / "test.tzst" + + # Test invalid levels that should return exit code 2 (argparse error) + invalid_levels = [0, 23, 50, 100, -1, -10] + + for level in invalid_levels: + result = main(["a", str(archive_path), str(test_file), "-l", str(level)]) + assert result == 2, ( + f"Invalid compression level {level} should return exit code 2" + ) + + def test_non_numeric_compression_levels(self, temp_dir): + """Test that non-numeric compression levels return proper error codes.""" + test_file = temp_dir / "test.txt" + test_file.write_text("Test content") + archive_path = temp_dir / "test.tzst" + + # Test non-numeric values + invalid_values = ["abc", "1.5", "high", "max", ""] + + for value in invalid_values: + result = main(["a", str(archive_path), str(test_file), "-l", value]) + assert result == 2, ( + f"Non-numeric compression level '{value}' should return exit code 2" + ) + + def test_compression_level_clamping(self, temp_dir): + """Test compression level validation with extreme values.""" + test_file = temp_dir / "test.txt" + test_file.write_text("Test content") + archive_path = temp_dir / "test.tzst" + + # Test level 50 - should return argparse error code 2 + result = main(["a", str(archive_path), str(test_file), "-l", "50"]) + assert result == 2, "Compression level 50 should return exit code 2" + + def test_extreme_compression_levels(self, temp_dir): + """Test extreme compression level values.""" + test_file = temp_dir / "test.txt" + test_file.write_text("Test content") + archive_path = temp_dir / "test.tzst" + + # Test level 50 - should return argparse error code 2 + result = main(["a", str(archive_path), str(test_file), "-l", "50"]) + assert result == 2, "Extreme compression level 50 should return exit code 2" + + # Test level 0 - should return argparse error code 2 + result = main(["a", str(archive_path), str(test_file), "-l", "0"]) + assert result == 2, "Extreme compression level 0 should return exit code 2"