diff --git a/PRODUCT.md b/PRODUCT.md new file mode 100644 index 0000000..6c693d6 --- /dev/null +++ b/PRODUCT.md @@ -0,0 +1,18 @@ +# Ideas + +## Structure + +Content + +Health Check + +Optimization Potential + +Target Run Time + +## Visualize + +Display the top three slowest context +Identify which context are full and which are sliced ones (feedback from Wim) +build up current test time +Better reuse stats due to context refresh issues diff --git a/README.md b/README.md index 33a378e..dc618eb 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@ -# Profile Your Tests. Speed Up Your Tests. Ship Faster 🚤 +# Profile Your Tests. Speed Up Your Build. Ship Faster 🚤

Spring Test Profiler Logo

-A Spring Test utility that provides visualization and insights for Spring Test execution, with a focus on Spring context caching statistics. +The Spring Test Profiler is a Spring Test utility that provides visualization and insights for Spring Test execution, with a focus on Spring context caching. + +It helps you identify optimization opportunities in your Spring Test suite to speed up your builds and ship to production faster and with more confidence. Find [more information](https://pragmatech.digital/products/spring-test-profiler/) about the profiler on our website. diff --git a/demo/spring-boot-3.5-maven/src/main/java/digital/pragmatech/demo/DemoApplication.java b/demo/spring-boot-3.5-maven/src/main/java/digital/pragmatech/demo/DemoApplication.java index b04e05f..f37a78d 100644 --- a/demo/spring-boot-3.5-maven/src/main/java/digital/pragmatech/demo/DemoApplication.java +++ b/demo/spring-boot-3.5-maven/src/main/java/digital/pragmatech/demo/DemoApplication.java @@ -7,7 +7,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ApplicationContext; +import org.springframework.scheduling.annotation.EnableScheduling; +@EnableScheduling @SpringBootApplication public class DemoApplication implements CommandLineRunner { diff --git a/demo/spring-boot-3.5-maven/src/main/java/digital/pragmatech/demo/service/ScheduledLogger.java b/demo/spring-boot-3.5-maven/src/main/java/digital/pragmatech/demo/service/ScheduledLogger.java new file mode 100644 index 0000000..42b3329 --- /dev/null +++ b/demo/spring-boot-3.5-maven/src/main/java/digital/pragmatech/demo/service/ScheduledLogger.java @@ -0,0 +1,24 @@ +package digital.pragmatech.demo.service; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +public class ScheduledLogger { + + private static final Logger LOG = LoggerFactory.getLogger(ScheduledLogger.class); + + private final ApplicationContext applicationContext; + + public ScheduledLogger(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + // @Scheduled(fixedDelay = 100L) + public void log() { + LOG.info("Scheduled task executed within context '{}'.", applicationContext.hashCode()); + } +} diff --git a/demo/spring-boot-3.5-maven/src/main/java/digital/pragmatech/demo/service/SlowDown.java b/demo/spring-boot-3.5-maven/src/main/java/digital/pragmatech/demo/service/SlowDown.java new file mode 100644 index 0000000..304cd89 --- /dev/null +++ b/demo/spring-boot-3.5-maven/src/main/java/digital/pragmatech/demo/service/SlowDown.java @@ -0,0 +1,21 @@ +package digital.pragmatech.demo.service; + +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.stereotype.Component; + +@Component +public class SlowDown implements CommandLineRunner { + + @Override + public void run(String... args) throws Exception { + try { + Thread.sleep(5000); + System.out.println("ApplicationContext initialized after delay."); + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/demo/spring-boot-3.5-maven/src/test/java/digital/pragmatech/demo/MigrationIT.java b/demo/spring-boot-3.5-maven/src/test/java/digital/pragmatech/demo/MigrationIT.java new file mode 100644 index 0000000..7acfb8a --- /dev/null +++ b/demo/spring-boot-3.5-maven/src/test/java/digital/pragmatech/demo/MigrationIT.java @@ -0,0 +1,26 @@ +package digital.pragmatech.demo; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.properties.PropertyMapping; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.test.context.TestPropertySources; + +import static org.assertj.core.api.Assertions.assertThat; + +// Spring Boot < 4.0 +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = "management.server.port=") +public class MigrationIT { + + @Autowired + private TestRestTemplate restTemplate; + + @Test + void shouldReturnSuccessfulHealthCheckRestTemplate() { + var response = restTemplate + .getForEntity("/actuator/health", String.class); + + assertThat(response.getStatusCode().value()).isEqualTo(200); + } +} diff --git a/demo/spring-boot-3.5-maven/src/test/java/digital/pragmatech/demo/pausing/BasicIT.java b/demo/spring-boot-3.5-maven/src/test/java/digital/pragmatech/demo/pausing/BasicIT.java new file mode 100644 index 0000000..6ad2f81 --- /dev/null +++ b/demo/spring-boot-3.5-maven/src/test/java/digital/pragmatech/demo/pausing/BasicIT.java @@ -0,0 +1,25 @@ +package digital.pragmatech.demo.pausing; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = "management.server.port=") +public class BasicIT { + + @Autowired + private TestRestTemplate restTemplate; + + @Test + void shouldReturnSuccessfulHealthCheckRestTemplate() throws Exception { + var response = restTemplate + .getForEntity("/actuator/health", String.class); + + assertThat(response.getStatusCode().value()).isEqualTo(200); + + Thread.sleep(5_000); // Pausing to allow scheduled tasks to run + } +} diff --git a/demo/spring-boot-3.5-maven/src/test/java/digital/pragmatech/demo/pausing/OtherIT.java b/demo/spring-boot-3.5-maven/src/test/java/digital/pragmatech/demo/pausing/OtherIT.java new file mode 100644 index 0000000..c6f8e18 --- /dev/null +++ b/demo/spring-boot-3.5-maven/src/test/java/digital/pragmatech/demo/pausing/OtherIT.java @@ -0,0 +1,25 @@ +package digital.pragmatech.demo.pausing; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = "management.server.port=") +public class OtherIT { + + @Autowired + private TestRestTemplate restTemplate; + + @Test + void shouldReturnSuccessfulHealthCheckRestTemplate() throws Exception { + var response = restTemplate + .getForEntity("/actuator/health", String.class); + + assertThat(response.getStatusCode().value()).isEqualTo(200); + + Thread.sleep(5_000); // Pausing to allow scheduled tasks to run + } +} diff --git a/demo/spring-boot-4.0-maven/pom.xml b/demo/spring-boot-4.0-maven/pom.xml index 1cbea34..c7efcb2 100644 --- a/demo/spring-boot-4.0-maven/pom.xml +++ b/demo/spring-boot-4.0-maven/pom.xml @@ -8,7 +8,7 @@ org.springframework.boot spring-boot-starter-parent - 4.0.0-M2 + 4.0.0-RC2 @@ -61,13 +61,31 @@ runtime - + org.springframework.boot spring-boot-starter-test test + + org.springframework.boot + spring-boot-starter-webmvc-test + test + + + + org.springframework.boot + spring-boot-starter-webclient-test + test + + + + org.springframework.boot + spring-boot-starter-restclient-test + test + + digital.pragmatech.testing diff --git a/demo/spring-boot-4.0-maven/src/main/java/digital/pragmatech/demo/DemoApplication.java b/demo/spring-boot-4.0-maven/src/main/java/digital/pragmatech/demo/DemoApplication.java index b04e05f..f37a78d 100644 --- a/demo/spring-boot-4.0-maven/src/main/java/digital/pragmatech/demo/DemoApplication.java +++ b/demo/spring-boot-4.0-maven/src/main/java/digital/pragmatech/demo/DemoApplication.java @@ -7,7 +7,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ApplicationContext; +import org.springframework.scheduling.annotation.EnableScheduling; +@EnableScheduling @SpringBootApplication public class DemoApplication implements CommandLineRunner { diff --git a/demo/spring-boot-4.0-maven/src/main/java/digital/pragmatech/demo/service/OtherService.java b/demo/spring-boot-4.0-maven/src/main/java/digital/pragmatech/demo/service/OtherService.java new file mode 100644 index 0000000..1f3ad2c --- /dev/null +++ b/demo/spring-boot-4.0-maven/src/main/java/digital/pragmatech/demo/service/OtherService.java @@ -0,0 +1,13 @@ +package digital.pragmatech.demo.service; + +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +@Component +@Scope("prototype") // can now be replaced in tests with @MockitoBean/@SpyBean/@TestBean +public class OtherService { + + public String doWork() { + return "Other Service is working!"; + } +} diff --git a/demo/spring-boot-4.0-maven/src/main/java/digital/pragmatech/demo/service/ScheduledLogger.java b/demo/spring-boot-4.0-maven/src/main/java/digital/pragmatech/demo/service/ScheduledLogger.java new file mode 100644 index 0000000..e669417 --- /dev/null +++ b/demo/spring-boot-4.0-maven/src/main/java/digital/pragmatech/demo/service/ScheduledLogger.java @@ -0,0 +1,32 @@ +package digital.pragmatech.demo.service; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.event.ContextPausedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +public class ScheduledLogger { + + private static final Logger LOG = LoggerFactory.getLogger(ScheduledLogger.class); + + private final ApplicationContext applicationContext; + + public ScheduledLogger(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + @Scheduled(fixedDelay = 100L) + public void log() { + LOG.info("Scheduled task executed within context '{}'.", applicationContext.hashCode()); + } + + // New event in Spring Framework 7.0 + @EventListener(ContextPausedEvent.class) + public void contextPaused(ContextPausedEvent event) { + LOG.info("Application context '{}' has been paused.", event.getApplicationContext().hashCode()); + } +} diff --git a/demo/spring-boot-4.0-maven/src/test/java/digital/pragmatech/demo/BadOneIT.java b/demo/spring-boot-4.0-maven/src/test/java/digital/pragmatech/demo/BadOneIT.java index 84deb02..37e3aaa 100644 --- a/demo/spring-boot-4.0-maven/src/test/java/digital/pragmatech/demo/BadOneIT.java +++ b/demo/spring-boot-4.0-maven/src/test/java/digital/pragmatech/demo/BadOneIT.java @@ -5,12 +5,14 @@ import digital.pragmatech.demo.entity.Book; import digital.pragmatech.demo.entity.BookCategory; import digital.pragmatech.demo.repository.BookRepository; +import digital.pragmatech.demo.service.OtherService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -30,6 +32,9 @@ @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) // BAD: Forces context reload class BadOneIT { + @MockitoBean + private OtherService otherService; + private final BookRepository bookRepository; public BadOneIT(@Autowired BookRepository bookRepository) { diff --git a/demo/spring-boot-4.0-maven/src/test/java/digital/pragmatech/demo/BadThreeIT.java b/demo/spring-boot-4.0-maven/src/test/java/digital/pragmatech/demo/BadThreeIT.java index 6da4c6b..e00fda4 100644 --- a/demo/spring-boot-4.0-maven/src/test/java/digital/pragmatech/demo/BadThreeIT.java +++ b/demo/spring-boot-4.0-maven/src/test/java/digital/pragmatech/demo/BadThreeIT.java @@ -6,9 +6,10 @@ import digital.pragmatech.demo.entity.BookCategory; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureWebMvc; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.web.server.test.client.TestRestTemplate; +import org.springframework.boot.resttestclient.TestRestTemplate; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.context.ActiveProfiles; @@ -24,8 +25,8 @@ * Also uses webEnvironment.RANDOM_PORT but different from other tests. */ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@AutoConfigureWebMvc // BAD: Unnecessary annotation that changes context @ActiveProfiles("test") // Same as first test, but other config differs +@AutoConfigureTestRestTemplate @TestPropertySource(properties = { "spring.datasource.url=jdbc:h2:mem:badtest3;DB_CLOSE_DELAY=-1", // Different DB name again "spring.jpa.hibernate.ddl-auto=create-drop", diff --git a/demo/spring-boot-4.0-maven/src/test/java/digital/pragmatech/demo/MigrationIT.java b/demo/spring-boot-4.0-maven/src/test/java/digital/pragmatech/demo/MigrationIT.java new file mode 100644 index 0000000..d698b79 --- /dev/null +++ b/demo/spring-boot-4.0-maven/src/test/java/digital/pragmatech/demo/MigrationIT.java @@ -0,0 +1,46 @@ +package digital.pragmatech.demo; + +import digital.pragmatech.demo.service.OtherService; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.resttestclient.TestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.client.RestTestClient; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@AutoConfigureTestRestTemplate +@AutoConfigureRestTestClient // consider as an alternative to the TestRestTemplate +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class MigrationIT { + + @Autowired + private TestRestTemplate restTemplate; + + @Autowired + private RestTestClient restClient; + + @MockitoBean + private OtherService otherService; + + @Test + void shouldReturnSuccessfulHealthCheckRestTemplate() { + var response = restTemplate + .getForEntity("/actuator/health", String.class); + + assertThat(response.getStatusCode().value()).isEqualTo(200); + } + + @Test + void shouldReturnSuccessfulHealthCheckRestClient() { + restClient + .get() + .uri("/actuator/health") + .exchange() + .expectStatus() + .is2xxSuccessful(); + } +} diff --git a/demo/spring-boot-4.0-maven/src/test/java/digital/pragmatech/demo/pausing/HealthCheckIT.java b/demo/spring-boot-4.0-maven/src/test/java/digital/pragmatech/demo/pausing/HealthCheckIT.java new file mode 100644 index 0000000..c5506d4 --- /dev/null +++ b/demo/spring-boot-4.0-maven/src/test/java/digital/pragmatech/demo/pausing/HealthCheckIT.java @@ -0,0 +1,29 @@ +package digital.pragmatech.demo.pausing; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.test.web.servlet.client.RestTestClient; + +@AutoConfigureRestTestClient +@AutoConfigureMockMvc +@SpringBootTest +public class HealthCheckIT { + + @Autowired + private RestTestClient restClient; + + @Test + void shouldReturnSuccessfulHealthCheckRestClient() throws Exception { + restClient + .get() + .uri("/actuator/health") + .exchange() + .expectStatus() + .is2xxSuccessful(); + + Thread.sleep(5_000); // Pausing to allow scheduled tasks to run + } +} diff --git a/demo/spring-boot-4.0-maven/src/test/java/digital/pragmatech/demo/pausing/HealthCheckSecondIT.java b/demo/spring-boot-4.0-maven/src/test/java/digital/pragmatech/demo/pausing/HealthCheckSecondIT.java new file mode 100644 index 0000000..5f602e3 --- /dev/null +++ b/demo/spring-boot-4.0-maven/src/test/java/digital/pragmatech/demo/pausing/HealthCheckSecondIT.java @@ -0,0 +1,27 @@ +package digital.pragmatech.demo.pausing; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.resttestclient.TestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@AutoConfigureTestRestTemplate +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class HealthCheckSecondIT { + + @Autowired + private TestRestTemplate restTemplate; + + @Test + void shouldReturnSuccessfulHealthCheckRestTemplate() throws Exception { + var response = restTemplate + .getForEntity("/actuator/health", String.class); + + assertThat(response.getStatusCode().value()).isEqualTo(200); + + Thread.sleep(5_000); // Pausing to allow scheduled tasks to run + } +} diff --git a/pom.xml b/pom.xml index 46d79c6..6406842 100644 --- a/pom.xml +++ b/pom.xml @@ -88,6 +88,12 @@ ${spring.version} provided + + org.springframework.boot + spring-boot + 3.5.6 + provided + org.junit.jupiter @@ -171,6 +177,18 @@ + + org.apache.maven.plugins + maven-jar-plugin + ${maven-jar-plugin.version} + + + + true + + + + org.apache.maven.plugins maven-surefire-plugin diff --git a/src/main/java/digital/pragmatech/testing/diagnostic/ContextDiagnosticApplicationInitializer.java b/src/main/java/digital/pragmatech/testing/diagnostic/ContextDiagnosticApplicationInitializer.java index f396bfd..e1c69a1 100644 --- a/src/main/java/digital/pragmatech/testing/diagnostic/ContextDiagnosticApplicationInitializer.java +++ b/src/main/java/digital/pragmatech/testing/diagnostic/ContextDiagnosticApplicationInitializer.java @@ -2,9 +2,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; @@ -21,14 +21,17 @@ public void initialize(ConfigurableApplicationContext applicationContext) { applicationContext.addApplicationListener( event -> { - if (event instanceof ContextRefreshedEvent contextEvent) { - if (contextEvent.getApplicationContext().getParent() == null + if (event instanceof ApplicationReadyEvent readyEvent) { + if (readyEvent.getApplicationContext().getParent() == null && !applicationContext.getBeanFactory().containsSingleton("contextDiagnostic")) { ContextDiagnostic completedDiagnostic = contextDiagnostic.completed(); applicationContext .getBeanFactory() .registerSingleton("contextDiagnostic", completedDiagnostic); - LOG.debug("Context Diagnostic Completed: {}", completedDiagnostic); + LOG.debug( + "Context Diagnostic Completed: {} in {}", + completedDiagnostic, + completedDiagnostic.getContextLoadDuration() / 1000); } } }); diff --git a/src/main/java/digital/pragmatech/testing/reporting/html/TestExecutionReporter.java b/src/main/java/digital/pragmatech/testing/reporting/html/TestExecutionReporter.java index e8a9ea0..ed44ae3 100644 --- a/src/main/java/digital/pragmatech/testing/reporting/html/TestExecutionReporter.java +++ b/src/main/java/digital/pragmatech/testing/reporting/html/TestExecutionReporter.java @@ -7,7 +7,6 @@ import java.time.LocalDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; -import java.util.Map; import digital.pragmatech.testing.ContextCacheTracker; import digital.pragmatech.testing.OptimizationStatistics; @@ -166,24 +165,6 @@ private String generateHtmlWithThymeleaf( + extensionVersion; context.setVariable("utmParameters", utmParameters); - // Pre-compute test status counts to avoid complex template expressions - Map classMetrics = - executionTracker.getClassMetrics(); - long passedTests = TemplateHelpers.countTestsByStatus(classMetrics, "PASSED"); - long failedTests = TemplateHelpers.countTestsByStatus(classMetrics, "FAILED"); - long disabledTests = TemplateHelpers.countTestsByStatus(classMetrics, "DISABLED"); - long abortedTests = TemplateHelpers.countTestsByStatus(classMetrics, "ABORTED"); - - context.setVariable("passedTests", passedTests); - context.setVariable("failedTests", failedTests); - context.setVariable("disabledTests", disabledTests); - context.setVariable("abortedTests", abortedTests); - - // Pre-compute success rate - int totalTestMethods = executionTracker.getTotalTestMethods(); - double successRate = totalTestMethods > 0 ? (passedTests * 100.0) / totalTestMethods : 0.0; - context.setVariable("successRate", successRate); - // Extract available processors from any context entry (they're all the same) Integer availableProcessors = null; if (contextCacheTracker != null) { diff --git a/src/main/java/digital/pragmatech/testing/util/VersionInfo.java b/src/main/java/digital/pragmatech/testing/util/VersionInfo.java index 125bbd3..c941fc7 100644 --- a/src/main/java/digital/pragmatech/testing/util/VersionInfo.java +++ b/src/main/java/digital/pragmatech/testing/util/VersionInfo.java @@ -10,18 +10,59 @@ public class VersionInfo { private static final Logger LOG = LoggerFactory.getLogger(VersionInfo.class); - private static final Properties props = new Properties(); + private static final String version; static { - try (InputStream is = VersionInfo.class.getResourceAsStream("/version.properties")) { - props.load(is); + version = loadVersion(); + } + + private static String loadVersion() { + String manifestVersion = loadVersionFromManifest(); + if (manifestVersion != null && !manifestVersion.isEmpty()) { + LOG.debug("Loaded version from MANIFEST.MF: {}", manifestVersion); + return manifestVersion; + } + + String propertiesVersion = loadVersionFromProperties(); + if (propertiesVersion != null && !propertiesVersion.isEmpty()) { + LOG.debug("Loaded version from properties file: {}", propertiesVersion); + return propertiesVersion; + } + + LOG.warn("Failed to load version information from MANIFEST.MF or properties file"); + return "unknown"; + } + + private static String loadVersionFromManifest() { + try { + Package pkg = VersionInfo.class.getPackage(); + if (pkg != null) { + String implementationVersion = pkg.getImplementationVersion(); + if (implementationVersion != null && !implementationVersion.isEmpty()) { + return implementationVersion; + } + } + } catch (Exception e) { + LOG.debug("Failed to load version from MANIFEST.MF", e); + } + return null; + } + + private static String loadVersionFromProperties() { + Properties props = new Properties(); + try (InputStream is = + VersionInfo.class.getResourceAsStream("/spring-test-profiler-version.properties")) { + if (is != null) { + props.load(is); + return props.getProperty("version"); + } } catch (IOException e) { - LOG.warn("Failed to load version.properties file", e); - props.setProperty("version", "unknown"); + LOG.debug("Failed to load version from properties file", e); } + return null; } public static String getVersion() { - return props.getProperty("version"); + return version; } } diff --git a/src/main/resources/version.properties b/src/main/resources/spring-test-profiler-version.properties similarity index 100% rename from src/main/resources/version.properties rename to src/main/resources/spring-test-profiler-version.properties diff --git a/src/main/resources/templates/fragments/summary.html b/src/main/resources/templates/fragments/summary.html index 4088cfd..fe79d6b 100644 --- a/src/main/resources/templates/fragments/summary.html +++ b/src/main/resources/templates/fragments/summary.html @@ -2,9 +2,12 @@
+ totalDurationMs=${executionTracker.getOverallDuration().toMillis()}, + cacheSize=${cacheStats != null ? cacheStats.size() : 0}, + cacheHits=${cacheStats != null ? cacheStats.hitCount() : 0}, + cacheMisses=${cacheStats != null ? cacheStats.missCount() : 0}, + totalCacheAccess=${cacheStats != null ? (cacheStats.hitCount() + cacheStats.missCount()) : 0}, + cacheHitRate=${totalCacheAccess > 0 ? (cacheHits * 100.0 / totalCacheAccess) : 0.0}">

Test Execution Summary

@@ -20,37 +23,25 @@

Test Execution Summary

-

Test Classes

-
0
+

Cache Size

+
0
-

Total Tests

-
0
+

Cache Hits

+
0
-
-

Passed

-
0
-
-
-

Failed

-
0
-
-
-

Disabled

-
0
+
+

Cache Misses

+
0
-
-

Aborted

-
0
+
+

Cache Hit Rate

+
0%

Total Runtime

0ms
-
-

Success Rate

-
0%
-

Available Processors

0
diff --git a/src/main/resources/templates/report.html b/src/main/resources/templates/report.html index 655098e..c2bacbc 100644 --- a/src/main/resources/templates/report.html +++ b/src/main/resources/templates/report.html @@ -41,9 +41,6 @@

- -
-