diff --git a/.envrc b/.envrc new file mode 100644 index 0000000000..97b3f16c6f --- /dev/null +++ b/.envrc @@ -0,0 +1,2 @@ +export VIRTUAL_ENV=".venv" +layout python3 diff --git a/.github/workflows/system-tests-backend.yml b/.github/workflows/system-tests-backend.yml index eee6f7f90c..e6bf48a139 100644 --- a/.github/workflows/system-tests-backend.yml +++ b/.github/workflows/system-tests-backend.yml @@ -66,7 +66,7 @@ jobs: - name: Install Python dependencies run: | python3 -m pip install --upgrade pip - python3 -m pip install requests + python3 -m pip install -r requirements.txt - name: Set up Java uses: actions/setup-java@v4 diff --git a/.gitignore b/.gitignore index 52bec85e0b..90f9e885cf 100644 --- a/.gitignore +++ b/.gitignore @@ -20,5 +20,7 @@ distributions/ *.vscode/ sentry-spring-boot-starter-jakarta/src/main/resources/META-INF/spring.factories sentry-samples/sentry-samples-spring-boot-jakarta/spy.log +sentry-mock-server.txt +spring-server.txt spy.log .kotlin diff --git a/Makefile b/Makefile index 0a0f2bbf8c..55f465a966 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all clean compile javadocs dryRelease update checkFormat api assembleBenchmarkTestRelease assembleUiTestRelease assembleUiTestCriticalRelease createCoverageReports runUiTestCritical check preMerge publish systemtest systemtest-interactive +.PHONY: all clean compile javadocs dryRelease update checkFormat api assembleBenchmarkTestRelease assembleUiTestRelease assembleUiTestCriticalRelease createCoverageReports runUiTestCritical setupPython systemTest systemTestInteractive check preMerge publish all: stop clean javadocs compile createCoverageReports assembleBenchmarks: assembleBenchmarkTestRelease @@ -10,6 +10,7 @@ publish: clean dryRelease clean: ./gradlew clean --no-configuration-cache rm -rf distributions + rm -rf .venv # build and run tests compile: @@ -59,13 +60,19 @@ createCoverageReports: ./gradlew jacocoTestReport ./gradlew koverXmlReportRelease +# Create the Python virtual environment for system tests, and install the necessary dependencies +setupPython: + @test -d .venv || python3 -m venv .venv + .venv/bin/pip install --upgrade pip + .venv/bin/pip install -r requirements.txt + # Run system tests for sample applications -systemtest: - python3 test/system-test-runner.py test --all +systemTest: setupPython + .venv/bin/python test/system-test-runner.py test --all # Run system tests with interactive module selection -systemtest-interactive: - python3 test/system-test-runner.py test --interactive +systemTestInteractive: setupPython + .venv/bin/python test/system-test-runner.py test --interactive # Run tests and lint check: diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000..08623cdf27 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +certifi==2025.7.14 +charset-normalizer==3.4.2 +idna==3.10 +requests==2.32.4 +urllib3==2.5.0 diff --git a/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java b/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java index 7ee46649e9..16f5a09d1b 100644 --- a/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java +++ b/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java @@ -66,8 +66,8 @@ public static void main(String[] args) throws InterruptedException { // Enable SDK logging with Debug level options.setDebug(true); // To change the verbosity, use: - // By default it's DEBUG. // options.setDiagnosticLevel(SentryLevel.ERROR); + // By default it's DEBUG. // A good option to have SDK debug log in prod is to use only level ERROR here. // Exclude frames from some packages from being "inApp" so are hidden by default in Sentry @@ -82,16 +82,15 @@ public static void main(String[] args) throws InterruptedException { options.setTracesSampleRate(1.0); // set 0.5 to send 50% of traces // Determine traces sample rate based on the sampling context - // options.setTracesSampler( - // context -> { - // // only 10% of transactions with "/product" prefix will be collected - // if (!context.getTransactionContext().getName().startsWith("/products")) - // { - // return 0.1; - // } else { - // return 0.5; - // } - // }); + // options.setTracesSampler( + // context -> { + // // only 10% of transactions with "/product" prefix will be collected + // if (!context.getTransactionContext().getName().startsWith("/products")) { + // return 0.1; + // } else { + // return 0.5; + // } + // }); }); Sentry.addBreadcrumb( diff --git a/test/system-test-runner.py b/test/system-test-runner.py index aed0d7126a..619e6bda10 100644 --- a/test/system-test-runner.py +++ b/test/system-test-runner.py @@ -42,6 +42,12 @@ from typing import Optional, List, Tuple from dataclasses import dataclass +TERMINAL_COLUMNS: int = 60 +try: + TERMINAL_COLUMNS: int = os.get_terminal_size().columns +except: + pass + def str_to_bool(value: str) -> str: """Convert true/false string to 1/0 string for internal compatibility.""" if value.lower() in ('true', '1'): @@ -51,30 +57,50 @@ def str_to_bool(value: str) -> str: else: raise ValueError(f"Invalid boolean value: {value}. Use 'true' or 'false'") +@dataclass +class ModuleConfig: + """Configuration for a test module.""" + name: str + java_agent: str + java_agent_auto_init: str + build_before_run: str + + def uses_agent(self) -> bool: + """Check if this module uses the Java agent.""" + return str_to_bool(self.java_agent) == "1" + + def needs_build(self) -> bool: + """Check if this module needs to be built before running.""" + return str_to_bool(self.build_before_run) == "1" + + def is_spring_module(self) -> bool: + """Check if this is a Spring Boot module.""" + return "spring" in self.name + @dataclass class InteractiveSelection: """Result of interactive module selection.""" - modules: List[Tuple[str, str, str, str]] + modules: List[ModuleConfig] manual_test_mode: bool build_agent: bool - + def is_empty(self) -> bool: """Check if no modules were selected.""" return len(self.modules) == 0 - + def is_single_module(self) -> bool: """Check if exactly one module was selected.""" return len(self.modules) == 1 - - def get_first_module(self) -> Tuple[str, str, str, str]: + + def get_first_module(self) -> ModuleConfig: """Get the first selected module (for manual test mode).""" if self.is_empty(): raise ValueError("No modules selected") return self.modules[0] - + def has_agent_modules(self) -> bool: """Check if any selected modules use the Java agent.""" - return any(str_to_bool(agent) == "1" for _, agent, _, _ in self.modules) + return any(module_config.uses_agent() for module_config in self.modules) class SystemTestRunner: def __init__(self): @@ -84,16 +110,16 @@ def __init__(self): self.spring_server_pid: Optional[int] = None self.mock_server_pid_file = "sentry-mock-server.pid" self.spring_server_pid_file = "spring-server.pid" - + # Load existing PIDs if available self.mock_server_pid = self.read_pid_file(self.mock_server_pid_file) self.spring_server_pid = self.read_pid_file(self.spring_server_pid_file) - + if self.mock_server_pid: print(f"Found existing mock server PID: {self.mock_server_pid}") if self.spring_server_pid: print(f"Found existing Spring server PID: {self.spring_server_pid}") - + def read_pid_file(self, pid_file: str) -> Optional[int]: """Read PID from file if it exists.""" try: @@ -103,7 +129,7 @@ def read_pid_file(self, pid_file: str) -> Optional[int]: except (ValueError, IOError) as e: print(f"Error reading PID file {pid_file}: {e}") return None - + def is_process_running(self, pid: int) -> bool: """Check if a process with given PID is still running.""" try: @@ -112,14 +138,14 @@ def is_process_running(self, pid: int) -> bool: return True except (OSError, ProcessLookupError): return False - + def kill_process(self, pid: int, name: str) -> None: """Kill a process by PID.""" try: print(f"Killing existing {name} process with PID {pid}") os.kill(pid, signal.SIGTERM) time.sleep(2) # Give it time to terminate gracefully - + # Check if it's still running and force kill if necessary if self.is_process_running(pid): print(f"Process {pid} didn't terminate gracefully, force killing...") @@ -127,9 +153,9 @@ def kill_process(self, pid: int, name: str) -> None: time.sleep(1) except (OSError, ProcessLookupError): print(f"Process {pid} was already dead") - - + + def start_sentry_mock_server(self) -> None: """Start the Sentry mock server.""" print("Starting Sentry mock server...") @@ -141,21 +167,21 @@ def start_sentry_mock_server(self) -> None: stdout=log_file, stderr=subprocess.STDOUT ) - + # Store PID in instance variable and write to file self.mock_server_pid = self.mock_server_process.pid with open(self.mock_server_pid_file, "w") as pid_file: pid_file.write(str(self.mock_server_pid)) - + print(f"Started mock server with PID {self.mock_server_pid}") - + # Wait a moment for the server to start time.sleep(2) - + except Exception as e: print(f"Failed to start mock server: {e}") raise - + def stop_sentry_mock_server(self) -> None: """Stop the Sentry mock server.""" try: @@ -165,7 +191,7 @@ def stop_sentry_mock_server(self) -> None: print("Sent stop signal to mock server") except: print("Could not send graceful stop signal") - + # Kill the process - try process object first, then PID from file if self.mock_server_process and self.mock_server_process.poll() is None: print(f"Killing mock server process object with PID {self.mock_server_process.pid}") @@ -174,7 +200,7 @@ def stop_sentry_mock_server(self) -> None: elif self.mock_server_pid and self.is_process_running(self.mock_server_pid): print(f"Killing mock server from PID file with PID {self.mock_server_pid}") self.kill_process(self.mock_server_pid, "mock server") - + except Exception as e: print(f"Error stopping mock server: {e}") finally: @@ -182,56 +208,56 @@ def stop_sentry_mock_server(self) -> None: if os.path.exists(self.mock_server_pid_file): os.remove(self.mock_server_pid_file) self.mock_server_pid = None - + def find_agent_jar(self) -> Optional[str]: """Find the OpenTelemetry agent JAR file.""" agent_dir = Path("sentry-opentelemetry/sentry-opentelemetry-agent/build/libs/") if not agent_dir.exists(): return None - + for jar_file in agent_dir.glob("*.jar"): name = jar_file.name - if ("agent" in name and - "javadoc" not in name and - "sources" not in name and + if ("agent" in name and + "javadoc" not in name and + "sources" not in name and "dontuse" not in name): return str(jar_file) return None - + def build_agent_jar(self) -> int: """Build the OpenTelemetry agent JAR file.""" print("Building OpenTelemetry agent JAR...") return self.run_gradle_task(":sentry-opentelemetry:sentry-opentelemetry-agent:assemble") - + def ensure_agent_jar(self, skip_build: bool = False) -> Optional[str]: """Ensure the OpenTelemetry agent JAR exists, building it if necessary.""" agent_jar = self.find_agent_jar() if agent_jar: return agent_jar - + if skip_build: print("OpenTelemetry agent JAR not found and build was skipped") return None - + # Agent JAR doesn't exist, try to build it print("OpenTelemetry agent JAR not found, building it...") build_result = self.build_agent_jar() if build_result != 0: print("Failed to build OpenTelemetry agent JAR") return None - + # Try to find it again after building agent_jar = self.find_agent_jar() if not agent_jar: print("OpenTelemetry agent JAR still not found after building") return None - + return agent_jar - + def start_spring_server(self, sample_module: str, java_agent: str, java_agent_auto_init: str) -> None: """Start a Spring Boot server for testing.""" print(f"Starting Spring server for {sample_module}...") - + # Build environment variables env = os.environ.copy() env.update({ @@ -239,15 +265,15 @@ def start_spring_server(self, sample_module: str, java_agent: str, java_agent_au "SENTRY_AUTO_INIT": java_agent_auto_init, "SENTRY_TRACES_SAMPLE_RATE": "1.0", "OTEL_TRACES_EXPORTER": "none", - "OTEL_METRICS_EXPORTER": "none", + "OTEL_METRICS_EXPORTER": "none", "OTEL_LOGS_EXPORTER": "none", "SENTRY_LOGS_ENABLED": "true" }) - + # Build command jar_path = f"sentry-samples/{sample_module}/build/libs/{sample_module}-0.0.1-SNAPSHOT.jar" cmd = ["java"] - + if java_agent == "1": agent_jar = self.ensure_agent_jar() if agent_jar: @@ -255,9 +281,9 @@ def start_spring_server(self, sample_module: str, java_agent: str, java_agent_au print(f"Using Java Agent: {agent_jar}") else: print("Warning: Java agent was requested but could not be found or built") - + cmd.extend(["-jar", jar_path]) - + try: # Start the Spring server with open("spring-server.txt", "w") as log_file: @@ -267,22 +293,22 @@ def start_spring_server(self, sample_module: str, java_agent: str, java_agent_au stdout=log_file, stderr=subprocess.STDOUT ) - + # Store PID in instance variable and write to file self.spring_server_pid = self.spring_server_process.pid with open(self.spring_server_pid_file, "w") as pid_file: pid_file.write(str(self.spring_server_pid)) - + print(f"Started Spring server with PID {self.spring_server_pid}") - + except Exception as e: print(f"Failed to start Spring server: {e}") raise - + def wait_for_spring(self, max_attempts: int = 20) -> bool: """Wait for Spring Boot application to be ready.""" print("Waiting for Spring application to be ready...") - + for attempt in range(1, max_attempts + 1): try: response = requests.head( @@ -295,13 +321,13 @@ def wait_for_spring(self, max_attempts: int = 20) -> bool: return True except: pass - + print(f"Waiting... (attempt {attempt}/{max_attempts})") time.sleep(1) - + print("Spring application failed to become ready") return False - + def get_spring_status(self) -> dict: """Get status of Spring Boot application.""" status = { @@ -309,10 +335,10 @@ def get_spring_status(self) -> dict: "pid": self.spring_server_pid, "http_ready": False } - + if self.spring_server_pid and self.is_process_running(self.spring_server_pid): status["process_running"] = True - + # Check HTTP endpoint try: response = requests.head( @@ -324,9 +350,9 @@ def get_spring_status(self) -> dict: status["http_ready"] = True except: pass - + return status - + def get_sentry_status(self) -> dict: """Get status of Sentry mock server.""" status = { @@ -334,10 +360,10 @@ def get_sentry_status(self) -> dict: "pid": self.mock_server_pid, "http_ready": False } - + if self.mock_server_pid and self.is_process_running(self.mock_server_pid): status["process_running"] = True - + # Check HTTP endpoint try: response = requests.get("http://127.0.0.1:8000/envelope-count", timeout=2) @@ -345,25 +371,25 @@ def get_sentry_status(self) -> dict: status["http_ready"] = True except: pass - + return status - + def print_status_summary(self) -> None: """Print status summary of all services.""" print("=== Service Status ===") - + sentry_status = self.get_sentry_status() print(f"Sentry Mock Server:") print(f" PID: {sentry_status['pid'] or 'None'}") print(f" Process Running: {'✅' if sentry_status['process_running'] else '❌'}") print(f" HTTP Ready: {'✅' if sentry_status['http_ready'] else '❌'}") - + spring_status = self.get_spring_status() print(f"Spring Boot App:") print(f" PID: {spring_status['pid'] or 'None'}") print(f" Process Running: {'✅' if spring_status['process_running'] else '❌'}") print(f" HTTP Ready: {'✅' if spring_status['http_ready'] else '❌'}") - + def stop_spring_server(self) -> None: """Stop the Spring Boot server.""" try: @@ -378,7 +404,7 @@ def stop_spring_server(self) -> None: elif self.spring_server_pid and self.is_process_running(self.spring_server_pid): print(f"Killing Spring server from PID file with PID {self.spring_server_pid}") self.kill_process(self.spring_server_pid, "Spring server") - + except Exception as e: print(f"Error stopping Spring server: {e}") finally: @@ -386,17 +412,17 @@ def stop_spring_server(self) -> None: if os.path.exists(self.spring_server_pid_file): os.remove(self.spring_server_pid_file) self.spring_server_pid = None - + def get_build_task(self, sample_module: str) -> str: """Get the appropriate build task for a module.""" return "bootJar" if "spring" in sample_module else "assemble" - + def build_module(self, sample_module: str) -> int: """Build a sample module using the appropriate task.""" build_task = self.get_build_task(sample_module) print(f"Building {sample_module} using {build_task} task") return self.run_gradle_task(f":sentry-samples:{sample_module}:{build_task}") - + def run_gradle_task(self, task: str) -> int: """Run a Gradle task and return the exit code.""" print(f"Running: ./gradlew {task}") @@ -406,8 +432,8 @@ def run_gradle_task(self, task: str) -> int: except Exception as e: print(f"Failed to run Gradle task: {e}") return 1 - - def setup_test_infrastructure(self, sample_module: str, java_agent: str, + + def setup_test_infrastructure(self, sample_module: str, java_agent: str, java_agent_auto_init: str, build_before_run: str) -> int: """Set up test infrastructure. Returns 0 on success, error code on failure.""" # Build if requested @@ -417,18 +443,18 @@ def setup_test_infrastructure(self, sample_module: str, java_agent: str, if build_result != 0: print("Build failed") return build_result - + # Ensure agent JAR is available if needed if java_agent == "1": agent_jar = self.ensure_agent_jar() if not agent_jar: print("Error: Java agent was requested but could not be found or built") return 1 - + # Start mock server print("Starting Sentry mock server...") self.start_sentry_mock_server() - + # Start Spring server if it's a Spring module if "spring" in sample_module: print(f"Starting Spring server for {sample_module}...") @@ -437,65 +463,65 @@ def setup_test_infrastructure(self, sample_module: str, java_agent: str, print("Spring application failed to start!") return 1 print("Spring application is ready!") - + return 0 - - def run_single_test(self, sample_module: str, java_agent: str, + + def run_single_test(self, sample_module: str, java_agent: str, java_agent_auto_init: str, build_before_run: str) -> int: """Run a single system test.""" print(f"Running system test for {sample_module}") - + try: # Set up infrastructure setup_result = self.setup_test_infrastructure(sample_module, java_agent, java_agent_auto_init, build_before_run) if setup_result != 0: return setup_result - + # Run the system test test_result = self.run_gradle_task(f":sentry-samples:{sample_module}:systemTest") - + return test_result - + finally: # Cleanup if "spring" in sample_module: self.stop_spring_server() self.stop_sentry_mock_server() - + def run_all_tests(self) -> int: """Run all system tests.""" test_configs = self.get_available_modules() - + failed_tests = [] - - for sample_module, java_agent, java_agent_auto_init, build_before_run in test_configs: + + for i, module_config in enumerate(test_configs): # Convert true/false to internal 1/0 format - agent = str_to_bool(java_agent) - auto_init = java_agent_auto_init # already in correct format - build = str_to_bool(build_before_run) - - print(f"\n{'='*60}") - print(f"Running test: {sample_module} (agent={java_agent}, auto_init={java_agent_auto_init})") - print(f"{'='*60}") - - result = self.run_single_test(sample_module, agent, auto_init, build) - + agent = str_to_bool(module_config.java_agent) + auto_init = module_config.java_agent_auto_init # already in correct format + build = str_to_bool(module_config.build_before_run) + + print(f"\n{'='*TERMINAL_COLUMNS}") + print(f"Running test {i + 1}/{len(test_configs)}: {module_config.name} (agent={module_config.java_agent}, auto_init={module_config.java_agent_auto_init})") + print(f"{'='*TERMINAL_COLUMNS}") + + result = self.run_single_test(module_config.name, agent, auto_init, build) + if result != 0: # Find the module number in the full list for interactive reference - module_number = self._find_module_number(sample_module, java_agent, java_agent_auto_init) - failed_tests.append((module_number, sample_module, java_agent, java_agent_auto_init)) - print(f"❌ Test failed: {sample_module}") + module_number = self._find_module_number(module_config.name, module_config.java_agent, module_config.java_agent_auto_init) + failed_tests.append((module_number, module_config.name, module_config.java_agent, module_config.java_agent_auto_init)) + print(f"❌ Test failed: {module_config.name}") else: - print(f"✅ Test passed: {sample_module}") - + print(f"✅ Test passed: {module_config.name}") + # Summary - print(f"\n{'='*60}") + print(f"\n{'='*TERMINAL_COLUMNS}") print("TEST SUMMARY") - print(f"{'='*60}") + print(f"{'='*TERMINAL_COLUMNS}") print(f"Total tests: {len(test_configs)}") print(f"Passed: {len(test_configs) - len(failed_tests)}") print(f"Failed: {len(failed_tests)}") - + if failed_tests: print("\nFailed tests (for interactive mode, use these numbers):") for module_number, sample_module, java_agent, java_agent_auto_init in failed_tests: @@ -504,22 +530,22 @@ def run_all_tests(self) -> int: else: print("\n🎉 All tests passed!") return 0 - - def run_manual_test_mode(self, sample_module: str, java_agent: str, + + def run_manual_test_mode(self, sample_module: str, java_agent: str, java_agent_auto_init: str, build_before_run: str) -> int: """Set up infrastructure for manual testing from IDE.""" print(f"Setting up manual test environment for {sample_module}") - + try: # Set up infrastructure setup_result = self.setup_test_infrastructure(sample_module, java_agent, java_agent_auto_init, build_before_run) if setup_result != 0: return setup_result - + # Show status and wait for user - print("\n" + "="*60) + print("\n" + "="*TERMINAL_COLUMNS) print("🚀 Manual test environment ready 🚀") - print("="*60) + print("="*TERMINAL_COLUMNS) self.print_status_summary() print(f"\nInfrastructure is ready for manual testing of: {sample_module}") print("You can now run your system tests from your IDE.") @@ -528,57 +554,59 @@ def run_manual_test_mode(self, sample_module: str, java_agent: str, print(f" - Java Agent: {'Yes' if java_agent == '1' else 'No'}") print(f" - Agent Auto-init: {java_agent_auto_init}") print(f" - Mock DSN: http://502f25099c204a2fbf4cb16edc5975d1@localhost:8000/0") - + if "spring" in sample_module: print("\nSpring Boot app is running on: http://localhost:8080") - + print("\nPress Enter to stop the infrastructure and exit...") - + # Wait for user input try: input() except KeyboardInterrupt: print("\nReceived interrupt signal") - + print("\nStopping infrastructure...") return 0 - + finally: # Cleanup will happen in the finally block of main() pass - - def get_available_modules(self) -> List[Tuple[str, str, str, str]]: + + def get_available_modules(self) -> List[ModuleConfig]: """Get list of all available test modules.""" return [ - ("sentry-samples-spring-boot", "false", "true", "false"), - ("sentry-samples-spring-boot-opentelemetry-noagent", "false", "true", "false"), - ("sentry-samples-spring-boot-opentelemetry", "true", "true", "false"), - ("sentry-samples-spring-boot-opentelemetry", "true", "false", "false"), - ("sentry-samples-spring-boot-webflux-jakarta", "false", "true", "false"), - ("sentry-samples-spring-boot-webflux", "false", "true", "false"), - ("sentry-samples-spring-boot-jakarta", "false", "true", "false"), - ("sentry-samples-spring-boot-jakarta-opentelemetry-noagent", "false", "true", "false"), - ("sentry-samples-spring-boot-jakarta-opentelemetry", "true", "true", "false"), - ("sentry-samples-spring-boot-jakarta-opentelemetry", "true", "false", "false"), - ("sentry-samples-console", "false", "true", "false"), + ModuleConfig("sentry-samples-spring-boot", "false", "true", "false"), + ModuleConfig("sentry-samples-spring-boot-opentelemetry-noagent", "false", "true", "false"), + ModuleConfig("sentry-samples-spring-boot-opentelemetry", "true", "true", "false"), + ModuleConfig("sentry-samples-spring-boot-opentelemetry", "true", "false", "false"), + ModuleConfig("sentry-samples-spring-boot-webflux-jakarta", "false", "true", "false"), + ModuleConfig("sentry-samples-spring-boot-webflux", "false", "true", "false"), + ModuleConfig("sentry-samples-spring-boot-jakarta", "false", "true", "false"), + ModuleConfig("sentry-samples-spring-boot-jakarta-opentelemetry-noagent", "false", "true", "false"), + ModuleConfig("sentry-samples-spring-boot-jakarta-opentelemetry", "true", "true", "false"), + ModuleConfig("sentry-samples-spring-boot-jakarta-opentelemetry", "true", "false", "false"), + ModuleConfig("sentry-samples-console", "false", "true", "false"), ] - + def _find_module_number(self, module_name: str, agent: str, auto_init: str) -> int: """Find the module number in the interactive list (1-based).""" modules = self.get_available_modules() - for i, (mod_name, mod_agent, mod_auto_init, _) in enumerate(modules, 1): - if mod_name == module_name and mod_agent == agent and mod_auto_init == auto_init: + for i, module_config in enumerate(modules, 1): + if (module_config.name == module_name and + module_config.java_agent == agent and + module_config.java_agent_auto_init == auto_init): return i return 0 # Should not happen, but return 0 if not found - + def parse_selection(self, user_input: str, max_index: int) -> List[int]: """Parse user selection string into list of indices.""" if user_input.strip() == "*": return list(range(max_index)) - + indices = [] parts = user_input.split(",") - + for part in parts: part = part.strip() if "-" in part: @@ -596,34 +624,34 @@ def parse_selection(self, user_input: str, max_index: int) -> List[int]: indices.append(int(part) - 1) except ValueError: raise ValueError(f"Invalid number: {part}") - + # Remove duplicates and sort indices = sorted(set(indices)) - + # Validate indices for idx in indices: if idx < 0 or idx >= max_index: raise ValueError(f"Index {idx + 1} is out of range (1-{max_index})") - + return indices - + def interactive_module_selection(self) -> InteractiveSelection: """Display modules and get user selection.""" modules = self.get_available_modules() - + print("\nAvailable test modules:") print("=" * 80) - for i, (module, agent, auto_init, build) in enumerate(modules, 1): - agent_text = "with agent" if str_to_bool(agent) == "1" else "no agent" - auto_init_text = f"auto-init: {auto_init}" - print(f"{i:2d}. {module:<50} ({agent_text}, {auto_init_text})") - + for i, module_config in enumerate(modules, 1): + agent_text = "with agent" if module_config.uses_agent() else "no agent" + auto_init_text = f"auto-init: {module_config.java_agent_auto_init}" + print(f"{i:2d}. {module_config.name:<50} ({agent_text}, {auto_init_text})") + print("\nSelection options:") print(" * = all modules") print(" Single: 1, 5, 8") print(" Range: 1-4, 6-8") print(" Combined: 1,2,4-5,8") - + selected_modules = [] while True: try: @@ -631,29 +659,29 @@ def interactive_module_selection(self) -> InteractiveSelection: if not user_input: print("Please enter a selection.") continue - + selected_indices = self.parse_selection(user_input, len(modules)) selected_modules = [modules[i] for i in selected_indices] - + # Show confirmation print(f"\nSelected {len(selected_modules)} module(s):") - for i, (module, agent, auto_init, build) in enumerate(selected_modules, 1): - agent_text = "with agent" if str_to_bool(agent) == "1" else "no agent" - print(f" {i}. {module} ({agent_text}, auto-init: {auto_init})") - + for i, module_config in enumerate(selected_modules, 1): + agent_text = "with agent" if module_config.uses_agent() else "no agent" + print(f" {i}. {module_config.name} ({agent_text}, auto-init: {module_config.java_agent_auto_init})") + confirm = input("\nProceed with these selections? [Y/n]: ").strip().lower() if confirm in ('', 'y', 'yes'): break else: print("Please make a new selection.") - + except ValueError as e: print(f"Error: {e}") print("Please try again.") except KeyboardInterrupt: print("\nOperation cancelled.") return InteractiveSelection(modules=[], manual_test_mode=False, build_agent=False) - + # Ask about test mode manual_test_mode = False while True: @@ -670,10 +698,10 @@ def interactive_module_selection(self) -> InteractiveSelection: except KeyboardInterrupt: print("\nOperation cancelled.") return InteractiveSelection(modules=[], manual_test_mode=False, build_agent=False) - + # Ask about building agent if any modules use it build_agent = False - has_agent_modules = any(str_to_bool(agent) == "1" for _, agent, _, _ in selected_modules) + has_agent_modules = any(module_config.uses_agent() for module_config in selected_modules) if has_agent_modules: while True: try: @@ -689,17 +717,17 @@ def interactive_module_selection(self) -> InteractiveSelection: except KeyboardInterrupt: print("\nOperation cancelled.") return InteractiveSelection(modules=[], manual_test_mode=False, build_agent=False) - + return InteractiveSelection(modules=selected_modules, manual_test_mode=manual_test_mode, build_agent=build_agent) - + def run_interactive_tests(self, agent: str, auto_init: str, build: str) -> int: """Run tests with interactive module selection.""" selection = self.interactive_module_selection() - + if selection.is_empty(): print("No modules selected. Exiting.") return 0 - + # Build agent JAR if requested and modules use agent if selection.build_agent and selection.has_agent_modules(): print("\nBuilding OpenTelemetry agent JAR...") @@ -708,55 +736,55 @@ def run_interactive_tests(self, agent: str, auto_init: str, build: str) -> int: print("Failed to build OpenTelemetry agent JAR") return build_result print("✅ OpenTelemetry agent JAR built successfully") - + # Handle manual test mode if selection.manual_test_mode: if not selection.is_single_module(): print("Error: Manual test mode can only be used with a single module.") print("Please select only one module for manual testing.") return 1 - - sample_module, test_agent, test_auto_init, test_build = selection.get_first_module() + + module_config = selection.get_first_module() # Convert true/false to internal 1/0 format - agent = str_to_bool(test_agent) - auto_init = test_auto_init # already in correct format - build = str_to_bool(test_build) - - print(f"\nSetting up manual test environment for: {sample_module}") - return self.run_manual_test_mode(sample_module, agent, auto_init, build) - + agent = str_to_bool(module_config.java_agent) + auto_init = module_config.java_agent_auto_init # already in correct format + build = str_to_bool(module_config.build_before_run) + + print(f"\nSetting up manual test environment for: {module_config.name}") + return self.run_manual_test_mode(module_config.name, agent, auto_init, build) + # Handle automatic test running failed_tests = [] - - for i, (sample_module, test_agent, test_auto_init, test_build) in enumerate(selection.modules, 1): + + for i, module_config in enumerate(selection.modules, 1): # Convert true/false to internal 1/0 format - agent = str_to_bool(test_agent) - auto_init = test_auto_init # already in correct format - build = str_to_bool(test_build) - - print(f"\n{'='*60}") - print(f"Running test {i}/{len(selection.modules)}: {sample_module}") - print(f"Agent: {test_agent}, Auto-init: {test_auto_init}") - print(f"{'='*60}") - - result = self.run_single_test(sample_module, agent, auto_init, build) - + agent = str_to_bool(module_config.java_agent) + auto_init = module_config.java_agent_auto_init # already in correct format + build = str_to_bool(module_config.build_before_run) + + print(f"\n{'='*TERMINAL_COLUMNS}") + print(f"Running test {i}/{len(selection.modules)}: {module_config.name}") + print(f"Agent: {module_config.java_agent}, Auto-init: {module_config.java_agent_auto_init}") + print(f"{'='*TERMINAL_COLUMNS}") + + result = self.run_single_test(module_config.name, agent, auto_init, build) + if result != 0: # Find the module number in the full list for interactive reference - module_number = self._find_module_number(sample_module, test_agent, test_auto_init) - failed_tests.append((module_number, sample_module, test_agent, test_auto_init)) - print(f"❌ Test failed: {sample_module}") + module_number = self._find_module_number(module_config.name, module_config.java_agent, module_config.java_agent_auto_init) + failed_tests.append((module_number, module_config.name, module_config.java_agent, module_config.java_agent_auto_init)) + print(f"❌ Test failed: {module_config.name}") else: - print(f"✅ Test passed: {sample_module}") - + print(f"✅ Test passed: {module_config.name}") + # Summary - print(f"\n{'='*60}") + print(f"\n{'='*TERMINAL_COLUMNS}") print("TEST SUMMARY") - print(f"{'='*60}") + print(f"{'='*TERMINAL_COLUMNS}") print(f"Total tests: {len(selection.modules)}") print(f"Passed: {len(selection.modules) - len(failed_tests)}") print(f"Failed: {len(failed_tests)}") - + if failed_tests: print("\nFailed tests (for interactive mode, use these numbers):") for module_number, sample_module, test_agent, test_auto_init in failed_tests: @@ -765,7 +793,7 @@ def run_interactive_tests(self, agent: str, auto_init: str, build: str) -> int: else: print("\n🎉 All tests passed!") return 0 - + def cleanup_on_exit(self, signum, frame): """Cleanup handler for signals.""" print(f"\nReceived signal {signum}, cleaning up...") @@ -776,7 +804,7 @@ def cleanup_on_exit(self, signum, frame): def main(): parser = argparse.ArgumentParser(description="System Test Runner for Sentry Java") subparsers = parser.add_subparsers(dest="command", help="Available commands") - + # Test subcommand test_parser = subparsers.add_parser("test", help="Run system tests") test_group = test_parser.add_mutually_exclusive_group(required=True) @@ -787,54 +815,54 @@ def main(): test_parser.add_argument("--auto-init", default="true", help="Auto-init agent (true or false)") test_parser.add_argument("--build", default="false", help="Build before running (true or false)") test_parser.add_argument("--manual-test", action="store_true", help="Set up infrastructure but pause for manual testing from IDE") - + # Spring subcommand spring_parser = subparsers.add_parser("spring", help="Manage Spring Boot applications") spring_subparsers = spring_parser.add_subparsers(dest="spring_action", help="Spring actions") - + spring_start_parser = spring_subparsers.add_parser("start", help="Start Spring Boot application") spring_start_parser.add_argument("module", help="Sample module to start") spring_start_parser.add_argument("--agent", default="false", help="Use Java agent (true or false)") spring_start_parser.add_argument("--auto-init", default="true", help="Auto-init agent (true or false)") spring_start_parser.add_argument("--build", default="false", help="Build before starting (true or false)") - + spring_stop_parser = spring_subparsers.add_parser("stop", help="Stop Spring Boot application") - + spring_wait_parser = spring_subparsers.add_parser("wait", help="Wait for Spring Boot application to be ready") spring_wait_parser.add_argument("--timeout", type=int, default=20, help="Max attempts to wait (default: 20)") - + spring_status_parser = spring_subparsers.add_parser("status", help="Check Spring Boot application status") - + # Sentry subcommand sentry_parser = subparsers.add_parser("sentry", help="Manage Sentry mock server") sentry_subparsers = sentry_parser.add_subparsers(dest="sentry_action", help="Sentry actions") - + sentry_start_parser = sentry_subparsers.add_parser("start", help="Start Sentry mock server") sentry_stop_parser = sentry_subparsers.add_parser("stop", help="Stop Sentry mock server") sentry_status_parser = sentry_subparsers.add_parser("status", help="Check Sentry mock server status") - + # Status subcommand status_parser = subparsers.add_parser("status", help="Show status of all services") - + args = parser.parse_args() - + if not args.command: parser.print_help() return 1 - + runner = SystemTestRunner() - + # Set up signal handlers for cleanup signal.signal(signal.SIGINT, runner.cleanup_on_exit) signal.signal(signal.SIGTERM, runner.cleanup_on_exit) - + try: if args.command == "test": # Convert true/false arguments to internal 1/0 format agent = str_to_bool(args.agent) auto_init = args.auto_init # already accepts true/false build = str_to_bool(args.build) - + if args.manual_test and args.module: return runner.run_manual_test_mode(args.module, agent, auto_init, build) elif args.manual_test and args.all: @@ -849,14 +877,14 @@ def main(): return runner.run_single_test(args.module, agent, auto_init, build) elif args.interactive: return runner.run_interactive_tests(agent, auto_init, build) - + elif args.command == "spring": if args.spring_action == "start": # Convert true/false arguments to internal format agent = str_to_bool(args.agent) auto_init = args.auto_init # already accepts true/false build = str_to_bool(args.build) - + # Build if requested if build == "1": print("Building before starting Spring application") @@ -864,7 +892,7 @@ def main(): if build_result != 0: print("Build failed") return build_result - + runner.start_spring_server(args.module, agent, auto_init) if runner.wait_for_spring(): print("Spring application started successfully!") @@ -893,7 +921,7 @@ def main(): else: spring_parser.print_help() return 1 - + elif args.command == "sentry": if args.sentry_action == "start": runner.start_sentry_mock_server() @@ -913,14 +941,14 @@ def main(): else: sentry_parser.print_help() return 1 - + elif args.command == "status": runner.print_status_summary() return 0 else: parser.print_help() return 1 - + except KeyboardInterrupt: print("\nInterrupted by user") return 1 @@ -934,4 +962,4 @@ def main(): runner.stop_sentry_mock_server() if __name__ == "__main__": - sys.exit(main()) \ No newline at end of file + sys.exit(main())