@@ -0,0 +1,461 @@ |
| 1 | +#!/usr/bin/env python3 |
| 2 | +""" |
| 3 | +test_shtick_fixed.py - Comprehensive test suite for shtick commands |
| 4 | +Fixed to handle interactive prompts |
| 5 | +""" |
| 6 | + |
| 7 | +import subprocess |
| 8 | +import tempfile |
| 9 | +import shutil |
| 10 | +import os |
| 11 | +import sys |
| 12 | +from pathlib import Path |
| 13 | + |
| 14 | +# ANSI color codes |
| 15 | +RED = "\033[0;31m" |
| 16 | +GREEN = "\033[0;32m" |
| 17 | +YELLOW = "\033[1;33m" |
| 18 | +NC = "\033[0m" # No Color |
| 19 | + |
| 20 | + |
| 21 | +class ShtickTester: |
| 22 | + def __init__(self): |
| 23 | + self.tests_run = 0 |
| 24 | + self.tests_passed = 0 |
| 25 | + self.tests_failed = 0 |
| 26 | + self.test_dir = None |
| 27 | + self.original_home = os.environ.get("HOME") |
| 28 | + self.shtick_cmd = self._find_shtick_command() |
| 29 | + |
| 30 | + def _find_shtick_command(self): |
| 31 | + """Find the shtick command to use""" |
| 32 | + # Method 1: Check if shtick is in PATH |
| 33 | + if shutil.which("shtick"): |
| 34 | + print(f"Found shtick in PATH: {shutil.which('shtick')}") |
| 35 | + return ["shtick"] |
| 36 | + |
| 37 | + # Method 2: Try running as module from current directory |
| 38 | + if Path("shtick/cli.py").exists(): |
| 39 | + print("Found shtick module in current directory") |
| 40 | + return [sys.executable, "-m", "shtick.cli"] |
| 41 | + |
| 42 | + # Method 3: Try parent directory |
| 43 | + if Path("../shtick/cli.py").exists(): |
| 44 | + print("Found shtick module in parent directory") |
| 45 | + os.chdir("..") |
| 46 | + return [sys.executable, "-m", "shtick.cli"] |
| 47 | + |
| 48 | + # Method 4: Direct cli.py execution |
| 49 | + if Path("cli.py").exists(): |
| 50 | + print("Found cli.py in current directory") |
| 51 | + return [sys.executable, "cli.py"] |
| 52 | + |
| 53 | + raise RuntimeError( |
| 54 | + "Cannot find shtick command! Please ensure shtick is installed or run from source directory." |
| 55 | + ) |
| 56 | + |
| 57 | + def setup(self): |
| 58 | + """Set up test environment""" |
| 59 | + self.test_dir = tempfile.mkdtemp(prefix="shtick_test_") |
| 60 | + os.environ["HOME"] = self.test_dir |
| 61 | + os.makedirs(os.path.join(self.test_dir, ".config", "shtick"), exist_ok=True) |
| 62 | + |
| 63 | + # Create a settings file that disables auto_source_prompt to avoid timeouts |
| 64 | + settings_path = os.path.join( |
| 65 | + self.test_dir, ".config", "shtick", "settings.toml" |
| 66 | + ) |
| 67 | + with open(settings_path, "w") as f: |
| 68 | + f.write( |
| 69 | + """# Test settings |
| 70 | +[behavior] |
| 71 | +auto_source_prompt = false |
| 72 | +check_conflicts = true |
| 73 | +backup_on_save = false |
| 74 | +interactive_mode = false |
| 75 | +""" |
| 76 | + ) |
| 77 | + |
| 78 | + print(f"Test directory: {self.test_dir}") |
| 79 | + print(f"Created settings to disable interactive prompts") |
| 80 | + |
| 81 | + def cleanup(self): |
| 82 | + """Clean up test environment""" |
| 83 | + if self.test_dir and os.path.exists(self.test_dir): |
| 84 | + shutil.rmtree(self.test_dir) |
| 85 | + if self.original_home: |
| 86 | + os.environ["HOME"] = self.original_home |
| 87 | + else: |
| 88 | + os.environ.pop("HOME", None) |
| 89 | + |
| 90 | + def run_command(self, args, input_text=None, env_override=None): |
| 91 | + """Run a shtick command and return (output, return_code)""" |
| 92 | + cmd = self.shtick_cmd + args |
| 93 | + |
| 94 | + env = os.environ.copy() |
| 95 | + if env_override: |
| 96 | + env.update(env_override) |
| 97 | + |
| 98 | + # Always ensure we're using our test HOME |
| 99 | + env["HOME"] = self.test_dir |
| 100 | + |
| 101 | + try: |
| 102 | + # For commands that might prompt, always provide 'n' as input |
| 103 | + # to avoid hanging on interactive prompts |
| 104 | + if input_text is None and any( |
| 105 | + x in args for x in ["alias", "env", "function", "add", "remove"] |
| 106 | + ): |
| 107 | + input_text = "n\n" |
| 108 | + |
| 109 | + result = subprocess.run( |
| 110 | + cmd, |
| 111 | + capture_output=True, |
| 112 | + text=True, |
| 113 | + input=input_text, |
| 114 | + env=env, |
| 115 | + timeout=5, # Reduced timeout since we're handling prompts |
| 116 | + ) |
| 117 | + return result.stdout + result.stderr, result.returncode |
| 118 | + except subprocess.TimeoutExpired: |
| 119 | + return "Command timed out", -1 |
| 120 | + except Exception as e: |
| 121 | + return f"Error running command: {e}", -1 |
| 122 | + |
| 123 | + def test_command( |
| 124 | + self, |
| 125 | + test_name, |
| 126 | + args, |
| 127 | + expected_status, |
| 128 | + description, |
| 129 | + input_text=None, |
| 130 | + env_override=None, |
| 131 | + ): |
| 132 | + """Run a test and check the result""" |
| 133 | + self.tests_run += 1 |
| 134 | + |
| 135 | + print(f"[{self.tests_run}] {test_name}: {description} ... ", end="", flush=True) |
| 136 | + |
| 137 | + output, actual_status = self.run_command(args, input_text, env_override) |
| 138 | + |
| 139 | + if actual_status == expected_status: |
| 140 | + print(f"{GREEN}PASS{NC}") |
| 141 | + self.tests_passed += 1 |
| 142 | + else: |
| 143 | + print(f"{RED}FAIL{NC}") |
| 144 | + print(f" Expected status: {expected_status}, Got: {actual_status}") |
| 145 | + print(f" Command: {' '.join(self.shtick_cmd + args)}") |
| 146 | + print(f" Output: {output[:500]}...") # Truncate long output |
| 147 | + self.tests_failed += 1 |
| 148 | + |
| 149 | + def verify_shtick(self): |
| 150 | + """Verify shtick command is working""" |
| 151 | + print("Verifying shtick command...") |
| 152 | + output, status = self.run_command(["--help"]) |
| 153 | + |
| 154 | + if status != 0: |
| 155 | + print(f"{RED}ERROR: shtick command not working!{NC}") |
| 156 | + print(f"Status: {status}") |
| 157 | + print(f"Output: {output}") |
| 158 | + sys.exit(1) |
| 159 | + |
| 160 | + print(f"{GREEN}✓ shtick command verified{NC}\n") |
| 161 | + |
| 162 | + def run_all_tests(self): |
| 163 | + """Run all tests""" |
| 164 | + print("===================================") |
| 165 | + print("Shtick Command Test Suite (Python)") |
| 166 | + print("===================================\n") |
| 167 | + |
| 168 | + self.setup() |
| 169 | + self.verify_shtick() |
| 170 | + |
| 171 | + try: |
| 172 | + # Test 1: Basic commands without config |
| 173 | + print(f"{YELLOW}Testing basic commands without config:{NC}") |
| 174 | + self.test_command( |
| 175 | + "status-no-config", ["status"], 0, "Status should work without config" |
| 176 | + ) |
| 177 | + self.test_command( |
| 178 | + "list-no-config", ["list"], 0, "List should work without config" |
| 179 | + ) |
| 180 | + self.test_command( |
| 181 | + "shells", ["shells"], 0, "Shells command should always work" |
| 182 | + ) |
| 183 | + |
| 184 | + # Test 2: Adding items - use proper quoting |
| 185 | + print(f"\n{YELLOW}Testing add commands:{NC}") |
| 186 | + self.test_command( |
| 187 | + "add-alias", ["alias", "ll=ls -la"], 0, "Add persistent alias" |
| 188 | + ) |
| 189 | + self.test_command( |
| 190 | + "add-env", ["env", "DEBUG=1"], 0, "Add persistent env var" |
| 191 | + ) |
| 192 | + self.test_command( |
| 193 | + "add-function", |
| 194 | + ["function", "greet=echo Hello"], |
| 195 | + 0, |
| 196 | + "Add persistent function", |
| 197 | + ) |
| 198 | + |
| 199 | + # Test 3: Invalid add commands |
| 200 | + print(f"\n{YELLOW}Testing invalid add commands:{NC}") |
| 201 | + self.test_command( |
| 202 | + "add-invalid-key", |
| 203 | + ["alias", "123bad=value"], |
| 204 | + 1, |
| 205 | + "Invalid key should fail", |
| 206 | + ) |
| 207 | + self.test_command( |
| 208 | + "add-no-equals", ["alias", "no_equals_sign"], 1, "Missing = should fail" |
| 209 | + ) |
| 210 | + self.test_command( |
| 211 | + "add-empty-key", ["alias", "=value"], 1, "Empty key should fail" |
| 212 | + ) |
| 213 | + self.test_command( |
| 214 | + "add-empty-value", ["alias", "key="], 1, "Empty value should fail" |
| 215 | + ) |
| 216 | + |
| 217 | + # Test 4: Group operations |
| 218 | + print(f"\n{YELLOW}Testing group operations:{NC}") |
| 219 | + self.test_command( |
| 220 | + "add-to-group", |
| 221 | + ["add", "alias", "work", "ll=ls -la"], |
| 222 | + 0, |
| 223 | + "Add to specific group", |
| 224 | + ) |
| 225 | + self.test_command( |
| 226 | + "activate-group", ["activate", "work"], 0, "Activate existing group" |
| 227 | + ) |
| 228 | + self.test_command( |
| 229 | + "activate-nonexistent", |
| 230 | + ["activate", "nonexistent"], |
| 231 | + 1, |
| 232 | + "Activate non-existent group should fail", |
| 233 | + ) |
| 234 | + self.test_command( |
| 235 | + "deactivate-group", ["deactivate", "work"], 0, "Deactivate active group" |
| 236 | + ) |
| 237 | + self.test_command( |
| 238 | + "deactivate-inactive", |
| 239 | + ["deactivate", "work"], |
| 240 | + 0, |
| 241 | + "Deactivate already inactive group", |
| 242 | + ) |
| 243 | + |
| 244 | + # Test 5: Remove operations - provide input for selection |
| 245 | + print(f"\n{YELLOW}Testing remove operations:{NC}") |
| 246 | + self.test_command( |
| 247 | + "remove-existing", |
| 248 | + ["remove-persistent", "alias", "ll"], |
| 249 | + 0, |
| 250 | + "Remove existing alias", |
| 251 | + input_text="1\n", |
| 252 | + ) |
| 253 | + self.test_command( |
| 254 | + "remove-nonexistent", |
| 255 | + ["remove-persistent", "alias", "nonexistent"], |
| 256 | + 0, |
| 257 | + "Remove non-existent item", |
| 258 | + ) |
| 259 | + |
| 260 | + # Re-add for next test |
| 261 | + self.run_command(["add", "alias", "work", "ll=ls -la"]) |
| 262 | + self.test_command( |
| 263 | + "remove-fuzzy", |
| 264 | + ["remove", "alias", "work", "ll"], |
| 265 | + 0, |
| 266 | + "Remove with fuzzy match", |
| 267 | + input_text="1\n", |
| 268 | + ) |
| 269 | + |
| 270 | + # Test 6: Generate command |
| 271 | + print(f"\n{YELLOW}Testing generate command:{NC}") |
| 272 | + self.test_command( |
| 273 | + "generate-default", |
| 274 | + ["generate", "--terse"], |
| 275 | + 0, |
| 276 | + "Generate with default config", |
| 277 | + ) |
| 278 | + config_path = os.path.join( |
| 279 | + self.test_dir, ".config", "shtick", "config.toml" |
| 280 | + ) |
| 281 | + self.test_command( |
| 282 | + "generate-custom", |
| 283 | + ["generate", config_path, "--terse"], |
| 284 | + 0, |
| 285 | + "Generate with custom config path", |
| 286 | + ) |
| 287 | + self.test_command( |
| 288 | + "generate-nonexistent", |
| 289 | + ["generate", "/tmp/nonexistent.toml"], |
| 290 | + 1, |
| 291 | + "Generate with non-existent config should fail", |
| 292 | + ) |
| 293 | + |
| 294 | + # Test 7: Source command |
| 295 | + print(f"\n{YELLOW}Testing source command:{NC}") |
| 296 | + self.test_command( |
| 297 | + "source-bash", |
| 298 | + ["source"], |
| 299 | + 0, |
| 300 | + "Source command for bash", |
| 301 | + env_override={"SHELL": "/bin/bash"}, |
| 302 | + ) |
| 303 | + self.test_command( |
| 304 | + "source-no-shell", |
| 305 | + ["source"], |
| 306 | + 1, |
| 307 | + "Source without shell should fail", |
| 308 | + env_override={"SHELL": ""}, |
| 309 | + ) |
| 310 | + |
| 311 | + # Test 8: Settings commands |
| 312 | + print(f"\n{YELLOW}Testing settings commands:{NC}") |
| 313 | + self.test_command("settings-show", ["settings", "show"], 0, "Show settings") |
| 314 | + # Don't re-init since we already have settings |
| 315 | + self.test_command( |
| 316 | + "settings-set-bool", |
| 317 | + ["settings", "set", "behavior.backup_on_save", "true"], |
| 318 | + 0, |
| 319 | + "Set boolean setting", |
| 320 | + ) |
| 321 | + self.test_command( |
| 322 | + "settings-set-invalid", |
| 323 | + ["settings", "set", "invalid.key", "value"], |
| 324 | + 1, |
| 325 | + "Set invalid setting should fail", |
| 326 | + ) |
| 327 | + |
| 328 | + # Test 9: Edge cases |
| 329 | + print(f"\n{YELLOW}Testing edge cases:{NC}") |
| 330 | + self.test_command( |
| 331 | + "persistent-activate", |
| 332 | + ["activate", "persistent"], |
| 333 | + 1, |
| 334 | + "Cannot activate persistent group", |
| 335 | + ) |
| 336 | + self.test_command( |
| 337 | + "persistent-deactivate", |
| 338 | + ["deactivate", "persistent"], |
| 339 | + 1, |
| 340 | + "Cannot deactivate persistent group", |
| 341 | + ) |
| 342 | + long_key = "a" * 65 |
| 343 | + self.test_command( |
| 344 | + "very-long-key", |
| 345 | + ["alias", f"{long_key}=value"], |
| 346 | + 1, |
| 347 | + "Key over 64 chars should fail", |
| 348 | + ) |
| 349 | + |
| 350 | + # Test 10: Special characters |
| 351 | + print(f"\n{YELLOW}Testing special characters:{NC}") |
| 352 | + self.test_command( |
| 353 | + "alias-with-quotes", |
| 354 | + ["alias", 'msg=echo "Hello World"'], |
| 355 | + 0, |
| 356 | + "Alias with quotes", |
| 357 | + ) |
| 358 | + self.test_command( |
| 359 | + "alias-with-dollar", |
| 360 | + ["alias", "home=cd $HOME"], |
| 361 | + 0, |
| 362 | + "Alias with dollar sign", |
| 363 | + ) |
| 364 | + self.test_command( |
| 365 | + "multiline-function", |
| 366 | + ["function", "hello=echo line1\necho line2"], |
| 367 | + 0, |
| 368 | + "Multiline function", |
| 369 | + ) |
| 370 | + |
| 371 | + # Test 11: List and status with content |
| 372 | + print(f"\n{YELLOW}Testing list and status with content:{NC}") |
| 373 | + self.test_command("list-with-content", ["list"], 0, "List with items") |
| 374 | + self.test_command( |
| 375 | + "list-long-format", ["list", "-l"], 0, "List in long format" |
| 376 | + ) |
| 377 | + self.test_command( |
| 378 | + "status-with-content", ["status"], 0, "Status with configuration" |
| 379 | + ) |
| 380 | + |
| 381 | + # Test 12: Conflict handling |
| 382 | + print(f"\n{YELLOW}Testing conflict handling:{NC}") |
| 383 | + # First create an alias in persistent |
| 384 | + self.run_command(["alias", "mytest=echo test1"]) |
| 385 | + # Then add conflicting alias to different group |
| 386 | + self.test_command( |
| 387 | + "add-conflict-alias", |
| 388 | + ["add", "alias", "temp", "mytest=echo test2"], |
| 389 | + 0, |
| 390 | + "Add conflicting alias to different group", |
| 391 | + ) |
| 392 | + |
| 393 | + # Check if warning appears when adding duplicate |
| 394 | + output, status = self.run_command(["alias", "mytest=echo test3"]) |
| 395 | + if "exists in groups" in output and status == 0: |
| 396 | + print( |
| 397 | + f"[{self.tests_run + 1}] check-conflict-warning: Should warn about conflicts ... {GREEN}PASS{NC}" |
| 398 | + ) |
| 399 | + self.tests_passed += 1 |
| 400 | + else: |
| 401 | + print( |
| 402 | + f"[{self.tests_run + 1}] check-conflict-warning: Should warn about conflicts ... {RED}FAIL{NC}" |
| 403 | + ) |
| 404 | + print(f" Expected warning about existing item, but got: {output}") |
| 405 | + print(f" Status: {status}") |
| 406 | + self.tests_failed += 1 |
| 407 | + self.tests_run += 1 |
| 408 | + |
| 409 | + # Test 13: Backup functionality |
| 410 | + print(f"\n{YELLOW}Testing backup functionality:{NC}") |
| 411 | + self.test_command("backup-create", ["backup", "create"], 0, "Create backup") |
| 412 | + self.test_command("backup-list", ["backup", "list"], 0, "List backups") |
| 413 | + |
| 414 | + # Test 14: Group management |
| 415 | + print(f"\n{YELLOW}Testing group management:{NC}") |
| 416 | + self.test_command( |
| 417 | + "group-create", ["group", "create", "testgroup"], 0, "Create new group" |
| 418 | + ) |
| 419 | + self.test_command( |
| 420 | + "group-rename", |
| 421 | + ["group", "rename", "testgroup", "newgroup"], |
| 422 | + 0, |
| 423 | + "Rename group", |
| 424 | + ) |
| 425 | + self.test_command( |
| 426 | + "group-remove", ["group", "remove", "newgroup", "-f"], 0, "Remove group" |
| 427 | + ) |
| 428 | + |
| 429 | + finally: |
| 430 | + self.cleanup() |
| 431 | + |
| 432 | + # Summary |
| 433 | + print("\n===================================") |
| 434 | + print("Test Summary") |
| 435 | + print("===================================") |
| 436 | + print(f"Tests run: {self.tests_run}") |
| 437 | + print(f"Tests passed: {GREEN}{self.tests_passed}{NC}") |
| 438 | + print(f"Tests failed: {RED}{self.tests_failed}{NC}") |
| 439 | + |
| 440 | + if self.tests_failed == 0: |
| 441 | + print(f"\n{GREEN}All tests passed!{NC}") |
| 442 | + return 0 |
| 443 | + else: |
| 444 | + print(f"\n{RED}Some tests failed!{NC}") |
| 445 | + return 1 |
| 446 | + |
| 447 | + |
| 448 | +def main(): |
| 449 | + """Run the test suite""" |
| 450 | + # Check if we should run in non-interactive mode |
| 451 | + if len(sys.argv) > 1 and sys.argv[1] == "--help": |
| 452 | + print("Usage: test_shtick_fixed.py") |
| 453 | + print("Run comprehensive tests for shtick command line tool") |
| 454 | + return 0 |
| 455 | + |
| 456 | + tester = ShtickTester() |
| 457 | + return tester.run_all_tests() |
| 458 | + |
| 459 | + |
| 460 | +if __name__ == "__main__": |
| 461 | + sys.exit(main()) |