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()