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
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,26 @@ Please ensure you set the following system variables (e.g. as part of the maven-
</systemPropertyVariables>
```

### Attachments
A test can attach files to its results: console logs, network HAR files, videos, Playwright traces, page HTML snapshots and images. Angles shows each one on the test (or the step) with a viewer that suits it. The file extension decides how it is shown, so keep the real one: `.log`/`.txt`, `.json`, `.har`, `.webm`/`.mp4`, `.zip` (shown as a Playwright trace when the name contains "trace"), `.html`/`.htm`, `.png`/`.jpg`/`.jpeg`/`.gif`/`.webp`. Requires an Angles server with test attachment support.

```java
reporter.fail("Order confirmation", "Order confirmed", "Payment declined", "");

// attach to the step you just reported, e.g. the page as it was when the assertion failed
reporter.attachDataToLastStep(driver.getPageSource().getBytes(StandardCharsets.UTF_8), "page.html");
reporter.attachFileToLastStep(screenshotFile);

// attach to the whole test
reporter.attachFile(new File("target/videos/checkout.webm"));
reporter.attachFile(new File("target/network.har"));
reporter.attachData(consoleLog.getBytes(StandardCharsets.UTF_8), "console.log");

reporter.saveTest();
```

Uploads happen straight away (they only need the build id), so this works in batch mode too. Like `storeScreenshot`, a failed upload throws an `Error`.

### Batch mode
By default every call to `saveTest()` sends the test execution to the Angles API straight away. If you'd rather send the whole test run in a single request at the end (e.g. for large runs), you can enable batch mode on the reporter. The build is still created up-front and screenshots are still uploaded individually as the tests run (they need the build id), but the executions are gathered by the reporter until you call `saveAllTests()`.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.github.angleshq.angles.api.exceptions.AnglesServerException;
import com.github.angleshq.angles.api.models.Platform;
import com.github.angleshq.angles.api.models.attachment.TestAttachment;
import com.github.angleshq.angles.api.models.build.Artifact;
import com.github.angleshq.angles.api.models.build.Build;
import com.github.angleshq.angles.api.models.build.CreateBuild;
Expand All @@ -15,6 +16,7 @@
import com.github.angleshq.angles.api.requests.*;
import org.apache.http.client.config.RequestConfig;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
Expand All @@ -38,6 +40,7 @@ public class AnglesReporter implements AnglesReporterInterface {
private EnvironmentRequests environmentRequests;
private TeamRequests teamRequests;
private ScreenshotRequests screenshotRequests;
private AttachmentRequests attachmentRequests;

protected static int connectionTimeout = 5000;
protected static int socketTimeout = 10000;
Expand Down Expand Up @@ -80,6 +83,7 @@ private AnglesReporter(String url, RequestConfig requestConfig) {
environmentRequests = new EnvironmentRequests(baseUrl);
teamRequests = new TeamRequests(baseUrl);
screenshotRequests = new ScreenshotRequests(baseUrl);
attachmentRequests = new AttachmentRequests(baseUrl);
}

/**
Expand All @@ -92,6 +96,7 @@ public void setApiKey(String apiKey) {
environmentRequests.setApiKey(apiKey);
teamRequests.setApiKey(apiKey);
screenshotRequests.setApiKey(apiKey);
attachmentRequests.setApiKey(apiKey);
}

public synchronized void startBuild(String name, String environmentName, String teamName, String componentName) {
Expand Down Expand Up @@ -317,6 +322,79 @@ public ImageCompareResponse compareScreenshotAgainstBaseline(String screenshotId
return null;
}

public TestAttachment attachFile(File file) {
return attachFile(file, null);
}

public TestAttachment attachFile(File file, String fileName) {
CreateExecution execution = requireExecution("attachFile");
TestAttachment attachment = upload("attachFile", () -> attachmentRequests.upload(currentBuild.get().getId(), file, fileName));
execution.addAttachment(attachment.getId());
return attachment;
}

public TestAttachment attachData(byte[] data, String fileName) {
CreateExecution execution = requireExecution("attachData");
TestAttachment attachment = upload("attachData", () -> attachmentRequests.upload(currentBuild.get().getId(), data, fileName));
execution.addAttachment(attachment.getId());
return attachment;
}

public TestAttachment attachFileToLastStep(File file) {
return attachFileToLastStep(file, null);
}

public TestAttachment attachFileToLastStep(File file, String fileName) {
Step step = requireLastStep("attachFileToLastStep");
TestAttachment attachment = upload("attachFileToLastStep", () -> attachmentRequests.upload(currentBuild.get().getId(), file, fileName));
step.addAttachment(attachment.getId());
return attachment;
}

public TestAttachment attachDataToLastStep(byte[] data, String fileName) {
Step step = requireLastStep("attachDataToLastStep");
TestAttachment attachment = upload("attachDataToLastStep", () -> attachmentRequests.upload(currentBuild.get().getId(), data, fileName));
step.addAttachment(attachment.getId());
return attachment;
}

private interface Upload {
TestAttachment run() throws IOException, AnglesServerException;
}

// Errors are raised the same way storeScreenshot raises them.
private TestAttachment upload(String method, Upload upload) {
try {
TestAttachment attachment = upload.run();
if (attachment == null || attachment.getId() == null) {
throw new Error(method + ": Angles did not return the stored attachment.");
}
return attachment;
} catch (IOException | AnglesServerException exception) {
throw new Error("Unable to store attachment due to [" + exception.getMessage() + "]");
}
}

private CreateExecution requireExecution(String method) {
if (currentBuild.get() == null || currentBuild.get().getId() == null) {
throw new Error(method + ": start a build before attaching files.");
}
if (currentExecution.get() == null) {
throw new Error(method + ": start a test before attaching files.");
}
return currentExecution.get();
}

private Step requireLastStep(String method) {
requireExecution(method);
Action action = currentAction.get();
List<Step> steps = action != null ? action.getSteps() : null;
if (steps == null || steps.isEmpty()) {
throw new Error(method + ": add a step (pass, fail, info, ...) before attaching a file to it.");
}
return steps.get(steps.size() - 1);
}

public String getBuildId() {
if (this.currentBuild.get() != null) {
return this.currentBuild.get().getId();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package com.github.angleshq.angles;

import com.github.angleshq.angles.api.models.Platform;
import com.github.angleshq.angles.api.models.attachment.TestAttachment;
import com.github.angleshq.angles.api.models.build.Artifact;
import com.github.angleshq.angles.api.models.screenshot.ImageCompareResponse;
import com.github.angleshq.angles.api.models.screenshot.Screenshot;
import com.github.angleshq.angles.api.models.screenshot.ScreenshotDetails;

import java.io.File;
import java.util.List;

public class AnglesReporterEmpty implements AnglesReporterInterface {
Expand Down Expand Up @@ -124,4 +126,34 @@ public String getBuildId() {
// do nothing
return null;
}

public TestAttachment attachFile(File file) {
// do nothing
return null;
}

public TestAttachment attachFile(File file, String fileName) {
// do nothing
return null;
}

public TestAttachment attachData(byte[] data, String fileName) {
// do nothing
return null;
}

public TestAttachment attachFileToLastStep(File file) {
// do nothing
return null;
}

public TestAttachment attachFileToLastStep(File file, String fileName) {
// do nothing
return null;
}

public TestAttachment attachDataToLastStep(byte[] data, String fileName) {
// do nothing
return null;
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package com.github.angleshq.angles;

import com.github.angleshq.angles.api.models.Platform;
import com.github.angleshq.angles.api.models.attachment.TestAttachment;
import com.github.angleshq.angles.api.models.build.Artifact;
import com.github.angleshq.angles.api.models.screenshot.ImageCompareResponse;
import com.github.angleshq.angles.api.models.screenshot.Screenshot;
import com.github.angleshq.angles.api.models.screenshot.ScreenshotDetails;

import java.io.File;
import java.util.List;

public interface AnglesReporterInterface {
Expand Down Expand Up @@ -61,4 +63,27 @@ public interface AnglesReporterInterface {
Screenshot storeScreenshot(ScreenshotDetails details);

ImageCompareResponse compareScreenshotAgainstBaseline(String screenshotId);

/**
* Uploads a file and attaches it to the current test: a video, a Playwright trace, a HAR
* file, a console log and so on. The extension decides how Angles shows it: .log/.txt,
* .json, .har, .webm/.mp4, .zip (a trace when the name contains "trace"), .html/.htm,
* or an image.
*/
TestAttachment attachFile(File file);

/** As {@link #attachFile(File)}, shown under {@code fileName} (keep the extension). */
TestAttachment attachFile(File file, String fileName);

/** Attaches in-memory content to the current test. The extension of {@code fileName} decides how it is shown. */
TestAttachment attachData(byte[] data, String fileName);

/** Uploads a file and attaches it to the most recent step, e.g. the page HTML when an assertion failed. */
TestAttachment attachFileToLastStep(File file);

/** As {@link #attachFileToLastStep(File)}, shown under {@code fileName} (keep the extension). */
TestAttachment attachFileToLastStep(File file, String fileName);

/** Attaches in-memory content to the most recent step, e.g. {@code driver.getPageSource()} as "page.html". */
TestAttachment attachDataToLastStep(byte[] data, String fileName);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.github.angleshq.angles.api.models.attachment;

import com.github.angleshq.angles.api.models.BaseModel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

/**
* A file an automated test uploaded against its build: a log, HAR file, video, Playwright
* trace, HTML snapshot or image. Listing its id on the execution (or a step) shows it there.
*/
@Getter @Setter @NoArgsConstructor
public class TestAttachment extends BaseModel {

/**
* What the file holds, decided by the server from the file extension: image, log, json,
* har, video, trace, archive or html.
*/
private String kind;
/** The uploaded file name, for display. */
private String originalName;
/** The type the file is served with, decided from its extension. */
private String mimeType;
private Long size;
private String build;
/** The execution that listed this attachment, once it has been saved. */
private String execution;
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,17 @@ public class CreateExecution implements Serializable {
/* prevent any modifications by other threads to cause an issue. */
private List<Action> actions = new CopyOnWriteArrayList<>();
private List<Platform> platforms = new ArrayList<>();
/* Ids of files attached to the whole test, e.g. a video, trace, HAR file or console log. */
private List<String> attachments = new CopyOnWriteArrayList<>();

public void addTag(String tag) {
this.tags.add(tag);
}

public void addAttachment(String attachmentId) {
this.attachments.add(attachmentId);
}

public void addAction(Action action) {
this.actions.add(action);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public class Execution extends BaseModel {
private Date start;
private List<Action> actions = new ArrayList<>();
private List<Platform> platforms = new ArrayList<>();
/* Ids of the files attached to the whole test. Step-level ones are on the steps. */
private List<String> attachments = new ArrayList<>();

public Execution(String build, String title, String suite, String feature,Date start) {
this.build = build;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

@Setter @Getter @NoArgsConstructor
public class Step {
Expand All @@ -17,6 +19,15 @@ public class Step {
private StepStatus status;
private Date timestamp;
private String screenshot;
/** Ids of files attached to this step (see AnglesReporter.attachFileToLastStep). */
private List<String> attachments;

public void addAttachment(String attachmentId) {
if (attachments == null) {
attachments = new ArrayList<>();
}
attachments.add(attachmentId);
}

public Step(String name, String info, StepStatus status, Date timestamp) {
this.name = name;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.github.angleshq.angles.api.requests;

import com.github.angleshq.angles.api.exceptions.AnglesServerException;
import com.github.angleshq.angles.api.models.attachment.TestAttachment;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.MultipartEntityBuilder;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;

/**
* Uploads files that an automated test produced (POST /build/{buildId}/attachment).
*
* The server decides how a file is shown from its extension, so keep the real one:
* .log/.txt, .json, .har, .webm/.mp4, .zip (a Playwright trace when the name contains
* "trace"), .html/.htm, .png/.jpg/.jpeg/.gif/.webp.
*/
public class AttachmentRequests extends BaseRequests {

public AttachmentRequests(String baseUrl) {
super(baseUrl);
}

public TestAttachment upload(String buildId, File file, String fileName) throws IOException, AnglesServerException {
HttpEntity entity = MultipartEntityBuilder.create()
.addBinaryBody("attachment", file, ContentType.APPLICATION_OCTET_STREAM,
fileName != null ? fileName : file.getName())
.build();
return send(buildId, entity);
}

public TestAttachment upload(String buildId, byte[] data, String fileName) throws IOException, AnglesServerException {
HttpEntity entity = MultipartEntityBuilder.create()
.addBinaryBody("attachment", data, ContentType.APPLICATION_OCTET_STREAM, fileName)
.build();
return send(buildId, entity);
}

private TestAttachment send(String buildId, HttpEntity entity) throws IOException, AnglesServerException {
try (CloseableHttpResponse response = sendMultiPartEntity("build/" + buildId + "/attachment", new HashMap<>(), entity)) {
return processResponse(response, TestAttachment.class);
}
}
}
Loading