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..3f29c9e0 --- /dev/null +++ b/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/client/RegistryClientFactoryTests.java @@ -0,0 +1,41 @@ +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 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; + +class RegistryClientFactoryTests { + + private GHCRClient ghcrClient; + private RegistryClientFactory factory; + + @BeforeEach + void setUp() { + ghcrClient = mock(GHCRClient.class); + factory = new RegistryClientFactory(ghcrClient); + } + + @Test + void shouldReturnGHCRClientForGhcrRegistry() { + assertThat(factory.getRegistryClient("ghcr.io")) + .isNotNull() + .isSameAs(ghcrClient); + } + + @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: " + registry); + } +} 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..b174e35e --- /dev/null +++ b/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/client/ghcr/GHCRClientLoggerTests.java @@ -0,0 +1,176 @@ +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.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +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 shouldNotLogResponseWhenDisabled() { + when(config.logResponses()).thenReturn(false); + HttpClientResponse response = mock(HttpClientResponse.class); + + logger.logResponse(response, false); + + verify(response, never()).bodyHandler(any()); + } + + @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()); + + logger.logResponse(mr.response, false); + mr.triggerBody(Buffer.buffer(rawBody)); + + verify(mr.response).statusCode(); + verify(mr.response).headers(); + } + + @Test + void shouldHandleNullBodyInResponse() { + when(config.logResponses()).thenReturn(true); + MockedResponse mr = mockedResponse(204, MultiMap.caseInsensitiveMultiMap()); + + logger.logResponse(mr.response, false); + mr.triggerBody(null); + + verify(mr.response).statusCode(); + } + + @Test + void shouldMaskSetCookieHeaderInResponse() { + when(config.logResponses()).thenReturn(true); + + MultiMap headers = MultiMap.caseInsensitiveMultiMap(); + headers.add(HttpHeaders.SET_COOKIE, "session=very-long-session-id"); + MockedResponse mr = mockedResponse(200, headers); + + logger.logResponse(mr.response, false); + mr.triggerBody(Buffer.buffer("{}")); + + verify(mr.response).headers(); + } + + @Test + void shouldNotLogRequestWhenDisabled() { + when(config.logRequests()).thenReturn(false); + HttpClientRequest request = mock(HttpClientRequest.class); + + logger.logRequest(request, Buffer.buffer("{\"test\":\"data\"}"), false); + + verify(request, never()).getMethod(); + verify(request, never()).absoluteURI(); + } + + @Test + void shouldLogRequestWhenEnabled() { + when(config.logRequests()).thenReturn(true); + + MultiMap headers = MultiMap.caseInsensitiveMultiMap(); + headers.add("Content-Type", "application/json"); + HttpClientRequest request = mockedRequest(HttpMethod.GET, "https://ghcr.io/v2/test/tags/list", headers); + + logger.logRequest(request, Buffer.buffer("{\"test\":\"data\"}"), false); + + verify(request).getMethod(); + verify(request).absoluteURI(); + verify(request).headers(); + } + + @Test + void shouldLogRequestWithNullBody() { + when(config.logRequests()).thenReturn(true); + HttpClientRequest request = mockedRequest( + HttpMethod.GET, "https://ghcr.io/token", MultiMap.caseInsensitiveMultiMap()); + + logger.logRequest(request, null, false); + + verify(request).getMethod(); + } + + @ParameterizedTest + @ValueSource(strings = { "Bearer very-long-secret-token", "abc" }) + void shouldMaskAuthorizationHeader(String authValue) { + when(config.logRequests()).thenReturn(true); + + MultiMap headers = MultiMap.caseInsensitiveMultiMap(); + headers.add(HttpHeaders.AUTHORIZATION, authValue); + HttpClientRequest request = mockedRequest( + HttpMethod.GET, "https://ghcr.io/v2/test/tags/list", headers); + + logger.logRequest(request, null, false); + + verify(request).headers(); + } + + private static class MockedResponse { + final HttpClientResponse response; + private final ArgumentCaptor> captor; + + MockedResponse(HttpClientResponse response, ArgumentCaptor> captor) { + this.response = response; + this.captor = captor; + } + + void triggerBody(Buffer body) { + captor.getValue().handle(body); + } + } + + @SuppressWarnings("unchecked") + private MockedResponse mockedResponse(int statusCode, MultiMap headers) { + HttpClientResponse response = mock(HttpClientResponse.class); + when(response.statusCode()).thenReturn(statusCode); + when(response.headers()).thenReturn(headers); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(Handler.class); + when(response.bodyHandler(captor.capture())).thenReturn(response); + + return new MockedResponse(response, captor); + } + + 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/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..3946a113 --- /dev/null +++ b/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/domain/TagsTestRequestTests.java @@ -0,0 +1,86 @@ +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 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() + .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() + .usingRecursiveComparison() + .isEqualTo(new TagsTestRequest("ghcr.io", "docling-project/docling-serve", TEST_EXECUTOR, true, List.of("v1.0.0", "v1.1.0"))); + } + + @Test + void shouldCreateRequestWithMinimalBuilder() { + var request = TagsTestRequest.builder() + .executor(TEST_EXECUTOR) + .build(); + + assertThat(request) + .isNotNull() + .usingRecursiveComparison() + .isEqualTo(new TagsTestRequest(null, null, TEST_EXECUTOR, false, null)); + } + + @Test + void shouldConvertToBuilderAndModify() { + var original = TagsTestRequest.builder() + .registry("ghcr.io") + .image("docling-project/docling-serve") + .executor(TEST_EXECUTOR) + .cleanupContainerImages(false) + .tags(List.of("v1.0.0")) + .build(); + + var modified = original.toBuilder() + .registry("quay.io") + .cleanupContainerImages(true) + .tags(List.of("v2.0.0")) + .build(); + + assertThat(original.registry()).isEqualTo("ghcr.io"); + assertThat(original.cleanupContainerImages()).isFalse(); + assertThat(original.tags()).containsExactly("v1.0.0"); + + assertThat(modified) + .isNotNull() + .usingRecursiveComparison() + .isEqualTo(new TagsTestRequest("quay.io", "docling-project/docling-serve", TEST_EXECUTOR, true, List.of("v2.0.0"))); + } +} 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..a88a3fc7 --- /dev/null +++ b/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/service/NamedThreadFactoryTests.java @@ -0,0 +1,23 @@ +package ai.docling.client.tester.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class NamedThreadFactoryTests { + + @ParameterizedTest(name = "prefix=\"{0}\"") + @ValueSource(strings = { "test-thread", "test-worker_pool", "" }) + void shouldCreateThreadWithExpectedName(String prefix) { + var factory = new NamedThreadFactory(prefix); + + Thread thread = factory.newThread(() -> {}); + + assertThat(thread) + .isNotNull() + .extracting(Thread::getName) + .asString() + .startsWith(prefix); + } +} 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..c69a4e2c --- /dev/null +++ b/docling-testing/docling-version-tests/src/test/java/ai/docling/client/tester/service/WorkParallelizerTests.java @@ -0,0 +1,99 @@ +package ai.docling.client.tester.service; + +import static org.assertj.core.api.Assertions.assertThat; + +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; + +import java.util.concurrent.ExecutorService; + +class WorkParallelizerTests { + + private ExecutorService executor; + + @BeforeEach + void setUp() { + executor = Executors.newFixedThreadPool(4); + } + + @AfterEach + void tearDown() { + executor.shutdown(); + } + + 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"))); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("transformCases") + void shouldTransformItemsInParallel( + String label, + List items, + java.util.function.Function mapper, + List expectedItems) { + + var results = WorkParallelizer.transformInParallelAndWait(executor, items, mapper); + + assertThat(results) + .hasSize(expectedItems.size()) + .containsExactlyInAnyOrderElementsOf(expectedItems); + } + + @Test + void shouldTransformEmptyList() { + List items = List.of(); + + var results = WorkParallelizer.transformInParallelAndWait(executor, items, i -> i * 2); + + assertThat(results).isEmpty(); + } + + @Test + void shouldRunItemsInParallel() { + var counter = new AtomicInteger(0); + + WorkParallelizer.runInParallelAndWait(executor, List.of(1, 2, 3, 4, 5), item -> counter.incrementAndGet()); + + assertThat(counter).hasValue(5); + } + + @Test + void shouldRunEmptyList() { + var counter = new AtomicInteger(0); + + WorkParallelizer.runInParallelAndWait(executor, List.of(), item -> counter.incrementAndGet()); + + assertThat(counter).hasValue(0); + } + +}