refactor: 本仓升为唯一源(原 sap-cli 源码仓归档)
方向反转:此前 SKILL.md 是「模板渲染产物」、sap-cli 是源;现 sap-cli 归档, sap-cli-skill 承接开发与分发,SKILL.md 回归手工维护的正本。 迁移(来自 sap-cli,共 104 文件): - tests/ 692 例测试(15 个文件的内联 sys.path 改指 assets/) - openspec/ SDD 规格与归档变更(42 文件) - docs/ 开发文档与 ADT 原理(含 dev/CLAUDE.md、AGENTS.md) - .claude/ rules 副本 + settings.json(供 Claude Code) - .github/ .hermes/ .pre-commit-config.yaml .editorconfig CLAUDE.md - scripts/ 保持仅 setup.py(pack_skill.py 已随旧仓归档,不迁) 修复(迁移暴露的真实缺陷): - assets/pyproject.toml 的 build-backend 写作 `setuptools.backends._legacy:_Backend`, 该模块在 setuptools 中不存在 → `pip install -e` 从来装不上。改为 build_meta。 实测:临时 venv 安装成功,sap-cli --help 正常列出 31 个命令 - pyproject readme 指向不存在的 assets/README.md(editable 安装会失败)→ 改内联文本 - pyproject urls 改指 sap-cli-skill 机制调整: - .github/workflows/ci.yml 适配 assets/ 布局;顶部注明该工作流仅 GitHub 执行, 本仓在 Gitee 不会自动跑 - pre-commit 增本地测试门禁(Gitee 上真正生效的那道) - .gitignore 合并旧仓完整规则(保留 log/ 下 md 知识库入库,只忽略运行日志) - 大文件上限 100KB→1MB(架构图 512KB) 守卫测试 tests/unit/test_repo_guards.py(10 → 18 例): - SKILL.md 须记录 parser 全部 CLI 命令 / 铁律 1-5 须为真实小节标题 / 示例不得违反铁律 5 - references/ 规则齐备;.claude/rules 与 references 必须一致(实测抓到一次真实漂移) - VERSION == sapcli.__version__ == README 版本 - 仓内不得再出现 pack_skill.py / skill-src(防废弃流程回潮) 698 tests OK;editable 安装与 CLI 入口经临时 venv 实测通过。 docs/RELEASING.md 重写为单源开发流程。
This commit is contained in:
@@ -0,0 +1,384 @@
|
||||
"""CLI 层(parser.py + app.py)单元测试。
|
||||
|
||||
运行: python tests/unit/test_cli.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "assets"))
|
||||
|
||||
from sapcli.cli.parser import build_parser
|
||||
from sapcli.types import all_type_keys
|
||||
|
||||
|
||||
class TestParserBuild(unittest.TestCase):
|
||||
"""test_parser_build: build_parser() 返回 ArgumentParser"""
|
||||
|
||||
def test_returns_argument_parser(self):
|
||||
import argparse
|
||||
parser = build_parser()
|
||||
self.assertIsInstance(parser, argparse.ArgumentParser)
|
||||
|
||||
def test_parser_has_description(self):
|
||||
parser = build_parser()
|
||||
self.assertIn("sap-cli", parser.description)
|
||||
|
||||
|
||||
class TestParserDownload(unittest.TestCase):
|
||||
"""test_parser_download: 解析 download 命令"""
|
||||
|
||||
def test_download_full_args(self):
|
||||
parser = build_parser()
|
||||
args = parser.parse_args([
|
||||
"download", "--name", "ZTEST", "--type", "report", "--path", "./out",
|
||||
])
|
||||
self.assertEqual(args.command, "download")
|
||||
self.assertEqual(args.name, "ZTEST")
|
||||
self.assertEqual(args.type, "report")
|
||||
self.assertEqual(args.path, "./out")
|
||||
|
||||
def test_download_class_type(self):
|
||||
parser = build_parser()
|
||||
args = parser.parse_args([
|
||||
"download", "--name", "ZCL_TEST", "--type", "class", "--path", "./out",
|
||||
])
|
||||
self.assertEqual(args.command, "download")
|
||||
self.assertEqual(args.type, "class")
|
||||
|
||||
|
||||
class TestParserSyncSingle(unittest.TestCase):
|
||||
"""test_parser_sync_single: 解析 sync 单对象模式"""
|
||||
|
||||
def test_sync_single(self):
|
||||
parser = build_parser()
|
||||
args = parser.parse_args([
|
||||
"sync", "--name", "ZTEST", "--type", "report", "--path", "./file.abap",
|
||||
])
|
||||
self.assertEqual(args.command, "sync")
|
||||
self.assertEqual(args.name, "ZTEST")
|
||||
self.assertEqual(args.type, "report")
|
||||
self.assertEqual(args.path, "./file.abap")
|
||||
self.assertFalse(getattr(args, "all", False))
|
||||
|
||||
|
||||
class TestParserSyncAll(unittest.TestCase):
|
||||
"""test_parser_sync_all: 解析 sync --all 批量模式"""
|
||||
|
||||
def test_sync_all(self):
|
||||
parser = build_parser()
|
||||
args = parser.parse_args([
|
||||
"sync", "--all", "--path", "./project",
|
||||
])
|
||||
self.assertEqual(args.command, "sync")
|
||||
self.assertTrue(args.all)
|
||||
self.assertEqual(args.path, "./project")
|
||||
self.assertIsNone(args.name)
|
||||
self.assertIsNone(args.type)
|
||||
|
||||
def test_sync_all_dry_run(self):
|
||||
parser = build_parser()
|
||||
args = parser.parse_args([
|
||||
"sync", "--all", "--path", "./project", "--dry-run",
|
||||
])
|
||||
self.assertTrue(args.all)
|
||||
self.assertTrue(args.dry_run)
|
||||
|
||||
|
||||
class TestParserCreate(unittest.TestCase):
|
||||
"""test_parser_create: 解析 create 命令"""
|
||||
|
||||
def test_create_full_args(self):
|
||||
parser = build_parser()
|
||||
args = parser.parse_args([
|
||||
"create", "--name", "ZTEST", "--type", "report",
|
||||
"--description", "test", "--package", "$TMP",
|
||||
])
|
||||
self.assertEqual(args.command, "create")
|
||||
self.assertEqual(args.name, "ZTEST")
|
||||
self.assertEqual(args.type, "report")
|
||||
self.assertEqual(args.description, "test")
|
||||
self.assertEqual(args.package, "$TMP")
|
||||
|
||||
def test_create_default_package(self):
|
||||
parser = build_parser()
|
||||
args = parser.parse_args([
|
||||
"create", "--name", "ZTEST", "--type", "report",
|
||||
])
|
||||
self.assertEqual(args.package, "$TMP")
|
||||
self.assertIsNone(args.description)
|
||||
|
||||
|
||||
class TestParserNoCommand(unittest.TestCase):
|
||||
"""test_parser_no_command: 无命令时 args.command 为 None"""
|
||||
|
||||
def test_no_command(self):
|
||||
parser = build_parser()
|
||||
args = parser.parse_args([])
|
||||
self.assertIsNone(args.command)
|
||||
|
||||
|
||||
class TestParserConfig(unittest.TestCase):
|
||||
"""test_parser_config: 解析 config 子命令"""
|
||||
|
||||
def test_config_show(self):
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(["config", "show"])
|
||||
self.assertEqual(args.command, "config")
|
||||
self.assertEqual(args.config_action, "show")
|
||||
|
||||
def test_config_set(self):
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(["config", "set", "host", "http://sap:8000"])
|
||||
self.assertEqual(args.command, "config")
|
||||
self.assertEqual(args.config_action, "set")
|
||||
self.assertEqual(args.key, "host")
|
||||
self.assertEqual(args.value, "http://sap:8000")
|
||||
|
||||
|
||||
class TestParserAuth(unittest.TestCase):
|
||||
"""test_parser_auth: 解析 auth 子命令"""
|
||||
|
||||
def test_auth_login(self):
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(["auth", "login"])
|
||||
self.assertEqual(args.command, "auth")
|
||||
self.assertEqual(args.auth_action, "login")
|
||||
|
||||
def test_auth_logout(self):
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(["auth", "logout"])
|
||||
self.assertEqual(args.command, "auth")
|
||||
self.assertEqual(args.auth_action, "logout")
|
||||
|
||||
def test_auth_status(self):
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(["auth", "status"])
|
||||
self.assertEqual(args.command, "auth")
|
||||
self.assertEqual(args.auth_action, "status")
|
||||
|
||||
|
||||
class TestParserTransport(unittest.TestCase):
|
||||
"""test_parser_transport: 解析 transport 子命令"""
|
||||
|
||||
def test_transport_info(self):
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(["transport", "info", "--corr_nr", "DEVK001"])
|
||||
self.assertEqual(args.command, "transport")
|
||||
self.assertEqual(args.transport_action, "info")
|
||||
self.assertEqual(args.corr_nr, "DEVK001")
|
||||
|
||||
def test_transport_list(self):
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(["transport", "list"])
|
||||
self.assertEqual(args.command, "transport")
|
||||
self.assertEqual(args.transport_action, "list")
|
||||
|
||||
|
||||
class TestParserInvalidType(unittest.TestCase):
|
||||
"""test_parser_invalid_type: --type invalid 应报错"""
|
||||
|
||||
def test_invalid_type_download(self):
|
||||
parser = build_parser()
|
||||
with self.assertRaises(SystemExit):
|
||||
parser.parse_args([
|
||||
"download", "--name", "ZTEST", "--type", "invalid", "--path", "./out",
|
||||
])
|
||||
|
||||
def test_invalid_type_create(self):
|
||||
parser = build_parser()
|
||||
with self.assertRaises(SystemExit):
|
||||
parser.parse_args([
|
||||
"create", "--name", "ZTEST", "--type", "invalid",
|
||||
])
|
||||
|
||||
|
||||
class TestParserScaffold(unittest.TestCase):
|
||||
"""test_parser_scaffold: 解析 scaffold 命令"""
|
||||
|
||||
def test_scaffold_with_template(self):
|
||||
parser = build_parser()
|
||||
args = parser.parse_args([
|
||||
"scaffold", "--name", "ZTEST", "--template", "alv-report",
|
||||
])
|
||||
self.assertEqual(args.command, "scaffold")
|
||||
self.assertEqual(args.name, "ZTEST")
|
||||
self.assertEqual(args.template, "alv-report")
|
||||
|
||||
def test_scaffold_invalid_template(self):
|
||||
parser = build_parser()
|
||||
with self.assertRaises(SystemExit):
|
||||
parser.parse_args([
|
||||
"scaffold", "--name", "ZTEST", "--template", "nonexistent",
|
||||
])
|
||||
|
||||
|
||||
class TestMainNoCommandExits(unittest.TestCase):
|
||||
"""test_main_no_command_exits: main() 无参数时 exit code 1"""
|
||||
|
||||
@patch("sapcli.cli.app.sys.argv", ["sap-cli"])
|
||||
@patch("sapcli.cli.app._setup_encoding")
|
||||
@patch("sapcli.cli.app._setup_logging")
|
||||
@patch("sapcli.cli.app._suppress_ssl_warnings")
|
||||
def test_main_no_command_exits(self, mock_ssl, mock_log, mock_enc):
|
||||
from sapcli.cli.app import main
|
||||
with self.assertRaises(SystemExit) as ctx:
|
||||
main()
|
||||
self.assertEqual(ctx.exception.code, 1)
|
||||
|
||||
|
||||
class TestParserEdgeCases(unittest.TestCase):
|
||||
"""补充边界情况测试。"""
|
||||
|
||||
def test_all_type_keys_accepted(self):
|
||||
"""确保 all_type_keys() 返回的每个类型都能被 parser 接受。"""
|
||||
parser = build_parser()
|
||||
for t in all_type_keys():
|
||||
args = parser.parse_args([
|
||||
"download", "--name", "ZTEST", "--type", t, "--path", "./out",
|
||||
])
|
||||
self.assertEqual(args.type, t, f"type={t} 应被 parser 接受")
|
||||
|
||||
def test_sync_fail_fast(self):
|
||||
parser = build_parser()
|
||||
args = parser.parse_args([
|
||||
"sync", "--name", "ZTEST", "--type", "report",
|
||||
"--path", "./file.abap", "--fail-fast",
|
||||
])
|
||||
self.assertTrue(args.fail_fast)
|
||||
|
||||
def test_delete_with_path(self):
|
||||
parser = build_parser()
|
||||
args = parser.parse_args([
|
||||
"delete", "--name", "ZTEST", "--type", "report", "--path", "./project",
|
||||
])
|
||||
self.assertEqual(args.command, "delete")
|
||||
self.assertEqual(args.path, "./project")
|
||||
|
||||
def test_info_command(self):
|
||||
parser = build_parser()
|
||||
args = parser.parse_args([
|
||||
"info", "--name", "ZCL_TEST", "--type", "class",
|
||||
])
|
||||
self.assertEqual(args.command, "info")
|
||||
self.assertEqual(args.name, "ZCL_TEST")
|
||||
|
||||
def test_init_command(self):
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(["init", "--path", "./project"])
|
||||
self.assertEqual(args.command, "init")
|
||||
self.assertEqual(args.path, "./project")
|
||||
|
||||
def test_refresh_command(self):
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(["refresh", "--path", "./project"])
|
||||
self.assertEqual(args.command, "refresh")
|
||||
|
||||
def test_list_command(self):
|
||||
parser = build_parser()
|
||||
args = parser.parse_args([
|
||||
"list", "--type", "report", "--prefix", "Z*",
|
||||
])
|
||||
self.assertEqual(args.command, "list")
|
||||
self.assertEqual(args.prefix, "Z*")
|
||||
|
||||
def test_whereused_command(self):
|
||||
parser = build_parser()
|
||||
args = parser.parse_args([
|
||||
"whereused", "--name", "ZTEST", "--type", "report",
|
||||
])
|
||||
self.assertEqual(args.command, "whereused")
|
||||
|
||||
def test_search_command(self):
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(["search", "--query", "CALL FUNCTION"])
|
||||
self.assertEqual(args.command, "search")
|
||||
self.assertEqual(args.query, "CALL FUNCTION")
|
||||
|
||||
def test_check_command(self):
|
||||
parser = build_parser()
|
||||
args = parser.parse_args([
|
||||
"check", "--name", "ZTEST", "--type", "report",
|
||||
])
|
||||
self.assertEqual(args.command, "check")
|
||||
|
||||
def test_format_command(self):
|
||||
parser = build_parser()
|
||||
args = parser.parse_args([
|
||||
"format", "--name", "ZTEST", "--type", "report",
|
||||
])
|
||||
self.assertEqual(args.command, "format")
|
||||
|
||||
def test_diff_command(self):
|
||||
parser = build_parser()
|
||||
args = parser.parse_args([
|
||||
"diff", "--name", "ZTEST", "--type", "report", "--path", "./ztest.abap",
|
||||
])
|
||||
self.assertEqual(args.command, "diff")
|
||||
|
||||
def test_analyze_command(self):
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(["analyze", "--path", "./project"])
|
||||
self.assertEqual(args.command, "analyze")
|
||||
|
||||
def test_package_create(self):
|
||||
parser = build_parser()
|
||||
args = parser.parse_args([
|
||||
"package", "create", "--name", "ZMY_PKG", "--description", "test pkg",
|
||||
])
|
||||
self.assertEqual(args.command, "package")
|
||||
self.assertEqual(args.package_action, "create")
|
||||
self.assertEqual(args.name, "ZMY_PKG")
|
||||
|
||||
def test_cds_download(self):
|
||||
parser = build_parser()
|
||||
args = parser.parse_args([
|
||||
"cds", "download", "--name", "ZMY_CDS", "--path", "./out",
|
||||
])
|
||||
self.assertEqual(args.command, "cds")
|
||||
self.assertEqual(args.cds_action, "download")
|
||||
|
||||
def test_transport_release(self):
|
||||
parser = build_parser()
|
||||
args = parser.parse_args([
|
||||
"transport", "release", "--corr_nr", "DEVK001",
|
||||
])
|
||||
self.assertEqual(args.transport_action, "release")
|
||||
|
||||
def test_transport_objects(self):
|
||||
parser = build_parser()
|
||||
args = parser.parse_args([
|
||||
"transport", "objects", "--corr_nr", "DEVK001",
|
||||
])
|
||||
self.assertEqual(args.transport_action, "objects")
|
||||
|
||||
def test_transport_create(self):
|
||||
parser = build_parser()
|
||||
args = parser.parse_args([
|
||||
"transport", "create", "--description", "CEM BIP 传输请求",
|
||||
])
|
||||
self.assertEqual(args.transport_action, "create")
|
||||
self.assertEqual(args.description, "CEM BIP 传输请求")
|
||||
|
||||
def test_global_config_option(self):
|
||||
parser = build_parser()
|
||||
args = parser.parse_args([
|
||||
"--config", "./my_config.ini",
|
||||
"download", "--name", "ZTEST", "--type", "report", "--path", "./out",
|
||||
])
|
||||
self.assertEqual(args.config, "./my_config.ini")
|
||||
|
||||
def test_global_profile_option(self):
|
||||
parser = build_parser()
|
||||
args = parser.parse_args([
|
||||
"--profile", "DEV",
|
||||
"download", "--name", "ZTEST", "--type", "report", "--path", "./out",
|
||||
])
|
||||
self.assertEqual(args.profile, "DEV")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user