From d37fef84a532943a5d3175754b992a31b05abc27 Mon Sep 17 00:00:00 2001 From: Margi <39212098+margi212@users.noreply.github.com> Date: Sat, 27 Jun 2026 10:09:28 +0530 Subject: [PATCH] add test cases. --- .../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