Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
6 changes: 6 additions & 0 deletions .github/workflows/test_docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,9 @@ jobs:
run: |
CONTAINER_ID="$(docker run --rm -e CI --ipc=host -v $(pwd):/root/playwright --name playwright-docker-test -d -t playwright-java:localbuild-${{ matrix.flavor }} /bin/bash)"
docker exec "${CONTAINER_ID}" /root/playwright/tools/test-local-installation/create_project_and_run_tests.sh
Copy link
Member

@yury-s yury-s Jul 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid the docker test failures let's just add docker stop "${CONTAINER_ID}" to this (and the new) step to shutdown the container after the test has finished. Or better yet, since the container is the same in both steps, you can move its creation in a separate step and then use its CONTAINER_ID without relaunching, something like

 - name: Start container
        run: |
          CONTAINER_ID=$(docker run --rm -e CI --ipc=host -v "$(pwd)":/root/playwright --name playwright-docker-test -d -t playwright-java:localbuild-${{ matrix.flavor }} /bin/bash)
          echo "CONTAINER_ID=$CONTAINER_ID" >> $GITHUB_ENV

      - name: Run test in container
        run: |
          docker exec "$CONTAINER_ID" /root/playwright/tools/test-local-installation/create_project_and_run_tests.sh

      - name: Test ClassLoader
        run: |
          docker exec "${CONTAINER_ID}" /root/playwright/tools/test-spring-boot-starter/package_and_run_async_test.sh

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added example code and stop step

- name: Test ClassLoader
env:
BROWSER: ${{ matrix.browser }}
run: |
CONTAINER_ID="$(docker run --rm -e CI --ipc=host -v $(pwd):/root/playwright --name playwright-docker-test -d -t playwright-java:localbuild-${{ matrix.flavor }} /bin/bash)"
docker exec "${CONTAINER_ID}" /root/playwright/tools/test-spring-classloader/package_and_run_classloader_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ private FileSystem initFileSystem(URI uri) throws IOException {
}

public static URI getDriverResourceURI() throws URISyntaxException {
ClassLoader classloader = Thread.currentThread().getContextClassLoader();
ClassLoader classloader = DriverJar.class.getClassLoader();
return classloader.getResource("driver/" + platformDir()).toURI();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/bin/bash

set -e
set +x

cd "$(dirname "$0")"
mvn package -D skipTests --no-transfer-progress
java -jar target/test-spring-classloader*.jar
36 changes: 36 additions & 0 deletions tools/test-spring-classloader/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.3</version>
</parent>
<groupId>com.microsoft.playwright</groupId>
<artifactId>test-spring-classloader</artifactId>
<version>1.50.0-SNAPSHOT</version>
<name>Test Playwright With Spring Boot</name>
<properties>
<spring.version>2.4.3</spring.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.microsoft.playwright.springboottest;

import com.microsoft.playwright.*;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import java.util.concurrent.CompletableFuture;

@SpringBootApplication
public class TestApp implements CommandLineRunner {

public static void main(String[] args) {
SpringApplication.run(TestApp.class, args);
}

public void run(String... args) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just run existing test-spring-boot-starter in Docker? It appears to do the same as the new test, so I'd just put the new shell script in test-spring-boot-starter and call it from test_docker.yml. Would that work or am I missing something?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The key difference is that the new test uses CompletableFuture for asynchronous execution. While the existing test works fine even in the Docker environment, i encountered an issue where DriverJar could not be read when executed from a new thread created by CompletableFuture inside the Docker container. That’s why this additional test was introduced.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case you let's just modify existing test to either always run playwright asynchronously or, if you want to keep testing sync code path as well, do it sync/async based on a command line flag and pass the flag only when running in docker. Something like this:

  public static void main(String[] args) {
    if ("--async".equals(args[0])) {
      CompletableFuture<Void> voidCompletableFuture = CompletableFuture.runAsync(() -> {
        SpringApplication.run(TestApp.class, args);
      });
      voidCompletableFuture.join();
    } else {
      SpringApplication.run(TestApp.class, args);
    }
  }

Would that work?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When running with --async, Spring Boot uses the main thread’s context class loader to load auto-configuration classes. However, in the asynchronous thread, this context class loader is not correctly inherited, which causes it to fail to locate the configuration files.
This results in the following error: java.lang.IllegalArgumentException: No auto configuration classes found in META-INF/spring.factories.

11:48:21.637 [ForkJoinPool.commonPool-worker-1] ERROR org.springframework.boot.SpringApplication - Application run failed
java.lang.IllegalArgumentException: No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.
        at org.springframework.util.Assert.notEmpty(Assert.java:470)
        at org.springframework.boot.autoconfigure.AutoConfigurationImportSelector.getCandidateConfigurations(AutoConfigurationImportSelector.java:180)
        at org.springframework.boot.autoconfigure.AutoConfigurationImportSelector.getAutoConfigurationEntry(AutoConfigurationImportSelector.java:123)
        at org.springframework.boot.autoconfigure.AutoConfigurationImportSelector$AutoConfigurationGroup.process(AutoConfigurationImportSelector.java:434)
        at org.springframework.context.annotation.ConfigurationClassParser$DeferredImportSelectorGrouping.getImports(ConfigurationClassParser.java:879)
        at org.springframework.context.annotation.ConfigurationClassParser$DeferredImportSelectorGroupingHandler.processGroupImports(ConfigurationClassParser.java:809)
        at org.springframework.context.annotation.ConfigurationClassParser$DeferredImportSelectorHandler.process(ConfigurationClassParser.java:780)
        at org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:193)
        at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:330)
        at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:246)
        at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:311)
        at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:112)
        at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:745)
        at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:563)
        at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:767)
        at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:759)
        at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:426)
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:326)
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1311)
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1300)
        at com.microsoft.playwright.springboottest.TestApp.lambda$main$0(TestApp.java:16)
        at java.base/java.util.concurrent.CompletableFuture$AsyncRun.run(CompletableFuture.java:1804)
        at java.base/java.util.concurrent.CompletableFuture$AsyncRun.exec(CompletableFuture.java:1796)
        at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:387)
        at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1312)
        at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1843)
        at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1808)
        at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:188)

While it’s possible to manually pass the main thread’s context class loader to the async thread like below, doing so defeats the original purpose of testing the class loader behavior.

public static void main(String[] args) {

    if (args.length == 0) {
      SpringApplication.run(TestApp.class, args);
    }
    else {
      if ("--async".equals(args[0])) {
        ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
        CompletableFuture<Void> voidCompletableFuture = CompletableFuture.runAsync(() -> {
          Thread.currentThread().setContextClassLoader(contextClassLoader);
          SpringApplication.run(TestApp.class, args);
        });
        voidCompletableFuture.join();
      }
    }
  }

For this reason, I think separating the test into a different package would make the intention clearer.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I meant to just share the implementation of the actual test method, I guess it should have been something like this:

  @Override
  public void run(String... args) {
    if ("--async".equals(args[0])) {
      runAsync();
    } else {
      runSync();
    }
  }

  private void runAsync() {
    CompletableFuture<Void> voidCompletableFuture = CompletableFuture.runAsync(() -> {
      runSync();
    });
    voidCompletableFuture.join();
  }


  private void runSync() {
    try (Playwright playwright = Playwright.create()) {
      BrowserType browserType = getBrowserTypeFromEnv(playwright);
      System.out.println("Running test with " + browserType.name());
      Browser browser = browserType.launch();
      BrowserContext context = browser.newContext();
      Page page = context.newPage();
      System.out.println(page.evaluate("'SUCCESS: did evaluate in page'"));
    }
  }

Basically the logic from the existing test is in runSync() and reused. I don't suggest we make any changes to the class loaders.

// use CompletableFuture to run Playwright asynchronously
CompletableFuture<Void> voidCompletableFuture = CompletableFuture.runAsync(() -> {
try (Playwright playwright = Playwright.create()) {
System.out.println("Playwright classLoader test started, waiting for completion...");
BrowserType browserType = getBrowserTypeFromEnv(playwright);
System.out.println("Running test with " + browserType.name());
Browser browser = browserType.launch();
BrowserContext context = browser.newContext();
Page page = context.newPage();
System.out.println(page.evaluate("'SUCCESS: did evaluate in page'"));
} catch (Exception e) {
System.out.println("FAILED: " + e.toString());
for (StackTraceElement ste : e.getStackTrace()) {
System.out.println("\tat " + ste);
}
}
});

System.out.println("Playwright classLoader test is running asynchronously, main thread will wait for it to complete.");

voidCompletableFuture.join();

System.out.println("Playwright classLoader test completed.");

}

static BrowserType getBrowserTypeFromEnv(Playwright playwright) {
String browserName = System.getenv("BROWSER");

if (browserName == null) {
browserName = "chromium";
}

switch (browserName) {
case "webkit":
return playwright.webkit();
case "firefox":
return playwright.firefox();
case "chromium":
return playwright.chromium();
default:
throw new IllegalArgumentException("Unknown browser: " + browserName);
}
}

}
Loading