Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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);
Expand All @@ -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.
*
* <p>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.
*
* <p>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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down