diff --git a/.github/workflows/system-tests-backend.yml b/.github/workflows/system-tests-backend.yml deleted file mode 100644 index 2d5b87a2c35..00000000000 --- a/.github/workflows/system-tests-backend.yml +++ /dev/null @@ -1,78 +0,0 @@ -name: 'System Tests Backend' - -on: - push: - branches: - - main - pull_request: - paths-ignore: - - '*android*/**' - - 'sentry-compose/**' - - 'sentry-samples/sentry-samples-android/**' - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - system-test: - runs-on: ubuntu-latest - continue-on-error: true - env: - SENTRY_URL: http://127.0.0.1:8000 - GRADLE_ENCRYPTION_KEY: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - strategy: - fail-fast: false - matrix: - sample: [ "sentry-samples-logback" ] - agent: [ "false" ] - agent-auto-init: [ "true" ] - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - submodules: 'recursive' - - - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - with: - python-version: '3.10' - - - name: Install Python dependencies - run: | - python3 -m pip install --upgrade pip - python3 -m pip install -r requirements.txt - - - name: Set up Java - uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6.1.0 - with: - distribution: 'temurin' - java-version: '21' - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0 - with: - cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - - - name: Build and run system tests - run: | - python3 test/system-test-runner.py test --module "${{ matrix.sample }}" --agent "${{ matrix.agent }}" --auto-init "${{ matrix.agent-auto-init }}" --build "true" - - - name: Upload test results - if: always() - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 - with: - name: test-results-${{ matrix.sample }}-${{ matrix.agent }}-${{ matrix.agent-auto-init }}-system-test - path: | - **/build/reports/* - sentry-mock-server.txt - spring-server.txt - - - name: Test Report - uses: phoenix-actions/test-reporting@7317eea6e13c47348dd0bb318669485157c518d6 # pin@v16 - if: always() - with: - name: JUnit System Tests ${{ matrix.sample }} - path: | - **/build/test-results/**/*.xml - reporter: java-junit - output-to: step-summary - fail-on-error: false diff --git a/Makefile b/Makefile index 3941eb93ec3..7a57ed54e09 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all clean compile javadocs dryRelease update checkFormat api assembleUiTestRelease assembleUiTestCriticalRelease runUiTestCritical setupPython systemTest systemTestInteractive check preMerge publish +.PHONY: all clean compile javadocs dryRelease update checkFormat api assembleUiTestRelease assembleUiTestCriticalRelease runUiTestCritical check preMerge publish all: stop clean javadocs compile assembleUiTests: assembleUiTestRelease @@ -46,20 +46,6 @@ assembleUiTestCriticalRelease: runUiTestCritical: ./scripts/test-ui-critical.sh -# 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: setupPython - .venv/bin/python test/system-test-runner.py test --all - -# Run system tests with interactive module selection -systemTestInteractive: setupPython - .venv/bin/python test/system-test-runner.py test --interactive - # Run tests and lint check: ./gradlew check diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 62c248385cd..00000000000 --- a/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -certifi==2026.10.19 -charset-normalizer==3.4.2 -idna==3.15 -requests==2.33.0 -urllib3==2.8.0 diff --git a/sentry-logback/src/test/kotlin/io/sentry/logback/SentryAppenderIntegrationTest.kt b/sentry-logback/src/test/kotlin/io/sentry/logback/SentryAppenderIntegrationTest.kt new file mode 100644 index 00000000000..8f654b3d038 --- /dev/null +++ b/sentry-logback/src/test/kotlin/io/sentry/logback/SentryAppenderIntegrationTest.kt @@ -0,0 +1,68 @@ +package io.sentry.logback + +import ch.qos.logback.classic.LoggerContext +import ch.qos.logback.classic.joran.JoranConfigurator +import com.google.common.truth.Truth.assertThat +import io.sentry.Sentry +import io.sentry.SentryLevel +import io.sentry.test.MockSentryServer +import io.sentry.test.event +import io.sentry.test.hasFlag +import io.sentry.test.logs +import kotlin.test.AfterTest +import kotlin.test.Test +import org.slf4j.MDC + +class SentryAppenderIntegrationTest { + private val server = MockSentryServer() + private val loggerContext = LoggerContext() + + @AfterTest + fun teardown() { + loggerContext.stop() + Sentry.close() + server.close() + MDC.clear() + } + + @Test + fun `appender configured from XML sends events, breadcrumbs and logs`() { + loggerContext.putProperty("dsn", server.dsn) + JoranConfigurator() + .apply { context = loggerContext } + .doConfigure(javaClass.getResource("/sentry-appender-integration-test.xml")) + val logger = loggerContext.getLogger(SentryAppenderIntegrationTest::class.java) + + logger.debug("Hello Sentry!") + MDC.put("userId", "user-1") + Sentry.setAttribute("user.type", "admin") + Sentry.setAttribute("feature.version", 2) + Sentry.setAttribute("debug.enabled", true) + Sentry.addFeatureFlag("my-feature-flag", true) + logger.warn("important warning") + logger.info("User has made a purchase of product: {}", 445) + logger.error("Something went wrong", RuntimeException("Invalid productId=445")) + Sentry.flush(5000) + + val warning = server.awaitEnvelope { it.event()?.level == SentryLevel.WARNING }.event()!! + assertThat(warning.message!!.message).isEqualTo("important warning") + assertThat(warning.tags).containsEntry("userId", "user-1") + assertThat(warning.hasFlag("my-feature-flag", true)).isTrue() + + val error = server.awaitEnvelope { it.event()?.level == SentryLevel.ERROR }.event()!! + assertThat(error.message!!.formatted).isEqualTo("Something went wrong") + assertThat(error.exceptions!!.map { it.type to it.value }) + .contains("RuntimeException" to "Invalid productId=445") + assertThat(error.breadcrumbs!!.map { it.message to it.level }) + .containsAtLeast( + "Hello Sentry!" to SentryLevel.DEBUG, + "User has made a purchase of product: 445" to SentryLevel.INFO, + ) + + val logs = server.awaitEnvelope { it.logs() != null }.logs()!! + assertThat(logs.items.map { it.body }).contains("User has made a purchase of product: 445") + val failureLog = logs.items.single { it.body == "Something went wrong" } + assertThat(failureLog.attributes!!.mapValues { it.value.value }) + .containsAtLeast("user.type", "admin", "feature.version", 2, "debug.enabled", true) + } +} diff --git a/sentry-logback/src/test/resources/sentry-appender-integration-test.xml b/sentry-logback/src/test/resources/sentry-appender-integration-test.xml new file mode 100644 index 00000000000..049317a1bcb --- /dev/null +++ b/sentry-logback/src/test/resources/sentry-appender-integration-test.xml @@ -0,0 +1,18 @@ + + + + ${dsn} + userId + + true + + + WARN + DEBUG + INFO + + + + + + diff --git a/sentry-samples/sentry-samples-logback/build.gradle.kts b/sentry-samples/sentry-samples-logback/build.gradle.kts index 6d6e7051030..12644bf2ded 100644 --- a/sentry-samples/sentry-samples-logback/build.gradle.kts +++ b/sentry-samples/sentry-samples-logback/build.gradle.kts @@ -1,32 +1,15 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - plugins { java application - alias(libs.plugins.kotlin.jvm) alias(libs.plugins.gradle.versions) alias(libs.plugins.shadow) - id("io.sentry.systemtest") } application { mainClass.set("io.sentry.samples.logback.Main") } -tasks.withType().configureEach { - kotlin { - compilerOptions.freeCompilerArgs = listOf("-Xjsr305=strict") - } -} - dependencies { implementation(projects.sentryLogback) implementation(libs.logback.classic) - - testImplementation(kotlin(Config.kotlinStdLib)) - testImplementation(projects.sentry) - testImplementation(projects.sentrySystemTestSupport) - testImplementation(libs.kotlin.test.junit) - testImplementation(libs.slf4j.api) - testImplementation(libs.slf4j.jdk14) } // Configure the Shadow JAR (executable JAR with all dependencies) @@ -44,22 +27,3 @@ tasks.jar { // Fix the startScripts task dependency tasks.startScripts { dependsOn(tasks.shadowJar) } - -tasks.register("systemTest").configure { - group = "verification" - description = "Runs the System tests" - - val test = project.extensions.getByType()["test"] - testClassesDirs = test.output.classesDirs - classpath = test.runtimeClasspath - - maxParallelForks = 1 - - filter { includeTestsMatching("io.sentry.systemtest*") } -} - -tasks.named("test").configure { - require(this is Test) - - filter { excludeTestsMatching("io.sentry.systemtest.*") } -} diff --git a/sentry-samples/sentry-samples-logback/src/test/kotlin/io/sentry/DummyTest.kt b/sentry-samples/sentry-samples-logback/src/test/kotlin/io/sentry/DummyTest.kt deleted file mode 100644 index 6f762b7e453..00000000000 --- a/sentry-samples/sentry-samples-logback/src/test/kotlin/io/sentry/DummyTest.kt +++ /dev/null @@ -1,12 +0,0 @@ -package io.sentry - -import kotlin.test.Test -import kotlin.test.assertTrue - -class DummyTest { - @Test - fun `the only test`() { - // only needed to have more than 0 tests and not fail the build - assertTrue(true) - } -} diff --git a/sentry-samples/sentry-samples-logback/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt b/sentry-samples/sentry-samples-logback/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt deleted file mode 100644 index ad28ff77762..00000000000 --- a/sentry-samples/sentry-samples-logback/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt +++ /dev/null @@ -1,85 +0,0 @@ -package io.sentry.systemtest - -import io.sentry.SentryLevel -import io.sentry.systemtest.util.TestHelper -import java.util.concurrent.TimeUnit -import org.junit.Assert -import org.junit.Before -import org.junit.Test - -class ConsoleApplicationSystemTest { - lateinit var testHelper: TestHelper - - @Before - fun setup() { - testHelper = TestHelper("http://localhost:8000") - testHelper.reset() - } - - @Test - fun `logback application sends expected events when run as JAR`() { - val jarFile = testHelper.findJar("sentry-samples-logback") - val process = - testHelper.launch( - jarFile, - mapOf( - "SENTRY_DSN" to testHelper.dsn, - "SENTRY_TRACES_SAMPLE_RATE" to "1.0", - "SENTRY_ENABLE_PRETTY_SERIALIZATION_OUTPUT" to "false", - "SENTRY_DEBUG" to "true", - ), - ) - - process.waitFor(30, TimeUnit.SECONDS) - Assert.assertEquals(0, process.exitValue()) - - // Verify that we received the expected events - verifyExpectedEvents() - } - - private fun verifyExpectedEvents() { - // Verify we received the RuntimeException - testHelper.ensureErrorReceived { event -> - event.exceptions?.any { ex -> - ex.type == "RuntimeException" && ex.value == "Invalid productId=445" - } == true && - event.message?.formatted == "Something went wrong" && - event.level?.name == "ERROR" - } - - testHelper.ensureErrorReceived { event -> - event.breadcrumbs?.firstOrNull { - it.message == "Hello Sentry!" && it.level == SentryLevel.DEBUG - } != null - } - - testHelper.ensureErrorReceived { event -> - event.message?.message == "important warning" && - testHelper.doesEventHaveFlag(event, "my-feature-flag", true) - } - - testHelper.ensureErrorReceived { event -> - event.breadcrumbs?.firstOrNull { - it.message == "User has made a purchase of product: 445" && it.level == SentryLevel.INFO - } != null - } - - testHelper.ensureLogsReceived { logs, _ -> - testHelper.doesContainLogWithBody(logs, "User has made a purchase of product: 445") && - testHelper.doesContainLogWithBody(logs, "Something went wrong") && - testHelper.doesLogWithBodyHaveAttribute( - logs, - "Something went wrong", - "user.type", - "admin", - ) && - testHelper.doesLogWithBodyHaveAttribute( - logs, - "Something went wrong", - "feature.version", - 2, - ) && - testHelper.doesLogWithBodyHaveAttribute(logs, "Something went wrong", "debug.enabled", true) - } - } -} diff --git a/test/system-test-runner.py b/test/system-test-runner.py deleted file mode 100644 index a6f8cb6ba9a..00000000000 --- a/test/system-test-runner.py +++ /dev/null @@ -1,1246 +0,0 @@ -#!/usr/bin/env python3 - -""" -System Test Runner for Sentry Java - -Usage examples: - # Run all tests - python3 test/system-test-runner.py test --all - - # Run specific module test - python3 test/system-test-runner.py test --module sentry-samples-console - - # Set up infrastructure for manual testing from IDE - python3 test/system-test-runner.py test --module sentry-samples-console --manual-test - - # Start Sentry mock server - python3 test/system-test-runner.py sentry start - - # Start Spring app served by Tomcat - python3 test/system-test-runner.py tomcat start sentry-samples-spring - - # Start Spring Boot app - python3 test/system-test-runner.py spring start sentry-samples-spring-boot - - # Start Spring Boot app with build - python3 test/system-test-runner.py spring start sentry-samples-spring-boot --build - - # Check status of all services - python3 test/system-test-runner.py status - - # Stop services - python3 test/system-test-runner.py sentry stop - python3 test/system-test-runner.py spring stop -""" - -import subprocess -import sys -import time -import signal -import os -from enum import Enum -import argparse -import requests -import threading -import socket -from pathlib import Path -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 - -SENTRY_ENVIRONMENT_VARIABLES = { - "SENTRY_DSN": "http://502f25099c204a2fbf4cb16edc5975d1@localhost:8000/0", - "SENTRY_TRACES_SAMPLE_RATE": "1.0", - "SENTRY_PROFILE_SESSION_SAMPLE_RATE": "1.0", - "SENTRY_PROFILE_LIFECYCLE": "TRACE", - "SENTRY_PROFILING_TRACES_DIR_PATH": "tmp/sentry/profiling-traces", - "OTEL_TRACES_EXPORTER": "none", - "OTEL_METRICS_EXPORTER": "none", - "OTEL_LOGS_EXPORTER": "none", - "SENTRY_LOGS_ENABLED": "true", - "SENTRY_ENABLE_CACHE_TRACING": "true" -} - -KAFKA_CONTAINER_NAME = "sentry-java-system-test-kafka" -KAFKA_BOOTSTRAP_SERVERS = "localhost:9092" -KAFKA_BROKER_REQUIRED_MODULES = set() -KAFKA_PROFILE_REQUIRED_MODULES = set() - -class ServerType(Enum): - TOMCAT = 0 - SPRING = 1 - -def get_server_type_for_module(sample_module: str) -> Optional[ServerType]: - if "spring" in sample_module and "spring-boot" not in sample_module: - return ServerType.TOMCAT - elif "spring" in sample_module: - return ServerType.SPRING - - return None - -def str_to_bool(value: str) -> str: - """Convert true/false string to 1/0 string for internal compatibility.""" - if value.lower() in ('true', '1'): - return "1" - elif value.lower() in ('false', '0'): - return "0" - else: - raise ValueError(f"Invalid boolean value: {value}. Use 'true' or 'false'") - -@dataclass -class Server: - name: str - pid_filepath: str - process: Optional[subprocess.Popen] = None - pid: Optional[int] = None - -def store_pid(server: Server): - # Store PID in instance variable and write to file - server.pid = server.process.pid - with open(server.pid_filepath, "w") as pid_file: - pid_file.write(str(server.pid)) - -def cleanup_pid(server: Server): - # Clean up PID file and instance variable - if os.path.exists(server.pid_filepath): - os.remove(server.pid_filepath) - server.pid = None - -@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[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) -> 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(module_config.uses_agent() for module_config in self.modules) - -class SystemTestRunner: - def __init__(self): - self.mock_server = Server(name="Mock", pid_filepath="sentry-mock-server.pid") - self.tomcat_server = Server(name="Tomcat", pid_filepath="tomcat-server.pid") - self.spring_server = Server(name="Spring", pid_filepath="spring-server.pid") - self.kafka_started_by_runner = False - - # Load existing PIDs if available - for server in (self.mock_server, self.tomcat_server, self.spring_server): - server.pid = self.read_pid_file(server.pid_filepath) - if server.pid: - print(f"Found existing {server.name} server PID: {server.pid}") - - def read_pid_file(self, pid_file: str) -> Optional[int]: - """Read PID from file if it exists.""" - try: - if os.path.exists(pid_file): - with open(pid_file, 'r') as f: - return int(f.read().strip()) - 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: - # Send signal 0 to check if process exists - os.kill(pid, 0) - 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...") - os.kill(pid, signal.SIGKILL) - time.sleep(1) - except (OSError, ProcessLookupError): - print(f"Process {pid} was already dead") - - def exclude_kafka(self) -> bool: - return os.environ.get("ORG_GRADLE_PROJECT_excludeKafka") == "true" - - def module_requires_kafka(self, sample_module: str) -> bool: - return not self.exclude_kafka() and sample_module in KAFKA_BROKER_REQUIRED_MODULES - - def module_requires_kafka_profile(self, sample_module: str) -> bool: - return not self.exclude_kafka() and sample_module in KAFKA_PROFILE_REQUIRED_MODULES - - def wait_for_port(self, host: str, port: int, max_attempts: int = 20) -> bool: - for _ in range(max_attempts): - try: - with socket.create_connection((host, port), timeout=1): - return True - except OSError: - time.sleep(1) - return False - - def remove_kafka_broker_container(self) -> None: - subprocess.run( - ["docker", "rm", "-f", KAFKA_CONTAINER_NAME], - check=False, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - - def start_kafka_broker(self) -> None: - if self.wait_for_port("localhost", 9092, max_attempts=1): - print("Kafka broker already running on localhost:9092, reusing it.") - self.kafka_started_by_runner = False - return - - self.remove_kafka_broker_container() - - print("Starting Kafka broker (Redpanda) for system tests...") - run_result = subprocess.run( - [ - "docker", - "run", - "-d", - "--name", - KAFKA_CONTAINER_NAME, - "-p", - "9092:9092", - "docker.redpanda.com/redpandadata/redpanda:v24.1.9", - "redpanda", - "start", - "--overprovisioned", - "--smp", - "1", - "--memory", - "1G", - "--reserve-memory", - "0M", - "--node-id", - "0", - "--check=false", - "--kafka-addr", - "PLAINTEXT://0.0.0.0:9092", - "--advertise-kafka-addr", - "PLAINTEXT://localhost:9092", - ], - check=False, - capture_output=True, - text=True, - ) - - if run_result.returncode != 0: - raise RuntimeError(f"Failed to start Kafka container: {run_result.stderr}") - - if not self.wait_for_port("localhost", 9092, max_attempts=30): - raise RuntimeError("Kafka broker did not become ready on localhost:9092") - - self.kafka_started_by_runner = True - - def stop_kafka_broker(self) -> None: - if not self.kafka_started_by_runner: - return - - self.remove_kafka_broker_container() - self.kafka_started_by_runner = False - - def start_sentry_mock_server(self) -> None: - """Start the Sentry mock server.""" - print("Starting Sentry mock server...") - try: - # Start the mock server in the background - with open("sentry-mock-server.txt", "w") as log_file: - self.mock_server.process = subprocess.Popen( - ["python3", "test/system-test-sentry-server.py"], - stdout=log_file, - stderr=subprocess.STDOUT - ) - - # Store PID in instance variable and write to file - store_pid(self.mock_server) - 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: - # Try graceful shutdown first - try: - response = requests.get("http://127.0.0.1:8000/STOP", timeout=5) - 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}") - self.mock_server.process.kill() - self.mock_server.process.wait(timeout=5) - 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: - # Clean up PID file and instance variable - cleanup_pid(self.mock_server) - - def read_version_from_gradle_properties(self) -> Optional[str]: - """Read the versionName from gradle.properties.""" - try: - with open("gradle.properties", "r") as f: - for line in f: - line = line.strip() - if line.startswith("versionName="): - return line.split("=", 1)[1] - except (IOError, IndexError) as e: - print(f"Error reading version from gradle.properties: {e}") - return None - - def find_agent_jar(self) -> Optional[str]: - """Find the OpenTelemetry agent JAR file with the specific version from gradle.properties.""" - agent_dir = Path("sentry-opentelemetry/sentry-opentelemetry-agent/build/libs/") - if not agent_dir.exists(): - return None - - # Get the version from gradle.properties - version = self.read_version_from_gradle_properties() - if not version: - print("Error: Could not read version from gradle.properties") - return None - - # Look for the specific versioned JAR - versioned_jar_pattern = f"*agent*{version}*.jar" - for jar_file in agent_dir.glob(versioned_jar_pattern): - name = jar_file.name - if ("javadoc" not in name and - "sources" not in name and - "dontuse" not in name): - print(f"Found versioned agent JAR: {jar_file}") - return str(jar_file) - - # If versioned JAR not found, print helpful message - print(f"Error: Could not find agent JAR with version {version} in {agent_dir}") - print("Available JAR files:") - for jar_file in agent_dir.glob("*.jar"): - print(f" {jar_file.name}") - 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_tomcat_server(self, sample_module: str) -> None: - # Build environment variables - env = os.environ.copy() - env.update(SENTRY_ENVIRONMENT_VARIABLES) - - try: - # Start the Tomcat server - with open("tomcat-server.txt", "w") as log_file: - self.tomcat_server.process = subprocess.Popen( - ["./gradlew", f"sentry-samples:{sample_module}:run", "--console=plain"], - env=env, - stdout=log_file, - stderr=subprocess.STDOUT - ) - - store_pid(self.tomcat_server) - print(f"Started Tomcat server with PID {self.tomcat_server.pid}") - except Exception as e: - print(f"Failed to start Tomcat server: {e}") - raise - - 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(SENTRY_ENVIRONMENT_VARIABLES) - env["SENTRY_AUTO_INIT"] = java_agent_auto_init - - if self.module_requires_kafka_profile(sample_module): - env["SPRING_PROFILES_ACTIVE"] = "kafka" - env["SENTRY_ENABLE_QUEUE_TRACING"] = "true" - print("Enabling Spring profile: kafka") - else: - env.pop("SPRING_PROFILES_ACTIVE", None) - - # 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: - cmd.append(f"-javaagent:{agent_jar}") - 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: - self.spring_server.process = subprocess.Popen( - cmd, - env=env, - stdout=log_file, - stderr=subprocess.STDOUT - ) - - # Store PID in instance variable and write to file - store_pid(self.spring_server) - 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_tomcat(self, module_name: str, max_attempts: int = 20) -> bool: - """Wait for Tomcat to be ready.""" - print("Waiting for Tomcat to be ready...") - - for attempt in range(1, max_attempts + 1): - try: - response = requests.head( - f"http://localhost:8080/{module_name}-0.0.1-SNAPSHOT/person/1", - timeout=5 - ) - if response.status_code != 404: - print("Tomcat is ready!") - return True - except: - pass - - print(f"Waiting... (attempt {attempt}/{max_attempts})") - time.sleep(1) - - print("Tomcat failed to become ready") - return False - - 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): - # All current Spring Boot samples expose actuator/health. Waiting for the - # health endpoint avoids false positives from unrelated services on 8080. - try: - response = requests.head( - "http://localhost:8080/actuator/health", - auth=("user", "password"), - timeout=5 - ) - if response.status_code == 200: - print("Spring application is ready!") - 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 = { - "process_running": False, - "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( - "http://localhost:8080/actuator/health", - auth=("user", "password"), - timeout=2 - ) - if response.status_code == 200: - status["http_ready"] = True - except: - pass - - return status - - def get_sentry_status(self) -> dict: - """Get status of Sentry mock server.""" - status = { - "process_running": False, - "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) - if response.status_code == 200: - 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_tomcat_server(self) -> None: - """Stop the Tomcat server.""" - try: - # Kill the process - try process object first, then PID from file - if self.tomcat_server.process and self.tomcat_server.process.poll() is None: - print(f"Killing Tomcat server process object with PID {self.tomcat_server.process.pid}") - self.tomcat_server.process.kill() - try: - self.tomcat_server.process.wait(timeout=10) - except subprocess.TimeoutExpired: - print("Tomcat server did not terminate gracefully") - elif self.tomcat_server.pid and self.is_process_running(self.tomcat_server.pid): - print(f"Killing Tomcat server from PID file with PID {self.tomcat_server.pid}") - self.kill_process(self.tomcat_server.pid, "Tomcat server") - - except Exception as e: - print(f"Error stopping Tomcat server: {e}") - finally: - # Clean up PID file and instance variable - cleanup_pid(self.tomcat_server) - - def stop_spring_server(self) -> None: - """Stop the Spring Boot server.""" - try: - # Kill the process - try process object first, then PID from file - if self.spring_server.process and self.spring_server.process.poll() is None: - print(f"Killing Spring server process object with PID {self.spring_server.process.pid}") - self.spring_server.process.kill() - try: - self.spring_server.process.wait(timeout=10) - except subprocess.TimeoutExpired: - print("Spring server did not terminate gracefully") - 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: - # Clean up PID file and instance variable - cleanup_pid(self.spring_server) - - def get_build_task(self, sample_module: str, server_type: Optional[ServerType]) -> str: - """Get the appropriate build task for a module.""" - if server_type == ServerType.TOMCAT: - return "war" - elif server_type == ServerType.SPRING: - # Modules using Shadow plugin (e.g. Spring Boot 2 samples) use shadowJar, - # modules using Spring Boot plugin (SB3/SB4 samples) use bootJar - build_file = Path(f"sentry-samples/{sample_module}/build.gradle.kts") - if build_file.exists() and "shadow" in build_file.read_text(): - return "shadowJar" - return "bootJar" - - return "assemble" - - def build_module(self, sample_module: str, server_type: Optional[ServerType]) -> int: - """Build a sample module using the appropriate task.""" - build_task = self.get_build_task(sample_module, server_type) - 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}") - try: - result = subprocess.run(["./gradlew", task, "--console=plain"], check=False) - return result.returncode - 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, - java_agent_auto_init: str, build_before_run: str, - server_type: Optional[ServerType]) -> int: - """Set up test infrastructure. Returns 0 on success, error code on failure.""" - if self.module_requires_kafka(sample_module): - self.start_kafka_broker() - os.environ["SENTRY_SAMPLE_KAFKA_BOOTSTRAP_SERVERS"] = KAFKA_BOOTSTRAP_SERVERS - else: - os.environ.pop("SENTRY_SAMPLE_KAFKA_BOOTSTRAP_SERVERS", None) - - # Build if requested - if build_before_run == "1": - print("Building before test run") - build_result = self.build_module(sample_module, server_type) - 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 Tomcat server given a Spring module - if server_type == ServerType.TOMCAT: - print(f"Starting Tomcat server for {sample_module}...") - self.start_tomcat_server(sample_module) - if not self.wait_for_tomcat(sample_module): - print("Tomcat application failed to start!") - return 1 - print("Tomcat application is ready!") - # Start Spring server if it's a Spring boot module - elif server_type == ServerType.SPRING: - print(f"Starting Spring server for {sample_module}...") - self.start_spring_server(sample_module, java_agent, java_agent_auto_init) - if not self.wait_for_spring(): - 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, - java_agent_auto_init: str, build_before_run: str) -> int: - """Run a single system test.""" - print(f"Running system test for {sample_module}") - - server_type = get_server_type_for_module(sample_module) - - try: - # Set up infrastructure - setup_result = self.setup_test_infrastructure(sample_module, - java_agent, - java_agent_auto_init, - build_before_run, - server_type) - 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 server_type == ServerType.TOMCAT: - self.stop_tomcat_server() - elif server_type == ServerType.SPRING: - self.stop_spring_server() - self.stop_sentry_mock_server() - self.stop_kafka_broker() - os.environ.pop("SENTRY_SAMPLE_KAFKA_BOOTSTRAP_SERVERS", None) - - def run_all_tests(self) -> int: - """Run all system tests.""" - test_configs = self.get_available_modules() - - failed_tests = [] - - for i, module_config in enumerate(test_configs): - # Convert true/false to internal 1/0 format - 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(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: {module_config.name}") - - # Summary - print(f"\n{'='*TERMINAL_COLUMNS}") - print("TEST SUMMARY") - 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: - print(f" {module_number}. {sample_module} (agent={java_agent}, auto_init={java_agent_auto_init})") - return 1 - else: - print("\n🎉 All tests passed!") - return 0 - - 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}") - - server_type = get_server_type_for_module(sample_module) - try: - # Set up infrastructure - setup_result = self.setup_test_infrastructure(sample_module, java_agent, java_agent_auto_init, build_before_run, server_type) - if setup_result != 0: - return setup_result - - # Show status and wait for user - print("\n" + "="*TERMINAL_COLUMNS) - print("🚀 Manual test environment ready 🚀") - 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.") - print("\nTest configuration:") - print(f" - Module: {sample_module}") - 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 server_type == ServerType.TOMCAT: - print(f"\nTomcat app is running on: http://localhost:8080/{sample_module}-0.0.1-SNAPSHOT") - elif server_type == ServerType.SPRING: - 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[ModuleConfig]: - """Get list of all available test modules.""" - return [ - ModuleConfig("sentry-samples-logback", "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, 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: - # Handle range like "1-4" - try: - start, end = map(int, part.split("-")) - # Convert from 1-based to 0-based indexing - indices.extend(range(start - 1, end)) - except ValueError: - raise ValueError(f"Invalid range format: {part}") - else: - # Handle single number - try: - # Convert from 1-based to 0-based indexing - 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_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: - user_input = input("\nEnter your selection: ").strip() - 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_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: - try: - mode_input = input("\nRun tests automatically (n = only set up infrastucture for testing in IDE)? [Y/n]: ").strip().lower() - if not mode_input or mode_input in ('y', 'yes'): - manual_test_mode = False - break - elif mode_input in ('n', 'no'): - manual_test_mode = True - break - else: - print("Please enter 'y' or 'n'.") - 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(module_config.uses_agent() for module_config in selected_modules) - if has_agent_modules: - while True: - try: - agent_input = input("\nBuild OpenTelemetry agent JAR (recommended to ensure latest version)? [Y/n]: ").strip().lower() - if not agent_input or agent_input in ('y', 'yes'): - build_agent = True - break - elif agent_input in ('n', 'no'): - build_agent = False - break - else: - print("Please enter 'y' or 'n'.") - 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...") - build_result = self.build_agent_jar() - if build_result != 0: - 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 - - module_config = selection.get_first_module() - # Convert true/false to internal 1/0 format - 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, module_config in enumerate(selection.modules, 1): - # Convert true/false to internal 1/0 format - 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(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: {module_config.name}") - - # Summary - print(f"\n{'='*TERMINAL_COLUMNS}") - print("TEST SUMMARY") - 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: - print(f" {module_number}. {sample_module} (agent={test_agent}, auto_init={test_auto_init})") - return 1 - 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...") - self.stop_spring_server() - self.stop_sentry_mock_server() - self.stop_tomcat_server() - self.stop_kafka_broker() - os.environ.pop("SENTRY_SAMPLE_KAFKA_BOOTSTRAP_SERVERS", None) - sys.exit(1) - -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) - test_group.add_argument("--all", action="store_true", help="Run all system tests") - test_group.add_argument("--module", help="Sample module to test") - test_group.add_argument("--interactive", "-i", action="store_true", help="Interactive module selection") - test_parser.add_argument("--agent", default="false", help="Use Java agent (true or false)") - 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") - - # Tomcat subcommand - tomcat_parser = subparsers.add_parser("tomcat", help="Manage Servlet applications deployed on Tomcat") - tomcat_subparsers = tomcat_parser.add_subparsers(dest="tomcat_action", help="Tomcat actions") - - tomcat_start_parser = tomcat_subparsers.add_parser("start", help="Start tomcat application") - tomcat_start_parser.add_argument("module", help="Sample module to start") - tomcat_start_parser.add_argument("--build", default="false", help="Build before starting (true or false)") - - tomcat_subparsers.add_parser("stop", help="Stop Spring Boot application") - - # 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: - print("Error: --manual-test requires a specific --module, cannot be used with --all") - return 1 - elif args.manual_test and args.interactive: - print("Error: --manual-test cannot be used with --interactive") - return 1 - elif args.all: - return runner.run_all_tests() - elif args.module: - 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 == "tomcat": - if args.tomcat_action == "start": - # Convert true/false arguments to internal format - build = str_to_bool(args.build) - - # Build if requested - if build == "1": - print("Building before starting Tomcat application") - build_result = runner.build_module(args.module, ServerType.TOMCAT) - if build_result != 0: - print("Build failed") - return build_result - - runner.start_tomcat_server(args.module) - - if runner.wait_for_tomcat(args.module): - print("Tomcat application started successfully!") - return 0 - else: - print("Tomcat application failed to start!") - return 1 - elif args.tomcat_action == "stop": - runner.stop_tomcat_server() - print("Tomcat application stopped.") - return 0 - - 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") - build_result = runner.build_module(args.module, ServerType.SPRING) - 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!") - return 0 - else: - print("Spring application failed to start!") - return 1 - elif args.spring_action == "stop": - runner.stop_spring_server() - print("Spring application stopped.") - return 0 - elif args.spring_action == "wait": - if runner.wait_for_spring(max_attempts=args.timeout): - print("Spring application is ready!") - return 0 - else: - print("Spring application is not ready!") - return 1 - elif args.spring_action == "status": - status = runner.get_spring_status() - print(f"Spring Boot Application Status:") - print(f" PID: {status['pid'] or 'None'}") - print(f" Process Running: {'✅' if status['process_running'] else '❌'}") - print(f" HTTP Ready: {'✅' if status['http_ready'] else '❌'}") - return 0 if (status['process_running'] and status['http_ready']) else 1 - else: - spring_parser.print_help() - return 1 - - elif args.command == "sentry": - if args.sentry_action == "start": - runner.start_sentry_mock_server() - print("Sentry mock server started successfully!") - return 0 - elif args.sentry_action == "stop": - runner.stop_sentry_mock_server() - print("Sentry mock server stopped.") - return 0 - elif args.sentry_action == "status": - status = runner.get_sentry_status() - print(f"Sentry Mock Server Status:") - print(f" PID: {status['pid'] or 'None'}") - print(f" Process Running: {'✅' if status['process_running'] else '❌'}") - print(f" HTTP Ready: {'✅' if status['http_ready'] else '❌'}") - return 0 if (status['process_running'] and status['http_ready']) else 1 - 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 - except Exception as e: - print(f"Error: {e}") - return 1 - finally: - # Only cleanup if running tests or manual test mode, not for individual start/stop commands - if args.command == "test": - runner.stop_spring_server() - runner.stop_sentry_mock_server() - runner.stop_tomcat_server() - runner.stop_kafka_broker() - os.environ.pop("SENTRY_SAMPLE_KAFKA_BOOTSTRAP_SERVERS", None) - -if __name__ == "__main__": - sys.exit(main()) diff --git a/test/system-test-sentry-server.py b/test/system-test-sentry-server.py deleted file mode 100755 index 28232410622..00000000000 --- a/test/system-test-sentry-server.py +++ /dev/null @@ -1,168 +0,0 @@ -#!/usr/bin/env python3 - -from http import HTTPStatus -from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer -from urllib.parse import urlparse -import sys -import threading -import binascii -import json -import gzip - -apiOrg = 'sentry-sdks' -apiProject = 'sentry-java' -uri = urlparse(sys.argv[1] if len(sys.argv) > 1 else 'http://127.0.0.1:8000') -version='1.1.0' -appIdentifier='com.sentry.fastlane.app' - -class EnvelopeStorage: - __envelopes_received = [] - - @classmethod - def add(cls, envelope): - cls.__envelopes_received.append(envelope) - - @classmethod - def get_envelopes_received(cls): - return cls.__envelopes_received - - @classmethod - def get_json(cls): - jsonObject = { - 'envelopes': cls.__envelopes_received - } - return json.dumps(jsonObject) - - @classmethod - def reset(cls): - cls.__envelopes_received.clear() - -class EnvelopeCount: - __envelopes_received = 0 - - @classmethod - def increment(cls): - cls.__envelopes_received += 1 - - @classmethod - def get_envelopes_received(cls): - return cls.__envelopes_received - - @classmethod - def get_json(cls): - jsonObject = { - 'envelopes': cls.__envelopes_received - } - return json.dumps(jsonObject) - - @classmethod - def reset(cls): - cls.__envelopes_received = 0 - - -class Handler(BaseHTTPRequestHandler): - body = None - - def do_GET(self): - self.start_response(HTTPStatus.OK) - - if self.path == "/STOP": - print("HTTP server stopping!") - threading.Thread(target=self.server.shutdown).start() - return - - if self.path == "/envelope-count": - print("Envelope count queried " + str(EnvelopeCount.get_envelopes_received())) - self.writeJSON(EnvelopeCount.get_json()) - return - - if self.path == "/envelopes-received": - print("Envelopes queried ") - self.writeJSON(EnvelopeStorage.get_json()) - return - - if self.path == "/reset": - print("Envelopes reset") - EnvelopeStorage.reset() - EnvelopeCount.reset() - self.writeJSON(json.dumps({})) - return - - self.flushLogs() - - def do_POST(self): - if self.isApi('/api/0/envelope/'): - EnvelopeCount.increment() - self.start_response(HTTPStatus.OK) - self.end_headers() - - self.flushLogs() - - def do_PUT(self): - self.start_response(HTTPStatus.OK) - self.end_headers() - - self.flushLogs() - - def start_response(self, code): - self.body = None - self.log_request(code) - self.send_response_only(code) - - def log_request(self, code=None, size=None): - if isinstance(code, HTTPStatus): - code = code.value - body = self.body = self.requestBody() - - if body: - EnvelopeStorage.add(str(body)) - body = self.body[0:min(1000, len(body))] - self.log_message('"%s" %s %s%s', - self.requestline, str(code), "({} bytes)".format(len(body)) if size else '', body) - - # Note: this may only be called once during a single request - can't `.read()` the same stream again. - def requestBody(self): - if self.command == "POST" and 'Content-Length' in self.headers: - content = self.getContent() - - try: - return content.decode("utf-8", errors='replace') - except Exception as e: - print(e) - return binascii.hexlify(bytearray(content)) - return None - - def isApi(self, api: str): - if self.path.strip('/') == api.strip('/'): - self.log_message("Matched API endpoint {}".format(api)) - return True - return False - - def writeJSONFile(self, file_name: str): - json_file = open(file_name, "r") - self.writeJSON(json_file.read()) - json_file.close() - - def writeJSON(self, string: str): - self.send_header("Content-type", "application/json") - self.end_headers() - self.wfile.write(str.encode(string)) - - def flushLogs(self): - sys.stdout.flush() - sys.stderr.flush() - - def getContent(self): - length = int(self.headers['Content-Length']) - content = self.rfile.read(length) - - if 'Content-Encoding' in self.headers and self.headers['Content-Encoding'] == 'gzip': - content = gzip.decompress(content) - - return content - - -print("HTTP server listening on {}".format(uri.geturl())) -print("To stop the server, execute a GET request to {}/STOP".format(uri.geturl())) -httpd = ThreadingHTTPServer((uri.hostname, uri.port), Handler) -target = httpd.serve_forever()