diff --git a/.github/workflows/test-install.yml b/.github/workflows/test-install.yml new file mode 100644 index 0000000..0837f6d --- /dev/null +++ b/.github/workflows/test-install.yml @@ -0,0 +1,52 @@ +name: Test Install Script + +on: + push: + branches: [ dev, master, github_ci_docker_test ] + pull_request: + branches: [ dev, master, github_ci_docker_test ] + +jobs: + test-install: + strategy: + matrix: + ubuntu_version: [18.04, 20.04, 22.04, 24.04] + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Run tests in Docker container + run: | + docker run --rm \ + -v ${{ github.workspace }}:${{ github.workspace }} \ + -w ${{ github.workspace }} \ + ubuntu:${{ matrix.ubuntu_version }} \ + bash -c " + set -u && + export DEBIAN_FRONTEND=noninteractive && + # Set timezone to avoid tzdata interactive prompt + ln -sf /usr/share/zoneinfo/UTC /etc/localtime && + apt update && + apt install -y locales && + locale-gen en_US.UTF-8 && + export LANG=en_US.UTF-8 && + export LC_ALL=en_US.UTF-8 && + apt update && apt install -y sudo python3 python3-pip python3-venv python3-yaml python3-distro wget && + python3 -m venv /tmp/test_env && + source /tmp/test_env/bin/activate && + pip install --upgrade pip && + pip install pyyaml distro && + cd tests && + PYTHONIOENCODING=utf-8 python3 -u test_runner.py + " + - name: Upload test reports + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-reports-ubuntu-${{ matrix.ubuntu_version }} + path: | + tests/test_report.json + tests/test_report.html + if-no-files-found: ignore diff --git a/.gitignore b/.gitignore index fd20fdd..3bcfd2e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ - *.pyc +tests/test_local \ No newline at end of file diff --git a/README.md b/README.md index b3aaa32..57907b0 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ - 一键安装:ROS(支持ROS和ROS2,树莓派Jetson) [贡献@小鱼](https://github.com/fishros) - 一键安装:VsCode(支持amd64和arm64) [贡献@小鱼](https://github.com/fishros) - 一键安装:github桌面版(小鱼常用的github客户端) [贡献@小鱼](https://github.com/fishros) -- 一键安装:nodejs开发环境(通过nodejs可以预览小鱼官网噢 [贡献@小鱼](https://github.com/fishros) +- 一键安装:nodejs开发环境(通过nodejs可以预览小鱼官网噢) [贡献@小鱼](https://github.com/fishros) - 一键配置:rosdep(小鱼的rosdepc,又快又好用) [贡献@小鱼](https://github.com/fishros) - 一键配置:ROS环境(快速更新ROS环境设置,自动生成环境选择) [贡献@小鱼](https://github.com/fishros) - 一键配置:系统源(更换系统源,支持全版本Ubuntu系统) [贡献@小鱼](https://github.com/fishros) diff --git a/install.py b/install.py index a9d8623..1885b8f 100644 --- a/install.py +++ b/install.py @@ -74,7 +74,7 @@ def main(): # 使用量统计 - CmdTask("wget https://fishros.org.cn/forum/topic/1733 -O /tmp/t1733 -q --timeout 10 && rm -rf /tmp/t1733").run() + CmdTask("wget https://fishros.org.cn/forum/topic/1733 -O /tmp/t1733 -q --no-check-certificate --timeout 10 && rm -rf /tmp/t1733").run() PrintUtils.print_success(tr.tr("已为您切换语言至当前所在国家语言:")+tr.lang) if tr.country != 'CN': @@ -121,11 +121,15 @@ def main(): else: download_tools(code,tools,url_prefix) run_tool_file(tools[code]['tool'].replace("/",".")) - config_helper.gen_config_file() - PrintUtils.print_delay(tr.tr("欢迎加入机器人学习交流QQ群:438144612(入群口令:一键安装)"),0.05) - PrintUtils.print_delay(tr.tr("鱼香小铺正式开业,最低499可入手一台能建图会导航的移动机器人,淘宝搜店:鱼香ROS 或打开链接查看:https://item.taobao.com/item.htm?id=696573635888"),0.001) - PrintUtils.print_delay(tr.tr("如在使用过程中遇到问题,请打开:https://fishros.org.cn/forum 进行反馈"),0.001) + # 检查是否在 GitHub Actions 环境中运行或使用了测试配置文件 + # 如果是,则跳过生成配置文件和后续的打印操作,因为这些操作需要用户输入 + if os.environ.get('GITHUB_ACTIONS') != 'true' and os.environ.get('FISH_INSTALL_CONFIG') is None: + config_helper.gen_config_file() + + PrintUtils.print_delay(tr.tr("欢迎加入机器人学习交流QQ群:438144612(入群口令:一键安装)"),0.05) + PrintUtils.print_delay(tr.tr("鱼香小铺正式开业,最低499可入手一台能建图会导航的移动机器人,淘宝搜店:鱼香ROS 或打开链接查看:https://item.taobao.com/item.htm?id=696573635888"),0.001) + PrintUtils.print_delay(tr.tr("如在使用过程中遇到问题,请打开:https://fishros.org.cn/forum 进行反馈"),0.001) if __name__=='__main__': run_exc = [] diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..8212475 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,64 @@ +# 自动化测试说明 + +本目录包含用于自动化测试 `FishROS Install` 项目的脚本和配置文件。 + +## 文件说明 + +- `test_runner.py`: 主测试运行器,负责执行所有测试用例并生成报告。 +- `fish_install_test.yaml`: 测试配置文件,定义了不同系统版本下的测试用例。 +- `generate_report.py`: 用于生成 HTML 格式的测试报告。 +- `test_report.json`: 测试运行后生成的 JSON 格式报告。 +- `test_report.html`: 测试运行后生成的 HTML 格式报告。 + +## 运行测试 + +在项目根目录下执行以下命令来运行测试: + +```bash +cd tests +python3 test_runner.py +``` + +### 指定目标系统版本 + +可以通过 `--target-os-version` 参数指定要测试的 Ubuntu 版本代号: + +```bash +python3 test_runner.py --target-os-version focal +``` + +## 测试配置文件 + +`fish_install_test.yaml` 文件定义了测试用例。每个测试用例包含以下信息: + +- `name`: 测试用例名称。 +- `target_os_version`: 目标系统版本代号 (可选,如果不指定则适用于所有系统)。 +- `chooses`: 一个列表,包含在安装过程中需要自动选择的选项。 + +### 配置文件易错点提醒 + +1. `chooses` 列表中的 `choose` 值必须与 `install.py` 中 `tools` 字典的键值对应。 +2. `target_os_version` 必须是有效的 Ubuntu 版本代号(如 `bionic`, `focal`, `jammy` 等),目前仅支持ubuntu系列。 +3. `desc` 字段虽然不是必须的,但建议填写以方便理解。 +4. 在添加新的测试用例时,确保 `chooses` 中的选项序列能够完整地执行一个安装流程,避免因选项不当导致测试中断。 + +## 自动化测试与用户实际安装的区别 + +自动化测试与用户实际安装在以下方面有所不同: + +1. **配置文件**: 自动化测试使用 `FISH_INSTALL_CONFIG` 环境变量指定的配置文件,而用户实际安装时会交互式地选择选项并生成配置文件。 +2. **环境变量**: 自动化测试会设置特定的环境变量(如 `FISH_INSTALL_CONFIG`),而用户实际安装时不会。 +3. **跳过某些步骤**: 在自动化测试环境中,会跳过一些需要用户交互的步骤,例如生成配置文件的确认提示。 +4. **GitHub Actions**: 在 GitHub Actions 中运行时,会进一步跳过一些步骤以适应 CI/CD 环境。 + +## 工作原理 + +1. `test_runner.py` 会读取 `fish_install_test.yaml` 文件,加载所有适用于当前系统版本的测试用例。 +2. 对于每个测试用例,`test_runner.py` 会创建一个临时的 `fish_install.yaml` 配置文件,其中包含该测试用例的 `chooses` 信息。 +3. 然后,`test_runner.py` 会运行 `../install.py` 脚本,并通过环境变量 `FISH_INSTALL_CONFIG` 指定使用临时配置文件。 +4. `install.py` 会根据配置文件中的选项自动执行安装过程,无需人工干预。 +5. 测试运行结束后,`test_runner.py` 会生成 JSON 和 HTML 格式的测试报告。 + +## GitHub Actions 集成 + +本测试套件已集成到 GitHub Actions 中,每次推送代码时都会自动运行。工作流文件位于 `.github/workflows/test-install.yml`。 \ No newline at end of file diff --git a/tests/fish_install_test.yaml b/tests/fish_install_test.yaml new file mode 100644 index 0000000..5e4ede6 --- /dev/null +++ b/tests/fish_install_test.yaml @@ -0,0 +1,56 @@ +# 测试配置文件,用于 GitHub Actions 自动化测试 +# 格式: +# - name: "测试用例名称" +# target_os_version: "目标系统版本代号" (可选,如果不指定则适用于所有系统) +# chooses: [{choose: <选项ID>, desc: <选项描述>}] + +# 为不同的 Ubuntu 版本定义具体的测试配置 +# Ubuntu 18.04 (bionic) - Melodic +- name: "Install_ROS_bionic" + target_os_version: "bionic" + chooses: + - { choose: 1, desc: "一键安装(推荐):ROS(支持ROS/ROS2,树莓派Jetson)" } + - { choose: 2, desc: "不更换系统源再继续安装" } + - { choose: 4, desc: "ROS官方源" } + - { choose: 1, desc: "支持的第一个ros版本" } + - { choose: 2, desc: "基础版(小)" } + +# Ubuntu 20.04 (focal) - Noetic 或 Foxy +- name: "Install_ROS_focal_noetic" + target_os_version: "focal" + chooses: + - { choose: 1, desc: "一键安装(推荐):ROS(支持ROS/ROS2,树莓派Jetson)" } + - { choose: 2, desc: "不更换系统源再继续安装" } + - { choose: 4, desc: "ROS官方源" } + - { choose: 1, desc: "支持的第一个ros版本" } + - { choose: 2, desc: "基础版(小)" } + +# - name: "Install_ROS_focal_foxy" +# target_os_version: "focal" +# chooses: +# - { choose: 1, desc: "一键安装(推荐):ROS(支持ROS/ROS2,树莓派Jetson)" } +# - { choose: 2, desc: "不更换系统源再继续安装" } +# - { choose: 4, desc: "ROS官方源" } +# - { choose: 1, desc: "支持的第一个ros版本" } +# - { choose: 2, desc: "基础版(小)" } + +# Ubuntu 22.04 (jammy) - Humble +- name: "Install_ROS_jammy" + target_os_version: "jammy" + chooses: + - { choose: 1, desc: "一键安装(推荐):ROS(支持ROS/ROS2,树莓派Jetson)" } + - { choose: 2, desc: "不更换系统源再继续安装" } + - { choose: 4, desc: "ROS官方源" } + - { choose: 1, desc: "支持的第一个ros版本" } + - { choose: 2, desc: "基础版(小)" } + +# Ubuntu 24.04 (noble) - Jazzy +- name: "Install_ROS_noble" + target_os_version: "noble" + chooses: + - { choose: 1, desc: "一键安装(推荐):ROS(支持ROS/ROS2,树莓派Jetson)" } + - { choose: 2, desc: "不更换系统源再继续安装" } + - { choose: 4, desc: "ROS官方源" } + - { choose: 1, desc: "支持的第一个ros版本" } + - { choose: 2, desc: "基础版(小)" } + diff --git a/tests/generate_report.py b/tests/generate_report.py new file mode 100644 index 0000000..ad77730 --- /dev/null +++ b/tests/generate_report.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import json +import time +import os + +def generate_html_report(report, output_file): + """生成HTML格式的测试报告""" + html_content = """ + + + + + + 一键安装工具测试报告 + + + +
+

一键安装工具测试报告

+

生成时间: """ + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) + """

+
+ +
+

测试摘要

+

总计: """ + str(report["summary"]["total"]) + """

+

通过: """ + str(report["summary"]["passed"]) + """

+

失败: """ + str(report["summary"]["failed"]) + """

+
+ +
+

详细测试结果

+""" + + for test_case in report["details"]: + status_class = "passed" if test_case["success"] else "failed" + status_text = "通过" if test_case["success"] else "失败" + status_style = "status-passed" if test_case["success"] else "status-failed" + + html_content += """ +
+
+ {} + {} +
+
输出日志:
+
{}
+
+""".format(status_class, test_case["name"], status_style, status_text, test_case["output"]) + + html_content += """ +
+ + + + +""" + + with open(output_file, 'w', encoding='utf-8') as f: + f.write(html_content) + + +if __name__ == "__main__": + # 读取JSON测试报告 + report_file = "test_report.json" + if os.path.exists(report_file): + with open(report_file, 'r', encoding='utf-8') as f: + report = json.load(f) + + # 生成HTML报告 + html_report_file = "test_report.html" + generate_html_report(report, html_report_file) + print("HTML测试报告已生成: {}".format(html_report_file)) + else: + print("找不到测试报告文件: {}".format(report_file)) \ No newline at end of file diff --git a/tests/test_runner.py b/tests/test_runner.py new file mode 100644 index 0000000..22d1e47 --- /dev/null +++ b/tests/test_runner.py @@ -0,0 +1,467 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import yaml +import subprocess +import os +import sys +import time +import json +import re +import argparse + +# 将项目根目录添加到 Python 路径中,以便能找到 tools 模块 +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) + +# 导入 distro 库以获取系统版本代号 +try: + import distro + HAVE_DISTRO = True +except ImportError: + HAVE_DISTRO = False + +def load_test_cases(config_file): + """加载测试用例""" + try: + with open(config_file, 'r', encoding='utf-8') as f: + test_cases = yaml.safe_load(f) + return test_cases + except Exception as e: + print("加载测试配置文件失败: {}".format(e)) + return [] + +def get_ubuntu_codename(): + """获取Ubuntu系统的版本代号""" + if HAVE_DISTRO: + # 使用 distro 库获取系统信息 + codename = distro.codename() + if codename: + return codename.lower() + + # 备用方法:尝试使用 lsb_release 命令 + try: + result = subprocess.run(['lsb_release', '-cs'], capture_output=True, text=True, timeout=10) + if result.returncode == 0: + return result.stdout.strip().lower() + except (subprocess.TimeoutExpired, FileNotFoundError): + pass + + # 备用方法:从 /etc/os-release 文件中读取 + try: + with open('/etc/os-release', 'r') as f: + for line in f: + if line.startswith('UBUNTU_CODENAME='): + codename = line.split('=')[1].strip().strip('"') + return codename.lower() + elif line.startswith('VERSION_CODENAME='): + codename = line.split('=')[1].strip().strip('"') + return codename.lower() + except FileNotFoundError: + pass + + # 如果所有方法都失败,返回 None + return None + +def check_output_for_errors(output): + """检查输出中是否包含错误信息""" + error_keywords = [ + "ModuleNotFoundError", + "ImportError", + "Exception", + "Error:", + "Traceback", + "检测到程序发生异常退出" + ] + + for line in output.split('\n'): + for keyword in error_keywords: + if keyword in line: + return True + return False + +def generate_html_report(report, output_file): + """生成HTML格式的测试报告""" + html_content = """ + + + + + + 一键安装工具测试报告 + + + +
+

一键安装工具测试报告

+

生成时间: """ + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) + """

+
+ +
+

测试摘要

+

总计: """ + str(report["summary"]["total"]) + """

+

通过: """ + str(report["summary"]["passed"]) + """

+

失败: """ + str(report["summary"]["failed"]) + """

+
+ +
+

详细测试结果

+""" + + for test_case in report["details"]: + status_class = "passed" if test_case["success"] else "failed" + status_text = "通过" if test_case["success"] else "失败" + status_style = "status-passed" if test_case["success"] else "status-failed" + + html_content += """ +
+
+ {} + {}<\/span> + <\/div> +
输出日志:<\/div> +
{}<\/div> + <\/div> +""".format(status_class, test_case["name"], status_style, status_text, test_case["output"]) + + html_content += """ +
+ +
+

测试报告由一键安装工具自动生成

+
+ + +""" + + with open(output_file, 'w', encoding='utf-8') as f: + f.write(html_content) + +def run_install_test(test_case): + + """运行单个安装测试""" + name = test_case.get('name', 'Unknown Test') + chooses = test_case.get('chooses', []) + + print("开始测试: {}".format(name)) + + # 创建临时配置文件路径 + temp_config = "/tmp/fish_install_test_temp.yaml" + + # 确保 /tmp 目录存在 + os.makedirs("/tmp", exist_ok=True) + + # 创建临时配置文件 + config_data = {'chooses': chooses} + try: + with open(temp_config, 'w', encoding='utf-8') as f: + yaml.dump(config_data, f, allow_unicode=True) + print("已创建临时配置文件: {}".format(temp_config)) + except Exception as e: + print("创建临时配置文件失败: {}".format(e)) + return False, "" + + # 备份原始的 fish_install.yaml (如果存在) + original_config = "../fish_install.yaml" + backup_config = "../fish_install.yaml.backup" + if os.path.exists(original_config): + try: + os.rename(original_config, backup_config) + print("已备份原始配置文件至: {}".format(backup_config)) + except Exception as e: + print("备份原始配置文件失败: {}".format(e)) + # 即使备份失败也继续执行,因为我们会在最后恢复 + + # 将临时配置文件复制为当前配置文件和/tmp/fishinstall/tools/fish_install.yaml + try: + import shutil + shutil.copy(temp_config, original_config) + print("已将临时配置文件复制为: {}".format(original_config)) + + # 同时将配置文件复制到/tmp/fishinstall/tools/目录下 + fishinstall_config = "/tmp/fishinstall/tools/fish_install.yaml" + # 确保目录存在 + os.makedirs(os.path.dirname(fishinstall_config), exist_ok=True) + shutil.copy(temp_config, fishinstall_config) + print("已将临时配置文件复制为: {}".format(fishinstall_config)) + except Exception as e: + print("复制配置文件失败: {}".format(e)) + # 恢复备份的配置文件 + if os.path.exists(backup_config): + try: + os.rename(backup_config, original_config) + print(f"已恢复备份的配置文件: {original_config}") + except: + pass + # 清理临时文件 + if os.path.exists(temp_config): + os.remove(temp_config) + return False, "" + + # 初始化输出和错误信息 + output = "" + error = "" + + # 运行安装脚本 + try: + # 使用 -u 参数确保输出不被缓冲,以便实时查看日志 + # 直接运行 install.py,它会自动检测并使用 ../fish_install.yaml + # 增加超时时间为 2 小时 (7200 秒) + process = subprocess.Popen( + [sys.executable, "../install.py"], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + universal_newlines=True, + bufsize=1, + env={**os.environ, 'FISH_INSTALL_CONFIG': '../fish_install.yaml'} + ) + + # 实时打印输出 + print("=== 脚本输出开始 ===") + while True: + output_line = process.stdout.readline() + if output_line == '' and process.poll() is not None: + break + if output_line: + print(output_line.strip()) + output += output_line + # 确保实时刷新输出 + sys.stdout.flush() + print("=== 脚本输出结束 ===") + + # 等待进程结束,超时时间为 2 小时 + # stdout, _ = process.communicate(timeout=7200) + # output = stdout + + # # 打印输出 + # print("=== 脚本输出开始 ===") + # print(stdout) + # print("=== 脚本输出结束 ===") + + # 检查退出码和输出中的错误信息 + if process.returncode == 0 and not check_output_for_errors(output): + print(f"测试通过: {name}") + return True, output + else: + if process.returncode != 0: + print(f"测试失败: {name} (退出码: {process.returncode})") + else: + print(f"测试失败: {name} (脚本中检测到错误)") + return False, output + except subprocess.TimeoutExpired: + print(f"测试超时: {name} (超过7200秒)") + # 终止进程 + process.kill() + stdout, _ = process.communicate() + output = stdout + return False, output + except Exception as e: + print(f"运行测试时发生异常: {e}") + error = str(e) + return False, output + "\n" + error + finally: + # 恢复备份的配置文件 (如果存在) + if os.path.exists(backup_config): + try: + os.rename(backup_config, original_config) + print(f"已恢复备份的配置文件: {original_config}") + except Exception as e: + print(f"恢复备份的配置文件失败: {e}") + # 如果没有备份文件,但创建了原始配置文件,则删除它 + elif os.path.exists(original_config): + try: + os.remove(original_config) + print(f"已删除临时创建的配置文件: {original_config}") + except Exception as e: + print(f"删除临时配置文件失败: {e}") + # 清理临时配置文件 + if os.path.exists(temp_config): + try: + os.remove(temp_config) + print(f"已清理临时配置文件: {temp_config}") + except Exception as e: + print(f"清理临时配置文件失败: {e}") + +def main(): + """主函数""" + # 解析命令行参数 + parser = argparse.ArgumentParser(description='运行一键安装工具测试') + parser.add_argument('--target-os-version', type=str, help='目标Ubuntu版本代号 (例如: bionic, focal, jammy, noble)') + args = parser.parse_args() + + target_os_version = args.target_os_version + if target_os_version: + print(f"目标系统版本: {target_os_version}") + else: + # 自动检测系统版本代号 + target_os_version = get_ubuntu_codename() + if target_os_version: + print(f"自动检测到系统版本: {target_os_version}") + else: + print("未指定目标系统版本,也未能自动检测到系统版本") + + config_file = "fish_install_test.yaml" + + # 检查配置文件是否存在 + if not os.path.exists(config_file): + print(f"错误: 找不到测试配置文件 {config_file}") + sys.exit(1) + + # 加载测试用例 + all_test_cases = load_test_cases(config_file) + if not all_test_cases: + print("错误: 没有找到有效的测试用例") + sys.exit(1) + + # 根据目标系统版本过滤测试用例 + if target_os_version: + test_cases = [tc for tc in all_test_cases if tc.get('target_os_version') == target_os_version] + if not test_cases: + # 如果没有找到特定于该系统的测试用例,则运行所有没有指定target_os_version的测试用例 + test_cases = [tc for tc in all_test_cases if 'target_os_version' not in tc] + if not test_cases: + print(f"错误: 没有找到适用于系统版本 {target_os_version} 的测试用例") + sys.exit(1) + else: + # 如果没有指定目标系统版本,则运行所有没有指定target_os_version的测试用例 + test_cases = [tc for tc in all_test_cases if 'target_os_version' not in tc] + if not test_cases: + print("错误: 没有找到适用于所有系统的通用测试用例") + sys.exit(1) + + print(f"共找到 {len(test_cases)} 个适用于当前系统版本的测试用例") + + # 运行所有测试用例并收集结果 + results = [] + passed = 0 + failed = 0 + + for i, test_case in enumerate(test_cases): + print("\n--- 测试用例 {}/{} ---".format(i+1, len(test_cases))) + success, output = run_install_test(test_case) + case_name = test_case.get('name', 'Test Case {}'.format(i+1)) + + result = { + "name": case_name, + "success": success, + "output": output + } + results.append(result) + + if success: + passed += 1 + else: + failed += 1 + # 在测试用例之间添加延迟,避免系统资源冲突 + time.sleep(2) + + # 生成详细的测试报告 + report = { + "summary": { + "total": len(test_cases), + "passed": passed, + "failed": failed + }, + "details": results + } + + # 将报告保存为 JSON 文件 + report_file = "test_report.json" + try: + with open(report_file, 'w', encoding='utf-8') as f: + json.dump(report, f, ensure_ascii=False, indent=2) + print("\n详细测试报告已保存至: {}".format(report_file)) + except Exception as e: + print("保存测试报告失败: {}".format(e)) + + # 生成HTML格式的测试报告 + html_report_file = "test_report.html" + try: + generate_html_report(report, html_report_file) + print("HTML测试报告已保存至: {}".format(html_report_file)) + except Exception as e: + print("生成HTML测试报告失败: {}".format(e)) + + # 输出测试结果摘要 + print("\n=== 测试结果摘要 ===") + print(f"通过: {passed}") + print(f"失败: {failed}") + print(f"总计: {len(test_cases)}") + + if failed > 0: + print("部分测试失败,请检查日志和测试报告。") + sys.exit(1) + else: + print("所有测试通过!") + sys.exit(0) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tools/base.py b/tools/base.py index 22ae6b9..b432863 100644 --- a/tools/base.py +++ b/tools/base.py @@ -33,7 +33,9 @@ def __init__(self,record_file=None): self.record_input_queue = Queue() self.record_file = record_file - if self.record_file==None: self.record_file = "./fish_install.yaml" + if self.record_file==None: + # 首先检查环境变量 + self.record_file = os.environ.get('FISH_INSTALL_CONFIG', "./fish_install.yaml") self.default_input_queue = self.get_default_queue(self.record_file) def record_input(self,item): @@ -62,12 +64,16 @@ def gen_config_file(self): # 检查目标文件是否存在 if os.path.exists(target_path): - print("检测到已存在的配置文件: {}".format(target_path)) - user_input = input("是否替换该文件?[y/N]: ") - if user_input.lower() not in ['y', 'yes']: - print("取消替换,保留原配置文件") - os.remove(temp_path) # 删除临时文件 - return + # 在自动化测试环境中,直接覆盖原配置文件 + if os.environ.get('GITHUB_ACTIONS') == 'true': + print("检测到GitHub Actions环境,直接覆盖已存在的配置文件: {}".format(target_path)) + else: + print("检测到已存在的配置文件: {}".format(target_path)) + user_input = input("是否替换该文件?[y/N]: ") + if user_input.lower() not in ['y', 'yes']: + print("取消替换,保留原配置文件") + os.remove(temp_path) # 删除临时文件 + return # 先尝试删除目标文件(如果存在),避免mv命令的交互提示 if os.path.exists(target_path): @@ -130,8 +136,6 @@ def get_default_queue(self,param_file_path): else: config_yaml = yaml.load(config_data) - for choose in config_yaml['chooses']: - choose_queue.put(choose) for choose in config_yaml['chooses']: choose_queue.put(choose) @@ -1112,8 +1116,17 @@ def __choose(data,tips,array): choose = str(choose_item['choose']) PrintUtils.print_text(tr.tr("为您从配置文件找到默认选项:")+str(choose_item)) else: - choose = input(tr.tr("请输入[]内的数字以选择:")) - choose_item = None + try: + choose = input(tr.tr("请输入[]内的数字以选择:")) + choose_item = None + except EOFError: + # 在自动化测试环境中,input()可能会引发EOFError + # 如果是从配置文件读取的选项,则使用它,否则返回默认值0(退出) + if choose_item: + choose = str(choose_item['choose']) + else: + choose = "0" # 默认选择退出 + PrintUtils.print_text(tr.tr("检测到自动化环境,使用默认选项: {}").format(choose)) # Input From Queue if choose.isdecimal() : if (int(choose) in dic.keys() ) or (int(choose)==0): @@ -1163,8 +1176,17 @@ def __choose(data,tips,array,categories): choose_id = str(choose_item['choose']) print(tr.tr("为您从配置文件找到默认选项:")+str(choose_item)) else: - choose_id = input(tr.tr("请输入[]内的数字以选择:")) - choose_item = None + try: + choose_id = input(tr.tr("请输入[]内的数字以选择:")) + choose_item = None + except EOFError: + # 在自动化测试环境中,input()可能会引发EOFError + # 如果是从配置文件读取的选项,则使用它,否则返回默认值0(退出) + if choose_item: + choose_id = str(choose_item['choose']) + else: + choose_id = "0" # 默认选择退出 + PrintUtils.print_text(tr.tr("检测到自动化环境,使用默认选项: {}").format(choose_id)) # Input From Queue if choose_id.isdecimal() : if int(choose_id) in tool_ids : @@ -1260,10 +1282,17 @@ def new(path,name=None,data=''): CmdTask("sudo mkdir -p {}".format(path),3).run() if name!=None: # 使用临时文件和sudo权限来创建受保护的文件 - temp_file = "/tmp/{}".format(name) - with open(temp_file, "w") as f: - f.write(data) - CmdTask("sudo mv {} {}".format(temp_file, path+name), 3).run() + # 修复:使用 uuid 生成唯一临时文件名,避免权限冲突 + import uuid + temp_file = "/tmp/{}_{}".format(uuid.uuid4(), name) + try: + with open(temp_file, "w") as f: + f.write(data) + CmdTask("sudo mv {} {}".format(temp_file, path+name), 3).run() + finally: + # 确保临时文件被清理 + if os.path.exists(temp_file): + os.remove(temp_file) return True @staticmethod diff --git a/tools/tool_config_system_source.py b/tools/tool_config_system_source.py index b9ba0a4..82ed7e1 100644 --- a/tools/tool_config_system_source.py +++ b/tools/tool_config_system_source.py @@ -88,8 +88,6 @@ def clean_old_source(self): dic_source_method = {1:"自动测速选择最快的源", 2:"根据测速结果手动选择源"} self.source_method_code, _ = ChooseTask(dic_source_method, "请选择源的选择方式").run() - - def get_source_by_system(self,system,codename,arch,failed_sources=[], return_all=False): # 实际测试发现,阿里云虽然延时很低,但是带宽也低的离谱,一点都不用心,删掉了 ubuntu_amd64_sources = [ @@ -170,9 +168,6 @@ def get_source_by_system(self,system,codename,arch,failed_sources=[], return_all else: return fast_source[0],template return None,None - - - for source in sources: if "tsinghua" in source: @@ -235,8 +230,6 @@ def get_source_by_system(self,system,codename,arch,failed_sources=[], return_all return source, template - - # 去除末尾的斜杠 if source.endswith("/"): source = source[:-1] @@ -291,6 +284,7 @@ def replace_source(self,failed_sources=[]): else: PrintUtils.print_success('为您选择最快镜像源:{}'.format(source)) + # 使用已修复的 FileUtils.new 方法 FileUtils.new('/etc/apt/','sources.list',template.replace("",codename).replace('',source)) return source diff --git a/tools/translation/translator.py b/tools/translation/translator.py index 50eeb57..5c6ce98 100644 --- a/tools/translation/translator.py +++ b/tools/translation/translator.py @@ -8,6 +8,8 @@ import os import tools.base from tools.base import CmdTask +import subprocess +import time _suported_languages = ['zh_CN', 'en_US'] url_prefix = os.environ.get('FISHROS_URL','http://mirror.fishros.com/install') @@ -33,7 +35,19 @@ def __init__(self): # Create directory for downloads CmdTask("mkdir -p /tmp/fishinstall/tools/translation/assets").run() for lang in _suported_languages: - CmdTask("wget {} -O /tmp/fishinstall/{} --no-check-certificate".format(lang_url.format(lang), lang_url.format(lang).replace(url_prefix, ''))).run() + # Add timeout and retry mechanism for downloading language files + # Use /tmp/ directory directly to avoid permission issues + temp_file = "/tmp/fishros_lang_{}.py".format(lang) + final_path = "/tmp/fishinstall/{}".format(lang_url.format(lang).replace(url_prefix, '')) + download_cmd = "wget {} -O {} --no-check-certificate --timeout=10 --tries=3".format(lang_url.format(lang), temp_file) + result = CmdTask(download_cmd).run() + # Move file to final destination if download was successful + if result[0] == 0: + CmdTask("mkdir -p $(dirname {})".format(final_path)).run() + CmdTask("mv {} {}".format(temp_file, final_path)).run() + else: + # Clean up temp file if download failed + CmdTask("rm -f {}".format(temp_file)).run() self.loadTranslationFile() tools.base.tr = self @@ -62,20 +76,30 @@ def isCN(self) -> bool: def getLocalFromIP(self) -> str: local_str = "" + temp_file = "/tmp/fishros_check_country.json" try: - os.system("""wget --header="Accept: application/json" --no-check-certificate "https://ip.renfei.net/" -O /tmp/fishros_check_country.json -qq""") - with open('/tmp/fishros_check_country.json', 'r') as json_file: - data = json.loads(json_file.read()) - self.ip_info = data - self.country = data['location']['countryCode'] - if data['location']['countryCode'] in COUNTRY_CODE_MAPPING: - local_str = COUNTRY_CODE_MAPPING[data['location']['countryCode']] - else: - local_str = "en_US" + # Add timeout for IP detection + result = subprocess.run(["wget", "--header=Accept: application/json", "--no-check-certificate", + "https://ip.renfei.net/", "-O", temp_file, "-qq", "--timeout=10"], + capture_output=True, text=True, timeout=15) + if result.returncode == 0: + with open(temp_file, 'r') as json_file: + data = json.loads(json_file.read()) + self.ip_info = data + self.country = data['location']['countryCode'] + if data['location']['countryCode'] in COUNTRY_CODE_MAPPING: + local_str = COUNTRY_CODE_MAPPING[data['location']['countryCode']] + else: + local_str = "en_US" + else: + local_str = "en_US" except Exception: local_str = "en_US" finally: - os.system("rm -f /tmp/fishros_check_country.json") + try: + os.remove(temp_file) + except: + pass return local_str