From 1ce96082813e1b82d64c7b04bae168d41b9e7589 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 10 Jul 2025 10:09:22 +0200 Subject: [PATCH 01/10] e2e tests for console app --- .../sentry-samples-console/build.gradle.kts | 80 +++++++++++++++- .../java/io/sentry/samples/console/Main.java | 25 +++-- .../src/test/kotlin/io/sentry/DummyTest.kt | 12 +++ .../ConsoleApplicationSystemTest.kt | 92 +++++++++++++++++++ .../io/sentry/systemtest/util/TestHelper.kt | 35 ++++++- test/system-test-run-all.sh | 1 + test/system-test-run.sh | 10 +- 7 files changed, 232 insertions(+), 23 deletions(-) create mode 100644 sentry-samples/sentry-samples-console/src/test/kotlin/io/sentry/DummyTest.kt create mode 100644 sentry-samples/sentry-samples-console/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt diff --git a/sentry-samples/sentry-samples-console/build.gradle.kts b/sentry-samples/sentry-samples-console/build.gradle.kts index 82a2bf61220..f259de9d965 100644 --- a/sentry-samples/sentry-samples-console/build.gradle.kts +++ b/sentry-samples/sentry-samples-console/build.gradle.kts @@ -1,14 +1,88 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + plugins { java application + kotlin("jvm") alias(libs.plugins.gradle.versions) + id("com.github.johnrengelman.shadow") version "8.1.1" } application { mainClass.set("io.sentry.samples.console.Main") } +java.sourceCompatibility = JavaVersion.VERSION_17 + +java.targetCompatibility = JavaVersion.VERSION_17 + +repositories { mavenCentral() } + configure { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +tasks.withType().configureEach { + kotlinOptions.jvmTarget = JavaVersion.VERSION_17.toString() +} + +tasks.withType().configureEach { + kotlinOptions { + freeCompilerArgs = listOf("-Xjsr305=strict") + jvmTarget = JavaVersion.VERSION_17.toString() + } +} + +dependencies { + implementation(projects.sentry) + + 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) +tasks.shadowJar { + manifest { + attributes["Main-Class"] = "io.sentry.samples.console.Main" + } + archiveClassifier.set("") // Remove the classifier so it replaces the regular JAR + mergeServiceFiles() +} + +// Make the regular jar task depend on shadowJar +tasks.jar { + enabled = false + dependsOn(tasks.shadowJar) +} + +// Fix the startScripts task dependency +tasks.startScripts { + dependsOn(tasks.shadowJar) +} + +configure { test { java.srcDir("src/test/java") } } + +tasks.register("systemTest").configure { + group = "verification" + description = "Runs the System tests" + + outputs.upToDateWhen { false } + + maxParallelForks = 1 + + // Cap JVM args per test + minHeapSize = "128m" + maxHeapSize = "1g" + + filter { includeTestsMatching("io.sentry.systemtest*") } +} + +tasks.named("test").configure { + require(this is Test) + + filter { excludeTestsMatching("io.sentry.systemtest.*") } } -dependencies { implementation(projects.sentry) } diff --git a/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java b/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java index a0c0d5dd9eb..71c0144955e 100644 --- a/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java +++ b/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java @@ -20,6 +20,7 @@ public static void main(String[] args) throws InterruptedException { options -> { // NOTE: Replace the test DSN below with YOUR OWN DSN to see the events from this app in // your Sentry project/dashboard + options.setEnableExternalConfiguration(true); options.setDsn( "https://502f25099c204a2fbf4cb16edc5975d1@o447951.ingest.sentry.io/5428563"); @@ -66,10 +67,8 @@ public static void main(String[] args) throws InterruptedException { options.setDebug(true); // To change the verbosity, use: // By default it's DEBUG. - options.setDiagnosticLevel( - SentryLevel - .ERROR); // A good option to have SDK debug log in prod is to use only level - // ERROR here. + // options.setDiagnosticLevel(SentryLevel.ERROR); + // A good option to have SDK debug log in prod is to use only level ERROR here. // Exclude frames from some packages from being "inApp" so are hidden by default in Sentry // UI: @@ -83,15 +82,15 @@ public static void main(String[] args) throws InterruptedException { options.setTracesSampleRate(1.0); // set 0.5 to send 50% of traces // Determine traces sample rate based on the sampling context - options.setTracesSampler( - context -> { - // only 10% of transactions with "/product" prefix will be collected - if (!context.getTransactionContext().getName().startsWith("/products")) { - return 0.1; - } else { - return 0.5; - } - }); +// options.setTracesSampler( +// context -> { +// // only 10% of transactions with "/product" prefix will be collected +// if (!context.getTransactionContext().getName().startsWith("/products")) { +// return 0.1; +// } else { +// return 0.5; +// } +// }); }); Sentry.addBreadcrumb( diff --git a/sentry-samples/sentry-samples-console/src/test/kotlin/io/sentry/DummyTest.kt b/sentry-samples/sentry-samples-console/src/test/kotlin/io/sentry/DummyTest.kt new file mode 100644 index 00000000000..6f762b7e453 --- /dev/null +++ b/sentry-samples/sentry-samples-console/src/test/kotlin/io/sentry/DummyTest.kt @@ -0,0 +1,12 @@ +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-console/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt b/sentry-samples/sentry-samples-console/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt new file mode 100644 index 00000000000..b709e4f506e --- /dev/null +++ b/sentry-samples/sentry-samples-console/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt @@ -0,0 +1,92 @@ +package io.sentry.systemtest + +import io.sentry.systemtest.util.TestHelper +import java.io.File +import java.util.concurrent.TimeUnit +import org.junit.Test +import org.junit.Before +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue + +class ConsoleApplicationSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8000") + testHelper.reset() + } + + @Test + fun `console application sends expected events when run as JAR`() { + val jarFile = testHelper.findJar("sentry-samples-console") + 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); + assertEquals(0, process.exitValue()) + + // Verify that we received the expected events + verifyExpectedEvents() + } + + private fun verifyExpectedEvents() { + // Verify we received a "Fatal message!" event + testHelper.ensureErrorReceived { event -> + event.message?.formatted == "Fatal message!" && event.level?.name == "FATAL" + } + + // Verify we received a "Some warning!" event + testHelper.ensureErrorReceived { event -> + event.message?.formatted == "Some warning!" && event.level?.name == "WARNING" + } + + // Verify we received the RuntimeException + testHelper.ensureErrorReceived { event -> + event.exceptions?.any { ex -> ex.type == "RuntimeException" && ex.value == "Some error!" } == + true + } + + // Verify we received the detailed event with fingerprint + testHelper.ensureErrorReceived { event -> + event.message?.message == "Detailed event" && + event.fingerprints?.contains("NewClientDebug") == true && + event.level?.name == "DEBUG" + } + + // Verify we received transaction events + testHelper.ensureTransactionReceived { transaction, _ -> + transaction.transaction == "transaction name" && + transaction.spans?.any { span -> span.op == "child" } == true + } + + // Verify we received the loop messages (should be 10 of them) + var loopMessageCount = 0 + try { + for (i in 0..9) { + testHelper.ensureErrorReceived { event -> + val matches = event.message?.message?.contains("items we'll wait to flush to Sentry!") == true + if (matches) loopMessageCount++ + matches + } + } + } catch (e: Exception) { + // Some loop messages might be missing, but we should have at least some + } + + assertTrue("Should receive at least 5 loop messages, got $loopMessageCount", loopMessageCount >= 5) + + // Verify we have breadcrumbs + testHelper.ensureErrorReceived { event -> + event.breadcrumbs?.any { breadcrumb -> + breadcrumb.message?.contains("Processed by") == true + } == true + } + } +} diff --git a/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt b/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt index db797646146..a34c5d600ab 100644 --- a/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt +++ b/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt @@ -11,7 +11,9 @@ import io.sentry.SentryOptions import io.sentry.protocol.SentrySpan import io.sentry.protocol.SentryTransaction import io.sentry.systemtest.graphql.GraphqlTestClient +import java.io.File import java.io.PrintWriter +import java.util.concurrent.TimeUnit import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNotNull @@ -22,6 +24,7 @@ class TestHelper(backendUrl: String) { val graphqlClient: GraphqlTestClient val sentryClient: SentryMockServerClient val jsonSerializer: JsonSerializer + val dsn = "http://502f25099c204a2fbf4cb16edc5975d1@localhost:8000/0" var envelopeCounts: EnvelopeCounts? = null @@ -42,8 +45,7 @@ class TestHelper(backendUrl: String) { assertTrue(envelopeCountsAfter!!.envelopes!! > envelopeCounts!!.envelopes!!) } - fun ensureEnvelopeReceived(callback: ((String) -> Boolean)) { - Thread.sleep(10000) + fun ensureEnvelopeReceived(retryCount: Int = 1, callback: ((String) -> Boolean)) { val envelopes = sentryClient.getEnvelopes() assertNotNull(envelopes.envelopes) envelopes.envelopes.forEach { envelopeString -> @@ -52,7 +54,11 @@ class TestHelper(backendUrl: String) { return } } - throw RuntimeException("Unable to find matching envelope received by relay") + if (retryCount <= 0) { + throw RuntimeException("Unable to find matching envelope received by relay") + } else { + ensureEnvelopeReceived(retryCount - 1, callback) + } } fun ensureNoEnvelopeReceived(callback: ((String) -> Boolean)) { @@ -268,4 +274,27 @@ class TestHelper(backendUrl: String) { return true } + + fun findJar(prefix: String, inDir: String = "build/libs"): File { + val buildDir = File(inDir) + val jarFiles = + buildDir + .listFiles { _, name -> name.startsWith(prefix) && name.endsWith(".jar") } + ?.toList() ?: emptyList() + + if (jarFiles.isEmpty()) { + throw AssertionError("No JAR found in ${buildDir.absolutePath}") + } + + return jarFiles.maxOf { it } + } + + fun launch(jar: File, env: Map): Process { + val processBuilder = ProcessBuilder("java", "-jar", jar.absolutePath) + .inheritIO() // forward i/o to current process + + processBuilder.environment().putAll(env) + + return processBuilder.start() + } } diff --git a/test/system-test-run-all.sh b/test/system-test-run-all.sh index 8389a4db431..007def46242 100755 --- a/test/system-test-run-all.sh +++ b/test/system-test-run-all.sh @@ -10,3 +10,4 @@ ./test/system-test-run.sh "sentry-samples-spring-boot-jakarta-opentelemetry-noagent" "0" "true" "0" ./test/system-test-run.sh "sentry-samples-spring-boot-jakarta-opentelemetry" "1" "true" "0" ./test/system-test-run.sh "sentry-samples-spring-boot-jakarta-opentelemetry" "1" "false" "0" +./test/system-test-run.sh "sentry-samples-console" "0" "true" "0" diff --git a/test/system-test-run.sh b/test/system-test-run.sh index 9560beb3639..c4838923c6a 100755 --- a/test/system-test-run.sh +++ b/test/system-test-run.sh @@ -14,11 +14,13 @@ test/system-test-sentry-server-start.sh MOCK_SERVER_PID=$(cat sentry-mock-server.pid) echo "started mock server ${SAMPLE_MODULE}-${JAVA_AGENT}-${JAVA_AGENT_AUTO_INIT} with PID ${MOCK_SERVER_PID}" -test/system-test-spring-server-start.sh "${SAMPLE_MODULE}" "${JAVA_AGENT}" "${JAVA_AGENT_AUTO_INIT}" -SUT_PID=$(cat spring-server.pid) -echo "started spring server ${SAMPLE_MODULE}-${JAVA_AGENT}-${JAVA_AGENT_AUTO_INIT} with PID ${SUT_PID}" +if [[ $SAMPLE_MODULE == *"spring"* ]]; then + test/system-test-spring-server-start.sh "${SAMPLE_MODULE}" "${JAVA_AGENT}" "${JAVA_AGENT_AUTO_INIT}" + SUT_PID=$(cat spring-server.pid) + echo "started spring server ${SAMPLE_MODULE}-${JAVA_AGENT}-${JAVA_AGENT_AUTO_INIT} with PID ${SUT_PID}" -test/wait-for-spring.sh + test/wait-for-spring.sh +fi ./gradlew :sentry-samples:${SAMPLE_MODULE}:systemTest TESTRUN_RETVAL=$? From a863ac5ed1425318544d6015a4fef4e44bb3d3f6 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 11 Jul 2025 07:45:07 +0200 Subject: [PATCH 02/10] fix test failures by waiting for 10s after first try to find envelopes --- .../src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt b/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt index a34c5d600ab..98f358da2ed 100644 --- a/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt +++ b/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt @@ -57,6 +57,7 @@ class TestHelper(backendUrl: String) { if (retryCount <= 0) { throw RuntimeException("Unable to find matching envelope received by relay") } else { + Thread.sleep(10000) ensureEnvelopeReceived(retryCount - 1, callback) } } From 811c9da1ece3dc0661f4cdd0ae60f7b8e84edf30 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 11 Jul 2025 07:45:45 +0200 Subject: [PATCH 03/10] add system-test-runner.py script to replace bash scripts for running e2e / system tests --- test/system-test-runner.py | 838 +++++++++++++++++++++++++++++++++++++ 1 file changed, 838 insertions(+) create mode 100644 test/system-test-runner.py diff --git a/test/system-test-runner.py b/test/system-test-runner.py new file mode 100644 index 00000000000..92cfc0d302b --- /dev/null +++ b/test/system-test-runner.py @@ -0,0 +1,838 @@ +#!/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 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 +import argparse +import requests +import threading +from pathlib import Path +from typing import Optional, List, Tuple +from dataclasses import dataclass + +@dataclass +class InteractiveSelection: + """Result of interactive module selection.""" + modules: List[Tuple[str, str, str, str]] + manual_test_mode: bool + + def is_empty(self) -> bool: + """Check if no modules were selected.""" + return len(self.modules) == 0 + + def is_single_module(self) -> bool: + """Check if exactly one module was selected.""" + return len(self.modules) == 1 + + def get_first_module(self) -> Tuple[str, str, str, str]: + """Get the first selected module (for manual test mode).""" + if self.is_empty(): + raise ValueError("No modules selected") + return self.modules[0] + +class SystemTestRunner: + def __init__(self): + self.mock_server_process: Optional[subprocess.Popen] = None + self.spring_server_process: Optional[subprocess.Popen] = None + self.mock_server_pid: Optional[int] = None + self.spring_server_pid: Optional[int] = None + self.mock_server_pid_file = "sentry-mock-server.pid" + self.spring_server_pid_file = "spring-server.pid" + + # Load existing PIDs if available + self.mock_server_pid = self.read_pid_file(self.mock_server_pid_file) + self.spring_server_pid = self.read_pid_file(self.spring_server_pid_file) + + if self.mock_server_pid: + print(f"Found existing mock server PID: {self.mock_server_pid}") + if self.spring_server_pid: + print(f"Found existing Spring server PID: {self.spring_server_pid}") + + def read_pid_file(self, pid_file: str) -> Optional[int]: + """Read PID from file if it exists.""" + try: + 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 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 + self.mock_server_pid = self.mock_server_process.pid + with open(self.mock_server_pid_file, "w") as pid_file: + pid_file.write(str(self.mock_server_pid)) + + print(f"Started mock server with PID {self.mock_server_pid}") + + # Wait a moment for the server to start + time.sleep(2) + + except Exception as e: + print(f"Failed to start mock server: {e}") + raise + + def stop_sentry_mock_server(self) -> None: + """Stop the Sentry mock server.""" + try: + # 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 + if os.path.exists(self.mock_server_pid_file): + os.remove(self.mock_server_pid_file) + self.mock_server_pid = None + + def find_agent_jar(self) -> Optional[str]: + """Find the OpenTelemetry agent JAR file.""" + agent_dir = Path("sentry-opentelemetry/sentry-opentelemetry-agent/build/libs/") + if not agent_dir.exists(): + return None + + for jar_file in agent_dir.glob("*.jar"): + name = jar_file.name + if ("agent" in name and + "javadoc" not in name and + "sources" not in name and + "dontuse" not in name): + return str(jar_file) + return None + + 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_DSN": "http://502f25099c204a2fbf4cb16edc5975d1@localhost:8000/0", + "SENTRY_AUTO_INIT": java_agent_auto_init, + "SENTRY_TRACES_SAMPLE_RATE": "1.0", + "OTEL_TRACES_EXPORTER": "none", + "OTEL_METRICS_EXPORTER": "none", + "OTEL_LOGS_EXPORTER": "none", + "SENTRY_LOGS_ENABLED": "true" + }) + + # Build command + jar_path = f"sentry-samples/{sample_module}/build/libs/{sample_module}-0.0.1-SNAPSHOT.jar" + cmd = ["java"] + + if java_agent == "1": + agent_jar = self.find_agent_jar() + if agent_jar: + cmd.append(f"-javaagent:{agent_jar}") + print(f"Using Java Agent: {agent_jar}") + + 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 + self.spring_server_pid = self.spring_server_process.pid + with open(self.spring_server_pid_file, "w") as pid_file: + pid_file.write(str(self.spring_server_pid)) + + print(f"Started Spring server with PID {self.spring_server_pid}") + + except Exception as e: + print(f"Failed to start Spring server: {e}") + raise + + def wait_for_spring(self, max_attempts: int = 20) -> bool: + """Wait for Spring Boot application to be ready.""" + print("Waiting for Spring application to be ready...") + + for attempt in range(1, max_attempts + 1): + try: + response = requests.head( + "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_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 + if os.path.exists(self.spring_server_pid_file): + os.remove(self.spring_server_pid_file) + self.spring_server_pid = None + + def get_build_task(self, sample_module: str) -> str: + """Get the appropriate build task for a module.""" + return "bootJar" if "spring" in sample_module else "assemble" + + def build_module(self, sample_module: str) -> int: + """Build a sample module using the appropriate task.""" + build_task = self.get_build_task(sample_module) + print(f"Building {sample_module} using {build_task} task") + return self.run_gradle_task(f":sentry-samples:{sample_module}:{build_task}") + + def run_gradle_task(self, task: str) -> int: + """Run a Gradle task and return the exit code.""" + print(f"Running: ./gradlew {task}") + try: + result = subprocess.run(["./gradlew", task], 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) -> int: + """Set up test infrastructure. Returns 0 on success, error code on failure.""" + # Build if requested + if build_before_run == "1": + print("Building before test run") + build_result = self.build_module(sample_module) + if build_result != 0: + print("Build failed") + return build_result + + # Start mock server + print("Starting Sentry mock server...") + self.start_sentry_mock_server() + + # Start Spring server if it's a Spring module + if "spring" in sample_module: + print(f"Starting Spring server for {sample_module}...") + 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}") + + try: + # Set up infrastructure + setup_result = self.setup_test_infrastructure(sample_module, java_agent, java_agent_auto_init, build_before_run) + if setup_result != 0: + return setup_result + + # Run the system test + test_result = self.run_gradle_task(f":sentry-samples:{sample_module}:systemTest") + + return test_result + + finally: + # Cleanup + if "spring" in sample_module: + self.stop_spring_server() + self.stop_sentry_mock_server() + + def run_all_tests(self) -> int: + """Run all system tests.""" + test_configs = [ + ("sentry-samples-spring-boot", "0", "true", "0"), + ("sentry-samples-spring-boot-opentelemetry-noagent", "0", "true", "0"), + ("sentry-samples-spring-boot-opentelemetry", "1", "true", "0"), + ("sentry-samples-spring-boot-opentelemetry", "1", "false", "0"), + ("sentry-samples-spring-boot-webflux-jakarta", "0", "true", "0"), + ("sentry-samples-spring-boot-webflux", "0", "true", "0"), + ("sentry-samples-spring-boot-jakarta", "0", "true", "0"), + ("sentry-samples-spring-boot-jakarta-opentelemetry-noagent", "0", "true", "0"), + ("sentry-samples-spring-boot-jakarta-opentelemetry", "1", "true", "0"), + ("sentry-samples-spring-boot-jakarta-opentelemetry", "1", "false", "0"), + ("sentry-samples-console", "0", "true", "0"), + ] + + failed_tests = [] + + for sample_module, java_agent, java_agent_auto_init, build_before_run in test_configs: + print(f"\n{'='*60}") + print(f"Running test: {sample_module} (agent={java_agent}, auto_init={java_agent_auto_init})") + print(f"{'='*60}") + + result = self.run_single_test(sample_module, java_agent, java_agent_auto_init, build_before_run) + + if result != 0: + # Find the module number in the full list for interactive reference + module_number = self._find_module_number(sample_module, java_agent, java_agent_auto_init) + failed_tests.append((module_number, sample_module, java_agent, java_agent_auto_init)) + print(f"❌ Test failed: {sample_module}") + else: + print(f"✅ Test passed: {sample_module}") + + # Summary + print(f"\n{'='*60}") + print("TEST SUMMARY") + print(f"{'='*60}") + 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}") + + try: + # Set up infrastructure + setup_result = self.setup_test_infrastructure(sample_module, java_agent, java_agent_auto_init, build_before_run) + if setup_result != 0: + return setup_result + + # Show status and wait for user + print("\n" + "="*60) + print("🚀 Manual test environment ready 🚀") + print("="*60) + 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 "spring" in sample_module: + print("\nSpring Boot app is running on: http://localhost:8080") + + print("\nPress Enter to stop the infrastructure and exit...") + + # Wait for user input + try: + input() + except KeyboardInterrupt: + print("\nReceived interrupt signal") + + print("\nStopping infrastructure...") + return 0 + + finally: + # Cleanup will happen in the finally block of main() + pass + + def get_available_modules(self) -> List[Tuple[str, str, str, str]]: + """Get list of all available test modules.""" + return [ + ("sentry-samples-spring-boot", "0", "true", "0"), + ("sentry-samples-spring-boot-opentelemetry-noagent", "0", "true", "0"), + ("sentry-samples-spring-boot-opentelemetry", "1", "true", "0"), + ("sentry-samples-spring-boot-opentelemetry", "1", "false", "0"), + ("sentry-samples-spring-boot-webflux-jakarta", "0", "true", "0"), + ("sentry-samples-spring-boot-webflux", "0", "true", "0"), + ("sentry-samples-spring-boot-jakarta", "0", "true", "0"), + ("sentry-samples-spring-boot-jakarta-opentelemetry-noagent", "0", "true", "0"), + ("sentry-samples-spring-boot-jakarta-opentelemetry", "1", "true", "0"), + ("sentry-samples-spring-boot-jakarta-opentelemetry", "1", "false", "0"), + ("sentry-samples-console", "0", "true", "0"), + ] + + def _find_module_number(self, module_name: str, agent: str, auto_init: str) -> int: + """Find the module number in the interactive list (1-based).""" + modules = self.get_available_modules() + for i, (mod_name, mod_agent, mod_auto_init, _) in enumerate(modules, 1): + if mod_name == module_name and mod_agent == agent and mod_auto_init == auto_init: + 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, agent, auto_init, build) in enumerate(modules, 1): + agent_text = "with agent" if agent == "1" else "no agent" + auto_init_text = f"auto-init: {auto_init}" + print(f"{i:2d}. {module:<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, agent, auto_init, build) in enumerate(selected_modules, 1): + agent_text = "with agent" if agent == "1" else "no agent" + print(f" {i}. {module} ({agent_text}, auto-init: {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) + + # Ask about test mode + 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'): + return InteractiveSelection(modules=selected_modules, manual_test_mode=False) + elif mode_input in ('n', 'no'): + return InteractiveSelection(modules=selected_modules, manual_test_mode=True) + else: + print("Please enter 'y' or 'n'.") + except KeyboardInterrupt: + print("\nOperation cancelled.") + return InteractiveSelection(modules=[], manual_test_mode=False) + + 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 + + # Handle manual test mode + if selection.manual_test_mode: + if not selection.is_single_module(): + print("Error: Manual test mode can only be used with a single module.") + print("Please select only one module for manual testing.") + return 1 + + sample_module, test_agent, test_auto_init, test_build = selection.get_first_module() + print(f"\nSetting up manual test environment for: {sample_module}") + return self.run_manual_test_mode(sample_module, test_agent, test_auto_init, test_build) + + # Handle automatic test running + failed_tests = [] + + for i, (sample_module, test_agent, test_auto_init, test_build) in enumerate(selection.modules, 1): + print(f"\n{'='*60}") + print(f"Running test {i}/{len(selection.modules)}: {sample_module}") + print(f"Agent: {test_agent}, Auto-init: {test_auto_init}") + print(f"{'='*60}") + + result = self.run_single_test(sample_module, test_agent, test_auto_init, test_build) + + if result != 0: + # Find the module number in the full list for interactive reference + module_number = self._find_module_number(sample_module, test_agent, test_auto_init) + failed_tests.append((module_number, sample_module, test_agent, test_auto_init)) + print(f"❌ Test failed: {sample_module}") + else: + print(f"✅ Test passed: {sample_module}") + + # Summary + print(f"\n{'='*60}") + print("TEST SUMMARY") + print(f"{'='*60}") + 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() + 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="0", help="Use Java agent (0 or 1)") + test_parser.add_argument("--auto-init", default="true", help="Auto-init agent (true or false)") + test_parser.add_argument("--build", default="0", help="Build before running (0 or 1)") + test_parser.add_argument("--manual-test", action="store_true", help="Set up infrastructure but pause for manual testing from IDE") + + # Spring subcommand + spring_parser = subparsers.add_parser("spring", help="Manage Spring Boot applications") + spring_subparsers = spring_parser.add_subparsers(dest="spring_action", help="Spring actions") + + spring_start_parser = spring_subparsers.add_parser("start", help="Start Spring Boot application") + spring_start_parser.add_argument("module", help="Sample module to start") + spring_start_parser.add_argument("--agent", default="0", help="Use Java agent (0 or 1)") + spring_start_parser.add_argument("--auto-init", default="true", help="Auto-init agent (true or false)") + spring_start_parser.add_argument("--build", action="store_true", help="Build before starting") + + 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": + if args.manual_test and args.module: + return runner.run_manual_test_mode(args.module, args.agent, args.auto_init, args.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, args.agent, args.auto_init, args.build) + elif args.interactive: + return runner.run_interactive_tests(args.agent, args.auto_init, args.build) + + elif args.command == "spring": + if args.spring_action == "start": + # Build if requested + if args.build: + print("Building before starting Spring application") + build_result = runner.build_module(args.module) + if build_result != 0: + print("Build failed") + return build_result + + runner.start_spring_server(args.module, args.agent, args.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() + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file From 16dcaee3176b320d45deeabb7f9e854f6a7a9b8a Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 11 Jul 2025 11:55:31 +0200 Subject: [PATCH 04/10] use py script for ci, cleanup, makefile --- .github/workflows/system-tests-backend.yml | 23 +-- Makefile | 10 +- test/system-test-runner.py | 185 ++++++++++++++++----- 3 files changed, 159 insertions(+), 59 deletions(-) diff --git a/.github/workflows/system-tests-backend.yml b/.github/workflows/system-tests-backend.yml index 3c949887c0b..dd4e9c4c90b 100644 --- a/.github/workflows/system-tests-backend.yml +++ b/.github/workflows/system-tests-backend.yml @@ -21,26 +21,27 @@ jobs: fail-fast: false matrix: sample: [ "sentry-samples-spring-boot-jakarta" ] - agent: [ "0" ] + agent: [ "false" ] agent-auto-init: [ "true" ] include: - sample: "sentry-samples-spring-boot" - sample: "sentry-samples-spring-boot-opentelemetry-noagent" - sample: "sentry-samples-spring-boot-opentelemetry" - agent: "1" + agent: "true" agent-auto-init: "true" - sample: "sentry-samples-spring-boot-opentelemetry" - agent: "1" + agent: "true" agent-auto-init: "false" - sample: "sentry-samples-spring-boot-webflux-jakarta" - sample: "sentry-samples-spring-boot-webflux" - sample: "sentry-samples-spring-boot-jakarta-opentelemetry-noagent" - sample: "sentry-samples-spring-boot-jakarta-opentelemetry" - agent: "1" + agent: "true" agent-auto-init: "true" - sample: "sentry-samples-spring-boot-jakarta-opentelemetry" - agent: "1" + agent: "true" agent-auto-init: "false" + - sample: "sentry-samples-console" steps: - uses: actions/checkout@v4 with: @@ -90,17 +91,9 @@ jobs: -e '/.*"sentry-samples-android",/d' \ build.gradle.kts - - name: Build server jar + - name: Build and run system tests run: | - ./gradlew :sentry-samples:${{ matrix.sample }}:bootJar - - - name: Build agent jar - run: | - ./gradlew :sentry-opentelemetry:sentry-opentelemetry-agent:assemble - - - name: Start server and run integration test for sentry-cli commands - run: | - test/system-test-run.sh "${{ matrix.sample }}" "${{ matrix.agent }}" "${{ matrix.agent-auto-init }}" "0" + 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() diff --git a/Makefile b/Makefile index b16966ba64d..0a0f2bbf8c2 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all clean compile javadocs dryRelease update checkFormat api assembleBenchmarkTestRelease assembleUiTestRelease assembleUiTestCriticalRelease createCoverageReports runUiTestCritical check preMerge publish +.PHONY: all clean compile javadocs dryRelease update checkFormat api assembleBenchmarkTestRelease assembleUiTestRelease assembleUiTestCriticalRelease createCoverageReports runUiTestCritical check preMerge publish systemtest systemtest-interactive all: stop clean javadocs compile createCoverageReports assembleBenchmarks: assembleBenchmarkTestRelease @@ -59,6 +59,14 @@ createCoverageReports: ./gradlew jacocoTestReport ./gradlew koverXmlReportRelease +# Run system tests for sample applications +systemtest: + python3 test/system-test-runner.py test --all + +# Run system tests with interactive module selection +systemtest-interactive: + python3 test/system-test-runner.py test --interactive + # Run tests and lint check: ./gradlew check diff --git a/test/system-test-runner.py b/test/system-test-runner.py index 92cfc0d302b..aed0d7126a2 100644 --- a/test/system-test-runner.py +++ b/test/system-test-runner.py @@ -42,11 +42,21 @@ from typing import Optional, List, Tuple from dataclasses import dataclass +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 InteractiveSelection: """Result of interactive module selection.""" modules: List[Tuple[str, str, str, str]] manual_test_mode: bool + build_agent: bool def is_empty(self) -> bool: """Check if no modules were selected.""" @@ -61,6 +71,10 @@ def get_first_module(self) -> Tuple[str, str, str, str]: if self.is_empty(): raise ValueError("No modules selected") return self.modules[0] + + def has_agent_modules(self) -> bool: + """Check if any selected modules use the Java agent.""" + return any(str_to_bool(agent) == "1" for _, agent, _, _ in self.modules) class SystemTestRunner: def __init__(self): @@ -184,6 +198,36 @@ def find_agent_jar(self) -> Optional[str]: return str(jar_file) return None + def build_agent_jar(self) -> int: + """Build the OpenTelemetry agent JAR file.""" + print("Building OpenTelemetry agent JAR...") + return self.run_gradle_task(":sentry-opentelemetry:sentry-opentelemetry-agent:assemble") + + def ensure_agent_jar(self, skip_build: bool = False) -> Optional[str]: + """Ensure the OpenTelemetry agent JAR exists, building it if necessary.""" + agent_jar = self.find_agent_jar() + if agent_jar: + return agent_jar + + if skip_build: + print("OpenTelemetry agent JAR not found and build was skipped") + return None + + # Agent JAR doesn't exist, try to build it + print("OpenTelemetry agent JAR not found, building it...") + build_result = self.build_agent_jar() + if build_result != 0: + print("Failed to build OpenTelemetry agent JAR") + return None + + # Try to find it again after building + agent_jar = self.find_agent_jar() + if not agent_jar: + print("OpenTelemetry agent JAR still not found after building") + return None + + return agent_jar + def start_spring_server(self, sample_module: str, java_agent: str, java_agent_auto_init: str) -> None: """Start a Spring Boot server for testing.""" print(f"Starting Spring server for {sample_module}...") @@ -205,10 +249,12 @@ def start_spring_server(self, sample_module: str, java_agent: str, java_agent_au cmd = ["java"] if java_agent == "1": - agent_jar = self.find_agent_jar() + 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]) @@ -372,6 +418,13 @@ def setup_test_infrastructure(self, sample_module: str, java_agent: str, 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() @@ -411,28 +464,21 @@ def run_single_test(self, sample_module: str, java_agent: str, def run_all_tests(self) -> int: """Run all system tests.""" - test_configs = [ - ("sentry-samples-spring-boot", "0", "true", "0"), - ("sentry-samples-spring-boot-opentelemetry-noagent", "0", "true", "0"), - ("sentry-samples-spring-boot-opentelemetry", "1", "true", "0"), - ("sentry-samples-spring-boot-opentelemetry", "1", "false", "0"), - ("sentry-samples-spring-boot-webflux-jakarta", "0", "true", "0"), - ("sentry-samples-spring-boot-webflux", "0", "true", "0"), - ("sentry-samples-spring-boot-jakarta", "0", "true", "0"), - ("sentry-samples-spring-boot-jakarta-opentelemetry-noagent", "0", "true", "0"), - ("sentry-samples-spring-boot-jakarta-opentelemetry", "1", "true", "0"), - ("sentry-samples-spring-boot-jakarta-opentelemetry", "1", "false", "0"), - ("sentry-samples-console", "0", "true", "0"), - ] + test_configs = self.get_available_modules() failed_tests = [] for sample_module, java_agent, java_agent_auto_init, build_before_run in test_configs: + # Convert true/false to internal 1/0 format + agent = str_to_bool(java_agent) + auto_init = java_agent_auto_init # already in correct format + build = str_to_bool(build_before_run) + print(f"\n{'='*60}") print(f"Running test: {sample_module} (agent={java_agent}, auto_init={java_agent_auto_init})") print(f"{'='*60}") - result = self.run_single_test(sample_module, java_agent, java_agent_auto_init, build_before_run) + result = self.run_single_test(sample_module, agent, auto_init, build) if result != 0: # Find the module number in the full list for interactive reference @@ -504,17 +550,17 @@ def run_manual_test_mode(self, sample_module: str, java_agent: str, def get_available_modules(self) -> List[Tuple[str, str, str, str]]: """Get list of all available test modules.""" return [ - ("sentry-samples-spring-boot", "0", "true", "0"), - ("sentry-samples-spring-boot-opentelemetry-noagent", "0", "true", "0"), - ("sentry-samples-spring-boot-opentelemetry", "1", "true", "0"), - ("sentry-samples-spring-boot-opentelemetry", "1", "false", "0"), - ("sentry-samples-spring-boot-webflux-jakarta", "0", "true", "0"), - ("sentry-samples-spring-boot-webflux", "0", "true", "0"), - ("sentry-samples-spring-boot-jakarta", "0", "true", "0"), - ("sentry-samples-spring-boot-jakarta-opentelemetry-noagent", "0", "true", "0"), - ("sentry-samples-spring-boot-jakarta-opentelemetry", "1", "true", "0"), - ("sentry-samples-spring-boot-jakarta-opentelemetry", "1", "false", "0"), - ("sentry-samples-console", "0", "true", "0"), + ("sentry-samples-spring-boot", "false", "true", "false"), + ("sentry-samples-spring-boot-opentelemetry-noagent", "false", "true", "false"), + ("sentry-samples-spring-boot-opentelemetry", "true", "true", "false"), + ("sentry-samples-spring-boot-opentelemetry", "true", "false", "false"), + ("sentry-samples-spring-boot-webflux-jakarta", "false", "true", "false"), + ("sentry-samples-spring-boot-webflux", "false", "true", "false"), + ("sentry-samples-spring-boot-jakarta", "false", "true", "false"), + ("sentry-samples-spring-boot-jakarta-opentelemetry-noagent", "false", "true", "false"), + ("sentry-samples-spring-boot-jakarta-opentelemetry", "true", "true", "false"), + ("sentry-samples-spring-boot-jakarta-opentelemetry", "true", "false", "false"), + ("sentry-samples-console", "false", "true", "false"), ] def _find_module_number(self, module_name: str, agent: str, auto_init: str) -> int: @@ -568,7 +614,7 @@ def interactive_module_selection(self) -> InteractiveSelection: print("\nAvailable test modules:") print("=" * 80) for i, (module, agent, auto_init, build) in enumerate(modules, 1): - agent_text = "with agent" if agent == "1" else "no agent" + agent_text = "with agent" if str_to_bool(agent) == "1" else "no agent" auto_init_text = f"auto-init: {auto_init}" print(f"{i:2d}. {module:<50} ({agent_text}, {auto_init_text})") @@ -592,7 +638,7 @@ def interactive_module_selection(self) -> InteractiveSelection: # Show confirmation print(f"\nSelected {len(selected_modules)} module(s):") for i, (module, agent, auto_init, build) in enumerate(selected_modules, 1): - agent_text = "with agent" if agent == "1" else "no agent" + agent_text = "with agent" if str_to_bool(agent) == "1" else "no agent" print(f" {i}. {module} ({agent_text}, auto-init: {auto_init})") confirm = input("\nProceed with these selections? [Y/n]: ").strip().lower() @@ -606,21 +652,45 @@ def interactive_module_selection(self) -> InteractiveSelection: print("Please try again.") except KeyboardInterrupt: print("\nOperation cancelled.") - return InteractiveSelection(modules=[], manual_test_mode=False) + 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'): - return InteractiveSelection(modules=selected_modules, manual_test_mode=False) + manual_test_mode = False + break elif mode_input in ('n', 'no'): - return InteractiveSelection(modules=selected_modules, manual_test_mode=True) + manual_test_mode = True + break else: print("Please enter 'y' or 'n'.") except KeyboardInterrupt: print("\nOperation cancelled.") - return InteractiveSelection(modules=[], manual_test_mode=False) + return InteractiveSelection(modules=[], manual_test_mode=False, build_agent=False) + + # Ask about building agent if any modules use it + build_agent = False + has_agent_modules = any(str_to_bool(agent) == "1" for _, agent, _, _ in selected_modules) + 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.""" @@ -630,6 +700,15 @@ def run_interactive_tests(self, agent: str, auto_init: str, build: str) -> int: 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(): @@ -638,19 +717,29 @@ def run_interactive_tests(self, agent: str, auto_init: str, build: str) -> int: return 1 sample_module, test_agent, test_auto_init, test_build = selection.get_first_module() + # Convert true/false to internal 1/0 format + agent = str_to_bool(test_agent) + auto_init = test_auto_init # already in correct format + build = str_to_bool(test_build) + print(f"\nSetting up manual test environment for: {sample_module}") - return self.run_manual_test_mode(sample_module, test_agent, test_auto_init, test_build) + return self.run_manual_test_mode(sample_module, agent, auto_init, build) # Handle automatic test running failed_tests = [] for i, (sample_module, test_agent, test_auto_init, test_build) in enumerate(selection.modules, 1): + # Convert true/false to internal 1/0 format + agent = str_to_bool(test_agent) + auto_init = test_auto_init # already in correct format + build = str_to_bool(test_build) + print(f"\n{'='*60}") print(f"Running test {i}/{len(selection.modules)}: {sample_module}") print(f"Agent: {test_agent}, Auto-init: {test_auto_init}") print(f"{'='*60}") - result = self.run_single_test(sample_module, test_agent, test_auto_init, test_build) + result = self.run_single_test(sample_module, agent, auto_init, build) if result != 0: # Find the module number in the full list for interactive reference @@ -694,9 +783,9 @@ def main(): 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="0", help="Use Java agent (0 or 1)") + 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="0", help="Build before running (0 or 1)") + test_parser.add_argument("--build", default="false", help="Build before running (true or false)") test_parser.add_argument("--manual-test", action="store_true", help="Set up infrastructure but pause for manual testing from IDE") # Spring subcommand @@ -705,9 +794,9 @@ def main(): 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="0", help="Use Java agent (0 or 1)") + 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", action="store_true", help="Build before starting") + 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") @@ -741,8 +830,13 @@ def main(): 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, args.agent, args.auto_init, args.build) + 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 @@ -752,21 +846,26 @@ def main(): elif args.all: return runner.run_all_tests() elif args.module: - return runner.run_single_test(args.module, args.agent, args.auto_init, args.build) + return runner.run_single_test(args.module, agent, auto_init, build) elif args.interactive: - return runner.run_interactive_tests(args.agent, args.auto_init, args.build) + return runner.run_interactive_tests(agent, auto_init, build) elif args.command == "spring": if args.spring_action == "start": + # Convert true/false arguments to internal format + agent = str_to_bool(args.agent) + auto_init = args.auto_init # already accepts true/false + build = str_to_bool(args.build) + # Build if requested - if args.build: + if build == "1": print("Building before starting Spring application") build_result = runner.build_module(args.module) if build_result != 0: print("Build failed") return build_result - runner.start_spring_server(args.module, args.agent, args.auto_init) + runner.start_spring_server(args.module, agent, auto_init) if runner.wait_for_spring(): print("Spring application started successfully!") return 0 From 5d9f0107257d0269f8ef231fa569ae79529a691f Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Fri, 11 Jul 2025 10:02:55 +0000 Subject: [PATCH 05/10] Format code --- .../sentry-samples-console/build.gradle.kts | 9 ++--- .../java/io/sentry/samples/console/Main.java | 19 ++++++----- .../ConsoleApplicationSystemTest.kt | 33 +++++++++++-------- .../io/sentry/systemtest/util/TestHelper.kt | 10 +++--- 4 files changed, 35 insertions(+), 36 deletions(-) diff --git a/sentry-samples/sentry-samples-console/build.gradle.kts b/sentry-samples/sentry-samples-console/build.gradle.kts index f259de9d965..b9d754db204 100644 --- a/sentry-samples/sentry-samples-console/build.gradle.kts +++ b/sentry-samples/sentry-samples-console/build.gradle.kts @@ -45,9 +45,7 @@ dependencies { // Configure the Shadow JAR (executable JAR with all dependencies) tasks.shadowJar { - manifest { - attributes["Main-Class"] = "io.sentry.samples.console.Main" - } + manifest { attributes["Main-Class"] = "io.sentry.samples.console.Main" } archiveClassifier.set("") // Remove the classifier so it replaces the regular JAR mergeServiceFiles() } @@ -59,9 +57,7 @@ tasks.jar { } // Fix the startScripts task dependency -tasks.startScripts { - dependsOn(tasks.shadowJar) -} +tasks.startScripts { dependsOn(tasks.shadowJar) } configure { test { java.srcDir("src/test/java") } } @@ -85,4 +81,3 @@ tasks.named("test").configure { filter { excludeTestsMatching("io.sentry.systemtest.*") } } - diff --git a/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java b/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java index 71c0144955e..7ee46649e97 100644 --- a/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java +++ b/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java @@ -82,15 +82,16 @@ public static void main(String[] args) throws InterruptedException { options.setTracesSampleRate(1.0); // set 0.5 to send 50% of traces // Determine traces sample rate based on the sampling context -// options.setTracesSampler( -// context -> { -// // only 10% of transactions with "/product" prefix will be collected -// if (!context.getTransactionContext().getName().startsWith("/products")) { -// return 0.1; -// } else { -// return 0.5; -// } -// }); + // options.setTracesSampler( + // context -> { + // // only 10% of transactions with "/product" prefix will be collected + // if (!context.getTransactionContext().getName().startsWith("/products")) + // { + // return 0.1; + // } else { + // return 0.5; + // } + // }); }); Sentry.addBreadcrumb( diff --git a/sentry-samples/sentry-samples-console/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt b/sentry-samples/sentry-samples-console/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt index b709e4f506e..18409c5b4e6 100644 --- a/sentry-samples/sentry-samples-console/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt +++ b/sentry-samples/sentry-samples-console/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt @@ -1,12 +1,11 @@ package io.sentry.systemtest import io.sentry.systemtest.util.TestHelper -import java.io.File import java.util.concurrent.TimeUnit -import org.junit.Test -import org.junit.Before import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test class ConsoleApplicationSystemTest { lateinit var testHelper: TestHelper @@ -20,16 +19,18 @@ class ConsoleApplicationSystemTest { @Test fun `console application sends expected events when run as JAR`() { val jarFile = testHelper.findJar("sentry-samples-console") - 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", - )) + 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); + process.waitFor(30, TimeUnit.SECONDS) assertEquals(0, process.exitValue()) // Verify that we received the expected events @@ -71,7 +72,8 @@ class ConsoleApplicationSystemTest { try { for (i in 0..9) { testHelper.ensureErrorReceived { event -> - val matches = event.message?.message?.contains("items we'll wait to flush to Sentry!") == true + val matches = + event.message?.message?.contains("items we'll wait to flush to Sentry!") == true if (matches) loopMessageCount++ matches } @@ -80,7 +82,10 @@ class ConsoleApplicationSystemTest { // Some loop messages might be missing, but we should have at least some } - assertTrue("Should receive at least 5 loop messages, got $loopMessageCount", loopMessageCount >= 5) + assertTrue( + "Should receive at least 5 loop messages, got $loopMessageCount", + loopMessageCount >= 5, + ) // Verify we have breadcrumbs testHelper.ensureErrorReceived { event -> diff --git a/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt b/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt index 98f358da2ed..56c699e995e 100644 --- a/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt +++ b/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt @@ -13,7 +13,6 @@ import io.sentry.protocol.SentryTransaction import io.sentry.systemtest.graphql.GraphqlTestClient import java.io.File import java.io.PrintWriter -import java.util.concurrent.TimeUnit import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNotNull @@ -279,9 +278,8 @@ class TestHelper(backendUrl: String) { fun findJar(prefix: String, inDir: String = "build/libs"): File { val buildDir = File(inDir) val jarFiles = - buildDir - .listFiles { _, name -> name.startsWith(prefix) && name.endsWith(".jar") } - ?.toList() ?: emptyList() + buildDir.listFiles { _, name -> name.startsWith(prefix) && name.endsWith(".jar") }?.toList() + ?: emptyList() if (jarFiles.isEmpty()) { throw AssertionError("No JAR found in ${buildDir.absolutePath}") @@ -291,8 +289,8 @@ class TestHelper(backendUrl: String) { } fun launch(jar: File, env: Map): Process { - val processBuilder = ProcessBuilder("java", "-jar", jar.absolutePath) - .inheritIO() // forward i/o to current process + val processBuilder = + ProcessBuilder("java", "-jar", jar.absolutePath).inheritIO() // forward i/o to current process processBuilder.environment().putAll(env) From e531d9d5c4db3c769cb8637bb011d8ed8c5c253b Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 11 Jul 2025 13:01:24 +0200 Subject: [PATCH 06/10] remove bash scripts --- test/system-test-run-all.sh | 13 ---------- test/system-test-run.sh | 33 ------------------------- test/system-test-sentry-server-start.sh | 4 --- test/system-test-sentry-server-stop.sh | 3 --- test/system-test-spring-server-start.sh | 19 -------------- test/wait-for-spring.sh | 3 --- 6 files changed, 75 deletions(-) delete mode 100755 test/system-test-run-all.sh delete mode 100755 test/system-test-run.sh delete mode 100755 test/system-test-sentry-server-start.sh delete mode 100755 test/system-test-sentry-server-stop.sh delete mode 100755 test/system-test-spring-server-start.sh delete mode 100755 test/wait-for-spring.sh diff --git a/test/system-test-run-all.sh b/test/system-test-run-all.sh deleted file mode 100755 index 007def46242..00000000000 --- a/test/system-test-run-all.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash - -./test/system-test-run.sh "sentry-samples-spring-boot" "0" "true" "0" -./test/system-test-run.sh "sentry-samples-spring-boot-opentelemetry-noagent" "0" "true" "0" -./test/system-test-run.sh "sentry-samples-spring-boot-opentelemetry" "1" "true" "0" -./test/system-test-run.sh "sentry-samples-spring-boot-opentelemetry" "1" "false" "0" -./test/system-test-run.sh "sentry-samples-spring-boot-webflux-jakarta" "0" "true" "0" -./test/system-test-run.sh "sentry-samples-spring-boot-webflux" "0" "true" "0" -./test/system-test-run.sh "sentry-samples-spring-boot-jakarta" "0" "true" "0" -./test/system-test-run.sh "sentry-samples-spring-boot-jakarta-opentelemetry-noagent" "0" "true" "0" -./test/system-test-run.sh "sentry-samples-spring-boot-jakarta-opentelemetry" "1" "true" "0" -./test/system-test-run.sh "sentry-samples-spring-boot-jakarta-opentelemetry" "1" "false" "0" -./test/system-test-run.sh "sentry-samples-console" "0" "true" "0" diff --git a/test/system-test-run.sh b/test/system-test-run.sh deleted file mode 100755 index c4838923c6a..00000000000 --- a/test/system-test-run.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash - -readonly SAMPLE_MODULE=$1 -readonly JAVA_AGENT=$2 -readonly JAVA_AGENT_AUTO_INIT=$3 -readonly BUILD_BEFORE_RUN=$4 - -if [[ "$BUILD_BEFORE_RUN" == "1" ]]; then - echo "Building before Test run" - ./gradlew :sentry-samples:${SAMPLE_MODULE}:assemble -fi - -test/system-test-sentry-server-start.sh -MOCK_SERVER_PID=$(cat sentry-mock-server.pid) -echo "started mock server ${SAMPLE_MODULE}-${JAVA_AGENT}-${JAVA_AGENT_AUTO_INIT} with PID ${MOCK_SERVER_PID}" - -if [[ $SAMPLE_MODULE == *"spring"* ]]; then - test/system-test-spring-server-start.sh "${SAMPLE_MODULE}" "${JAVA_AGENT}" "${JAVA_AGENT_AUTO_INIT}" - SUT_PID=$(cat spring-server.pid) - echo "started spring server ${SAMPLE_MODULE}-${JAVA_AGENT}-${JAVA_AGENT_AUTO_INIT} with PID ${SUT_PID}" - - test/wait-for-spring.sh -fi - -./gradlew :sentry-samples:${SAMPLE_MODULE}:systemTest -TESTRUN_RETVAL=$? - -echo "killing mock server ${SAMPLE_MODULE}-${JAVA_AGENT}-${JAVA_AGENT_AUTO_INIT} with PID ${MOCK_SERVER_PID}" -kill $SUT_PID -echo "killing spring server ${SAMPLE_MODULE}-${JAVA_AGENT}-${JAVA_AGENT_AUTO_INIT} with PID ${SUT_PID}" -kill $MOCK_SERVER_PID - -exit $TESTRUN_RETVAL diff --git a/test/system-test-sentry-server-start.sh b/test/system-test-sentry-server-start.sh deleted file mode 100755 index 181c77b8d79..00000000000 --- a/test/system-test-sentry-server-start.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash - -python3 test/system-test-sentry-server.py > sentry-mock-server.txt 2>&1 & -echo $! > sentry-mock-server.pid diff --git a/test/system-test-sentry-server-stop.sh b/test/system-test-sentry-server-stop.sh deleted file mode 100755 index a9168eddd27..00000000000 --- a/test/system-test-sentry-server-stop.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash - -curl http://127.0.0.1:8000/STOP diff --git a/test/system-test-spring-server-start.sh b/test/system-test-spring-server-start.sh deleted file mode 100755 index 7e8aef516f3..00000000000 --- a/test/system-test-spring-server-start.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash - -readonly SAMPLE_MODULE=$1 -readonly JAVA_AGENT=$2 -readonly JAVA_AGENT_AUTO_INIT=$3 - -JAVA_AGENT_STRING="" - -echo "$JAVA_AGENT" - -if [[ "$JAVA_AGENT" == "1" ]]; then - JAVA_AGENT_STRING="-javaagent:$(find ./sentry-opentelemetry/sentry-opentelemetry-agent/build/libs/ -not -name '*javadoc*' -name '*-agent-*' -not -name '*sources*' -not -name '*dontuse*' -type f)" - echo "Using Java Agent: ${JAVA_AGENT_STRING}" -fi - -echo "$JAVA_AGENT_STRING" - -SENTRY_DSN="http://502f25099c204a2fbf4cb16edc5975d1@localhost:8000/0" SENTRY_AUTO_INIT=${JAVA_AGENT_AUTO_INIT} SENTRY_TRACES_SAMPLE_RATE=1.0 OTEL_TRACES_EXPORTER=none OTEL_METRICS_EXPORTER=none OTEL_LOGS_EXPORTER=none SENTRY_LOGS_ENABLED=true java ${JAVA_AGENT_STRING} -jar sentry-samples/${SAMPLE_MODULE}/build/libs/${SAMPLE_MODULE}-0.0.1-SNAPSHOT.jar > spring-server.txt 2>&1 & -echo $! > spring-server.pid diff --git a/test/wait-for-spring.sh b/test/wait-for-spring.sh deleted file mode 100755 index 7f989f793e6..00000000000 --- a/test/wait-for-spring.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash - -for attempt in {1..20}; do sleep 1; if $(curl --output /dev/null --silent --head --fail http://user:password@localhost:8080/actuator/health); then echo ready; break; fi; echo waiting...; done From 9d019163a6d267d89fc45c19d55f25f5ce1c019d Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 11 Jul 2025 13:28:01 +0200 Subject: [PATCH 07/10] install requests module --- .github/workflows/system-tests-backend.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/system-tests-backend.yml b/.github/workflows/system-tests-backend.yml index dd4e9c4c90b..e81c59082b7 100644 --- a/.github/workflows/system-tests-backend.yml +++ b/.github/workflows/system-tests-backend.yml @@ -51,6 +51,11 @@ jobs: with: python-version: '3.10.5' + - name: Install Python dependencies + run: | + python3 -m pip install --upgrade pip + python3 -m pip install requests + - name: Set up Java uses: actions/setup-java@v4 with: From 105e0d04d69e9a68a473c4b3ebf1d234ed63f4d2 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 11 Jul 2025 13:28:07 +0200 Subject: [PATCH 08/10] api --- .../api/sentry-system-test-support.api | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sentry-system-test-support/api/sentry-system-test-support.api b/sentry-system-test-support/api/sentry-system-test-support.api index 4f5bb9634a0..3e2653f717a 100644 --- a/sentry-system-test-support/api/sentry-system-test-support.api +++ b/sentry-system-test-support/api/sentry-system-test-support.api @@ -558,7 +558,8 @@ public final class io/sentry/systemtest/util/TestHelper { public final fun doesTransactionHaveOp (Lio/sentry/protocol/SentryTransaction;Ljava/lang/String;)Z public final fun doesTransactionHaveTraceId (Lio/sentry/protocol/SentryTransaction;Ljava/lang/String;)Z public final fun ensureEnvelopeCountIncreased ()V - public final fun ensureEnvelopeReceived (Lkotlin/jvm/functions/Function1;)V + public final fun ensureEnvelopeReceived (ILkotlin/jvm/functions/Function1;)V + public static synthetic fun ensureEnvelopeReceived$default (Lio/sentry/systemtest/util/TestHelper;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)V public final fun ensureErrorCount (Lcom/apollographql/apollo3/api/ApolloResponse;I)V public final fun ensureErrorReceived (Lkotlin/jvm/functions/Function1;)V public final fun ensureLogsReceived (Lkotlin/jvm/functions/Function2;)V @@ -567,11 +568,15 @@ public final class io/sentry/systemtest/util/TestHelper { public final fun ensureNoTransactionReceived (Lkotlin/jvm/functions/Function2;)V public final fun ensureTransactionReceived (Lkotlin/jvm/functions/Function2;)V public final fun ensureTransactionWithSpanReceived (Lkotlin/jvm/functions/Function1;)V + public final fun findJar (Ljava/lang/String;Ljava/lang/String;)Ljava/io/File; + public static synthetic fun findJar$default (Lio/sentry/systemtest/util/TestHelper;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Ljava/io/File; + public final fun getDsn ()Ljava/lang/String; public final fun getEnvelopeCounts ()Lio/sentry/systemtest/util/EnvelopeCounts; public final fun getGraphqlClient ()Lio/sentry/systemtest/graphql/GraphqlTestClient; public final fun getJsonSerializer ()Lio/sentry/JsonSerializer; public final fun getRestClient ()Lio/sentry/systemtest/util/RestTestClient; public final fun getSentryClient ()Lio/sentry/systemtest/util/SentryMockServerClient; + public final fun launch (Ljava/io/File;Ljava/util/Map;)Ljava/lang/Process; public final fun logObject (Ljava/lang/Object;)V public final fun reset ()V public final fun setEnvelopeCounts (Lio/sentry/systemtest/util/EnvelopeCounts;)V From 195937ca8cf3be1ab2e1562c720e0896729d2c1e Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 11 Jul 2025 14:35:54 +0200 Subject: [PATCH 09/10] fix gh script --- .github/workflows/system-tests-backend.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/system-tests-backend.yml b/.github/workflows/system-tests-backend.yml index e81c59082b7..eee6f7f90cc 100644 --- a/.github/workflows/system-tests-backend.yml +++ b/.github/workflows/system-tests-backend.yml @@ -25,7 +25,11 @@ jobs: agent-auto-init: [ "true" ] include: - sample: "sentry-samples-spring-boot" + agent: "false" + agent-auto-init: "true" - sample: "sentry-samples-spring-boot-opentelemetry-noagent" + agent: "false" + agent-auto-init: "true" - sample: "sentry-samples-spring-boot-opentelemetry" agent: "true" agent-auto-init: "true" @@ -33,8 +37,14 @@ jobs: agent: "true" agent-auto-init: "false" - sample: "sentry-samples-spring-boot-webflux-jakarta" + agent: "false" + agent-auto-init: "true" - sample: "sentry-samples-spring-boot-webflux" + agent: "false" + agent-auto-init: "true" - sample: "sentry-samples-spring-boot-jakarta-opentelemetry-noagent" + agent: "false" + agent-auto-init: "true" - sample: "sentry-samples-spring-boot-jakarta-opentelemetry" agent: "true" agent-auto-init: "true" @@ -42,6 +52,8 @@ jobs: agent: "true" agent-auto-init: "false" - sample: "sentry-samples-console" + agent: "false" + agent-auto-init: "true" steps: - uses: actions/checkout@v4 with: From 59e4f2b77608a020e82dba3bd2f3daa9b484139d Mon Sep 17 00:00:00 2001 From: Lorenzo Cian Date: Mon, 14 Jul 2025 19:46:49 +0200 Subject: [PATCH 10/10] Improve setup for backend e2e tests (#4554) --- .envrc | 2 + .github/workflows/system-tests-backend.yml | 2 +- .gitignore | 2 + Makefile | 17 +- requirements.txt | 5 + .../java/io/sentry/samples/console/Main.java | 21 +- test/system-test-runner.py | 440 ++++++++++-------- 7 files changed, 266 insertions(+), 223 deletions(-) create mode 100644 .envrc create mode 100644 requirements.txt diff --git a/.envrc b/.envrc new file mode 100644 index 00000000000..97b3f16c6f7 --- /dev/null +++ b/.envrc @@ -0,0 +1,2 @@ +export VIRTUAL_ENV=".venv" +layout python3 diff --git a/.github/workflows/system-tests-backend.yml b/.github/workflows/system-tests-backend.yml index eee6f7f90cc..e6bf48a139f 100644 --- a/.github/workflows/system-tests-backend.yml +++ b/.github/workflows/system-tests-backend.yml @@ -66,7 +66,7 @@ jobs: - name: Install Python dependencies run: | python3 -m pip install --upgrade pip - python3 -m pip install requests + python3 -m pip install -r requirements.txt - name: Set up Java uses: actions/setup-java@v4 diff --git a/.gitignore b/.gitignore index 52bec85e0ba..90f9e885cf4 100644 --- a/.gitignore +++ b/.gitignore @@ -20,5 +20,7 @@ distributions/ *.vscode/ sentry-spring-boot-starter-jakarta/src/main/resources/META-INF/spring.factories sentry-samples/sentry-samples-spring-boot-jakarta/spy.log +sentry-mock-server.txt +spring-server.txt spy.log .kotlin diff --git a/Makefile b/Makefile index 0a0f2bbf8c2..55f465a9663 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all clean compile javadocs dryRelease update checkFormat api assembleBenchmarkTestRelease assembleUiTestRelease assembleUiTestCriticalRelease createCoverageReports runUiTestCritical check preMerge publish systemtest systemtest-interactive +.PHONY: all clean compile javadocs dryRelease update checkFormat api assembleBenchmarkTestRelease assembleUiTestRelease assembleUiTestCriticalRelease createCoverageReports runUiTestCritical setupPython systemTest systemTestInteractive check preMerge publish all: stop clean javadocs compile createCoverageReports assembleBenchmarks: assembleBenchmarkTestRelease @@ -10,6 +10,7 @@ publish: clean dryRelease clean: ./gradlew clean --no-configuration-cache rm -rf distributions + rm -rf .venv # build and run tests compile: @@ -59,13 +60,19 @@ createCoverageReports: ./gradlew jacocoTestReport ./gradlew koverXmlReportRelease +# Create the Python virtual environment for system tests, and install the necessary dependencies +setupPython: + @test -d .venv || python3 -m venv .venv + .venv/bin/pip install --upgrade pip + .venv/bin/pip install -r requirements.txt + # Run system tests for sample applications -systemtest: - python3 test/system-test-runner.py test --all +systemTest: setupPython + .venv/bin/python test/system-test-runner.py test --all # Run system tests with interactive module selection -systemtest-interactive: - python3 test/system-test-runner.py test --interactive +systemTestInteractive: setupPython + .venv/bin/python test/system-test-runner.py test --interactive # Run tests and lint check: diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000000..08623cdf27b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +certifi==2025.7.14 +charset-normalizer==3.4.2 +idna==3.10 +requests==2.32.4 +urllib3==2.5.0 diff --git a/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java b/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java index 7ee46649e97..16f5a09d1b4 100644 --- a/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java +++ b/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java @@ -66,8 +66,8 @@ public static void main(String[] args) throws InterruptedException { // Enable SDK logging with Debug level options.setDebug(true); // To change the verbosity, use: - // By default it's DEBUG. // options.setDiagnosticLevel(SentryLevel.ERROR); + // By default it's DEBUG. // A good option to have SDK debug log in prod is to use only level ERROR here. // Exclude frames from some packages from being "inApp" so are hidden by default in Sentry @@ -82,16 +82,15 @@ public static void main(String[] args) throws InterruptedException { options.setTracesSampleRate(1.0); // set 0.5 to send 50% of traces // Determine traces sample rate based on the sampling context - // options.setTracesSampler( - // context -> { - // // only 10% of transactions with "/product" prefix will be collected - // if (!context.getTransactionContext().getName().startsWith("/products")) - // { - // return 0.1; - // } else { - // return 0.5; - // } - // }); + // options.setTracesSampler( + // context -> { + // // only 10% of transactions with "/product" prefix will be collected + // if (!context.getTransactionContext().getName().startsWith("/products")) { + // return 0.1; + // } else { + // return 0.5; + // } + // }); }); Sentry.addBreadcrumb( diff --git a/test/system-test-runner.py b/test/system-test-runner.py index aed0d7126a2..619e6bda104 100644 --- a/test/system-test-runner.py +++ b/test/system-test-runner.py @@ -42,6 +42,12 @@ from typing import Optional, List, Tuple from dataclasses import dataclass +TERMINAL_COLUMNS: int = 60 +try: + TERMINAL_COLUMNS: int = os.get_terminal_size().columns +except: + pass + def str_to_bool(value: str) -> str: """Convert true/false string to 1/0 string for internal compatibility.""" if value.lower() in ('true', '1'): @@ -51,30 +57,50 @@ def str_to_bool(value: str) -> str: else: raise ValueError(f"Invalid boolean value: {value}. Use 'true' or 'false'") +@dataclass +class ModuleConfig: + """Configuration for a test module.""" + name: str + java_agent: str + java_agent_auto_init: str + build_before_run: str + + def uses_agent(self) -> bool: + """Check if this module uses the Java agent.""" + return str_to_bool(self.java_agent) == "1" + + def needs_build(self) -> bool: + """Check if this module needs to be built before running.""" + return str_to_bool(self.build_before_run) == "1" + + def is_spring_module(self) -> bool: + """Check if this is a Spring Boot module.""" + return "spring" in self.name + @dataclass class InteractiveSelection: """Result of interactive module selection.""" - modules: List[Tuple[str, str, str, str]] + modules: List[ModuleConfig] manual_test_mode: bool build_agent: bool - + def is_empty(self) -> bool: """Check if no modules were selected.""" return len(self.modules) == 0 - + def is_single_module(self) -> bool: """Check if exactly one module was selected.""" return len(self.modules) == 1 - - def get_first_module(self) -> Tuple[str, str, str, str]: + + def get_first_module(self) -> ModuleConfig: """Get the first selected module (for manual test mode).""" if self.is_empty(): raise ValueError("No modules selected") return self.modules[0] - + def has_agent_modules(self) -> bool: """Check if any selected modules use the Java agent.""" - return any(str_to_bool(agent) == "1" for _, agent, _, _ in self.modules) + return any(module_config.uses_agent() for module_config in self.modules) class SystemTestRunner: def __init__(self): @@ -84,16 +110,16 @@ def __init__(self): self.spring_server_pid: Optional[int] = None self.mock_server_pid_file = "sentry-mock-server.pid" self.spring_server_pid_file = "spring-server.pid" - + # Load existing PIDs if available self.mock_server_pid = self.read_pid_file(self.mock_server_pid_file) self.spring_server_pid = self.read_pid_file(self.spring_server_pid_file) - + if self.mock_server_pid: print(f"Found existing mock server PID: {self.mock_server_pid}") if self.spring_server_pid: print(f"Found existing Spring server PID: {self.spring_server_pid}") - + def read_pid_file(self, pid_file: str) -> Optional[int]: """Read PID from file if it exists.""" try: @@ -103,7 +129,7 @@ def read_pid_file(self, pid_file: str) -> Optional[int]: except (ValueError, IOError) as e: print(f"Error reading PID file {pid_file}: {e}") return None - + def is_process_running(self, pid: int) -> bool: """Check if a process with given PID is still running.""" try: @@ -112,14 +138,14 @@ def is_process_running(self, pid: int) -> bool: return True except (OSError, ProcessLookupError): return False - + def kill_process(self, pid: int, name: str) -> None: """Kill a process by PID.""" try: print(f"Killing existing {name} process with PID {pid}") os.kill(pid, signal.SIGTERM) time.sleep(2) # Give it time to terminate gracefully - + # Check if it's still running and force kill if necessary if self.is_process_running(pid): print(f"Process {pid} didn't terminate gracefully, force killing...") @@ -127,9 +153,9 @@ def kill_process(self, pid: int, name: str) -> None: time.sleep(1) except (OSError, ProcessLookupError): print(f"Process {pid} was already dead") - - + + def start_sentry_mock_server(self) -> None: """Start the Sentry mock server.""" print("Starting Sentry mock server...") @@ -141,21 +167,21 @@ def start_sentry_mock_server(self) -> None: stdout=log_file, stderr=subprocess.STDOUT ) - + # Store PID in instance variable and write to file self.mock_server_pid = self.mock_server_process.pid with open(self.mock_server_pid_file, "w") as pid_file: pid_file.write(str(self.mock_server_pid)) - + print(f"Started mock server with PID {self.mock_server_pid}") - + # Wait a moment for the server to start time.sleep(2) - + except Exception as e: print(f"Failed to start mock server: {e}") raise - + def stop_sentry_mock_server(self) -> None: """Stop the Sentry mock server.""" try: @@ -165,7 +191,7 @@ def stop_sentry_mock_server(self) -> None: print("Sent stop signal to mock server") except: print("Could not send graceful stop signal") - + # Kill the process - try process object first, then PID from file if self.mock_server_process and self.mock_server_process.poll() is None: print(f"Killing mock server process object with PID {self.mock_server_process.pid}") @@ -174,7 +200,7 @@ def stop_sentry_mock_server(self) -> None: elif self.mock_server_pid and self.is_process_running(self.mock_server_pid): print(f"Killing mock server from PID file with PID {self.mock_server_pid}") self.kill_process(self.mock_server_pid, "mock server") - + except Exception as e: print(f"Error stopping mock server: {e}") finally: @@ -182,56 +208,56 @@ def stop_sentry_mock_server(self) -> None: if os.path.exists(self.mock_server_pid_file): os.remove(self.mock_server_pid_file) self.mock_server_pid = None - + def find_agent_jar(self) -> Optional[str]: """Find the OpenTelemetry agent JAR file.""" agent_dir = Path("sentry-opentelemetry/sentry-opentelemetry-agent/build/libs/") if not agent_dir.exists(): return None - + for jar_file in agent_dir.glob("*.jar"): name = jar_file.name - if ("agent" in name and - "javadoc" not in name and - "sources" not in name and + if ("agent" in name and + "javadoc" not in name and + "sources" not in name and "dontuse" not in name): return str(jar_file) return None - + def build_agent_jar(self) -> int: """Build the OpenTelemetry agent JAR file.""" print("Building OpenTelemetry agent JAR...") return self.run_gradle_task(":sentry-opentelemetry:sentry-opentelemetry-agent:assemble") - + def ensure_agent_jar(self, skip_build: bool = False) -> Optional[str]: """Ensure the OpenTelemetry agent JAR exists, building it if necessary.""" agent_jar = self.find_agent_jar() if agent_jar: return agent_jar - + if skip_build: print("OpenTelemetry agent JAR not found and build was skipped") return None - + # Agent JAR doesn't exist, try to build it print("OpenTelemetry agent JAR not found, building it...") build_result = self.build_agent_jar() if build_result != 0: print("Failed to build OpenTelemetry agent JAR") return None - + # Try to find it again after building agent_jar = self.find_agent_jar() if not agent_jar: print("OpenTelemetry agent JAR still not found after building") return None - + return agent_jar - + def start_spring_server(self, sample_module: str, java_agent: str, java_agent_auto_init: str) -> None: """Start a Spring Boot server for testing.""" print(f"Starting Spring server for {sample_module}...") - + # Build environment variables env = os.environ.copy() env.update({ @@ -239,15 +265,15 @@ def start_spring_server(self, sample_module: str, java_agent: str, java_agent_au "SENTRY_AUTO_INIT": java_agent_auto_init, "SENTRY_TRACES_SAMPLE_RATE": "1.0", "OTEL_TRACES_EXPORTER": "none", - "OTEL_METRICS_EXPORTER": "none", + "OTEL_METRICS_EXPORTER": "none", "OTEL_LOGS_EXPORTER": "none", "SENTRY_LOGS_ENABLED": "true" }) - + # Build command jar_path = f"sentry-samples/{sample_module}/build/libs/{sample_module}-0.0.1-SNAPSHOT.jar" cmd = ["java"] - + if java_agent == "1": agent_jar = self.ensure_agent_jar() if agent_jar: @@ -255,9 +281,9 @@ def start_spring_server(self, sample_module: str, java_agent: str, java_agent_au print(f"Using Java Agent: {agent_jar}") else: print("Warning: Java agent was requested but could not be found or built") - + cmd.extend(["-jar", jar_path]) - + try: # Start the Spring server with open("spring-server.txt", "w") as log_file: @@ -267,22 +293,22 @@ def start_spring_server(self, sample_module: str, java_agent: str, java_agent_au stdout=log_file, stderr=subprocess.STDOUT ) - + # Store PID in instance variable and write to file self.spring_server_pid = self.spring_server_process.pid with open(self.spring_server_pid_file, "w") as pid_file: pid_file.write(str(self.spring_server_pid)) - + print(f"Started Spring server with PID {self.spring_server_pid}") - + except Exception as e: print(f"Failed to start Spring server: {e}") raise - + def wait_for_spring(self, max_attempts: int = 20) -> bool: """Wait for Spring Boot application to be ready.""" print("Waiting for Spring application to be ready...") - + for attempt in range(1, max_attempts + 1): try: response = requests.head( @@ -295,13 +321,13 @@ def wait_for_spring(self, max_attempts: int = 20) -> bool: return True except: pass - + print(f"Waiting... (attempt {attempt}/{max_attempts})") time.sleep(1) - + print("Spring application failed to become ready") return False - + def get_spring_status(self) -> dict: """Get status of Spring Boot application.""" status = { @@ -309,10 +335,10 @@ def get_spring_status(self) -> dict: "pid": self.spring_server_pid, "http_ready": False } - + if self.spring_server_pid and self.is_process_running(self.spring_server_pid): status["process_running"] = True - + # Check HTTP endpoint try: response = requests.head( @@ -324,9 +350,9 @@ def get_spring_status(self) -> dict: status["http_ready"] = True except: pass - + return status - + def get_sentry_status(self) -> dict: """Get status of Sentry mock server.""" status = { @@ -334,10 +360,10 @@ def get_sentry_status(self) -> dict: "pid": self.mock_server_pid, "http_ready": False } - + if self.mock_server_pid and self.is_process_running(self.mock_server_pid): status["process_running"] = True - + # Check HTTP endpoint try: response = requests.get("http://127.0.0.1:8000/envelope-count", timeout=2) @@ -345,25 +371,25 @@ def get_sentry_status(self) -> dict: status["http_ready"] = True except: pass - + return status - + def print_status_summary(self) -> None: """Print status summary of all services.""" print("=== Service Status ===") - + sentry_status = self.get_sentry_status() print(f"Sentry Mock Server:") print(f" PID: {sentry_status['pid'] or 'None'}") print(f" Process Running: {'✅' if sentry_status['process_running'] else '❌'}") print(f" HTTP Ready: {'✅' if sentry_status['http_ready'] else '❌'}") - + spring_status = self.get_spring_status() print(f"Spring Boot App:") print(f" PID: {spring_status['pid'] or 'None'}") print(f" Process Running: {'✅' if spring_status['process_running'] else '❌'}") print(f" HTTP Ready: {'✅' if spring_status['http_ready'] else '❌'}") - + def stop_spring_server(self) -> None: """Stop the Spring Boot server.""" try: @@ -378,7 +404,7 @@ def stop_spring_server(self) -> None: elif self.spring_server_pid and self.is_process_running(self.spring_server_pid): print(f"Killing Spring server from PID file with PID {self.spring_server_pid}") self.kill_process(self.spring_server_pid, "Spring server") - + except Exception as e: print(f"Error stopping Spring server: {e}") finally: @@ -386,17 +412,17 @@ def stop_spring_server(self) -> None: if os.path.exists(self.spring_server_pid_file): os.remove(self.spring_server_pid_file) self.spring_server_pid = None - + def get_build_task(self, sample_module: str) -> str: """Get the appropriate build task for a module.""" return "bootJar" if "spring" in sample_module else "assemble" - + def build_module(self, sample_module: str) -> int: """Build a sample module using the appropriate task.""" build_task = self.get_build_task(sample_module) print(f"Building {sample_module} using {build_task} task") return self.run_gradle_task(f":sentry-samples:{sample_module}:{build_task}") - + def run_gradle_task(self, task: str) -> int: """Run a Gradle task and return the exit code.""" print(f"Running: ./gradlew {task}") @@ -406,8 +432,8 @@ def run_gradle_task(self, task: str) -> int: except Exception as e: print(f"Failed to run Gradle task: {e}") return 1 - - def setup_test_infrastructure(self, sample_module: str, java_agent: str, + + def setup_test_infrastructure(self, sample_module: str, java_agent: str, java_agent_auto_init: str, build_before_run: str) -> int: """Set up test infrastructure. Returns 0 on success, error code on failure.""" # Build if requested @@ -417,18 +443,18 @@ def setup_test_infrastructure(self, sample_module: str, java_agent: str, if build_result != 0: print("Build failed") return build_result - + # Ensure agent JAR is available if needed if java_agent == "1": agent_jar = self.ensure_agent_jar() if not agent_jar: print("Error: Java agent was requested but could not be found or built") return 1 - + # Start mock server print("Starting Sentry mock server...") self.start_sentry_mock_server() - + # Start Spring server if it's a Spring module if "spring" in sample_module: print(f"Starting Spring server for {sample_module}...") @@ -437,65 +463,65 @@ def setup_test_infrastructure(self, sample_module: str, java_agent: str, print("Spring application failed to start!") return 1 print("Spring application is ready!") - + return 0 - - def run_single_test(self, sample_module: str, java_agent: str, + + def run_single_test(self, sample_module: str, java_agent: str, java_agent_auto_init: str, build_before_run: str) -> int: """Run a single system test.""" print(f"Running system test for {sample_module}") - + try: # Set up infrastructure setup_result = self.setup_test_infrastructure(sample_module, java_agent, java_agent_auto_init, build_before_run) if setup_result != 0: return setup_result - + # Run the system test test_result = self.run_gradle_task(f":sentry-samples:{sample_module}:systemTest") - + return test_result - + finally: # Cleanup if "spring" in sample_module: self.stop_spring_server() self.stop_sentry_mock_server() - + def run_all_tests(self) -> int: """Run all system tests.""" test_configs = self.get_available_modules() - + failed_tests = [] - - for sample_module, java_agent, java_agent_auto_init, build_before_run in test_configs: + + for i, module_config in enumerate(test_configs): # Convert true/false to internal 1/0 format - agent = str_to_bool(java_agent) - auto_init = java_agent_auto_init # already in correct format - build = str_to_bool(build_before_run) - - print(f"\n{'='*60}") - print(f"Running test: {sample_module} (agent={java_agent}, auto_init={java_agent_auto_init})") - print(f"{'='*60}") - - result = self.run_single_test(sample_module, agent, auto_init, build) - + agent = str_to_bool(module_config.java_agent) + auto_init = module_config.java_agent_auto_init # already in correct format + build = str_to_bool(module_config.build_before_run) + + print(f"\n{'='*TERMINAL_COLUMNS}") + print(f"Running test {i + 1}/{len(test_configs)}: {module_config.name} (agent={module_config.java_agent}, auto_init={module_config.java_agent_auto_init})") + print(f"{'='*TERMINAL_COLUMNS}") + + result = self.run_single_test(module_config.name, agent, auto_init, build) + if result != 0: # Find the module number in the full list for interactive reference - module_number = self._find_module_number(sample_module, java_agent, java_agent_auto_init) - failed_tests.append((module_number, sample_module, java_agent, java_agent_auto_init)) - print(f"❌ Test failed: {sample_module}") + module_number = self._find_module_number(module_config.name, module_config.java_agent, module_config.java_agent_auto_init) + failed_tests.append((module_number, module_config.name, module_config.java_agent, module_config.java_agent_auto_init)) + print(f"❌ Test failed: {module_config.name}") else: - print(f"✅ Test passed: {sample_module}") - + print(f"✅ Test passed: {module_config.name}") + # Summary - print(f"\n{'='*60}") + print(f"\n{'='*TERMINAL_COLUMNS}") print("TEST SUMMARY") - print(f"{'='*60}") + print(f"{'='*TERMINAL_COLUMNS}") print(f"Total tests: {len(test_configs)}") print(f"Passed: {len(test_configs) - len(failed_tests)}") print(f"Failed: {len(failed_tests)}") - + if failed_tests: print("\nFailed tests (for interactive mode, use these numbers):") for module_number, sample_module, java_agent, java_agent_auto_init in failed_tests: @@ -504,22 +530,22 @@ def run_all_tests(self) -> int: else: print("\n🎉 All tests passed!") return 0 - - def run_manual_test_mode(self, sample_module: str, java_agent: str, + + def run_manual_test_mode(self, sample_module: str, java_agent: str, java_agent_auto_init: str, build_before_run: str) -> int: """Set up infrastructure for manual testing from IDE.""" print(f"Setting up manual test environment for {sample_module}") - + try: # Set up infrastructure setup_result = self.setup_test_infrastructure(sample_module, java_agent, java_agent_auto_init, build_before_run) if setup_result != 0: return setup_result - + # Show status and wait for user - print("\n" + "="*60) + print("\n" + "="*TERMINAL_COLUMNS) print("🚀 Manual test environment ready 🚀") - print("="*60) + print("="*TERMINAL_COLUMNS) self.print_status_summary() print(f"\nInfrastructure is ready for manual testing of: {sample_module}") print("You can now run your system tests from your IDE.") @@ -528,57 +554,59 @@ def run_manual_test_mode(self, sample_module: str, java_agent: str, print(f" - Java Agent: {'Yes' if java_agent == '1' else 'No'}") print(f" - Agent Auto-init: {java_agent_auto_init}") print(f" - Mock DSN: http://502f25099c204a2fbf4cb16edc5975d1@localhost:8000/0") - + if "spring" in sample_module: print("\nSpring Boot app is running on: http://localhost:8080") - + print("\nPress Enter to stop the infrastructure and exit...") - + # Wait for user input try: input() except KeyboardInterrupt: print("\nReceived interrupt signal") - + print("\nStopping infrastructure...") return 0 - + finally: # Cleanup will happen in the finally block of main() pass - - def get_available_modules(self) -> List[Tuple[str, str, str, str]]: + + def get_available_modules(self) -> List[ModuleConfig]: """Get list of all available test modules.""" return [ - ("sentry-samples-spring-boot", "false", "true", "false"), - ("sentry-samples-spring-boot-opentelemetry-noagent", "false", "true", "false"), - ("sentry-samples-spring-boot-opentelemetry", "true", "true", "false"), - ("sentry-samples-spring-boot-opentelemetry", "true", "false", "false"), - ("sentry-samples-spring-boot-webflux-jakarta", "false", "true", "false"), - ("sentry-samples-spring-boot-webflux", "false", "true", "false"), - ("sentry-samples-spring-boot-jakarta", "false", "true", "false"), - ("sentry-samples-spring-boot-jakarta-opentelemetry-noagent", "false", "true", "false"), - ("sentry-samples-spring-boot-jakarta-opentelemetry", "true", "true", "false"), - ("sentry-samples-spring-boot-jakarta-opentelemetry", "true", "false", "false"), - ("sentry-samples-console", "false", "true", "false"), + ModuleConfig("sentry-samples-spring-boot", "false", "true", "false"), + ModuleConfig("sentry-samples-spring-boot-opentelemetry-noagent", "false", "true", "false"), + ModuleConfig("sentry-samples-spring-boot-opentelemetry", "true", "true", "false"), + ModuleConfig("sentry-samples-spring-boot-opentelemetry", "true", "false", "false"), + ModuleConfig("sentry-samples-spring-boot-webflux-jakarta", "false", "true", "false"), + ModuleConfig("sentry-samples-spring-boot-webflux", "false", "true", "false"), + ModuleConfig("sentry-samples-spring-boot-jakarta", "false", "true", "false"), + ModuleConfig("sentry-samples-spring-boot-jakarta-opentelemetry-noagent", "false", "true", "false"), + ModuleConfig("sentry-samples-spring-boot-jakarta-opentelemetry", "true", "true", "false"), + ModuleConfig("sentry-samples-spring-boot-jakarta-opentelemetry", "true", "false", "false"), + ModuleConfig("sentry-samples-console", "false", "true", "false"), ] - + def _find_module_number(self, module_name: str, agent: str, auto_init: str) -> int: """Find the module number in the interactive list (1-based).""" modules = self.get_available_modules() - for i, (mod_name, mod_agent, mod_auto_init, _) in enumerate(modules, 1): - if mod_name == module_name and mod_agent == agent and mod_auto_init == auto_init: + for i, module_config in enumerate(modules, 1): + if (module_config.name == module_name and + module_config.java_agent == agent and + module_config.java_agent_auto_init == auto_init): return i return 0 # Should not happen, but return 0 if not found - + def parse_selection(self, user_input: str, max_index: int) -> List[int]: """Parse user selection string into list of indices.""" if user_input.strip() == "*": return list(range(max_index)) - + indices = [] parts = user_input.split(",") - + for part in parts: part = part.strip() if "-" in part: @@ -596,34 +624,34 @@ def parse_selection(self, user_input: str, max_index: int) -> List[int]: indices.append(int(part) - 1) except ValueError: raise ValueError(f"Invalid number: {part}") - + # Remove duplicates and sort indices = sorted(set(indices)) - + # Validate indices for idx in indices: if idx < 0 or idx >= max_index: raise ValueError(f"Index {idx + 1} is out of range (1-{max_index})") - + return indices - + def interactive_module_selection(self) -> InteractiveSelection: """Display modules and get user selection.""" modules = self.get_available_modules() - + print("\nAvailable test modules:") print("=" * 80) - for i, (module, agent, auto_init, build) in enumerate(modules, 1): - agent_text = "with agent" if str_to_bool(agent) == "1" else "no agent" - auto_init_text = f"auto-init: {auto_init}" - print(f"{i:2d}. {module:<50} ({agent_text}, {auto_init_text})") - + for i, module_config in enumerate(modules, 1): + agent_text = "with agent" if module_config.uses_agent() else "no agent" + auto_init_text = f"auto-init: {module_config.java_agent_auto_init}" + print(f"{i:2d}. {module_config.name:<50} ({agent_text}, {auto_init_text})") + print("\nSelection options:") print(" * = all modules") print(" Single: 1, 5, 8") print(" Range: 1-4, 6-8") print(" Combined: 1,2,4-5,8") - + selected_modules = [] while True: try: @@ -631,29 +659,29 @@ def interactive_module_selection(self) -> InteractiveSelection: if not user_input: print("Please enter a selection.") continue - + selected_indices = self.parse_selection(user_input, len(modules)) selected_modules = [modules[i] for i in selected_indices] - + # Show confirmation print(f"\nSelected {len(selected_modules)} module(s):") - for i, (module, agent, auto_init, build) in enumerate(selected_modules, 1): - agent_text = "with agent" if str_to_bool(agent) == "1" else "no agent" - print(f" {i}. {module} ({agent_text}, auto-init: {auto_init})") - + for i, module_config in enumerate(selected_modules, 1): + agent_text = "with agent" if module_config.uses_agent() else "no agent" + print(f" {i}. {module_config.name} ({agent_text}, auto-init: {module_config.java_agent_auto_init})") + confirm = input("\nProceed with these selections? [Y/n]: ").strip().lower() if confirm in ('', 'y', 'yes'): break else: print("Please make a new selection.") - + except ValueError as e: print(f"Error: {e}") print("Please try again.") except KeyboardInterrupt: print("\nOperation cancelled.") return InteractiveSelection(modules=[], manual_test_mode=False, build_agent=False) - + # Ask about test mode manual_test_mode = False while True: @@ -670,10 +698,10 @@ def interactive_module_selection(self) -> InteractiveSelection: except KeyboardInterrupt: print("\nOperation cancelled.") return InteractiveSelection(modules=[], manual_test_mode=False, build_agent=False) - + # Ask about building agent if any modules use it build_agent = False - has_agent_modules = any(str_to_bool(agent) == "1" for _, agent, _, _ in selected_modules) + has_agent_modules = any(module_config.uses_agent() for module_config in selected_modules) if has_agent_modules: while True: try: @@ -689,17 +717,17 @@ def interactive_module_selection(self) -> InteractiveSelection: except KeyboardInterrupt: print("\nOperation cancelled.") return InteractiveSelection(modules=[], manual_test_mode=False, build_agent=False) - + return InteractiveSelection(modules=selected_modules, manual_test_mode=manual_test_mode, build_agent=build_agent) - + def run_interactive_tests(self, agent: str, auto_init: str, build: str) -> int: """Run tests with interactive module selection.""" selection = self.interactive_module_selection() - + if selection.is_empty(): print("No modules selected. Exiting.") return 0 - + # Build agent JAR if requested and modules use agent if selection.build_agent and selection.has_agent_modules(): print("\nBuilding OpenTelemetry agent JAR...") @@ -708,55 +736,55 @@ def run_interactive_tests(self, agent: str, auto_init: str, build: str) -> int: print("Failed to build OpenTelemetry agent JAR") return build_result print("✅ OpenTelemetry agent JAR built successfully") - + # Handle manual test mode if selection.manual_test_mode: if not selection.is_single_module(): print("Error: Manual test mode can only be used with a single module.") print("Please select only one module for manual testing.") return 1 - - sample_module, test_agent, test_auto_init, test_build = selection.get_first_module() + + module_config = selection.get_first_module() # Convert true/false to internal 1/0 format - agent = str_to_bool(test_agent) - auto_init = test_auto_init # already in correct format - build = str_to_bool(test_build) - - print(f"\nSetting up manual test environment for: {sample_module}") - return self.run_manual_test_mode(sample_module, agent, auto_init, build) - + agent = str_to_bool(module_config.java_agent) + auto_init = module_config.java_agent_auto_init # already in correct format + build = str_to_bool(module_config.build_before_run) + + print(f"\nSetting up manual test environment for: {module_config.name}") + return self.run_manual_test_mode(module_config.name, agent, auto_init, build) + # Handle automatic test running failed_tests = [] - - for i, (sample_module, test_agent, test_auto_init, test_build) in enumerate(selection.modules, 1): + + for i, module_config in enumerate(selection.modules, 1): # Convert true/false to internal 1/0 format - agent = str_to_bool(test_agent) - auto_init = test_auto_init # already in correct format - build = str_to_bool(test_build) - - print(f"\n{'='*60}") - print(f"Running test {i}/{len(selection.modules)}: {sample_module}") - print(f"Agent: {test_agent}, Auto-init: {test_auto_init}") - print(f"{'='*60}") - - result = self.run_single_test(sample_module, agent, auto_init, build) - + agent = str_to_bool(module_config.java_agent) + auto_init = module_config.java_agent_auto_init # already in correct format + build = str_to_bool(module_config.build_before_run) + + print(f"\n{'='*TERMINAL_COLUMNS}") + print(f"Running test {i}/{len(selection.modules)}: {module_config.name}") + print(f"Agent: {module_config.java_agent}, Auto-init: {module_config.java_agent_auto_init}") + print(f"{'='*TERMINAL_COLUMNS}") + + result = self.run_single_test(module_config.name, agent, auto_init, build) + if result != 0: # Find the module number in the full list for interactive reference - module_number = self._find_module_number(sample_module, test_agent, test_auto_init) - failed_tests.append((module_number, sample_module, test_agent, test_auto_init)) - print(f"❌ Test failed: {sample_module}") + module_number = self._find_module_number(module_config.name, module_config.java_agent, module_config.java_agent_auto_init) + failed_tests.append((module_number, module_config.name, module_config.java_agent, module_config.java_agent_auto_init)) + print(f"❌ Test failed: {module_config.name}") else: - print(f"✅ Test passed: {sample_module}") - + print(f"✅ Test passed: {module_config.name}") + # Summary - print(f"\n{'='*60}") + print(f"\n{'='*TERMINAL_COLUMNS}") print("TEST SUMMARY") - print(f"{'='*60}") + print(f"{'='*TERMINAL_COLUMNS}") print(f"Total tests: {len(selection.modules)}") print(f"Passed: {len(selection.modules) - len(failed_tests)}") print(f"Failed: {len(failed_tests)}") - + if failed_tests: print("\nFailed tests (for interactive mode, use these numbers):") for module_number, sample_module, test_agent, test_auto_init in failed_tests: @@ -765,7 +793,7 @@ def run_interactive_tests(self, agent: str, auto_init: str, build: str) -> int: else: print("\n🎉 All tests passed!") return 0 - + def cleanup_on_exit(self, signum, frame): """Cleanup handler for signals.""" print(f"\nReceived signal {signum}, cleaning up...") @@ -776,7 +804,7 @@ def cleanup_on_exit(self, signum, frame): def main(): parser = argparse.ArgumentParser(description="System Test Runner for Sentry Java") subparsers = parser.add_subparsers(dest="command", help="Available commands") - + # Test subcommand test_parser = subparsers.add_parser("test", help="Run system tests") test_group = test_parser.add_mutually_exclusive_group(required=True) @@ -787,54 +815,54 @@ def main(): test_parser.add_argument("--auto-init", default="true", help="Auto-init agent (true or false)") test_parser.add_argument("--build", default="false", help="Build before running (true or false)") test_parser.add_argument("--manual-test", action="store_true", help="Set up infrastructure but pause for manual testing from IDE") - + # Spring subcommand spring_parser = subparsers.add_parser("spring", help="Manage Spring Boot applications") spring_subparsers = spring_parser.add_subparsers(dest="spring_action", help="Spring actions") - + spring_start_parser = spring_subparsers.add_parser("start", help="Start Spring Boot application") spring_start_parser.add_argument("module", help="Sample module to start") spring_start_parser.add_argument("--agent", default="false", help="Use Java agent (true or false)") spring_start_parser.add_argument("--auto-init", default="true", help="Auto-init agent (true or false)") spring_start_parser.add_argument("--build", default="false", help="Build before starting (true or false)") - + spring_stop_parser = spring_subparsers.add_parser("stop", help="Stop Spring Boot application") - + spring_wait_parser = spring_subparsers.add_parser("wait", help="Wait for Spring Boot application to be ready") spring_wait_parser.add_argument("--timeout", type=int, default=20, help="Max attempts to wait (default: 20)") - + spring_status_parser = spring_subparsers.add_parser("status", help="Check Spring Boot application status") - + # Sentry subcommand sentry_parser = subparsers.add_parser("sentry", help="Manage Sentry mock server") sentry_subparsers = sentry_parser.add_subparsers(dest="sentry_action", help="Sentry actions") - + sentry_start_parser = sentry_subparsers.add_parser("start", help="Start Sentry mock server") sentry_stop_parser = sentry_subparsers.add_parser("stop", help="Stop Sentry mock server") sentry_status_parser = sentry_subparsers.add_parser("status", help="Check Sentry mock server status") - + # Status subcommand status_parser = subparsers.add_parser("status", help="Show status of all services") - + args = parser.parse_args() - + if not args.command: parser.print_help() return 1 - + runner = SystemTestRunner() - + # Set up signal handlers for cleanup signal.signal(signal.SIGINT, runner.cleanup_on_exit) signal.signal(signal.SIGTERM, runner.cleanup_on_exit) - + try: if args.command == "test": # Convert true/false arguments to internal 1/0 format agent = str_to_bool(args.agent) auto_init = args.auto_init # already accepts true/false build = str_to_bool(args.build) - + if args.manual_test and args.module: return runner.run_manual_test_mode(args.module, agent, auto_init, build) elif args.manual_test and args.all: @@ -849,14 +877,14 @@ def main(): return runner.run_single_test(args.module, agent, auto_init, build) elif args.interactive: return runner.run_interactive_tests(agent, auto_init, build) - + elif args.command == "spring": if args.spring_action == "start": # Convert true/false arguments to internal format agent = str_to_bool(args.agent) auto_init = args.auto_init # already accepts true/false build = str_to_bool(args.build) - + # Build if requested if build == "1": print("Building before starting Spring application") @@ -864,7 +892,7 @@ def main(): if build_result != 0: print("Build failed") return build_result - + runner.start_spring_server(args.module, agent, auto_init) if runner.wait_for_spring(): print("Spring application started successfully!") @@ -893,7 +921,7 @@ def main(): else: spring_parser.print_help() return 1 - + elif args.command == "sentry": if args.sentry_action == "start": runner.start_sentry_mock_server() @@ -913,14 +941,14 @@ def main(): else: sentry_parser.print_help() return 1 - + elif args.command == "status": runner.print_status_summary() return 0 else: parser.print_help() return 1 - + except KeyboardInterrupt: print("\nInterrupted by user") return 1 @@ -934,4 +962,4 @@ def main(): runner.stop_sentry_mock_server() if __name__ == "__main__": - sys.exit(main()) \ No newline at end of file + sys.exit(main())