From e926c1ae82805fa53ad02a283cdd61a5b9d023b7 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 20 Dec 2021 12:20:57 -0800 Subject: [PATCH] feat(tracing): collect sources for remote tracing (#755) --- .../playwright/impl/BrowserContextImpl.java | 1 + .../playwright/impl/BrowserImpl.java | 2 ++ .../playwright/impl/BrowserTypeImpl.java | 9 ++++- .../microsoft/playwright/impl/Connection.java | 1 + .../microsoft/playwright/impl/LocalUtils.java | 35 +++++++++++++++++++ .../playwright/impl/PlaywrightImpl.java | 5 +++ .../playwright/impl/TracingImpl.java | 8 ++++- .../playwright/TestBrowserTypeConnect.java | 30 +++++++++++++--- .../com/microsoft/playwright/TestClick.java | 1 - .../com/microsoft/playwright/TestTracing.java | 29 +++------------ .../java/com/microsoft/playwright/Utils.java | 22 ++++++++++-- 11 files changed, 108 insertions(+), 35 deletions(-) create mode 100644 playwright/src/main/java/com/microsoft/playwright/impl/LocalUtils.java diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java index ccd2eda0..41a503e1 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java @@ -55,6 +55,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext { Path videosDir; URL baseUrl; Path recordHarPath; + LocalUtils localUtils; enum EventType { CLOSE, diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserImpl.java index 6edfd281..bffefb17 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserImpl.java @@ -40,6 +40,7 @@ class BrowserImpl extends ChannelOwner implements Browser { boolean isRemote; boolean isConnectedOverWebSocket; private boolean isConnected = true; + LocalUtils localUtils; enum EventType { DISCONNECTED, @@ -171,6 +172,7 @@ class BrowserImpl extends ChannelOwner implements Browser { context.setBaseUrl(options.baseURL); } context.recordHarPath = options.recordHarPath; + context.localUtils = localUtils; contexts.add(context); return context; } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserTypeImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserTypeImpl.java index 4545686b..a827c9e9 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserTypeImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserTypeImpl.java @@ -30,6 +30,8 @@ import java.util.function.Consumer; import static com.microsoft.playwright.impl.Serialization.gson; class BrowserTypeImpl extends ChannelOwner implements BrowserType { + LocalUtils localUtils; + BrowserTypeImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) { super(parent, type, guid, initializer); } @@ -45,7 +47,9 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType { } JsonObject params = gson().toJsonTree(options).getAsJsonObject(); JsonElement result = sendMessage("launch", params); - return connection.getExistingObject(result.getAsJsonObject().getAsJsonObject("browser").get("guid").getAsString()); + BrowserImpl browser = connection.getExistingObject(result.getAsJsonObject().getAsJsonObject("browser").get("guid").getAsString()); + browser.localUtils = localUtils; + return browser; } @Override @@ -76,6 +80,7 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType { BrowserImpl browser = connection.getExistingObject(playwright.initializer.getAsJsonObject("preLaunchedBrowser").get("guid").getAsString()); browser.isRemote = true; browser.isConnectedOverWebSocket = true; + browser.localUtils = localUtils; Consumer connectionCloseListener = t -> browser.notifyRemoteClosed(); pipe.onClose(connectionCloseListener); browser.onDisconnected(b -> { @@ -109,6 +114,7 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType { BrowserImpl browser = connection.getExistingObject(json.getAsJsonObject("browser").get("guid").getAsString()); browser.isRemote = true; + browser.localUtils = localUtils; if (json.has("defaultContext")) { String contextId = json.getAsJsonObject("defaultContext").get("guid").getAsString(); BrowserContextImpl defaultContext = connection.getExistingObject(contextId); @@ -174,6 +180,7 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType { context.setBaseUrl(options.baseURL); } context.recordHarPath = options.recordHarPath; + context.localUtils = localUtils; return context; } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java b/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java index fc81d344..e5d2b1de 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java @@ -268,6 +268,7 @@ public class Connection { result = new JsonPipe(parent, type, guid, initializer); break; case "LocalUtils": + result = new LocalUtils(parent, type, guid, initializer); break; case "Page": result = new PageImpl(parent, type, guid, initializer); diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/LocalUtils.java b/playwright/src/main/java/com/microsoft/playwright/impl/LocalUtils.java new file mode 100644 index 00000000..09db1ad8 --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/impl/LocalUtils.java @@ -0,0 +1,35 @@ +/* + * 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 java.nio.file.Path; + +class LocalUtils extends ChannelOwner { + LocalUtils(ChannelOwner parent, String type, String guid, JsonObject initializer) { + super(parent, type, guid, initializer); + } + + void zip(Path zipFile, JsonArray entries) { + JsonObject params = new JsonObject(); + params.addProperty("zipFile", zipFile.toString()); + params.add("entries", entries); + sendMessage("zip", params); + } +} diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/PlaywrightImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/PlaywrightImpl.java index d2c9ffc4..0aab51a1 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/PlaywrightImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/PlaywrightImpl.java @@ -57,6 +57,7 @@ public class PlaywrightImpl extends ChannelOwner implements Playwright { private final BrowserTypeImpl webkit; private final SelectorsImpl selectors; private final APIRequestImpl apiRequest; + private final LocalUtils localUtils; private SharedSelectors sharedSelectors; PlaywrightImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) { @@ -67,6 +68,10 @@ public class PlaywrightImpl extends ChannelOwner implements Playwright { selectors = connection.getExistingObject(initializer.getAsJsonObject("selectors").get("guid").getAsString()); apiRequest = new APIRequestImpl(this); + localUtils = connection.getExistingObject(initializer.getAsJsonObject("utils").get("guid").getAsString()); + chromium.localUtils = localUtils; + firefox.localUtils = localUtils; + webkit.localUtils = localUtils; } void initSharedSelectors(PlaywrightImpl parent) { diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/TracingImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/TracingImpl.java index c476ff4a..9495fb61 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/TracingImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/TracingImpl.java @@ -16,6 +16,7 @@ package com.microsoft.playwright.impl; +import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.microsoft.playwright.PlaywrightException; import com.microsoft.playwright.Tracing; @@ -40,7 +41,6 @@ class TracingImpl implements Tracing { if (isRemote) { mode = "compressTrace"; } else { - // TODO: support source zips and do compression on the client. mode = "compressTraceAndSources"; } } @@ -57,6 +57,12 @@ class TracingImpl implements Tracing { } artifact.saveAs(path); artifact.delete(); + + // Add local sources to the remote trace if necessary. + if (isRemote && json.has("sourceEntries")) { + JsonArray entries = json.getAsJsonArray("sourceEntries"); + context.localUtils.zip(path, entries); + } } @Override diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBrowserTypeConnect.java b/playwright/src/test/java/com/microsoft/playwright/TestBrowserTypeConnect.java index 3dd98656..0a61d434 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestBrowserTypeConnect.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestBrowserTypeConnect.java @@ -19,21 +19,22 @@ package com.microsoft.playwright; import com.microsoft.playwright.impl.Driver; import com.microsoft.playwright.options.WaitForSelectorState; import org.junit.jupiter.api.AfterAll; +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.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; +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 static com.microsoft.playwright.Utils.mapOf; +import static com.microsoft.playwright.Utils.parseTrace; +import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Arrays.asList; import static org.junit.jupiter.api.Assertions.*; @@ -485,4 +486,25 @@ public class TestBrowserTypeConnect extends TestBase { assertTrue(Files.exists(traceFile)); assertTrue(Files.size(traceFile) > 0); } + + @Test + void shouldRecordTraceWithSources(@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(""); + page.click("'Click'"); + Path trace = tmpDir.resolve("trace1.zip"); + context.tracing().stop(new Tracing.StopOptions().setPath(trace)); + + Map entries = parseTrace(trace); + Map 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)); + } + } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestClick.java b/playwright/src/test/java/com/microsoft/playwright/TestClick.java index 50ae49f5..8be6c150 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestClick.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestClick.java @@ -20,7 +20,6 @@ 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; diff --git a/playwright/src/test/java/com/microsoft/playwright/TestTracing.java b/playwright/src/test/java/com/microsoft/playwright/TestTracing.java index 85cc6725..01b294da 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestTracing.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestTracing.java @@ -21,26 +21,19 @@ 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.io.File; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.*; +import java.util.Map; 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() { @@ -121,7 +114,7 @@ public class TestTracing extends TestBase { Path trace = tmpDir.resolve("trace1.zip"); context.tracing().stop(new Tracing.StopOptions().setPath(trace)); - Map entries = parseTrace(trace); + Map entries = Utils.parseTrace(trace); Map sources = entries.entrySet().stream().filter(e -> e.getKey().endsWith(".txt")).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); assertEquals(1, sources.size()); @@ -131,18 +124,4 @@ public class TestTracing extends TestBase { assertEquals(new String(thisFile, UTF_8), new String(sources.values().iterator().next(), UTF_8)); } - private static Map parseTrace(Path trace) throws IOException { - Map 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; - } } diff --git a/playwright/src/test/java/com/microsoft/playwright/Utils.java b/playwright/src/test/java/com/microsoft/playwright/Utils.java index 0dd795b1..fdea5174 100644 --- a/playwright/src/test/java/com/microsoft/playwright/Utils.java +++ b/playwright/src/test/java/com/microsoft/playwright/Utils.java @@ -20,16 +20,17 @@ import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.JsonParser; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; +import java.io.*; import java.net.ServerSocket; import java.net.Socket; +import java.nio.file.Path; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -88,6 +89,21 @@ class Utils { } } + static Map parseTrace(Path trace) throws IOException { + Map 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; + } + enum OS { WINDOWS, MAC, LINUX, UNKNOWN } static OS getOS() { String name = System.getProperty("os.name").toLowerCase();