Initial commit: add AutoDeploy project
This commit is contained in:
674
tests/test_deployer.py
Normal file
674
tests/test_deployer.py
Normal file
@@ -0,0 +1,674 @@
|
||||
import unittest
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
from unittest.mock import patch
|
||||
|
||||
from deploy_tool.deployer import DeploymentConfig, DeploymentStep, build_deployment_plan, run_steps, resolve_process_command
|
||||
import deploy_tool.deployer as deployer_module
|
||||
from deploy_tool.scanner import ProjectComponent, ScanResult
|
||||
|
||||
|
||||
class DeployerTests(unittest.TestCase):
|
||||
def test_extracts_backend_port_from_java_system_property(self):
|
||||
extract_port = getattr(deployer_module, "_extract_server_port", lambda command: None)
|
||||
|
||||
port = extract_port(["java", "-Dserver.port=9090", "-jar", "app.jar"])
|
||||
|
||||
self.assertEqual(port, 9090)
|
||||
|
||||
def test_rejects_backend_port_outside_valid_tcp_range(self):
|
||||
for value in ("0", "-1", "65536"):
|
||||
with self.subTest(value=value), self.assertRaisesRegex(ValueError, "1 到 65535"):
|
||||
deployer_module._extract_server_port(["java", f"-Dserver.port={value}", "-jar", "app.jar"])
|
||||
|
||||
def test_parses_ipv4_and_ipv6_listeners_for_configured_port(self):
|
||||
parse_listeners = getattr(deployer_module, "_parse_windows_listening_pids", lambda output, port: set())
|
||||
output = """
|
||||
TCP 0.0.0.0:8080 0.0.0.0:0 LISTENING 27544
|
||||
TCP [::]:8080 [::]:0 LISTENING 27544
|
||||
TCP 127.0.0.1:8080 127.0.0.1:52100 ESTABLISHED 30000
|
||||
TCP 0.0.0.0:9090 0.0.0.0:0 LISTENING 31000
|
||||
"""
|
||||
|
||||
pids = parse_listeners(output, 8080)
|
||||
|
||||
self.assertEqual(pids, {27544})
|
||||
|
||||
def test_port_release_waits_until_no_interface_has_a_listener(self):
|
||||
listener_snapshots = iter(({27544}, set()))
|
||||
|
||||
with (
|
||||
patch.object(deployer_module, "_find_listening_pids", side_effect=lambda port: next(listener_snapshots)) as find_listeners,
|
||||
patch.object(deployer_module, "_is_port_open", return_value=False),
|
||||
patch.object(deployer_module.time, "sleep", return_value=None),
|
||||
):
|
||||
released = deployer_module._wait_until_port_closed(8080)
|
||||
|
||||
self.assertTrue(released)
|
||||
self.assertEqual(find_listeners.call_count, 2)
|
||||
|
||||
def test_replaces_all_processes_listening_on_backend_port(self):
|
||||
replace_listeners = getattr(deployer_module, "_replace_port_listeners", lambda *args, **kwargs: False)
|
||||
terminated = []
|
||||
events = []
|
||||
|
||||
released = replace_listeners(
|
||||
8080,
|
||||
events.append,
|
||||
find_listeners=lambda port: {27544, 23780},
|
||||
terminate=lambda pid: terminated.append(pid),
|
||||
wait_until_closed=lambda port: True,
|
||||
)
|
||||
|
||||
self.assertTrue(released)
|
||||
self.assertEqual(set(terminated), {27544, 23780})
|
||||
self.assertTrue(any("8080" in event and "27544" in event for event in events))
|
||||
|
||||
def test_port_replacement_skips_pid_removed_by_an_earlier_tree_kill(self):
|
||||
replace_listeners = getattr(deployer_module, "_replace_port_listeners", lambda *args, **kwargs: False)
|
||||
active_pids = {23780, 27544}
|
||||
terminated = []
|
||||
|
||||
def terminate_tree(pid):
|
||||
terminated.append(pid)
|
||||
active_pids.clear()
|
||||
|
||||
released = replace_listeners(
|
||||
8080,
|
||||
lambda value: None,
|
||||
find_listeners=lambda port: set(active_pids),
|
||||
terminate=terminate_tree,
|
||||
wait_until_closed=lambda port: True,
|
||||
)
|
||||
|
||||
self.assertTrue(released)
|
||||
self.assertEqual(terminated, [23780])
|
||||
|
||||
def test_backend_restart_fails_before_spawn_when_port_is_not_released(self):
|
||||
restart_backend = getattr(deployer_module, "_restart_backend", lambda *args, **kwargs: 0)
|
||||
spawned = []
|
||||
|
||||
with TemporaryDirectory() as temp:
|
||||
exit_code = restart_backend(
|
||||
["java", "-Dserver.port=8080", "-jar", "app.jar"],
|
||||
Path(temp),
|
||||
lambda value: None,
|
||||
replace_listeners=lambda port, on_output: False,
|
||||
popen=lambda *args, **kwargs: spawned.append(args),
|
||||
)
|
||||
|
||||
self.assertEqual(exit_code, 1)
|
||||
self.assertEqual(spawned, [])
|
||||
|
||||
def test_backend_restart_returns_child_exit_code_when_java_exits_early(self):
|
||||
restart_backend = getattr(deployer_module, "_restart_backend", lambda *args, **kwargs: 0)
|
||||
|
||||
class ExitedProcess:
|
||||
pid = 23780
|
||||
|
||||
def poll(self):
|
||||
return 2
|
||||
|
||||
with TemporaryDirectory() as temp:
|
||||
exit_code = restart_backend(
|
||||
["java", "-Dserver.port=65530", "-jar", "app.jar"],
|
||||
Path(temp),
|
||||
lambda value: None,
|
||||
replace_listeners=lambda port, on_output: True,
|
||||
popen=lambda *args, **kwargs: ExitedProcess(),
|
||||
)
|
||||
|
||||
self.assertEqual(exit_code, 2)
|
||||
|
||||
def test_backend_restart_reports_success_only_after_port_is_reachable(self):
|
||||
restart_backend = getattr(deployer_module, "_restart_backend", lambda *args, **kwargs: 0)
|
||||
checked_ports = []
|
||||
logs = []
|
||||
|
||||
class RunningProcess:
|
||||
pid = 27544
|
||||
|
||||
def poll(self):
|
||||
return None
|
||||
|
||||
def port_is_open(port):
|
||||
checked_ports.append(port)
|
||||
return True
|
||||
|
||||
with (
|
||||
TemporaryDirectory() as temp,
|
||||
patch.object(deployer_module, "_is_port_open", side_effect=port_is_open),
|
||||
patch.object(deployer_module, "_find_listening_pids", return_value={27544}),
|
||||
):
|
||||
exit_code = restart_backend(
|
||||
["java", "-Dserver.port=65530", "-jar", "app.jar"],
|
||||
Path(temp),
|
||||
logs.append,
|
||||
replace_listeners=lambda port, on_output: True,
|
||||
popen=lambda *args, **kwargs: RunningProcess(),
|
||||
)
|
||||
|
||||
self.assertEqual(exit_code, 0)
|
||||
self.assertEqual(checked_ports, [65530])
|
||||
self.assertTrue(any("27544" in line and "启动成功" in line for line in logs))
|
||||
|
||||
def test_backend_restart_rejects_reachable_port_owned_by_another_process(self):
|
||||
restart_backend = getattr(deployer_module, "_restart_backend", lambda *args, **kwargs: 0)
|
||||
terminated = []
|
||||
|
||||
class RunningProcess:
|
||||
pid = 27544
|
||||
|
||||
def poll(self):
|
||||
return None
|
||||
|
||||
with (
|
||||
TemporaryDirectory() as temp,
|
||||
patch.object(deployer_module, "_is_port_open", return_value=True),
|
||||
patch.object(deployer_module, "_find_listening_pids", return_value={31000}),
|
||||
patch.object(deployer_module, "_terminate_process_tree", side_effect=lambda pid: terminated.append(pid)),
|
||||
patch.object(deployer_module.time, "monotonic", side_effect=[0.0, 0.0, 61.0]),
|
||||
):
|
||||
exit_code = restart_backend(
|
||||
["java", "-Dserver.port=65530", "-jar", "app.jar"],
|
||||
Path(temp),
|
||||
lambda value: None,
|
||||
replace_listeners=lambda port, on_output: True,
|
||||
popen=lambda *args, **kwargs: RunningProcess(),
|
||||
)
|
||||
|
||||
self.assertEqual(exit_code, 1)
|
||||
self.assertEqual(terminated, [27544])
|
||||
|
||||
def test_backend_restart_terminates_child_after_startup_timeout(self):
|
||||
restart_backend = getattr(deployer_module, "_restart_backend", lambda *args, **kwargs: 0)
|
||||
terminated = []
|
||||
|
||||
class RunningProcess:
|
||||
pid = 27544
|
||||
|
||||
def poll(self):
|
||||
return None
|
||||
|
||||
with (
|
||||
TemporaryDirectory() as temp,
|
||||
patch.object(deployer_module, "_is_port_open", return_value=False),
|
||||
patch.object(deployer_module, "_terminate_process_tree", side_effect=lambda pid: terminated.append(pid)),
|
||||
patch.object(deployer_module.time, "monotonic", side_effect=[0.0, 61.0]),
|
||||
):
|
||||
exit_code = restart_backend(
|
||||
["java", "-Dserver.port=65530", "-jar", "app.jar"],
|
||||
Path(temp),
|
||||
lambda value: None,
|
||||
replace_listeners=lambda port, on_output: True,
|
||||
popen=lambda *args, **kwargs: RunningProcess(),
|
||||
)
|
||||
|
||||
self.assertEqual(exit_code, 1)
|
||||
self.assertEqual(terminated, [27544])
|
||||
|
||||
@unittest.skipUnless(os.name == "nt", "Windows creation flags only apply on Windows")
|
||||
def test_backend_restart_uses_visible_windows_console(self):
|
||||
restart_backend = getattr(deployer_module, "_restart_backend", lambda *args, **kwargs: 0)
|
||||
popen_options = {}
|
||||
|
||||
class RunningProcess:
|
||||
pid = 27544
|
||||
|
||||
def poll(self):
|
||||
return None
|
||||
|
||||
def fake_popen(*args, **kwargs):
|
||||
popen_options.update(kwargs)
|
||||
return RunningProcess()
|
||||
|
||||
with (
|
||||
TemporaryDirectory() as temp,
|
||||
patch.object(deployer_module, "_is_port_open", return_value=True),
|
||||
patch.object(deployer_module, "_find_listening_pids", return_value={27544}),
|
||||
):
|
||||
exit_code = restart_backend(
|
||||
["java", "-Dserver.port=65530", "-jar", "app.jar"],
|
||||
Path(temp),
|
||||
lambda value: None,
|
||||
replace_listeners=lambda port, on_output: True,
|
||||
popen=fake_popen,
|
||||
)
|
||||
|
||||
self.assertEqual(exit_code, 0)
|
||||
self.assertIn("creationflags", popen_options)
|
||||
self.assertTrue(popen_options["creationflags"] & subprocess.CREATE_NEW_CONSOLE)
|
||||
self.assertTrue(popen_options["creationflags"] & subprocess.CREATE_NEW_PROCESS_GROUP)
|
||||
|
||||
def test_background_runner_returns_backend_restart_health_result(self):
|
||||
class FakeProcess:
|
||||
pid = 27544
|
||||
|
||||
with (
|
||||
TemporaryDirectory() as temp,
|
||||
patch.object(deployer_module, "_restart_backend", return_value=7) as restart_backend,
|
||||
patch.object(deployer_module.subprocess, "Popen", return_value=FakeProcess()),
|
||||
):
|
||||
exit_code = deployer_module._run_process(
|
||||
["java", "-Dserver.port=8080", "-jar", "app.jar"],
|
||||
Path(temp),
|
||||
lambda value: None,
|
||||
background=True,
|
||||
)
|
||||
|
||||
self.assertEqual(exit_code, 7)
|
||||
restart_backend.assert_called_once()
|
||||
|
||||
def test_backend_restart_separates_each_run_in_backend_log(self):
|
||||
restart_backend = getattr(deployer_module, "_restart_backend", lambda *args, **kwargs: 0)
|
||||
|
||||
class RunningProcess:
|
||||
pid = 27544
|
||||
|
||||
def poll(self):
|
||||
return None
|
||||
|
||||
with (
|
||||
TemporaryDirectory() as temp,
|
||||
patch.object(deployer_module, "_is_port_open", return_value=True),
|
||||
patch.object(deployer_module, "_find_listening_pids", return_value={27544}),
|
||||
):
|
||||
root = Path(temp)
|
||||
exit_code = restart_backend(
|
||||
["java", "-Dserver.port=65530", "-jar", "app.jar"],
|
||||
root,
|
||||
lambda value: None,
|
||||
replace_listeners=lambda port, on_output: True,
|
||||
popen=lambda *args, **kwargs: RunningProcess(),
|
||||
)
|
||||
log_text = (root / "backend.log").read_text(encoding="utf-8")
|
||||
|
||||
self.assertEqual(exit_code, 0)
|
||||
self.assertIn("后端部署", log_text)
|
||||
|
||||
def test_builds_plan_for_detected_components(self):
|
||||
root = Path("C:/project")
|
||||
scan = ScanResult(
|
||||
root=root,
|
||||
backend=ProjectComponent("Spring Boot", root / "server", "Maven project"),
|
||||
frontend=ProjectComponent(
|
||||
"Vue",
|
||||
root / "web",
|
||||
"Vue project",
|
||||
{"role": "user", "display_name": "用户端", "start_script": "serve"},
|
||||
),
|
||||
sql_files=[root / "db" / "schema.sql"],
|
||||
)
|
||||
config = DeploymentConfig(
|
||||
mysql_user="root",
|
||||
mysql_password="secret",
|
||||
mysql_database="demo",
|
||||
backend_port="8080",
|
||||
output_dir=root / "deploy",
|
||||
)
|
||||
|
||||
plan = build_deployment_plan(scan, config)
|
||||
|
||||
self.assertEqual(
|
||||
[step.kind for step in plan],
|
||||
[
|
||||
"backend_build",
|
||||
"frontend_install",
|
||||
"sql_import",
|
||||
"backend_start",
|
||||
"frontend_start",
|
||||
"deployment_summary",
|
||||
],
|
||||
)
|
||||
self.assertIn("mvn", plan[0].command[0])
|
||||
self.assertEqual(plan[2].cwd, root)
|
||||
|
||||
def test_builds_and_starts_user_and_admin_frontends_then_logs_all_addresses(self):
|
||||
with TemporaryDirectory() as temp:
|
||||
root = Path(temp)
|
||||
backend = root / "server"
|
||||
client = root / "client_code"
|
||||
admin = root / "manage_code"
|
||||
(backend / "target").mkdir(parents=True)
|
||||
(backend / "target" / "app.jar").write_text("jar", encoding="utf-8")
|
||||
|
||||
frontends = []
|
||||
for path, role, display_name, port in (
|
||||
(client, "user", "用户端", "8082"),
|
||||
(admin, "admin", "管理端", "8081"),
|
||||
):
|
||||
(path / "node_modules").mkdir(parents=True)
|
||||
(path / "dist").mkdir()
|
||||
(path / "dist" / "index.html").write_text("<main>app</main>", encoding="utf-8")
|
||||
(path / "package.json").write_text(
|
||||
'{"scripts":{"build":"vue-cli-service build","serve":"vue-cli-service serve"}}',
|
||||
encoding="utf-8",
|
||||
)
|
||||
frontends.append(
|
||||
ProjectComponent(
|
||||
"Vue",
|
||||
path,
|
||||
"Vue project",
|
||||
{"role": role, "display_name": display_name, "port": port, "start_script": "serve"},
|
||||
)
|
||||
)
|
||||
|
||||
scan = ScanResult(
|
||||
root=root,
|
||||
backend=ProjectComponent(
|
||||
"Spring Boot", backend, "Maven project", {"context_path": "/movies"}
|
||||
),
|
||||
frontend=frontends[0],
|
||||
sql_files=[],
|
||||
frontends=frontends,
|
||||
)
|
||||
plan = build_deployment_plan(scan, DeploymentConfig(output_dir=root / "deploy"))
|
||||
|
||||
self.assertEqual(
|
||||
[step.kind for step in plan],
|
||||
[
|
||||
"backend_build_skip",
|
||||
"backend_start",
|
||||
"frontend_start",
|
||||
"frontend_start",
|
||||
"deployment_summary",
|
||||
],
|
||||
)
|
||||
start_steps = [step for step in plan if step.kind == "frontend_start"]
|
||||
self.assertEqual([step.port for step in start_steps], [8082, 8081])
|
||||
self.assertEqual([step.service_name for step in start_steps], ["用户端", "管理端"])
|
||||
self.assertEqual(start_steps[0].output_path, root / "deploy" / "frontend-user.log")
|
||||
self.assertEqual(start_steps[1].output_path, root / "deploy" / "frontend-admin.log")
|
||||
|
||||
logs = []
|
||||
exit_code = run_steps(plan, logs.append, process_runner=lambda *args: 0)
|
||||
|
||||
self.assertEqual(exit_code, 0)
|
||||
combined = "\n".join(logs)
|
||||
self.assertIn("后端: http://localhost:8080/movies", combined)
|
||||
self.assertIn("用户端: http://localhost:8082/", combined)
|
||||
self.assertIn("管理端: http://localhost:8081/", combined)
|
||||
|
||||
def test_plan_uses_executable_steps_without_shell_placeholders(self):
|
||||
with TemporaryDirectory() as temp:
|
||||
root = Path(temp)
|
||||
backend = root / "server"
|
||||
frontend = root / "web"
|
||||
database = root / "db"
|
||||
backend.mkdir()
|
||||
frontend.mkdir()
|
||||
database.mkdir()
|
||||
(backend / "pom.xml").write_text("<project />", encoding="utf-8")
|
||||
(frontend / "package.json").write_text(
|
||||
'{"scripts":{"build":"vite build","dev":"vite"}}', encoding="utf-8"
|
||||
)
|
||||
sql_file = database / "schema.sql"
|
||||
sql_file.write_text("create table demo(id int);", encoding="utf-8")
|
||||
scan = ScanResult(
|
||||
root=root,
|
||||
backend=ProjectComponent("Spring Boot", backend, "Maven project"),
|
||||
frontend=ProjectComponent("Vue", frontend, "Vue project"),
|
||||
sql_files=[sql_file],
|
||||
)
|
||||
config = DeploymentConfig(mysql_database="demo", output_dir=root / "deploy")
|
||||
|
||||
plan = build_deployment_plan(scan, config)
|
||||
|
||||
self.assertNotIn("frontend_build", [step.kind for step in plan])
|
||||
self.assertNotIn("frontend_copy", [step.kind for step in plan])
|
||||
for step in plan:
|
||||
joined = " ".join(step.command)
|
||||
self.assertNotIn("<", joined)
|
||||
self.assertNotIn(">", joined)
|
||||
sql_step = next(step for step in plan if step.kind == "sql_import")
|
||||
self.assertEqual(sql_step.input_path, sql_file)
|
||||
self.assertNotIn(str(sql_file), sql_step.command)
|
||||
|
||||
def test_run_steps_installs_frontend_imports_sql_and_starts_services(self):
|
||||
with TemporaryDirectory() as temp:
|
||||
root = Path(temp)
|
||||
backend = root / "server"
|
||||
frontend = root / "web"
|
||||
target = backend / "target"
|
||||
database = root / "db"
|
||||
target.mkdir(parents=True)
|
||||
frontend.mkdir(parents=True)
|
||||
database.mkdir()
|
||||
(backend / "pom.xml").write_text("<project />", encoding="utf-8")
|
||||
(frontend / "package.json").write_text(
|
||||
'{"scripts":{"build":"vite build","dev":"vite"}}', encoding="utf-8"
|
||||
)
|
||||
old_jar = target / "old.jar"
|
||||
app_jar = target / "app.jar"
|
||||
old_jar.write_text("old", encoding="utf-8")
|
||||
app_jar.write_text("new", encoding="utf-8")
|
||||
os.utime(old_jar, (1, 1))
|
||||
os.utime(app_jar, (2, 2))
|
||||
sql_file = database / "schema.sql"
|
||||
sql_file.write_text("create table demo(id int);", encoding="utf-8")
|
||||
scan = ScanResult(
|
||||
root=root,
|
||||
backend=ProjectComponent("Spring Boot", backend, "Maven project"),
|
||||
frontend=ProjectComponent("Vue", frontend, "Vue project"),
|
||||
sql_files=[sql_file],
|
||||
)
|
||||
config = DeploymentConfig(
|
||||
mysql_user="root",
|
||||
mysql_password="secret",
|
||||
mysql_database="demo",
|
||||
backend_port="9090",
|
||||
output_dir=root / "deploy",
|
||||
tool_search_roots=[root / "missing"],
|
||||
)
|
||||
plan = build_deployment_plan(scan, config)
|
||||
calls = []
|
||||
|
||||
def fake_runner(command, cwd, on_output, input_path=None, background=False):
|
||||
calls.append((command, cwd, input_path, background))
|
||||
return 0
|
||||
|
||||
exit_code = run_steps(plan, lambda value: None, process_runner=fake_runner)
|
||||
|
||||
self.assertEqual(exit_code, 0)
|
||||
self.assertIn((["mysql", "-u", "root", "-psecret", "demo"], root, sql_file, False), calls)
|
||||
self.assertIn((["java", "-Dserver.port=9090", "-jar", str(app_jar)], root / "deploy", None, True), calls)
|
||||
|
||||
def test_plan_uses_configured_maven_path_for_backend_build(self):
|
||||
with TemporaryDirectory() as temp:
|
||||
root = Path(temp)
|
||||
backend = root / "server"
|
||||
maven = root / "tools" / "apache-maven" / "bin" / "mvn.cmd"
|
||||
backend.mkdir()
|
||||
maven.parent.mkdir(parents=True)
|
||||
(backend / "pom.xml").write_text("<project />", encoding="utf-8")
|
||||
maven.write_text("", encoding="utf-8")
|
||||
scan = ScanResult(
|
||||
root=root,
|
||||
backend=ProjectComponent("Spring Boot", backend, "Maven project"),
|
||||
frontend=None,
|
||||
sql_files=[],
|
||||
)
|
||||
config = DeploymentConfig(tool_paths={"Maven": maven})
|
||||
|
||||
plan = build_deployment_plan(scan, config)
|
||||
|
||||
self.assertEqual(plan[0].command, [str(maven), "clean", "package", "-DskipTests"])
|
||||
|
||||
def test_plan_uses_discovered_mysql_path_for_sql_import(self):
|
||||
with TemporaryDirectory() as temp:
|
||||
root = Path(temp)
|
||||
mysql = root / "MySQL" / "MySQL Server 8.0" / "bin" / "mysql.exe"
|
||||
database = root / "db"
|
||||
mysql.parent.mkdir(parents=True)
|
||||
database.mkdir()
|
||||
mysql.write_text("", encoding="utf-8")
|
||||
sql_file = database / "schema.sql"
|
||||
sql_file.write_text("create table demo(id int);", encoding="utf-8")
|
||||
scan = ScanResult(
|
||||
root=root,
|
||||
backend=None,
|
||||
frontend=None,
|
||||
sql_files=[sql_file],
|
||||
)
|
||||
config = DeploymentConfig(mysql_database="demo", tool_search_roots=[root])
|
||||
|
||||
plan = build_deployment_plan(scan, config)
|
||||
|
||||
self.assertEqual(plan[0].command, [str(mysql), "-u", "root", "demo"])
|
||||
|
||||
def test_sql_dump_with_database_selection_does_not_require_existing_database(self):
|
||||
with TemporaryDirectory() as temp:
|
||||
root = Path(temp)
|
||||
database = root / "db"
|
||||
database.mkdir()
|
||||
sql_file = database / "movie.sql"
|
||||
sql_file.write_text(
|
||||
"CREATE DATABASE IF NOT EXISTS `movie`;\nUSE `movie`;\nCREATE TABLE demo(id int);",
|
||||
encoding="utf-8",
|
||||
)
|
||||
scan = ScanResult(
|
||||
root=root,
|
||||
backend=None,
|
||||
frontend=None,
|
||||
sql_files=[sql_file],
|
||||
)
|
||||
config = DeploymentConfig(mysql_database="movie", tool_search_roots=[root / "missing"])
|
||||
|
||||
plan = build_deployment_plan(scan, config)
|
||||
|
||||
self.assertEqual(plan[0].command, ["mysql", "-u", "root"])
|
||||
|
||||
def test_plan_skips_backend_build_when_runnable_jar_already_exists(self):
|
||||
with TemporaryDirectory() as temp:
|
||||
root = Path(temp)
|
||||
backend = root / "server"
|
||||
target = backend / "target"
|
||||
target.mkdir(parents=True)
|
||||
(backend / "pom.xml").write_text("<project />", encoding="utf-8")
|
||||
(target / "app.jar").write_text("jar", encoding="utf-8")
|
||||
scan = ScanResult(
|
||||
root=root,
|
||||
backend=ProjectComponent("Spring Boot", backend, "Maven project"),
|
||||
frontend=None,
|
||||
sql_files=[],
|
||||
)
|
||||
|
||||
plan = build_deployment_plan(scan, DeploymentConfig(output_dir=root / "deploy", tool_search_roots=[root / "missing"]))
|
||||
|
||||
self.assertEqual([step.kind for step in plan], ["backend_build_skip", "backend_start", "deployment_summary"])
|
||||
self.assertEqual(plan[0].command, [])
|
||||
self.assertIn("已有后端 jar", plan[0].description)
|
||||
|
||||
def test_existing_dist_does_not_add_frontend_build_or_copy_steps(self):
|
||||
with TemporaryDirectory() as temp:
|
||||
root = Path(temp)
|
||||
frontend = root / "web"
|
||||
dist = frontend / "dist"
|
||||
dist.mkdir(parents=True)
|
||||
(frontend / "package.json").write_text(
|
||||
'{"scripts":{"build":"vite build","dev":"vite"}}', encoding="utf-8"
|
||||
)
|
||||
(dist / "index.html").write_text("<main>built</main>", encoding="utf-8")
|
||||
scan = ScanResult(
|
||||
root=root,
|
||||
backend=None,
|
||||
frontend=ProjectComponent("Vue", frontend, "Vue project"),
|
||||
sql_files=[],
|
||||
)
|
||||
|
||||
plan = build_deployment_plan(scan, DeploymentConfig(output_dir=root / "deploy", tool_search_roots=[root / "missing"]))
|
||||
|
||||
self.assertEqual(
|
||||
[step.kind for step in plan],
|
||||
["frontend_install", "frontend_start", "deployment_summary"],
|
||||
)
|
||||
self.assertNotIn("frontend_build", [step.kind for step in plan])
|
||||
self.assertNotIn("frontend_copy", [step.kind for step in plan])
|
||||
|
||||
def test_run_steps_logs_skip_steps_without_running_a_process(self):
|
||||
with TemporaryDirectory() as temp:
|
||||
root = Path(temp)
|
||||
logs = []
|
||||
calls = []
|
||||
skip_step = DeploymentStep("跳过构建", [], root, "检测到已有产物,跳过构建", kind="skip")
|
||||
|
||||
def fake_runner(command, cwd, on_output, input_path=None, background=False):
|
||||
calls.append(command)
|
||||
return 0
|
||||
|
||||
exit_code = run_steps([skip_step], logs.append, process_runner=fake_runner)
|
||||
|
||||
self.assertEqual(exit_code, 0)
|
||||
self.assertEqual(calls, [])
|
||||
self.assertTrue(any("检测到已有产物" in line for line in logs))
|
||||
|
||||
def test_plan_uses_configured_npm_path_for_frontend_start(self):
|
||||
with TemporaryDirectory() as temp:
|
||||
root = Path(temp)
|
||||
frontend = root / "web"
|
||||
npm = root / "nodejs" / "npm.cmd"
|
||||
frontend.mkdir()
|
||||
(frontend / "node_modules").mkdir()
|
||||
npm.parent.mkdir(parents=True)
|
||||
(frontend / "package.json").write_text(
|
||||
'{"scripts":{"build":"vite build","dev":"vite"}}', encoding="utf-8"
|
||||
)
|
||||
npm.write_text("", encoding="utf-8")
|
||||
scan = ScanResult(
|
||||
root=root,
|
||||
backend=None,
|
||||
frontend=ProjectComponent("Vue", frontend, "Vue project"),
|
||||
sql_files=[],
|
||||
)
|
||||
config = DeploymentConfig(tool_paths={"npm": npm})
|
||||
|
||||
plan = build_deployment_plan(scan, config)
|
||||
|
||||
self.assertEqual(
|
||||
plan[0].command,
|
||||
[str(npm), "run", "dev", "--", "--host", "0.0.0.0", "--port", "8081"],
|
||||
)
|
||||
|
||||
def test_plan_installs_frontend_dependencies_before_start_when_node_modules_missing(self):
|
||||
with TemporaryDirectory() as temp:
|
||||
root = Path(temp)
|
||||
frontend = root / "web"
|
||||
frontend.mkdir()
|
||||
(frontend / "package.json").write_text(
|
||||
'{"scripts":{"build":"vue-cli-service build","serve":"vue-cli-service serve"},'
|
||||
'"devDependencies":{"@vue/cli-service":"^5.0.0"}}',
|
||||
encoding="utf-8",
|
||||
)
|
||||
scan = ScanResult(
|
||||
root=root,
|
||||
backend=None,
|
||||
frontend=ProjectComponent("Vue", frontend, "Vue project"),
|
||||
sql_files=[],
|
||||
)
|
||||
|
||||
plan = build_deployment_plan(scan, DeploymentConfig(output_dir=root / "deploy", tool_search_roots=[root / "missing"]))
|
||||
|
||||
npm = "npm.cmd" if os.name == "nt" else "npm"
|
||||
self.assertEqual(
|
||||
[step.kind for step in plan],
|
||||
["frontend_install", "frontend_start", "deployment_summary"],
|
||||
)
|
||||
self.assertEqual(plan[0].command, [npm, "install"])
|
||||
self.assertEqual(plan[1].command[1:3], ["run", "serve"])
|
||||
|
||||
def test_resolves_windows_cmd_shims_before_starting_process(self):
|
||||
with TemporaryDirectory() as temp:
|
||||
root = Path(temp)
|
||||
npm = root / "npm.cmd"
|
||||
npm.write_text("", encoding="utf-8")
|
||||
|
||||
command = resolve_process_command(["npm", "run", "build"], path=str(root))
|
||||
|
||||
self.assertEqual(os.path.normcase(command[0]), os.path.normcase(str(npm)))
|
||||
self.assertEqual(command[1:], ["run", "build"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user