diff --git a/.github/workflows/system-tests-backend.yml b/.github/workflows/system-tests-backend.yml index 13f438987a3..47c2af613e5 100644 --- a/.github/workflows/system-tests-backend.yml +++ b/.github/workflows/system-tests-backend.yml @@ -63,6 +63,9 @@ jobs: - sample: "sentry-samples-jul" agent: "false" agent-auto-init: "true" + - sample: "sentry-samples-spring-jakarta" + agent: "false" + agent-auto-init: "true" steps: - uses: actions/checkout@v4 with: diff --git a/.gitignore b/.gitignore index 90f9e885cf4..be4f11ce3d2 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,9 @@ distributions/ 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 +tomcat-server.txt spring-server.txt +*.pid spy.log .kotlin +**/tomcat.8080/webapps/ diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 06fa80f9436..47c4edcd825 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -122,7 +122,7 @@ retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit retrofit-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" } sentry-native-ndk = { module = "io.sentry:sentry-native-ndk", version = "0.10.0" } servlet-api = { module = "javax.servlet:javax.servlet-api", version = "3.1.0" } -servlet-jakarta-api = { module = "jakarta.servlet:jakarta.servlet-api", version = "5.0.0" } +servlet-jakarta-api = { module = "jakarta.servlet:jakarta.servlet-api", version = "6.1.0" } slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } slf4j-jdk14 = { module = "org.slf4j:slf4j-jdk14", version.ref = "slf4j" } slf4j2-api = { module = "org.slf4j:slf4j-api", version = "2.0.5" } @@ -152,6 +152,10 @@ springboot3-starter-jdbc = { module = "org.springframework.boot:spring-boot-star springboot3-starter-actuator = { module = "org.springframework.boot:spring-boot-starter-actuator", version.ref = "springboot3" } timber = { module = "com.jakewharton.timber:timber", version = "4.7.1" } +# tomcat libraries +tomcat-catalina-jakarta = { module = "org.apache.tomcat:tomcat-catalina", version = "11.0.10" } +tomcat-embed-jasper-jakarta = { module = "org.apache.tomcat.embed:tomcat-embed-jasper", version = "11.0.10" } + # test libraries androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version = "1.6.8" } androidx-test-core = { module = "androidx.test:core", version.ref = "androidxTestCore" } diff --git a/sentry-samples/sentry-samples-spring-jakarta/build.gradle.kts b/sentry-samples/sentry-samples-spring-jakarta/build.gradle.kts index 2ecc26c4045..60f36b98515 100644 --- a/sentry-samples/sentry-samples-spring-jakarta/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-jakarta/build.gradle.kts @@ -3,6 +3,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.springframework.boot.gradle.plugin.SpringBootPlugin plugins { + application alias(libs.plugins.springboot3) apply false alias(libs.plugins.spring.dependency.management) kotlin("jvm") @@ -11,6 +12,11 @@ plugins { alias(libs.plugins.gretty) } +application { mainClass.set("io.sentry.samples.spring.jakarta.Main") } + +// Ensure WAR is up to date before run task +tasks.named("run") { dependsOn(tasks.named("war")) } + group = "io.sentry.sample.spring-jakarta" version = "0.0.1-SNAPSHOT" @@ -37,16 +43,43 @@ dependencies { implementation(libs.logback.classic) implementation(libs.servlet.jakarta.api) implementation(libs.slf4j2.api) + + implementation(libs.tomcat.catalina.jakarta) + implementation(libs.tomcat.embed.jasper.jakarta) + + testImplementation(projects.sentrySystemTestSupport) + testImplementation(libs.kotlin.test.junit) testImplementation(libs.springboot.starter.test) { exclude(group = "org.junit.vintage", module = "junit-vintage-engine") } } -tasks.withType().configureEach { useJUnitPlatform() } - tasks.withType().configureEach { kotlinOptions { freeCompilerArgs = listOf("-Xjsr305=strict") jvmTarget = JavaVersion.VERSION_17.toString() } } + +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.*") } +} diff --git a/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/Main.java b/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/Main.java new file mode 100644 index 00000000000..0d67ee55c79 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/Main.java @@ -0,0 +1,32 @@ +package io.sentry.samples.spring.jakarta; + +import java.io.File; +import java.io.IOException; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.startup.Tomcat; + +public class Main { + + public static void main(String[] args) throws LifecycleException, IOException { + File webappsDirectory = new File("./tomcat.8080/webapps"); + if (!webappsDirectory.exists()) { + boolean didCreateDirectories = webappsDirectory.mkdirs(); + if (!didCreateDirectories) { + throw new RuntimeException( + "Failed to create directory required by Tomcat: " + webappsDirectory.getAbsolutePath()); + } + } + + String pathToWar = "./build/libs"; + String warName = "sentry-samples-spring-jakarta-0.0.1-SNAPSHOT"; + File war = new File(pathToWar + "/" + warName + ".war"); + + Tomcat tomcat = new Tomcat(); + tomcat.setPort(8080); + tomcat.getConnector(); + + tomcat.addWebapp("/" + warName, war.getCanonicalPath()); + tomcat.start(); + tomcat.getServer().await(); + } +} diff --git a/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/web/Person.java b/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/web/Person.java index ee1f4f0827e..6138cd6403e 100644 --- a/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/web/Person.java +++ b/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/web/Person.java @@ -1,10 +1,15 @@ package io.sentry.samples.spring.jakarta.web; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + public class Person { private final String firstName; private final String lastName; - public Person(String firstName, String lastName) { + @JsonCreator + public Person( + @JsonProperty("firstName") String firstName, @JsonProperty("lastName") String lastName) { this.firstName = firstName; this.lastName = lastName; } diff --git a/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/web/PersonController.java b/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/web/PersonController.java index c5ee953810c..dab805281e1 100644 --- a/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/web/PersonController.java +++ b/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/web/PersonController.java @@ -22,7 +22,7 @@ public PersonController(PersonService personService) { } @GetMapping("{id}") - Person person(@PathVariable Long id) { + Person person(@PathVariable("id") Long id) { Sentry.logger().warn("warn Sentry logging"); Sentry.logger().error("error Sentry logging"); Sentry.logger().info("hello %s %s", "there", "world!"); diff --git a/sentry-samples/sentry-samples-spring-jakarta/src/test/kotlin/io/sentry/DummyTest.kt b/sentry-samples/sentry-samples-spring-jakarta/src/test/kotlin/io/sentry/DummyTest.kt new file mode 100644 index 00000000000..6f762b7e453 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-jakarta/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-spring-jakarta/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt b/sentry-samples/sentry-samples-spring-jakarta/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt new file mode 100644 index 00000000000..80d21b1d934 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-jakarta/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt @@ -0,0 +1,46 @@ +package io.sentry.systemtest + +import io.sentry.systemtest.util.TestHelper +import kotlin.test.Test +import kotlin.test.assertEquals +import org.junit.Before + +class PersonSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080/sentry-samples-spring-jakarta-0.0.1-SNAPSHOT") + testHelper.reset() + } + + @Test + fun `get person fails`() { + val restClient = testHelper.restClient + restClient.getPerson(11L) + assertEquals(500, restClient.lastKnownStatusCode) + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionHaveOp(transaction, "http.server") + } + + Thread.sleep(10000) + + testHelper.ensureLogsReceived { logs, envelopeHeader -> + testHelper.doesContainLogWithBody(logs, "warn Sentry logging") && + testHelper.doesContainLogWithBody(logs, "error Sentry logging") && + testHelper.doesContainLogWithBody(logs, "hello there world!") + } + } + + @Test + fun `create person works`() { + val restClient = testHelper.restClient + val person = Person("firstA", "lastB") + val returnedPerson = restClient.createPerson(person) + assertEquals(200, restClient.lastKnownStatusCode) + + assertEquals(person.firstName, returnedPerson!!.firstName) + assertEquals(person.lastName, returnedPerson!!.lastName) + } +} diff --git a/test/system-test-runner.py b/test/system-test-runner.py index 7269f8d8955..f2aab974df3 100644 --- a/test/system-test-runner.py +++ b/test/system-test-runner.py @@ -16,6 +16,9 @@ # Start Sentry mock server python3 test/system-test-runner.py sentry start + # Start Spring app served by Tomcat + python3 test/system-test-runner.py tomcat start sentry-samples-spring + # Start Spring Boot app python3 test/system-test-runner.py spring start sentry-samples-spring-boot @@ -35,6 +38,7 @@ import time import signal import os +from enum import Enum import argparse import requests import threading @@ -48,6 +52,27 @@ except: pass +SENTRY_ENVIRONMENT_VARIABLES = { + "SENTRY_DSN": "http://502f25099c204a2fbf4cb16edc5975d1@localhost:8000/0", + "SENTRY_TRACES_SAMPLE_RATE": "1.0", + "OTEL_TRACES_EXPORTER": "none", + "OTEL_METRICS_EXPORTER": "none", + "OTEL_LOGS_EXPORTER": "none", + "SENTRY_LOGS_ENABLED": "true" +} + +class ServerType(Enum): + TOMCAT = 0 + SPRING = 1 + +def get_server_type_for_module(sample_module: str) -> Optional[ServerType]: + if "spring" in sample_module and "spring-boot" not in sample_module: + return ServerType.TOMCAT + elif "spring" in sample_module: + return ServerType.SPRING + + return None + def str_to_bool(value: str) -> str: """Convert true/false string to 1/0 string for internal compatibility.""" if value.lower() in ('true', '1'): @@ -57,6 +82,25 @@ def str_to_bool(value: str) -> str: else: raise ValueError(f"Invalid boolean value: {value}. Use 'true' or 'false'") +@dataclass +class Server: + name: str + pid_filepath: str + process: Optional[subprocess.Popen] = None + pid: Optional[int] = None + +def store_pid(server: Server): + # Store PID in instance variable and write to file + server.pid = server.process.pid + with open(server.pid_filepath, "w") as pid_file: + pid_file.write(str(server.pid)) + +def cleanup_pid(server: Server): + # Clean up PID file and instance variable + if os.path.exists(server.pid_filepath): + os.remove(server.pid_filepath) + server.pid = None + @dataclass class ModuleConfig: """Configuration for a test module.""" @@ -104,21 +148,15 @@ def has_agent_modules(self) -> bool: 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" + self.mock_server = Server(name="Mock", pid_filepath="sentry-mock-server.pid") + self.tomcat_server = Server(name="Tomcat", pid_filepath="tomcat-server.pid") + self.spring_server = Server(name="Spring", pid_filepath="spring-server.pid") # 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}") + for server in (self.mock_server, self.tomcat_server, self.spring_server): + server.pid = self.read_pid_file(server.pid_filepath) + if server.pid: + print(f"Found existing {server.name} server PID: {server.pid}") def read_pid_file(self, pid_file: str) -> Optional[int]: """Read PID from file if it exists.""" @@ -162,18 +200,15 @@ def start_sentry_mock_server(self) -> None: try: # Start the mock server in the background with open("sentry-mock-server.txt", "w") as log_file: - self.mock_server_process = subprocess.Popen( + 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}") + store_pid(self.mock_server) + print(f"Started mock server with PID {self.mock_server.pid}") # Wait a moment for the server to start time.sleep(2) @@ -193,21 +228,19 @@ def stop_sentry_mock_server(self) -> None: 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") + 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 + cleanup_pid(self.mock_server) def read_version_from_gradle_properties(self) -> Optional[str]: """Read the versionName from gradle.properties.""" @@ -280,21 +313,35 @@ def ensure_agent_jar(self, skip_build: bool = False) -> Optional[str]: return agent_jar + def start_tomcat_server(self, sample_module: str) -> None: + # Build environment variables + env = os.environ.copy() + env.update(SENTRY_ENVIRONMENT_VARIABLES) + + try: + # Start the Tomcat server + with open("tomcat-server.txt", "w") as log_file: + self.tomcat_server.process = subprocess.Popen( + ["./gradlew", f"sentry-samples:{sample_module}:run"], + env=env, + stdout=log_file, + stderr=subprocess.STDOUT + ) + + store_pid(self.tomcat_server) + print(f"Started Tomcat server with PID {self.tomcat_server.pid}") + except Exception as e: + print(f"Failed to start Tomcat server: {e}") + raise + def start_spring_server(self, sample_module: str, java_agent: str, java_agent_auto_init: str) -> None: """Start a Spring Boot server for testing.""" print(f"Starting Spring server for {sample_module}...") # Build environment variables env = os.environ.copy() - env.update({ - "SENTRY_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" - }) + env.update(SENTRY_ENVIRONMENT_VARIABLES) + env["SENTRY_AUTO_INIT"] = java_agent_auto_init # Build command jar_path = f"sentry-samples/{sample_module}/build/libs/{sample_module}-0.0.1-SNAPSHOT.jar" @@ -313,7 +360,7 @@ def start_spring_server(self, sample_module: str, java_agent: str, java_agent_au try: # Start the Spring server with open("spring-server.txt", "w") as log_file: - self.spring_server_process = subprocess.Popen( + self.spring_server.process = subprocess.Popen( cmd, env=env, stdout=log_file, @@ -321,16 +368,35 @@ def start_spring_server(self, sample_module: str, java_agent: str, java_agent_au ) # 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}") + store_pid(self.spring_server) + print(f"Started Spring server with PID {self.spring_server.pid}") except Exception as e: print(f"Failed to start Spring server: {e}") raise + def wait_for_tomcat(self, module_name: str, max_attempts: int = 20) -> bool: + """Wait for Tomcat to be ready.""" + print("Waiting for Tomcat to be ready...") + + for attempt in range(1, max_attempts + 1): + try: + response = requests.head( + f"http://localhost:8080/{module_name}-0.0.1-SNAPSHOT/person/1", + timeout=5 + ) + if response.status_code != 404: + print("Tomcat is ready!") + return True + except: + pass + + print(f"Waiting... (attempt {attempt}/{max_attempts})") + time.sleep(1) + + print("Tomcat failed to become ready") + return False + def wait_for_spring(self, max_attempts: int = 20) -> bool: """Wait for Spring Boot application to be ready.""" print("Waiting for Spring application to be ready...") @@ -358,11 +424,11 @@ def get_spring_status(self) -> dict: """Get status of Spring Boot application.""" status = { "process_running": False, - "pid": self.spring_server_pid, + "pid": self.spring_server.pid, "http_ready": False } - if self.spring_server_pid and self.is_process_running(self.spring_server_pid): + if self.spring_server.pid and self.is_process_running(self.spring_server.pid): status["process_running"] = True # Check HTTP endpoint @@ -383,11 +449,11 @@ def get_sentry_status(self) -> dict: """Get status of Sentry mock server.""" status = { "process_running": False, - "pid": self.mock_server_pid, + "pid": self.mock_server.pid, "http_ready": False } - if self.mock_server_pid and self.is_process_running(self.mock_server_pid): + if self.mock_server.pid and self.is_process_running(self.mock_server.pid): status["process_running"] = True # Check HTTP endpoint @@ -416,36 +482,60 @@ def print_status_summary(self) -> None: print(f" Process Running: {'✅' if spring_status['process_running'] else '❌'}") print(f" HTTP Ready: {'✅' if spring_status['http_ready'] else '❌'}") + def stop_tomcat_server(self) -> None: + """Stop the Tomcat server.""" + try: + # Kill the process - try process object first, then PID from file + if self.tomcat_server.process and self.tomcat_server.process.poll() is None: + print(f"Killing Tomcat server process object with PID {self.tomcat_server.process.pid}") + self.tomcat_server.process.kill() + try: + self.tomcat_server.process.wait(timeout=10) + except subprocess.TimeoutExpired: + print("Tomcat server did not terminate gracefully") + elif self.tomcat_server.pid and self.is_process_running(self.tomcat_server.pid): + print(f"Killing Tomcat server from PID file with PID {self.tomcat_server.pid}") + self.kill_process(self.tomcat_server.pid, "Tomcat server") + + except Exception as e: + print(f"Error stopping Tomcat server: {e}") + finally: + # Clean up PID file and instance variable + cleanup_pid(self.tomcat_server) + def stop_spring_server(self) -> None: """Stop the Spring Boot server.""" try: # Kill the process - try process object first, then PID from file - if self.spring_server_process and self.spring_server_process.poll() is None: - print(f"Killing Spring server process object with PID {self.spring_server_process.pid}") - self.spring_server_process.kill() + 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) + 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") + 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 + cleanup_pid(self.spring_server) - def get_build_task(self, sample_module: str) -> str: + def get_build_task(self, server_type: Optional[ServerType]) -> str: """Get the appropriate build task for a module.""" - return "bootJar" if "spring" in sample_module else "assemble" + if server_type == ServerType.TOMCAT: + return "war" + elif server_type == ServerType.SPRING: + return "bootJar" - def build_module(self, sample_module: str) -> int: + return "assemble" + + def build_module(self, sample_module: str, server_type: Optional[ServerType]) -> int: """Build a sample module using the appropriate task.""" - build_task = self.get_build_task(sample_module) + build_task = self.get_build_task(server_type) print(f"Building {sample_module} using {build_task} task") return self.run_gradle_task(f":sentry-samples:{sample_module}:{build_task}") @@ -460,12 +550,13 @@ def run_gradle_task(self, task: str) -> int: return 1 def setup_test_infrastructure(self, sample_module: str, java_agent: str, - java_agent_auto_init: str, build_before_run: str) -> int: + java_agent_auto_init: str, build_before_run: str, + server_type: Optional[ServerType]) -> int: """Set up test infrastructure. Returns 0 on success, error code on failure.""" # Build if requested if build_before_run == "1": print("Building before test run") - build_result = self.build_module(sample_module) + build_result = self.build_module(sample_module, server_type) if build_result != 0: print("Build failed") return build_result @@ -481,8 +572,16 @@ def setup_test_infrastructure(self, sample_module: str, java_agent: str, print("Starting Sentry mock server...") self.start_sentry_mock_server() - # Start Spring server if it's a Spring module - if "spring" in sample_module: + # Start Tomcat server given a Spring module + if server_type == ServerType.TOMCAT: + print(f"Starting Tomcat server for {sample_module}...") + self.start_tomcat_server(sample_module) + if not self.wait_for_tomcat(sample_module): + print("Tomcat application failed to start!") + return 1 + print("Tomcat application is ready!") + # Start Spring server if it's a Spring boot module + elif server_type == ServerType.SPRING: print(f"Starting Spring server for {sample_module}...") self.start_spring_server(sample_module, java_agent, java_agent_auto_init) if not self.wait_for_spring(): @@ -497,9 +596,15 @@ def run_single_test(self, sample_module: str, java_agent: str, """Run a single system test.""" print(f"Running system test for {sample_module}") + server_type = get_server_type_for_module(sample_module) + try: # Set up infrastructure - setup_result = self.setup_test_infrastructure(sample_module, java_agent, java_agent_auto_init, build_before_run) + setup_result = self.setup_test_infrastructure(sample_module, + java_agent, + java_agent_auto_init, + build_before_run, + server_type) if setup_result != 0: return setup_result @@ -510,7 +615,9 @@ def run_single_test(self, sample_module: str, java_agent: str, finally: # Cleanup - if "spring" in sample_module: + if server_type == ServerType.TOMCAT: + self.stop_tomcat_server() + elif server_type == ServerType.SPRING: self.stop_spring_server() self.stop_sentry_mock_server() @@ -562,9 +669,10 @@ def run_manual_test_mode(self, sample_module: str, java_agent: str, """Set up infrastructure for manual testing from IDE.""" print(f"Setting up manual test environment for {sample_module}") + server_type = get_server_type_for_module(sample_module) try: # Set up infrastructure - setup_result = self.setup_test_infrastructure(sample_module, java_agent, java_agent_auto_init, build_before_run) + setup_result = self.setup_test_infrastructure(sample_module, java_agent, java_agent_auto_init, build_before_run, server_type) if setup_result != 0: return setup_result @@ -581,7 +689,9 @@ def run_manual_test_mode(self, sample_module: str, java_agent: str, print(f" - Agent Auto-init: {java_agent_auto_init}") print(f" - Mock DSN: http://502f25099c204a2fbf4cb16edc5975d1@localhost:8000/0") - if "spring" in sample_module: + if server_type == ServerType.TOMCAT: + print(f"\nTomcat app is running on: http://localhost:8080/{sample_module}-0.0.1-SNAPSHOT") + elif server_type == ServerType.SPRING: print("\nSpring Boot app is running on: http://localhost:8080") print("\nPress Enter to stop the infrastructure and exit...") @@ -602,6 +712,7 @@ def run_manual_test_mode(self, sample_module: str, java_agent: str, def get_available_modules(self) -> List[ModuleConfig]: """Get list of all available test modules.""" return [ + ModuleConfig("sentry-samples-spring-jakarta", "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"), @@ -829,6 +940,7 @@ def cleanup_on_exit(self, signum, frame): print(f"\nReceived signal {signum}, cleaning up...") self.stop_spring_server() self.stop_sentry_mock_server() + self.stop_tomcat_server() sys.exit(1) def main(): @@ -846,6 +958,16 @@ def main(): test_parser.add_argument("--build", default="false", help="Build before running (true or false)") test_parser.add_argument("--manual-test", action="store_true", help="Set up infrastructure but pause for manual testing from IDE") + # Tomcat subcommand + tomcat_parser = subparsers.add_parser("tomcat", help="Manage Servlet applications deployed on Tomcat") + tomcat_subparsers = tomcat_parser.add_subparsers(dest="tomcat_action", help="Tomcat actions") + + tomcat_start_parser = tomcat_subparsers.add_parser("start", help="Start tomcat application") + tomcat_start_parser.add_argument("module", help="Sample module to start") + tomcat_start_parser.add_argument("--build", default="false", help="Build before starting (true or false)") + + tomcat_subparsers.add_parser("stop", help="Stop Spring Boot application") + # Spring subcommand spring_parser = subparsers.add_parser("spring", help="Manage Spring Boot applications") spring_subparsers = spring_parser.add_subparsers(dest="spring_action", help="Spring actions") @@ -908,6 +1030,32 @@ def main(): elif args.interactive: return runner.run_interactive_tests(agent, auto_init, build) + elif args.command == "tomcat": + if args.tomcat_action == "start": + # Convert true/false arguments to internal format + build = str_to_bool(args.build) + + # Build if requested + if build == "1": + print("Building before starting Tomcat application") + build_result = runner.build_module(args.module, ServerType.TOMCAT) + if build_result != 0: + print("Build failed") + return build_result + + runner.start_tomcat_server(args.module) + + if runner.wait_for_tomcat(args.module): + print("Tomcat application started successfully!") + return 0 + else: + print("Tomcat application failed to start!") + return 1 + elif args.tomcat_action == "stop": + runner.stop_tomcat_server() + print("Tomcat application stopped.") + return 0 + elif args.command == "spring": if args.spring_action == "start": # Convert true/false arguments to internal format @@ -918,7 +1066,7 @@ def main(): # Build if requested if build == "1": print("Building before starting Spring application") - build_result = runner.build_module(args.module) + build_result = runner.build_module(args.module, ServerType.SPRING) if build_result != 0: print("Build failed") return build_result @@ -990,6 +1138,7 @@ def main(): if args.command == "test": runner.stop_spring_server() runner.stop_sentry_mock_server() + runner.stop_tomcat_server() if __name__ == "__main__": sys.exit(main())