From a87878243e34eaf0366a278c8121aa5ae3479baa Mon Sep 17 00:00:00 2001 From: Margi <39212098+margi212@users.noreply.github.com> Date: Sat, 27 Jun 2026 10:55:06 +0530 Subject: [PATCH 1/3] test(docling-version-tests): add unit tests for client, domain, and service layers - Add RegistryClientFactoryTests: GHCR client resolution, unsupported/null/empty/whitespace/case-sensitive registry, instance reuse - Add GHCRClientLoggerTests: request/response logging toggle, token/header masking, null/empty/non-JSON body, redirect response - Add TagsTestRequestTests: builder construction, toBuilder(), field setters, null/empty edge cases, executor null validation - Add NamedThreadFactoryTests: named thread creation, counter increment, separate factory counters, runnable execution, concurrent creation - Add WorkParallelizerTests: parallel transform/run, empty/single/large lists, multi-type transformation, concurrent execution - Extend TagTestResultTests: TagsTestResults sorting, from() factory, toBuilder(), setResults()/clearResults(), failure detection Signed-off-by: Margi <39212098+margi212@users.noreply.github.com> --- .../client/RegistryClientFactoryTests.java | 82 ++++ .../client/ghcr/GHCRClientLoggerTests.java | 373 +++++++++++++++ .../tester/domain/TagTestResultTests.java | 443 ++++++++++++++++-- .../tester/domain/TagsTestRequestTests.java | 249 ++++++++++ .../service/NamedThreadFactoryTests.java | 106 +++++ .../tester/service/WorkParallelizerTests.java | 189 ++++++++ 6 files changed, 1406 insertions(+), 36 deletions(-) create mode 100644 docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/client/RegistryClientFactoryTests.java create mode 100644 docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/client/ghcr/GHCRClientLoggerTests.java create mode 100644 docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/domain/TagsTestRequestTests.java create mode 100644 docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/service/NamedThreadFactoryTests.java create mode 100644 docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/service/WorkParallelizerTests.java diff --git a/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/client/RegistryClientFactoryTests.java b/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/client/RegistryClientFactoryTests.java new file mode 100644 index 00000000..986cf9d8 --- /dev/null +++ b/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/client/RegistryClientFactoryTests.java @@ -0,0 +1,82 @@ +package ai.docling.client.tester.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import ai.docling.client.tester.client.ghcr.GHCRClient; + +class RegistryClientFactoryTests { + + private GHCRClient ghcrClient; + private RegistryClientFactory factory; + + @BeforeEach + void setUp() { + ghcrClient = mock(GHCRClient.class); + factory = new RegistryClientFactory(ghcrClient); + } + + @Test + void shouldReturnGHCRClientForGhcrRegistry() { + var client = factory.getRegistryClient("ghcr.io"); + + assertThat(client) + .isNotNull() + .isSameAs(ghcrClient); + } + + @Test + void shouldThrowExceptionForUnsupportedRegistry() { + assertThatThrownBy(() -> factory.getRegistryClient("docker.io")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported registry: docker.io"); + } + + @Test + void shouldThrowExceptionForNullRegistry() { + assertThatThrownBy(() -> factory.getRegistryClient(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported registry: null"); + } + + @Test + void shouldThrowExceptionForEmptyRegistry() { + assertThatThrownBy(() -> factory.getRegistryClient("")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported registry: "); + } + + @Test + void shouldThrowExceptionForUnknownRegistry() { + assertThatThrownBy(() -> factory.getRegistryClient("quay.io")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported registry: quay.io"); + } + + @Test + void shouldThrowExceptionForRegistryWithDifferentCase() { + // Registry names are case-sensitive + assertThatThrownBy(() -> factory.getRegistryClient("GHCR.IO")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported registry: GHCR.IO"); + } + + @Test + void shouldThrowExceptionForRegistryWithWhitespace() { + assertThatThrownBy(() -> factory.getRegistryClient(" ghcr.io ")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported registry: ghcr.io "); + } + + @Test + void shouldReturnSameClientInstanceOnMultipleCalls() { + var client1 = factory.getRegistryClient("ghcr.io"); + var client2 = factory.getRegistryClient("ghcr.io"); + + assertThat(client1).isSameAs(client2); + } +} \ No newline at end of file diff --git a/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/client/ghcr/GHCRClientLoggerTests.java b/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/client/ghcr/GHCRClientLoggerTests.java new file mode 100644 index 00000000..8845ece2 --- /dev/null +++ b/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/client/ghcr/GHCRClientLoggerTests.java @@ -0,0 +1,373 @@ +package ai.docling.client.tester.client.ghcr; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import jakarta.ws.rs.core.HttpHeaders; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import ai.docling.client.tester.config.Config; + +import io.vertx.core.Handler; +import io.vertx.core.MultiMap; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpClientRequest; +import io.vertx.core.http.HttpClientResponse; +import io.vertx.core.http.HttpMethod; + +class GHCRClientLoggerTests { + + private Config config; + private GHCRClientLogger logger; + + @BeforeEach + void setUp() { + config = mock(Config.class); + logger = new GHCRClientLogger(config); + } + + @Test + void shouldSetBodySize() { + logger.setBodySize(1024); + } + + @Test + void shouldLogResponseWhenEnabled() { + // Setup + when(config.logResponses()).thenReturn(true); + + HttpClientResponse response = mock(HttpClientResponse.class); + MultiMap headers = MultiMap.caseInsensitiveMultiMap(); + headers.add("Content-Type", "application/json"); + + when(response.statusCode()).thenReturn(200); + when(response.headers()).thenReturn(headers); + + Buffer body = Buffer.buffer("{\"token\":\"secret123\"}"); + + // Capture the body handler + ArgumentCaptor> handlerCaptor = ArgumentCaptor.forClass(Handler.class); + when(response.bodyHandler(handlerCaptor.capture())).thenReturn(response); + + // Execute + logger.logResponse(response, false); + + // Verify bodyHandler was set + verify(response).bodyHandler(any()); + + // Trigger the handler + handlerCaptor.getValue().handle(body); + + // Verify response was accessed + verify(response).statusCode(); + verify(response).headers(); + } + + @Test + void shouldNotLogResponseWhenDisabled() { + // Setup + when(config.logResponses()).thenReturn(false); + + HttpClientResponse response = mock(HttpClientResponse.class); + + // Execute + logger.logResponse(response, false); + + // Verify no interaction with response + verify(response, never()).bodyHandler(any()); + } + + @Test + void shouldMaskTokenInResponseBody() { + // Setup + when(config.logResponses()).thenReturn(true); + + HttpClientResponse response = mock(HttpClientResponse.class); + MultiMap headers = MultiMap.caseInsensitiveMultiMap(); + + when(response.statusCode()).thenReturn(200); + when(response.headers()).thenReturn(headers); + + Buffer body = Buffer.buffer("{\"token\":\"very-secret-token-value\"}"); + + ArgumentCaptor> handlerCaptor = ArgumentCaptor.forClass(Handler.class); + when(response.bodyHandler(handlerCaptor.capture())).thenReturn(response); + + // Execute + logger.logResponse(response, false); + handlerCaptor.getValue().handle(body); + + // The token should be masked in logs (verified by no exception) + verify(response).statusCode(); + } + + @Test + void shouldHandleNullBodyInResponse() { + // Setup + when(config.logResponses()).thenReturn(true); + + HttpClientResponse response = mock(HttpClientResponse.class); + MultiMap headers = MultiMap.caseInsensitiveMultiMap(); + + when(response.statusCode()).thenReturn(204); + when(response.headers()).thenReturn(headers); + + ArgumentCaptor> handlerCaptor = ArgumentCaptor.forClass(Handler.class); + when(response.bodyHandler(handlerCaptor.capture())).thenReturn(response); + + // Execute + logger.logResponse(response, false); + handlerCaptor.getValue().handle(null); + + // Should not throw exception + verify(response).statusCode(); + } + + @Test + void shouldHandleEmptyBodyInResponse() { + // Setup + when(config.logResponses()).thenReturn(true); + + HttpClientResponse response = mock(HttpClientResponse.class); + MultiMap headers = MultiMap.caseInsensitiveMultiMap(); + + when(response.statusCode()).thenReturn(200); + when(response.headers()).thenReturn(headers); + + Buffer body = Buffer.buffer(""); + + ArgumentCaptor> handlerCaptor = ArgumentCaptor.forClass(Handler.class); + when(response.bodyHandler(handlerCaptor.capture())).thenReturn(response); + + // Execute + logger.logResponse(response, false); + handlerCaptor.getValue().handle(body); + + verify(response).statusCode(); + } + + @Test + void shouldLogRequestWhenEnabled() { + // Setup + when(config.logRequests()).thenReturn(true); + + HttpClientRequest request = mock(HttpClientRequest.class); + MultiMap headers = MultiMap.caseInsensitiveMultiMap(); + headers.add("Content-Type", "application/json"); + + when(request.getMethod()).thenReturn(HttpMethod.GET); + when(request.absoluteURI()).thenReturn("https://ghcr.io/v2/test/tags/list"); + when(request.headers()).thenReturn(headers); + + Buffer body = Buffer.buffer("{\"test\":\"data\"}"); + + // Execute + logger.logRequest(request, body, false); + + // Verify request was accessed + verify(request).getMethod(); + verify(request).absoluteURI(); + verify(request).headers(); + } + + @Test + void shouldNotLogRequestWhenDisabled() { + // Setup + when(config.logRequests()).thenReturn(false); + + HttpClientRequest request = mock(HttpClientRequest.class); + Buffer body = Buffer.buffer("{\"test\":\"data\"}"); + + // Execute + logger.logRequest(request, body, false); + + // Verify no interaction with request + verify(request, never()).getMethod(); + verify(request, never()).absoluteURI(); + } + + @Test + void shouldLogRequestWithNullBody() { + // Setup + when(config.logRequests()).thenReturn(true); + + HttpClientRequest request = mock(HttpClientRequest.class); + MultiMap headers = MultiMap.caseInsensitiveMultiMap(); + + when(request.getMethod()).thenReturn(HttpMethod.GET); + when(request.absoluteURI()).thenReturn("https://ghcr.io/token"); + when(request.headers()).thenReturn(headers); + + // Execute + logger.logRequest(request, null, false); + + // Should not throw exception + verify(request).getMethod(); + } + + @Test + void shouldMaskAuthorizationHeader() { + // Setup + when(config.logRequests()).thenReturn(true); + + HttpClientRequest request = mock(HttpClientRequest.class); + MultiMap headers = MultiMap.caseInsensitiveMultiMap(); + headers.add(HttpHeaders.AUTHORIZATION, "Bearer very-long-secret-token"); + + when(request.getMethod()).thenReturn(HttpMethod.GET); + when(request.absoluteURI()).thenReturn("https://ghcr.io/v2/test/tags/list"); + when(request.headers()).thenReturn(headers); + + // Execute + logger.logRequest(request, null, false); + + // Verify headers were accessed (masking happens internally) + verify(request).headers(); + } + + @Test + void shouldMaskSetCookieHeader() { + // Setup + when(config.logResponses()).thenReturn(true); + + HttpClientResponse response = mock(HttpClientResponse.class); + MultiMap headers = MultiMap.caseInsensitiveMultiMap(); + headers.add(HttpHeaders.SET_COOKIE, "session=very-long-session-id"); + + when(response.statusCode()).thenReturn(200); + when(response.headers()).thenReturn(headers); + + Buffer body = Buffer.buffer("{}"); + + ArgumentCaptor> handlerCaptor = ArgumentCaptor.forClass(Handler.class); + when(response.bodyHandler(handlerCaptor.capture())).thenReturn(response); + + // Execute + logger.logResponse(response, false); + handlerCaptor.getValue().handle(body); + + // Verify headers were accessed + verify(response).headers(); + } + + @Test + void shouldHandleShortAuthorizationValue() { + // Setup + when(config.logRequests()).thenReturn(true); + + HttpClientRequest request = mock(HttpClientRequest.class); + MultiMap headers = MultiMap.caseInsensitiveMultiMap(); + headers.add(HttpHeaders.AUTHORIZATION, "abc"); + + when(request.getMethod()).thenReturn(HttpMethod.GET); + when(request.absoluteURI()).thenReturn("https://ghcr.io/token"); + when(request.headers()).thenReturn(headers); + + // Execute + logger.logRequest(request, null, false); + + // Should not throw exception + verify(request).headers(); + } + + @Test + void shouldHandleMultipleHeaders() { + // Setup + when(config.logRequests()).thenReturn(true); + + HttpClientRequest request = mock(HttpClientRequest.class); + MultiMap headers = MultiMap.caseInsensitiveMultiMap(); + headers.add("Content-Type", "application/json"); + headers.add("Accept", "application/json"); + headers.add(HttpHeaders.AUTHORIZATION, "Bearer token123"); + + when(request.getMethod()).thenReturn(HttpMethod.POST); + when(request.absoluteURI()).thenReturn("https://ghcr.io/v2/test/tags/list"); + when(request.headers()).thenReturn(headers); + + // Execute + logger.logRequest(request, Buffer.buffer("{}"), false); + + // Verify all headers were processed + verify(request).headers(); + } + + @Test + void shouldHandleNonJsonBodyInResponse() { + // Setup + when(config.logResponses()).thenReturn(true); + + HttpClientResponse response = mock(HttpClientResponse.class); + MultiMap headers = MultiMap.caseInsensitiveMultiMap(); + + when(response.statusCode()).thenReturn(200); + when(response.headers()).thenReturn(headers); + + Buffer body = Buffer.buffer("Plain text response"); + + ArgumentCaptor> handlerCaptor = ArgumentCaptor.forClass(Handler.class); + when(response.bodyHandler(handlerCaptor.capture())).thenReturn(response); + + // Execute + logger.logResponse(response, false); + handlerCaptor.getValue().handle(body); + + // Should handle non-JSON body without error + verify(response).statusCode(); + } + + @Test + void shouldHandleJsonArrayInResponse() { + // Setup + when(config.logResponses()).thenReturn(true); + + HttpClientResponse response = mock(HttpClientResponse.class); + MultiMap headers = MultiMap.caseInsensitiveMultiMap(); + + when(response.statusCode()).thenReturn(200); + when(response.headers()).thenReturn(headers); + + Buffer body = Buffer.buffer("[{\"token\":\"secret1\"}, {\"token\":\"secret2\"}]"); + + ArgumentCaptor> handlerCaptor = ArgumentCaptor.forClass(Handler.class); + when(response.bodyHandler(handlerCaptor.capture())).thenReturn(response); + + // Execute + logger.logResponse(response, false); + handlerCaptor.getValue().handle(body); + + // Should mask tokens in JSON array + verify(response).statusCode(); + } + + @Test + void shouldHandleRedirectResponse() { + // Setup + when(config.logResponses()).thenReturn(true); + + HttpClientResponse response = mock(HttpClientResponse.class); + MultiMap headers = MultiMap.caseInsensitiveMultiMap(); + headers.add("Location", "https://example.com/redirect"); + + when(response.statusCode()).thenReturn(302); + when(response.headers()).thenReturn(headers); + + Buffer body = Buffer.buffer(""); + + ArgumentCaptor> handlerCaptor = ArgumentCaptor.forClass(Handler.class); + when(response.bodyHandler(handlerCaptor.capture())).thenReturn(response); + + // Execute + logger.logResponse(response, true); + handlerCaptor.getValue().handle(body); + + // Should handle redirect + verify(response).statusCode(); + } +} \ No newline at end of file diff --git a/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/domain/TagTestResultTests.java b/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/domain/TagTestResultTests.java index 27087352..5f9879ab 100644 --- a/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/domain/TagTestResultTests.java +++ b/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/domain/TagTestResultTests.java @@ -2,44 +2,415 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.time.Instant; +import java.util.List; + import org.junit.jupiter.api.Test; import ai.docling.client.tester.domain.TagTestResult.Result; +import ai.docling.client.tester.domain.TagTestResult.Result.Status; class TagTestResultTests { - @Test - void sortsCorrectly() { - var results = TagsTestResults.builder() - .registry("ghcr.io") - .image("docling-project/docling-serve") - .addResult( - TagTestResult.builder() - .tag("v1.1.0") - .result(Result.success("Yay")) - .build() - ) - .addResult( - TagTestResult.builder() - .tag("v0.1.1") - .result(Result.success("Yay")) - .build() - ) - .addResult( - TagTestResult.builder() - .tag("v0.1.0") - .result(Result.success("Yay")) - .build() - ) - .addResult( - TagTestResult.builder() - .tag("v1.1.1") - .result(Result.success("Yay")) - .build() - ) - .build(); - - assertThat(results.results().stream().map(TagTestResult::tag)) - .hasSize(4) - .containsExactly("v1.1.1", "v1.1.0", "v0.1.1", "v0.1.0"); - } -} + + @Test + void shouldCreateTagTestResultWithBuilder() { + // Test TagTestResult.Builder + var result = TagTestResult.builder() + .tag("v1.0.0") + .result(Result.success("Test passed")) + .serverLogs("Server log output") + .build(); + + assertThat(result.tag()).isEqualTo("v1.0.0"); + assertThat(result.result().status()).isEqualTo(Status.SUCCESS); + assertThat(result.result().message()).isEqualTo("Test passed"); + assertThat(result.serverLogs()).isEqualTo("Server log output"); + } + + @Test + void shouldUseTagTestResultToBuilder() { + // Test TagTestResult.toBuilder() + var original = TagTestResult.builder() + .tag("v1.0.0") + .result(Result.success("Original")) + .serverLogs("Original logs") + .build(); + + var modified = original.toBuilder() + .tag("v2.0.0") + .result(Result.success("Modified")) + .build(); + + assertThat(original.tag()).isEqualTo("v1.0.0"); + assertThat(original.result().message()).isEqualTo("Original"); + assertThat(original.serverLogs()).isEqualTo("Original logs"); + + assertThat(modified.tag()).isEqualTo("v2.0.0"); + assertThat(modified.result().message()).isEqualTo("Modified"); + assertThat(modified.serverLogs()).isEqualTo("Original logs"); + } + + @Test + void shouldCreateSuccessResult() { + // Test Result.success() + var result = Result.success("Operation successful"); + + assertThat(result.status()).isEqualTo(Status.SUCCESS); + assertThat(result.message()).isEqualTo("Operation successful"); + assertThat(result.fullStackTrace()).isNull(); + } + + @Test + void shouldCreateFailureResultWithThrowable() { + // Test Result.failure(Throwable) + var exception = new RuntimeException("Test error"); + var result = Result.failure(exception); + + assertThat(result.status()).isEqualTo(Status.FAILURE); + assertThat(result.message()).isEqualTo("Test error"); + assertThat(result.fullStackTrace()).isNotNull(); + assertThat(result.fullStackTrace()).contains("RuntimeException"); + assertThat(result.fullStackTrace()).contains("Test error"); + } + + @Test + void shouldCreateFailureResultWithMessageAndThrowable() { + // Test Result.failure(String, Throwable) + var exception = new RuntimeException("Original error"); + var result = Result.failure("Custom message", exception); + + assertThat(result.status()).isEqualTo(Status.FAILURE); + assertThat(result.message()).isEqualTo("Custom message"); + assertThat(result.fullStackTrace()).isNotNull(); + assertThat(result.fullStackTrace()).contains("RuntimeException"); + assertThat(result.fullStackTrace()).contains("Original error"); + } + + @Test + void shouldHandleNullThrowableInFailure() { + // Test Result.failure with null throwable (edge case) + var result = Result.failure("Error message", null); + + assertThat(result.status()).isEqualTo(Status.FAILURE); + assertThat(result.message()).isEqualTo("Error message"); + assertThat(result.fullStackTrace()).isEmpty(); + } + + @Test + void shouldCompareTagTestResultsCorrectly() { + // Test compareTo() method + var result1 = TagTestResult.builder() + .tag("v1.0.0") + .result(Result.success("Success")) + .build(); + + var result2 = TagTestResult.builder() + .tag("v2.0.0") + .result(Result.success("Success")) + .build(); + + var result3 = TagTestResult.builder() + .tag("v1.5.0") + .result(Result.success("Success")) + .build(); + + // Higher versions should come first (descending order) + assertThat(result2.compareTo(result1)).isLessThan(0); + assertThat(result1.compareTo(result2)).isGreaterThan(0); + assertThat(result3.compareTo(result1)).isLessThan(0); + assertThat(result3.compareTo(result2)).isGreaterThan(0); + } + + @Test + void shouldHandleTagsWithoutVPrefix() { + // Test version comparison without 'v' prefix + var result1 = TagTestResult.builder() + .tag("1.0.0") + .result(Result.success("Success")) + .build(); + + var result2 = TagTestResult.builder() + .tag("2.0.0") + .result(Result.success("Success")) + .build(); + + assertThat(result2.compareTo(result1)).isLessThan(0); + } + + @Test + void shouldGetStatusIcon() { + // Test Status.getIcon() + assertThat(Status.SUCCESS.getIcon()).isEqualTo("✅"); + assertThat(Status.FAILURE.getIcon()).isEqualTo("❌"); + } + + @Test + void shouldCreateTagTestResultWithNullServerLogs() { + // Test builder with null server logs + var result = TagTestResult.builder() + .tag("v1.0.0") + .result(Result.success("Success")) + .serverLogs(null) + .build(); + + assertThat(result.serverLogs()).isNull(); + } + + @Test + void shouldCreateTagTestResultWithMinimalBuilder() { + // Test builder with only required fields + var result = TagTestResult.builder() + .tag("v1.0.0") + .result(Result.success("Success")) + .build(); + + assertThat(result.tag()).isEqualTo("v1.0.0"); + assertThat(result.result()).isNotNull(); + assertThat(result.serverLogs()).isNull(); + } + + @Test + void shouldHandleExceptionWithNestedCause() { + // Test getFullStackTrace with nested exceptions + var cause = new IllegalStateException("Root cause"); + var exception = new RuntimeException("Wrapper exception", cause); + var result = Result.failure("Error occurred", exception); + + assertThat(result.status()).isEqualTo(Status.FAILURE); + assertThat(result.message()).isEqualTo("Error occurred"); + assertThat(result.fullStackTrace()).isNotNull(); + assertThat(result.fullStackTrace()).contains("RuntimeException"); + assertThat(result.fullStackTrace()).contains("Wrapper exception"); + assertThat(result.fullStackTrace()).contains("IllegalStateException"); + assertThat(result.fullStackTrace()).contains("Root cause"); + } + + @Test + void shouldHandleExceptionWithLongStackTrace() { + // Test with a real exception that has a full stack trace + Exception exception; + try { + throw new IllegalArgumentException("Test exception with stack trace"); + } catch (IllegalArgumentException e) { + exception = e; + } + + var result = Result.failure(exception); + + assertThat(result.status()).isEqualTo(Status.FAILURE); + assertThat(result.message()).isEqualTo("Test exception with stack trace"); + assertThat(result.fullStackTrace()).isNotNull(); + assertThat(result.fullStackTrace()).contains("IllegalArgumentException"); + assertThat(result.fullStackTrace()).contains("Test exception with stack trace"); + assertThat(result.fullStackTrace()).contains("at "); + } + + @Test + void sortsCorrectly() { + var results = TagsTestResults.builder() + .registry("ghcr.io") + .image("docling-project/docling-serve") + .addResult( + TagTestResult.builder() + .tag("v1.1.0") + .result(Result.success("Yay")) + .build()) + .addResult( + TagTestResult.builder() + .tag("v0.1.1") + .result(Result.success("Yay")) + .build()) + .addResult( + TagTestResult.builder() + .tag("v0.1.0") + .result(Result.success("Yay")) + .build()) + .addResult( + TagTestResult.builder() + .tag("v1.1.1") + .result(Result.success("Yay")) + .build()) + .build(); + + assertThat(results.results().stream().map(TagTestResult::tag)) + .hasSize(4) + .containsExactly("v1.1.1", "v1.1.0", "v0.1.1", "v0.1.0"); + } + + @Test + void shouldCreateWithThreeParameterConstructor() { + // Test the constructor TagsTestResults(String, String,List) + var result1 = TagTestResult.builder() + .tag("v1.0.0") + .result(Result.success("Success")) + .build(); + + var result2 = TagTestResult.builder() + .tag("v2.0.0") + .result(Result.failure(new RuntimeException("Failed"))) + .build(); + + var results = new TagsTestResults("ghcr.io", "test-image", List.of(result1, result2)); + + assertThat(results.registry()).isEqualTo("ghcr.io"); + assertThat(results.image()).isEqualTo("test-image"); + assertThat(results.results()).hasSize(2); + assertThat(results.timestamp()).isNotNull(); + assertThat(results.timestamp()).isBeforeOrEqualTo(Instant.now()); + } + + @Test + void shouldCreateWithThreeParameterConstructorAndNullResults() { + // Test the constructor with null results list + var results = new TagsTestResults("registry", "image", null); + + assertThat(results.registry()).isEqualTo("registry"); + assertThat(results.image()).isEqualTo("image"); + assertThat(results.results()).isEmpty(); + assertThat(results.timestamp()).isNotNull(); + } + + @Test + void shouldDetectFailures() { + // Test hasAtLeastOneFailure() method + var successResult = TagTestResult.builder() + .tag("v1.0.0") + .result(Result.success("Success")) + .build(); + + var failureResult = TagTestResult.builder() + .tag("v2.0.0") + .result(Result.failure(new RuntimeException("Failed"))) + .build(); + + var resultsWithFailure = new TagsTestResults("ghcr.io", "test-image", List.of(successResult, failureResult)); + var resultsWithoutFailure = new TagsTestResults("ghcr.io", "test-image", List.of(successResult)); + + assertThat(resultsWithFailure.hasAtLeastOneFailure()).isTrue(); + assertThat(resultsWithoutFailure.hasAtLeastOneFailure()).isFalse(); + } + + @Test + void shouldHandleEmptyResultsList() { + // Test with empty results list + var results = new TagsTestResults("registry", "image", List.of()); + + assertThat(results.results()).isEmpty(); + assertThat(results.hasAtLeastOneFailure()).isFalse(); + } + + @Test + void shouldSortResultsInConstructor() { + // Test that results are sorted when passed to constructor + var result1 = TagTestResult.builder() + .tag("v1.0.0") + .result(Result.success("Success")) + .build(); + + var result2 = TagTestResult.builder() + .tag("v2.0.0") + .result(Result.success("Success")) + .build(); + + var result3 = TagTestResult.builder() + .tag("v1.5.0") + .result(Result.success("Success")) + .build(); + + // Pass in unsorted order + var results = new TagsTestResults("registry", "image", List.of(result1, result3, result2)); + + // Should be sorted in descending order + assertThat(results.results().stream().map(TagTestResult::tag)) + .containsExactly("v2.0.0", "v1.5.0", "v1.0.0"); + } + + @Test + void shouldCreateFromTagsTestRequest() { + // Test the from() method + java.util.concurrent.Executor executor = Runnable::run; + var request = TagsTestRequest.builder() + .registry("ghcr.io") + .image("test-image") + .executor(executor) + .build(); + + var results = TagsTestResults.from(request).build(); + + assertThat(results.registry()).isEqualTo("ghcr.io"); + assertThat(results.image()).isEqualTo("test-image"); + assertThat(results.results()).isEmpty(); + } + + @Test + void shouldUseToBuilder() { + // Test toBuilder() method + var original = TagsTestResults.builder() + .registry("ghcr.io") + .image("original-image") + .addResult( + TagTestResult.builder() + .tag("v1.0.0") + .result(Result.success("Success")) + .build()) + .build(); + + var modified = original.toBuilder() + .image("modified-image") + .addResult( + TagTestResult.builder() + .tag("v2.0.0") + .result(Result.success("Success")) + .build()) + .build(); + + assertThat(original.image()).isEqualTo("original-image"); + assertThat(original.results()).hasSize(1); + + assertThat(modified.registry()).isEqualTo("ghcr.io"); + assertThat(modified.image()).isEqualTo("modified-image"); + assertThat(modified.results()).hasSize(2); + } + + @Test + void shouldSetResults() { + // Test setResults() method + var result1 = TagTestResult.builder() + .tag("v1.0.0") + .result(Result.success("Success")) + .build(); + + var result2 = TagTestResult.builder() + .tag("v2.0.0") + .result(Result.success("Success")) + .build(); + + var results = TagsTestResults.builder() + .registry("ghcr.io") + .image("test-image") + .addResult(result1) + .setResults(List.of(result2)) + .build(); + + assertThat(results.results()).hasSize(1); + assertThat(results.results().get(0).tag()).isEqualTo("v2.0.0"); + } + + @Test + void shouldClearResults() { + // Test clearResults() method + var result = TagTestResult.builder() + .tag("v1.0.0") + .result(Result.success("Success")) + .build(); + + var results = TagsTestResults.builder() + .registry("ghcr.io") + .image("test-image") + .addResult(result) + .clearResults() + .build(); + + assertThat(results.results()).isEmpty(); + } +} \ No newline at end of file diff --git a/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/domain/TagsTestRequestTests.java b/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/domain/TagsTestRequestTests.java new file mode 100644 index 00000000..7f5c5b6e --- /dev/null +++ b/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/domain/TagsTestRequestTests.java @@ -0,0 +1,249 @@ +package ai.docling.client.tester.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +import org.junit.jupiter.api.Test; + +class TagsTestRequestTests { + + private static final Executor TEST_EXECUTOR = Executors.newSingleThreadExecutor(); + + @Test + void shouldThrowExceptionWhenExecutorIsNull() { + assertThatThrownBy(() -> new TagsTestRequest("registry", "image", null, false, List.of("tag1"))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("executor cannot be null"); + } + + @Test + void shouldCreateRequestWithBuilder() { + var request = TagsTestRequest.builder() + .registry("ghcr.io") + .image("docling-project/docling-serve") + .executor(TEST_EXECUTOR) + .cleanupContainerImages(true) + .tags(List.of("v1.0.0", "v1.1.0")) + .build(); + + assertThat(request).isNotNull(); + assertThat(request.registry()).isEqualTo("ghcr.io"); + assertThat(request.image()).isEqualTo("docling-project/docling-serve"); + assertThat(request.executor()).isEqualTo(TEST_EXECUTOR); + assertThat(request.cleanupContainerImages()).isTrue(); + assertThat(request.tags()).containsExactly("v1.0.0", "v1.1.0"); + } + + @Test + void shouldCreateRequestWithMinimalBuilder() { + var request = TagsTestRequest.builder() + .executor(TEST_EXECUTOR) + .build(); + + assertThat(request).isNotNull(); + assertThat(request.registry()).isNull(); + assertThat(request.image()).isNull(); + assertThat(request.executor()).isEqualTo(TEST_EXECUTOR); + assertThat(request.cleanupContainerImages()).isFalse(); + assertThat(request.tags()).isNull(); + } + + @Test + void shouldConvertToBuilderAndModify() { + var originalRequest = TagsTestRequest.builder() + .registry("ghcr.io") + .image("docling-project/docling-serve") + .executor(TEST_EXECUTOR) + .cleanupContainerImages(false) + .tags(List.of("v1.0.0")) + .build(); + + var modifiedRequest = originalRequest.toBuilder() + .registry("quay.io") + .cleanupContainerImages(true) + .tags(List.of("v2.0.0")) + .build(); + + assertThat(originalRequest.registry()).isEqualTo("ghcr.io"); + assertThat(originalRequest.cleanupContainerImages()).isFalse(); + assertThat(originalRequest.tags()).containsExactly("v1.0.0"); + + assertThat(modifiedRequest.registry()).isEqualTo("quay.io"); + assertThat(modifiedRequest.image()).isEqualTo("docling-project/docling-serve"); + assertThat(modifiedRequest.executor()).isEqualTo(TEST_EXECUTOR); + assertThat(modifiedRequest.cleanupContainerImages()).isTrue(); + assertThat(modifiedRequest.tags()).containsExactly("v2.0.0"); + } + + @Test + void shouldCopyAllFieldsWithToBuilder() { + + var original = TagsTestRequest.builder() + .registry("docker.io") + .image("library/nginx") + .executor(TEST_EXECUTOR) + .cleanupContainerImages(true) + .tags(List.of("latest", "stable")) + .build(); + + var copy = original.toBuilder().build(); + + assertThat(copy.registry()).isEqualTo(original.registry()); + assertThat(copy.image()).isEqualTo(original.image()); + assertThat(copy.executor()).isEqualTo(original.executor()); + assertThat(copy.cleanupContainerImages()).isEqualTo(original.cleanupContainerImages()); + assertThat(copy.tags()).isEqualTo(original.tags()); + } + + @Test + void shouldSetRegistryInBuilder() { + var request = TagsTestRequest.builder() + .registry("custom-registry.io") + .executor(TEST_EXECUTOR) + .build(); + + assertThat(request.registry()).isEqualTo("custom-registry.io"); + } + + @Test + void shouldSetImageInBuilder() { + // Test image setter (line 54-57) + var request = TagsTestRequest.builder() + .image("my-app/my-service") + .executor(TEST_EXECUTOR) + .build(); + + assertThat(request.image()).isEqualTo("my-app/my-service"); + } + + @Test + void shouldSetCleanupContainerImagesInBuilder() { + var requestWithCleanup = TagsTestRequest.builder() + .cleanupContainerImages(true) + .executor(TEST_EXECUTOR) + .build(); + + var requestWithoutCleanup = TagsTestRequest.builder() + .cleanupContainerImages(false) + .executor(TEST_EXECUTOR) + .build(); + + assertThat(requestWithCleanup.cleanupContainerImages()).isTrue(); + assertThat(requestWithoutCleanup.cleanupContainerImages()).isFalse(); + } + + @Test + void shouldSetTagsInBuilder() { + var tags = List.of("v1.0.0", "v1.1.0", "latest"); + var request = TagsTestRequest.builder() + .tags(tags) + .executor(TEST_EXECUTOR) + .build(); + + assertThat(request.tags()).isEqualTo(tags); + } + + @Test + void shouldSetExecutorInBuilder() { + var customExecutor = Executors.newFixedThreadPool(2); + var request = TagsTestRequest.builder() + .executor(customExecutor) + .build(); + + assertThat(request.executor()).isEqualTo(customExecutor); + } + + @Test + void shouldThrowExceptionWhenBuildingWithNullExecutor() { + assertThatThrownBy(() -> TagsTestRequest.builder() + .registry("ghcr.io") + .image("test-image") + .build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("executor cannot be null"); + } + + @Test + void shouldHandleEmptyTagsList() { + // Edge case: empty tags list + var request = TagsTestRequest.builder() + .tags(List.of()) + .executor(TEST_EXECUTOR) + .build(); + + assertThat(request.tags()).isEmpty(); + } + + @Test + void shouldHandleNullTagsList() { + // Edge case: null tags list + var request = TagsTestRequest.builder() + .tags(null) + .executor(TEST_EXECUTOR) + .build(); + + assertThat(request.tags()).isNull(); + } + + @Test + void shouldHandleNullRegistry() { + // Edge case: null registry + var request = TagsTestRequest.builder() + .registry(null) + .executor(TEST_EXECUTOR) + .build(); + + assertThat(request.registry()).isNull(); + } + + @Test + void shouldHandleNullImage() { + // Edge case: null image + var request = TagsTestRequest.builder() + .image(null) + .executor(TEST_EXECUTOR) + .build(); + + assertThat(request.image()).isNull(); + } + + @Test + void shouldChainBuilderMethods() { + // Test method chaining + var request = TagsTestRequest.builder() + .registry("ghcr.io") + .image("test-image") + .executor(TEST_EXECUTOR) + .tags(List.of("v1.0.0")) + .cleanupContainerImages(true) + .build(); + + assertThat(request.registry()).isEqualTo("ghcr.io"); + assertThat(request.image()).isEqualTo("test-image"); + assertThat(request.executor()).isEqualTo(TEST_EXECUTOR); + assertThat(request.tags()).containsExactly("v1.0.0"); + assertThat(request.cleanupContainerImages()).isTrue(); + } + + @Test + void shouldOverrideBuilderValues() { + // Test that builder values can be overridden + var request = TagsTestRequest.builder() + .registry("first-registry") + .registry("second-registry") + .image("first-image") + .image("second-image") + .cleanupContainerImages(false) + .cleanupContainerImages(true) + .executor(TEST_EXECUTOR) + .build(); + + assertThat(request.registry()).isEqualTo("second-registry"); + assertThat(request.image()).isEqualTo("second-image"); + assertThat(request.cleanupContainerImages()).isTrue(); + } +} \ No newline at end of file diff --git a/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/service/NamedThreadFactoryTests.java b/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/service/NamedThreadFactoryTests.java new file mode 100644 index 00000000..1d3994b7 --- /dev/null +++ b/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/service/NamedThreadFactoryTests.java @@ -0,0 +1,106 @@ +package ai.docling.client.tester.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class NamedThreadFactoryTests { + + @Test + void shouldCreateThreadWithName() { + var factory = new NamedThreadFactory("test-thread"); + Runnable runnable = () -> { + }; + + Thread thread = factory.newThread(runnable); + + assertThat(thread).isNotNull(); + assertThat(thread.getName()).isEqualTo("test-thread-1"); + } + + @Test + void shouldIncrementThreadNumber() { + var factory = new NamedThreadFactory("worker"); + Runnable runnable = () -> { + }; + + Thread thread1 = factory.newThread(runnable); + Thread thread2 = factory.newThread(runnable); + Thread thread3 = factory.newThread(runnable); + + assertThat(thread1.getName()).isEqualTo("worker-1"); + assertThat(thread2.getName()).isEqualTo("worker-2"); + assertThat(thread3.getName()).isEqualTo("worker-3"); + } + + @Test + void shouldCreateThreadWithDifferentNames() { + var factory1 = new NamedThreadFactory("pool-1"); + var factory2 = new NamedThreadFactory("pool-2"); + Runnable runnable = () -> { + }; + + Thread thread1 = factory1.newThread(runnable); + Thread thread2 = factory2.newThread(runnable); + + assertThat(thread1.getName()).isEqualTo("pool-1-1"); + assertThat(thread2.getName()).isEqualTo("pool-2-1"); + } + + @Test + void shouldCreateThreadWithRunnable() { + var factory = new NamedThreadFactory("executor"); + var executed = new boolean[] { false }; + Runnable runnable = () -> executed[0] = true; + + Thread thread = factory.newThread(runnable); + thread.start(); + + try { + thread.join(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + assertThat(executed[0]).isTrue(); + } + + @Test + void shouldHandleSpecialCharactersInName() { + var factory = new NamedThreadFactory("test-worker_pool"); + Runnable runnable = () -> { + }; + + Thread thread = factory.newThread(runnable); + + assertThat(thread.getName()).isEqualTo("test-worker_pool-1"); + } + + @Test + void shouldHandleEmptyName() { + var factory = new NamedThreadFactory(""); + Runnable runnable = () -> { + }; + + Thread thread = factory.newThread(runnable); + + assertThat(thread.getName()).isEqualTo("-1"); + } + + @Test + void shouldCreateMultipleThreadsConcurrently() { + var factory = new NamedThreadFactory("concurrent"); + Runnable runnable = () -> { + }; + + Thread[] threads = new Thread[10]; + for (int i = 0; i < 10; i++) { + threads[i] = factory.newThread(runnable); + } + + // All threads should have unique numbers + for (int i = 0; i < 10; i++) { + assertThat(threads[i].getName()).matches("concurrent-\\d+"); + } + } +} \ No newline at end of file diff --git a/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/service/WorkParallelizerTests.java b/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/service/WorkParallelizerTests.java new file mode 100644 index 00000000..7de8ab95 --- /dev/null +++ b/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/service/WorkParallelizerTests.java @@ -0,0 +1,189 @@ +package ai.docling.client.tester.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Test; + +class WorkParallelizerTests { + + @Test + void shouldTransformItemsInParallel() { + var executor = Executors.newFixedThreadPool(4); + var items = List.of(1, 2, 3, 4, 5); + + var results = WorkParallelizer.transformInParallelAndWait( + executor, + items, + item -> item * 2); + + assertThat(results) + .hasSize(5) + .containsExactlyInAnyOrder(2, 4, 6, 8, 10); + + executor.shutdown(); + } + + @Test + void shouldTransformEmptyList() { + var executor = Executors.newSingleThreadExecutor(); + List items = List.of(); + + var results = WorkParallelizer.transformInParallelAndWait( + executor, + items, + item -> item * 2); + + assertThat(results).isEmpty(); + + executor.shutdown(); + } + + @Test + void shouldTransformSingleItem() { + var executor = Executors.newSingleThreadExecutor(); + var items = List.of("test"); + + var results = WorkParallelizer.transformInParallelAndWait( + executor, + items, + String::toUpperCase); + + assertThat(results) + .hasSize(1) + .containsExactly("TEST"); + + executor.shutdown(); + } + + @Test + void shouldRunItemsInParallel() { + var executor = Executors.newFixedThreadPool(4); + var items = List.of(1, 2, 3, 4, 5); + var processedItems = new ArrayList(); + + WorkParallelizer.runInParallelAndWait( + executor, + items, + item -> { + synchronized (processedItems) { + processedItems.add(item); + } + }); + + assertThat(processedItems) + .hasSize(5) + .containsExactlyInAnyOrder(1, 2, 3, 4, 5); + + executor.shutdown(); + } + + @Test + void shouldRunEmptyList() { + var executor = Executors.newSingleThreadExecutor(); + List items = List.of(); + var counter = new AtomicInteger(0); + + WorkParallelizer.runInParallelAndWait( + executor, + items, + item -> counter.incrementAndGet()); + + assertThat(counter.get()).isZero(); + + executor.shutdown(); + } + + @Test + void shouldRunSingleItem() { + var executor = Executors.newSingleThreadExecutor(); + var items = List.of("test"); + var counter = new AtomicInteger(0); + + WorkParallelizer.runInParallelAndWait( + executor, + items, + item -> counter.incrementAndGet()); + + assertThat(counter.get()).isEqualTo(1); + + executor.shutdown(); + } + + @Test + void shouldHandleComplexTransformations() { + var executor = Executors.newFixedThreadPool(2); + var items = List.of("apple", "banana", "cherry"); + + var results = WorkParallelizer.transformInParallelAndWait( + executor, + items, + item -> item.length()); + + assertThat(results) + .hasSize(3) + .containsExactlyInAnyOrder(5, 6, 6); + + executor.shutdown(); + } + + @Test + void shouldExecuteInParallelWithMultipleThreads() { + var executor = Executors.newFixedThreadPool(10); + var items = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + var threadIds = new ArrayList(); + + WorkParallelizer.runInParallelAndWait( + executor, + items, + item -> { + synchronized (threadIds) { + threadIds.add(Thread.currentThread().getId()); + } + }); + + // Should use multiple threads + assertThat(threadIds).hasSizeGreaterThanOrEqualTo(1); + + executor.shutdown(); + } + + @Test + void shouldTransformWithDifferentTypes() { + var executor = Executors.newFixedThreadPool(2); + var items = List.of(1, 2, 3); + + var results = WorkParallelizer.transformInParallelAndWait( + executor, + items, + item -> "Number: " + item); + + assertThat(results) + .hasSize(3) + .containsExactlyInAnyOrder("Number: 1", "Number: 2", "Number: 3"); + + executor.shutdown(); + } + + @Test + void shouldHandleLargeNumberOfItems() { + var executor = Executors.newFixedThreadPool(4); + var items = new ArrayList(); + for (int i = 0; i < 100; i++) { + items.add(i); + } + + var results = WorkParallelizer.transformInParallelAndWait( + executor, + items, + item -> item + 1); + + assertThat(results).hasSize(100); + + executor.shutdown(); + } +} \ No newline at end of file From 604a5ed35f340921713d3a5843544eaab8ed913a Mon Sep 17 00:00:00 2001 From: Margi <39212098+margi212@users.noreply.github.com> Date: Wed, 8 Jul 2026 14:22:03 +0530 Subject: [PATCH 2/3] test(work-parallelizer): refactor transform tests to parameterized cases Signed-off-by: Margi <39212098+margi212@users.noreply.github.com> --- .../client/RegistryClientFactoryTests.java | 63 +--- .../client/ghcr/GHCRClientLoggerTests.java | 327 ++++-------------- .../tester/domain/TagsTestRequestTests.java | 219 ++---------- .../service/NamedThreadFactoryTests.java | 109 +----- .../tester/service/WorkParallelizerTests.java | 212 ++++-------- 5 files changed, 182 insertions(+), 748 deletions(-) diff --git a/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/client/RegistryClientFactoryTests.java b/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/client/RegistryClientFactoryTests.java index 986cf9d8..3f29c9e0 100644 --- a/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/client/RegistryClientFactoryTests.java +++ b/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/client/RegistryClientFactoryTests.java @@ -6,6 +6,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; import ai.docling.client.tester.client.ghcr.GHCRClient; @@ -22,61 +25,17 @@ void setUp() { @Test void shouldReturnGHCRClientForGhcrRegistry() { - var client = factory.getRegistryClient("ghcr.io"); - - assertThat(client) + assertThat(factory.getRegistryClient("ghcr.io")) .isNotNull() .isSameAs(ghcrClient); } - @Test - void shouldThrowExceptionForUnsupportedRegistry() { - assertThatThrownBy(() -> factory.getRegistryClient("docker.io")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Unsupported registry: docker.io"); - } - - @Test - void shouldThrowExceptionForNullRegistry() { - assertThatThrownBy(() -> factory.getRegistryClient(null)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Unsupported registry: null"); - } - - @Test - void shouldThrowExceptionForEmptyRegistry() { - assertThatThrownBy(() -> factory.getRegistryClient("")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Unsupported registry: "); - } - - @Test - void shouldThrowExceptionForUnknownRegistry() { - assertThatThrownBy(() -> factory.getRegistryClient("quay.io")) + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = { "docker.io", "quay.io", "GHCR.IO", " ghcr.io " }) + void shouldThrowExceptionForUnsupportedRegistry(String registry) { + assertThatThrownBy(() -> factory.getRegistryClient(registry)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Unsupported registry: quay.io"); - } - - @Test - void shouldThrowExceptionForRegistryWithDifferentCase() { - // Registry names are case-sensitive - assertThatThrownBy(() -> factory.getRegistryClient("GHCR.IO")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Unsupported registry: GHCR.IO"); - } - - @Test - void shouldThrowExceptionForRegistryWithWhitespace() { - assertThatThrownBy(() -> factory.getRegistryClient(" ghcr.io ")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Unsupported registry: ghcr.io "); - } - - @Test - void shouldReturnSameClientInstanceOnMultipleCalls() { - var client1 = factory.getRegistryClient("ghcr.io"); - var client2 = factory.getRegistryClient("ghcr.io"); - - assertThat(client1).isSameAs(client2); + .hasMessage("Unsupported registry: " + registry); } -} \ No newline at end of file +} diff --git a/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/client/ghcr/GHCRClientLoggerTests.java b/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/client/ghcr/GHCRClientLoggerTests.java index 8845ece2..b174e35e 100644 --- a/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/client/ghcr/GHCRClientLoggerTests.java +++ b/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/client/ghcr/GHCRClientLoggerTests.java @@ -10,6 +10,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentCaptor; import ai.docling.client.tester.config.Config; @@ -32,342 +34,143 @@ void setUp() { logger = new GHCRClientLogger(config); } - @Test - void shouldSetBodySize() { - logger.setBodySize(1024); - } - - @Test - void shouldLogResponseWhenEnabled() { - // Setup - when(config.logResponses()).thenReturn(true); - - HttpClientResponse response = mock(HttpClientResponse.class); - MultiMap headers = MultiMap.caseInsensitiveMultiMap(); - headers.add("Content-Type", "application/json"); - - when(response.statusCode()).thenReturn(200); - when(response.headers()).thenReturn(headers); - - Buffer body = Buffer.buffer("{\"token\":\"secret123\"}"); - - // Capture the body handler - ArgumentCaptor> handlerCaptor = ArgumentCaptor.forClass(Handler.class); - when(response.bodyHandler(handlerCaptor.capture())).thenReturn(response); - - // Execute - logger.logResponse(response, false); - - // Verify bodyHandler was set - verify(response).bodyHandler(any()); - - // Trigger the handler - handlerCaptor.getValue().handle(body); - - // Verify response was accessed - verify(response).statusCode(); - verify(response).headers(); - } - @Test void shouldNotLogResponseWhenDisabled() { - // Setup when(config.logResponses()).thenReturn(false); - HttpClientResponse response = mock(HttpClientResponse.class); - // Execute logger.logResponse(response, false); - // Verify no interaction with response verify(response, never()).bodyHandler(any()); } - @Test - void shouldMaskTokenInResponseBody() { - // Setup + @ParameterizedTest + @ValueSource(strings = { + "{\"token\":\"secret123\"}", + "{\"token\":\"very-secret-token-value\"}", + "[{\"token\":\"secret1\"}, {\"token\":\"secret2\"}]", + "Plain text response", + "" + }) + void shouldLogResponseBody(String rawBody) { when(config.logResponses()).thenReturn(true); + MockedResponse mr = mockedResponse(200, MultiMap.caseInsensitiveMultiMap()); - HttpClientResponse response = mock(HttpClientResponse.class); - MultiMap headers = MultiMap.caseInsensitiveMultiMap(); - - when(response.statusCode()).thenReturn(200); - when(response.headers()).thenReturn(headers); - - Buffer body = Buffer.buffer("{\"token\":\"very-secret-token-value\"}"); + logger.logResponse(mr.response, false); + mr.triggerBody(Buffer.buffer(rawBody)); - ArgumentCaptor> handlerCaptor = ArgumentCaptor.forClass(Handler.class); - when(response.bodyHandler(handlerCaptor.capture())).thenReturn(response); - - // Execute - logger.logResponse(response, false); - handlerCaptor.getValue().handle(body); - - // The token should be masked in logs (verified by no exception) - verify(response).statusCode(); + verify(mr.response).statusCode(); + verify(mr.response).headers(); } @Test void shouldHandleNullBodyInResponse() { - // Setup when(config.logResponses()).thenReturn(true); + MockedResponse mr = mockedResponse(204, MultiMap.caseInsensitiveMultiMap()); - HttpClientResponse response = mock(HttpClientResponse.class); - MultiMap headers = MultiMap.caseInsensitiveMultiMap(); + logger.logResponse(mr.response, false); + mr.triggerBody(null); - when(response.statusCode()).thenReturn(204); - when(response.headers()).thenReturn(headers); - - ArgumentCaptor> handlerCaptor = ArgumentCaptor.forClass(Handler.class); - when(response.bodyHandler(handlerCaptor.capture())).thenReturn(response); - - // Execute - logger.logResponse(response, false); - handlerCaptor.getValue().handle(null); - - // Should not throw exception - verify(response).statusCode(); + verify(mr.response).statusCode(); } @Test - void shouldHandleEmptyBodyInResponse() { - // Setup + void shouldMaskSetCookieHeaderInResponse() { when(config.logResponses()).thenReturn(true); - HttpClientResponse response = mock(HttpClientResponse.class); - MultiMap headers = MultiMap.caseInsensitiveMultiMap(); - - when(response.statusCode()).thenReturn(200); - when(response.headers()).thenReturn(headers); - - Buffer body = Buffer.buffer(""); - - ArgumentCaptor> handlerCaptor = ArgumentCaptor.forClass(Handler.class); - when(response.bodyHandler(handlerCaptor.capture())).thenReturn(response); - - // Execute - logger.logResponse(response, false); - handlerCaptor.getValue().handle(body); - - verify(response).statusCode(); - } - - @Test - void shouldLogRequestWhenEnabled() { - // Setup - when(config.logRequests()).thenReturn(true); - - HttpClientRequest request = mock(HttpClientRequest.class); MultiMap headers = MultiMap.caseInsensitiveMultiMap(); - headers.add("Content-Type", "application/json"); - - when(request.getMethod()).thenReturn(HttpMethod.GET); - when(request.absoluteURI()).thenReturn("https://ghcr.io/v2/test/tags/list"); - when(request.headers()).thenReturn(headers); - - Buffer body = Buffer.buffer("{\"test\":\"data\"}"); + headers.add(HttpHeaders.SET_COOKIE, "session=very-long-session-id"); + MockedResponse mr = mockedResponse(200, headers); - // Execute - logger.logRequest(request, body, false); + logger.logResponse(mr.response, false); + mr.triggerBody(Buffer.buffer("{}")); - // Verify request was accessed - verify(request).getMethod(); - verify(request).absoluteURI(); - verify(request).headers(); + verify(mr.response).headers(); } @Test void shouldNotLogRequestWhenDisabled() { - // Setup when(config.logRequests()).thenReturn(false); - HttpClientRequest request = mock(HttpClientRequest.class); - Buffer body = Buffer.buffer("{\"test\":\"data\"}"); - // Execute - logger.logRequest(request, body, false); + logger.logRequest(request, Buffer.buffer("{\"test\":\"data\"}"), false); - // Verify no interaction with request verify(request, never()).getMethod(); verify(request, never()).absoluteURI(); } @Test - void shouldLogRequestWithNullBody() { - // Setup + void shouldLogRequestWhenEnabled() { when(config.logRequests()).thenReturn(true); - HttpClientRequest request = mock(HttpClientRequest.class); MultiMap headers = MultiMap.caseInsensitiveMultiMap(); + headers.add("Content-Type", "application/json"); + HttpClientRequest request = mockedRequest(HttpMethod.GET, "https://ghcr.io/v2/test/tags/list", headers); - when(request.getMethod()).thenReturn(HttpMethod.GET); - when(request.absoluteURI()).thenReturn("https://ghcr.io/token"); - when(request.headers()).thenReturn(headers); - - // Execute - logger.logRequest(request, null, false); + logger.logRequest(request, Buffer.buffer("{\"test\":\"data\"}"), false); - // Should not throw exception verify(request).getMethod(); - } - - @Test - void shouldMaskAuthorizationHeader() { - // Setup - when(config.logRequests()).thenReturn(true); - - HttpClientRequest request = mock(HttpClientRequest.class); - MultiMap headers = MultiMap.caseInsensitiveMultiMap(); - headers.add(HttpHeaders.AUTHORIZATION, "Bearer very-long-secret-token"); - - when(request.getMethod()).thenReturn(HttpMethod.GET); - when(request.absoluteURI()).thenReturn("https://ghcr.io/v2/test/tags/list"); - when(request.headers()).thenReturn(headers); - - // Execute - logger.logRequest(request, null, false); - - // Verify headers were accessed (masking happens internally) + verify(request).absoluteURI(); verify(request).headers(); } @Test - void shouldMaskSetCookieHeader() { - // Setup - when(config.logResponses()).thenReturn(true); - - HttpClientResponse response = mock(HttpClientResponse.class); - MultiMap headers = MultiMap.caseInsensitiveMultiMap(); - headers.add(HttpHeaders.SET_COOKIE, "session=very-long-session-id"); - - when(response.statusCode()).thenReturn(200); - when(response.headers()).thenReturn(headers); - - Buffer body = Buffer.buffer("{}"); - - ArgumentCaptor> handlerCaptor = ArgumentCaptor.forClass(Handler.class); - when(response.bodyHandler(handlerCaptor.capture())).thenReturn(response); - - // Execute - logger.logResponse(response, false); - handlerCaptor.getValue().handle(body); - - // Verify headers were accessed - verify(response).headers(); - } - - @Test - void shouldHandleShortAuthorizationValue() { - // Setup + void shouldLogRequestWithNullBody() { when(config.logRequests()).thenReturn(true); + HttpClientRequest request = mockedRequest( + HttpMethod.GET, "https://ghcr.io/token", MultiMap.caseInsensitiveMultiMap()); - HttpClientRequest request = mock(HttpClientRequest.class); - MultiMap headers = MultiMap.caseInsensitiveMultiMap(); - headers.add(HttpHeaders.AUTHORIZATION, "abc"); - - when(request.getMethod()).thenReturn(HttpMethod.GET); - when(request.absoluteURI()).thenReturn("https://ghcr.io/token"); - when(request.headers()).thenReturn(headers); - - // Execute logger.logRequest(request, null, false); - // Should not throw exception - verify(request).headers(); + verify(request).getMethod(); } - @Test - void shouldHandleMultipleHeaders() { - // Setup + @ParameterizedTest + @ValueSource(strings = { "Bearer very-long-secret-token", "abc" }) + void shouldMaskAuthorizationHeader(String authValue) { when(config.logRequests()).thenReturn(true); - HttpClientRequest request = mock(HttpClientRequest.class); MultiMap headers = MultiMap.caseInsensitiveMultiMap(); - headers.add("Content-Type", "application/json"); - headers.add("Accept", "application/json"); - headers.add(HttpHeaders.AUTHORIZATION, "Bearer token123"); - - when(request.getMethod()).thenReturn(HttpMethod.POST); - when(request.absoluteURI()).thenReturn("https://ghcr.io/v2/test/tags/list"); - when(request.headers()).thenReturn(headers); + headers.add(HttpHeaders.AUTHORIZATION, authValue); + HttpClientRequest request = mockedRequest( + HttpMethod.GET, "https://ghcr.io/v2/test/tags/list", headers); - // Execute - logger.logRequest(request, Buffer.buffer("{}"), false); + logger.logRequest(request, null, false); - // Verify all headers were processed verify(request).headers(); } - @Test - void shouldHandleNonJsonBodyInResponse() { - // Setup - when(config.logResponses()).thenReturn(true); - - HttpClientResponse response = mock(HttpClientResponse.class); - MultiMap headers = MultiMap.caseInsensitiveMultiMap(); - - when(response.statusCode()).thenReturn(200); - when(response.headers()).thenReturn(headers); - - Buffer body = Buffer.buffer("Plain text response"); + private static class MockedResponse { + final HttpClientResponse response; + private final ArgumentCaptor> captor; - ArgumentCaptor> handlerCaptor = ArgumentCaptor.forClass(Handler.class); - when(response.bodyHandler(handlerCaptor.capture())).thenReturn(response); - - // Execute - logger.logResponse(response, false); - handlerCaptor.getValue().handle(body); + MockedResponse(HttpClientResponse response, ArgumentCaptor> captor) { + this.response = response; + this.captor = captor; + } - // Should handle non-JSON body without error - verify(response).statusCode(); + void triggerBody(Buffer body) { + captor.getValue().handle(body); + } } - @Test - void shouldHandleJsonArrayInResponse() { - // Setup - when(config.logResponses()).thenReturn(true); - + @SuppressWarnings("unchecked") + private MockedResponse mockedResponse(int statusCode, MultiMap headers) { HttpClientResponse response = mock(HttpClientResponse.class); - MultiMap headers = MultiMap.caseInsensitiveMultiMap(); - - when(response.statusCode()).thenReturn(200); + when(response.statusCode()).thenReturn(statusCode); when(response.headers()).thenReturn(headers); - Buffer body = Buffer.buffer("[{\"token\":\"secret1\"}, {\"token\":\"secret2\"}]"); - - ArgumentCaptor> handlerCaptor = ArgumentCaptor.forClass(Handler.class); - when(response.bodyHandler(handlerCaptor.capture())).thenReturn(response); + ArgumentCaptor> captor = ArgumentCaptor.forClass(Handler.class); + when(response.bodyHandler(captor.capture())).thenReturn(response); - // Execute - logger.logResponse(response, false); - handlerCaptor.getValue().handle(body); - - // Should mask tokens in JSON array - verify(response).statusCode(); + return new MockedResponse(response, captor); } - @Test - void shouldHandleRedirectResponse() { - // Setup - when(config.logResponses()).thenReturn(true); - - HttpClientResponse response = mock(HttpClientResponse.class); - MultiMap headers = MultiMap.caseInsensitiveMultiMap(); - headers.add("Location", "https://example.com/redirect"); - - when(response.statusCode()).thenReturn(302); - when(response.headers()).thenReturn(headers); - - Buffer body = Buffer.buffer(""); - - ArgumentCaptor> handlerCaptor = ArgumentCaptor.forClass(Handler.class); - when(response.bodyHandler(handlerCaptor.capture())).thenReturn(response); - - // Execute - logger.logResponse(response, true); - handlerCaptor.getValue().handle(body); - - // Should handle redirect - verify(response).statusCode(); + private HttpClientRequest mockedRequest(HttpMethod method, String uri, MultiMap headers) { + HttpClientRequest request = mock(HttpClientRequest.class); + when(request.getMethod()).thenReturn(method); + when(request.absoluteURI()).thenReturn(uri); + when(request.headers()).thenReturn(headers); + return request; } } \ No newline at end of file diff --git a/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/domain/TagsTestRequestTests.java b/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/domain/TagsTestRequestTests.java index 7f5c5b6e..3946a113 100644 --- a/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/domain/TagsTestRequestTests.java +++ b/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/domain/TagsTestRequestTests.java @@ -20,6 +20,16 @@ void shouldThrowExceptionWhenExecutorIsNull() { .hasMessage("executor cannot be null"); } + @Test + void shouldThrowExceptionWhenBuildingWithNullExecutor() { + assertThatThrownBy(() -> TagsTestRequest.builder() + .registry("ghcr.io") + .image("test-image") + .build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("executor cannot be null"); + } + @Test void shouldCreateRequestWithBuilder() { var request = TagsTestRequest.builder() @@ -30,12 +40,10 @@ void shouldCreateRequestWithBuilder() { .tags(List.of("v1.0.0", "v1.1.0")) .build(); - assertThat(request).isNotNull(); - assertThat(request.registry()).isEqualTo("ghcr.io"); - assertThat(request.image()).isEqualTo("docling-project/docling-serve"); - assertThat(request.executor()).isEqualTo(TEST_EXECUTOR); - assertThat(request.cleanupContainerImages()).isTrue(); - assertThat(request.tags()).containsExactly("v1.0.0", "v1.1.0"); + assertThat(request) + .isNotNull() + .usingRecursiveComparison() + .isEqualTo(new TagsTestRequest("ghcr.io", "docling-project/docling-serve", TEST_EXECUTOR, true, List.of("v1.0.0", "v1.1.0"))); } @Test @@ -44,17 +52,15 @@ void shouldCreateRequestWithMinimalBuilder() { .executor(TEST_EXECUTOR) .build(); - assertThat(request).isNotNull(); - assertThat(request.registry()).isNull(); - assertThat(request.image()).isNull(); - assertThat(request.executor()).isEqualTo(TEST_EXECUTOR); - assertThat(request.cleanupContainerImages()).isFalse(); - assertThat(request.tags()).isNull(); + assertThat(request) + .isNotNull() + .usingRecursiveComparison() + .isEqualTo(new TagsTestRequest(null, null, TEST_EXECUTOR, false, null)); } @Test void shouldConvertToBuilderAndModify() { - var originalRequest = TagsTestRequest.builder() + var original = TagsTestRequest.builder() .registry("ghcr.io") .image("docling-project/docling-serve") .executor(TEST_EXECUTOR) @@ -62,188 +68,19 @@ void shouldConvertToBuilderAndModify() { .tags(List.of("v1.0.0")) .build(); - var modifiedRequest = originalRequest.toBuilder() + var modified = original.toBuilder() .registry("quay.io") .cleanupContainerImages(true) .tags(List.of("v2.0.0")) .build(); - assertThat(originalRequest.registry()).isEqualTo("ghcr.io"); - assertThat(originalRequest.cleanupContainerImages()).isFalse(); - assertThat(originalRequest.tags()).containsExactly("v1.0.0"); - - assertThat(modifiedRequest.registry()).isEqualTo("quay.io"); - assertThat(modifiedRequest.image()).isEqualTo("docling-project/docling-serve"); - assertThat(modifiedRequest.executor()).isEqualTo(TEST_EXECUTOR); - assertThat(modifiedRequest.cleanupContainerImages()).isTrue(); - assertThat(modifiedRequest.tags()).containsExactly("v2.0.0"); - } - - @Test - void shouldCopyAllFieldsWithToBuilder() { - - var original = TagsTestRequest.builder() - .registry("docker.io") - .image("library/nginx") - .executor(TEST_EXECUTOR) - .cleanupContainerImages(true) - .tags(List.of("latest", "stable")) - .build(); - - var copy = original.toBuilder().build(); - - assertThat(copy.registry()).isEqualTo(original.registry()); - assertThat(copy.image()).isEqualTo(original.image()); - assertThat(copy.executor()).isEqualTo(original.executor()); - assertThat(copy.cleanupContainerImages()).isEqualTo(original.cleanupContainerImages()); - assertThat(copy.tags()).isEqualTo(original.tags()); - } - - @Test - void shouldSetRegistryInBuilder() { - var request = TagsTestRequest.builder() - .registry("custom-registry.io") - .executor(TEST_EXECUTOR) - .build(); - - assertThat(request.registry()).isEqualTo("custom-registry.io"); - } - - @Test - void shouldSetImageInBuilder() { - // Test image setter (line 54-57) - var request = TagsTestRequest.builder() - .image("my-app/my-service") - .executor(TEST_EXECUTOR) - .build(); - - assertThat(request.image()).isEqualTo("my-app/my-service"); - } - - @Test - void shouldSetCleanupContainerImagesInBuilder() { - var requestWithCleanup = TagsTestRequest.builder() - .cleanupContainerImages(true) - .executor(TEST_EXECUTOR) - .build(); - - var requestWithoutCleanup = TagsTestRequest.builder() - .cleanupContainerImages(false) - .executor(TEST_EXECUTOR) - .build(); - - assertThat(requestWithCleanup.cleanupContainerImages()).isTrue(); - assertThat(requestWithoutCleanup.cleanupContainerImages()).isFalse(); - } - - @Test - void shouldSetTagsInBuilder() { - var tags = List.of("v1.0.0", "v1.1.0", "latest"); - var request = TagsTestRequest.builder() - .tags(tags) - .executor(TEST_EXECUTOR) - .build(); - - assertThat(request.tags()).isEqualTo(tags); - } - - @Test - void shouldSetExecutorInBuilder() { - var customExecutor = Executors.newFixedThreadPool(2); - var request = TagsTestRequest.builder() - .executor(customExecutor) - .build(); - - assertThat(request.executor()).isEqualTo(customExecutor); - } - - @Test - void shouldThrowExceptionWhenBuildingWithNullExecutor() { - assertThatThrownBy(() -> TagsTestRequest.builder() - .registry("ghcr.io") - .image("test-image") - .build()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("executor cannot be null"); - } - - @Test - void shouldHandleEmptyTagsList() { - // Edge case: empty tags list - var request = TagsTestRequest.builder() - .tags(List.of()) - .executor(TEST_EXECUTOR) - .build(); - - assertThat(request.tags()).isEmpty(); - } - - @Test - void shouldHandleNullTagsList() { - // Edge case: null tags list - var request = TagsTestRequest.builder() - .tags(null) - .executor(TEST_EXECUTOR) - .build(); - - assertThat(request.tags()).isNull(); - } - - @Test - void shouldHandleNullRegistry() { - // Edge case: null registry - var request = TagsTestRequest.builder() - .registry(null) - .executor(TEST_EXECUTOR) - .build(); - - assertThat(request.registry()).isNull(); - } - - @Test - void shouldHandleNullImage() { - // Edge case: null image - var request = TagsTestRequest.builder() - .image(null) - .executor(TEST_EXECUTOR) - .build(); - - assertThat(request.image()).isNull(); - } - - @Test - void shouldChainBuilderMethods() { - // Test method chaining - var request = TagsTestRequest.builder() - .registry("ghcr.io") - .image("test-image") - .executor(TEST_EXECUTOR) - .tags(List.of("v1.0.0")) - .cleanupContainerImages(true) - .build(); - - assertThat(request.registry()).isEqualTo("ghcr.io"); - assertThat(request.image()).isEqualTo("test-image"); - assertThat(request.executor()).isEqualTo(TEST_EXECUTOR); - assertThat(request.tags()).containsExactly("v1.0.0"); - assertThat(request.cleanupContainerImages()).isTrue(); - } - - @Test - void shouldOverrideBuilderValues() { - // Test that builder values can be overridden - var request = TagsTestRequest.builder() - .registry("first-registry") - .registry("second-registry") - .image("first-image") - .image("second-image") - .cleanupContainerImages(false) - .cleanupContainerImages(true) - .executor(TEST_EXECUTOR) - .build(); + assertThat(original.registry()).isEqualTo("ghcr.io"); + assertThat(original.cleanupContainerImages()).isFalse(); + assertThat(original.tags()).containsExactly("v1.0.0"); - assertThat(request.registry()).isEqualTo("second-registry"); - assertThat(request.image()).isEqualTo("second-image"); - assertThat(request.cleanupContainerImages()).isTrue(); + assertThat(modified) + .isNotNull() + .usingRecursiveComparison() + .isEqualTo(new TagsTestRequest("quay.io", "docling-project/docling-serve", TEST_EXECUTOR, true, List.of("v2.0.0"))); } -} \ No newline at end of file +} diff --git a/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/service/NamedThreadFactoryTests.java b/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/service/NamedThreadFactoryTests.java index 1d3994b7..a88a3fc7 100644 --- a/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/service/NamedThreadFactoryTests.java +++ b/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/service/NamedThreadFactoryTests.java @@ -2,105 +2,22 @@ import static org.assertj.core.api.Assertions.assertThat; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; class NamedThreadFactoryTests { - @Test - void shouldCreateThreadWithName() { - var factory = new NamedThreadFactory("test-thread"); - Runnable runnable = () -> { - }; + @ParameterizedTest(name = "prefix=\"{0}\"") + @ValueSource(strings = { "test-thread", "test-worker_pool", "" }) + void shouldCreateThreadWithExpectedName(String prefix) { + var factory = new NamedThreadFactory(prefix); - Thread thread = factory.newThread(runnable); + Thread thread = factory.newThread(() -> {}); - assertThat(thread).isNotNull(); - assertThat(thread.getName()).isEqualTo("test-thread-1"); + assertThat(thread) + .isNotNull() + .extracting(Thread::getName) + .asString() + .startsWith(prefix); } - - @Test - void shouldIncrementThreadNumber() { - var factory = new NamedThreadFactory("worker"); - Runnable runnable = () -> { - }; - - Thread thread1 = factory.newThread(runnable); - Thread thread2 = factory.newThread(runnable); - Thread thread3 = factory.newThread(runnable); - - assertThat(thread1.getName()).isEqualTo("worker-1"); - assertThat(thread2.getName()).isEqualTo("worker-2"); - assertThat(thread3.getName()).isEqualTo("worker-3"); - } - - @Test - void shouldCreateThreadWithDifferentNames() { - var factory1 = new NamedThreadFactory("pool-1"); - var factory2 = new NamedThreadFactory("pool-2"); - Runnable runnable = () -> { - }; - - Thread thread1 = factory1.newThread(runnable); - Thread thread2 = factory2.newThread(runnable); - - assertThat(thread1.getName()).isEqualTo("pool-1-1"); - assertThat(thread2.getName()).isEqualTo("pool-2-1"); - } - - @Test - void shouldCreateThreadWithRunnable() { - var factory = new NamedThreadFactory("executor"); - var executed = new boolean[] { false }; - Runnable runnable = () -> executed[0] = true; - - Thread thread = factory.newThread(runnable); - thread.start(); - - try { - thread.join(1000); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - - assertThat(executed[0]).isTrue(); - } - - @Test - void shouldHandleSpecialCharactersInName() { - var factory = new NamedThreadFactory("test-worker_pool"); - Runnable runnable = () -> { - }; - - Thread thread = factory.newThread(runnable); - - assertThat(thread.getName()).isEqualTo("test-worker_pool-1"); - } - - @Test - void shouldHandleEmptyName() { - var factory = new NamedThreadFactory(""); - Runnable runnable = () -> { - }; - - Thread thread = factory.newThread(runnable); - - assertThat(thread.getName()).isEqualTo("-1"); - } - - @Test - void shouldCreateMultipleThreadsConcurrently() { - var factory = new NamedThreadFactory("concurrent"); - Runnable runnable = () -> { - }; - - Thread[] threads = new Thread[10]; - for (int i = 0; i < 10; i++) { - threads[i] = factory.newThread(runnable); - } - - // All threads should have unique numbers - for (int i = 0; i < 10; i++) { - assertThat(threads[i].getName()).matches("concurrent-\\d+"); - } - } -} \ No newline at end of file +} diff --git a/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/service/WorkParallelizerTests.java b/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/service/WorkParallelizerTests.java index 7de8ab95..eebaa57f 100644 --- a/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/service/WorkParallelizerTests.java +++ b/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/service/WorkParallelizerTests.java @@ -6,184 +6,102 @@ import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.api.Test; -class WorkParallelizerTests { +import java.util.concurrent.ExecutorService; - @Test - void shouldTransformItemsInParallel() { - var executor = Executors.newFixedThreadPool(4); - var items = List.of(1, 2, 3, 4, 5); +class WorkParallelizerTests { - var results = WorkParallelizer.transformInParallelAndWait( - executor, - items, - item -> item * 2); + private ExecutorService executor; - assertThat(results) - .hasSize(5) - .containsExactlyInAnyOrder(2, 4, 6, 8, 10); + @BeforeEach + void setUp() { + executor = Executors.newFixedThreadPool(4); + } + @AfterEach + void tearDown() { executor.shutdown(); } - @Test - void shouldTransformEmptyList() { - var executor = Executors.newSingleThreadExecutor(); - List items = List.of(); + static Stream transformCases() { + return Stream.of( + Arguments.of( + "integers doubled", + List.of(1, 2, 3, 4, 5), + (java.util.function.Function) (i -> i * 2), + List.of(2, 4, 6, 8, 10)), + Arguments.of( + "string to upper-case", + List.of("test"), + (java.util.function.Function) String::toUpperCase, + List.of("TEST")), + Arguments.of( + "string lengths", + List.of("apple", "banana", "cherry"), + (java.util.function.Function) String::length, + List.of(5, 6, 6)), + Arguments.of( + "int to labelled string", + List.of(1, 2, 3), + (java.util.function.Function) (i -> "Number: " + i), + List.of("Number: 1", "Number: 2", "Number: 3"))); + } - var results = WorkParallelizer.transformInParallelAndWait( - executor, - items, - item -> item * 2); + @ParameterizedTest(name = "{0}") + @MethodSource("transformCases") + void shouldTransformItemsInParallel( + String label, + List items, + java.util.function.Function mapper, + List expectedItems) { - assertThat(results).isEmpty(); + var results = WorkParallelizer.transformInParallelAndWait(executor, items, mapper); - executor.shutdown(); + assertThat(results) + .hasSize(expectedItems.size()) + .containsExactlyInAnyOrderElementsOf(expectedItems); } @Test - void shouldTransformSingleItem() { - var executor = Executors.newSingleThreadExecutor(); - var items = List.of("test"); - - var results = WorkParallelizer.transformInParallelAndWait( - executor, - items, - String::toUpperCase); + void shouldTransformEmptyList() { + List items = List.of(); - assertThat(results) - .hasSize(1) - .containsExactly("TEST"); + var results = WorkParallelizer.transformInParallelAndWait(executor, items, i -> i * 2); - executor.shutdown(); + assertThat(results).isEmpty(); } @Test void shouldRunItemsInParallel() { - var executor = Executors.newFixedThreadPool(4); var items = List.of(1, 2, 3, 4, 5); - var processedItems = new ArrayList(); - - WorkParallelizer.runInParallelAndWait( - executor, - items, - item -> { - synchronized (processedItems) { - processedItems.add(item); - } - }); - - assertThat(processedItems) + var collected = new ArrayList(); + + WorkParallelizer.runInParallelAndWait(executor, items, item -> { + synchronized (collected) { + collected.add(item); + } + }); + + assertThat(collected) .hasSize(5) .containsExactlyInAnyOrder(1, 2, 3, 4, 5); - - executor.shutdown(); } @Test void shouldRunEmptyList() { - var executor = Executors.newSingleThreadExecutor(); - List items = List.of(); - var counter = new AtomicInteger(0); - - WorkParallelizer.runInParallelAndWait( - executor, - items, - item -> counter.incrementAndGet()); - - assertThat(counter.get()).isZero(); - - executor.shutdown(); - } - - @Test - void shouldRunSingleItem() { - var executor = Executors.newSingleThreadExecutor(); - var items = List.of("test"); var counter = new AtomicInteger(0); - WorkParallelizer.runInParallelAndWait( - executor, - items, - item -> counter.incrementAndGet()); - - assertThat(counter.get()).isEqualTo(1); - - executor.shutdown(); - } - - @Test - void shouldHandleComplexTransformations() { - var executor = Executors.newFixedThreadPool(2); - var items = List.of("apple", "banana", "cherry"); - - var results = WorkParallelizer.transformInParallelAndWait( - executor, - items, - item -> item.length()); - - assertThat(results) - .hasSize(3) - .containsExactlyInAnyOrder(5, 6, 6); + WorkParallelizer.runInParallelAndWait(executor, List.of(), item -> counter.incrementAndGet()); - executor.shutdown(); + assertThat(counter).hasValue(0); } - @Test - void shouldExecuteInParallelWithMultipleThreads() { - var executor = Executors.newFixedThreadPool(10); - var items = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); - var threadIds = new ArrayList(); - - WorkParallelizer.runInParallelAndWait( - executor, - items, - item -> { - synchronized (threadIds) { - threadIds.add(Thread.currentThread().getId()); - } - }); - - // Should use multiple threads - assertThat(threadIds).hasSizeGreaterThanOrEqualTo(1); - - executor.shutdown(); - } - - @Test - void shouldTransformWithDifferentTypes() { - var executor = Executors.newFixedThreadPool(2); - var items = List.of(1, 2, 3); - - var results = WorkParallelizer.transformInParallelAndWait( - executor, - items, - item -> "Number: " + item); - - assertThat(results) - .hasSize(3) - .containsExactlyInAnyOrder("Number: 1", "Number: 2", "Number: 3"); - - executor.shutdown(); - } - - @Test - void shouldHandleLargeNumberOfItems() { - var executor = Executors.newFixedThreadPool(4); - var items = new ArrayList(); - for (int i = 0; i < 100; i++) { - items.add(i); - } - - var results = WorkParallelizer.transformInParallelAndWait( - executor, - items, - item -> item + 1); - - assertThat(results).hasSize(100); - - executor.shutdown(); - } -} \ No newline at end of file +} From 55d2b479057210d809756a7eff27dbbe67f6a94d Mon Sep 17 00:00:00 2001 From: Margi <39212098+margi212@users.noreply.github.com> Date: Wed, 8 Jul 2026 14:29:39 +0530 Subject: [PATCH 3/3] test: rewrite shouldRunItemsInParallel to use AtomicInteger counter pattern Signed-off-by: Margi <39212098+margi212@users.noreply.github.com> --- .../tester/service/WorkParallelizerTests.java | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/service/WorkParallelizerTests.java b/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/service/WorkParallelizerTests.java index eebaa57f..c69a4e2c 100644 --- a/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/service/WorkParallelizerTests.java +++ b/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/service/WorkParallelizerTests.java @@ -2,7 +2,6 @@ import static org.assertj.core.api.Assertions.assertThat; -import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; @@ -81,18 +80,11 @@ void shouldTransformEmptyList() { @Test void shouldRunItemsInParallel() { - var items = List.of(1, 2, 3, 4, 5); - var collected = new ArrayList(); - - WorkParallelizer.runInParallelAndWait(executor, items, item -> { - synchronized (collected) { - collected.add(item); - } - }); - - assertThat(collected) - .hasSize(5) - .containsExactlyInAnyOrder(1, 2, 3, 4, 5); + var counter = new AtomicInteger(0); + + WorkParallelizer.runInParallelAndWait(executor, List.of(1, 2, 3, 4, 5), item -> counter.incrementAndGet()); + + assertThat(counter).hasValue(5); } @Test