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
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright 2026 Google LLC
*
* 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
*
* https://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.google.appengine.tools.development;

import com.google.common.collect.ImmutableList;
import java.io.File;
import java.io.IOException;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

@RunWith(Parameterized.class)
public class DevAppServerFullScanTest extends DevAppServerTestBase {

public DevAppServerFullScanTest(String runtimeVersion, String jettyVersion, String jakartaVersion) {
super(runtimeVersion, jettyVersion, jakartaVersion);
}

private File appDir;

@Before
public void setUpClass() throws IOException, InterruptedException {
appDir =
Boolean.getBoolean("appengine.use.EE10") || Boolean.getBoolean("appengine.use.EE11")
? createApp("allinone_jakarta")
: createApp("allinone");
setUpClass(appDir);
}

@Override
protected List<String> getExtraJvmArgs() {
return ImmutableList.of("-Dappengine.fullscan.seconds=1");
}

@Test
public void testFullScanStartAndReload() throws Exception {
// Basic request to ensure server is running and hot reload scanner starts up fine
executeHttpGet("/?memcache_loops=1&memcache_size=1", "Running memcache for 1 loops with value size 1\nCache hits: 1\nCache misses: 0\n", RESPONSE_200);

// Clear logs to ensure we only search for the subsequent reload event
serverLogs.clear();

// Touch web.xml to trigger a reload
File webXml = new File(appDir, "WEB-INF/web.xml");
com.google.common.truth.Truth.assertThat(webXml.exists()).isTrue();
long oldLastModified = webXml.lastModified();
long newTime = oldLastModified + 2000;
boolean modified = webXml.setLastModified(newTime);
com.google.common.truth.Truth.assertThat(modified).isTrue();

// Verify that the hot-reload scanner initiates a reload
boolean reloaded = awaitLogContains("A file has changed, reloading the web application.", 10);
com.google.common.truth.Truth.assertThat(reloaded).isTrue();

// Clear logs again to test a deeply nested resource file (depth > 3)
serverLogs.clear();

File nestedClassFile = new File(appDir, "WEB-INF/classes/allinone/deeper/package/test/Dummy.properties");
nestedClassFile.getParentFile().mkdirs();
java.nio.file.Files.write(nestedClassFile.toPath(), new byte[]{0, 1, 2, 3});

// Verify that the hot-reload scanner initiates a reload for the deeply nested resource file
boolean nestedReloaded = awaitLogContains("A file has changed, reloading the web application.", 10);
com.google.common.truth.Truth.assertThat(nestedReloaded).isTrue();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public abstract class DevAppServerTestBase {
int jettyPort;
private Process runtimeProc;
private CountDownLatch serverStarted;
protected final List<String> serverLogs = java.util.Collections.synchronizedList(new ArrayList<>());

static final int NUMBER_OF_RETRIES = 5;

Expand Down Expand Up @@ -177,6 +178,7 @@ public void setUpClass(File appDir) throws IOException, InterruptedException {
runtimeArgs.add("-Dappengine.use.EE10=" + System.getProperty("appengine.use.EE10"));
runtimeArgs.add("-Dappengine.use.EE11=" + System.getProperty("appengine.use.EE11"));
runtimeArgs.add("-Dappengine.use.jetty121=" + System.getProperty("appengine.use.jetty121"));
runtimeArgs.addAll(getExtraJvmArgs());
runtimeArgs.add("-cp");
runtimeArgs.add(TOOLS_JAR);
runtimeArgs.add("com.google.appengine.tools.development.DevAppServerMain");
Expand All @@ -185,10 +187,19 @@ public void setUpClass(File appDir) throws IOException, InterruptedException {
runtimeArgs.add("--allow_remote_shutdown"); // Keep as used in Maven plugin
runtimeArgs.add("--disable_update_check"); // Keep, as used in Maven plugin

runtimeArgs.addAll(getExtraAppArgs());
runtimeArgs.add(appDir.toString());
createRuntime(ImmutableList.copyOf(runtimeArgs), ImmutableMap.of(), jettyPort);
}

protected List<String> getExtraJvmArgs() {
return ImmutableList.of();
}

protected List<String> getExtraAppArgs() {
return ImmutableList.of();
}

void createRuntime(
ImmutableList<String> runtimeArgs,
ImmutableMap<String, String> extraEnvironmentEntries,
Expand Down Expand Up @@ -221,8 +232,8 @@ private Process launchRuntime(
pb.environment().putAll(extraEnvironmentEntries);
Process process = pb.start();

OutputPump outPump = new OutputPump(process.getInputStream(), serverStarted);
OutputPump errPump = new OutputPump(process.getErrorStream(), serverStarted);
OutputPump outPump = new OutputPump(process.getInputStream(), serverStarted, serverLogs);
OutputPump errPump = new OutputPump(process.getErrorStream(), serverStarted, serverLogs);
new Thread(outPump).start();
new Thread(errPump).start();
if (!serverStarted.await(120, TimeUnit.SECONDS)) {
Expand Down Expand Up @@ -284,12 +295,28 @@ void executeHttpGetWithRetriesContains(
assertThat(retCode).isEqualTo(expectedReturnCode);
}

protected boolean awaitLogContains(String expected, int timeoutSeconds) throws InterruptedException {
for (int i = 0; i < timeoutSeconds * 10; i++) {
synchronized (serverLogs) {
for (String logLine : serverLogs) {
if (logLine.contains(expected)) {
return true;
}
}
}
Thread.sleep(100);
}
return false;
}

private static class OutputPump implements Runnable {
private final BufferedReader stream;
private final CountDownLatch serverStarted;
private final List<String> serverLogs;

public OutputPump(InputStream instream, CountDownLatch serverStarted) {
public OutputPump(InputStream instream, CountDownLatch serverStarted, List<String> serverLogs) {
this.serverStarted = serverStarted;
this.serverLogs = serverLogs;
this.stream = new BufferedReader(new InputStreamReader(instream, UTF_8));
}

Expand All @@ -299,6 +326,7 @@ public void run() {
try {
while ((line = stream.readLine()) != null) {
System.out.println(line);
serverLogs.add(line);
if (line.contains("INFO: Dev App Server is now running")) {
serverStarted.countDown();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,7 @@ private void fullWebAppScanner(int interval) throws IOException {
scanner.setScanInterval(interval);
scanner.setScanDirs(scanList);
scanner.setReportExistingFilesOnStartup(false);
scanner.setScanDepth(-1); // -1 means unlimited depth.
scanner.setScanDepth(Integer.MAX_VALUE); // Integer.MAX_VALUE means unlimited depth.

scanner.addListener(
new Scanner.BulkListener() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -507,7 +507,7 @@ private void fullWebAppScanner(int interval) throws IOException {
scanner.setScanInterval(interval);
scanner.setScanDirs(scanList);
scanner.setReportExistingFilesOnStartup(false);
scanner.setScanDepth(3);
scanner.setScanDepth(Integer.MAX_VALUE); // Integer.MAX_VALUE means unlimited depth.

scanner.addListener(
new Scanner.BulkListener() {
Expand Down
Loading