整理 tjwater-cli 代码和文档
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
@@ -0,0 +1,306 @@
|
||||
from pathlib import Path
|
||||
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from tjwater_agent_cli import core
|
||||
from tjwater_agent_cli.main import app, main
|
||||
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
|
||||
class DummyResponse:
|
||||
def __init__(self, *, status_code=200, json_data=None, text="", headers=None, content=None):
|
||||
self.status_code = status_code
|
||||
self._json_data = json_data
|
||||
self.text = text
|
||||
self.headers = headers or {"content-type": "application/json"}
|
||||
self.content = content if content is not None else text.encode("utf-8")
|
||||
|
||||
@property
|
||||
def ok(self):
|
||||
return 200 <= self.status_code < 300
|
||||
|
||||
def json(self):
|
||||
if self._json_data is None:
|
||||
raise ValueError("no json")
|
||||
return self._json_data
|
||||
|
||||
|
||||
def test_load_auth_context_supports_aliases(tmp_path: Path):
|
||||
auth_path = tmp_path / "auth.json"
|
||||
auth_path.write_text(
|
||||
'{"base_url":"http://server","token":"abc","projectId":"p1","userId":"u1","username":"tester","projectCode":"net1"}',
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
auth = core.load_auth_context(auth_path)
|
||||
|
||||
assert auth.server == "http://server"
|
||||
assert auth.access_token == "abc"
|
||||
assert auth.project_id == "p1"
|
||||
assert auth.user_id == "u1"
|
||||
assert auth.username == "tester"
|
||||
assert auth.network == "net1"
|
||||
|
||||
|
||||
def test_build_runtime_context_uses_default_server(monkeypatch):
|
||||
monkeypatch.delenv("TJWATER_SERVER", raising=False)
|
||||
monkeypatch.delenv("TJWATER_ACCESS_TOKEN", raising=False)
|
||||
monkeypatch.delenv("TJWATER_PROJECT_ID", raising=False)
|
||||
monkeypatch.delenv("TJWATER_USER_ID", raising=False)
|
||||
monkeypatch.delenv("TJWATER_USERNAME", raising=False)
|
||||
monkeypatch.delenv("TJWATER_NETWORK", raising=False)
|
||||
monkeypatch.delenv("TJWATER_EXTRA_HEADERS", raising=False)
|
||||
|
||||
runtime = core.build_runtime_context(
|
||||
server=None,
|
||||
auth_context_path=None,
|
||||
scheme=None,
|
||||
timeout=core.DEFAULT_TIMEOUT,
|
||||
request_id="req-1",
|
||||
)
|
||||
|
||||
assert runtime.server == core.DEFAULT_SERVER
|
||||
|
||||
|
||||
def test_help_json_lists_commands():
|
||||
result = runner.invoke(app, ["help", "--json"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert '"schema_version": "tjwater-cli/v1"' in result.stdout
|
||||
assert '"command": "project"' in result.stdout
|
||||
assert '"command": "analysis"' in result.stdout
|
||||
assert '"menu_level": 1' in result.stdout
|
||||
assert '"command": "project list"' not in result.stdout
|
||||
|
||||
|
||||
def test_help_defaults_to_text():
|
||||
result = runner.invoke(app, ["help"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Commands:" in result.stdout
|
||||
assert "project: 项目与项目级元数据相关命令。" in result.stdout
|
||||
assert "analysis: 分析计算与诊断相关命令。" in result.stdout
|
||||
assert "Use `tjwater <menu> help` to see subcommands." in result.stdout
|
||||
assert "usage: tjwater project help" not in result.stdout
|
||||
assert "example: tjwater project help" not in result.stdout
|
||||
assert "project list: 列出当前用户可访问项目" not in result.stdout
|
||||
assert '"schema_version": "tjwater-cli/v1"' not in result.stdout
|
||||
|
||||
|
||||
def test_simulation_help_lists_subcommands():
|
||||
result = runner.invoke(app, ["simulation", "help"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "模拟运行与调度相关命令。" in result.stdout
|
||||
assert "simulation run: 触发指定绝对时间的模拟运行" in result.stdout
|
||||
assert "usage: tjwater simulation run --start-time <START_TIME> --duration <DURATION>" in result.stdout
|
||||
assert "example: tjwater --auth-context auth.json simulation run --start-time 2025-01-02T03:04:05+08:00 --duration 30" in result.stdout
|
||||
|
||||
|
||||
def test_nested_group_help_lists_examples():
|
||||
result = runner.invoke(app, ["analysis", "leakage", "help"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "漏损分析相关命令。" in result.stdout
|
||||
assert "analysis leakage identify: 执行漏损识别" in result.stdout
|
||||
assert "example: tjwater --auth-context auth.json analysis leakage identify" in result.stdout
|
||||
|
||||
|
||||
def test_analysis_help_uses_group_summaries_for_nested_groups():
|
||||
result = runner.invoke(app, ["analysis", "help"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "analysis leakage: 漏损分析相关命令。" in result.stdout
|
||||
assert "analysis burst-detection: 爆管检测相关命令。" in result.stdout
|
||||
assert "analysis burst-location" not in result.stdout
|
||||
assert "analysis risk" not in result.stdout
|
||||
assert "analysis leakage: 执行漏损识别" not in result.stdout
|
||||
assert "example: tjwater --auth-context auth.json analysis burst --start-time 2025-01-02T03:04:05+08:00 --duration 30 --burst-file ./burst.json --scheme burst_case_01" in result.stdout
|
||||
assert "example: tjwater --auth-context auth.json analysis valve --mode close --start-time 2025-01-02T03:04:05+08:00 --valve V1 --duration 900" in result.stdout
|
||||
|
||||
|
||||
def test_bare_analysis_uses_typer_help_with_descriptions():
|
||||
result = runner.invoke(app, ["analysis"])
|
||||
|
||||
assert result.exit_code == 2
|
||||
assert "分析计算与诊断相关命令。" in result.stdout
|
||||
assert "burst 执行爆管分析" in result.stdout
|
||||
assert "valve 执行阀门关闭或隔离分析" in result.stdout
|
||||
assert "leakage 漏损分析相关命令。" in result.stdout
|
||||
assert "burst-location" not in result.stdout
|
||||
assert "risk" not in result.stdout
|
||||
|
||||
|
||||
def test_leaf_help_shows_usage_and_example():
|
||||
result = runner.invoke(app, ["help", "simulation", "run"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Command: simulation run" in result.stdout
|
||||
assert "结果需后续通过 data timeseries 在对应时间段查询" in result.stdout
|
||||
assert "Usage: tjwater simulation run --start-time <START_TIME> --duration <DURATION>" in result.stdout
|
||||
assert "Examples:" in result.stdout
|
||||
assert "tjwater --auth-context auth.json simulation run --start-time 2025-01-02T03:04:05+08:00 --duration 30" in result.stdout
|
||||
|
||||
|
||||
def test_project_help_uses_legal_kind_example():
|
||||
result = runner.invoke(app, ["project", "help"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "example: tjwater --auth-context auth.json project data --kind scada-info" in result.stdout
|
||||
assert "--kind time" not in result.stdout
|
||||
|
||||
|
||||
def test_analysis_burst_returns_next_step_to_fetch_scheme(monkeypatch, tmp_path: Path):
|
||||
auth_path = tmp_path / "auth.json"
|
||||
auth_path.write_text(
|
||||
'{"server":"http://server","access_token":"abc","network":"demo"}',
|
||||
encoding="utf-8",
|
||||
)
|
||||
burst_path = tmp_path / "burst.json"
|
||||
burst_path.write_text('[{"id":"P1","size":3.5}]', encoding="utf-8")
|
||||
|
||||
def fake_request(**kwargs):
|
||||
return DummyResponse(text="success", headers={"content-type": "text/plain"})
|
||||
|
||||
monkeypatch.setattr(core.requests, "request", fake_request)
|
||||
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"--auth-context",
|
||||
str(auth_path),
|
||||
"analysis",
|
||||
"burst",
|
||||
"--start-time",
|
||||
"2025-01-02T03:04:05+08:00",
|
||||
"--duration",
|
||||
"30",
|
||||
"--burst-file",
|
||||
str(burst_path),
|
||||
"--scheme",
|
||||
"burst_case_01",
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert '"summary": "爆管分析执行成功"' in result.stdout
|
||||
assert '"tjwater --auth-context auth.json data scheme get --name burst_case_01"' in result.stdout
|
||||
assert '"tjwater --auth-context auth.json data scheme list"' in result.stdout
|
||||
|
||||
|
||||
def test_main_missing_option_error_includes_usage_and_next_step(capsys):
|
||||
exit_code = main(["simulation", "run"])
|
||||
stdout = capsys.readouterr().out
|
||||
|
||||
assert exit_code == 2
|
||||
assert '"summary": "缺少参数"' in stdout
|
||||
assert '"code": "MISSING_PARAMETER"' in stdout
|
||||
assert '"usage": "tjwater simulation run --start-time <START_TIME> --duration <DURATION>"' in stdout
|
||||
assert '"tjwater help simulation run"' in stdout
|
||||
|
||||
|
||||
def test_main_bare_analysis_returns_typer_help_without_json_error(capsys):
|
||||
exit_code = main(["analysis"])
|
||||
stdout = capsys.readouterr().out
|
||||
|
||||
assert exit_code == 0
|
||||
assert "Usage: tjwater analysis" in stdout
|
||||
assert "分析计算与诊断相关命令。" in stdout
|
||||
assert '"ok": false' not in stdout
|
||||
|
||||
|
||||
def test_project_list_uses_auth_headers(monkeypatch, tmp_path: Path):
|
||||
auth_path = tmp_path / "auth.json"
|
||||
auth_path.write_text(
|
||||
'{"server":"http://server","access_token":"abc","project_id":"pid","network":"demo"}',
|
||||
encoding="utf-8",
|
||||
)
|
||||
captured = {}
|
||||
|
||||
def fake_request(**kwargs):
|
||||
captured.update(kwargs)
|
||||
return DummyResponse(json_data=[{"project_id": "pid", "name": "Demo"}])
|
||||
|
||||
monkeypatch.setattr(core.requests, "request", fake_request)
|
||||
|
||||
result = runner.invoke(app, ["--auth-context", str(auth_path), "project", "list"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert '"ok": true' in result.stdout
|
||||
assert captured["headers"]["Authorization"] == "Bearer abc"
|
||||
assert captured["url"] == "http://server/api/v1/meta/projects"
|
||||
|
||||
|
||||
def test_simulation_run_translates_rfc3339(monkeypatch, tmp_path: Path):
|
||||
auth_path = tmp_path / "auth.json"
|
||||
auth_path.write_text(
|
||||
'{"server":"http://server","access_token":"abc","network":"demo"}',
|
||||
encoding="utf-8",
|
||||
)
|
||||
captured = {}
|
||||
|
||||
def fake_request(**kwargs):
|
||||
captured.update(kwargs)
|
||||
return DummyResponse(json_data={"status": "success", "message": "Simulation started"})
|
||||
|
||||
monkeypatch.setattr(core.requests, "request", fake_request)
|
||||
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"--auth-context",
|
||||
str(auth_path),
|
||||
"simulation",
|
||||
"run",
|
||||
"--start-time",
|
||||
"2025-01-02T03:04:05+08:00",
|
||||
"--duration",
|
||||
"30",
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert captured["json"] == {
|
||||
"name": "demo",
|
||||
"simulation_date": "2025-01-02",
|
||||
"start_time": "03:04:05+08:00",
|
||||
"duration": 30,
|
||||
}
|
||||
assert '"tjwater --auth-context auth.json data timeseries realtime links --start-time 2025-01-02T03:04:05+08:00 --end-time 2025-01-02T03:34:05+08:00"' in result.stdout
|
||||
assert '"tjwater --auth-context auth.json data timeseries realtime nodes --start-time 2025-01-02T03:04:05+08:00 --end-time 2025-01-02T03:34:05+08:00"' in result.stdout
|
||||
|
||||
|
||||
def test_project_export_inp_downloads_file(monkeypatch, tmp_path: Path):
|
||||
auth_path = tmp_path / "auth.json"
|
||||
auth_path.write_text(
|
||||
'{"server":"http://server","access_token":"abc","network":"demo"}',
|
||||
encoding="utf-8",
|
||||
)
|
||||
output = tmp_path / "demo.inp"
|
||||
calls = []
|
||||
|
||||
def fake_request(**kwargs):
|
||||
calls.append(kwargs["url"])
|
||||
if kwargs["url"].endswith("/dumpinp/"):
|
||||
return DummyResponse(json_data=True)
|
||||
return DummyResponse(
|
||||
headers={"content-type": "application/octet-stream"},
|
||||
content=b"inp-content",
|
||||
text="inp-content",
|
||||
)
|
||||
|
||||
monkeypatch.setattr(core.requests, "request", fake_request)
|
||||
|
||||
result = runner.invoke(
|
||||
app,
|
||||
["--auth-context", str(auth_path), "project", "export-inp", "--output", str(output)],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert output.read_bytes() == b"inp-content"
|
||||
assert calls == [
|
||||
"http://server/api/v1/dumpinp/",
|
||||
"http://server/api/v1/downloadinp/",
|
||||
]
|
||||
Reference in New Issue
Block a user