diff --git a/.github/workflows/test-install.yml b/.github/workflows/test-install.yml new file mode 100644 index 0000000..8979d8e --- /dev/null +++ b/.github/workflows/test-install.yml @@ -0,0 +1,47 @@ +name: Test Install Script + +on: + push: + branches: [ dev, master ] + pull_request: + branches: [ dev, master ] + +jobs: + test-install: + strategy: + matrix: + os: [ubuntu-18.04, ubuntu-20.04, ubuntu-22.04, ubuntu-24.04, ubuntu-25.10] + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.8' + + - name: Install system dependencies + run: | + sudo apt update + sudo apt install -y python3-yaml python3-distro + + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip + pip install pyyaml distro + + - name: Run tests + run: | + cd tests && python3 test_runner.py + + - name: Upload test reports + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-reports-${{ matrix.os }} + path: | + tests/test_report.json + tests/test_report.html + if-no-files-found: ignore \ No newline at end of file diff --git a/.gitignore b/.gitignore index fd20fdd..7e99e36 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ - -*.pyc +*.pyc \ 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..5853d9e 100644 --- a/install.py +++ b/install.py @@ -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': + 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/fish_install_test.yaml b/tests/fish_install_test.yaml new file mode 100644 index 0000000..563a4d9 --- /dev/null +++ b/tests/fish_install_test.yaml @@ -0,0 +1,13 @@ +# 测试配置文件,用于 GitHub Actions 自动化测试 +# 格式: chooses: [{choose: <选项ID>, desc: <选项描述>}] + +# 测试用例 1: 安装 ROS +- name: "Install ROS" + chooses: + - { choose: 1, desc: "一键安装(推荐):ROS(支持ROS/ROS2,树莓派Jetson)" } + - { choose: 1, desc: "更换系统源再继续安装" } + - { choose: 2, desc: "更换系统源并清理第三方源" } + - { choose: 1, desc: "自动测速选择最快的源" } + - { choose: 1, desc: "中科大镜像源 (推荐国内用户使用)" } + - { choose: 1, desc: "humble(ROS2)" } + - { choose: 1, desc: "桌面端完整版" } \ No newline at end of file diff --git a/tests/generate_report.py b/tests/generate_report.py new file mode 100644 index 0000000..3d8ee60 --- /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 += f""" +
+
+ {test_case["name"]} + {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(f"HTML测试报告已生成: {html_report_file}") + else: + print(f"找不到测试报告文件: {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..1e70a06 --- /dev/null +++ b/tests/test_runner.py @@ -0,0 +1,383 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import yaml +import subprocess +import os +import sys +import time +import json +import re + +# 将项目根目录添加到 Python 路径中,以便能找到 tools 模块 +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) + +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(f"加载测试配置文件失败: {e}") + return [] + +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 += f""" +
+
+ {test_case["name"]} + {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(f"开始测试: {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(f"已创建临时配置文件: {temp_config}") + except Exception as e: + print(f"创建临时配置文件失败: {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(f"已备份原始配置文件至: {backup_config}") + except Exception as e: + print(f"备份原始配置文件失败: {e}") + # 即使备份失败也继续执行,因为我们会在最后恢复 + + # 将临时配置文件复制为当前配置文件和/tmp/fishinstall/tools/fish_install.yaml + try: + import shutil + shutil.copy(temp_config, original_config) + print(f"已将临时配置文件复制为: {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(f"已将临时配置文件复制为: {fishinstall_config}") + except Exception as e: + print(f"复制配置文件失败: {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 + 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'} + ) + + # 等待进程结束 (设置一个更长的超时时间,例如600秒=10分钟) + stdout, _ = process.communicate(timeout=600) + 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} (超过600秒)") + # 终止进程 + 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(): + """主函数""" + config_file = "fish_install_test.yaml" + + # 检查配置文件是否存在 + if not os.path.exists(config_file): + print(f"错误: 找不到测试配置文件 {config_file}") + sys.exit(1) + + # 加载测试用例 + test_cases = load_test_cases(config_file) + 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(f"\n--- 测试用例 {i+1}/{len(test_cases)} ---") + success, output = run_install_test(test_case) + case_name = test_case.get('name', f'Test Case {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(f"\n详细测试报告已保存至: {report_file}") + except Exception as e: + print(f"保存测试报告失败: {e}") + + # 生成HTML格式的测试报告 + html_report_file = "test_report.html" + try: + generate_html_report(report, html_report_file) + print(f"HTML测试报告已保存至: {html_report_file}") + except Exception as e: + print(f"生成HTML测试报告失败: {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() diff --git a/tools/base.py b/tools/base.py index 22ae6b9..c1f2b69 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): @@ -130,8 +132,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 +1112,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 +1172,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 :