From e423a8cbeea066be792d9cd68e889c7b8a74ca95 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Thu, 15 Oct 2026 15:08:31 +0200 Subject: [PATCH 1/2] test(logback): Move the logback sample's system test into sentry-logback The test now configures the appender from XML in-process and sends to MockSentryServer, so it runs as part of :sentry-logback:test instead of needing the Python runner and a shadow JAR. It also checks that the XML contextTag reaches the event. The logback sample has no tests left, so its system-test wiring, CI matrix entry and runner entry are removed. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../logback/SentryAppenderIntegrationTest.kt | 68 +++++++++++++++ .../sentry-appender-integration-test.xml | 18 ++++ .../sentry-samples-logback/build.gradle.kts | 36 -------- .../src/test/kotlin/io/sentry/DummyTest.kt | 12 --- .../ConsoleApplicationSystemTest.kt | 85 ------------------- 5 files changed, 86 insertions(+), 133 deletions(-) create mode 100644 sentry-logback/src/test/kotlin/io/sentry/logback/SentryAppenderIntegrationTest.kt create mode 100644 sentry-logback/src/test/resources/sentry-appender-integration-test.xml delete mode 100644 sentry-samples/sentry-samples-logback/src/test/kotlin/io/sentry/DummyTest.kt delete mode 100644 sentry-samples/sentry-samples-logback/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt 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) - } - } -} From 6ba410a9e80e3f094ec07089e5d3968c925ef0c5 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Tue, 20 Oct 2026 20:16:56 +0200 Subject: [PATCH 2/2] test: Remove the system-test runner and its Python mock server Every sample's system test now lives in the module it tests, so the Python runner, its mock Sentry server, requirements.txt, the Makefile targets that ran them, and the backend workflow that ran them in CI have nothing left to run. This branch merges #6268's, which moves the last one, the logback sample's, into sentry-logback. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/system-tests-backend.yml | 78 -- Makefile | 16 +- requirements.txt | 5 - test/system-test-runner.py | 1246 -------------------- test/system-test-sentry-server.py | 168 --- 5 files changed, 1 insertion(+), 1512 deletions(-) delete mode 100644 .github/workflows/system-tests-backend.yml delete mode 100644 requirements.txt delete mode 100644 test/system-test-runner.py delete mode 100755 test/system-test-sentry-server.py 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/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()