feat: include source files in trace (#754)

This commit is contained in:
Yury Semikhatsky 2021-12-17 16:58:00 -08:00 committed by GitHub
parent 963afac983
commit 86e91590cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 178 additions and 62 deletions

View File

@ -40,6 +40,11 @@ jobs:
run: mvn test --no-transfer-progress --fail-at-end
env:
BROWSER: ${{ matrix.browser }}
- name: Run tracing tests w/ sources
run: mvn test --no-transfer-progress --fail-at-end -D test=*TestTracing*
env:
BROWSER: ${{ matrix.browser }}
PLAYWRIGHT_JAVA_SRC: src/test/java
- name: Test Spring Boot Starter
shell: bash
env:

View File

@ -13,7 +13,7 @@ Playwright is a Java library to automate [Chromium](https://www.chromium.org/Hom
| :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->99.0.4763.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->15.4<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->94.0.1<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Firefox <!-- GEN:firefox-version -->95.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
Headless execution is supported for all the browsers on all platforms. Check out [system requirements](https://playwright.dev/java/docs/next/intro/#system-requirements) for details.

View File

@ -22,7 +22,7 @@ import com.microsoft.playwright.Page;
/**
* The {@code PageAssertions} class provides assertion methods that can be used to make assertions about the {@code Page} state in the
* tests. A new instance of {@code LocatorAssertions} is created by calling {@link PlaywrightAssertions#assertThat
* tests. A new instance of {@code PageAssertions} is created by calling {@link PlaywrightAssertions#assertThat
* PlaywrightAssertions.assertThat()}:
* <pre>{@code
* ...

View File

@ -51,6 +51,11 @@ public interface Tracing {
* Whether to capture DOM snapshot on every action.
*/
public Boolean snapshots;
/**
* Whether to include source files for trace actions. List of the directories with source code for the application must be
* provided via {@code PLAYWRIGHT_JAVA_SRC} environment variable.
*/
public Boolean sources;
/**
* Trace name to be shown in the Trace Viewer.
*/
@ -78,6 +83,14 @@ public interface Tracing {
this.snapshots = snapshots;
return this;
}
/**
* Whether to include source files for trace actions. List of the directories with source code for the application must be
* provided via {@code PLAYWRIGHT_JAVA_SRC} environment variable.
*/
public StartOptions setSources(boolean sources) {
this.sources = sources;
return this;
}
/**
* Trace name to be shown in the Trace Viewer.
*/

View File

@ -16,23 +16,18 @@
package com.microsoft.playwright.impl;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.Playwright;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.TimeoutError;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import static com.microsoft.playwright.impl.LoggingSupport.logWithTimestamp;
import static com.microsoft.playwright.impl.Serialization.gson;
class Message {
@ -62,7 +57,7 @@ public class Connection {
private final Map<String, ChannelOwner> objects = new HashMap<>();
private final Root root;
private int lastId = 0;
private final Path srcDir;
private final StackTraceCollector stackTraceCollector;
private final Map<Integer, WaitableResult<JsonElement>> callbacks = new HashMap<>();
private String apiName;
private static final boolean isLogging;
@ -91,14 +86,11 @@ public class Connection {
this.transport = transport;
root = new Root(this);
String srcRoot = System.getenv("PLAYWRIGHT_JAVA_SRC");
if (srcRoot == null) {
srcDir = null;
} else {
srcDir = Paths.get(srcRoot);
if (!Files.exists(srcDir)) {
throw new PlaywrightException("PLAYWRIGHT_JAVA_SRC environment variable points to non-existing location: '" + srcRoot + "'");
}
}
stackTraceCollector = (srcRoot == null) ? null: new StackTraceCollector(Paths.get(srcRoot));
}
boolean isCollectingStacks() {
return stackTraceCollector != null;
}
String setApiName(String name) {
@ -119,45 +111,6 @@ public class Connection {
return internalSendMessage(guid, method, params);
}
private String sourceFile(StackTraceElement frame) {
String pkg = frame.getClassName();
int lastDot = pkg.lastIndexOf('.');
if (lastDot == -1) {
pkg = "";
} else {
pkg = frame.getClassName().substring(0, lastDot + 1);
}
pkg = pkg.replace('.', File.separatorChar);
return srcDir.resolve(pkg).resolve(frame.getFileName()).toString();
}
private JsonArray currentStackTrace() {
StackTraceElement[] stack = Thread.currentThread().getStackTrace();
int index = 0;
while (index < stack.length && !stack[index].getClassName().equals(getClass().getName())) {
index++;
};
// Find Playwright API call
while (index < stack.length && stack[index].getClassName().startsWith("com.microsoft.playwright.")) {
// hack for tests
if (stack[index].getClassName().startsWith("com.microsoft.playwright.Test")) {
break;
}
index++;
}
JsonArray jsonStack = new JsonArray();
for (; index < stack.length; index++) {
StackTraceElement frame = stack[index];
JsonObject jsonFrame = new JsonObject();
jsonFrame.addProperty("file", sourceFile(frame));
jsonFrame.addProperty("line", frame.getLineNumber());
jsonFrame.addProperty("function", frame.getClassName() + "." + frame.getMethodName());
jsonStack.add(jsonFrame);
}
return jsonStack;
}
private WaitableResult<JsonElement> internalSendMessage(String guid, String method, JsonObject params) {
int id = ++lastId;
WaitableResult<JsonElement> result = new WaitableResult<>();
@ -168,8 +121,8 @@ public class Connection {
message.addProperty("method", method);
message.add("params", params);
JsonObject metadata = new JsonObject();
if (srcDir != null) {
metadata.add("stack", currentStackTrace());
if (stackTraceCollector != null) {
metadata.add("stack", stackTraceCollector.currentStackTrace());
}
if (apiName != null) {
metadata.addProperty("apiName", apiName);

View File

@ -0,0 +1,81 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.microsoft.playwright.PlaywrightException;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
class StackTraceCollector {
private final Path srcDir;
StackTraceCollector(Path srcDir) {
if (!Files.exists(srcDir.toAbsolutePath())) {
throw new PlaywrightException("Source location doesn't exist: '" + srcDir.toAbsolutePath() + "'");
}
this.srcDir = srcDir;
}
private String sourceFile(StackTraceElement frame) {
String pkg = frame.getClassName();
int lastDot = pkg.lastIndexOf('.');
if (lastDot == -1) {
pkg = "";
} else {
pkg = frame.getClassName().substring(0, lastDot + 1);
}
pkg = pkg.replace('.', File.separatorChar);
String file = frame.getFileName();
if (file == null) {
return "";
}
return srcDir.resolve(pkg).resolve(file).toString();
}
JsonArray currentStackTrace() {
StackTraceElement[] stack = Thread.currentThread().getStackTrace();
int index = 0;
while (index < stack.length && !stack[index].getClassName().equals(getClass().getName())) {
index++;
};
// Find Playwright API call
while (index < stack.length && stack[index].getClassName().startsWith("com.microsoft.playwright.")) {
// hack for tests
if (stack[index].getClassName().startsWith("com.microsoft.playwright.Test")) {
break;
}
index++;
}
JsonArray jsonStack = new JsonArray();
for (; index < stack.length; index++) {
StackTraceElement frame = stack[index];
JsonObject jsonFrame = new JsonObject();
jsonFrame.addProperty("file", sourceFile(frame));
jsonFrame.addProperty("line", frame.getLineNumber());
jsonFrame.addProperty("function", frame.getClassName() + "." + frame.getMethodName());
jsonStack.add(jsonFrame);
}
return jsonStack;
}
}

View File

@ -16,8 +16,8 @@
package com.microsoft.playwright.impl;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.Tracing;
import java.nio.file.Path;
@ -26,16 +26,23 @@ import static com.microsoft.playwright.impl.Serialization.gson;
class TracingImpl implements Tracing {
private final BrowserContextImpl context;
private boolean includeSources;
TracingImpl(BrowserContextImpl context) {
this.context = context;
}
private void stopChunkImpl(Path path) {
boolean isRemote = context.browser() != null && context.browser().isRemote;
JsonObject params = new JsonObject();
String mode = "doNotSave";
if (path != null) {
mode = "compressTrace";
if (isRemote) {
mode = "compressTrace";
} else {
// TODO: support source zips and do compression on the client.
mode = "compressTraceAndSources";
}
}
params.addProperty("mode", mode);
JsonObject json = context.sendMessage("tracingStopChunk", params).getAsJsonObject();
@ -45,7 +52,7 @@ class TracingImpl implements Tracing {
ArtifactImpl artifact = context.connection.getExistingObject(json.getAsJsonObject("artifact").get("guid").getAsString());
// In case of CDP connection browser is null but since the connection is established by
// the driver it is safe to consider the artifact local.
if (context.browser() != null && context.browser().isRemote) {
if (isRemote) {
artifact.isRemote = true;
}
artifact.saveAs(path);
@ -77,6 +84,13 @@ class TracingImpl implements Tracing {
options = new StartOptions();
}
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
includeSources = options.sources != null;
if (includeSources) {
if (!context.connection.isCollectingStacks()) {
throw new PlaywrightException("Source root directories must be provided to enable source collection");
}
params.addProperty("sources", true);
}
context.sendMessage("tracingStart", params);
context.sendMessage("tracingStartChunk");
}

View File

@ -20,6 +20,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledIf;
import org.junit.jupiter.api.condition.EnabledIf;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

View File

@ -45,7 +45,7 @@ public class TestElementHandleBoundingBox extends TestBase {
@Test
void shouldHandleNestedFrames() {
page.setViewportSize(500, 500);
page.setViewportSize(616, 500);
page.navigate(server.PREFIX + "/frames/nested-frames.html");
Frame nestedFrame = page.frame("dos");
assertNotNull(nestedFrame);

View File

@ -16,17 +16,31 @@
package com.microsoft.playwright;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Collectors;
import java.util.zip.GZIPOutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import static com.microsoft.playwright.Utils.copy;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class TestTracing extends TestBase {
private static final String _PW_JAVA_TEST_SRC = "_PW_JAVA_TEST_SRC";
@Override
@BeforeAll
void launchBrowser() {
@ -96,4 +110,39 @@ public class TestTracing extends TestBase {
assertTrue(Files.exists(traceFile1));
assertTrue(Files.exists(traceFile2));
}
@Test
void shouldCollectSources(@TempDir Path tmpDir) throws IOException {
Assumptions.assumeTrue(System.getenv("PLAYWRIGHT_JAVA_SRC") != null, "PLAYWRIGHT_JAVA_SRC must point to the directory containing this test source.");
context.tracing().start(new Tracing.StartOptions().setSources(true));
page.navigate(server.EMPTY_PAGE);
page.setContent("<button>Click</button>");
page.click("'Click'");
Path trace = tmpDir.resolve("trace1.zip");
context.tracing().stop(new Tracing.StopOptions().setPath(trace));
Map<String, byte[]> entries = parseTrace(trace);
Map<String, byte[]> sources = entries.entrySet().stream().filter(e -> e.getKey().endsWith(".txt")).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
assertEquals(1, sources.size());
String path = getClass().getName().replace('.', File.separatorChar);
Path sourceFile = Paths.get(System.getenv("PLAYWRIGHT_JAVA_SRC"), path + ".java");
byte[] thisFile = Files.readAllBytes(sourceFile);
assertEquals(new String(thisFile, UTF_8), new String(sources.values().iterator().next(), UTF_8));
}
private static Map<String, byte[]> parseTrace(Path trace) throws IOException {
Map<String, byte[]> entries = new HashMap<>();
try (ZipInputStream zis = new ZipInputStream(new FileInputStream(trace.toFile()))) {
for (ZipEntry zipEntry = zis.getNextEntry(); zipEntry != null; zipEntry = zis.getNextEntry()) {
ByteArrayOutputStream content = new ByteArrayOutputStream();
try (OutputStream output = content) {
copy(zis, output);
}
entries.put(zipEntry.getName(), content.toByteArray());
}
zis.closeEntry();
}
return entries;
}
}

View File

@ -1 +1 @@
1.18.0-alpha-dec-16-2021
1.18.0-alpha-1639770748000