Reuse npm cache for run previews
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,623 @@
|
|||||||
|
package com.ruoyi.generator.service.front;
|
||||||
|
|
||||||
|
import com.ruoyi.generator.domain.front.FrontProject;
|
||||||
|
import com.ruoyi.generator.domain.front.dto.ProjectRunPreviewStatus;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.MockitoAnnotations;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
|
import com.sun.net.httpserver.HttpHandler;
|
||||||
|
import com.sun.net.httpserver.HttpServer;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
|
||||||
|
public class FrontProjectRunPreviewServiceImplTest
|
||||||
|
{
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
private FrontProjectRunPreviewServiceImpl runPreviewService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private IFrontProjectService frontProjectService;
|
||||||
|
@Mock
|
||||||
|
private IFrontProjectPreviewService frontProjectPreviewService;
|
||||||
|
@Mock
|
||||||
|
private RunPreviewPortAllocator portAllocator;
|
||||||
|
@Mock
|
||||||
|
private RunPreviewDatabaseInitializer databaseInitializer;
|
||||||
|
@Mock
|
||||||
|
private RunPreviewProcessRunner processRunner;
|
||||||
|
|
||||||
|
private RecordingProcessKiller processKiller;
|
||||||
|
private FakeProcess backendProcess;
|
||||||
|
private FakeProcess frontendProcess;
|
||||||
|
private FakeProcess adminFrontendProcess;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() throws Exception
|
||||||
|
{
|
||||||
|
MockitoAnnotations.initMocks(this);
|
||||||
|
runPreviewService = new FrontProjectRunPreviewServiceImpl();
|
||||||
|
processKiller = new RecordingProcessKiller();
|
||||||
|
backendProcess = new FakeProcess();
|
||||||
|
frontendProcess = new FakeProcess();
|
||||||
|
adminFrontendProcess = new FakeProcess();
|
||||||
|
|
||||||
|
setField("frontProjectService", frontProjectService);
|
||||||
|
setField("frontProjectPreviewService", frontProjectPreviewService);
|
||||||
|
setField("portAllocator", portAllocator);
|
||||||
|
setField("databaseInitializer", databaseInitializer);
|
||||||
|
setField("processRunner", processRunner);
|
||||||
|
setField("processKiller", processKiller);
|
||||||
|
setField("workspaceRoot", temporaryFolder.newFolder("preview-workspaces").getAbsolutePath());
|
||||||
|
setField("mysqlHost", "127.0.0.1");
|
||||||
|
setField("mysqlPort", 3306);
|
||||||
|
setField("mysqlUsername", "root");
|
||||||
|
setField("mysqlPassword", "123456");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void startCreatesWorkspaceInitializesDatabaseAndStartsProcesses() throws Exception
|
||||||
|
{
|
||||||
|
when(frontProjectService.getProject(10L, 20L)).thenReturn(project());
|
||||||
|
when(frontProjectPreviewService.downloadAll(10L, 20L)).thenReturn(zipWithGeneratedProject());
|
||||||
|
when(portAllocator.allocate()).thenReturn(18080, 18081);
|
||||||
|
when(databaseInitializer.initialize(any(File.class), anyString(), eq("127.0.0.1"), eq(3306), eq("root"), eq("123456")))
|
||||||
|
.thenReturn("jdbc:mysql://127.0.0.1:3306/preview_20_test");
|
||||||
|
whenBackendStarts();
|
||||||
|
when(processRunner.startFrontend(any(File.class), any(Map.class))).thenReturn(frontendProcess);
|
||||||
|
|
||||||
|
ProjectRunPreviewStatus status = runPreviewService.start(10L, 20L);
|
||||||
|
|
||||||
|
assertEquals("STARTING", status.getStatus());
|
||||||
|
assertEquals("http://127.0.0.1:18080", status.getBackendUrl());
|
||||||
|
assertEquals("http://127.0.0.1:18081", status.getFrontendUrl());
|
||||||
|
assertTrue(new File(status.getWorkspacePath(), "pom.xml").isFile());
|
||||||
|
assertTrue(new File(status.getWorkspacePath(), "vue/package.json").isFile());
|
||||||
|
|
||||||
|
ArgumentCaptor<File> sqlDirectoryCaptor = ArgumentCaptor.forClass(File.class);
|
||||||
|
ArgumentCaptor<String> databaseNameCaptor = ArgumentCaptor.forClass(String.class);
|
||||||
|
verify(databaseInitializer).initialize(sqlDirectoryCaptor.capture(), databaseNameCaptor.capture(), eq("127.0.0.1"), eq(3306), eq("root"), eq("123456"));
|
||||||
|
assertEquals("sql", sqlDirectoryCaptor.getValue().getName());
|
||||||
|
assertTrue(databaseNameCaptor.getValue().startsWith("preview_20_"));
|
||||||
|
|
||||||
|
ArgumentCaptor<Map> backendEnvCaptor = ArgumentCaptor.forClass(Map.class);
|
||||||
|
verify(processRunner).startBackend(any(File.class), any(File.class), any(File.class), anyString(), any(File.class), backendEnvCaptor.capture());
|
||||||
|
assertEquals("18080", backendEnvCaptor.getValue().get("SERVER_PORT"));
|
||||||
|
assertEquals("jdbc:mysql://127.0.0.1:3306/preview_20_test", backendEnvCaptor.getValue().get("DB_URL"));
|
||||||
|
assertEquals("root", backendEnvCaptor.getValue().get("DB_USERNAME"));
|
||||||
|
assertEquals("123456", backendEnvCaptor.getValue().get("DB_PASSWORD"));
|
||||||
|
assertTrue(((String) backendEnvCaptor.getValue().get("JAVA_HOME")).length() > 0);
|
||||||
|
assertTrue(((String) backendEnvCaptor.getValue().get("MAVEN_OPTS")).contains("java.io.tmpdir"));
|
||||||
|
assertTrue(((String) backendEnvCaptor.getValue().get("MAVEN_OPTS")).contains("java-tmp"));
|
||||||
|
assertTrue(((String) backendEnvCaptor.getValue().get("JAVA_TOOL_OPTIONS")).contains("java.io.tmpdir"));
|
||||||
|
|
||||||
|
ArgumentCaptor<Map> frontendEnvCaptor = ArgumentCaptor.forClass(Map.class);
|
||||||
|
verify(processRunner).startFrontend(any(File.class), frontendEnvCaptor.capture());
|
||||||
|
assertEquals("18081", frontendEnvCaptor.getValue().get("PORT"));
|
||||||
|
assertEquals("http://127.0.0.1:18080", frontendEnvCaptor.getValue().get("VUE_APP_API_BASE_URL"));
|
||||||
|
String frontendCache = (String) frontendEnvCaptor.getValue().get("NPM_CONFIG_CACHE");
|
||||||
|
assertEquals(".npm-cache", new File(frontendCache).getName());
|
||||||
|
assertEquals(new File(status.getWorkspacePath()).getParentFile().getCanonicalFile(),
|
||||||
|
new File(frontendCache).getParentFile().getCanonicalFile());
|
||||||
|
assertEquals(frontendCache, frontendEnvCaptor.getValue().get("npm_config_cache"));
|
||||||
|
assertEquals("false", frontendEnvCaptor.getValue().get("NPM_CONFIG_UPDATE_NOTIFIER"));
|
||||||
|
assertEquals("1", frontendEnvCaptor.getValue().get("NO_UPDATE_NOTIFIER"));
|
||||||
|
assertEquals("false", frontendEnvCaptor.getValue().get("NPM_CONFIG_OFFLINE"));
|
||||||
|
assertEquals("true", frontendEnvCaptor.getValue().get("NPM_CONFIG_PREFER_OFFLINE"));
|
||||||
|
assertEquals("false", frontendEnvCaptor.getValue().get("NPM_CONFIG_PREFER_ONLINE"));
|
||||||
|
assertEquals("", frontendEnvCaptor.getValue().get("NPM_CONFIG_PROXY"));
|
||||||
|
assertEquals("", frontendEnvCaptor.getValue().get("NPM_CONFIG_HTTPS_PROXY"));
|
||||||
|
assertEquals("*", frontendEnvCaptor.getValue().get("NO_PROXY"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void startRenamesGeneratedJavaFileToMatchPublicApplicationClass() throws Exception
|
||||||
|
{
|
||||||
|
when(frontProjectService.getProject(10L, 20L)).thenReturn(project());
|
||||||
|
when(frontProjectPreviewService.downloadAll(10L, 20L)).thenReturn(zipWithMismatchedApplicationFile());
|
||||||
|
when(portAllocator.allocate()).thenReturn(18080, 18081);
|
||||||
|
when(databaseInitializer.initialize(any(File.class), anyString(), anyString(), anyInt(), anyString(), anyString()))
|
||||||
|
.thenReturn("jdbc:mysql://127.0.0.1:3306/preview_20_test");
|
||||||
|
whenBackendStarts();
|
||||||
|
when(processRunner.startFrontend(any(File.class), any(Map.class))).thenReturn(frontendProcess);
|
||||||
|
|
||||||
|
ProjectRunPreviewStatus status = runPreviewService.start(10L, 20L);
|
||||||
|
|
||||||
|
File javaDirectory = new File(status.getWorkspacePath(), "demo-backend/src/main/java/com/example/demo");
|
||||||
|
assertTrue(new File(javaDirectory, "DemoApplication.java").isFile());
|
||||||
|
assertTrue(!new File(javaDirectory, "Application.java").exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void startSupportsCombinedDownloadPackageLayout() throws Exception
|
||||||
|
{
|
||||||
|
when(frontProjectService.getProject(10L, 20L)).thenReturn(project());
|
||||||
|
when(frontProjectPreviewService.downloadAll(10L, 20L)).thenReturn(zipWithCombinedGeneratedProject());
|
||||||
|
when(portAllocator.allocate()).thenReturn(18080, 18081, 18082);
|
||||||
|
when(databaseInitializer.initialize(any(File.class), anyString(), eq("127.0.0.1"), eq(3306), eq("root"), eq("123456")))
|
||||||
|
.thenReturn("jdbc:mysql://127.0.0.1:3306/preview_20_test");
|
||||||
|
whenBackendStarts();
|
||||||
|
when(processRunner.startFrontend(any(File.class), any(Map.class))).thenReturn(frontendProcess, adminFrontendProcess);
|
||||||
|
|
||||||
|
ProjectRunPreviewStatus status = runPreviewService.start(10L, 20L);
|
||||||
|
|
||||||
|
assertEquals("STARTING", status.getStatus());
|
||||||
|
assertEquals("http://127.0.0.1:18081", status.getFrontendUrl());
|
||||||
|
assertEquals("http://127.0.0.1:18082", status.getAdminFrontendUrl());
|
||||||
|
assertTrue(new File(status.getWorkspacePath(), "demo-backend/pom.xml").isFile());
|
||||||
|
assertTrue(new File(status.getWorkspacePath(), "demo-web/package.json").isFile());
|
||||||
|
|
||||||
|
ArgumentCaptor<File> previewBuildDirectoryCaptor = ArgumentCaptor.forClass(File.class);
|
||||||
|
ArgumentCaptor<File> rootPomCaptor = ArgumentCaptor.forClass(File.class);
|
||||||
|
ArgumentCaptor<File> reactorPomCaptor = ArgumentCaptor.forClass(File.class);
|
||||||
|
ArgumentCaptor<String> backendModulePathCaptor = ArgumentCaptor.forClass(String.class);
|
||||||
|
ArgumentCaptor<File> localRepositoryCaptor = ArgumentCaptor.forClass(File.class);
|
||||||
|
verify(processRunner).startBackend(previewBuildDirectoryCaptor.capture(), rootPomCaptor.capture(), reactorPomCaptor.capture(), backendModulePathCaptor.capture(), localRepositoryCaptor.capture(), any(Map.class));
|
||||||
|
assertEquals(".easycode-preview", previewBuildDirectoryCaptor.getValue().getName());
|
||||||
|
assertEquals("pom.xml", rootPomCaptor.getValue().getName());
|
||||||
|
assertEquals("pom.xml", reactorPomCaptor.getValue().getName());
|
||||||
|
assertEquals("../demo-backend", backendModulePathCaptor.getValue());
|
||||||
|
assertEquals("repository", localRepositoryCaptor.getValue().getName());
|
||||||
|
|
||||||
|
String reactorPom = new String(Files.readAllBytes(reactorPomCaptor.getValue().toPath()), StandardCharsets.UTF_8);
|
||||||
|
assertTrue(reactorPom.contains("ruoyi-common"));
|
||||||
|
assertTrue(reactorPom.contains("ruoyi-system"));
|
||||||
|
assertTrue(reactorPom.contains("ruoyi-framework"));
|
||||||
|
assertTrue(reactorPom.contains("<module>../demo-backend</module>"));
|
||||||
|
|
||||||
|
ArgumentCaptor<File> frontendDirectoryCaptor = ArgumentCaptor.forClass(File.class);
|
||||||
|
ArgumentCaptor<Map> frontendEnvCaptor = ArgumentCaptor.forClass(Map.class);
|
||||||
|
verify(processRunner, times(2)).startFrontend(frontendDirectoryCaptor.capture(), frontendEnvCaptor.capture());
|
||||||
|
assertEquals("demo-web", frontendDirectoryCaptor.getAllValues().get(0).getName());
|
||||||
|
assertEquals("demo-admin", frontendDirectoryCaptor.getAllValues().get(1).getName());
|
||||||
|
String frontendCache = (String) frontendEnvCaptor.getAllValues().get(0).get("NPM_CONFIG_CACHE");
|
||||||
|
String adminFrontendCache = (String) frontendEnvCaptor.getAllValues().get(1).get("NPM_CONFIG_CACHE");
|
||||||
|
assertEquals(frontendCache, adminFrontendCache);
|
||||||
|
assertEquals(".npm-cache", new File(frontendCache).getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void startInitializesDatabaseFromBackendSqlWhenCombinedPackageHasRootSql() throws Exception
|
||||||
|
{
|
||||||
|
when(frontProjectService.getProject(10L, 20L)).thenReturn(project());
|
||||||
|
when(frontProjectPreviewService.downloadAll(10L, 20L)).thenReturn(zipWithCombinedGeneratedProjectAndBackendSql());
|
||||||
|
when(portAllocator.allocate()).thenReturn(18080, 18081);
|
||||||
|
when(databaseInitializer.initialize(any(File.class), anyString(), eq("127.0.0.1"), eq(3306), eq("root"), eq("123456")))
|
||||||
|
.thenReturn("jdbc:mysql://127.0.0.1:3306/preview_20_test");
|
||||||
|
whenBackendStarts();
|
||||||
|
when(processRunner.startFrontend(any(File.class), any(Map.class))).thenReturn(frontendProcess);
|
||||||
|
|
||||||
|
ProjectRunPreviewStatus status = runPreviewService.start(10L, 20L);
|
||||||
|
|
||||||
|
ArgumentCaptor<File> sqlDirectoryCaptor = ArgumentCaptor.forClass(File.class);
|
||||||
|
verify(databaseInitializer).initialize(sqlDirectoryCaptor.capture(), anyString(), eq("127.0.0.1"), eq(3306), eq("root"), eq("123456"));
|
||||||
|
assertEquals("STARTING", status.getStatus());
|
||||||
|
assertEquals("sql", sqlDirectoryCaptor.getValue().getName());
|
||||||
|
assertEquals("demo-backend", sqlDirectoryCaptor.getValue().getParentFile().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void startWrapsGeneratedVueConfigForPreviewPorts() throws Exception
|
||||||
|
{
|
||||||
|
when(frontProjectService.getProject(10L, 20L)).thenReturn(project());
|
||||||
|
when(frontProjectPreviewService.downloadAll(10L, 20L)).thenReturn(zipWithCombinedGeneratedProject());
|
||||||
|
when(portAllocator.allocate()).thenReturn(18080, 18081);
|
||||||
|
when(databaseInitializer.initialize(any(File.class), anyString(), eq("127.0.0.1"), eq(3306), eq("root"), eq("123456")))
|
||||||
|
.thenReturn("jdbc:mysql://127.0.0.1:3306/preview_20_test");
|
||||||
|
whenBackendStarts();
|
||||||
|
when(processRunner.startFrontend(any(File.class), any(Map.class))).thenReturn(frontendProcess);
|
||||||
|
|
||||||
|
ProjectRunPreviewStatus status = runPreviewService.start(10L, 20L);
|
||||||
|
|
||||||
|
File frontendDirectory = new File(status.getWorkspacePath(), "demo-web");
|
||||||
|
assertTrue(new File(frontendDirectory, "vue.config.generated.js").isFile());
|
||||||
|
String previewConfig = new String(Files.readAllBytes(new File(frontendDirectory, "vue.config.js").toPath()), StandardCharsets.UTF_8);
|
||||||
|
assertTrue(previewConfig.contains("process.env.PORT"));
|
||||||
|
assertTrue(previewConfig.contains("process.env.VUE_APP_API_BASE_URL"));
|
||||||
|
assertTrue(previewConfig.contains("vue.config.generated.js"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void startWritesViteConfigForGeneratedViteFrontend() throws Exception
|
||||||
|
{
|
||||||
|
when(frontProjectService.getProject(10L, 20L)).thenReturn(project());
|
||||||
|
when(frontProjectPreviewService.downloadAll(10L, 20L)).thenReturn(zipWithViteGeneratedProject());
|
||||||
|
when(portAllocator.allocate()).thenReturn(18080, 18081);
|
||||||
|
when(databaseInitializer.initialize(any(File.class), anyString(), eq("127.0.0.1"), eq(3306), eq("root"), eq("123456")))
|
||||||
|
.thenReturn("jdbc:mysql://127.0.0.1:3306/preview_20_test");
|
||||||
|
whenBackendStarts();
|
||||||
|
when(processRunner.startFrontend(any(File.class), any(Map.class))).thenReturn(frontendProcess);
|
||||||
|
|
||||||
|
ProjectRunPreviewStatus status = runPreviewService.start(10L, 20L);
|
||||||
|
|
||||||
|
File frontendDirectory = new File(status.getWorkspacePath(), "demo-portal");
|
||||||
|
String viteConfig = new String(Files.readAllBytes(new File(frontendDirectory, "vite.config.js").toPath()), StandardCharsets.UTF_8);
|
||||||
|
assertTrue(viteConfig.contains("process.env.PORT"));
|
||||||
|
assertTrue(viteConfig.contains("process.env.VUE_APP_API_BASE_URL"));
|
||||||
|
assertTrue(viteConfig.contains("process.env.VITE_APP_API_BASE_URL"));
|
||||||
|
|
||||||
|
ArgumentCaptor<Map> frontendEnvCaptor = ArgumentCaptor.forClass(Map.class);
|
||||||
|
verify(processRunner).startFrontend(any(File.class), frontendEnvCaptor.capture());
|
||||||
|
assertEquals("/api", frontendEnvCaptor.getValue().get("VITE_APP_BASE_API"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void startUsesFreshWorkspaceWhenPreviousProjectWorkspaceExists() throws Exception
|
||||||
|
{
|
||||||
|
File legacyWorkspace = new File(temporaryFolder.getRoot(), "preview-workspaces/project-20");
|
||||||
|
assertTrue(legacyWorkspace.mkdirs());
|
||||||
|
File legacyFile = new File(legacyWorkspace, "server_code");
|
||||||
|
assertTrue(legacyFile.createNewFile());
|
||||||
|
|
||||||
|
when(frontProjectService.getProject(10L, 20L)).thenReturn(project());
|
||||||
|
when(frontProjectPreviewService.downloadAll(10L, 20L)).thenReturn(zipWithGeneratedProject());
|
||||||
|
when(portAllocator.allocate()).thenReturn(18080, 18081);
|
||||||
|
when(databaseInitializer.initialize(any(File.class), anyString(), anyString(), anyInt(), anyString(), anyString()))
|
||||||
|
.thenReturn("jdbc:mysql://127.0.0.1:3306/preview_20_test");
|
||||||
|
whenBackendStarts();
|
||||||
|
when(processRunner.startFrontend(any(File.class), any(Map.class))).thenReturn(frontendProcess);
|
||||||
|
|
||||||
|
ProjectRunPreviewStatus status = runPreviewService.start(10L, 20L);
|
||||||
|
|
||||||
|
assertEquals("STARTING", status.getStatus());
|
||||||
|
assertTrue("legacy workspace should be left alone when a new preview starts", legacyFile.isFile());
|
||||||
|
assertTrue("fresh workspace should include the project id and a unique suffix",
|
||||||
|
new File(status.getWorkspacePath()).getName().startsWith("project-20-"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void statusMarksPreviewRunningAfterFrontendServerIsReady() throws Exception
|
||||||
|
{
|
||||||
|
HttpServer frontendServer = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
|
||||||
|
frontendServer.createContext("/", new HttpHandler()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void handle(HttpExchange exchange) throws IOException
|
||||||
|
{
|
||||||
|
byte[] body = "ready".getBytes("UTF-8");
|
||||||
|
exchange.sendResponseHeaders(200, body.length);
|
||||||
|
OutputStream response = exchange.getResponseBody();
|
||||||
|
response.write(body);
|
||||||
|
response.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
frontendServer.start();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
when(frontProjectService.getProject(10L, 20L)).thenReturn(project());
|
||||||
|
when(frontProjectPreviewService.downloadAll(10L, 20L)).thenReturn(zipWithGeneratedProject());
|
||||||
|
when(portAllocator.allocate()).thenReturn(18080, frontendServer.getAddress().getPort());
|
||||||
|
when(databaseInitializer.initialize(any(File.class), anyString(), anyString(), anyInt(), anyString(), anyString()))
|
||||||
|
.thenReturn("jdbc:mysql://127.0.0.1:3306/preview_20_test");
|
||||||
|
whenBackendStarts();
|
||||||
|
when(processRunner.startFrontend(any(File.class), any(Map.class))).thenReturn(frontendProcess);
|
||||||
|
|
||||||
|
ProjectRunPreviewStatus startStatus = runPreviewService.start(10L, 20L);
|
||||||
|
ProjectRunPreviewStatus readyStatus = runPreviewService.status(10L, 20L);
|
||||||
|
|
||||||
|
assertEquals("STARTING", startStatus.getStatus());
|
||||||
|
assertEquals("RUNNING", readyStatus.getStatus());
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
frontendServer.stop(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void statusWaitsForBothFrontendsWhenAdminFrontendExists() throws Exception
|
||||||
|
{
|
||||||
|
HttpServer frontendServer = readyServer();
|
||||||
|
HttpServer adminFrontendServer = readyServer();
|
||||||
|
frontendServer.start();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
when(frontProjectService.getProject(10L, 20L)).thenReturn(project());
|
||||||
|
when(frontProjectPreviewService.downloadAll(10L, 20L)).thenReturn(zipWithCombinedGeneratedProject());
|
||||||
|
when(portAllocator.allocate()).thenReturn(18080, frontendServer.getAddress().getPort(), adminFrontendServer.getAddress().getPort());
|
||||||
|
when(databaseInitializer.initialize(any(File.class), anyString(), anyString(), anyInt(), anyString(), anyString()))
|
||||||
|
.thenReturn("jdbc:mysql://127.0.0.1:3306/preview_20_test");
|
||||||
|
whenBackendStarts();
|
||||||
|
when(processRunner.startFrontend(any(File.class), any(Map.class))).thenReturn(frontendProcess, adminFrontendProcess);
|
||||||
|
|
||||||
|
runPreviewService.start(10L, 20L);
|
||||||
|
ProjectRunPreviewStatus waitingStatus = runPreviewService.status(10L, 20L);
|
||||||
|
adminFrontendServer.start();
|
||||||
|
ProjectRunPreviewStatus readyStatus = runPreviewService.status(10L, 20L);
|
||||||
|
|
||||||
|
assertEquals("STARTING", waitingStatus.getStatus());
|
||||||
|
assertEquals("RUNNING", readyStatus.getStatus());
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
frontendServer.stop(0);
|
||||||
|
adminFrontendServer.stop(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void statusUsesChineseMessageWhenGeneratedFrontendProcessExits() throws Exception
|
||||||
|
{
|
||||||
|
when(frontProjectService.getProject(10L, 20L)).thenReturn(project());
|
||||||
|
when(frontProjectPreviewService.downloadAll(10L, 20L)).thenReturn(zipWithGeneratedProject());
|
||||||
|
when(portAllocator.allocate()).thenReturn(18080, 18081);
|
||||||
|
when(databaseInitializer.initialize(any(File.class), anyString(), anyString(), anyInt(), anyString(), anyString()))
|
||||||
|
.thenReturn("jdbc:mysql://127.0.0.1:3306/preview_20_test");
|
||||||
|
whenBackendStarts();
|
||||||
|
when(processRunner.startFrontend(any(File.class), any(Map.class))).thenReturn(frontendProcess);
|
||||||
|
|
||||||
|
runPreviewService.start(10L, 20L);
|
||||||
|
frontendProcess.destroy();
|
||||||
|
ProjectRunPreviewStatus status = runPreviewService.status(10L, 20L);
|
||||||
|
|
||||||
|
assertEquals("FAILED", status.getStatus());
|
||||||
|
assertEquals("生成的前台前端进程已退出", status.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void stopDestroysRunningPreviewProcesses() throws Exception
|
||||||
|
{
|
||||||
|
when(frontProjectService.getProject(10L, 20L)).thenReturn(project());
|
||||||
|
when(frontProjectPreviewService.downloadAll(10L, 20L)).thenReturn(zipWithGeneratedProject());
|
||||||
|
when(portAllocator.allocate()).thenReturn(18080, 18081);
|
||||||
|
when(databaseInitializer.initialize(any(File.class), anyString(), anyString(), anyInt(), anyString(), anyString()))
|
||||||
|
.thenReturn("jdbc:mysql://127.0.0.1:3306/preview_20_test");
|
||||||
|
whenBackendStarts();
|
||||||
|
when(processRunner.startFrontend(any(File.class), any(Map.class))).thenReturn(frontendProcess);
|
||||||
|
|
||||||
|
runPreviewService.start(10L, 20L);
|
||||||
|
ProjectRunPreviewStatus status = runPreviewService.stop(10L, 20L);
|
||||||
|
|
||||||
|
assertEquals("STOPPED", status.getStatus());
|
||||||
|
assertTrue(backendProcess.destroyed);
|
||||||
|
assertTrue(frontendProcess.destroyed);
|
||||||
|
assertEquals(Integer.valueOf(18081), processKiller.stoppedPorts.get(0));
|
||||||
|
assertEquals(Integer.valueOf(18080), processKiller.stoppedPorts.get(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void stopDestroysAdminFrontendAndAllListeningPorts() throws Exception
|
||||||
|
{
|
||||||
|
when(frontProjectService.getProject(10L, 20L)).thenReturn(project());
|
||||||
|
when(frontProjectPreviewService.downloadAll(10L, 20L)).thenReturn(zipWithCombinedGeneratedProject());
|
||||||
|
when(portAllocator.allocate()).thenReturn(18080, 18081, 18082);
|
||||||
|
when(databaseInitializer.initialize(any(File.class), anyString(), anyString(), anyInt(), anyString(), anyString()))
|
||||||
|
.thenReturn("jdbc:mysql://127.0.0.1:3306/preview_20_test");
|
||||||
|
whenBackendStarts();
|
||||||
|
when(processRunner.startFrontend(any(File.class), any(Map.class))).thenReturn(frontendProcess, adminFrontendProcess);
|
||||||
|
|
||||||
|
runPreviewService.start(10L, 20L);
|
||||||
|
ProjectRunPreviewStatus status = runPreviewService.stop(10L, 20L);
|
||||||
|
|
||||||
|
assertEquals("STOPPED", status.getStatus());
|
||||||
|
assertTrue(backendProcess.destroyed);
|
||||||
|
assertTrue(frontendProcess.destroyed);
|
||||||
|
assertTrue(adminFrontendProcess.destroyed);
|
||||||
|
assertEquals(Integer.valueOf(18082), processKiller.stoppedPorts.get(0));
|
||||||
|
assertEquals(Integer.valueOf(18081), processKiller.stoppedPorts.get(1));
|
||||||
|
assertEquals(Integer.valueOf(18080), processKiller.stoppedPorts.get(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void statusReturnsNotStartedWhenNoSessionExists()
|
||||||
|
{
|
||||||
|
when(frontProjectService.getProject(10L, 20L)).thenReturn(project());
|
||||||
|
|
||||||
|
ProjectRunPreviewStatus status = runPreviewService.status(10L, 20L);
|
||||||
|
|
||||||
|
assertEquals("NOT_STARTED", status.getStatus());
|
||||||
|
assertEquals(Long.valueOf(20L), status.getProjectId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private FrontProject project()
|
||||||
|
{
|
||||||
|
FrontProject project = new FrontProject();
|
||||||
|
project.setProjectId(20L);
|
||||||
|
project.setProjectName("Demo");
|
||||||
|
project.setProjectFileName("demo");
|
||||||
|
return project;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void whenBackendStarts() throws IOException
|
||||||
|
{
|
||||||
|
when(processRunner.startBackend(any(File.class), any(File.class), any(File.class), anyString(), any(File.class), any(Map.class)))
|
||||||
|
.thenReturn(backendProcess);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpServer readyServer() throws IOException
|
||||||
|
{
|
||||||
|
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
|
||||||
|
server.createContext("/", new HttpHandler()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void handle(HttpExchange exchange) throws IOException
|
||||||
|
{
|
||||||
|
byte[] body = "ready".getBytes("UTF-8");
|
||||||
|
exchange.sendResponseHeaders(200, body.length);
|
||||||
|
OutputStream response = exchange.getResponseBody();
|
||||||
|
response.write(body);
|
||||||
|
response.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] zipWithGeneratedProject() throws IOException
|
||||||
|
{
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
ZipOutputStream zip = new ZipOutputStream(outputStream);
|
||||||
|
addEntry(zip, "pom.xml", "<project />");
|
||||||
|
addEntry(zip, "vue/package.json", "{\"scripts\":{\"dev\":\"vue-cli-service serve\"}}");
|
||||||
|
addEntry(zip, "sql/demo.sql", "create table demo (id bigint);");
|
||||||
|
zip.close();
|
||||||
|
return outputStream.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] zipWithMismatchedApplicationFile() throws IOException
|
||||||
|
{
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
ZipOutputStream zip = new ZipOutputStream(outputStream);
|
||||||
|
addEntry(zip, "demo-backend/pom.xml", "<project />");
|
||||||
|
addEntry(zip, "demo-backend/src/main/java/com/example/demo/Application.java",
|
||||||
|
"package com.example.demo;\n\npublic class DemoApplication {}\n");
|
||||||
|
addEntry(zip, "demo-web/package.json", "{\"scripts\":{\"dev\":\"vue-cli-service serve\"}}");
|
||||||
|
addEntry(zip, "sql/demo.sql", "create table demo (id bigint);");
|
||||||
|
zip.close();
|
||||||
|
return outputStream.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] zipWithCombinedGeneratedProject() throws IOException
|
||||||
|
{
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
ZipOutputStream zip = new ZipOutputStream(outputStream);
|
||||||
|
addEntry(zip, "demo-backend/pom.xml", "<project />");
|
||||||
|
addEntry(zip, "demo-web/package.json", "{\"scripts\":{\"dev\":\"vue-cli-service serve\"}}");
|
||||||
|
addEntry(zip, "demo-web/vue.config.js", "module.exports = { devServer: { port: 8082, proxy: { \"/api\": { target: \"http://localhost:8080\" } } } }");
|
||||||
|
addEntry(zip, "demo-admin/package.json", "{\"scripts\":{\"dev\":\"vue-cli-service serve\"}}");
|
||||||
|
addEntry(zip, "sql/demo.sql", "create table demo (id bigint);");
|
||||||
|
zip.close();
|
||||||
|
return outputStream.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] zipWithCombinedGeneratedProjectAndBackendSql() throws IOException
|
||||||
|
{
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
ZipOutputStream zip = new ZipOutputStream(outputStream);
|
||||||
|
addEntry(zip, "demo-backend/pom.xml", "<project />");
|
||||||
|
addEntry(zip, "demo-backend/sql/demo.sql", "create table demo_backend (id bigint);");
|
||||||
|
addEntry(zip, "demo-web/package.json", "{\"scripts\":{\"dev\":\"vue-cli-service serve\"}}");
|
||||||
|
addEntry(zip, "sql/demo.sql", "create table demo_root (id bigint);");
|
||||||
|
zip.close();
|
||||||
|
return outputStream.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] zipWithViteGeneratedProject() throws IOException
|
||||||
|
{
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
ZipOutputStream zip = new ZipOutputStream(outputStream);
|
||||||
|
addEntry(zip, "demo-backend/pom.xml", "<project />");
|
||||||
|
addEntry(zip, "demo-portal/package.json", "{\"scripts\":{\"dev\":\"vite --host 0.0.0.0\"},\"dependencies\":{\"@vitejs/plugin-vue\":\"5.0.5\",\"vite\":\"5.3.1\",\"vue\":\"3.4.29\"}}");
|
||||||
|
addEntry(zip, "sql/demo.sql", "create table demo (id bigint);");
|
||||||
|
zip.close();
|
||||||
|
return outputStream.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addEntry(ZipOutputStream zip, String name, String content) throws IOException
|
||||||
|
{
|
||||||
|
zip.putNextEntry(new ZipEntry(name));
|
||||||
|
zip.write(content.getBytes("UTF-8"));
|
||||||
|
zip.closeEntry();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setField(String name, Object value) throws Exception
|
||||||
|
{
|
||||||
|
Field field = FrontProjectRunPreviewServiceImpl.class.getDeclaredField(name);
|
||||||
|
field.setAccessible(true);
|
||||||
|
field.set(runPreviewService, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class FakeProcess extends Process
|
||||||
|
{
|
||||||
|
private boolean alive = true;
|
||||||
|
private boolean destroyed = false;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OutputStream getOutputStream()
|
||||||
|
{
|
||||||
|
return new ByteArrayOutputStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream getInputStream()
|
||||||
|
{
|
||||||
|
return new ByteArrayInputStream(new byte[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream getErrorStream()
|
||||||
|
{
|
||||||
|
return new ByteArrayInputStream(new byte[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int waitFor()
|
||||||
|
{
|
||||||
|
alive = false;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int exitValue()
|
||||||
|
{
|
||||||
|
if (alive)
|
||||||
|
{
|
||||||
|
throw new IllegalThreadStateException();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroy()
|
||||||
|
{
|
||||||
|
alive = false;
|
||||||
|
destroyed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAlive()
|
||||||
|
{
|
||||||
|
return alive;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class RecordingProcessKiller extends RunPreviewProcessKiller
|
||||||
|
{
|
||||||
|
private final List<Integer> stoppedPorts = new ArrayList<Integer>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stop(Process process, Integer port)
|
||||||
|
{
|
||||||
|
if (process != null)
|
||||||
|
{
|
||||||
|
process.destroy();
|
||||||
|
}
|
||||||
|
if (port != null)
|
||||||
|
{
|
||||||
|
stoppedPorts.add(port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user