From c700ff3ee7635b824c3b5bd3fa183c98a2cea792 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Thu, 19 Jun 2025 12:47:45 +0200 Subject: [PATCH] Implemented --log option to create logfile.log --- main.py | 50 ++++++++++++++++++++++---- tests/unit/test_main.py | 77 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 7 deletions(-) create mode 100644 tests/unit/test_main.py diff --git a/main.py b/main.py index 31fb5b46..44549a0d 100755 --- a/main.py +++ b/main.py @@ -7,6 +7,8 @@ import sys import textwrap import threading import signal +from datetime import datetime +import pty from cli.sounds import Sound @@ -60,17 +62,20 @@ def failure_with_warning_loop(): print("Warnings stopped by user.") if __name__ == "__main__": - # Parse --no-sound early and remove from args + # Parse --no-sound and --log early and remove from args no_sound = False + log_enabled = False if '--no-sound' in sys.argv: no_sound = True sys.argv.remove('--no-sound') + if '--log' in sys.argv: + log_enabled = True + sys.argv.remove('--log') # Setup segfault handler to catch crashes def segv_handler(signum, frame): if not no_sound: Sound.play_finished_failed_sound() - # Loop warning until interrupted try: while True: Sound.play_warning_sound() @@ -91,12 +96,13 @@ if __name__ == "__main__": available_cli_commands = list_cli_commands(cli_dir) - # Handle help invocation + # Handle help invocation if len(sys.argv) == 1 or sys.argv[1] in ('-h', '--help'): print("CyMaIS CLI – proxy to tools in ./cli/") - print("Usage: cymais [--no-sound] [options]") + print("Usage: cymais [--no-sound] [--log] [options]") print("Options:") print(" --no-sound Suppress all sounds during execution") + print(" --log Log all proxied command output to logfile.log") print(" -h, --help Show this help message and exit") print("Available commands:") for cmd in available_cli_commands: @@ -119,10 +125,40 @@ if __name__ == "__main__": cmd_path = os.path.join(cli_dir, f"{args.cli_command}.py") full_cmd = [sys.executable, cmd_path] + args.cli_args + log_file = None + if log_enabled: + log_file_path = os.path.join(script_dir, 'logfile.log') + log_file = open(log_file_path, 'a', encoding='utf-8') + try: - proc = subprocess.Popen(full_cmd) - proc.wait() - rc = proc.returncode + if log_enabled: + # Use a pseudo-terminal to preserve color formatting + master_fd, slave_fd = pty.openpty() + proc = subprocess.Popen( + full_cmd, + stdin=slave_fd, + stdout=slave_fd, + stderr=slave_fd, + text=True + ) + os.close(slave_fd) + with os.fdopen(master_fd) as m: + for line in m: + ts = datetime.now().strftime('%Y-%m-%dT%H:%M:%S') + log_file.write(f"{ts} {line}") + log_file.flush() + # Print raw line (with ANSI escapes) to stdout + print(line, end='') + proc.wait() + rc = proc.returncode + else: + proc = subprocess.Popen(full_cmd) + proc.wait() + rc = proc.returncode + + if log_file: + log_file.close() + if rc != 0: print(f"Command '{args.cli_command}' failed with exit code {rc}.") failure_with_warning_loop() diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py new file mode 100644 index 00000000..77898f22 --- /dev/null +++ b/tests/unit/test_main.py @@ -0,0 +1,77 @@ +# tests/unit/test_main.py + +import os +import sys +import stat +import tempfile +import unittest + +# Insert project root into import path so we can import main.py +sys.path.insert( + 0, + os.path.abspath(os.path.join(os.path.dirname(__file__), "../../")) +) + +import main # assumes main.py lives at the project root + + +class TestMainHelpers(unittest.TestCase): + def test_format_command_help_basic(self): + name = "cmd" + description = "A basic description" + output = main.format_command_help( + name, description, + indent=2, col_width=20, width=40 + ) + # Should start with two spaces and the command name + self.assertTrue(output.startswith(" cmd")) + # Description should appear somewhere in the wrapped text + self.assertIn("A basic description", output) + + def test_list_cli_commands_filters_and_sorts(self): + # Create a temporary directory with sample files + with tempfile.TemporaryDirectory() as tmpdir: + open(os.path.join(tmpdir, "one.py"), "w").close() + open(os.path.join(tmpdir, "__init__.py"), "w").close() + open(os.path.join(tmpdir, "ignore.txt"), "w").close() + open(os.path.join(tmpdir, "two.py"), "w").close() + + # Only 'one' and 'two' should be returned, in sorted order + commands = main.list_cli_commands(tmpdir) + self.assertEqual(commands, ["one", "two"]) + + def test_extract_description_via_help_with_description(self): + # Create a dummy script that prints a help description + with tempfile.TemporaryDirectory() as tmpdir: + script_path = os.path.join(tmpdir, "dummy.py") + with open(script_path, "w") as f: + f.write( + "#!/usr/bin/env python3\n" + "import sys\n" + "if '--help' in sys.argv:\n" + " print('usage: dummy.py [options]')\n" + " print()\n" + " print('This is a help description.')\n" + ) + # Make it executable + mode = os.stat(script_path).st_mode + os.chmod(script_path, mode | stat.S_IXUSR) + + description = main.extract_description_via_help(script_path) + self.assertEqual(description, "This is a help description.") + + def test_extract_description_via_help_without_description(self): + # Script that has no help description + with tempfile.TemporaryDirectory() as tmpdir: + script_path = os.path.join(tmpdir, "empty.py") + with open(script_path, "w") as f: + f.write( + "#!/usr/bin/env python3\n" + "print('no help here')\n" + ) + description = main.extract_description_via_help(script_path) + self.assertEqual(description, "-") + + +if __name__ == "__main__": + unittest.main()