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()
|
||||
103
tests/test_environment.py
Normal file
103
tests/test_environment.py
Normal file
@@ -0,0 +1,103 @@
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from deploy_tool.environment import check_environment, load_tool_paths, save_tool_paths
|
||||
|
||||
|
||||
class EnvironmentTests(unittest.TestCase):
|
||||
def test_reports_installed_command(self):
|
||||
def runner(command):
|
||||
return subprocess.CompletedProcess(command, 0, "java 21\n", "")
|
||||
|
||||
results = check_environment(runner=runner, checks={"JDK": ["java", "-version"]})
|
||||
|
||||
self.assertTrue(results[0].installed)
|
||||
self.assertEqual(results[0].name, "JDK")
|
||||
self.assertEqual(results[0].source, "PATH")
|
||||
self.assertIn("java 21", results[0].version)
|
||||
|
||||
def test_reports_missing_command(self):
|
||||
def runner(command):
|
||||
raise FileNotFoundError(command[0])
|
||||
|
||||
results = check_environment(runner=runner, checks={"MySQL": ["mysql", "--version"]})
|
||||
|
||||
self.assertFalse(results[0].installed)
|
||||
self.assertEqual(results[0].name, "MySQL")
|
||||
self.assertIn("未安装", results[0].message)
|
||||
|
||||
def test_uses_manual_path_when_command_is_not_in_path(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
mysql = Path(tmp) / "mysql.exe"
|
||||
mysql.write_text("", encoding="utf-8")
|
||||
|
||||
def runner(command):
|
||||
if command[0] == "mysql":
|
||||
raise FileNotFoundError(command[0])
|
||||
return subprocess.CompletedProcess(command, 0, "mysql 8.4\n", "")
|
||||
|
||||
results = check_environment(
|
||||
runner=runner,
|
||||
checks={"MySQL": ["mysql", "--version"]},
|
||||
manual_paths={"MySQL": mysql},
|
||||
)
|
||||
|
||||
self.assertTrue(results[0].installed)
|
||||
self.assertEqual(results[0].source, "手动指定")
|
||||
self.assertEqual(results[0].command[0], str(mysql))
|
||||
|
||||
def test_discovers_executable_from_known_search_paths(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
mysql = Path(tmp) / "MySQL" / "MySQL Server 8.4" / "bin" / "mysql.exe"
|
||||
mysql.parent.mkdir(parents=True)
|
||||
mysql.write_text("", encoding="utf-8")
|
||||
|
||||
def runner(command):
|
||||
if command[0] == "mysql":
|
||||
raise FileNotFoundError(command[0])
|
||||
return subprocess.CompletedProcess(command, 0, "mysql 8.4\n", "")
|
||||
|
||||
results = check_environment(
|
||||
runner=runner,
|
||||
checks={"MySQL": ["mysql", "--version"]},
|
||||
search_roots=[Path(tmp)],
|
||||
)
|
||||
|
||||
self.assertTrue(results[0].installed)
|
||||
self.assertEqual(results[0].source, "自动发现")
|
||||
self.assertEqual(results[0].command[0], str(mysql))
|
||||
|
||||
def test_does_not_recursively_scan_arbitrary_deep_directories(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
mysql = Path(tmp) / "random" / "deep" / "folder" / "mysql.exe"
|
||||
mysql.parent.mkdir(parents=True)
|
||||
mysql.write_text("", encoding="utf-8")
|
||||
|
||||
def runner(command):
|
||||
if command[0] == "mysql":
|
||||
raise FileNotFoundError(command[0])
|
||||
return subprocess.CompletedProcess(command, 0, "mysql 8.4\n", "")
|
||||
|
||||
results = check_environment(
|
||||
runner=runner,
|
||||
checks={"MySQL": ["mysql", "--version"]},
|
||||
search_roots=[Path(tmp)],
|
||||
)
|
||||
|
||||
self.assertFalse(results[0].installed)
|
||||
|
||||
def test_saves_and_loads_manual_paths(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
config = Path(tmp) / "tool-paths.json"
|
||||
mysql = Path(tmp) / "mysql.exe"
|
||||
|
||||
save_tool_paths({"MySQL": mysql}, config)
|
||||
loaded = load_tool_paths(config)
|
||||
|
||||
self.assertEqual(loaded, {"MySQL": mysql})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
163
tests/test_gui_navigation.py
Normal file
163
tests/test_gui_navigation.py
Normal file
@@ -0,0 +1,163 @@
|
||||
import unittest
|
||||
import queue
|
||||
from unittest.mock import patch
|
||||
|
||||
from deploy_tool.gui import DeployWorkbench, database_name_from_sql_files
|
||||
import deploy_tool.gui as gui_module
|
||||
|
||||
|
||||
class FakeWidget:
|
||||
def __init__(self):
|
||||
self.focused = False
|
||||
self.seen = False
|
||||
|
||||
def focus_set(self):
|
||||
self.focused = True
|
||||
|
||||
def see(self, index):
|
||||
self.seen = index
|
||||
|
||||
|
||||
class FakeButton:
|
||||
def __init__(self):
|
||||
self.state = None
|
||||
|
||||
def configure(self, **options):
|
||||
self.state = options.get("state")
|
||||
|
||||
|
||||
class GuiNavigationTests(unittest.TestCase):
|
||||
def test_database_name_comes_from_first_sql_filename(self):
|
||||
from pathlib import Path
|
||||
|
||||
self.assertEqual(database_name_from_sql_files([Path("db/movie.sql")]), "movie")
|
||||
self.assertEqual(database_name_from_sql_files([]), "")
|
||||
|
||||
def test_deployment_running_state_updates_deploy_button(self):
|
||||
app = DeployWorkbench.__new__(DeployWorkbench)
|
||||
app.deployment_running = False
|
||||
app.deploy_buttons = [FakeButton()]
|
||||
set_running = getattr(DeployWorkbench, "_set_deployment_running", lambda self, running: None)
|
||||
|
||||
set_running(app, True)
|
||||
|
||||
self.assertTrue(app.deployment_running)
|
||||
self.assertEqual([button.state for button in app.deploy_buttons], ["disabled"])
|
||||
|
||||
set_running(app, False)
|
||||
|
||||
self.assertFalse(app.deployment_running)
|
||||
self.assertEqual([button.state for button in app.deploy_buttons], ["normal"])
|
||||
|
||||
def test_running_deployment_ignores_another_deploy_request(self):
|
||||
app = DeployWorkbench.__new__(DeployWorkbench)
|
||||
app.deployment_running = True
|
||||
plan_calls = []
|
||||
logs = []
|
||||
app._current_plan = lambda: plan_calls.append("planned") or []
|
||||
app._append_log = logs.append
|
||||
|
||||
DeployWorkbench.run_deploy_plan_async(app)
|
||||
|
||||
self.assertEqual(plan_calls, [])
|
||||
self.assertTrue(any("正在执行" in line for line in logs))
|
||||
|
||||
def test_confirmed_deployment_locks_buttons_before_worker_starts(self):
|
||||
app = DeployWorkbench.__new__(DeployWorkbench)
|
||||
app.deployment_running = False
|
||||
app.deploy_buttons = [FakeButton()]
|
||||
app._current_plan = lambda: [object()]
|
||||
app.ui_queue = object()
|
||||
app._thread_log = lambda value: None
|
||||
|
||||
with patch.object(gui_module.messagebox, "askyesno", return_value=True), patch.object(gui_module.threading, "Thread") as thread_class:
|
||||
DeployWorkbench.run_deploy_plan_async(app)
|
||||
|
||||
self.assertTrue(app.deployment_running)
|
||||
self.assertEqual([button.state for button in app.deploy_buttons], ["disabled"])
|
||||
thread_class.return_value.start.assert_called_once_with()
|
||||
|
||||
def test_thread_start_failure_restores_deploy_buttons(self):
|
||||
app = DeployWorkbench.__new__(DeployWorkbench)
|
||||
app.deployment_running = False
|
||||
app.deploy_buttons = [FakeButton()]
|
||||
app._current_plan = lambda: [object()]
|
||||
logs = []
|
||||
app._append_log = logs.append
|
||||
|
||||
with patch.object(gui_module.messagebox, "askyesno", return_value=True), patch.object(gui_module.threading, "Thread") as thread_class:
|
||||
thread_class.return_value.start.side_effect = RuntimeError("thread unavailable")
|
||||
try:
|
||||
DeployWorkbench.run_deploy_plan_async(app)
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
self.assertFalse(app.deployment_running)
|
||||
self.assertEqual([button.state for button in app.deploy_buttons], ["normal"])
|
||||
self.assertTrue(any("thread unavailable" in line for line in logs))
|
||||
|
||||
def test_worker_exception_always_queues_deploy_done(self):
|
||||
app = DeployWorkbench.__new__(DeployWorkbench)
|
||||
app.ui_queue = queue.Queue()
|
||||
run_worker = getattr(DeployWorkbench, "_run_deployment_worker", lambda self, plan: None)
|
||||
|
||||
with patch.object(gui_module, "run_steps", side_effect=RuntimeError("unexpected failure")):
|
||||
run_worker(app, [object()])
|
||||
|
||||
events = []
|
||||
while not app.ui_queue.empty():
|
||||
events.append(app.ui_queue.get_nowait())
|
||||
|
||||
self.assertIn(("deploy_done", 1), events)
|
||||
self.assertTrue(any(event == "log" and "unexpected failure" in str(payload) for event, payload in events))
|
||||
|
||||
def test_deploy_done_event_restores_deploy_buttons(self):
|
||||
app = DeployWorkbench.__new__(DeployWorkbench)
|
||||
app.deployment_running = True
|
||||
app.deploy_buttons = [FakeButton()]
|
||||
app.ui_queue = queue.Queue()
|
||||
app.ui_queue.put(("deploy_done", 1))
|
||||
logs = []
|
||||
app._append_log = logs.append
|
||||
app.after = lambda *args, **kwargs: None
|
||||
|
||||
DeployWorkbench._drain_queue(app)
|
||||
|
||||
self.assertFalse(app.deployment_running)
|
||||
self.assertEqual([button.state for button in app.deploy_buttons], ["normal"])
|
||||
self.assertTrue(any("退出码: 1" in line for line in logs))
|
||||
|
||||
def test_top_action_only_exposes_execution(self):
|
||||
self.assertNotIn("plan", gui_module.TEXT)
|
||||
self.assertEqual(gui_module.TEXT["deploy"], "执行部署")
|
||||
|
||||
def test_sidebar_navigation_focuses_logs(self):
|
||||
app = DeployWorkbench.__new__(DeployWorkbench)
|
||||
app.log_text = FakeWidget()
|
||||
app.result_text = FakeWidget()
|
||||
|
||||
app.activate_sidebar_item("运行日志")
|
||||
|
||||
self.assertTrue(app.log_text.focused)
|
||||
self.assertEqual(app.log_text.seen, "end")
|
||||
|
||||
def test_sidebar_navigation_focuses_database_config(self):
|
||||
app = DeployWorkbench.__new__(DeployWorkbench)
|
||||
app.database_widget = FakeWidget()
|
||||
|
||||
app.activate_sidebar_item("数据库")
|
||||
|
||||
self.assertTrue(app.database_widget.focused)
|
||||
|
||||
def test_sidebar_navigation_can_trigger_environment_check(self):
|
||||
app = DeployWorkbench.__new__(DeployWorkbench)
|
||||
calls = []
|
||||
app.check_environment_async = lambda: calls.append("checked")
|
||||
|
||||
app.activate_sidebar_item("环境检测")
|
||||
|
||||
self.assertEqual(calls, ["checked"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
100
tests/test_installer.py
Normal file
100
tests/test_installer.py
Normal file
@@ -0,0 +1,100 @@
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from deploy_tool.installer import build_install_command, explain_install_failure, format_install_output, recommend_jdk_version
|
||||
from deploy_tool.scanner import scan_project
|
||||
|
||||
|
||||
class InstallerTests(unittest.TestCase):
|
||||
def test_recommends_jdk_8_from_maven_source_property(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
backend = root / "server"
|
||||
(backend / "src" / "main" / "java").mkdir(parents=True)
|
||||
(backend / "pom.xml").write_text(
|
||||
"""
|
||||
<project>
|
||||
<properties>
|
||||
<maven.compiler.source>1.8</maven.compiler.source>
|
||||
<maven.compiler.target>1.8</maven.compiler.target>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>spring-boot-starter-web</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
scan = scan_project(root)
|
||||
|
||||
recommendation = recommend_jdk_version(scan)
|
||||
self.assertEqual(recommendation.version, "8")
|
||||
self.assertIn("maven.compiler.source", recommendation.reason)
|
||||
|
||||
def test_recommends_jdk_17_for_spring_boot_3(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
backend = root / "server"
|
||||
(backend / "src" / "main" / "java").mkdir(parents=True)
|
||||
(backend / "pom.xml").write_text(
|
||||
"""
|
||||
<project>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.2.5</version>
|
||||
</parent>
|
||||
</project>
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
scan = scan_project(root)
|
||||
|
||||
recommendation = recommend_jdk_version(scan)
|
||||
self.assertEqual(recommendation.version, "17")
|
||||
self.assertIn("Spring Boot 3", recommendation.reason)
|
||||
|
||||
def test_defaults_to_jdk_8_without_project_signal(self):
|
||||
recommendation = recommend_jdk_version(None)
|
||||
|
||||
self.assertEqual(recommendation.version, "8")
|
||||
self.assertIn("默认", recommendation.reason)
|
||||
|
||||
def test_builds_jdk_winget_install_command(self):
|
||||
command = build_install_command("JDK", version="8")
|
||||
|
||||
self.assertEqual(command, ["winget", "install", "-e", "--id", "EclipseAdoptium.Temurin.8.JDK"])
|
||||
|
||||
def test_builds_node_winget_install_command(self):
|
||||
command = build_install_command("Node.js")
|
||||
|
||||
self.assertEqual(command, ["winget", "install", "-e", "--id", "OpenJS.NodeJS.LTS"])
|
||||
|
||||
def test_formats_winget_progress_without_garbled_bar(self):
|
||||
raw = "████████████▒▒▒▒▒▒ 63%\r".encode("utf-8")
|
||||
|
||||
self.assertEqual(format_install_output(raw), ["安装进度: 63%"])
|
||||
|
||||
def test_keeps_readable_winget_text(self):
|
||||
raw = "已找到 Eclipse Temurin JDK [EclipseAdoptium.Temurin.8.JDK]\n".encode("utf-8")
|
||||
|
||||
self.assertEqual(format_install_output(raw), ["已找到 Eclipse Temurin JDK [EclipseAdoptium.Temurin.8.JDK]"])
|
||||
|
||||
|
||||
def test_explains_missing_maven_winget_package(self):
|
||||
message = explain_install_failure(
|
||||
"Maven",
|
||||
2316632084,
|
||||
["找不到与输入条件匹配的程序包。"],
|
||||
)
|
||||
|
||||
self.assertIn("winget 当前源找不到 Maven 包", message)
|
||||
self.assertIn("手动下载 Apache Maven", message)
|
||||
self.assertIn("mvn.cmd", message)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
48
tests/test_path_manager.py
Normal file
48
tests/test_path_manager.py
Normal file
@@ -0,0 +1,48 @@
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from deploy_tool.path_manager import EnvironmentUpdate, build_environment_update, merge_path_entries, apply_user_environment
|
||||
|
||||
|
||||
class FakeRegistry:
|
||||
def __init__(self):
|
||||
self.values = {}
|
||||
self.broadcasted = False
|
||||
|
||||
def get_value(self, name):
|
||||
return self.values.get(name, "")
|
||||
|
||||
def set_value(self, name, value):
|
||||
self.values[name] = value
|
||||
|
||||
def broadcast_change(self):
|
||||
self.broadcasted = True
|
||||
|
||||
|
||||
class PathManagerTests(unittest.TestCase):
|
||||
def test_merge_path_entries_appends_without_duplicates(self):
|
||||
current = r"C:\Windows;C:\Java\bin"
|
||||
merged = merge_path_entries(current, [Path(r"C:\Java\bin"), Path(r"C:\mysql\bin")])
|
||||
|
||||
self.assertEqual(merged, r"C:\Windows;C:\Java\bin;C:\mysql\bin")
|
||||
|
||||
def test_build_environment_update_sets_java_home_from_bin(self):
|
||||
update = build_environment_update("JDK", Path(r"C:\Program Files\Eclipse Adoptium\jdk-8\bin\java.exe"))
|
||||
|
||||
self.assertEqual(update.path_entries, [Path(r"C:\Program Files\Eclipse Adoptium\jdk-8\bin")])
|
||||
self.assertEqual(update.env_vars["JAVA_HOME"], r"C:\Program Files\Eclipse Adoptium\jdk-8")
|
||||
|
||||
def test_apply_user_environment_writes_path_and_env_vars(self):
|
||||
registry = FakeRegistry()
|
||||
registry.values["Path"] = r"C:\Windows"
|
||||
update = EnvironmentUpdate(path_entries=[Path(r"C:\Tools\bin")], env_vars={"JAVA_HOME": r"C:\Java"})
|
||||
|
||||
apply_user_environment(update, registry)
|
||||
|
||||
self.assertEqual(registry.values["Path"], r"C:\Windows;C:\Tools\bin")
|
||||
self.assertEqual(registry.values["JAVA_HOME"], r"C:\Java")
|
||||
self.assertTrue(registry.broadcasted)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
112
tests/test_scanner.py
Normal file
112
tests/test_scanner.py
Normal file
@@ -0,0 +1,112 @@
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from deploy_tool.scanner import scan_project
|
||||
|
||||
|
||||
class ScannerTests(unittest.TestCase):
|
||||
def test_prunes_generated_and_dependency_directories(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
frontend = root / "web"
|
||||
frontend.mkdir()
|
||||
(frontend / "package.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"dependencies": {"vue": "^3.4.0"},
|
||||
"scripts": {"dev": "vite", "build": "vite build"},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
ignored_package = frontend / "node_modules" / "fake" / "package.json"
|
||||
ignored_package.parent.mkdir(parents=True)
|
||||
ignored_package.write_text(
|
||||
json.dumps({"dependencies": {"vue": "^3.4.0"}, "scripts": {"dev": "vite"}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
ignored_sql = frontend / "dist" / "bundled.sql"
|
||||
ignored_sql.parent.mkdir()
|
||||
ignored_sql.write_text("select 1;", encoding="utf-8")
|
||||
|
||||
result = scan_project(root)
|
||||
|
||||
self.assertEqual([item.path for item in result.detected_frontends], [frontend])
|
||||
self.assertEqual(result.sql_files, [])
|
||||
|
||||
def test_detects_user_and_admin_frontends_with_ports_and_backend_context(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
backend = root / "server"
|
||||
client = root / "前端" / "client_code"
|
||||
admin = root / "前端" / "manage_code"
|
||||
|
||||
(backend / "src" / "main" / "java").mkdir(parents=True)
|
||||
resources = backend / "src" / "main" / "resources"
|
||||
resources.mkdir(parents=True)
|
||||
(backend / "pom.xml").write_text("<project>spring-boot</project>", encoding="utf-8")
|
||||
(resources / "application.yml").write_text(
|
||||
"server:\n servlet:\n context-path: /movies\n", encoding="utf-8"
|
||||
)
|
||||
|
||||
for frontend, port in ((client, 8082), (admin, 8081)):
|
||||
frontend.mkdir(parents=True)
|
||||
(frontend / "package.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"dependencies": {"vue": "^3.4.0"},
|
||||
"scripts": {"build": "vue-cli-service build", "serve": "vue-cli-service serve"},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(frontend / "vue.config.js").write_text(
|
||||
f"module.exports = {{ devServer: {{ port: {port} }} }}", encoding="utf-8"
|
||||
)
|
||||
|
||||
result = scan_project(root)
|
||||
|
||||
self.assertEqual([item.path for item in result.detected_frontends], [client, admin])
|
||||
self.assertEqual([item.metadata["role"] for item in result.detected_frontends], ["user", "admin"])
|
||||
self.assertEqual([item.metadata["port"] for item in result.detected_frontends], ["8082", "8081"])
|
||||
self.assertEqual(result.backend.metadata["context_path"], "/movies")
|
||||
|
||||
def test_detects_spring_boot_vue_and_sql(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
backend = root / "server"
|
||||
frontend = root / "web"
|
||||
sql_dir = root / "db"
|
||||
|
||||
(backend / "src" / "main" / "java").mkdir(parents=True)
|
||||
(backend / "pom.xml").write_text(
|
||||
"<project><dependencies><dependency>spring-boot-starter-web</dependency></dependencies></project>",
|
||||
encoding="utf-8",
|
||||
)
|
||||
frontend.mkdir()
|
||||
(frontend / "package.json").write_text(
|
||||
json.dumps({"dependencies": {"vue": "^3.4.0"}, "scripts": {"build": "vite build"}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
sql_dir.mkdir()
|
||||
(sql_dir / "schema.sql").write_text("create table demo(id int);", encoding="utf-8")
|
||||
|
||||
result = scan_project(root)
|
||||
|
||||
self.assertEqual(result.backend.path, backend)
|
||||
self.assertEqual(result.frontend.path, frontend)
|
||||
self.assertEqual(result.sql_files, [sql_dir / "schema.sql"])
|
||||
|
||||
def test_empty_folder_reports_missing_parts(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
result = scan_project(Path(tmp))
|
||||
|
||||
self.assertIsNone(result.backend)
|
||||
self.assertIsNone(result.frontend)
|
||||
self.assertEqual(result.sql_files, [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
37
tests/test_service_console.py
Normal file
37
tests/test_service_console.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from deploy_tool.service_console import main
|
||||
|
||||
|
||||
class ServiceConsoleTests(unittest.TestCase):
|
||||
def test_tees_service_output_to_log_and_removes_pid_file(self):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
root = Path(temp)
|
||||
log_path = root / "service.log"
|
||||
pid_path = root / "service.pid"
|
||||
argv = [
|
||||
"service_console.py",
|
||||
"--log",
|
||||
str(log_path),
|
||||
"--pid-file",
|
||||
str(pid_path),
|
||||
"--",
|
||||
sys.executable,
|
||||
"-c",
|
||||
"print('service-ready')",
|
||||
]
|
||||
|
||||
with patch.object(sys, "argv", argv):
|
||||
exit_code = main()
|
||||
|
||||
self.assertEqual(exit_code, 0)
|
||||
self.assertIn("service-ready", log_path.read_text(encoding="utf-8"))
|
||||
self.assertFalse(pid_path.exists())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user