From 14f6803444a1da5b72431174c1e2b9f7ea6dc7a2 Mon Sep 17 00:00:00 2001 From: Nick Date: Mon, 6 Jul 2026 10:55:37 +0300 Subject: [PATCH 1/2] Issue-135. feat: store test results in a JSONL file --- java-reporter-core/pom.xml | 2 +- .../core/batch/BatchResultManager.java | 36 +++++++ .../io/testomat/core/client/ApiInterface.java | 2 + .../core/client/TestomatioClient.java | 41 +++++++ .../request/NativeRequestBodyBuilder.java | 63 +++++++++-- .../core/constants/ArtifactPropertyNames.java | 3 + .../core/constants/CommonConstants.java | 2 +- .../constants/PropertyValuesConstants.java | 1 + .../io/testomat/core/facade/Testomatio.java | 9 -- .../TempArtifactDirectoriesStorage.java | 13 ++- .../methods/artifact/client/AwsService.java | 28 +++-- .../methods/artifact/client/JsonlService.java | 44 ++++++++ .../artifact/client/S3ClientFactory.java | 2 +- .../artifact/model/AddTestsBatchRequest.java | 59 ++++++++++ .../facade/methods/artifact/model/Step.java | 62 +++++++++++ .../methods/artifact/model/TestFile.java | 22 ++++ .../methods/artifact/model/TestItem.java | 44 ++++++++ .../util/DefaultPropertiesStorage.java | 6 +- .../core/runmanager/GlobalRunManager.java | 36 ++++++- .../java/io/testomat/core/step/StepData.java | 21 ++++ .../TempArtifactDirectoriesStorageTest.java | 102 ++++++++++-------- .../core/batch/BatchResultManagerTest.java | 74 ++++++++++++- .../request/NativeRequestBodyBuilderTest.java | 96 +++++++++++++++++ .../core/runmanager/GlobalRunManagerTest.java | 49 +++++++++ java-reporter-cucumber/pom.xml | 4 +- .../listener/FacadeFunctionsHandler.java | 59 +++++++++- java-reporter-junit/pom.xml | 4 +- .../junit/listener/AbstractHookContainer.java | 74 +++++++++++++ .../listener/FacadeFunctionsHandler.java | 31 +++++- .../testomat/junit/listener/TestomatHook.java | 70 ++++++++++++ java-reporter-karate/pom.xml | 4 +- .../karate/hooks/FacadeFunctionsHandler.java | 69 ++++++++++-- .../hooks/FacadeFunctionsHandlerTest.java | 8 +- java-reporter-testng/pom.xml | 4 +- .../listener/AbstractHooksContainer.java | 74 +++++++++++++ .../listener/FacadeFunctionsHandler.java | 30 +++++- .../testng/listener/TestomatHook.java | 88 +++++++++++++++ pom.xml | 2 +- testomat-allure-adapter/pom.xml | 4 +- 39 files changed, 1240 insertions(+), 102 deletions(-) create mode 100644 java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/client/JsonlService.java create mode 100644 java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/model/AddTestsBatchRequest.java create mode 100644 java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/model/Step.java create mode 100644 java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/model/TestFile.java create mode 100644 java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/model/TestItem.java create mode 100644 java-reporter-core/src/main/java/io/testomat/core/step/StepData.java create mode 100644 java-reporter-junit/src/main/java/io/testomat/junit/listener/TestomatHook.java create mode 100644 java-reporter-testng/src/main/java/io/testomat/testng/listener/TestomatHook.java diff --git a/java-reporter-core/pom.xml b/java-reporter-core/pom.xml index 97da840d..e1e1bcd7 100644 --- a/java-reporter-core/pom.xml +++ b/java-reporter-core/pom.xml @@ -7,7 +7,7 @@ io.testomat java-reporter-core - 0.14.0 + 0.15.0 jar Testomat.io Reporter Core diff --git a/java-reporter-core/src/main/java/io/testomat/core/batch/BatchResultManager.java b/java-reporter-core/src/main/java/io/testomat/core/batch/BatchResultManager.java index 516ba536..5b29c80c 100644 --- a/java-reporter-core/src/main/java/io/testomat/core/batch/BatchResultManager.java +++ b/java-reporter-core/src/main/java/io/testomat/core/batch/BatchResultManager.java @@ -4,10 +4,15 @@ import static io.testomat.core.constants.PropertyValuesConstants.DEFAULT_FLUSH_INTERVAL_SECONDS; import io.testomat.core.client.ApiInterface; +import io.testomat.core.facade.methods.artifact.model.AddTestsBatchRequest; +import io.testomat.core.facade.methods.artifact.model.Step; +import io.testomat.core.facade.methods.artifact.model.TestItem; import io.testomat.core.model.TestResult; import java.io.IOException; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -35,6 +40,9 @@ public class BatchResultManager { private final ScheduledExecutorService scheduler; private final AtomicBoolean isActive = new AtomicBoolean(true); + private final Map testItems = new LinkedHashMap<>(); + private final Object testItemsLock = new Object(); + /** * Creates batch result manager with configurable settings. * Initializes batch size and flush interval from properties with fallback to defaults. @@ -83,6 +91,34 @@ public synchronized void addResult(TestResult result) { } } + public void addTestItem(TestItem testItem) { + synchronized (testItemsLock) { + testItems.put(testItem.getRid(), testItem); + } + } + + public AddTestsBatchRequest buildRequest() { + synchronized (testItemsLock) { + return AddTestsBatchRequest.builder(runUid, "addTestsBatch") + .addTests(new ArrayList<>(testItems.values())) + .build(); + } + } + + public void updateTestSteps(String rid, List steps) { + synchronized (testItemsLock) { + TestItem item = testItems.get(rid); + + if (item == null) { + log.warn("TestItem not found for rid={}", rid); + return; + } + + item.setSteps(steps); + log.info("Updated steps for {}", rid); + } + } + /** * Immediately flushes all pending test results to API. * Thread-safe operation that can be called concurrently. diff --git a/java-reporter-core/src/main/java/io/testomat/core/client/ApiInterface.java b/java-reporter-core/src/main/java/io/testomat/core/client/ApiInterface.java index 6da1bed0..8680c8da 100644 --- a/java-reporter-core/src/main/java/io/testomat/core/client/ApiInterface.java +++ b/java-reporter-core/src/main/java/io/testomat/core/client/ApiInterface.java @@ -41,6 +41,8 @@ public interface ApiInterface { void sendTestWithArtifacts(String uid); + void writeArtifactsToJsonl(String uid); + /** * Finalizes the test run and marks it as completed. * diff --git a/java-reporter-core/src/main/java/io/testomat/core/client/TestomatioClient.java b/java-reporter-core/src/main/java/io/testomat/core/client/TestomatioClient.java index d375e091..dbee8605 100644 --- a/java-reporter-core/src/main/java/io/testomat/core/client/TestomatioClient.java +++ b/java-reporter-core/src/main/java/io/testomat/core/client/TestomatioClient.java @@ -1,5 +1,6 @@ package io.testomat.core.client; +import static io.testomat.core.constants.ArtifactPropertyNames.JSONL_PATH_PROPERTY_NAME; import static io.testomat.core.constants.CommonConstants.RESPONSE_UID_KEY; import io.testomat.core.InfoDisplay; @@ -17,8 +18,16 @@ import io.testomat.core.exception.FinishReportFailedException; import io.testomat.core.exception.ReportingFailedException; import io.testomat.core.exception.RunCreationFailedException; +import io.testomat.core.facade.methods.artifact.model.AddTestsBatchRequest; import io.testomat.core.model.TestResult; +import io.testomat.core.propertyconfig.impl.PropertyProviderFactoryImpl; +import io.testomat.core.propertyconfig.interf.PropertyProvider; +import io.testomat.core.runmanager.GlobalRunManager; +import java.io.FileWriter; import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.List; import java.util.Map; import org.slf4j.Logger; @@ -40,6 +49,7 @@ public class TestomatioClient implements ApiInterface { private final CredentialsManager credentialsManager = new CredentialsManager(); private final LinkUploadBodyBuilder linkUploadBodyBuilder = new LinkUploadBodyBuilder(); private final CredentialsValidationService credentialsValidationService = new CredentialsValidationService(); + private final PropertyProvider provider; /** * Creates API client with injectable dependencies. @@ -55,6 +65,8 @@ public TestomatioClient(String apiKey, this.apiKey = apiKey; this.client = client; this.requestBodyBuilder = requestBodyBuilder; + this.provider = + PropertyProviderFactoryImpl.getPropertyProviderFactory().getPropertyProvider(); } @Override @@ -132,6 +144,35 @@ public void sendTestWithArtifacts(String uid) { } } + @Override + public void writeArtifactsToJsonl(String uid) { + AddTestsBatchRequest request = + GlobalRunManager.getInstance().buildBatchRequest(); + if (request == null || request.getTests() == null || request.getTests().isEmpty()) { + return; + } + String filePath = provider.getProperty(JSONL_PATH_PROPERTY_NAME); + Path path = Path.of(filePath); + try { + Files.createDirectories(path.getParent()); + try (PrintWriter writer = new PrintWriter(Files.newBufferedWriter(path))) { + writer.println(toJson(request)); + log.debug("Wrote {} entries to {}", request.getTests().size(), path); + } + } catch (IOException e) { + log.error("Failed to write JSON file {}", path, e); + } + } + + private String toJson(Object value) { + try { + return new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(value); + } catch (com.fasterxml.jackson.core.JsonProcessingException e) { + log.error("Failed to serialize JSON", e); + return "{}"; + } + } + @Override public void finishTestRun(String uid, float duration) { try { diff --git a/java-reporter-core/src/main/java/io/testomat/core/client/request/NativeRequestBodyBuilder.java b/java-reporter-core/src/main/java/io/testomat/core/client/request/NativeRequestBodyBuilder.java index 279b29e2..86c21e28 100644 --- a/java-reporter-core/src/main/java/io/testomat/core/client/request/NativeRequestBodyBuilder.java +++ b/java-reporter-core/src/main/java/io/testomat/core/client/request/NativeRequestBodyBuilder.java @@ -15,6 +15,7 @@ import io.testomat.core.exception.FailedToCreateRunBodyException; import io.testomat.core.facade.methods.artifact.ReportedTestStorage; import io.testomat.core.facade.methods.artifact.TempArtifactDirectoriesStorage; +import io.testomat.core.facade.methods.artifact.model.Step; import io.testomat.core.facade.methods.label.LabelStorage; import io.testomat.core.facade.methods.logmethod.LogStorage; import io.testomat.core.facade.methods.meta.MetaStorage; @@ -22,8 +23,11 @@ import io.testomat.core.model.TestResult; import io.testomat.core.propertyconfig.impl.PropertyProviderFactoryImpl; import io.testomat.core.propertyconfig.interf.PropertyProvider; +import io.testomat.core.runmanager.GlobalRunManager; +import io.testomat.core.step.StepData; import io.testomat.core.step.TestStep; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -31,6 +35,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.UUID; import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -175,7 +180,15 @@ private Map buildTestResultMap(TestResult result) throws JsonPro result.getSteps().forEach(this::processStepArtifacts); List> stepsMap = convertStepsToMap(result.getSteps()); body.put("steps", stepsMap); - log.debug("Adding {} steps to request body for test: {}", result.getSteps().size(), result.getTitle()); + + GlobalRunManager.getInstance().updateTestSteps( + result.getRid(), + convertToJsonlSteps(result.getSteps()) + ); + + log.debug("Adding {} steps to request body for test: {}", + result.getSteps().size(), + result.getTitle()); } if (createParam) { @@ -394,16 +407,54 @@ private void addLinks(TestResult result, String rid) { * @param step step to process */ private void processStepArtifacts(TestStep step) { - List links = - TempArtifactDirectoriesStorage.STEP_DIRECTORIES - .remove(step.getId()); + StepData stepData = null; + + for (Map steps : TempArtifactDirectoriesStorage.STEP_DATA.values()) { + stepData = steps.get(step.getId()); + if (stepData != null) { + break; + } + } - if (links != null && !links.isEmpty()) { - step.setArtifacts(links.toArray(new String[0])); + if (stepData != null && !stepData.getArtifacts().isEmpty()) { + step.setArtifacts(stepData.getArtifacts().toArray(new String[0])); } if (step.getSubsteps() != null && !step.getSubsteps().isEmpty()) { step.getSubsteps().forEach(this::processStepArtifacts); } } + + private List convertToJsonlSteps(List steps) { + return steps.stream() + .map(this::convertToJsonlStep) + .collect(Collectors.toList()); + } + + private Step convertToJsonlStep(TestStep step) { + StepData stepData = null; + + for (Map steps : TempArtifactDirectoriesStorage.STEP_DATA.values()) { + stepData = steps.remove(step.getId()); + if (stepData != null) { + break; + } + } + + List substeps = null; + if (step.getSubsteps() != null && !step.getSubsteps().isEmpty()) { + substeps = convertToJsonlSteps(step.getSubsteps()); + } + + return new Step( + step.getStepTitle(), + step.getStatus(), + step.getLog(), + step.getError(), + step.getDuration(), + step.getCategory(), + stepData != null ? stepData.getDirectories() : null, + substeps + ); + } } \ No newline at end of file diff --git a/java-reporter-core/src/main/java/io/testomat/core/constants/ArtifactPropertyNames.java b/java-reporter-core/src/main/java/io/testomat/core/constants/ArtifactPropertyNames.java index 0f08cc87..8ad62f3e 100644 --- a/java-reporter-core/src/main/java/io/testomat/core/constants/ArtifactPropertyNames.java +++ b/java-reporter-core/src/main/java/io/testomat/core/constants/ArtifactPropertyNames.java @@ -16,4 +16,7 @@ public class ArtifactPropertyNames { public static final String MAX_SIZE_ARTIFACTS_PROPERTY_NAME = "testomatio.artifact.max-size"; public static final String STEP_ARTIFACT_ENABLED_PROPERTY_NAME = "testomatio.step.artifacts.enabled"; + + public static final String JSONL_EXPORT_PROPERTY_NAME = "testomatio.artifact.json.disable"; + public static final String JSONL_PATH_PROPERTY_NAME = "testomatio.artifact.json.path"; } diff --git a/java-reporter-core/src/main/java/io/testomat/core/constants/CommonConstants.java b/java-reporter-core/src/main/java/io/testomat/core/constants/CommonConstants.java index 9826cc7e..95b07d1c 100644 --- a/java-reporter-core/src/main/java/io/testomat/core/constants/CommonConstants.java +++ b/java-reporter-core/src/main/java/io/testomat/core/constants/CommonConstants.java @@ -1,7 +1,7 @@ package io.testomat.core.constants; public class CommonConstants { - public static final String REPORTER_VERSION = "0.14.0"; + public static final String REPORTER_VERSION = "0.15.0"; public static final String TESTS_STRING = "tests"; public static final String API_KEY_STRING = "api_key"; diff --git a/java-reporter-core/src/main/java/io/testomat/core/constants/PropertyValuesConstants.java b/java-reporter-core/src/main/java/io/testomat/core/constants/PropertyValuesConstants.java index 8f10fa81..62ec28ba 100644 --- a/java-reporter-core/src/main/java/io/testomat/core/constants/PropertyValuesConstants.java +++ b/java-reporter-core/src/main/java/io/testomat/core/constants/PropertyValuesConstants.java @@ -5,4 +5,5 @@ public class PropertyValuesConstants { public static final int DEFAULT_FLUSH_INTERVAL_SECONDS = 10; public static final String DEFAULT_URL = "https://app.testomat.io/"; public static final String DEFAULT_RUN_TITLE = "Default Run Title"; + public static final String DEFAULT_JSONL_PATH = "target/testomat/testomat-artifacts.json"; } diff --git a/java-reporter-core/src/main/java/io/testomat/core/facade/Testomatio.java b/java-reporter-core/src/main/java/io/testomat/core/facade/Testomatio.java index 29d6f940..9a3b76bd 100644 --- a/java-reporter-core/src/main/java/io/testomat/core/facade/Testomatio.java +++ b/java-reporter-core/src/main/java/io/testomat/core/facade/Testomatio.java @@ -1,13 +1,9 @@ package io.testomat.core.facade; -import static io.testomat.core.constants.ArtifactPropertyNames.STEP_ARTIFACT_ENABLED_PROPERTY_NAME; - import io.testomat.core.facade.methods.artifact.manager.ArtifactManager; import io.testomat.core.facade.methods.label.LabelStorage; import io.testomat.core.facade.methods.logmethod.LogStorage; import io.testomat.core.facade.methods.meta.MetaStorage; -import io.testomat.core.propertyconfig.impl.PropertyProviderFactoryImpl; -import io.testomat.core.propertyconfig.interf.PropertyProvider; import io.testomat.core.step.StepLifecycle; import io.testomat.core.step.StepStatus; import io.testomat.core.step.StepTimer; @@ -22,8 +18,6 @@ * Provides simple static methods for test artifact management and reporting. */ public class Testomatio { - private static final PropertyProvider provider = - PropertyProviderFactoryImpl.getPropertyProviderFactory().getPropertyProvider(); /** * Registers artifact files or directories to be uploaded for the current test. * @@ -39,9 +33,6 @@ public static void artifact(String... directories) { * @param directories artifact directories to attach (ignored if null or empty) */ public static void stepArtifact(String... directories) { - if (!provider.getBooleanProperty(STEP_ARTIFACT_ENABLED_PROPERTY_NAME)) { - return; - } TestStep testStep = StepLifecycle.current(); if (testStep == null) { diff --git a/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/TempArtifactDirectoriesStorage.java b/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/TempArtifactDirectoriesStorage.java index 2182d185..e98edbc7 100644 --- a/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/TempArtifactDirectoriesStorage.java +++ b/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/TempArtifactDirectoriesStorage.java @@ -1,7 +1,7 @@ package io.testomat.core.facade.methods.artifact; +import io.testomat.core.step.StepData; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.UUID; @@ -13,15 +13,18 @@ */ public class TempArtifactDirectoriesStorage { public static final ThreadLocal> DIRECTORIES = ThreadLocal.withInitial(ArrayList::new); - public static final Map> STEP_DIRECTORIES = new ConcurrentHashMap<>(); + public static final Map> STEP_DATA = new ConcurrentHashMap<>(); public static void store(String dir) { DIRECTORIES.get().add(dir); } public static void stepStore(UUID stepId, String dir) { - STEP_DIRECTORIES - .computeIfAbsent(stepId, - k -> Collections.synchronizedList(new ArrayList<>())) + long threadId = Thread.currentThread().getId(); + + STEP_DATA + .computeIfAbsent(threadId, k -> new ConcurrentHashMap<>()) + .computeIfAbsent(stepId, k -> new StepData()) + .getDirectories() .add(dir); } } diff --git a/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/client/AwsService.java b/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/client/AwsService.java index e87083a3..9907a8c8 100644 --- a/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/client/AwsService.java +++ b/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/client/AwsService.java @@ -1,5 +1,7 @@ package io.testomat.core.facade.methods.artifact.client; +import static io.testomat.core.constants.ArtifactPropertyNames.STEP_ARTIFACT_ENABLED_PROPERTY_NAME; + import io.testomat.core.facade.methods.artifact.ArtifactLinkData; import io.testomat.core.facade.methods.artifact.ArtifactLinkDataStorage; import io.testomat.core.facade.methods.artifact.TempArtifactDirectoriesStorage; @@ -8,6 +10,9 @@ import io.testomat.core.facade.methods.artifact.util.ArtifactKeyGenerator; import io.testomat.core.facade.methods.artifact.util.ArtifactUrlGenerator; import io.testomat.core.exception.ArtifactManagementException; +import io.testomat.core.propertyconfig.impl.PropertyProviderFactoryImpl; +import io.testomat.core.propertyconfig.interf.PropertyProvider; +import io.testomat.core.step.StepData; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -30,6 +35,8 @@ */ public class AwsService { private static final Logger log = LoggerFactory.getLogger(AwsService.class); + private static final PropertyProvider provider = + PropertyProviderFactoryImpl.getPropertyProviderFactory().getPropertyProvider(); private static final Map bucketAclSupport = new ConcurrentHashMap<>(); private static final String ACL_PRIVATE = "private"; @@ -67,7 +74,7 @@ public AwsService(ArtifactKeyGenerator keyGenerator, AwsClient awsClient, public void uploadAllArtifactsForTest(String testName, String rid, String testId) { List artifactDirectories = TempArtifactDirectoriesStorage.DIRECTORIES.get(); - Map> stepArtifactDirectories = TempArtifactDirectoriesStorage.STEP_DIRECTORIES; + Map stepArtifactDirectories = TempArtifactDirectoriesStorage.STEP_DATA.get(Thread.currentThread().getId()); if (artifactDirectories.isEmpty() && stepArtifactDirectories.isEmpty()) { log.debug("Artifact list is empty for test: {}", testName); @@ -76,7 +83,8 @@ public void uploadAllArtifactsForTest(String testName, String rid, String testId S3Credentials credentials = CredentialsManager.getCredentials(); - if (!TempArtifactDirectoriesStorage.STEP_DIRECTORIES.isEmpty()) { + if (!stepArtifactDirectories.isEmpty() && + provider.getBooleanProperty(STEP_ARTIFACT_ENABLED_PROPERTY_NAME)) { List directories = prepareStepArtifactsForUpload(testName, rid, credentials); processArtifacts(directories, testName, rid, credentials); } @@ -84,9 +92,6 @@ public void uploadAllArtifactsForTest(String testName, String rid, String testId if (!artifactDirectories.isEmpty()) { List uploadedArtifactsLinks = processArtifacts(artifactDirectories, testName, rid, credentials); storeArtifactLinkData(testName, rid, testId, uploadedArtifactsLinks); - - // Clear artifact directories after processing - TempArtifactDirectoriesStorage.DIRECTORIES.remove(); } } @@ -113,14 +118,19 @@ private List processArtifacts(List artifactDirectories, String t private List prepareStepArtifactsForUpload(String testName, String rid, S3Credentials credentials) { List artifactDirectories = new ArrayList<>(); - for (List list : TempArtifactDirectoriesStorage.STEP_DIRECTORIES.values()) { - for (int i = 0; i < list.size(); i++) { - String dir = list.get(i); + for (StepData stepData : TempArtifactDirectoriesStorage.STEP_DATA.get(Thread.currentThread().getId()).values()) { + stepData.getArtifacts().clear(); + for (String dir : stepData.getDirectories()) { if (!dir.startsWith("http")) { artifactDirectories.add(dir); + String key = keyGenerator.generateKey(dir, rid, testName); - list.set(i, urlGenerator.generateUrl(credentials.getBucket(), key)); + stepData.getArtifacts().add( + urlGenerator.generateUrl(credentials.getBucket(), key) + ); + } else { + stepData.getArtifacts().add(dir); } } } diff --git a/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/client/JsonlService.java b/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/client/JsonlService.java new file mode 100644 index 00000000..634eef65 --- /dev/null +++ b/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/client/JsonlService.java @@ -0,0 +1,44 @@ +package io.testomat.core.facade.methods.artifact.client; + +import io.testomat.core.facade.methods.artifact.TempArtifactDirectoriesStorage; +import io.testomat.core.facade.methods.artifact.model.TestFile; +import io.testomat.core.facade.methods.artifact.model.TestItem; +import io.testomat.core.runmanager.GlobalRunManager; +import io.testomat.core.step.StepData; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class JsonlService { + + private static final Logger log = LoggerFactory.getLogger(JsonlService.class); + + public void saveTestArtifacts(String testName, String rid, String testId) { + List artifactDirectories = TempArtifactDirectoriesStorage.DIRECTORIES.get(); + Map stepArtifactDirectories = + TempArtifactDirectoriesStorage.STEP_DATA.getOrDefault( + Thread.currentThread().getId(), + Collections.emptyMap() + ); + + if (artifactDirectories.isEmpty() && stepArtifactDirectories.isEmpty()) { + log.debug("Artifact list is empty for test: {}", testName); + return; + } + + List files = new ArrayList<>(); + + if (!artifactDirectories.isEmpty()) { + files = artifactDirectories.stream() + .map(TestFile::new) + .collect(Collectors.toList()); + } + + GlobalRunManager.getInstance().addTestItem(new TestItem(rid, testId, files, null)); + } +} diff --git a/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/client/S3ClientFactory.java b/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/client/S3ClientFactory.java index 9db839ce..3eb8cba9 100644 --- a/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/client/S3ClientFactory.java +++ b/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/client/S3ClientFactory.java @@ -49,7 +49,7 @@ public S3Client createS3Client() { * Builds AWS credentials provider. */ private AwsCredentialsProvider buildCredentialsProvider(S3Credentials s3, Region region) { - boolean useIamRole = s3.getRoleArn() != null && !s3.getRoleArn().isBlank(); + boolean useIamRole = s3.isIam() && s3.getRoleArn() != null && !s3.getRoleArn().isBlank(); if (useIamRole) { return buildIamRoleProvider(s3, region); diff --git a/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/model/AddTestsBatchRequest.java b/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/model/AddTestsBatchRequest.java new file mode 100644 index 00000000..eee57eaa --- /dev/null +++ b/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/model/AddTestsBatchRequest.java @@ -0,0 +1,59 @@ +package io.testomat.core.facade.methods.artifact.model; + +import java.util.ArrayList; +import java.util.List; + +public class AddTestsBatchRequest { + + private final String runId; + private final String action; + private final List tests; + + private AddTestsBatchRequest(Builder builder) { + this.runId = builder.runId; + this.action = builder.action; + this.tests = List.copyOf(builder.tests); + } + + public String getRunId() { + return runId; + } + + public String getAction() { + return action; + } + + public List getTests() { + return tests; + } + + public static Builder builder(String runId, String action) { + return new Builder(runId, action); + } + + public static class Builder { + + private final String runId; + private final String action; + private final List tests = new ArrayList<>(); + + private Builder(String runId, String action) { + this.runId = runId; + this.action = action; + } + + public Builder addTest(TestItem test) { + tests.add(test); + return this; + } + + public Builder addTests(List tests) { + this.tests.addAll(tests); + return this; + } + + public AddTestsBatchRequest build() { + return new AddTestsBatchRequest(this); + } + } +} diff --git a/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/model/Step.java b/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/model/Step.java new file mode 100644 index 00000000..7c41e20a --- /dev/null +++ b/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/model/Step.java @@ -0,0 +1,62 @@ +package io.testomat.core.facade.methods.artifact.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.testomat.core.step.StepStatus; +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class Step { + + private String category; + private String title; + private StepStatus status; + private String log; + private String error; + private double duration; + private final List artifacts; + private final List steps; + + public Step(String title, StepStatus status, String log, String error, double duration, String category, + List artifacts, List steps) { + this.title = title; + this.status = status; + this.log = log; + this.error = error; + this.duration = duration; + this.category = category; + this.artifacts = artifacts; + this.steps = steps; + } + + public String getCategory() { + return category; + } + + public double getDuration() { + return duration; + } + + public String getError() { + return error; + } + + public String getLog() { + return log; + } + + public StepStatus getStatus() { + return status; + } + + public String getTitle() { + return title; + } + + public List getArtifacts() { + return artifacts; + } + + public List getSteps() { + return steps; + } +} diff --git a/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/model/TestFile.java b/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/model/TestFile.java new file mode 100644 index 00000000..5eab1283 --- /dev/null +++ b/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/model/TestFile.java @@ -0,0 +1,22 @@ +package io.testomat.core.facade.methods.artifact.model; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class TestFile { + + String path; + String type; + + public TestFile(String path) { + this.path = path; + } + + public String getPath() { + return path; + } + + public String getType() { + return type; + } +} diff --git a/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/model/TestItem.java b/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/model/TestItem.java new file mode 100644 index 00000000..431f8cb1 --- /dev/null +++ b/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/model/TestItem.java @@ -0,0 +1,44 @@ +package io.testomat.core.facade.methods.artifact.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class TestItem { + + String rid; + String test_id; + List files; + List steps; + + public TestItem(String rid, String test_id, List files, List steps) { + this.test_id = test_id; + this.rid = rid; + this.files = files; + this.steps = steps; + } + + public void setFiles(List files) { + this.files = files; + } + + public void setSteps(List steps) { + this.steps = steps; + } + + public String getRid() { + return rid; + } + + public String getTest_id() { + return test_id; + } + + public List getFiles() { + return files; + } + + public List getSteps() { + return steps; + } +} diff --git a/java-reporter-core/src/main/java/io/testomat/core/propertyconfig/util/DefaultPropertiesStorage.java b/java-reporter-core/src/main/java/io/testomat/core/propertyconfig/util/DefaultPropertiesStorage.java index 4eef2e9a..9cb57c8b 100644 --- a/java-reporter-core/src/main/java/io/testomat/core/propertyconfig/util/DefaultPropertiesStorage.java +++ b/java-reporter-core/src/main/java/io/testomat/core/propertyconfig/util/DefaultPropertiesStorage.java @@ -1,7 +1,9 @@ package io.testomat.core.propertyconfig.util; +import static io.testomat.core.constants.ArtifactPropertyNames.JSONL_PATH_PROPERTY_NAME; import static io.testomat.core.constants.PropertyNameConstants.HOST_URL_PROPERTY_NAME; import static io.testomat.core.constants.PropertyNameConstants.RUN_TITLE_PROPERTY_NAME; +import static io.testomat.core.constants.PropertyValuesConstants.DEFAULT_JSONL_PATH; import static io.testomat.core.constants.PropertyValuesConstants.DEFAULT_RUN_TITLE; import static io.testomat.core.constants.PropertyValuesConstants.DEFAULT_URL; @@ -28,6 +30,8 @@ public class DefaultPropertiesStorage { static { DEFAULTS = Map.of( HOST_URL_PROPERTY_NAME, DEFAULT_URL, - RUN_TITLE_PROPERTY_NAME, DEFAULT_RUN_TITLE); + RUN_TITLE_PROPERTY_NAME, DEFAULT_RUN_TITLE, + JSONL_PATH_PROPERTY_NAME, DEFAULT_JSONL_PATH + ); } } diff --git a/java-reporter-core/src/main/java/io/testomat/core/runmanager/GlobalRunManager.java b/java-reporter-core/src/main/java/io/testomat/core/runmanager/GlobalRunManager.java index 8062f086..689f0b2b 100644 --- a/java-reporter-core/src/main/java/io/testomat/core/runmanager/GlobalRunManager.java +++ b/java-reporter-core/src/main/java/io/testomat/core/runmanager/GlobalRunManager.java @@ -6,6 +6,9 @@ import io.testomat.core.facade.methods.artifact.ArtifactLinkDataStorage; import io.testomat.core.facade.methods.artifact.ReportedTestStorage; +import io.testomat.core.facade.methods.artifact.model.AddTestsBatchRequest; +import io.testomat.core.facade.methods.artifact.model.Step; +import io.testomat.core.facade.methods.artifact.model.TestItem; import io.testomat.core.facade.methods.artifact.util.ArtifactKeyGenerator; import io.testomat.core.batch.BatchResultManager; import io.testomat.core.client.ApiInterface; @@ -15,6 +18,7 @@ import io.testomat.core.propertyconfig.impl.PropertyProviderFactoryImpl; import io.testomat.core.propertyconfig.interf.PropertyProvider; import java.io.IOException; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; @@ -39,6 +43,7 @@ public class GlobalRunManager { private final AtomicReference apiClient = new AtomicReference<>(); private final AtomicBoolean shutdownHookRegistered = new AtomicBoolean(false); private volatile long startTime; + private final AtomicReference batchRequest = new AtomicReference<>(); /** * Default constructor that initializes all dependencies internally. @@ -250,13 +255,16 @@ private synchronized void finalizeRun(boolean force) { * Shuts down the batch manager if it exists. */ private void shutdownBatchManager() { - BatchResultManager manager = batchManager.getAndSet(null); + BatchResultManager manager = batchManager.get(); if (manager != null) { try { manager.shutdown(); + batchRequest.set(manager.buildRequest()); log.debug("Batch manager shutdown completed"); } catch (Exception e) { log.error("Error shutting down batch manager: {}", e.getMessage()); + } finally { + batchManager.set(null); } } } @@ -277,6 +285,7 @@ private void finalizeTestRunWithApi(String uid) { client.finishTestRun(uid, duration); log.debug("Test run finished: {} (duration: {}s)", uid, duration); + client.writeArtifactsToJsonl(uid); processAndSendArtifacts(client, uid); } catch (IOException e) { log.error("Failed to finish test run {}: {}", uid, e.getMessage()); @@ -368,4 +377,29 @@ private static int getDelayBeforeArtifactsSendingMs() { return defaultDelayMs; } } + + public void addTestItem(TestItem testItem) { + BatchResultManager manager = batchManager.get(); + if (manager != null) { + manager.addTestItem(testItem); + } + } + + public AddTestsBatchRequest buildBatchRequest() { + AddTestsBatchRequest request = batchRequest.get(); + if (request != null) { + return request; + } + + BatchResultManager manager = batchManager.get(); + return manager != null ? manager.buildRequest() : null; + } + + public void updateTestSteps(String rid, List steps) { + BatchResultManager manager = batchManager.get(); + + if (manager != null) { + manager.updateTestSteps(rid, steps); + } + } } diff --git a/java-reporter-core/src/main/java/io/testomat/core/step/StepData.java b/java-reporter-core/src/main/java/io/testomat/core/step/StepData.java new file mode 100644 index 00000000..59610c2b --- /dev/null +++ b/java-reporter-core/src/main/java/io/testomat/core/step/StepData.java @@ -0,0 +1,21 @@ +package io.testomat.core.step; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class StepData { + private final List directories = + Collections.synchronizedList(new ArrayList<>()); + + private final List artifacts = + Collections.synchronizedList(new ArrayList<>()); + + public List getDirectories() { + return directories; + } + + public List getArtifacts() { + return artifacts; + } +} \ No newline at end of file diff --git a/java-reporter-core/src/test/java/io/testomat/core/artifact/TempArtifactDirectoriesStorageTest.java b/java-reporter-core/src/test/java/io/testomat/core/artifact/TempArtifactDirectoriesStorageTest.java index bdf303a6..3acb0195 100644 --- a/java-reporter-core/src/test/java/io/testomat/core/artifact/TempArtifactDirectoriesStorageTest.java +++ b/java-reporter-core/src/test/java/io/testomat/core/artifact/TempArtifactDirectoriesStorageTest.java @@ -8,6 +8,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import io.testomat.core.facade.methods.artifact.TempArtifactDirectoriesStorage; +import io.testomat.core.step.StepData; +import java.util.Map; import java.util.UUID; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -23,14 +25,14 @@ class TempArtifactDirectoriesStorageTest { void setUp() { // Clean storage before each test TempArtifactDirectoriesStorage.DIRECTORIES.get().clear(); - TempArtifactDirectoriesStorage.STEP_DIRECTORIES.clear(); + TempArtifactDirectoriesStorage.STEP_DATA.clear(); } @AfterEach void tearDown() { // Clean storage after each test TempArtifactDirectoriesStorage.DIRECTORIES.remove(); - TempArtifactDirectoriesStorage.STEP_DIRECTORIES.clear(); + TempArtifactDirectoriesStorage.STEP_DATA.clear(); } @Test @@ -305,11 +307,14 @@ void stepStoreShouldStoreDirectory(){ TempArtifactDirectoriesStorage.stepStore(stepId,"dir1"); - List dirs = TempArtifactDirectoriesStorage.STEP_DIRECTORIES.get(stepId); + StepData stepData = + TempArtifactDirectoriesStorage.STEP_DATA + .get(Thread.currentThread().getId()) + .get(stepId); - assertNotNull(dirs); - assertEquals(1,dirs.size()); - assertEquals("dir1",dirs.get(0)); + assertNotNull(stepData); + assertEquals(1, stepData.getDirectories().size()); + assertEquals("dir1", stepData.getDirectories().get(0)); } @Test @@ -320,11 +325,14 @@ void stepStoreShouldStoreMultiple(){ TempArtifactDirectoriesStorage.stepStore(stepId,"dir1"); TempArtifactDirectoriesStorage.stepStore(stepId,"dir2"); - List dirs = TempArtifactDirectoriesStorage.STEP_DIRECTORIES.get(stepId); + StepData stepData = + TempArtifactDirectoriesStorage.STEP_DATA + .get(Thread.currentThread().getId()) + .get(stepId); - assertEquals(2,dirs.size()); - assertTrue(dirs.contains("dir1")); - assertTrue(dirs.contains("dir2")); + assertEquals(2, stepData.getDirectories().size()); + assertTrue(stepData.getDirectories().contains("dir1")); + assertTrue(stepData.getDirectories().contains("dir2")); } @Test @@ -336,17 +344,16 @@ void stepStoreShouldIsolateSteps(){ TempArtifactDirectoriesStorage.stepStore(step1,"dir1"); TempArtifactDirectoriesStorage.stepStore(step2,"dir2"); - assertEquals(1, - TempArtifactDirectoriesStorage - .STEP_DIRECTORIES.get(step1).size()); + Map stepData = + TempArtifactDirectoriesStorage.STEP_DATA + .get(Thread.currentThread().getId()); - assertEquals(1, - TempArtifactDirectoriesStorage - .STEP_DIRECTORIES.get(step2).size()); + assertEquals(1, stepData.get(step1).getDirectories().size()); + assertEquals(1, stepData.get(step2).getDirectories().size()); assertFalse( - TempArtifactDirectoriesStorage - .STEP_DIRECTORIES.get(step1) + stepData.get(step1) + .getDirectories() .contains("dir2") ); } @@ -374,9 +381,16 @@ void stepStoreShouldBeThreadSafe() throws InterruptedException { t1.join(); t2.join(); - List dirs= TempArtifactDirectoriesStorage.STEP_DIRECTORIES.get(stepId); + assertEquals(2, TempArtifactDirectoriesStorage.STEP_DATA.size()); - assertEquals(200,dirs.size()); + for (Map stepData : + TempArtifactDirectoriesStorage.STEP_DATA.values()) { + + assertEquals( + 100, + stepData.get(stepId).getDirectories().size() + ); + } } @Test @@ -387,18 +401,19 @@ void stepStoreShouldReuseList(){ TempArtifactDirectoriesStorage .stepStore(stepId,"dir1"); - List first= - TempArtifactDirectoriesStorage - .STEP_DIRECTORIES.get(stepId); + StepData first = + TempArtifactDirectoriesStorage.STEP_DATA + .get(Thread.currentThread().getId()) + .get(stepId); - TempArtifactDirectoriesStorage - .stepStore(stepId,"dir2"); + TempArtifactDirectoriesStorage.stepStore(stepId, "dir2"); - List second= - TempArtifactDirectoriesStorage - .STEP_DIRECTORIES.get(stepId); + StepData second = + TempArtifactDirectoriesStorage.STEP_DATA + .get(Thread.currentThread().getId()) + .get(stepId); - assertSame(first,second); + assertSame(first, second); } @Test @@ -408,26 +423,29 @@ void stepStoreShouldHandleNullDir(){ TempArtifactDirectoriesStorage .stepStore(stepId,null); - List dirs= - TempArtifactDirectoriesStorage - .STEP_DIRECTORIES.get(stepId); + StepData stepData = + TempArtifactDirectoriesStorage.STEP_DATA + .get(Thread.currentThread().getId()) + .get(stepId); - assertEquals(1,dirs.size()); - assertNull(dirs.get(0)); + assertEquals(1, stepData.getDirectories().size()); + assertNull(stepData.getDirectories().get(0)); } @Test - void stepDirectoriesShouldAllowRemoval(){ - UUID stepId=UUID.randomUUID(); + void stepDataShouldAllowRemoval() { + UUID stepId = UUID.randomUUID(); - TempArtifactDirectoriesStorage - .stepStore(stepId,"dir"); + TempArtifactDirectoriesStorage.stepStore(stepId, "dir"); - TempArtifactDirectoriesStorage - .STEP_DIRECTORIES.remove(stepId); + TempArtifactDirectoriesStorage.STEP_DATA + .get(Thread.currentThread().getId()) + .remove(stepId); assertNull( - TempArtifactDirectoriesStorage - .STEP_DIRECTORIES.get(stepId)); + TempArtifactDirectoriesStorage.STEP_DATA + .get(Thread.currentThread().getId()) + .get(stepId) + ); } } diff --git a/java-reporter-core/src/test/java/io/testomat/core/batch/BatchResultManagerTest.java b/java-reporter-core/src/test/java/io/testomat/core/batch/BatchResultManagerTest.java index 6128bfa4..9bfd8de2 100644 --- a/java-reporter-core/src/test/java/io/testomat/core/batch/BatchResultManagerTest.java +++ b/java-reporter-core/src/test/java/io/testomat/core/batch/BatchResultManagerTest.java @@ -7,12 +7,13 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doAnswer; import io.testomat.core.client.ApiInterface; import io.testomat.core.constants.PropertyValuesConstants; +import io.testomat.core.facade.methods.artifact.model.AddTestsBatchRequest; +import io.testomat.core.facade.methods.artifact.model.Step; +import io.testomat.core.facade.methods.artifact.model.TestItem; import io.testomat.core.model.TestResult; import java.io.IOException; import java.lang.reflect.Field; @@ -341,6 +342,75 @@ void addResult_NullResult_ShouldThrowException() { assertThrows(NullPointerException.class, () -> batchManager.addResult(null)); } + @Test + @DisplayName("Should build batch request from added test items") + void buildRequestShouldReturnAddedTestItems() { + TestItem item1 = new TestItem("rid1", "test1", List.of(), null); + TestItem item2 = new TestItem("rid2", "test2", List.of(), null); + + batchManager.addTestItem(item1); + batchManager.addTestItem(item2); + + AddTestsBatchRequest request = batchManager.buildRequest(); + + assertEquals("addTestsBatch", request.getAction()); + assertEquals(testRunUid, request.getRunId()); + assertEquals(2, request.getTests().size()); + + assertEquals("rid1", request.getTests().get(0).getRid()); + assertEquals("rid2", request.getTests().get(1).getRid()); + } + +// @Test +// @DisplayName("Should update steps for existing test item") +// void updateTestStepsShouldUpdateExistingItem() { +// TestItem item = new TestItem("rid1", "test1", List.of(), null); +// batchManager.addTestItem(item); +// +// List steps = List.of( +// new Step(List.of("artifact1"), null) +// ); +// +// batchManager.updateTestSteps("rid1", steps); +// +// AddTestsBatchRequest request = batchManager.buildRequest(); +// +// assertEquals(steps, request.getTests().get(0).getSteps()); +// } +// +// @Test +// @DisplayName("Should ignore update for unknown rid") +// void updateTestStepsShouldIgnoreUnknownRid() { +// List steps = List.of( +// new Step(List.of("artifact1"), null) +// ); +// +// assertDoesNotThrow(() -> +// batchManager.updateTestSteps("unknown-rid", steps) +// ); +// +// AddTestsBatchRequest request = batchManager.buildRequest(); +// +// assertTrue(request.getTests().isEmpty()); +// } + + @Test + @DisplayName("Should replace existing test item with same rid") + void addTestItem_ShouldReplaceExistingItem() { + batchManager.addTestItem( + new TestItem("rid1", "test1", List.of(), null) + ); + + batchManager.addTestItem( + new TestItem("rid1", "test2", List.of(), null) + ); + + AddTestsBatchRequest request = batchManager.buildRequest(); + + assertEquals(1, request.getTests().size()); + assertEquals("test2", request.getTests().get(0).getTest_id()); + } + private TestResult createTestResult(String title, String status) { return new TestResult.Builder() .withTitle(title) diff --git a/java-reporter-core/src/test/java/io/testomat/core/client/request/NativeRequestBodyBuilderTest.java b/java-reporter-core/src/test/java/io/testomat/core/client/request/NativeRequestBodyBuilderTest.java index 37d94a45..08183f4f 100644 --- a/java-reporter-core/src/test/java/io/testomat/core/client/request/NativeRequestBodyBuilderTest.java +++ b/java-reporter-core/src/test/java/io/testomat/core/client/request/NativeRequestBodyBuilderTest.java @@ -1,5 +1,6 @@ package io.testomat.core.client.request; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -15,13 +16,19 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.testomat.core.constants.ApiRequestFields; import io.testomat.core.constants.PropertyNameConstants; +import io.testomat.core.facade.methods.artifact.TempArtifactDirectoriesStorage; import io.testomat.core.model.Link; import io.testomat.core.model.TestResult; import io.testomat.core.propertyconfig.impl.PropertyProviderFactoryImpl; import io.testomat.core.propertyconfig.interf.PropertyProvider; import io.testomat.core.propertyconfig.interf.PropertyProviderFactory; +import io.testomat.core.step.StepData; +import io.testomat.core.step.TestStep; import java.util.Arrays; +import java.util.Collections; import java.util.List; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -54,6 +61,9 @@ void setUp() { requestBodyBuilder = new NativeRequestBodyBuilder(); } + + TempArtifactDirectoriesStorage.STEP_DATA.clear(); + TempArtifactDirectoriesStorage.DIRECTORIES.remove(); } @Test @@ -383,4 +393,90 @@ void allMethodsShouldProduceValidJson() throws Exception { assertDoesNotThrow(() -> objectMapper.readTree(finishResult)); } + + @Test + @DisplayName("Should attach step artifacts from storage") + void shouldAttachStepArtifactsFromStorage() throws Exception { + UUID stepId = UUID.randomUUID(); + + StepData stepData = new StepData(); + stepData.getArtifacts().add("https://artifact"); + + TempArtifactDirectoriesStorage.STEP_DATA + .computeIfAbsent(Thread.currentThread().getId(), k -> new ConcurrentHashMap<>()) + .put(stepId, stepData); + + TestStep step = new TestStep(); + step.setId(stepId); + + TestResult result = new TestResult.Builder() + .withTitle("Test") + .withSuiteTitle("Suite") + .withFile("Test.java") + .withStatus("passed") + .withSteps(Collections.singletonList(step)) + .build(); + + requestBodyBuilder.buildSingleTestReportBody(result); + + assertArrayEquals( + new String[]{"https://artifact"}, + step.getArtifacts() + ); + } + + @Test + @DisplayName("Should convert step directories to jsonl") + void shouldConvertDirectoriesToJsonl() throws Exception { + UUID stepId = UUID.randomUUID(); + + StepData stepData = new StepData(); + stepData.getDirectories().add("dir1"); + + TempArtifactDirectoriesStorage.STEP_DATA + .computeIfAbsent(Thread.currentThread().getId(), k -> new ConcurrentHashMap<>()) + .put(stepId, stepData); + + TestStep step = new TestStep(); + step.setId(stepId); + step.setStepTitle("Step"); + + TestResult result = new TestResult.Builder() + .withTitle("Test") + .withSuiteTitle("Suite") + .withFile("Test.java") + .withStatus("passed") + .withRid("rid") + .withSteps(Collections.singletonList(step)) + .build(); + + requestBodyBuilder.buildSingleTestReportBody(result); + + assertTrue( + TempArtifactDirectoriesStorage.STEP_DATA + .get(Thread.currentThread().getId()) + .isEmpty() + ); + } + + @Test + @DisplayName("Should handle missing step data") + void shouldHandleMissingStepData() { + UUID stepId = UUID.randomUUID(); + + TestStep step = new TestStep(); + step.setId(stepId); + + TestResult result = new TestResult.Builder() + .withTitle("Test") + .withSuiteTitle("Suite") + .withFile("Test.java") + .withStatus("passed") + .withSteps(Collections.singletonList(step)) + .build(); + + assertDoesNotThrow(() -> + requestBodyBuilder.buildSingleTestReportBody(result) + ); + } } \ No newline at end of file diff --git a/java-reporter-core/src/test/java/io/testomat/core/runmanager/GlobalRunManagerTest.java b/java-reporter-core/src/test/java/io/testomat/core/runmanager/GlobalRunManagerTest.java index 60fafd0a..f3985f11 100644 --- a/java-reporter-core/src/test/java/io/testomat/core/runmanager/GlobalRunManagerTest.java +++ b/java-reporter-core/src/test/java/io/testomat/core/runmanager/GlobalRunManagerTest.java @@ -18,10 +18,13 @@ import io.testomat.core.batch.BatchResultManager; import io.testomat.core.client.ApiInterface; import io.testomat.core.client.ClientFactory; +import io.testomat.core.facade.methods.artifact.TempArtifactDirectoriesStorage; import io.testomat.core.model.TestResult; import io.testomat.core.propertyconfig.interf.PropertyProvider; +import io.testomat.core.step.StepData; import java.io.IOException; import java.lang.reflect.Field; +import java.util.UUID; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -427,4 +430,50 @@ void shouldHandleRunCreationFailure() throws IOException { assertNull(globalRunManager.getRunUid()); } } + + @Test + void shouldIsolateStepDataBetweenThreads() throws InterruptedException { + UUID stepId = UUID.randomUUID(); + + Thread t1 = new Thread(() -> + TempArtifactDirectoriesStorage.stepStore(stepId, "thread1")); + + Thread t2 = new Thread(() -> + TempArtifactDirectoriesStorage.stepStore(stepId, "thread2")); + + t1.start(); + t2.start(); + + t1.join(); + t2.join(); + + long count = TempArtifactDirectoriesStorage.STEP_DATA.values().stream() + .filter(map -> map.containsKey(stepId)) + .count(); + + assertEquals(2, count); + } + + @Test + @DisplayName("Should reuse StepData for same step") + void shouldReuseStepData() { + UUID stepId = UUID.randomUUID(); + + TempArtifactDirectoriesStorage.stepStore(stepId, "dir1"); + + StepData first = + TempArtifactDirectoriesStorage.STEP_DATA + .get(Thread.currentThread().getId()) + .get(stepId); + + TempArtifactDirectoriesStorage.stepStore(stepId, "dir2"); + + StepData second = + TempArtifactDirectoriesStorage.STEP_DATA + .get(Thread.currentThread().getId()) + .get(stepId); + + assertSame(first, second); + assertEquals(2, first.getDirectories().size()); + } } \ No newline at end of file diff --git a/java-reporter-cucumber/pom.xml b/java-reporter-cucumber/pom.xml index 8cca9f0a..024e447e 100644 --- a/java-reporter-cucumber/pom.xml +++ b/java-reporter-cucumber/pom.xml @@ -6,7 +6,7 @@ io.testomat java-reporter-cucumber - 0.7.18 + 0.8.0 jar Testomat.io Java Reporter Cucumber @@ -51,7 +51,7 @@ io.testomat java-reporter-core - 0.14.0 + 0.15.0 org.slf4j diff --git a/java-reporter-cucumber/src/main/java/io/testomat/cucumber/listener/FacadeFunctionsHandler.java b/java-reporter-cucumber/src/main/java/io/testomat/cucumber/listener/FacadeFunctionsHandler.java index fd71dd98..ec1f54ba 100644 --- a/java-reporter-cucumber/src/main/java/io/testomat/cucumber/listener/FacadeFunctionsHandler.java +++ b/java-reporter-cucumber/src/main/java/io/testomat/cucumber/listener/FacadeFunctionsHandler.java @@ -1,26 +1,41 @@ package io.testomat.cucumber.listener; +import static io.testomat.core.constants.ArtifactPropertyNames.ARTIFACT_DISABLE_PROPERTY_NAME; +import static io.testomat.core.constants.ArtifactPropertyNames.JSONL_EXPORT_PROPERTY_NAME; + import io.cucumber.plugin.event.TestCaseFinished; +import io.testomat.core.facade.methods.artifact.TempArtifactDirectoriesStorage; import io.testomat.core.facade.methods.artifact.client.AwsService; +import io.testomat.core.facade.methods.artifact.client.JsonlService; import io.testomat.core.facade.methods.label.LabelStorage; import io.testomat.core.facade.methods.logmethod.LogStorage; import io.testomat.core.facade.methods.meta.MetaStorage; +import io.testomat.core.propertyconfig.impl.PropertyProviderFactoryImpl; +import io.testomat.core.propertyconfig.interf.PropertyProvider; import io.testomat.cucumber.extractor.TestDataExtractor; import java.util.List; import java.util.Map; public class FacadeFunctionsHandler { private final AwsService awsService; + private final JsonlService jsonlService; private final TestDataExtractor dataExtractor; + private final PropertyProvider provider; - public FacadeFunctionsHandler(AwsService awsService, TestDataExtractor dataExtractor) { + public FacadeFunctionsHandler(AwsService awsService, JsonlService jsonlService, + TestDataExtractor dataExtractor, PropertyProvider provider) { this.dataExtractor = dataExtractor; this.awsService = awsService; + this.jsonlService = jsonlService; + this.provider = provider; } public FacadeFunctionsHandler() { this.dataExtractor = new TestDataExtractor(); this.awsService = new AwsService(); + this.jsonlService = new JsonlService(); + this.provider = + PropertyProviderFactoryImpl.getPropertyProviderFactory().getPropertyProvider(); } public void handleFacadeFunctions(TestCaseFinished testCaseFinished) { @@ -41,9 +56,20 @@ private void handleMetaAfterEach(String rid) { } private void handleArtifactsAfterEach(TestCaseFinished testCaseFinished) { - awsService.uploadAllArtifactsForTest(dataExtractor.extractTitle(testCaseFinished), - testCaseFinished.getTestCase().getId().toString(), - dataExtractor.extractTestId(testCaseFinished)); + if (!defineArtifactsDisabled()) { + awsService.uploadAllArtifactsForTest(dataExtractor.extractTitle(testCaseFinished), + testCaseFinished.getTestCase().getId().toString(), + dataExtractor.extractTestId(testCaseFinished)); + } + TempArtifactDirectoriesStorage.DIRECTORIES.remove(); + } + + private void handleJsonlAfterEach(TestCaseFinished testCaseFinished) { + if (!defineJsonlExportEnabled()) { + jsonlService.saveTestArtifacts(dataExtractor.extractTitle(testCaseFinished), + testCaseFinished.getTestCase().getId().toString(), + dataExtractor.extractTestId(testCaseFinished)); + } } private void handleLabels(String rid) { @@ -61,4 +87,29 @@ private void handleLogFunction(String rid) { LogStorage.LINKED_LOG_STORAGE.put(rid, logs); } } + + private boolean defineArtifactsDisabled() { + boolean result; + String property; + try { + property = provider.getProperty(ARTIFACT_DISABLE_PROPERTY_NAME); + result = property != null + && !property.trim().isEmpty() + && !property.equalsIgnoreCase("0"); + + } catch (Exception e) { + return false; + } + return result; + } + + private boolean defineJsonlExportEnabled() { + try { + return Boolean.parseBoolean( + provider.getProperty(JSONL_EXPORT_PROPERTY_NAME) + ); + } catch (Exception e) { + return false; + } + } } diff --git a/java-reporter-junit/pom.xml b/java-reporter-junit/pom.xml index 3c9ff996..85ffad26 100644 --- a/java-reporter-junit/pom.xml +++ b/java-reporter-junit/pom.xml @@ -6,7 +6,7 @@ io.testomat java-reporter-junit - 0.8.10 + 0.9.0 jar Testomat.io Java Reporter JUnit @@ -51,7 +51,7 @@ io.testomat java-reporter-core - 0.14.0 + 0.15.0 org.slf4j diff --git a/java-reporter-junit/src/main/java/io/testomat/junit/listener/AbstractHookContainer.java b/java-reporter-junit/src/main/java/io/testomat/junit/listener/AbstractHookContainer.java index 7b4e62f7..57721183 100644 --- a/java-reporter-junit/src/main/java/io/testomat/junit/listener/AbstractHookContainer.java +++ b/java-reporter-junit/src/main/java/io/testomat/junit/listener/AbstractHookContainer.java @@ -1,69 +1,143 @@ package io.testomat.junit.listener; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Optional; +import java.util.ServiceLoader; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.platform.launcher.TestPlan; public abstract class AbstractHookContainer { + private static final List HOOKS; + + static { + List hooks = new ArrayList<>(); + ServiceLoader.load(TestomatHook.class) + .forEach(hooks::add); + + HOOKS = Collections.unmodifiableList(hooks); + } + protected void onSuiteStartHookAfterExecution(ExtensionContext context) { + for (TestomatHook hook : HOOKS) { + hook.onSuiteStartHookAfterExecution(context); + } } protected void onSuiteStartHookBeforeExecution(ExtensionContext context) { + for (TestomatHook hook : HOOKS) { + hook.onSuiteStartHookBeforeExecution(context); + } } protected void onSuiteFinishHookAfterExecution(ExtensionContext context) { + for (TestomatHook hook : HOOKS) { + hook.onSuiteFinishHookAfterExecution(context); + } } protected void onSuiteFinishHookBeforeExecution(ExtensionContext context) { + for (TestomatHook hook : HOOKS) { + hook.onSuiteFinishHookBeforeExecution(context); + } } protected void beforeEachHookAfterExecution(ExtensionContext context) { + for (TestomatHook hook : HOOKS) { + hook.beforeEachHookAfterExecution(context); + } } protected void beforeEachHookBeforeExecution(ExtensionContext context) { + for (TestomatHook hook : HOOKS) { + hook.beforeEachHookBeforeExecution(context); + } } protected void onTestSuccessHookAfterExecution(ExtensionContext context) { + for (TestomatHook hook : HOOKS) { + hook.onTestSuccessHookAfterExecution(context); + } } protected void onTestSuccessHookBeforeExecution(ExtensionContext context) { + for (TestomatHook hook : HOOKS) { + hook.onTestSuccessHookBeforeExecution(context); + } } protected void onTestFailureHookAfterExecution(ExtensionContext context, Throwable cause) { + for (TestomatHook hook : HOOKS) { + hook.onTestFailureHookAfterExecution(context, cause); + } } protected void onTestFailureHookBeforeExecution(ExtensionContext context, Throwable cause) { + for (TestomatHook hook : HOOKS) { + hook.onTestFailureHookBeforeExecution(context, cause); + } } protected void onTestDisabledHookAfterExecution(ExtensionContext context, Optional reason) { + for (TestomatHook hook : HOOKS) { + hook.onTestDisabledHookAfterExecution(context, reason); + } } protected void onTestDisabledHookBeforeExecution(ExtensionContext context, Optional reason) { + for (TestomatHook hook : HOOKS) { + hook.onTestDisabledHookBeforeExecution(context, reason); + } } protected void onTestAbortedHookAfterExecution(ExtensionContext context, Throwable cause) { + for (TestomatHook hook : HOOKS) { + hook.onTestAbortedHookAfterExecution(context, cause); + } } protected void onTestAbortedHookBeforeExecution(ExtensionContext context, Throwable cause) { + for (TestomatHook hook : HOOKS) { + hook.onTestAbortedHookBeforeExecution(context, cause); + } } protected void afterEachHookAfterExecution(ExtensionContext context) { + for (TestomatHook hook : HOOKS) { + hook.afterEachHookAfterExecution(context); + } } protected void afterEachHookBeforeExecution(ExtensionContext context) { + for (TestomatHook hook : HOOKS) { + hook.afterEachHookBeforeExecution(context); + } } protected void onExecutionStartHookAfterExecution(TestPlan testPlan) { + for (TestomatHook hook : HOOKS) { + hook.onExecutionStartHookAfterExecution(testPlan); + } } protected void onExecutionStartHookBeforeExecution(TestPlan testPlan) { + for (TestomatHook hook : HOOKS) { + hook.onExecutionStartHookBeforeExecution(testPlan); + } } protected void onExecutionFinishHookAfterExecution(TestPlan testPlan) { + for (TestomatHook hook : HOOKS) { + hook.onExecutionFinishHookAfterExecution(testPlan); + } } protected void onExecutionFinishHookBeforeExecution(TestPlan testPlan) { + for (TestomatHook hook : HOOKS) { + hook.onExecutionFinishHookBeforeExecution(testPlan); + } } } diff --git a/java-reporter-junit/src/main/java/io/testomat/junit/listener/FacadeFunctionsHandler.java b/java-reporter-junit/src/main/java/io/testomat/junit/listener/FacadeFunctionsHandler.java index 4832e9c3..3334de33 100644 --- a/java-reporter-junit/src/main/java/io/testomat/junit/listener/FacadeFunctionsHandler.java +++ b/java-reporter-junit/src/main/java/io/testomat/junit/listener/FacadeFunctionsHandler.java @@ -1,8 +1,11 @@ package io.testomat.junit.listener; import static io.testomat.core.constants.ArtifactPropertyNames.ARTIFACT_DISABLE_PROPERTY_NAME; +import static io.testomat.core.constants.ArtifactPropertyNames.JSONL_EXPORT_PROPERTY_NAME; +import io.testomat.core.facade.methods.artifact.TempArtifactDirectoriesStorage; import io.testomat.core.facade.methods.artifact.client.AwsService; +import io.testomat.core.facade.methods.artifact.client.JsonlService; import io.testomat.core.facade.methods.label.LabelStorage; import io.testomat.core.facade.methods.logmethod.LogStorage; import io.testomat.core.facade.methods.meta.MetaStorage; @@ -16,20 +19,27 @@ public class FacadeFunctionsHandler { private final PropertyProvider provider; private final AwsService awsService; + private final JsonlService jsonlService; private boolean artifactDisabled = false; + private boolean jsonlDisabled = false; public FacadeFunctionsHandler() { this.awsService = new AwsService(); + this.jsonlService = new JsonlService(); this.provider = PropertyProviderFactoryImpl.getPropertyProviderFactory().getPropertyProvider(); this.artifactDisabled = defineArtifactsDisabled(); + this.jsonlDisabled = defineJsonlExportEnabled(); } public FacadeFunctionsHandler(boolean artifactDisabled, PropertyProvider provider, - AwsService awsService) { + AwsService awsService, + JsonlService jsonlService) { this.awsService = awsService; + this.jsonlService = jsonlService; this.artifactDisabled = artifactDisabled; + this.jsonlDisabled = defineJsonlExportEnabled(); this.provider = provider; } @@ -37,6 +47,7 @@ public void handleFacadeFunctions(ExtensionContext context) { handleLogsAfterEach(context); handleMetaAfterEach(context); handleLabels(context); + handleJsonlAfterEach(context); handleArtifactsAfterEach(context); } @@ -65,6 +76,14 @@ private void handleArtifactsAfterEach(ExtensionContext context) { awsService.uploadAllArtifactsForTest(context.getDisplayName(), context.getUniqueId(), JunitMetaDataExtractor.extractTestId(context.getTestMethod().get())); } + TempArtifactDirectoriesStorage.DIRECTORIES.remove(); + } + + private void handleJsonlAfterEach(ExtensionContext context) { + if (!jsonlDisabled) { + jsonlService.saveTestArtifacts(context.getDisplayName(), context.getUniqueId(), + JunitMetaDataExtractor.extractTestId(context.getTestMethod().get())); + } } private void handleLabels(ExtensionContext context) { @@ -89,4 +108,14 @@ private boolean defineArtifactsDisabled() { } return result; } + + private boolean defineJsonlExportEnabled() { + try { + return Boolean.parseBoolean( + provider.getProperty(JSONL_EXPORT_PROPERTY_NAME) + ); + } catch (Exception e) { + return false; + } + } } diff --git a/java-reporter-junit/src/main/java/io/testomat/junit/listener/TestomatHook.java b/java-reporter-junit/src/main/java/io/testomat/junit/listener/TestomatHook.java new file mode 100644 index 00000000..4ac17180 --- /dev/null +++ b/java-reporter-junit/src/main/java/io/testomat/junit/listener/TestomatHook.java @@ -0,0 +1,70 @@ +package io.testomat.junit.listener; + +import java.util.Optional; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.platform.launcher.TestPlan; + +public interface TestomatHook { + + default void onSuiteStartHookAfterExecution(ExtensionContext context) { + } + + default void onSuiteStartHookBeforeExecution(ExtensionContext context) { + } + + default void onSuiteFinishHookAfterExecution(ExtensionContext context) { + } + + default void onSuiteFinishHookBeforeExecution(ExtensionContext context) { + } + + default void beforeEachHookAfterExecution(ExtensionContext context) { + } + + default void beforeEachHookBeforeExecution(ExtensionContext context) { + } + + default void onTestSuccessHookAfterExecution(ExtensionContext context) { + } + + default void onTestSuccessHookBeforeExecution(ExtensionContext context) { + } + + default void onTestFailureHookAfterExecution(ExtensionContext context, Throwable cause) { + } + + default void onTestFailureHookBeforeExecution(ExtensionContext context, Throwable cause) { + } + + default void onTestDisabledHookAfterExecution(ExtensionContext context, + Optional reason) { + } + + default void onTestDisabledHookBeforeExecution(ExtensionContext context, + Optional reason) { + } + + default void onTestAbortedHookAfterExecution(ExtensionContext context, Throwable cause) { + } + + default void onTestAbortedHookBeforeExecution(ExtensionContext context, Throwable cause) { + } + + default void afterEachHookAfterExecution(ExtensionContext context) { + } + + default void afterEachHookBeforeExecution(ExtensionContext context) { + } + + default void onExecutionStartHookAfterExecution(TestPlan testPlan) { + } + + default void onExecutionStartHookBeforeExecution(TestPlan testPlan) { + } + + default void onExecutionFinishHookAfterExecution(TestPlan testPlan) { + } + + default void onExecutionFinishHookBeforeExecution(TestPlan testPlan) { + } +} diff --git a/java-reporter-karate/pom.xml b/java-reporter-karate/pom.xml index 46e8026b..3d145aed 100644 --- a/java-reporter-karate/pom.xml +++ b/java-reporter-karate/pom.xml @@ -6,7 +6,7 @@ io.testomat java-reporter-karate - 0.2.12 + 0.3.0 jar Testomat.io Java Reporter Karate @@ -52,7 +52,7 @@ io.testomat java-reporter-core - 0.14.0 + 0.15.0 io.karatelabs diff --git a/java-reporter-karate/src/main/java/io/testomat/karate/hooks/FacadeFunctionsHandler.java b/java-reporter-karate/src/main/java/io/testomat/karate/hooks/FacadeFunctionsHandler.java index bc265683..3c70d647 100644 --- a/java-reporter-karate/src/main/java/io/testomat/karate/hooks/FacadeFunctionsHandler.java +++ b/java-reporter-karate/src/main/java/io/testomat/karate/hooks/FacadeFunctionsHandler.java @@ -1,10 +1,17 @@ package io.testomat.karate.hooks; +import static io.testomat.core.constants.ArtifactPropertyNames.ARTIFACT_DISABLE_PROPERTY_NAME; +import static io.testomat.core.constants.ArtifactPropertyNames.JSONL_EXPORT_PROPERTY_NAME; + import com.intuit.karate.core.ScenarioRuntime; +import io.testomat.core.facade.methods.artifact.TempArtifactDirectoriesStorage; import io.testomat.core.facade.methods.artifact.client.AwsService; +import io.testomat.core.facade.methods.artifact.client.JsonlService; import io.testomat.core.facade.methods.label.LabelStorage; import io.testomat.core.facade.methods.logmethod.LogStorage; import io.testomat.core.facade.methods.meta.MetaStorage; +import io.testomat.core.propertyconfig.impl.PropertyProviderFactoryImpl; +import io.testomat.core.propertyconfig.interf.PropertyProvider; import io.testomat.karate.extractor.TestDataExtractor; import java.util.HashMap; import java.util.List; @@ -13,17 +20,28 @@ public class FacadeFunctionsHandler { + private final PropertyProvider provider; private final AwsService awsService; + private final JsonlService jsonlService; private final TestDataExtractor dataExtractor; - public FacadeFunctionsHandler(AwsService awsService, TestDataExtractor dataExtractor) { + public FacadeFunctionsHandler( + PropertyProvider provider, + AwsService awsService, + JsonlService jsonlService, + TestDataExtractor dataExtractor) { + this.provider = provider; this.dataExtractor = dataExtractor; this.awsService = awsService; + this.jsonlService = jsonlService; } public FacadeFunctionsHandler() { + this.provider = + PropertyProviderFactoryImpl.getPropertyProviderFactory().getPropertyProvider(); this.dataExtractor = new TestDataExtractor(); this.awsService = new AwsService(); + this.jsonlService = new JsonlService(); } public void handleFacadeFunctions(ScenarioRuntime sr) { @@ -31,6 +49,7 @@ public void handleFacadeFunctions(ScenarioRuntime sr) { handleMetaAfterEach(rid); handleLogFunction(rid); handleLabels(rid); + handleJsonlAfterEach(sr, rid); handleArtifactsAfterEach(sr, rid); } @@ -46,11 +65,24 @@ private void handleMetaAfterEach(String rid) { } private void handleArtifactsAfterEach(ScenarioRuntime sr, String rid) { - awsService.uploadAllArtifactsForTest( - dataExtractor.extractTitle(sr), - rid, - dataExtractor.extractTestId(sr) - ); + if (!defineArtifactsDisabled()) { + awsService.uploadAllArtifactsForTest( + dataExtractor.extractTitle(sr), + rid, + dataExtractor.extractTestId(sr) + ); + } + TempArtifactDirectoriesStorage.DIRECTORIES.remove(); + } + + private void handleJsonlAfterEach(ScenarioRuntime sr, String rid) { + if (!defineJsonlExportEnabled()) { + jsonlService.saveTestArtifacts( + dataExtractor.extractTitle(sr), + rid, + dataExtractor.extractTestId(sr) + ); + } } private void handleLabels(String rid) { @@ -72,4 +104,29 @@ private void handleLogFunction(String rid) { LogStorage.LINKED_LOG_STORAGE.put(rid, storedLogs.toArray(String[]::new)); } } + + private boolean defineArtifactsDisabled() { + boolean result; + String property; + try { + property = provider.getProperty(ARTIFACT_DISABLE_PROPERTY_NAME); + result = property != null + && !property.trim().isEmpty() + && !property.equalsIgnoreCase("0"); + + } catch (Exception e) { + return false; + } + return result; + } + + private boolean defineJsonlExportEnabled() { + try { + return Boolean.parseBoolean( + provider.getProperty(JSONL_EXPORT_PROPERTY_NAME) + ); + } catch (Exception e) { + return false; + } + } } diff --git a/java-reporter-karate/src/test/java/io/testomat/karate/hooks/FacadeFunctionsHandlerTest.java b/java-reporter-karate/src/test/java/io/testomat/karate/hooks/FacadeFunctionsHandlerTest.java index 44682b2b..64141e9e 100644 --- a/java-reporter-karate/src/test/java/io/testomat/karate/hooks/FacadeFunctionsHandlerTest.java +++ b/java-reporter-karate/src/test/java/io/testomat/karate/hooks/FacadeFunctionsHandlerTest.java @@ -5,6 +5,8 @@ import com.intuit.karate.core.ScenarioRuntime; import io.testomat.core.facade.methods.artifact.client.AwsService; +import io.testomat.core.facade.methods.artifact.client.JsonlService; +import io.testomat.core.propertyconfig.interf.PropertyProvider; import io.testomat.karate.extractor.TestDataExtractor; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -18,15 +20,19 @@ class FacadeFunctionsHandlerTest { @Mock AwsService awsService; @Mock + JsonlService jsonlService; + @Mock TestDataExtractor dataExtractor; @Mock ScenarioRuntime sr; + @Mock + PropertyProvider provider; FacadeFunctionsHandler handler; @BeforeEach void init() { - handler = new FacadeFunctionsHandler(awsService, dataExtractor); + handler = new FacadeFunctionsHandler(provider, awsService, jsonlService, dataExtractor); } @Test diff --git a/java-reporter-testng/pom.xml b/java-reporter-testng/pom.xml index b25d1f81..9fb9d6d1 100644 --- a/java-reporter-testng/pom.xml +++ b/java-reporter-testng/pom.xml @@ -6,7 +6,7 @@ io.testomat java-reporter-testng - 0.7.17 + 0.8.0 jar Testomat.io Java Reporter TestNG @@ -47,7 +47,7 @@ io.testomat java-reporter-core - 0.14.0 + 0.15.0 org.slf4j diff --git a/java-reporter-testng/src/main/java/io/testomat/testng/listener/AbstractHooksContainer.java b/java-reporter-testng/src/main/java/io/testomat/testng/listener/AbstractHooksContainer.java index 501d2428..c0652737 100644 --- a/java-reporter-testng/src/main/java/io/testomat/testng/listener/AbstractHooksContainer.java +++ b/java-reporter-testng/src/main/java/io/testomat/testng/listener/AbstractHooksContainer.java @@ -1,72 +1,146 @@ package io.testomat.testng.listener; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.ServiceLoader; import org.testng.IInvokedMethod; import org.testng.ISuite; import org.testng.ITestResult; public abstract class AbstractHooksContainer { + private static final List HOOKS; + + static { + List hooks = new ArrayList<>(); + ServiceLoader.load(TestomatHook.class) + .forEach(hooks::add); + + HOOKS = Collections.unmodifiableList(hooks); + } + protected void onSuiteStartHookAfterExecution(ISuite suite) { + for (TestomatHook hook : HOOKS) { + hook.onSuiteStartHookAfterExecution(suite); + } } protected void onSuiteStartHookBeforeExecution(ISuite suite) { + for (TestomatHook hook : HOOKS) { + hook.onSuiteStartHookBeforeExecution(suite); + } } protected void onSuiteFinishHookAfterExecution(ISuite suite) { + for (TestomatHook hook : HOOKS) { + hook.onSuiteFinishHookAfterExecution(suite); + } } protected void onSuiteFinishHookBeforeExecution(ISuite suite) { + for (TestomatHook hook : HOOKS) { + hook.onSuiteFinishHookBeforeExecution(suite); + } } protected void onTestSuccessHookAfterExecution(ITestResult result) { + for (TestomatHook hook : HOOKS) { + hook.onTestSuccessHookAfterExecution(result); + } } protected void onTestSuccessHookBeforeExecution(ITestResult result) { + for (TestomatHook hook : HOOKS) { + hook.onTestSuccessHookBeforeExecution(result); + } } protected void onTestFailureHookAfterExecution(ITestResult result) { + for (TestomatHook hook : HOOKS) { + hook.onTestFailureHookAfterExecution(result); + } } protected void onTestFailureHookBeforeExecution(ITestResult result) { + for (TestomatHook hook : HOOKS) { + hook.onTestFailureHookBeforeExecution(result); + } } protected void onTestSkippedHookAfterExecution(ITestResult result) { + for (TestomatHook hook : HOOKS) { + hook.onTestSkippedHookAfterExecution(result); + } } protected void onTestSkippedHookBeforeExecution(ITestResult result) { + for (TestomatHook hook : HOOKS) { + hook.onTestSkippedHookBeforeExecution(result); + } } protected void onTestStartHookAfterExecution(ITestResult result) { + for (TestomatHook hook : HOOKS) { + hook.onTestStartHookAfterExecution(result); + } } protected void onTestStartHookBeforeExecution(ITestResult result) { + for (TestomatHook hook : HOOKS) { + hook.onTestStartHookBeforeExecution(result); + } } protected void beforeInvocationHookAfterExecution(IInvokedMethod method, ITestResult testResult) { + for (TestomatHook hook : HOOKS) { + hook.beforeInvocationHookAfterExecution(method, testResult); + } } protected void beforeInvocationHookBeforeExecution(IInvokedMethod method, ITestResult testResult) { + for (TestomatHook hook : HOOKS) { + hook.beforeInvocationHookBeforeExecution(method, testResult); + } } protected void afterInvocationHookAfterExecution(IInvokedMethod method, ITestResult testResult) { + for (TestomatHook hook : HOOKS) { + hook.afterInvocationAfter(method, testResult); + } } protected void afterInvocationHookBeforeExecution(IInvokedMethod method, ITestResult testResult) { + for (TestomatHook hook : HOOKS) { + hook.afterInvocationHookBeforeExecution(method, testResult); + } } protected void onExecutionStartHookAfterExecution() { + for (TestomatHook hook : HOOKS) { + hook.onExecutionStartHookAfterExecution(); + } } protected void onExecutionStartHookBeforeExecution() { + for (TestomatHook hook : HOOKS) { + hook.onExecutionStartHookBeforeExecution(); + } } protected void onExecutionFinishHookAfterExecution() { + for (TestomatHook hook : HOOKS) { + hook.onExecutionFinishHookAfterExecution(); + } } protected void onExecutionFinishHookBeforeExecution() { + for (TestomatHook hook : HOOKS) { + hook.onExecutionFinishHookBeforeExecution(); + } } } diff --git a/java-reporter-testng/src/main/java/io/testomat/testng/listener/FacadeFunctionsHandler.java b/java-reporter-testng/src/main/java/io/testomat/testng/listener/FacadeFunctionsHandler.java index 347450c1..52da4c0b 100644 --- a/java-reporter-testng/src/main/java/io/testomat/testng/listener/FacadeFunctionsHandler.java +++ b/java-reporter-testng/src/main/java/io/testomat/testng/listener/FacadeFunctionsHandler.java @@ -1,8 +1,11 @@ package io.testomat.testng.listener; import static io.testomat.core.constants.ArtifactPropertyNames.ARTIFACT_DISABLE_PROPERTY_NAME; +import static io.testomat.core.constants.ArtifactPropertyNames.JSONL_EXPORT_PROPERTY_NAME; +import io.testomat.core.facade.methods.artifact.TempArtifactDirectoriesStorage; import io.testomat.core.facade.methods.artifact.client.AwsService; +import io.testomat.core.facade.methods.artifact.client.JsonlService; import io.testomat.core.facade.methods.label.LabelStorage; import io.testomat.core.facade.methods.logmethod.LogStorage; import io.testomat.core.facade.methods.meta.MetaStorage; @@ -20,11 +23,13 @@ public class FacadeFunctionsHandler { private final TestNgMetaDataExtractor metaDataExtractor; private final PropertyProvider provider; private final AwsService awsService; + private final JsonlService jsonlService; public FacadeFunctionsHandler() { this.testNgParameterExtractor = new TestNgParameterExtractor(); this.metaDataExtractor = new TestNgMetaDataExtractor(); this.awsService = new AwsService(); + this.jsonlService = new JsonlService(); this.provider = PropertyProviderFactoryImpl.getPropertyProviderFactory().getPropertyProvider(); } @@ -32,16 +37,19 @@ public FacadeFunctionsHandler() { public FacadeFunctionsHandler(TestNgParameterExtractor testNgParameterExtractor, TestNgMetaDataExtractor metaDataExtractor, PropertyProvider provider, - AwsService awsService) { + AwsService awsService, + JsonlService jsonlService) { this.testNgParameterExtractor = testNgParameterExtractor; this.metaDataExtractor = metaDataExtractor; this.awsService = awsService; + this.jsonlService = jsonlService; this.provider = provider; } public void handleFacadeFunctions(IInvokedMethod method, ITestResult testResult) { handleMetaAfterInvocation(testResult); handleLogsAfterInvocation(testResult); + handleJsonlAfterInvocation(method, testResult); handleArtifactsAfterInvocation(method, testResult); handleLabels(testResult); } @@ -64,6 +72,16 @@ private void handleArtifactsAfterInvocation(IInvokedMethod method, ITestResult t method.getTestMethod().getConstructorOrMethod().getMethod()) ); } + TempArtifactDirectoriesStorage.DIRECTORIES.remove(); + } + + private void handleJsonlAfterInvocation(IInvokedMethod method, ITestResult testResult) { + if (!defineJsonlExportEnabled()) { + jsonlService.saveTestArtifacts(testResult.getName(), + testNgParameterExtractor.generateRid(testResult), + metaDataExtractor.getTestId( + method.getTestMethod().getConstructorOrMethod().getMethod())); + } } private void handleLogsAfterInvocation(ITestResult testResult) { @@ -98,4 +116,14 @@ private boolean defineArtifactsDisabled() { } return result; } + + private boolean defineJsonlExportEnabled() { + try { + return Boolean.parseBoolean( + provider.getProperty(JSONL_EXPORT_PROPERTY_NAME) + ); + } catch (Exception e) { + return false; + } + } } diff --git a/java-reporter-testng/src/main/java/io/testomat/testng/listener/TestomatHook.java b/java-reporter-testng/src/main/java/io/testomat/testng/listener/TestomatHook.java new file mode 100644 index 00000000..003ac6fa --- /dev/null +++ b/java-reporter-testng/src/main/java/io/testomat/testng/listener/TestomatHook.java @@ -0,0 +1,88 @@ +package io.testomat.testng.listener; + +import org.testng.IInvokedMethod; +import org.testng.ISuite; +import org.testng.ITestResult; + +public interface TestomatHook { + + default void onSuiteStartHookBeforeExecution(ISuite suite) { + + } + + default void onSuiteStartHookAfterExecution(ISuite suite) { + + } + + default void onSuiteFinishHookBeforeExecution(ISuite suite) { + + } + + default void onSuiteFinishHookAfterExecution(ISuite suite) { + + } + + default void onTestStartHookBeforeExecution(ITestResult result) { + + } + + default void onTestStartHookAfterExecution(ITestResult result) { + + } + + default void onTestSuccessHookBeforeExecution(ITestResult result) { + + } + + default void onTestSuccessHookAfterExecution(ITestResult result) { + + } + + default void onTestFailureHookBeforeExecution(ITestResult result) { + + } + + default void onTestFailureHookAfterExecution(ITestResult result) { + + } + + default void onTestSkippedHookBeforeExecution(ITestResult result) { + + } + + default void onTestSkippedHookAfterExecution(ITestResult result) { + + } + + default void beforeInvocationHookBeforeExecution(IInvokedMethod method, ITestResult result) { + + } + + default void beforeInvocationHookAfterExecution(IInvokedMethod method, ITestResult result) { + + } + + default void afterInvocationHookBeforeExecution(IInvokedMethod method, ITestResult result) { + + } + + default void afterInvocationAfter(IInvokedMethod method, ITestResult result) { + + } + + default void onExecutionStartHookBeforeExecution() { + + } + + default void onExecutionStartHookAfterExecution() { + + } + + default void onExecutionFinishHookBeforeExecution() { + + } + + default void onExecutionFinishHookAfterExecution() { + + } +} diff --git a/pom.xml b/pom.xml index 6079a9c0..3f8319fe 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.testomat java-reporter - 0.3.6 + 0.4.0 pom Testomat.io Java Reporter diff --git a/testomat-allure-adapter/pom.xml b/testomat-allure-adapter/pom.xml index 81fc96af..036aca92 100644 --- a/testomat-allure-adapter/pom.xml +++ b/testomat-allure-adapter/pom.xml @@ -6,7 +6,7 @@ io.testomat testomat-allure-adapter - 0.1.0 + 0.1.1 jar Testomat.io Testomat Allure adapter @@ -67,7 +67,7 @@ io.testomat java-reporter-core - 0.14.0 + 0.15.0 io.qameta.allure From 219a5f472c16c2312e227bc5362b438b41294d01 Mon Sep 17 00:00:00 2001 From: Nick Date: Mon, 6 Jul 2026 16:50:58 +0300 Subject: [PATCH 2/2] Issue-135. docs: document artifact export workflow in README --- README.md | 74 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 57 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index c10afad5..f135d0e7 100644 --- a/README.md +++ b/README.md @@ -382,19 +382,21 @@ Artifacts are stored in external S3 buckets. S3 Access can be configured in **tw > NOTE: Environment variables(env/jvm/testomatio.properties) take precedence over server provided credentials. -| Setting | Description | Default | -|-------------------------------------|-------------------------------------------------------|-------------| -| `testomatio.artifact.disable` | Completely disable artifact uploading | `false` | -| `testomatio.artifact.private` | Keep artifacts private (no public URLs) | `false` | -| `testomatio.step.artifacts.enabled` | Enables uploading artifacts for test steps | `false` | -| `s3.force-path-style` | Use path-style URLs for S3-compatible storage | `false` | -| `s3.endpoint` | Custom endpoint to be used with force-path-style | `false` | -| `s3.bucket` | Provides bucket name for configuration | | -| `s3.access-key-id` | Access key for the bucket | | -| `s3.secret.access-key-id` | Secret access key for the bucket | | -| `s3.region` | Bucket region | `us-west-1` | -| `s3.assume.role.arn` | AWS IAM role ARN used for AssumeRole authentication | | -| `s3.assume.role.external.id` | External ID for AssumeRole authentication | | +| Setting | Description | Default | +|-------------------------------------|-----------------------------------------------------|-------------------------------------------| +| `testomatio.artifact.disable` | Completely disable artifact uploading | `false` | +| `testomatio.artifact.private` | Keep artifacts private (no public URLs) | `false` | +| `testomatio.step.artifacts.enabled` | Enables uploading artifacts for test steps | `false` | +| `s3.force-path-style` | Use path-style URLs for S3-compatible storage | `false` | +| `s3.endpoint` | Custom endpoint to be used with force-path-style | `false` | +| `s3.bucket` | Provides bucket name for configuration | | +| `s3.access-key-id` | Access key for the bucket | | +| `s3.secret.access-key-id` | Secret access key for the bucket | | +| `s3.region` | Bucket region | `us-west-1` | +| `s3.assume.role.arn` | AWS IAM role ARN used for AssumeRole authentication | | +| `s3.assume.role.external.id` | External ID for AssumeRole authentication | | +| `testomatio.artifact.json.path` | Custom path to the JSONL export file | `target/testomat/testomat-artifacts.json` | +| `testomatio.artifact.json.disable` | Disable JSONL artifact export | `false` | **Note**: S3 credentials can be configured either in properties file or provided automatically on Testomat.io UI. Environment variables take precedence over server-provided credentials. @@ -506,6 +508,18 @@ As a result you will see something like this in the UI after the run is complete --- +## Artifact Export + +In addition to uploading artifacts directly to Testomat.io the reporter automatically exports artifact metadata to a JSONL file. + +The generated file can later be uploaded using the Testomat CLI: + +```bash +npx @testomatio/reporter upload-artifacts +``` + +--- + ## Step-by-Step Reporting Track detailed test execution flow using the `@Step` annotation. @@ -834,10 +848,36 @@ And the dashboard something like this: ## Advanced Customization -There are void hooks in the listeners that allow you to customize reporting much more. -These hooks are located in the listeners' tests lifecycle methods according to their names. -External API calls, logging and any custom logic can be added to the hooks. -The hooks are executed **after** the lifecycle method logic finishes and do not replace it. +The TestomatHook interface allows you to customize reporting by implementing hooks for the listeners' lifecycle methods. +Each hook corresponds to a specific test lifecycle event. Override only the methods you need—all hook methods have default empty implementations. +External API calls, logging, artifact uploads and any other custom logic can be added to the hooks. +Hooks with the *BeforeExecution suffix are executed before the listener's default logic, while hooks with the *AfterExecution suffix are executed after it. They extend the default behavior and do not replace it. + +Example: + +```java +import io.testomat.testng.listener.TestomatHook; +import org.testng.ITestResult; + +public class CustomHook implements TestomatHook { + + @Override + public void onTestFailureHookBeforeExecution(ITestResult result) { + // Your custom logic + } +} +``` + +Register your implementation via Java ServiceLoader by creating the following file in the resources directory: +``` +META-INF/services/io.testomat.junit.listener.TestomatHook +``` + +with the fully qualified name of your implementation: + + ```properties + com.yourcompany.yourproject.CustomListener + ``` ### JUnit, TestNG