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 = """
+
+
+
+
+
+ 一键安装工具测试报告
+
+
+
+
+
+
+
测试摘要
+
总计: """ + 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 = """
+
+
+
+
+
+ 一键安装工具测试报告
+
+
+
+
+
+
+
测试摘要
+
总计: """ + 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 :