From 948d2cb994c50e95bb91a8ce233ea95e5b9edd50 Mon Sep 17 00:00:00 2001 From: Nick Date: Thu, 19 Mar 2026 21:31:57 +0200 Subject: [PATCH 1/3] [DRAFT] Issue-105 Add artifacts support for steps --- .../io/testomat/core/annotation/Step.java | 1 + .../request/NativeRequestBodyBuilder.java | 21 ++++++++++ .../io/testomat/core/facade/Testomatio.java | 11 +++++ .../TempArtifactDirectoriesStorage.java | 10 +++++ .../methods/artifact/client/AwsService.java | 17 ++++++++ .../artifact/manager/ArtifactManager.java | 23 +++++++++-- .../io/testomat/core/step/StepAspect.java | 40 ++++++++++++++----- .../io/testomat/core/step/StepLifecycle.java | 23 +++++++++++ .../io/testomat/core/step/StepStatus.java | 5 +++ .../java/io/testomat/core/step/TestStep.java | 28 +++++++++++++ 10 files changed, 165 insertions(+), 14 deletions(-) create mode 100644 java-reporter-core/src/main/java/io/testomat/core/step/StepLifecycle.java create mode 100644 java-reporter-core/src/main/java/io/testomat/core/step/StepStatus.java diff --git a/java-reporter-core/src/main/java/io/testomat/core/annotation/Step.java b/java-reporter-core/src/main/java/io/testomat/core/annotation/Step.java index dd6587b2..9e9a3b1d 100644 --- a/java-reporter-core/src/main/java/io/testomat/core/annotation/Step.java +++ b/java-reporter-core/src/main/java/io/testomat/core/annotation/Step.java @@ -9,4 +9,5 @@ @Target(ElementType.METHOD) public @interface Step { String value() default ""; + String[] artifacts() default {}; } 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 f17c07d8..c82eafbc 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 @@ -14,6 +14,7 @@ import io.testomat.core.constants.ApiRequestFields; 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.label.LabelStorage; import io.testomat.core.facade.methods.logmethod.LogStorage; import io.testomat.core.facade.methods.meta.MetaStorage; @@ -158,6 +159,18 @@ private Map buildTestResultMap(TestResult result) throws JsonPro } if (result.getSteps() != null && !result.getSteps().isEmpty()) { + result.getSteps().forEach(step -> { + List links = + TempArtifactDirectoriesStorage.STEP_DIRECTORIES + .remove(step.getId()); + if (links == null || links.isEmpty()) { + return; + } + step.setArtifacts( + links.toArray(new String[0]) + ); + }); + List> stepsMap = convertStepsToMap(result.getSteps()); body.put("steps", stepsMap); System.out.println("DEBUG: Adding " + result.getSteps().size() + " steps to request body for test: " + result.getTitle()); @@ -203,6 +216,14 @@ private List> convertStepsToMap(List steps) { stepMap.put("title", step.getStepTitle()); } + if (step.getArtifacts() != null) { + stepMap.put("artifacts", step.getArtifacts()); + } + + if (step.getStatus() != null) { + stepMap.put("status", step.getStatus()); + } + stepMap.put("duration", step.getDuration()); if (step.getSubsteps() != null && !step.getSubsteps().isEmpty()) { 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 aac70ab8..87e252e0 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 @@ -4,8 +4,10 @@ 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.step.StepLifecycle; import java.util.List; import java.util.Map; +import java.util.UUID; /** * Main public API facade for Testomat.io integration. @@ -21,6 +23,15 @@ public static void artifact(String... directories) { ServiceRegistryUtil.getService(ArtifactManager.class).storeDirectories(directories); } + public static void stepArtifact(String... directories) { + UUID stepId = StepLifecycle.current(); + ServiceRegistryUtil.getService(ArtifactManager.class).storeStepDirectories(stepId, directories); + } + + public static void stepArtifact(UUID stepId, String... directories) { + ServiceRegistryUtil.getService(ArtifactManager.class).storeStepDirectories(stepId, directories); + } + public static void meta(String key, String value) { MetaStorage.TEMP_META_STORAGE.get().put(key, value); } 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 e86a7ec1..c4cf5fd2 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,10 @@ package io.testomat.core.facade.methods.artifact; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; /** * Thread-local storage for temporarily holding artifact file paths during test execution. @@ -9,8 +12,15 @@ */ public class TempArtifactDirectoriesStorage { public static final ThreadLocal> DIRECTORIES = ThreadLocal.withInitial(ArrayList::new); + public static final ConcurrentHashMap> STEP_DIRECTORIES = 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<>())) + .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 737caaa5..30be5f0c 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 @@ -74,6 +74,10 @@ public void uploadAllArtifactsForTest(String testName, String rid, String testId S3Credentials credentials = CredentialsManager.getCredentials(); List uploadedArtifactsLinks = processArtifacts(artifactDirectories, testName, rid, credentials); + if (!TempArtifactDirectoriesStorage.STEP_DIRECTORIES.isEmpty()) { + processStepArtifacts(testName, rid, credentials); + } + storeArtifactLinkData(testName, rid, testId, uploadedArtifactsLinks); // Clear artifact directories after processing @@ -92,6 +96,19 @@ private List processArtifacts(List artifactDirectories, String t return uploadedLinks; } + private void processStepArtifacts(String testName, String rid, S3Credentials credentials) { + + TempArtifactDirectoriesStorage.STEP_DIRECTORIES + .forEach((stepId, list) -> list.replaceAll(dir -> { + if (dir.startsWith("http")) { + return dir; + } + String key = keyGenerator.generateKey(dir, rid, testName); + uploadArtifact(dir, key, credentials); + return urlGenerator.generateUrl(credentials.getBucket(), key); + })); + } + private void storeArtifactLinkData(String testName, String rid, String testId, List uploadedLinks) { ArtifactLinkData linkData = new ArtifactLinkData(testName, rid, testId, uploadedLinks); ArtifactLinkDataStorage.ARTEFACT_LINK_DATA_STORAGE.add(linkData); diff --git a/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/manager/ArtifactManager.java b/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/manager/ArtifactManager.java index 0bca9d0c..5019a8f2 100644 --- a/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/manager/ArtifactManager.java +++ b/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/manager/ArtifactManager.java @@ -11,6 +11,8 @@ import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.UUID; +import java.util.function.Consumer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,12 +20,25 @@ public class ArtifactManager { private static final Logger log = LoggerFactory.getLogger(ArtifactManager.class); public void storeDirectories(String... directories) { + store(directories, + TempArtifactDirectoriesStorage::store, + "Invalid artifact path provided: {}"); + } + + public void storeStepDirectories(UUID stepId, String... directories) { + store(directories, + dir -> TempArtifactDirectoriesStorage.stepStore(stepId, dir), + "Invalid step artifact path provided: {}"); + } + + private void store(String[] directories, Consumer storage, String logMessage) { + for (String dir : directories) { - if (isValidFilePath(dir)) { - TempArtifactDirectoriesStorage.store(dir); - } else { - log.info("Invalid artifact path provided: {}", dir); + if (!isValidFilePath(dir)) { + log.info(logMessage, dir); + continue; } + storage.accept(dir); } } diff --git a/java-reporter-core/src/main/java/io/testomat/core/step/StepAspect.java b/java-reporter-core/src/main/java/io/testomat/core/step/StepAspect.java index 567aabab..01563869 100644 --- a/java-reporter-core/src/main/java/io/testomat/core/step/StepAspect.java +++ b/java-reporter-core/src/main/java/io/testomat/core/step/StepAspect.java @@ -1,6 +1,9 @@ package io.testomat.core.step; +import static io.testomat.core.facade.Testomatio.stepArtifact; + import io.testomat.core.annotation.Step; +import java.util.UUID; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; @@ -29,40 +32,45 @@ public class StepAspect { @Around("execution(@io.testomat.core.annotation.Step * *(..)) && @annotation(step)") public Object aroundStep(ProceedingJoinPoint joinPoint, Step step) throws Throwable { String stepName = resolveStepName(joinPoint, step); + String[] artifacts = resolveAttachments(step); long startTime = System.currentTimeMillis(); log.info("Step aspect triggered for: {}", stepName); - + StepLifecycle.start(UUID.randomUUID()); + Object result; try { - return executeStepSuccessfully(joinPoint, stepName, startTime); + result = executeStepSuccessfully(joinPoint, stepName, artifacts, startTime); } catch (Throwable e) { - handleStepFailure(stepName, startTime, e); + handleStepFailure(stepName, artifacts, startTime, e); throw e; + } finally { + StepLifecycle.finish(); } + return result; } - private Object executeStepSuccessfully(ProceedingJoinPoint joinPoint, String stepName, long startTime) throws Throwable { + private Object executeStepSuccessfully(ProceedingJoinPoint joinPoint, String stepName, String[] artifacts, long startTime) throws Throwable { Object result = joinPoint.proceed(); long duration = calculateDuration(startTime); - recordStep(stepName, duration); + recordStep(stepName, artifacts, StepStatus.passed, duration); log.info("Step '{}' added to storage. Total steps: {}", stepName, StepStorage.getSteps().size()); return result; } - private void handleStepFailure(String stepName, long startTime, Throwable e) { + private void handleStepFailure(String stepName, String[] artifacts, long startTime, Throwable e) { long duration = calculateDuration(startTime); log.error("Step '{}' failed after {} ms", stepName, duration, e); - recordStep(stepName, duration); + recordStep(stepName, artifacts, StepStatus.failed, duration); } private long calculateDuration(long startTime) { return System.currentTimeMillis() - startTime; } - private void recordStep(String stepName, long duration) { - TestStep testStep = createTestStep(stepName, duration); + private void recordStep(String stepName, String[] artifacts, StepStatus stepStatus, long duration) { + TestStep testStep = createTestStep(stepName, artifacts, stepStatus, duration); StepStorage.addStep(testStep); } @@ -87,6 +95,13 @@ private String getStepNameTemplate(ProceedingJoinPoint joinPoint, Step step) { return signature.getName(); } + private String[] resolveAttachments(Step step) { + if (step.artifacts() != null && step.artifacts().length > 0) { + return step.artifacts(); + } + return null; + } + /** * Substitutes parameter placeholders in the step name with actual parameter values. * Supports both indexed placeholders {0}, {1}, etc. and named placeholders {parameterName}. @@ -157,12 +172,17 @@ private String formatParameterValue(Object value) { * @param durationMillis the execution duration in milliseconds * @return populated TestStep object */ - private TestStep createTestStep(String stepName, long durationMillis) { + private TestStep createTestStep(String stepName, String[] artifacts, StepStatus stepStatus, long durationMillis) { TestStep testStep = new TestStep(); + testStep.setId(StepLifecycle.current()); testStep.setCategory("user"); testStep.setStepTitle(stepName); + testStep.setStatus(stepStatus); testStep.setDuration(durationMillis); + if (artifacts != null) { + stepArtifact(testStep.getId(), artifacts); + } log.debug("Step '{}' completed in {} ms", stepName, durationMillis); return testStep; diff --git a/java-reporter-core/src/main/java/io/testomat/core/step/StepLifecycle.java b/java-reporter-core/src/main/java/io/testomat/core/step/StepLifecycle.java new file mode 100644 index 00000000..199596b1 --- /dev/null +++ b/java-reporter-core/src/main/java/io/testomat/core/step/StepLifecycle.java @@ -0,0 +1,23 @@ +package io.testomat.core.step; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.UUID; + +public class StepLifecycle { + + private static final ThreadLocal> CURRENT_STEPS = + ThreadLocal.withInitial(ArrayDeque::new); + + public static void start(UUID stepId) { + CURRENT_STEPS.get().push(stepId); + } + + public static void finish() { + CURRENT_STEPS.get().pop(); + } + + public static UUID current() { + return CURRENT_STEPS.get().peek(); + } +} \ No newline at end of file diff --git a/java-reporter-core/src/main/java/io/testomat/core/step/StepStatus.java b/java-reporter-core/src/main/java/io/testomat/core/step/StepStatus.java new file mode 100644 index 00000000..d8143eb1 --- /dev/null +++ b/java-reporter-core/src/main/java/io/testomat/core/step/StepStatus.java @@ -0,0 +1,5 @@ +package io.testomat.core.step; + +public enum StepStatus { + passed, failed, none +} diff --git a/java-reporter-core/src/main/java/io/testomat/core/step/TestStep.java b/java-reporter-core/src/main/java/io/testomat/core/step/TestStep.java index 570cb437..78892b93 100644 --- a/java-reporter-core/src/main/java/io/testomat/core/step/TestStep.java +++ b/java-reporter-core/src/main/java/io/testomat/core/step/TestStep.java @@ -2,12 +2,16 @@ import java.util.ArrayList; import java.util.List; +import java.util.UUID; public class TestStep { + private UUID id; private String category; private String stepTitle; + private StepStatus status; private double duration; private List substeps = new ArrayList<>(); + private String[] artifacts; public String getCategory() { return category; @@ -40,4 +44,28 @@ public List getSubsteps() { public void setSubsteps(List substeps) { this.substeps = substeps; } + + public String[] getArtifacts() { + return artifacts; + } + + public void setArtifacts(String[] artifacts) { + this.artifacts = artifacts; + } + + public void setId(UUID id) { + this.id = id; + } + + public UUID getId() { + return id; + } + + public StepStatus getStatus() { + return status; + } + + public void setStatus(StepStatus status) { + this.status = status; + } } From 24176ae2fc9202ff18eb65d6ef6e25c671c4df2b Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 24 Mar 2026 22:40:55 +0200 Subject: [PATCH 2/3] Issue-105 Add an ability to add attachments to the current step and it substep --- java-reporter-core/pom.xml | 2 +- .../request/NativeRequestBodyBuilder.java | 40 +++-- .../core/constants/CommonConstants.java | 2 +- .../io/testomat/core/facade/Testomatio.java | 65 ++++++++- .../methods/artifact/client/AwsService.java | 1 - .../io/testomat/core/step/StepAspect.java | 36 +++-- .../io/testomat/core/step/StepLifecycle.java | 67 ++++++++- .../io/testomat/core/step/StepStorage.java | 2 +- .../java/io/testomat/core/step/TestStep.java | 22 +++ .../TempArtifactDirectoriesStorageTest.java | 138 ++++++++++++++++++ .../io/testomat/core/step/StepAspectTest.java | 66 +++++++++ .../testomat/core/step/StepLifecycleTest.java | 81 ++++++++++ java-reporter-cucumber/pom.xml | 4 +- .../CucumberTestResultConstructor.java | 2 + java-reporter-junit/pom.xml | 4 +- .../JUnitTestResultConstructor.java | 2 + java-reporter-karate/pom.xml | 4 +- .../KarateTestResultConstructor.java | 2 + .../io/testomat/karate/hooks/KarateHook.java | 25 +++- .../testomat/karate/hooks/KarateHookTest.java | 61 +++++--- java-reporter-testng/pom.xml | 4 +- .../TestNgTestResultConstructor.java | 2 + pom.xml | 2 +- 23 files changed, 565 insertions(+), 69 deletions(-) create mode 100644 java-reporter-core/src/test/java/io/testomat/core/step/StepLifecycleTest.java diff --git a/java-reporter-core/pom.xml b/java-reporter-core/pom.xml index e92b44da..4ed47e78 100644 --- a/java-reporter-core/pom.xml +++ b/java-reporter-core/pom.xml @@ -7,7 +7,7 @@ io.testomat java-reporter-core - 0.9.3 + 0.10.0 jar Testomat.io Reporter Core 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 c82eafbc..331467b9 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 @@ -159,18 +159,7 @@ private Map buildTestResultMap(TestResult result) throws JsonPro } if (result.getSteps() != null && !result.getSteps().isEmpty()) { - result.getSteps().forEach(step -> { - List links = - TempArtifactDirectoriesStorage.STEP_DIRECTORIES - .remove(step.getId()); - if (links == null || links.isEmpty()) { - return; - } - step.setArtifacts( - links.toArray(new String[0]) - ); - }); - + result.getSteps().forEach(this::processStepArtifacts); List> stepsMap = convertStepsToMap(result.getSteps()); body.put("steps", stepsMap); System.out.println("DEBUG: Adding " + result.getSteps().size() + " steps to request body for test: " + result.getTitle()); @@ -220,6 +209,14 @@ private List> convertStepsToMap(List steps) { stepMap.put("artifacts", step.getArtifacts()); } + if (step.getError() != null) { + stepMap.put("error", step.getError()); + } + + if (step.getLog() != null) { + stepMap.put("log", step.getLog()); + } + if (step.getStatus() != null) { stepMap.put("status", step.getStatus()); } @@ -297,4 +294,23 @@ private void addLinks(Map body, String rid) { links.addAll(labels); } } + + /** + * Recursively attaches stored artifacts to a step and its substeps. + * + * @param step step to process + */ + private void processStepArtifacts(TestStep step) { + List links = + TempArtifactDirectoriesStorage.STEP_DIRECTORIES + .remove(step.getId()); + + if (links != null && !links.isEmpty()) { + step.setArtifacts(links.toArray(new String[0])); + } + + if (step.getSubsteps() != null && !step.getSubsteps().isEmpty()) { + step.getSubsteps().forEach(this::processStepArtifacts); + } + } } \ No newline at end of file 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 5a4032f0..0df6885c 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.9.3"; + public static final String REPORTER_VERSION = "0.10.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/facade/Testomatio.java b/java-reporter-core/src/main/java/io/testomat/core/facade/Testomatio.java index 87e252e0..78a6bdaa 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 @@ -5,8 +5,13 @@ import io.testomat.core.facade.methods.logmethod.LogStorage; import io.testomat.core.facade.methods.meta.MetaStorage; import io.testomat.core.step.StepLifecycle; +import io.testomat.core.step.StepStatus; +import io.testomat.core.step.StepTimer; +import io.testomat.core.step.TestStep; +import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.UUID; /** @@ -23,15 +28,71 @@ public static void artifact(String... directories) { ServiceRegistryUtil.getService(ArtifactManager.class).storeDirectories(directories); } + /** + * Attaches artifact directories to the current or last finished test step. + * + * @param directories artifact directories to attach (ignored if null or empty) + */ public static void stepArtifact(String... directories) { - UUID stepId = StepLifecycle.current(); - ServiceRegistryUtil.getService(ArtifactManager.class).storeStepDirectories(stepId, directories); + TestStep testStep = StepLifecycle.current(); + + if(directories == null || directories.length == 0){ + return; + } + + if (testStep == null) { + testStep = StepLifecycle.lastFinished(); + } + if (testStep == null || testStep.getId() == null) { + return; + } + + ServiceRegistryUtil.getService(ArtifactManager.class).storeStepDirectories(testStep.getId(), directories); } + /** + * Attaches artifact directories to the specified test step. + * + * @param stepId step identifier + * @param directories artifact directories to attach + */ public static void stepArtifact(UUID stepId, String... directories) { ServiceRegistryUtil.getService(ArtifactManager.class).storeStepDirectories(stepId, directories); } + /** + * Executes a named test step and tracks its status, duration and errors. + * + * @param stepName step display name + * @param action code to execute inside the step + */ + public static void step(String stepName, Runnable action) { + TestStep step = new TestStep(); + step.setCategory("user"); + step.setStepTitle(stepName); + + long durationMillis; + StepLifecycle.start(step); + + try { + StepTimer.start(step.getId().toString()); + action.run(); + step.setStatus(StepStatus.passed); + } catch (Throwable t) { + step.setStatus(StepStatus.failed); + step.setLog(Arrays.toString(t.getStackTrace())); + step.setError( + Optional.ofNullable(t.getMessage()) + .orElse(t.getClass().getSimpleName()) + ); + throw new RuntimeException(t); + } finally { + durationMillis = StepTimer.stop(step.getId().toString()); + step.setDuration(durationMillis); + StepLifecycle.finish(); + } + } + public static void meta(String key, String value) { MetaStorage.TEMP_META_STORAGE.get().put(key, value); } 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 30be5f0c..b10d4aa0 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 @@ -97,7 +97,6 @@ private List processArtifacts(List artifactDirectories, String t } private void processStepArtifacts(String testName, String rid, S3Credentials credentials) { - TempArtifactDirectoriesStorage.STEP_DIRECTORIES .forEach((stepId, list) -> list.replaceAll(dir -> { if (dir.startsWith("http")) { diff --git a/java-reporter-core/src/main/java/io/testomat/core/step/StepAspect.java b/java-reporter-core/src/main/java/io/testomat/core/step/StepAspect.java index 01563869..44ef4d8d 100644 --- a/java-reporter-core/src/main/java/io/testomat/core/step/StepAspect.java +++ b/java-reporter-core/src/main/java/io/testomat/core/step/StepAspect.java @@ -3,7 +3,7 @@ import static io.testomat.core.facade.Testomatio.stepArtifact; import io.testomat.core.annotation.Step; -import java.util.UUID; +import java.util.Arrays; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; @@ -33,10 +33,10 @@ public class StepAspect { public Object aroundStep(ProceedingJoinPoint joinPoint, Step step) throws Throwable { String stepName = resolveStepName(joinPoint, step); String[] artifacts = resolveAttachments(step); + createTestStep(); long startTime = System.currentTimeMillis(); log.info("Step aspect triggered for: {}", stepName); - StepLifecycle.start(UUID.randomUUID()); Object result; try { result = executeStepSuccessfully(joinPoint, stepName, artifacts, startTime); @@ -62,16 +62,17 @@ private Object executeStepSuccessfully(ProceedingJoinPoint joinPoint, String ste private void handleStepFailure(String stepName, String[] artifacts, long startTime, Throwable e) { long duration = calculateDuration(startTime); log.error("Step '{}' failed after {} ms", stepName, duration, e); - recordStep(stepName, artifacts, StepStatus.failed, duration); + TestStep testStep = recordStep(stepName, artifacts, StepStatus.failed, duration); + testStep.setError(e.getMessage()); + testStep.setLog(Arrays.toString(e.getStackTrace())); } private long calculateDuration(long startTime) { return System.currentTimeMillis() - startTime; } - private void recordStep(String stepName, String[] artifacts, StepStatus stepStatus, long duration) { - TestStep testStep = createTestStep(stepName, artifacts, stepStatus, duration); - StepStorage.addStep(testStep); + private TestStep recordStep(String stepName, String[] artifacts, StepStatus stepStatus, long duration) { + return initTestStep(stepName, artifacts, stepStatus, duration); } /** @@ -166,15 +167,24 @@ private String formatParameterValue(Object value) { } /** - * Creates a TestStep object with the provided metadata. - * - * @param stepName the name of the step - * @param durationMillis the execution duration in milliseconds - * @return populated TestStep object + * Initializes and starts a new {@link TestStep}. */ - private TestStep createTestStep(String stepName, String[] artifacts, StepStatus stepStatus, long durationMillis) { + private void createTestStep() { TestStep testStep = new TestStep(); - testStep.setId(StepLifecycle.current()); + StepLifecycle.start(testStep); + } + + /** + * Initializes the current test step with metadata and optional artifacts. + * + * @param stepName step name + * @param artifacts artifact directories (optional) + * @param stepStatus step execution status + * @param durationMillis step duration in milliseconds + * @return initialized test step + */ + private TestStep initTestStep(String stepName, String[] artifacts, StepStatus stepStatus, long durationMillis) { + TestStep testStep = StepLifecycle.current(); testStep.setCategory("user"); testStep.setStepTitle(stepName); testStep.setStatus(stepStatus); diff --git a/java-reporter-core/src/main/java/io/testomat/core/step/StepLifecycle.java b/java-reporter-core/src/main/java/io/testomat/core/step/StepLifecycle.java index 199596b1..dd275515 100644 --- a/java-reporter-core/src/main/java/io/testomat/core/step/StepLifecycle.java +++ b/java-reporter-core/src/main/java/io/testomat/core/step/StepLifecycle.java @@ -2,22 +2,77 @@ import java.util.ArrayDeque; import java.util.Deque; -import java.util.UUID; +/** + * Manages the lifecycle of test steps including nesting, tracking current step + * and accessing the last finished step. Uses ThreadLocal storage to support + * parallel test execution. + */ public class StepLifecycle { - private static final ThreadLocal> CURRENT_STEPS = + private static final ThreadLocal> CURRENT_STEPS = ThreadLocal.withInitial(ArrayDeque::new); + private static final ThreadLocal LAST_FINISHED = + new ThreadLocal<>(); - public static void start(UUID stepId) { - CURRENT_STEPS.get().push(stepId); + /** + * Starts a new test step and registers it as a child of the current step + * if one exists. + * + * @param step step to start + */ + public static void start(TestStep step) { + Deque stack = CURRENT_STEPS.get(); + TestStep parent = stack.peek(); + + if (parent != null) { + parent.getSubsteps().add(step); + } else { + StepStorage.addStep(step); + } + stack.push(step); } + /** + * Finishes the current step and stores it as the last finished step. + */ public static void finish() { - CURRENT_STEPS.get().pop(); + Deque stack = CURRENT_STEPS.get(); + if (!stack.isEmpty()) { + LAST_FINISHED.set(stack.pop()); + } + if(stack.isEmpty()) { + CURRENT_STEPS.remove(); + } } - public static UUID current() { + /** + * Returns the currently active step. + * + * @return current step or null if none exists + */ + public static TestStep current(){ return CURRENT_STEPS.get().peek(); } + + /** + * Returns the last finished step. + * + * @return last finished step or null + */ + public static TestStep lastFinished() { + TestStep lastStep = LAST_FINISHED.get(); + if (lastStep != null) { + LAST_FINISHED.remove(); + } + return lastStep; + } + + /** + * Clears lifecycle state for the current thread. + */ + public static void reset(){ + CURRENT_STEPS.remove(); + LAST_FINISHED.remove(); + } } \ No newline at end of file diff --git a/java-reporter-core/src/main/java/io/testomat/core/step/StepStorage.java b/java-reporter-core/src/main/java/io/testomat/core/step/StepStorage.java index b59377aa..ab9b2007 100644 --- a/java-reporter-core/src/main/java/io/testomat/core/step/StepStorage.java +++ b/java-reporter-core/src/main/java/io/testomat/core/step/StepStorage.java @@ -33,6 +33,6 @@ public static List getSteps() { * Should be called after reporting test results. */ public static void clear() { - STEPS.get().clear(); + STEPS.remove(); } } diff --git a/java-reporter-core/src/main/java/io/testomat/core/step/TestStep.java b/java-reporter-core/src/main/java/io/testomat/core/step/TestStep.java index 78892b93..f9cdbad8 100644 --- a/java-reporter-core/src/main/java/io/testomat/core/step/TestStep.java +++ b/java-reporter-core/src/main/java/io/testomat/core/step/TestStep.java @@ -9,10 +9,16 @@ public class TestStep { private String category; private String stepTitle; private StepStatus status; + private String log; + private String error; private double duration; private List substeps = new ArrayList<>(); private String[] artifacts; + public TestStep() { + this.id = UUID.randomUUID(); + } + public String getCategory() { return category; } @@ -68,4 +74,20 @@ public StepStatus getStatus() { public void setStatus(StepStatus status) { this.status = status; } + + public String getError() { + return error; + } + + public void setError(String error) { + this.error = error; + } + + public String getLog() { + return log; + } + + public void setLog(String log) { + this.log = log; + } } 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 2bfd6537..bdf303a6 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 @@ -3,9 +3,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; import io.testomat.core.facade.methods.artifact.TempArtifactDirectoriesStorage; +import java.util.UUID; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -20,12 +23,14 @@ class TempArtifactDirectoriesStorageTest { void setUp() { // Clean storage before each test TempArtifactDirectoriesStorage.DIRECTORIES.get().clear(); + TempArtifactDirectoriesStorage.STEP_DIRECTORIES.clear(); } @AfterEach void tearDown() { // Clean storage after each test TempArtifactDirectoriesStorage.DIRECTORIES.remove(); + TempArtifactDirectoriesStorage.STEP_DIRECTORIES.clear(); } @Test @@ -292,4 +297,137 @@ void testMultipleStoreOperationsInSequence() { assertEquals("dir-" + i, directories.get(i)); } } + + @Test + @DisplayName("Should store step directory") + void stepStoreShouldStoreDirectory(){ + UUID stepId = UUID.randomUUID(); + + TempArtifactDirectoriesStorage.stepStore(stepId,"dir1"); + + List dirs = TempArtifactDirectoriesStorage.STEP_DIRECTORIES.get(stepId); + + assertNotNull(dirs); + assertEquals(1,dirs.size()); + assertEquals("dir1",dirs.get(0)); + } + + @Test + @DisplayName("Should store multiple directories per step") + void stepStoreShouldStoreMultiple(){ + UUID stepId = UUID.randomUUID(); + + TempArtifactDirectoriesStorage.stepStore(stepId,"dir1"); + TempArtifactDirectoriesStorage.stepStore(stepId,"dir2"); + + List dirs = TempArtifactDirectoriesStorage.STEP_DIRECTORIES.get(stepId); + + assertEquals(2,dirs.size()); + assertTrue(dirs.contains("dir1")); + assertTrue(dirs.contains("dir2")); + } + + @Test + @DisplayName("Should isolate different step ids") + void stepStoreShouldIsolateSteps(){ + UUID step1 = UUID.randomUUID(); + UUID step2 = UUID.randomUUID(); + + TempArtifactDirectoriesStorage.stepStore(step1,"dir1"); + TempArtifactDirectoriesStorage.stepStore(step2,"dir2"); + + assertEquals(1, + TempArtifactDirectoriesStorage + .STEP_DIRECTORIES.get(step1).size()); + + assertEquals(1, + TempArtifactDirectoriesStorage + .STEP_DIRECTORIES.get(step2).size()); + + assertFalse( + TempArtifactDirectoriesStorage + .STEP_DIRECTORIES.get(step1) + .contains("dir2") + ); + } + + @Test + @DisplayName("Should be thread safe for step store") + void stepStoreShouldBeThreadSafe() throws InterruptedException { + UUID stepId = UUID.randomUUID(); + + Thread t1=new Thread(() -> { + for(int i = 0; i < 100; i++){ + TempArtifactDirectoriesStorage.stepStore(stepId,"t1-"+i); + } + }); + + Thread t2=new Thread(() -> { + for(int i = 0; i < 100; i++){ + TempArtifactDirectoriesStorage.stepStore(stepId,"t2-"+i); + } + }); + + t1.start(); + t2.start(); + + t1.join(); + t2.join(); + + List dirs= TempArtifactDirectoriesStorage.STEP_DIRECTORIES.get(stepId); + + assertEquals(200,dirs.size()); + } + + @Test + @DisplayName("Should create list only once") + void stepStoreShouldReuseList(){ + UUID stepId=UUID.randomUUID(); + + TempArtifactDirectoriesStorage + .stepStore(stepId,"dir1"); + + List first= + TempArtifactDirectoriesStorage + .STEP_DIRECTORIES.get(stepId); + + TempArtifactDirectoriesStorage + .stepStore(stepId,"dir2"); + + List second= + TempArtifactDirectoriesStorage + .STEP_DIRECTORIES.get(stepId); + + assertSame(first,second); + } + + @Test + void stepStoreShouldHandleNullDir(){ + UUID stepId=UUID.randomUUID(); + + TempArtifactDirectoriesStorage + .stepStore(stepId,null); + + List dirs= + TempArtifactDirectoriesStorage + .STEP_DIRECTORIES.get(stepId); + + assertEquals(1,dirs.size()); + assertNull(dirs.get(0)); + } + + @Test + void stepDirectoriesShouldAllowRemoval(){ + UUID stepId=UUID.randomUUID(); + + TempArtifactDirectoriesStorage + .stepStore(stepId,"dir"); + + TempArtifactDirectoriesStorage + .STEP_DIRECTORIES.remove(stepId); + + assertNull( + TempArtifactDirectoriesStorage + .STEP_DIRECTORIES.get(stepId)); + } } diff --git a/java-reporter-core/src/test/java/io/testomat/core/step/StepAspectTest.java b/java-reporter-core/src/test/java/io/testomat/core/step/StepAspectTest.java index d924b4a7..25a40792 100644 --- a/java-reporter-core/src/test/java/io/testomat/core/step/StepAspectTest.java +++ b/java-reporter-core/src/test/java/io/testomat/core/step/StepAspectTest.java @@ -235,4 +235,70 @@ public String toString() { return "User{name='" + name + "', age=" + age + "}"; } } + + @Test + void testFailureStatus(){ + assertThrows(RuntimeException.class, this::stepThatThrows); + TestStep step = StepStorage.getSteps().get(0); + assertEquals(StepStatus.failed, step.getStatus()); + } + + @Test + void testErrorMessageStored(){ + assertThrows(RuntimeException.class, this::stepThatThrows); + TestStep step= StepStorage.getSteps().get(0); + assertEquals("Test exception", step.getError()); + } + + @Test + void testNoParameters(){ + simpleStep(); + + TestStep step= StepStorage.getSteps().get(0); + assertEquals("No params", step.getStepTitle()); + } + + @Test + void testReturnValue(){ + String result= stepReturningValue(); + assertEquals("ok",result); + } + + @Test + void testParameterMismatch(){ + methodWithVarArgs("a","b"); + assertEquals(1, StepStorage.getSteps().size()); + } + + @Test + void testNoArtifacts(){ + stepWithoutArtifacts(); + + TestStep step = StepStorage.getSteps().get(0); + assertNull(step.getArtifacts()); + } + + @Test + void testLifecycleCleanup(){ + try { + stepThatThrows(); + } catch (Exception ignored) { + } + assertNull(StepLifecycle.current()); + } + + @Step("no artifacts") + private void stepWithoutArtifacts(){} + + @Step("test {0}") + private void methodWithVarArgs(String... args){} + + @Step("return") + private String stepReturningValue(){ + return "ok"; + } + + @Step("No params") + private void simpleStep(){} + } diff --git a/java-reporter-core/src/test/java/io/testomat/core/step/StepLifecycleTest.java b/java-reporter-core/src/test/java/io/testomat/core/step/StepLifecycleTest.java new file mode 100644 index 00000000..22e8fe5e --- /dev/null +++ b/java-reporter-core/src/test/java/io/testomat/core/step/StepLifecycleTest.java @@ -0,0 +1,81 @@ +package io.testomat.core.step; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class StepLifecycleTest { + + @AfterEach + void cleanup(){ + StepStorage.clear(); + StepLifecycle.reset(); + } + + @Test + void shouldStartStep() { + TestStep step = new TestStep(); + StepLifecycle.start(step); + TestStep current = StepLifecycle.current(); + + assertNotNull(current); + assertEquals(step, current); + } + + @Test + void shouldHandleNestedSteps() { + TestStep parent = new TestStep(); + TestStep child = new TestStep(); + + StepLifecycle.start(parent); + StepLifecycle.start(child); + + assertEquals(child, StepLifecycle.current()); + assertEquals(1, parent.getSubsteps().size()); + assertEquals(child, parent.getSubsteps().get(0)); + } + + @Test + void shouldFinishStep() { + TestStep step = new TestStep(); + + StepLifecycle.start(step); + StepLifecycle.finish(); + + TestStep last = StepLifecycle.lastFinished(); + + assertEquals(step,last); + assertNull(StepLifecycle.current()); + } + + @Test + void shouldResetState() { + TestStep step = new TestStep(); + + StepLifecycle.start(step); + StepLifecycle.reset(); + + assertNull(StepLifecycle.current()); + assertNull(StepLifecycle.lastFinished()); + } + + @Test + void shouldSupportMultipleFinishCalls() { + TestStep parent = new TestStep(); + TestStep child = new TestStep(); + + StepLifecycle.start(parent); + StepLifecycle.start(child); + + StepLifecycle.finish(); + + assertEquals(child,StepLifecycle.lastFinished()); + assertEquals(parent,StepLifecycle.current()); + + StepLifecycle.finish(); + + assertEquals(parent,StepLifecycle.lastFinished()); + assertNull(StepLifecycle.current()); + } +} diff --git a/java-reporter-cucumber/pom.xml b/java-reporter-cucumber/pom.xml index 084fd094..4f3933d6 100644 --- a/java-reporter-cucumber/pom.xml +++ b/java-reporter-cucumber/pom.xml @@ -6,7 +6,7 @@ io.testomat java-reporter-cucumber - 0.7.11 + 0.7.12 jar Testomat.io Java Reporter Cucumber @@ -51,7 +51,7 @@ io.testomat java-reporter-core - 0.9.3 + 0.10.0 org.slf4j diff --git a/java-reporter-cucumber/src/main/java/io/testomat/cucumber/constructor/CucumberTestResultConstructor.java b/java-reporter-cucumber/src/main/java/io/testomat/cucumber/constructor/CucumberTestResultConstructor.java index 6fb01cb6..975e6aa3 100644 --- a/java-reporter-cucumber/src/main/java/io/testomat/cucumber/constructor/CucumberTestResultConstructor.java +++ b/java-reporter-cucumber/src/main/java/io/testomat/cucumber/constructor/CucumberTestResultConstructor.java @@ -3,6 +3,7 @@ import io.cucumber.plugin.event.TestCaseFinished; import io.testomat.core.model.ExceptionDetails; import io.testomat.core.model.TestResult; +import io.testomat.core.step.StepLifecycle; import io.testomat.core.step.StepStorage; import io.testomat.core.step.TestStep; import io.testomat.cucumber.extractor.TestDataExtractor; @@ -66,6 +67,7 @@ public TestResult constructTestRunResult(TestCaseFinished event) { // Clear steps after collecting them StepStorage.clear(); + StepLifecycle.reset(); return builder.build(); } diff --git a/java-reporter-junit/pom.xml b/java-reporter-junit/pom.xml index 05f70185..b60ec66a 100644 --- a/java-reporter-junit/pom.xml +++ b/java-reporter-junit/pom.xml @@ -6,7 +6,7 @@ io.testomat java-reporter-junit - 0.8.2 + 0.8.3 jar Testomat.io Java Reporter JUnit @@ -51,7 +51,7 @@ io.testomat java-reporter-core - 0.9.3 + 0.10.0 org.slf4j diff --git a/java-reporter-junit/src/main/java/io/testomat/junit/constructor/JUnitTestResultConstructor.java b/java-reporter-junit/src/main/java/io/testomat/junit/constructor/JUnitTestResultConstructor.java index f8adac6a..a07cc186 100644 --- a/java-reporter-junit/src/main/java/io/testomat/junit/constructor/JUnitTestResultConstructor.java +++ b/java-reporter-junit/src/main/java/io/testomat/junit/constructor/JUnitTestResultConstructor.java @@ -3,6 +3,7 @@ import io.testomat.core.model.ExceptionDetails; import io.testomat.core.model.TestMetadata; import io.testomat.core.model.TestResult; +import io.testomat.core.step.StepLifecycle; import io.testomat.core.step.StepStorage; import io.testomat.core.step.TestStep; import io.testomat.junit.exception.ReporterException; @@ -145,6 +146,7 @@ private TestResult createTestResult(TestMetadata metadata, // Clear steps after collecting them StepStorage.clear(); + StepLifecycle.reset(); return builder.build(); } diff --git a/java-reporter-karate/pom.xml b/java-reporter-karate/pom.xml index 0e8b9ce3..2c7d69cf 100644 --- a/java-reporter-karate/pom.xml +++ b/java-reporter-karate/pom.xml @@ -6,7 +6,7 @@ io.testomat java-reporter-karate - 0.2.5 + 0.2.6 jar Testomat.io Java Reporter Karate @@ -52,7 +52,7 @@ io.testomat java-reporter-core - 0.9.3 + 0.10.0 io.karatelabs diff --git a/java-reporter-karate/src/main/java/io/testomat/karate/constructor/KarateTestResultConstructor.java b/java-reporter-karate/src/main/java/io/testomat/karate/constructor/KarateTestResultConstructor.java index e977e7e9..9af3e36e 100644 --- a/java-reporter-karate/src/main/java/io/testomat/karate/constructor/KarateTestResultConstructor.java +++ b/java-reporter-karate/src/main/java/io/testomat/karate/constructor/KarateTestResultConstructor.java @@ -3,6 +3,7 @@ import com.intuit.karate.core.ScenarioRuntime; import io.testomat.core.model.ExceptionDetails; import io.testomat.core.model.TestResult; +import io.testomat.core.step.StepLifecycle; import io.testomat.core.step.StepStorage; import io.testomat.core.step.TestStep; import io.testomat.karate.extractor.TestDataExtractor; @@ -66,6 +67,7 @@ public TestResult constructTestRunResult(ScenarioRuntime sr) { // Clear steps after collecting them StepStorage.clear(); + StepLifecycle.reset(); return builder.build(); } diff --git a/java-reporter-karate/src/main/java/io/testomat/karate/hooks/KarateHook.java b/java-reporter-karate/src/main/java/io/testomat/karate/hooks/KarateHook.java index 76d3a912..c2cf45d7 100644 --- a/java-reporter-karate/src/main/java/io/testomat/karate/hooks/KarateHook.java +++ b/java-reporter-karate/src/main/java/io/testomat/karate/hooks/KarateHook.java @@ -4,13 +4,15 @@ import com.intuit.karate.RuntimeHook; import com.intuit.karate.Suite; +import com.intuit.karate.core.Result; import com.intuit.karate.core.ScenarioRuntime; import com.intuit.karate.core.Step; import com.intuit.karate.core.StepResult; import io.testomat.core.exception.ReportTestResultException; import io.testomat.core.model.TestResult; import io.testomat.core.runmanager.GlobalRunManager; -import io.testomat.core.step.StepStorage; +import io.testomat.core.step.StepLifecycle; +import io.testomat.core.step.StepStatus; import io.testomat.core.step.StepTimer; import io.testomat.core.step.TestStep; import io.testomat.karate.adapter.CustomKarateEngineAdapter; @@ -80,6 +82,7 @@ public boolean beforeStep(Step step, ScenarioRuntime sr) { boolean logAllSteps = isDslStep && sr.tags.getTags().contains(LOG_STEPS); if (logAllSteps || isMarked) { + StepLifecycle.start(new TestStep()); engine.setVariable(LOG_NEXT_STEP, false); String stepId = Thread.currentThread().getId() + ":" + System.identityHashCode(step); StepTimer.start(stepId); @@ -102,12 +105,28 @@ public void afterStep(StepResult result, ScenarioRuntime sr) { engine.setVariable(LOG_NEXT_STEP_TITLE, null); } - TestStep testStep = new TestStep(); + TestStep testStep = StepLifecycle.current(); testStep.setCategory("user"); testStep.setStepTitle(stepName); testStep.setDuration(durationMillis); - StepStorage.addStep(testStep); + Result karateResult = result.getResult(); + + if (karateResult == null) { + testStep.setStatus(StepStatus.none); + } else if (karateResult.isFailed()) { + testStep.setStatus(StepStatus.failed); + Throwable error = karateResult.getError(); + if (error != null) { + testStep.setLog(error.getMessage()); + } + } else if ("passed".equals(karateResult.getStatus())) { + testStep.setStatus(StepStatus.passed); + } else { + testStep.setStatus(StepStatus.none); + } + + StepLifecycle.finish(); log.debug("Step '{}' completed in {} ms", stepName, durationMillis); } diff --git a/java-reporter-karate/src/test/java/io/testomat/karate/hooks/KarateHookTest.java b/java-reporter-karate/src/test/java/io/testomat/karate/hooks/KarateHookTest.java index 63ae9a75..67814285 100644 --- a/java-reporter-karate/src/test/java/io/testomat/karate/hooks/KarateHookTest.java +++ b/java-reporter-karate/src/test/java/io/testomat/karate/hooks/KarateHookTest.java @@ -1,9 +1,9 @@ package io.testomat.karate.hooks; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; @@ -14,6 +14,7 @@ import static org.mockito.Mockito.when; import com.intuit.karate.Suite; +import com.intuit.karate.core.Result; import com.intuit.karate.core.Scenario; import com.intuit.karate.core.ScenarioEngine; import com.intuit.karate.core.ScenarioRuntime; @@ -23,7 +24,8 @@ import io.testomat.core.exception.ReportTestResultException; import io.testomat.core.model.TestResult; import io.testomat.core.runmanager.GlobalRunManager; -import io.testomat.core.step.StepStorage; +import io.testomat.core.step.StepLifecycle; +import io.testomat.core.step.StepStatus; import io.testomat.core.step.StepTimer; import io.testomat.core.step.TestStep; import io.testomat.karate.adapter.CustomKarateEngineAdapter; @@ -33,9 +35,11 @@ import java.lang.reflect.Field; import java.util.Collections; import java.util.List; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.MockedStatic; +import org.mockito.Mockito; class KarateHookTest { @@ -54,6 +58,11 @@ void setUp() { hook = new KarateHook(constructor, functionsHandler, runManager, engine); } + @AfterEach + void cleanup() { + StepLifecycle.reset(); + } + @Test void shouldIncrementSuiteCounter() { hook.beforeSuite(mock(Suite.class)); @@ -207,33 +216,41 @@ void shouldStoreStepWhenTimerStopped() { StepResult result = mock(StepResult.class); Step step = mock(Step.class); ScenarioRuntime sr = mock(ScenarioRuntime.class); + Result karateResult = mock(Result.class); when(result.getStep()).thenReturn(step); + when(result.getResult()).thenReturn(karateResult); + when(karateResult.isFailed()).thenReturn(false); + when(karateResult.getStatus()).thenReturn("passed"); when(step.getText()).thenReturn("Some step"); - ScenarioEngine engine = mock(ScenarioEngine.class); - - try (MockedStatic mockedEngine = mockStatic(ScenarioEngine.class); - MockedStatic mockedTimer = mockStatic(StepTimer.class); - MockedStatic storage = mockStatic(StepStorage.class)) { - - mockedEngine.when(ScenarioEngine::get).thenReturn(engine); - mockedTimer.when(() -> StepTimer.stop(any())).thenReturn(100L); - + try (MockedStatic timer = mockStatic(StepTimer.class)) { + TestStep testStep = new TestStep(); + StepLifecycle.start(testStep); + timer.when(() -> StepTimer.stop(Mockito.anyString())) + .thenReturn(100L); hook.afterStep(result, sr); - storage.verify(() -> StepStorage.addStep(any(TestStep.class))); + TestStep finished = StepLifecycle.lastFinished(); + + assertEquals("Some step", finished.getStepTitle()); + assertEquals(100L, finished.getDuration()); + assertEquals("user", finished.getCategory()); + assertEquals(StepStatus.passed, finished.getStatus()); // ← добавь } } @Test void shouldUseCustomStepTitle() { - StepResult result = mock(StepResult.class); Step step = mock(Step.class); ScenarioRuntime sr = mock(ScenarioRuntime.class); + Result karateResult = mock(Result.class); when(result.getStep()).thenReturn(step); + when(result.getResult()).thenReturn(karateResult); + when(karateResult.isFailed()).thenReturn(false); + when(karateResult.getStatus()).thenReturn("passed"); when(step.getText()).thenReturn("Original"); KarateEngineAdapter engine = mock(KarateEngineAdapter.class); @@ -241,10 +258,11 @@ void shouldUseCustomStepTitle() { when(engine.getVariable("log_next_step_title")) .thenReturn("Custom title"); - try (MockedStatic timer = mockStatic(StepTimer.class); - MockedStatic storage = mockStatic(StepStorage.class)) { - - timer.when(() -> StepTimer.stop(any())).thenReturn(50L); + try (MockedStatic timer = mockStatic(StepTimer.class)) { + TestStep testStep = new TestStep(); + StepLifecycle.start(testStep); + timer.when(() -> StepTimer.stop(Mockito.anyString())) + .thenReturn(50L); KarateHook hook = new KarateHook( constructor, @@ -255,9 +273,12 @@ void shouldUseCustomStepTitle() { hook.afterStep(result, sr); - storage.verify(() -> StepStorage.addStep( - argThat(s -> s.getStepTitle().equals("Custom title")) - )); + TestStep finished = StepLifecycle.lastFinished(); + + assertEquals("Custom title", finished.getStepTitle()); + assertEquals(50L, finished.getDuration()); + assertEquals("user", finished.getCategory()); + assertEquals(StepStatus.passed, finished.getStatus()); verify(engine).setVariable("log_next_step_title", null); } diff --git a/java-reporter-testng/pom.xml b/java-reporter-testng/pom.xml index f18217ef..53d4175c 100644 --- a/java-reporter-testng/pom.xml +++ b/java-reporter-testng/pom.xml @@ -6,7 +6,7 @@ io.testomat java-reporter-testng - 0.7.10 + 0.7.11 jar Testomat.io Java Reporter TestNG @@ -47,7 +47,7 @@ io.testomat java-reporter-core - 0.9.3 + 0.10.0 org.slf4j diff --git a/java-reporter-testng/src/main/java/io/testomat/testng/constructor/TestNgTestResultConstructor.java b/java-reporter-testng/src/main/java/io/testomat/testng/constructor/TestNgTestResultConstructor.java index 66965ed7..f6e9f631 100644 --- a/java-reporter-testng/src/main/java/io/testomat/testng/constructor/TestNgTestResultConstructor.java +++ b/java-reporter-testng/src/main/java/io/testomat/testng/constructor/TestNgTestResultConstructor.java @@ -2,6 +2,7 @@ import io.testomat.core.model.ExceptionDetails; import io.testomat.core.model.TestResult; +import io.testomat.core.step.StepLifecycle; import io.testomat.core.step.StepStorage; import io.testomat.core.step.TestStep; import java.io.PrintWriter; @@ -80,6 +81,7 @@ private TestResult buildTestResult(TestResultWrapper wrapper, String message, St // Clear steps after collecting them StepStorage.clear(); + StepLifecycle.reset(); return builder.build(); } diff --git a/pom.xml b/pom.xml index f7754c3f..253c54ae 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.testomat java-reporter - 0.2.1 + 0.2.2 pom Testomat.io Java Reporter From 72931c3c7ad7cca79ca23a616cc24f3028ebef43 Mon Sep 17 00:00:00 2001 From: Nick Date: Wed, 25 Mar 2026 17:26:34 +0200 Subject: [PATCH 3/3] Issue-105 Add step and artifact usage examples to README --- README.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/README.md b/README.md index ace35f0f..933d5f51 100644 --- a/README.md +++ b/README.md @@ -536,6 +536,57 @@ If a scenario is annotated with the @LogSteps tag, all Karate steps in that scen And match response.id == 1 ``` +Steps can also be created using `Testomatio.step(stepName, Runnable action)`. + +```java + Testomatio.step("Login", () -> { + // actions + }); +``` + +If executed inside another step (including methods annotated with `@Step`) a substep will be created automatically. + +```java + Testomatio.step("Login", () -> { + // actions + Testomatio.step("Check page", () -> { + // actions + }); + }); +``` + +```java + @Step("Open login page") + private void openLoginPage() { + Testomatio.step("Check page", () -> { + driver.get("https://example.com/login"); + }); + } +``` + +### Adding artifacts to steps + +Artifacts can be attached to a step using the `artifacts` attribute of the `@Step` annotation. + +```java +@Step(value = "Login", artifacts = {"path_to_artifact1", "path_to_artifact2"}) +public void login() { + // test logic +} +``` + +Artifacts will be automatically collected after the step execution. + +You can also attach artifacts to a step programmatically: + +```java + Testomatio.stepArtifact("path_to_attachment1", "path_to_attachment2"); +``` + +If called inside a step, the artifacts will be attached to the current step. + +If called after a step finishes, it will be attached to the last completed step. + ### What You'll See Steps appear in test reports with: