aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/test.yml26
-rw-r--r--pdm.lock49
-rw-r--r--pyproject.toml3
-rw-r--r--src/ipm/__main__.py97
-rw-r--r--src/ipm/api.py79
-rw-r--r--src/ipm/const.py6
-rw-r--r--src/ipm/exceptions.py12
-rw-r--r--src/ipm/logging.py12
-rw-r--r--src/ipm/models/ipk.py11
-rw-r--r--src/ipm/utils/freeze.py18
-rw-r--r--test/infini.toml9
-rw-r--r--test/src/__init__.py1
-rw-r--r--tests/test-api.py64
-rw-r--r--tests/test_api.py20
-rw-r--r--tests/test_cli.py64
15 files changed, 323 insertions, 148 deletions
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 0a6958f..f0c1b6c 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -3,8 +3,8 @@ on:
workflow_dispatch:
jobs:
- test-api:
- name: test commit
+ tests:
+ name: Test IPM
runs-on: ubuntu-latest
permissions:
id-token: write
@@ -17,18 +17,14 @@ jobs:
with:
python-version: "3.9"
- - name: setup pdm
- run: pip install pdm
-
- - run: pdm install
-
- - name: install test deps
+ - name: Setup PDM
run: |
- pdm install -dG test
- # pip install pytest
-
- - name: test api
+ pip install pdm
+ pdm add pytest
+
+ - name: Install IPM
+ run: pdm install
+
+ - name: Test API
run: |
- cd tests
- pdm venv activate in-project
- pdm run test-api.py \ No newline at end of file
+ pdm run python -m pytest tests/
diff --git a/pdm.lock b/pdm.lock
index 3f90529..7233b29 100644
--- a/pdm.lock
+++ b/pdm.lock
@@ -5,7 +5,11 @@
groups = ["default", "test"]
strategy = ["cross_platform", "inherit_metadata"]
lock_version = "4.4.1"
+<<<<<<< HEAD
+content_hash = "sha256:9470f87391867615509911906fda898fd0fc55d1edecca94597578d34f5713ab"
+=======
content_hash = "sha256:7e92b84c2b52abef8bf2cca785657bbd0686221a6d3174500294332114b6df90"
+>>>>>>> 55623b0cb2e0cb49f5ff4f456121b07c2376659c
[[package]]
name = "certifi"
@@ -75,12 +79,31 @@ files = [
]
[[package]]
+name = "click"
+version = "8.1.7"
+requires_python = ">=3.7"
+summary = "Composable command line interface toolkit"
+groups = ["default"]
+dependencies = [
+ "colorama; platform_system == \"Windows\"",
+]
+files = [
+ {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
+ {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
+]
+
+[[package]]
name = "colorama"
version = "0.4.6"
requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
summary = "Cross-platform colored terminal text."
+<<<<<<< HEAD
+groups = ["default"]
+marker = "sys_platform == \"win32\" or platform_system == \"Windows\""
+=======
groups = ["default", "test"]
marker = "sys_platform == \"win32\""
+>>>>>>> 55623b0cb2e0cb49f5ff4f456121b07c2376659c
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
@@ -231,6 +254,32 @@ files = [
]
[[package]]
+name = "typer"
+version = "0.9.0"
+requires_python = ">=3.6"
+summary = "Typer, build great CLIs. Easy to code. Based on Python type hints."
+groups = ["default"]
+dependencies = [
+ "click<9.0.0,>=7.1.1",
+ "typing-extensions>=3.7.4.3",
+]
+files = [
+ {file = "typer-0.9.0-py3-none-any.whl", hash = "sha256:5d96d986a21493606a358cae4461bd8cdf83cbf33a5aa950ae629ca3b51467ee"},
+ {file = "typer-0.9.0.tar.gz", hash = "sha256:50922fd79aea2f4751a8e0408ff10d2662bd0c8bbfa84755a699f3bada2978b2"},
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.9.0"
+requires_python = ">=3.8"
+summary = "Backported and Experimental Type Hints for Python 3.8+"
+groups = ["default"]
+files = [
+ {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"},
+ {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"},
+]
+
+[[package]]
name = "urllib3"
version = "2.1.0"
requires_python = ">=3.8"
diff --git a/pyproject.toml b/pyproject.toml
index fc41f6c..d10bb74 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "ipm"
-version = "0.1.0-alpha.2"
+version = "0.1.0-alpha.3"
description = "Infini Package Manager"
authors = [
{ name = "苏向夜", email = "fu050409@163.com" },
@@ -11,6 +11,7 @@ dependencies = [
"pytest>=7.4.4",
"toml>=0.10.2",
"requests>=2.31.0",
+ "typer>=0.9.0",
]
requires-python = ">=3.10"
readme = "README.md"
diff --git a/src/ipm/__main__.py b/src/ipm/__main__.py
index c40e86e..753c3a7 100644
--- a/src/ipm/__main__.py
+++ b/src/ipm/__main__.py
@@ -1,41 +1,62 @@
-from .api import install, extract, build
-import argparse
-import sys
-
-def main():
- parser = argparse.ArgumentParser(
- prog="ipm", description="Infini 包管理器", exit_on_error=False
- )
- subparsers = parser.add_subparsers(
- title="指令", dest="command", metavar="<operation>"
- )
-
- # Install command
- install_parser = subparsers.add_parser("install", help="安装一个 Infini 规则包到此计算机")
- install_parser.add_argument("uri", help="Infini 包的统一资源标识符")
- install_parser.add_argument("--index", help="IPM 包服务器")
-
- # Extract command
- extract_parser = subparsers.add_parser("extract", help="解压缩 Infini 包")
- extract_parser.add_argument("package", help="Infini 包路径")
- extract_parser.add_argument(
- "--dist",
- default=".",
- help="特定的解压路径 (默认: 当前工作目录)",
- )
-
- # Build command
- build_parser = subparsers.add_parser("build", help="打包 Infini 规则包")
- build_parser.add_argument("package", nargs="?", help="Infini 库路径", default=".")
-
- args = parser.parse_args(sys.argv[1:] or ["-h"])
-
- if args.command == "install":
- install(args.uri, args.index, echo=True)
- elif args.command == "extract":
- extract(args.package, args.dist)
- elif args.command == "build":
- build(args.package)
+from . import api
+from .exceptions import IpmException
+from .logging import logger
+import typer
+
+main = typer.Typer(
+ name="ipm", help="Infini 包管理器", no_args_is_help=True, add_completion=False
+)
+
+
+@main.command()
+def install(
+ uri: str = typer.Argument(help="Infini 包的统一资源标识符"),
+ index: str = typer.Option(None, help="IPM 包服务器"),
+):
+ """安装一个 Infini 规则包到此计算机"""
+ try:
+ api.install(uri, index, echo=True)
+ except IpmException as error:
+ logger.error(error)
+
+
+@main.command()
+def extract(
+ package: str = typer.Argument(help="Infini 项目路径"),
+ dist: str = typer.Option(".", help="特定的解压路径"),
+):
+ """解压缩 Infini 包"""
+ try:
+ api.extract(package, dist, echo=True)
+ except IpmException as error:
+ logger.error(error)
+
+
+@main.command()
+def init(force: bool = typer.Option(None, "--force", "-f", help="强制初始化")):
+ """初始化一个 Infini 项目"""
+ try:
+ api.init(".", force, echo=True)
+ except IpmException as error:
+ logger.error(error)
+
+
+@main.command()
+def new(package: str = typer.Argument(help="Infini 项目路径")):
+ """新建一个 Infini 项目"""
+ try:
+ api.new(package, echo=True)
+ except IpmException as error:
+ logger.error(error)
+
+
+@main.command()
+def build(package: str = typer.Argument(".", help="Infini 项目路径")):
+ """打包 Infini 规则包"""
+ try:
+ api.build(package, echo=True)
+ except IpmException as error:
+ logger.error(error)
if __name__ == "__main__":
diff --git a/src/ipm/api.py b/src/ipm/api.py
index faf7e0b..2cb6eee 100644
--- a/src/ipm/api.py
+++ b/src/ipm/api.py
@@ -1,16 +1,58 @@
from pathlib import Path
from .typing import StrPath
from .utils import freeze, urlparser, loader
-from .models.ipk import InfiniPackage
-from .exceptions import FileTypeMismatch
+from .models.ipk import InfiniPackage, InfiniFrozenPackage
+from .exceptions import FileTypeMismatch, TomlLoadFailed, FileNotFoundError
from .const import INDEX, HOME
-from .logging import info, success
+from .logging import info, success, warning, error
-import os
+import toml
-def build(source_path: StrPath, echo: bool = False) -> None:
- freeze.build_ipk(InfiniPackage(source_path))
+def init(source_path: StrPath, force: bool = False, echo: bool = False) -> None:
+ source_path = Path(source_path).resolve()
+ if (toml_path := (source_path / "infini.toml")).exists() and not force:
+ warning(f"无法在已经初始化的地址重新初始化, 如果你的确希望重新初始化, 请使用[ipm init --force].", echo)
+
+ toml_file = toml_path.open("w", encoding="utf-8")
+ toml.dump(
+ {
+ "infini": {
+ "name": source_path.name,
+ "version": "0.1.0",
+ "description": "COC 规则包",
+ "license": "MIT",
+ },
+ "requirements": {},
+ "dependencies": {},
+ },
+ toml_file,
+ )
+ toml_file.close()
+
+ (source_path / "src").mkdir(parents=True, exist_ok=True)
+ (source_path / "src" / "__init__.py").write_text(
+ "# Initialized `__init__.py` generated by ipm."
+ )
+
+
+def new(dist_path: StrPath, echo: bool = False) -> None:
+ info("初始化环境中...")
+ path = Path(dist_path).resolve()
+ if path.exists():
+ return warning(f"路径[{path}]已经存在.", echo)
+ path.mkdir(parents=True, exist_ok=True)
+ return init(path, echo=echo)
+
+
+def build(source_path: StrPath, echo: bool = False) -> InfiniFrozenPackage:
+ info("检查构建环境...", echo)
+ try:
+ ipk = InfiniPackage(source_path)
+ info(f"包[{ipk.name}]构建环境载入完毕.", echo)
+ except TomlLoadFailed as e:
+ return error(f"环境存在异常: {e}", echo)
+ return freeze.build_ipk(ipk, echo)
def extract(
@@ -28,12 +70,8 @@ def install(uri: str, index: str = "", echo: bool = False) -> None:
HOME.mkdir(parents=True, exist_ok=True)
index = index or INDEX
- if os.path.isabs(uri):
- info(f"检定给定的 URI 地址[{uri}]为本地路径.", echo)
- if not uri.endswith(".ipk"):
- raise FileTypeMismatch("文件类型与预期[.ipk]不匹配.")
- info("安装中...", echo)
- ipk = extract(Path(uri).resolve(), HOME, echo)
+ if uri.isalpha():
+ ...
elif urlparser.is_valid_url(uri):
filename = uri.rstrip("/").split("/")[-1]
ipk = loader.load(
@@ -41,9 +79,20 @@ def install(uri: str, index: str = "", echo: bool = False) -> None:
uri.rstrip("/").rsplit("/")[0],
filename,
)
- elif uri.isalpha():
- ...
else:
- raise FileTypeMismatch("URI指向未知的位置.")
+ info(f"检定给定的 URI 地址[{uri}]为本地路径.", echo)
+ path = Path(uri).resolve()
+ if not path.exists():
+ raise FileNotFoundError("给定的 URI 路径不存在!")
+
+ if uri.endswith(".ipk"):
+ info("安装中...", echo)
+ ipk = extract(Path(uri).resolve(), HOME, echo)
+ else:
+ raise FileTypeMismatch("文件类型与预期[.ipk]不匹配.")
success(f"包[{ipk.name}]成功安装在[{ipk.source_path}].", echo)
+
+
+def uninstall(ipk: str | InfiniPackage):
+ ...
diff --git a/src/ipm/const.py b/src/ipm/const.py
index c682fdd..d31b10a 100644
--- a/src/ipm/const.py
+++ b/src/ipm/const.py
@@ -1,8 +1,12 @@
from pathlib import Path
+# 控制参数
DEBUG = False
+# 初始化参数
INDEX = "https://ipm.hydroroll.team/index/"
HOME = Path.home() / ".ipm" / "src"
+
+# 文本参数
ATTENSION = """# This file is @generated by IPM.
-# It is not intended for manual editing."""
+# It is not intended for manual editing.\n\n"""
diff --git a/src/ipm/exceptions.py b/src/ipm/exceptions.py
index e6a01c0..b19b60c 100644
--- a/src/ipm/exceptions.py
+++ b/src/ipm/exceptions.py
@@ -2,11 +2,19 @@ class IpmException(Exception):
"""IPM Base Exception"""
-class FileNotFoundError(IpmException, FileNotFoundError):
+class FileException(IpmException):
+ """IPM File Base Exception"""
+
+
+class TomlLoadFailed(FileException):
+ """Failed to load `infini.toml`"""
+
+
+class FileNotFoundError(FileException, FileNotFoundError):
"""Raises when file not founded"""
-class FileExistsError(IpmException, FileExistsError):
+class FileExistsError(FileException, FileExistsError):
"""Raises when file not founded"""
diff --git a/src/ipm/logging.py b/src/ipm/logging.py
index 2594930..f923e64 100644
--- a/src/ipm/logging.py
+++ b/src/ipm/logging.py
@@ -4,9 +4,17 @@ from .const import DEBUG
logger = multilogger(name="IPM", level="DEBUG" if DEBUG else "INFO", notime=True)
-def info(message: str, echo: bool = True) -> None:
+def info(message: str, echo: bool = False) -> None:
return logger.info(message) if echo else None
-def success(message: str, echo: bool = True) -> None:
+def success(message: str, echo: bool = False) -> None:
return logger.success(message) if echo else None
+
+
+def warning(message: str, echo: bool = False) -> None:
+ return logger.warning(message) if echo else None
+
+
+def error(message: str, echo: bool = False) -> None:
+ return logger.error(message) if echo else None
diff --git a/src/ipm/models/ipk.py b/src/ipm/models/ipk.py
index a50ebde..27a3d68 100644
--- a/src/ipm/models/ipk.py
+++ b/src/ipm/models/ipk.py
@@ -1,6 +1,6 @@
from pathlib import Path
from ..typing import List, Dict, Literal
-from ..exceptions import SyntaxError
+from ..exceptions import SyntaxError, TomlLoadFailed
import toml
@@ -39,7 +39,11 @@ class InfiniPackage:
self.source_path = Path(path).resolve()
toml_path = self.source_path / "infini.toml"
- data_load = toml.load(toml_path.open("r", encoding="utf-8"))
+ try:
+ data_load = toml.load(toml_path.open("r", encoding="utf-8"))
+ except Exception as error:
+ raise TomlLoadFailed(f"项目文件[infini.toml]导入失败: {error}") from error
+
if "infini" not in data_load.keys():
raise SyntaxError("配置文件中缺少[infini]项.")
@@ -58,9 +62,6 @@ class InfiniPackage:
def hash_name(self) -> str:
return f"{self.name}-{self.version}.ipk.hash"
- # @property
- # def home_p
-
class InfiniFrozenPackage:
source_path: Path
diff --git a/src/ipm/utils/freeze.py b/src/ipm/utils/freeze.py
index 74f4b40..5c6951c 100644
--- a/src/ipm/utils/freeze.py
+++ b/src/ipm/utils/freeze.py
@@ -10,8 +10,9 @@ import tempfile
import shutil
-def build_ipk(ipk: InfiniPackage) -> InfiniFrozenPackage:
- build_dir = ipk.source_path / ".build"
+def build_ipk(ipk: InfiniPackage, echo: bool = False) -> InfiniFrozenPackage:
+ info("正在初始化开发环境...", echo)
+ build_dir = ipk.source_path / "build"
src_path = ipk.source_path / "src"
dist_path = ipk.source_path / "dist"
ifp_path = dist_path / ipk.default_name
@@ -24,28 +25,35 @@ def build_ipk(ipk: InfiniPackage) -> InfiniFrozenPackage:
dist_path.mkdir(parents=True, exist_ok=True)
build_dir.mkdir(parents=True, exist_ok=True)
+ info("开发环境构建完成, 开始复制工程文件...", echo)
shutil.copytree(src_path, build_dir / "src")
shutil.copy2(ipk.source_path / "infini.toml", build_dir / "infini.toml")
+ info("工程文件复制完毕, 开始打包[ipk]文件...", echo)
_freeze.create_tar_gz(
str(build_dir),
str(ifp_path),
)
- (dist_path / ipk.hash_name).write_bytes(ifp_hash(ifp_path))
+ success(f"打包文件已存至[{ifp_path}].", echo)
+ info("开始创建SHA256验证文件...", echo)
+ hash_bytes = ifp_hash(ifp_path)
+ info(f"文件SHA256值为[{hash_bytes.hex()}].", echo)
+ (dist_path / ipk.hash_name).write_bytes(hash_bytes)
+ success(f"包[{ipk.name}]构建成功.", echo)
return InfiniFrozenPackage(source_path=ifp_path, **{"name": ipk.name})
def extract_ipk(
- source_path: StrPath, dist_path: str | Path, echo: bool = False
+ source_path: StrPath, dist_path: StrPath, echo: bool = False
) -> InfiniPackage:
ifp_path = Path(source_path).resolve()
dist_path = Path(dist_path).resolve()
hash_path = ifp_path.parent / (ifp_path.name + ".hash")
if not hash_path.exists():
- raise VerifyFailed("哈希文件不存在!")
+ raise VerifyFailed(f"哈希文件[{hash_path}]不存在!")
if not ifp_verify(ifp_path, hash_path.read_bytes()):
raise VerifyFailed("文件完整性验证失败!")
diff --git a/test/infini.toml b/test/infini.toml
new file mode 100644
index 0000000..cc0f720
--- /dev/null
+++ b/test/infini.toml
@@ -0,0 +1,9 @@
+[infini]
+name = "test"
+version = "0.1.0"
+description = "COC 规则包"
+license = "MIT"
+
+[requirements]
+
+[dependencies]
diff --git a/test/src/__init__.py b/test/src/__init__.py
new file mode 100644
index 0000000..d93492e
--- /dev/null
+++ b/test/src/__init__.py
@@ -0,0 +1 @@
+# Initialized `__init__.py` generated by ipm. \ No newline at end of file
diff --git a/tests/test-api.py b/tests/test-api.py
deleted file mode 100644
index 9afe40b..0000000
--- a/tests/test-api.py
+++ /dev/null
@@ -1,64 +0,0 @@
-import pytest
-from ipm.__main__ import main
-from unittest.mock import patch, MagicMock
-
-# Test IDs for parametrization
-HAPPY_PATH_INSTALL = "happy_install"
-HAPPY_PATH_EXTRACT = "happy_extract"
-HAPPY_PATH_BUILD = "happy_build"
-EDGE_CASE_NO_ARGS = "edge_no_args"
-ERROR_CASE_UNKNOWN_COMMAND = "error_unknown_command"
-
-# Mock the sys.argv to simulate command line arguments
-@pytest.fixture
-def mock_sys_argv(monkeypatch):
- def _mock_sys_argv(args):
- monkeypatch.setattr("sys.argv", ["ipm"] + args)
- return _mock_sys_argv
-
-# Mock the api functions to prevent actual execution
-@pytest.fixture
-def mock_api_functions(monkeypatch):
- install_mock = MagicMock()
- extract_mock = MagicMock()
- build_mock = MagicMock()
- monkeypatch.setattr("ipm.__main__.install", install_mock)
- monkeypatch.setattr("ipm.__main__.extract", extract_mock)
- monkeypatch.setattr("ipm.__main__.build", build_mock)
- return install_mock, extract_mock, build_mock
-
-@pytest.mark.parametrize("test_id, args, expected_call", [
- # Happy path tests
- (HAPPY_PATH_INSTALL, ["install", "http://ipm.hydroroll.team/package.ipk"], ("install", ["http://ipm.hydroroll.team/package.ipk", None])),
- (HAPPY_PATH_EXTRACT, ["extract", "package.ipk", "--dist", "dist_folder"], ("extract", ["package.ipk", "dist_folder"])),
- (HAPPY_PATH_BUILD, ["build", "source_folder"], ("build", ["source_folder"])),
-
- # Edge case tests
- (EDGE_CASE_NO_ARGS, [], ("help", [])),
-
- # Error case tests
- (ERROR_CASE_UNKNOWN_COMMAND, ["unknown", "arg"], ("error", ["unknown"])),
-])
-def test_main_commands(test_id, args, expected_call, mock_sys_argv, mock_api_functions, capsys):
- mock_sys_argv(args)
- install_mock, extract_mock, build_mock = mock_api_functions
-
- # Act
- with pytest.raises(SystemExit): # argparse exits the program when -h is called or on error
- main()
-
- # Assert
- if expected_call[0] == "install":
- install_mock.assert_called_once_with(*expected_call[1], echo=True)
- elif expected_call[0] == "extract":
- extract_mock.assert_called_once_with(*expected_call[1])
- elif expected_call[0] == "build":
- build_mock.assert_called_once_with(*expected_call[1])
- elif expected_call[0] == "help":
- captured = capsys.readouterr()
- assert "Infini 包管理器" in captured.out
- elif expected_call[0] == "error":
- captured = capsys.readouterr()
- assert "error: unrecognized arguments" in captured.err
-
-pytest.main() \ No newline at end of file
diff --git a/tests/test_api.py b/tests/test_api.py
new file mode 100644
index 0000000..7136b36
--- /dev/null
+++ b/tests/test_api.py
@@ -0,0 +1,20 @@
+from ipm import api
+
+
+def test_new():
+ api.new("test")
+
+
+def test_build():
+ api.new("test")
+ api.build("test")
+
+
+def test_extract():
+ api.build("test")
+ api.extract("test\\dist\\test-0.1.0.ipk")
+
+
+def test_install():
+ api.build("test")
+ api.install("test\\dist\\test-0.1.0.ipk")
diff --git a/tests/test_cli.py b/tests/test_cli.py
new file mode 100644
index 0000000..8b22848
--- /dev/null
+++ b/tests/test_cli.py
@@ -0,0 +1,64 @@
+# import pytest
+# from ipm.__main__ import main
+# from unittest.mock import patch, MagicMock
+
+# # Test IDs for parametrization
+# HAPPY_PATH_INSTALL = "happy_install"
+# HAPPY_PATH_EXTRACT = "happy_extract"
+# HAPPY_PATH_BUILD = "happy_build"
+# EDGE_CASE_NO_ARGS = "edge_no_args"
+# ERROR_CASE_UNKNOWN_COMMAND = "error_unknown_command"
+
+# # Mock the sys.argv to simulate command line arguments
+# @pytest.fixture
+# def mock_sys_argv(monkeypatch):
+# def _mock_sys_argv(args):
+# monkeypatch.setattr("sys.argv", ["ipm"] + args)
+# return _mock_sys_argv
+
+# # Mock the api functions to prevent actual execution
+# @pytest.fixture
+# def mock_api_functions(monkeypatch):
+# install_mock = MagicMock()
+# extract_mock = MagicMock()
+# build_mock = MagicMock()
+# monkeypatch.setattr("ipm.__main__.install", install_mock)
+# monkeypatch.setattr("ipm.__main__.extract", extract_mock)
+# monkeypatch.setattr("ipm.__main__.build", build_mock)
+# return install_mock, extract_mock, build_mock
+
+# @pytest.mark.parametrize("test_id, args, expected_call", [
+# # Happy path tests
+# (HAPPY_PATH_INSTALL, ["install", "http://ipm.hydroroll.team/package.ipk"], ("install", ["http://ipm.hydroroll.team/package.ipk", None])),
+# (HAPPY_PATH_EXTRACT, ["extract", "package.ipk", "--dist", "dist_folder"], ("extract", ["package.ipk", "dist_folder"])),
+# (HAPPY_PATH_BUILD, ["build", "source_folder"], ("build", ["source_folder"])),
+
+# # Edge case tests
+# (EDGE_CASE_NO_ARGS, [], ("help", [])),
+
+# # Error case tests
+# (ERROR_CASE_UNKNOWN_COMMAND, ["unknown", "arg"], ("error", ["unknown"])),
+# ])
+# def test_main_commands(test_id, args, expected_call, mock_sys_argv, mock_api_functions, capsys):
+# mock_sys_argv(args)
+# install_mock, extract_mock, build_mock = mock_api_functions
+
+# # Act
+# with pytest.raises(SystemExit): # argparse exits the program when -h is called or on error
+# main()
+
+# # Assert
+# if expected_call[0] == "install":
+# install_mock.assert_called_once_with(*expected_call[1], echo=True)
+# elif expected_call[0] == "extract":
+# extract_mock.assert_called_once_with(*expected_call[1])
+# elif expected_call[0] == "build":
+# build_mock.assert_called_once_with(*expected_call[1])
+# elif expected_call[0] == "help":
+# captured = capsys.readouterr()
+# assert "Infini 包管理器" in captured.out
+# elif expected_call[0] == "error":
+# captured = capsys.readouterr()
+# assert "error: unrecognized arguments" in captured.err
+
+# pytest.main() \ No newline at end of file