Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion java-reporter-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

<groupId>io.testomat</groupId>
<artifactId>java-reporter-core</artifactId>
<version>0.9.3</version>
<version>0.10.0</version>
<packaging>jar</packaging>

<name>Testomat.io Reporter Core</name>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@
@Target(ElementType.METHOD)
public @interface Step {
String value() default "";
String[] artifacts() default {};
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -158,6 +159,7 @@ private Map<String, Object> buildTestResultMap(TestResult result) throws JsonPro
}

if (result.getSteps() != null && !result.getSteps().isEmpty()) {
result.getSteps().forEach(this::processStepArtifacts);
List<Map<String, Object>> stepsMap = convertStepsToMap(result.getSteps());
body.put("steps", stepsMap);
System.out.println("DEBUG: Adding " + result.getSteps().size() + " steps to request body for test: " + result.getTitle());
Expand Down Expand Up @@ -203,6 +205,22 @@ private List<Map<String, Object>> convertStepsToMap(List<TestStep> steps) {
stepMap.put("title", step.getStepTitle());
}

if (step.getArtifacts() != null) {
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());
}

stepMap.put("duration", step.getDuration());

if (step.getSubsteps() != null && !step.getSubsteps().isEmpty()) {
Expand Down Expand Up @@ -276,4 +294,23 @@ private void addLinks(Map<String, Object> 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<String> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,15 @@
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 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;

/**
* Main public API facade for Testomat.io integration.
Expand All @@ -21,6 +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) {
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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
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.
* Ensures thread safety when multiple tests run concurrently.
*/
public class TempArtifactDirectoriesStorage {
public static final ThreadLocal<List<String>> DIRECTORIES = ThreadLocal.withInitial(ArrayList::new);
public static final ConcurrentHashMap<UUID, List<String>> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ public void uploadAllArtifactsForTest(String testName, String rid, String testId
S3Credentials credentials = CredentialsManager.getCredentials();
List<String> 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
Expand All @@ -92,6 +96,18 @@ private List<String> processArtifacts(List<String> 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<String> uploadedLinks) {
ArtifactLinkData linkData = new ArtifactLinkData(testName, rid, testId, uploadedLinks);
ArtifactLinkDataStorage.ARTEFACT_LINK_DATA_STORAGE.add(linkData);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,34 @@
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;

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<String> 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);
}
}

Expand Down
Loading
Loading