diff --git a/src/main/java/com/pdsl/junit/jupiter/extension/extension/LoggingConstants.java b/src/main/java/com/pdsl/junit/jupiter/extension/extension/LoggingConstants.java index 6d0a00a..02e7af4 100644 --- a/src/main/java/com/pdsl/junit/jupiter/extension/extension/LoggingConstants.java +++ b/src/main/java/com/pdsl/junit/jupiter/extension/extension/LoggingConstants.java @@ -2,6 +2,8 @@ public final class LoggingConstants { public static final String TEST_ID_KEY = "testId"; + public static final String TEST_ID_DELIMITER = "_"; + public static final int MAX_TEST_ID_LENGTH = 120; private LoggingConstants() { } diff --git a/src/main/java/com/pdsl/junit/jupiter/extension/extension/MdcLoggingExtension.java b/src/main/java/com/pdsl/junit/jupiter/extension/extension/MdcLoggingExtension.java index 011f3ad..df1fec1 100644 --- a/src/main/java/com/pdsl/junit/jupiter/extension/extension/MdcLoggingExtension.java +++ b/src/main/java/com/pdsl/junit/jupiter/extension/extension/MdcLoggingExtension.java @@ -1,5 +1,6 @@ package com.pdsl.junit.jupiter.extension.extension; +import org.checkerframework.checker.nullness.qual.NonNull; import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; @@ -26,17 +27,8 @@ public class MdcLoggingExtension implements BeforeEachCallback, AfterEachCallbac */ @Override public void beforeEach(ExtensionContext context) { - String testClassName = context.getRequiredTestClass().getSimpleName(); - String testMethodName = context.getRequiredTestMethod().getName(); - // For @TestTemplate or parameterized tests, getDisplayName() returns the unique invocation name - String displayName = context.getDisplayName(); - - // Sanitize the display name to construct a safe filename - String sanitizedDisplayName = displayName.replaceAll("[^a-zA-Z0-9_\\-]", "_") - .replaceAll("_+", "_") - .trim(); - // Construct a unique Test ID - String testId = String.format("%s_%s_%s", testClassName, testMethodName, sanitizedDisplayName); + String testId = buildUniqTestIdValue(context); + testId = truncateTestId(testId); // Put the unique identifier into MDC MDC.put(LoggingConstants.TEST_ID_KEY, testId); @@ -55,4 +47,44 @@ public void afterEach(ExtensionContext context) throws Exception { // Remove the key to prevent context leakage between threads or test executions MDC.remove(LoggingConstants.TEST_ID_KEY); } + + /** + * Constructs a unique, sanitized test identifier string from the extension context. + * + *
This method combines the test class simple name, the test method name, and a + * sanitized version of the display name (removing any characters not safe for filenames). + * + * @param context the extension context of the current test + * @return the constructed unique test ID string + */ + private static @NonNull String buildUniqTestIdValue(ExtensionContext context) { + String testClassName = context.getRequiredTestClass().getSimpleName(); + String testMethodName = context.getRequiredTestMethod().getName(); + // For @TestTemplate or parameterized tests, getDisplayName() returns the unique invocation name + String displayName = context.getDisplayName(); + + String sanitizedDisplayName = displayName.replaceAll("[^a-zA-Z0-9_\\-]", LoggingConstants.TEST_ID_DELIMITER) + .replaceAll(LoggingConstants.TEST_ID_DELIMITER + "+", LoggingConstants.TEST_ID_DELIMITER) + .trim(); + return String.join(LoggingConstants.TEST_ID_DELIMITER, testClassName, testMethodName, sanitizedDisplayName); + } + + /** + * Limits the length of the constructed test ID to prevent filesystem issues. + * + *
If the test ID is longer than the limit defined in {@link LoggingConstants#MAX_TEST_ID_LENGTH}, + * it is truncated. A hexadecimal hash suffix generated from the original test ID is appended + * to guarantee uniqueness and prevent collisions. + * + * @param testId the full constructed test ID to truncate + * @return the truncated test ID (guaranteed to be under {@link LoggingConstants#MAX_TEST_ID_LENGTH} characters) + */ + private static @NonNull String truncateTestId(String testId) { + if (testId.length() > LoggingConstants.MAX_TEST_ID_LENGTH) { + String hash = Integer.toHexString(testId.hashCode()); + int truncateIndex = LoggingConstants.MAX_TEST_ID_LENGTH - hash.length() - LoggingConstants.TEST_ID_DELIMITER.length(); + return testId.substring(0, truncateIndex) + LoggingConstants.TEST_ID_DELIMITER + hash; + } + return testId; + } } diff --git a/src/test/java/com/pdsl/junit/jupiter/extension/MdcLoggingExtensionTest.java b/src/test/java/com/pdsl/junit/jupiter/extension/MdcLoggingExtensionTest.java index 5d0a6b8..1202d2c 100644 --- a/src/test/java/com/pdsl/junit/jupiter/extension/MdcLoggingExtensionTest.java +++ b/src/test/java/com/pdsl/junit/jupiter/extension/MdcLoggingExtensionTest.java @@ -82,6 +82,36 @@ public void beforeEach_withSpecialCharactersDisplayName_sanitizesAndPutsFormatte assertThat(MDC.get(LoggingConstants.TEST_ID_KEY)).isEqualTo(expectedTestId); } + @Test + public void beforeEach_withTooLongTestId_truncatesAndAppendsHash() throws NoSuchMethodException { + // Arrange + Class> testClass = DummyTestClass.class; + Method testMethod = DummyTestClass.class.getDeclaredMethod("dummyMethod"); + // Create an exceptionally long display name (e.g. 200 characters) + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 20; i++) { + sb.append("veryLongDisplayNameSegment"); + } + String displayName = sb.toString(); + + when(mockContext.getRequiredTestClass()).thenAnswer(invocation -> testClass); + when(mockContext.getRequiredTestMethod()).thenReturn(testMethod); + when(mockContext.getDisplayName()).thenReturn(displayName); + + // Act + extension.beforeEach(mockContext); + + // Assert + String actualTestId = MDC.get(LoggingConstants.TEST_ID_KEY); + assertThat(actualTestId).isNotNull(); + assertThat(actualTestId.length()).isEqualTo(LoggingConstants.MAX_TEST_ID_LENGTH); + + // Calculate expected hash for full testId before truncation + String fullTestId = String.join(LoggingConstants.TEST_ID_DELIMITER, testClass.getSimpleName(), testMethod.getName(), displayName); + String expectedHash = Integer.toHexString(fullTestId.hashCode()); + assertThat(actualTestId).endsWith(LoggingConstants.TEST_ID_DELIMITER + expectedHash); + } + @Test public void afterEach_clearsTestIdFromMdc() throws Exception { // Arrange