@@ -1,7 +1,7 @@ |
| 1 | #!/usr/bin/env python3 | 1 | #!/usr/bin/env python3 |
| 2 | """ | 2 | """ |
| 3 | -test_shtick_fixed.py - Comprehensive test suite for shtick commands | 3 | +test_shtick_enhanced.py - Comprehensive test suite for shtick commands |
| 4 | -Fixed to handle interactive prompts | 4 | +Enhanced with backup and group management tests |
| 5 | """ | 5 | """ |
| 6 | | 6 | |
| 7 | import subprocess | 7 | import subprocess |
@@ -58,6 +58,9 @@ class ShtickTester: |
| 58 | """Set up test environment""" | 58 | """Set up test environment""" |
| 59 | self.test_dir = tempfile.mkdtemp(prefix="shtick_test_") | 59 | self.test_dir = tempfile.mkdtemp(prefix="shtick_test_") |
| 60 | os.environ["HOME"] = self.test_dir | 60 | os.environ["HOME"] = self.test_dir |
| | 61 | + os.environ["SHTICK_ORIGINAL_HOME"] = ( |
| | 62 | + self.original_home |
| | 63 | + ) # For security.py to work |
| 61 | os.makedirs(os.path.join(self.test_dir, ".config", "shtick"), exist_ok=True) | 64 | os.makedirs(os.path.join(self.test_dir, ".config", "shtick"), exist_ok=True) |
| 62 | | 65 | |
| 63 | # Create a settings file that disables auto_source_prompt to avoid timeouts | 66 | # Create a settings file that disables auto_source_prompt to avoid timeouts |
@@ -86,6 +89,7 @@ interactive_mode = false |
| 86 | os.environ["HOME"] = self.original_home | 89 | os.environ["HOME"] = self.original_home |
| 87 | else: | 90 | else: |
| 88 | os.environ.pop("HOME", None) | 91 | os.environ.pop("HOME", None) |
| | 92 | + os.environ.pop("SHTICK_ORIGINAL_HOME", None) |
| 89 | | 93 | |
| 90 | def run_command(self, args, input_text=None, env_override=None): | 94 | def run_command(self, args, input_text=None, env_override=None): |
| 91 | """Run a shtick command and return (output, return_code)""" | 95 | """Run a shtick command and return (output, return_code)""" |
@@ -97,6 +101,7 @@ interactive_mode = false |
| 97 | | 101 | |
| 98 | # Always ensure we're using our test HOME | 102 | # Always ensure we're using our test HOME |
| 99 | env["HOME"] = self.test_dir | 103 | env["HOME"] = self.test_dir |
| | 104 | + env["SHTICK_ORIGINAL_HOME"] = self.original_home # For security.py |
| 100 | | 105 | |
| 101 | try: | 106 | try: |
| 102 | # For commands that might prompt, always provide 'n' as input | 107 | # For commands that might prompt, always provide 'n' as input |
@@ -128,6 +133,8 @@ interactive_mode = false |
| 128 | description, | 133 | description, |
| 129 | input_text=None, | 134 | input_text=None, |
| 130 | env_override=None, | 135 | env_override=None, |
| | 136 | + check_output=None, |
| | 137 | + check_not_output=None, |
| 131 | ): | 138 | ): |
| 132 | """Run a test and check the result""" | 139 | """Run a test and check the result""" |
| 133 | self.tests_run += 1 | 140 | self.tests_run += 1 |
@@ -136,12 +143,28 @@ interactive_mode = false |
| 136 | | 143 | |
| 137 | output, actual_status = self.run_command(args, input_text, env_override) | 144 | output, actual_status = self.run_command(args, input_text, env_override) |
| 138 | | 145 | |
| 139 | - if actual_status == expected_status: | 146 | + # Check status code |
| | 147 | + status_ok = actual_status == expected_status |
| | 148 | + |
| | 149 | + # Check output if requested |
| | 150 | + output_ok = True |
| | 151 | + if check_output is not None: |
| | 152 | + output_ok = check_output in output |
| | 153 | + if check_not_output is not None and output_ok: |
| | 154 | + output_ok = check_not_output not in output |
| | 155 | + |
| | 156 | + if status_ok and output_ok: |
| 140 | print(f"{GREEN}PASS{NC}") | 157 | print(f"{GREEN}PASS{NC}") |
| 141 | self.tests_passed += 1 | 158 | self.tests_passed += 1 |
| 142 | else: | 159 | else: |
| 143 | print(f"{RED}FAIL{NC}") | 160 | print(f"{RED}FAIL{NC}") |
| | 161 | + if not status_ok: |
| 144 | print(f" Expected status: {expected_status}, Got: {actual_status}") | 162 | print(f" Expected status: {expected_status}, Got: {actual_status}") |
| | 163 | + if not output_ok: |
| | 164 | + if check_output is not None: |
| | 165 | + print(f" Expected output to contain: {check_output}") |
| | 166 | + if check_not_output is not None: |
| | 167 | + print(f" Expected output NOT to contain: {check_not_output}") |
| 145 | print(f" Command: {' '.join(self.shtick_cmd + args)}") | 168 | print(f" Command: {' '.join(self.shtick_cmd + args)}") |
| 146 | print(f" Output: {output[:500]}...") # Truncate long output | 169 | print(f" Output: {output[:500]}...") # Truncate long output |
| 147 | self.tests_failed += 1 | 170 | self.tests_failed += 1 |
@@ -162,7 +185,7 @@ interactive_mode = false |
| 162 | def run_all_tests(self): | 185 | def run_all_tests(self): |
| 163 | """Run all tests""" | 186 | """Run all tests""" |
| 164 | print("===================================") | 187 | print("===================================") |
| 165 | - print("Shtick Command Test Suite (Python)") | 188 | + print("Shtick Command Test Suite (Enhanced)") |
| 166 | print("===================================\n") | 189 | print("===================================\n") |
| 167 | | 190 | |
| 168 | self.setup() | 191 | self.setup() |
@@ -238,7 +261,7 @@ interactive_mode = false |
| 238 | "deactivate-inactive", | 261 | "deactivate-inactive", |
| 239 | ["deactivate", "work"], | 262 | ["deactivate", "work"], |
| 240 | 0, | 263 | 0, |
| 241 | - "Deactivate already inactive group", | 264 | + "Deactivate already inactive group (idempotent)", |
| 242 | ) | 265 | ) |
| 243 | | 266 | |
| 244 | # Test 5: Remove operations - provide input for selection | 267 | # Test 5: Remove operations - provide input for selection |
@@ -254,7 +277,7 @@ interactive_mode = false |
| 254 | "remove-nonexistent", | 277 | "remove-nonexistent", |
| 255 | ["remove-persistent", "alias", "nonexistent"], | 278 | ["remove-persistent", "alias", "nonexistent"], |
| 256 | 0, | 279 | 0, |
| 257 | - "Remove non-existent item", | 280 | + "Remove non-existent item (no-op)", |
| 258 | ) | 281 | ) |
| 259 | | 282 | |
| 260 | # Re-add for next test | 283 | # Re-add for next test |
@@ -408,23 +431,198 @@ interactive_mode = false |
| 408 | | 431 | |
| 409 | # Test 13: Backup functionality | 432 | # Test 13: Backup functionality |
| 410 | print(f"\n{YELLOW}Testing backup functionality:{NC}") | 433 | print(f"\n{YELLOW}Testing backup functionality:{NC}") |
| | 434 | + # Add some content first |
| | 435 | + self.run_command(["alias", "backup_test=echo backup"]) |
| 411 | self.test_command("backup-create", ["backup", "create"], 0, "Create backup") | 436 | self.test_command("backup-create", ["backup", "create"], 0, "Create backup") |
| | 437 | + self.test_command( |
| | 438 | + "backup-create-named", |
| | 439 | + ["backup", "create", "-n", "test_backup"], |
| | 440 | + 0, |
| | 441 | + "Create named backup", |
| | 442 | + ) |
| 412 | self.test_command("backup-list", ["backup", "list"], 0, "List backups") | 443 | self.test_command("backup-list", ["backup", "list"], 0, "List backups") |
| 413 | | 444 | |
| 414 | - # Test 14: Group management | 445 | + # Modify config |
| 415 | - print(f"\n{YELLOW}Testing group management:{NC}") | 446 | + self.run_command(["alias", "after_backup=echo after"]) |
| | 447 | + |
| | 448 | + # Restore and verify |
| | 449 | + self.test_command( |
| | 450 | + "backup-restore", |
| | 451 | + ["backup", "restore", "test_backup"], |
| | 452 | + 0, |
| | 453 | + "Restore from backup", |
| | 454 | + ) |
| | 455 | + |
| | 456 | + # Verify restore worked by checking if the new alias is gone |
| | 457 | + output, _ = self.run_command(["list"]) |
| | 458 | + if "after_backup" not in output: |
| | 459 | + print( |
| | 460 | + f"[{self.tests_run + 1}] verify-restore: Backup restore worked correctly ... {GREEN}PASS{NC}" |
| | 461 | + ) |
| | 462 | + self.tests_passed += 1 |
| | 463 | + else: |
| | 464 | + print( |
| | 465 | + f"[{self.tests_run + 1}] verify-restore: Backup restore failed ... {RED}FAIL{NC}" |
| | 466 | + ) |
| | 467 | + self.tests_failed += 1 |
| | 468 | + self.tests_run += 1 |
| | 469 | + |
| | 470 | + # Test 14: Group management - NOW WITH REAL FUNCTIONALITY! |
| | 471 | + print(f"\n{YELLOW}Testing group management (enhanced):{NC}") |
| | 472 | + |
| | 473 | + # Test creating a new group |
| | 474 | + self.test_command( |
| | 475 | + "group-create", |
| | 476 | + ["group", "create", "testgroup"], |
| | 477 | + 0, |
| | 478 | + "Create new group", |
| | 479 | + check_output="✓ Created group 'testgroup'", |
| | 480 | + check_not_output="will be created when you add", |
| | 481 | + ) |
| | 482 | + |
| | 483 | + # Verify the group exists in status |
| | 484 | + output, _ = self.run_command(["status"]) |
| | 485 | + if "testgroup: 0 items" in output: |
| | 486 | + print( |
| | 487 | + f"[{self.tests_run + 1}] verify-group-exists: Created group shows in status ... {GREEN}PASS{NC}" |
| | 488 | + ) |
| | 489 | + self.tests_passed += 1 |
| | 490 | + else: |
| | 491 | + print( |
| | 492 | + f"[{self.tests_run + 1}] verify-group-exists: Created group should show in status ... {RED}FAIL{NC}" |
| | 493 | + ) |
| | 494 | + print(f" Status output: {output}") |
| | 495 | + self.tests_failed += 1 |
| | 496 | + self.tests_run += 1 |
| | 497 | + |
| | 498 | + # Verify we can activate the empty group |
| | 499 | + self.test_command( |
| | 500 | + "activate-empty-group", |
| | 501 | + ["activate", "testgroup"], |
| | 502 | + 0, |
| | 503 | + "Activate empty group", |
| | 504 | + ) |
| | 505 | + |
| | 506 | + # Verify the group shows as active |
| | 507 | + output, _ = self.run_command(["status"]) |
| | 508 | + if "testgroup: 0 items (ACTIVE)" in output: |
| | 509 | + print( |
| | 510 | + f"[{self.tests_run + 1}] verify-group-active: Empty group shows as active ... {GREEN}PASS{NC}" |
| | 511 | + ) |
| | 512 | + self.tests_passed += 1 |
| | 513 | + else: |
| | 514 | + print( |
| | 515 | + f"[{self.tests_run + 1}] verify-group-active: Empty group should show as active ... {RED}FAIL{NC}" |
| | 516 | + ) |
| | 517 | + print(f" Status output: {output}") |
| | 518 | + self.tests_failed += 1 |
| | 519 | + self.tests_run += 1 |
| | 520 | + |
| | 521 | + # Test adding to the newly created group |
| 416 | self.test_command( | 522 | self.test_command( |
| 417 | - "group-create", ["group", "create", "testgroup"], 0, "Create new group" | 523 | + "add-to-created-group", |
| | 524 | + ["add", "alias", "testgroup", "tg=echo testgroup"], |
| | 525 | + 0, |
| | 526 | + "Add item to newly created group", |
| | 527 | + ) |
| | 528 | + |
| | 529 | + # Verify the group now has 1 item |
| | 530 | + output, _ = self.run_command(["status"]) |
| | 531 | + if "testgroup: 1 items (ACTIVE)" in output: |
| | 532 | + print( |
| | 533 | + f"[{self.tests_run + 1}] verify-group-has-item: Group shows 1 item after add ... {GREEN}PASS{NC}" |
| | 534 | + ) |
| | 535 | + self.tests_passed += 1 |
| | 536 | + else: |
| | 537 | + print( |
| | 538 | + f"[{self.tests_run + 1}] verify-group-has-item: Group should show 1 item ... {RED}FAIL{NC}" |
| 418 | ) | 539 | ) |
| | 540 | + print(f" Status output: {output}") |
| | 541 | + self.tests_failed += 1 |
| | 542 | + self.tests_run += 1 |
| | 543 | + |
| | 544 | + # Test creating duplicate group |
| | 545 | + self.test_command( |
| | 546 | + "group-create-duplicate", |
| | 547 | + ["group", "create", "testgroup"], |
| | 548 | + 1, |
| | 549 | + "Cannot create duplicate group", |
| | 550 | + check_output="already exists", |
| | 551 | + ) |
| | 552 | + |
| | 553 | + # Check the TOML file contains empty sections |
| | 554 | + config_path = os.path.join( |
| | 555 | + self.test_dir, ".config", "shtick", "config.toml" |
| | 556 | + ) |
| | 557 | + if os.path.exists(config_path): |
| | 558 | + with open(config_path, "r") as f: |
| | 559 | + toml_content = f.read() |
| | 560 | + # Check for either nested format (with tomli_w) or flat format (fallback) |
| | 561 | + has_nested = ( |
| | 562 | + "[testgroup]" in toml_content |
| | 563 | + and "[testgroup.aliases]" in toml_content |
| | 564 | + ) |
| | 565 | + has_flat = "[testgroup.aliases]" in toml_content |
| | 566 | + if has_nested or has_flat: |
| | 567 | + print( |
| | 568 | + f"[{self.tests_run + 1}] verify-toml-structure: TOML has proper empty group structure ... {GREEN}PASS{NC}" |
| | 569 | + ) |
| | 570 | + self.tests_passed += 1 |
| | 571 | + else: |
| | 572 | + print( |
| | 573 | + f"[{self.tests_run + 1}] verify-toml-structure: TOML should have empty group sections ... {RED}FAIL{NC}" |
| | 574 | + ) |
| | 575 | + print(f" TOML content:\n{toml_content[:500]}") |
| | 576 | + self.tests_failed += 1 |
| | 577 | + self.tests_run += 1 |
| | 578 | + |
| | 579 | + # Still test unimplemented features |
| 419 | self.test_command( | 580 | self.test_command( |
| 420 | "group-rename", | 581 | "group-rename", |
| 421 | ["group", "rename", "testgroup", "newgroup"], | 582 | ["group", "rename", "testgroup", "newgroup"], |
| 422 | - 0, | 583 | + 1, |
| 423 | - "Rename group", | 584 | + "Rename group (not implemented)", |
| | 585 | + check_output="not yet implemented", |
| | 586 | + ) |
| | 587 | + self.test_command( |
| | 588 | + "group-remove", |
| | 589 | + ["group", "remove", "testgroup", "-f"], |
| | 590 | + 1, |
| | 591 | + "Remove group (not implemented)", |
| | 592 | + check_output="not yet implemented", |
| 424 | ) | 593 | ) |
| | 594 | + |
| | 595 | + # Test 15: Exit code consistency |
| | 596 | + print(f"\n{YELLOW}Testing exit code consistency:{NC}") |
| | 597 | + # Test that user cancellation uses exit code 2 |
| | 598 | + output, status = self.run_command( |
| | 599 | + ["settings", "init"], input_text="\x03" |
| | 600 | + ) # Ctrl+C |
| | 601 | + if status == 2 or "Cancelled" in output: |
| | 602 | + print( |
| | 603 | + f"[{self.tests_run + 1}] user-cancel-exit-code: User cancellation returns code 2 ... {GREEN}PASS{NC}" |
| | 604 | + ) |
| | 605 | + self.tests_passed += 1 |
| | 606 | + else: |
| | 607 | + print( |
| | 608 | + f"[{self.tests_run + 1}] user-cancel-exit-code: User cancellation should return code 2 ... {RED}FAIL{NC}" |
| | 609 | + ) |
| | 610 | + print(f" Got status: {status}, output: {output}") |
| | 611 | + self.tests_failed += 1 |
| | 612 | + self.tests_run += 1 |
| | 613 | + |
| | 614 | + # Test 16: No-output commands should still exit properly |
| | 615 | + print(f"\n{YELLOW}Testing commands with no output still exit properly:{NC}") |
| | 616 | + # Create an empty config scenario |
| | 617 | + empty_dir = tempfile.mkdtemp() |
| 425 | self.test_command( | 618 | self.test_command( |
| 426 | - "group-remove", ["group", "remove", "newgroup", "-f"], 0, "Remove group" | 619 | + "list-empty-explicit-exit", |
| | 620 | + ["list"], |
| | 621 | + 0, |
| | 622 | + "List with empty config exits 0", |
| | 623 | + env_override={"HOME": empty_dir}, |
| 427 | ) | 624 | ) |
| | 625 | + shutil.rmtree(empty_dir) |
| 428 | | 626 | |
| 429 | finally: | 627 | finally: |
| 430 | self.cleanup() | 628 | self.cleanup() |
@@ -449,7 +647,7 @@ def main(): |
| 449 | """Run the test suite""" | 647 | """Run the test suite""" |
| 450 | # Check if we should run in non-interactive mode | 648 | # Check if we should run in non-interactive mode |
| 451 | if len(sys.argv) > 1 and sys.argv[1] == "--help": | 649 | if len(sys.argv) > 1 and sys.argv[1] == "--help": |
| 452 | - print("Usage: test_shtick_fixed.py") | 650 | + print("Usage: test_shtick_enhanced.py") |
| 453 | print("Run comprehensive tests for shtick command line tool") | 651 | print("Run comprehensive tests for shtick command line tool") |
| 454 | return 0 | 652 | return 0 |
| 455 | | 653 | |