From 628b9e264151fee069605f91dfe64ec697f4438b Mon Sep 17 00:00:00 2001 From: dennis <488132230@qq.com> Date: Mon, 4 Aug 2025 21:22:05 +0800 Subject: [PATCH 01/18] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E6=BA=90=E9=85=8D=E7=BD=AE=E5=92=8Crosdep=E4=B8=80?= =?UTF-8?q?=E9=94=AE=E5=AE=89=E8=A3=85=E5=B7=A5=E5=85=B7=E7=BD=91=E7=BB=9C?= =?UTF-8?q?=E9=80=89=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 增强rosdep工具: - 添加pip源选择功能功能 2. 改进系统源配置工具: - 添加源选择方式说明 - 提供两种源选择方法(自动选择最快源/手动选择源) - 扩展get_source_by_system函数支持返回所有源 这些改进提高了安装脚本的用户体验和网络适应性,特别是在网络条件不稳定的环境中。 --- tools/tool_config_rosdep.py | 76 +++++++++++++- tools/tool_config_system_source.py | 154 +++++++++++++++++++++++++++-- 2 files changed, 218 insertions(+), 12 deletions(-) diff --git a/tools/tool_config_rosdep.py b/tools/tool_config_rosdep.py index 7216dfe..e0e357c 100644 --- a/tools/tool_config_rosdep.py +++ b/tools/tool_config_rosdep.py @@ -1,4 +1,7 @@ # -*- coding: utf-8 -*- +import time +import http.client +from urllib.parse import urlparse from .base import BaseTool from .base import PrintUtils,CmdTask,FileUtils,AptUtils,ChooseTask from .base import osversion @@ -9,13 +12,78 @@ def __init__(self): self.type = BaseTool.TYPE_CONFIG self.name = "模板工程" self.author = '小鱼' + + def test_source_speed(self, url): + """测试源的速度 + + Args: + url (str): 源的URL + + Returns: + float: 响应时间(秒) + """ + try: + start_time = time.time() + parsed_url = urlparse(url) + conn = http.client.HTTPSConnection(parsed_url.netloc, timeout=5) + conn.request("HEAD", parsed_url.path) + resp = conn.getresponse() + conn.close() + end_time = time.time() + return end_time - start_time + except Exception as e: + PrintUtils.print_error(f"测试源 {url} 失败: {str(e)}") + return float('inf') # 返回无穷大表示连接失败 + + def choose_pip_source(self): + """选择pip源 + + Returns: + str: 选择的源URL + """ + sources = { + "清华源": "https://pypi.tuna.tsinghua.edu.cn/simple", + "阿里云": "https://mirrors.aliyun.com/pypi/simple", + "中国科技大学": "https://pypi.mirrors.ustc.edu.cn/simple", + "华为云": "https://repo.huaweicloud.com/repository/pypi/simple" + } + + # 构建选择字典 + choose_dict = {} + i = 1 + for name, url in sources.items(): + choose_dict[i] = f"{name} - {url}" + i += 1 + + PrintUtils.print_info("请选择要使用的pip源:") + choose_index, choose_content = ChooseTask(choose_dict, "请选择pip源:").run() + + if choose_index == 0: + PrintUtils.print_info("未选择源,默认使用中国科技大学源") + return sources["中国科技大学"] + + try: + # choose_index 已经是整数,直接使用 + selected_name = list(sources.keys())[choose_index-1] + return sources[selected_name] + except (IndexError) as e: + PrintUtils.print_error(f"选择源时出错: {str(e)},使用中国科技大学源") + return sources["中国科技大学"] def install_rosdepc(self): CmdTask("sudo apt install python3-pip -y", 0).run() - cmd_ret = CmdTask("sudo pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple rosdepc").run() + + # 选择源 + selected_source = self.choose_pip_source() + PrintUtils.print_success(f"您选择了: {selected_source}") + + # 直接使用 --break-system-packages 参数安装,避免第一次安装失败 + PrintUtils.print_info(f"正在使用 {selected_source} 安装 rosdepc...") + cmd_ret = CmdTask(f"sudo pip3 install -i {selected_source} rosdepc --break-system-packages").run() if cmd_ret[0]!=0: - # fix: https://fishros.org.cn/forum/topic/2981 - cmd_ret = CmdTask("sudo pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple rosdepc --break-system-packages").run() + # 如果安装失败,尝试不带参数安装 + PrintUtils.print_warning("安装失败,尝试使用其他方式安装...") + cmd_ret = CmdTask(f"sudo pip3 install -i {selected_source} rosdepc").run() CmdTask("sudo rosdepc init", 0).run() CmdTask("sudo rosdepc fix-permissions", 0).run() PrintUtils.print_info('已为您安装好rosdepc,请使用:\nrosdepc update \n进行测试更新,最后欢迎关注微信公众号《鱼香ROS》') @@ -23,4 +91,4 @@ def install_rosdepc(self): def run(self): #正式的运行 - self.install_rosdepc() + self.install_rosdepc() \ No newline at end of file diff --git a/tools/tool_config_system_source.py b/tools/tool_config_system_source.py index df618f5..b9ba0a4 100644 --- a/tools/tool_config_system_source.py +++ b/tools/tool_config_system_source.py @@ -79,10 +79,18 @@ def clean_old_source(self): FileUtils.delete('/etc/apt/sources.list.d') # fix add source failed before config system source CmdTask('sudo mkdir -p /etc/apt/sources.list.d').run() + + # 添加选择源的方式 + PrintUtils.print_info("源选择方式说明:") + PrintUtils.print_info("1. 自动测速选择最快的源: 系统将自动测试各个源的速度,并选择最快的源") + PrintUtils.print_info("2. 根据测速结果手动选择源: 系统将测试各个源的速度,然后让您从测试结果中选择") + + 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=[]): + def get_source_by_system(self,system,codename,arch,failed_sources=[], return_all=False): # 实际测试发现,阿里云虽然延时很低,但是带宽也低的离谱,一点都不用心,删掉了 ubuntu_amd64_sources = [ "https://mirrors.tuna.tsinghua.edu.cn/ubuntu", @@ -143,6 +151,15 @@ def get_source_by_system(self,system,codename,arch,failed_sources=[]): PrintUtils.print_delay('接下来将进行自动测速以为您选择最快的源:') fast_source = AptUtils.get_fast_url(sources) + # 如果需要返回所有源和模板(不进行测速) + if return_all: + # 直接返回源列表和模板 + if len(failed_sources) > 0: + filtered_sources = [source for source in sources if source not in failed_sources] + return filtered_sources, template + return sources, template + + # 正常返回最快的源 if len(failed_sources)>0: PrintUtils.print_warn('接下来为您排除已经失败的源') for source in fast_source: @@ -155,6 +172,77 @@ def get_source_by_system(self,system,codename,arch,failed_sources=[]): return None,None + + + for source in sources: + if "tsinghua" in source: + tsinghua_sources.append(source) + elif "ustc" in source: + ustc_sources.append(source) + elif "archive.ubuntu" in source or "deb.debian" in source: + official_sources.append(source) + elif "kernel" in source: + kernel_sources.append(source) + elif "aliyun" in source: + aliyun_sources.append(source) + else: + other_sources.append(source) + + # 添加到选择字典 + index = 1 + + # 添加清华源 + if tsinghua_sources: + for source in tsinghua_sources: + source_dict[index] = source + index += 1 + + # 添加中科大源 + if ustc_sources: + for source in ustc_sources: + source_dict[index] = source + index += 1 + + # 添加阿里云源 + if aliyun_sources: + for source in aliyun_sources: + source_dict[index] = source + index += 1 + + # 添加官方源 + if official_sources: + for source in official_sources: + source_dict[index] = source + index += 1 + + # 添加kernel源 + if kernel_sources: + for source in kernel_sources: + source_dict[index] = source + index += 1 + + # 添加其他源 + if other_sources: + for source in other_sources: + source_dict[index] = source + index += 1 + + PrintUtils.print_info("请选择您想使用的镜像源:") + code, source = ChooseTask(source_dict, "请选择一个镜像源").run() + + if not source: + return None, None + + return source, template + + + + # 去除末尾的斜杠 + if source.endswith("/"): + source = source[:-1] + + return source, template + def replace_source(self,failed_sources=[]): arch = AptUtils.getArch() name = osversion.get_name() @@ -165,10 +253,44 @@ def replace_source(self,failed_sources=[]): system = 'debian' else: return None + PrintUtils.print_delay('检测到当前系统:{} 架构:{} 代号:{},正在为你搜索适合的源...'.format(system,arch,codename)) - source,template = self.get_source_by_system(system,codename,arch,failed_sources) + + # 根据用户选择的方式获取源 + if hasattr(self, 'source_method_code'): + if self.source_method_code == 2: + # 根据测速结果手动选择源 + sorted_sources, template = self.get_source_by_system(system, codename, arch, failed_sources, return_all=True) + if not sorted_sources: + return None + + # 创建选择字典 + source_dict = {} + for i, src in enumerate(sorted_sources, 1): + source_dict[i] = src + + PrintUtils.print_info("请从测速结果中选择您想使用的镜像源:") + code, source = ChooseTask(source_dict, "请选择一个镜像源").run() + + if not source: + return None + else: + # 自动测速选择源 + source, template = self.get_source_by_system(system, codename, arch, failed_sources) + else: + # 自动测速选择源 + source, template = self.get_source_by_system(system, codename, arch, failed_sources) + if not source: return None - PrintUtils.print_success('为您选择最快镜像源:{}'.format(source)) + + if hasattr(self, 'source_method_code'): + if self.source_method_code == 2: + PrintUtils.print_success('您选择的镜像源:{}'.format(source)) + else: + PrintUtils.print_success('为您选择最快镜像源:{}'.format(source)) + else: + PrintUtils.print_success('为您选择最快镜像源:{}'.format(source)) + FileUtils.new('/etc/apt/','sources.list',template.replace("",codename).replace('',source)) return source @@ -181,11 +303,27 @@ def change_sys_source(self): if source: PrintUtils.print_delay("替换镜像源完成,尝试进行更新....") result = CmdTask('sudo apt update',100).run() - while result[0]!= 0: - failed_sources.append(source) - PrintUtils.print_warn("更新失败,尝试更换其他源") - source = self.replace_source(failed_sources) - result = CmdTask('sudo apt update',100).run() + + # 如果是手动选择源且更新失败,提示用户重新选择 + if result[0] != 0 and hasattr(self, 'source_method_code') and self.source_method_code == 2: + PrintUtils.print_warn("您选择的源更新失败,请重新选择其他源") + while result[0] != 0: + failed_sources.append(source) + source = self.replace_source(failed_sources) + if not source: + PrintUtils.print_error("没有找到合适的镜像源,臣妾告退!") + return + result = CmdTask('sudo apt update',100).run() + # 如果是自动测速选择源且更新失败,自动尝试其他源 + elif result[0] != 0: + while result[0] != 0: + failed_sources.append(source) + PrintUtils.print_warn("更新失败,尝试更换其他源") + source = self.replace_source(failed_sources) + if not source: + PrintUtils.print_error("没有找到合适的镜像源,臣妾告退!") + return + result = CmdTask('sudo apt update',100).run() else: PrintUtils.print_error("没有找到合适的镜像源,臣妾告退!") From d1a9315fefacb50ac660e2d20a3100aabbc51fd8 Mon Sep 17 00:00:00 2001 From: Dennis <488132230@qq.com> Date: Fri, 29 Aug 2025 13:27:05 +0800 Subject: [PATCH 02/18] =?UTF-8?q?feat(tools):=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E4=B8=AD=E7=A7=91=E5=A4=A7=20ROS=20=E9=95=9C?= =?UTF-8?q?=E5=83=8F=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改 base.py 中的文件写入逻辑,使用临时文件和 sudo 权限 - 优化 tool_install_ros.py 中的ROS镜像源选择逻辑,让用户可以自行选择ROS镜像源 --- tools/base.py | 51 ++++++++- tools/tool_install_ros.py | 231 +++++++++++++++++++++++++------------- 2 files changed, 200 insertions(+), 82 deletions(-) diff --git a/tools/base.py b/tools/base.py index bd2f357..ae4ef98 100644 --- a/tools/base.py +++ b/tools/base.py @@ -52,9 +52,49 @@ def gen_config_file(self): config_yaml['chooses'] = chooses config_yaml['time'] = str(time.time()) - with open("/tmp/fish_install.yaml", "w", encoding="utf-8") as f: - if have_yaml_module: - yaml.dump(config_yaml, f,allow_unicode=True) + # 先写入临时文件,再使用sudo移动到目标位置 + temp_path = "/tmp/fish_install_temp.yaml" + target_path = "/tmp/fish_install.yaml" + try: + with open(temp_path, "w", encoding="utf-8") as f: + if have_yaml_module: + yaml.dump(config_yaml, f, allow_unicode=True) + + # 检查目标文件是否存在 + 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 + + # 先尝试删除目标文件(如果存在),避免mv命令的交互提示 + if os.path.exists(target_path): + try: + os.remove(target_path) + except PermissionError: + # 如果普通权限无法删除,则使用sudo + print("使用sudo权限删除已存在的配置文件...") + os.system("sudo rm -f {}".format(target_path)) + + # 使用mv命令移动文件,避免权限问题 + result = os.system("mv {} {}".format(temp_path, target_path)) + if result == 0: + print("配置文件已保存至: {}".format(target_path)) + else: + # 如果普通权限移动失败,则尝试使用sudo + print("尝试使用sudo权限保存配置文件...") + result = os.system("sudo mv {} {}".format(temp_path, target_path)) + if result == 0: + print("配置文件已保存至: {} (使用sudo权限)".format(target_path)) + else: + print("配置文件保存失败") + except Exception as e: + print("配置文件生成过程中发生错误: {}".format(str(e))) + # 清理临时文件(如果存在) + if os.path.exists(temp_path): + os.remove(temp_path) def get_input_value(self): if self.default_input_queue.qsize()>0: @@ -1219,8 +1259,11 @@ def new(path,name=None,data=''): if not os.path.exists(path): CmdTask("sudo mkdir -p {}".format(path),3).run() if name!=None: - with open(path+name,"w") as f: + # 使用临时文件和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() return True @staticmethod diff --git a/tools/tool_install_ros.py b/tools/tool_install_ros.py index 2fa6376..a008942 100644 --- a/tools/tool_install_ros.py +++ b/tools/tool_install_ros.py @@ -86,6 +86,7 @@ def get_desktop_version(name): ros_mirror_dic = { "tsinghua":{"ROS1":"http://mirrors.tuna.tsinghua.edu.cn/ros/ubuntu/","ROS2":"http://mirrors.tuna.tsinghua.edu.cn/ros2/ubuntu/"}, + "ustc":{"ROS1":"https://mirrors.ustc.edu.cn/ros/ubuntu/","ROS2":"https://mirrors.ustc.edu.cn/ros2/ubuntu/"}, "huawei":{"ROS1":"https://repo.huaweicloud.com/ros/ubuntu/","ROS2":"https://repo.huaweicloud.com/ros2/ubuntu/"}, "packages.ros":{"ROS1":"http://packages.ros.org/ros/ubuntu/","ROS2":"http://packages.ros.org/ros2/ubuntu/"}, "https.packages.ros":{"ROS1":"https://packages.ros.org/ros/ubuntu/","ROS2":"https://packages.ros.org/ros2/ubuntu/"}, @@ -95,13 +96,13 @@ def get_desktop_version(name): ros_dist_dic = { 'artful':{"packages.ros"}, - 'bionic':{"tsinghua","huawei","packages.ros","https.packages.ros"}, + 'bionic':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, 'buster':{"packages.ros"}, 'cosmic':{"packages.ros"}, 'disco':{"packages.ros"}, 'eoan':{"packages.ros"}, - 'focal':{"tsinghua","huawei","packages.ros","https.packages.ros"}, - 'jessie':{"tsinghua","huawei","packages.ros","https.packages.ros"}, + 'focal':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, + 'jessie':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, 'lucid':{"packages.ros"}, 'maverick':{"packages.ros"}, 'natty':{"packages.ros"}, @@ -110,32 +111,46 @@ def get_desktop_version(name): 'quantal':{"packages.ros"}, 'raring':{"packages.ros"}, 'saucy':{"packages.ros"}, - 'stretch':{"tsinghua","huawei","packages.ros","https.packages.ros"}, - 'trusty':{"tsinghua","huawei","packages.ros","https.packages.ros"}, + 'stretch':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, + 'trusty':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, 'utopic':{"packages.ros"}, 'vivid':{"packages.ros"}, 'wheezy':{"packages.ros"}, 'wily':{"packages.ros"}, - 'xenial':{"tsinghua","huawei","packages.ros","https.packages.ros"}, + 'xenial':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, 'yakkety':{"packages.ros"}, 'zesty':{"packages.ros"}, + 'focal':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, + 'jammy':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, + 'kinetic':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, + 'lunar':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, + 'melodic':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, + 'noble':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, + 'trusty':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, + 'utopic':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, + 'xenial':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, + 'yakkety':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, + 'zesty':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, } ros2_dist_dic = { - 'bionic':{"tsinghua","huawei","packages.ros","https.packages.ros"}, - 'bullseye':{"tsinghua","huawei","packages.ros","https.packages.ros"}, + 'bionic':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, + 'bullseye':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, 'buster':{"packages.ros"}, - 'cosmic':{"tsinghua","huawei","packages.ros","https.packages.ros"}, - 'disco':{"tsinghua","huawei","packages.ros","https.packages.ros"}, - 'eoan':{"tsinghua","huawei","packages.ros","https.packages.ros"}, - 'focal':{"tsinghua","huawei","packages.ros","https.packages.ros"}, - 'jessie':{"tsinghua","huawei"}, - 'jammy':{"tsinghua","huawei","packages.ros","https.packages.ros"}, - 'noble':{"tsinghua","huawei","packages.ros","https.packages.ros"}, - 'stretch':{"tsinghua","huawei","packages.ros","https.packages.ros"}, - 'trusty':{"tsinghua","huawei"}, - 'xenial':{"tsinghua","huawei","packages.ros","https.packages.ros"}, + 'cosmic':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, + 'disco':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, + 'eoan':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, + 'focal':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, + 'jessie':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, + 'jammy':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, + 'noble':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, + 'stretch':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, + 'trusty':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, + 'utopic':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, + 'xenial':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, + 'yakkety':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, + 'zesty':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, } @@ -151,11 +166,11 @@ def get_mirror_by_code(self,code,arch='amd64',first_choose="tsinghua"): """ 获取镜像通过系统版本号 """ - ros1_choose_queue = [first_choose,"tsinghua","huawei","packages.ros"] - ros2_choose_queue = [first_choose,"tsinghua","huawei","packages.ros"] + ros1_choose_queue = [first_choose,"tsinghua","ustc","huawei","packages.ros"] + ros2_choose_queue = [first_choose,"tsinghua","ustc","huawei","packages.ros"] # armhf架构,优先使用官方源 - if arch=='armhf': ros2_choose_queue =["packages.ros","tsinghua","huawei"] + if arch=='armhf': ros2_choose_queue =["packages.ros","tsinghua","ustc","huawei"] mirror = [] # 确认源里有对应的系统的,比如jammy @@ -174,61 +189,62 @@ def get_mirror_by_code(self,code,arch='amd64',first_choose="tsinghua"): # mirror.append(ros_mirror_dic['packages.ros']['ROS2']) return mirror - - def add_key(self): - PrintUtils.print_success(tr.tr('============正在添加ROS源密钥=================')) - # check apt - if not AptUtils.checkapt(): - pass - # install dep - AptUtils.install_pkg('curl') - AptUtils.install_pkg('gnupg2') - - # add key - PrintUtils.print_success(tr.tr('正在挑选最快的密钥服务:{}').format(key_urls)) - key_url = AptUtils.get_fast_url(key_urls) - if not key_url: - PrintUtils.print_error(tr.tr("获取密钥失败")) - return - key_url = key_url[0] - PrintUtils.print_success(tr.tr('已自动选择最快密钥服务:{}').format(key_url)) - - cmd_result = CmdTask("curl -s {} | sudo apt-key add -".format(key_url)).run() - if cmd_result[0]!=0: - cmd_result = CmdTask("curl -s {} | sudo apt-key add -".format(key_url)).run() - # 针对近期密钥更新问题 - CmdTask("sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys F42ED6FBAB17C654",10).run() - if cmd_result[0]!=0: - PrintUtils.print_info(tr.tr("导入密钥失败,开始更换导入方式并二次尝试...")) - cmd_result = CmdTask("sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys F42ED6FBAB17C654",10).run() - - # 针对trusted.gpg.d问题解决方案 - if FileUtils.check_result(cmd_result,['trusted.gpg.d']): - cmd_result = CmdTask("curl -s {} | sudo gpg --no-default-keyring --keyring gnupg-ring:/etc/apt/trusted.gpg.d/ros.gpg --import".format(key_url)).run() - cmd_result = CmdTask("sudo chmod 644 /etc/apt/trusted.gpg.d/ros.gpg",10).run() - - return cmd_result - - - def check_sys_source(self): - # 更换系统源 - dic = {1:"更换系统源再继续安装",2:"不更换继续安装"} - PrintUtils.print_warn("=========接下来这一步很很很很重要,如果不知道怎么选请选择1========") - code,result = ChooseTask(dic, "新手或首次安装一定要一定要一定要换源并清理三方源,换源!!!系统默认国外源容易失败!!").run() - if code==1: - tool = run_tool_file('tools.tool_config_system_source',authorun=False) - tool.change_sys_source() - - def get_all_instsll_ros_pkgs(self): - AptUtils.checkapt() - dic_base = AptUtils.search_package('ros-base','ros-[A-Za-z]+-ros-base',"ros-","-base") - if dic_base== None: return None - ros_name = {} - for a in dic_base.keys(): - ros_name[RosVersions.get_version_string(a)] = a - if len(ros_name) == 0: - return None - return ros_name + def select_mirror(self): + """ + 让用户选择镜像源 + """ + # 检查当前系统是否支持中科大镜像 + codename = osversion.get_codename() + supported_mirrors = [] + + if codename in ros_dist_dic.keys() or codename in ros2_dist_dic.keys(): + if "ustc" in ros_dist_dic.get(codename, []) or "ustc" in ros2_dist_dic.get(codename, []): + supported_mirrors.append("ustc") + + if codename in ros_dist_dic.keys() or codename in ros2_dist_dic.keys(): + if "tsinghua" in ros_dist_dic.get(codename, []) or "tsinghua" in ros2_dist_dic.get(codename, []): + supported_mirrors.append("tsinghua") + + if codename in ros_dist_dic.keys() or codename in ros2_dist_dic.keys(): + if "huawei" in ros_dist_dic.get(codename, []) or "huawei" in ros2_dist_dic.get(codename, []): + supported_mirrors.append("huawei") + + # 如果系统支持多个镜像源,则让用户选择 + if len(supported_mirrors) > 1: + mirror_dict = {} + count = 1 + for mirror in supported_mirrors: + if mirror == "ustc": + mirror_dict[count] = "中科大镜像源 (推荐国内用户使用)" + elif mirror == "tsinghua": + mirror_dict[count] = "清华镜像源 (容易被封禁)" + elif mirror == "huawei": + mirror_dict[count] = "华为镜像源" + count += 1 + + mirror_dict[count] = "ROS官方源 (国外用户或需要最新版本时使用)" + + code, result = ChooseTask(mirror_dict, "检测到您的系统支持多个ROS镜像源,请选择您想要使用的ROS镜像源(默认清华):").run() + if code == 0: + return "tsinghua" # 默认返回清华源 + elif code == count: + return "packages.ros" # 官方源 + else: + # 根据选择返回对应的镜像源 + for key, value in mirror_dict.items(): + if key == code: + if "中科大" in value: + return "ustc" + elif "清华" in value: + return "tsinghua" + elif "华为" in value: + return "huawei" + else: + # 系统只支持默认的清华源 + PrintUtils.print_info("您的系统默认使用清华镜像源") + return "tsinghua" + + return "tsinghua" def add_source(self): """ @@ -237,9 +253,13 @@ def add_source(self): arch = AptUtils.getArch() if arch==None: return False + # 让用户选择镜像源 + selected_mirror = self.select_mirror() + PrintUtils.print_info("您选择的镜像源: {}".format(selected_mirror)) + #add source 1 - mirrors = self.get_mirror_by_code(osversion.get_codename(),arch=arch) - PrintUtils.print_info("根据您的系统,为您推荐安装源为{}".format(mirrors)) + mirrors = self.get_mirror_by_code(osversion.get_codename(),arch=arch,first_choose=selected_mirror) + PrintUtils.print_info("根据您的系统和选择,为您推荐安装源为{}".format(mirrors)) source_data = '' for mirror in mirrors: source_data += 'deb [arch={}] {} {} main\n'.format(arch,mirror,osversion.get_codename()) @@ -297,6 +317,61 @@ def add_source(self): if not AptUtils.checkapt(): PrintUtils.print_error("四次换源后都失败了,请及时联系小鱼获取解决方案并处理!") + def add_key(self): + PrintUtils.print_success(tr.tr('============正在添加ROS源密钥=================')) + # check apt + if not AptUtils.checkapt(): + pass + # install dep + AptUtils.install_pkg('curl') + AptUtils.install_pkg('gnupg2') + + # add key + PrintUtils.print_success(tr.tr('正在挑选最快的密钥服务:{}').format(key_urls)) + key_url = AptUtils.get_fast_url(key_urls) + if not key_url: + PrintUtils.print_error(tr.tr("获取密钥失败")) + return + key_url = key_url[0] + PrintUtils.print_success(tr.tr('已自动选择最快密钥服务:{}').format(key_url)) + + cmd_result = CmdTask("curl -s {} | sudo apt-key add -".format(key_url)).run() + if cmd_result[0]!=0: + cmd_result = CmdTask("curl -s {} | sudo apt-key add -".format(key_url)).run() + # 针对近期密钥更新问题 + CmdTask("sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys F42ED6FBAB17C654",10).run() + if cmd_result[0]!=0: + PrintUtils.print_info(tr.tr("导入密钥失败,开始更换导入方式并二次尝试...")) + cmd_result = CmdTask("sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys F42ED6FBAB17C654",10).run() + + # 针对trusted.gpg.d问题解决方案 + if FileUtils.check_result(cmd_result,['trusted.gpg.d']): + cmd_result = CmdTask("curl -s {} | sudo gpg --no-default-keyring --keyring gnupg-ring:/etc/apt/trusted.gpg.d/ros.gpg --import".format(key_url)).run() + cmd_result = CmdTask("sudo chmod 644 /etc/apt/trusted.gpg.d/ros.gpg",10).run() + + return cmd_result + + + def check_sys_source(self): + # 更换系统源 + dic = {1:"更换系统源再继续安装",2:"不更换继续安装"} + PrintUtils.print_warn("=========接下来这一步很很很很重要,如果不知道怎么选请选择1========") + code,result = ChooseTask(dic, "新手或首次安装一定要一定要一定要换源并清理三方源,换源!!!系统默认国外源容易失败!!").run() + if code==1: + tool = run_tool_file('tools.tool_config_system_source',authorun=False) + tool.change_sys_source() + + def get_all_instsll_ros_pkgs(self): + AptUtils.checkapt() + dic_base = AptUtils.search_package('ros-base','ros-[A-Za-z]+-ros-base',"ros-","-base") + if dic_base== None: return None + ros_name = {} + for a in dic_base.keys(): + ros_name[RosVersions.get_version_string(a)] = a + if len(ros_name) == 0: + return None + return ros_name + def support_install(self): # check support if (osversion.get_codename() not in ros_dist_dic.keys()) and (osversion.get_codename() not in ros2_dist_dic.keys()): From ade9adb7886d733b53f802ebd943f55c77f51402 Mon Sep 17 00:00:00 2001 From: Dennis <488132230@qq.com> Date: Fri, 29 Aug 2025 13:32:33 +0800 Subject: [PATCH 03/18] =?UTF-8?q?refactor(tools):=20=E4=BC=98=E5=8C=96=20R?= =?UTF-8?q?OS=20=E6=BA=90=E6=B7=BB=E5=8A=A0=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除多次固定源添加尝试,改为让用户重新选择镜像源 - 在首次尝试失败后,提示用户重新选择镜像源进行尝试 - 如果重新选择的镜像源成功添加,继续进行后续操作 - 如果用户选择相同的镜像源且四次尝试均失败,提示联系小鱼获取解决方案 --- tools/tool_install_ros.py | 66 +++++++++++++-------------------------- 1 file changed, 21 insertions(+), 45 deletions(-) diff --git a/tools/tool_install_ros.py b/tools/tool_install_ros.py index a008942..9b8654a 100644 --- a/tools/tool_install_ros.py +++ b/tools/tool_install_ros.py @@ -271,51 +271,27 @@ def add_source(self): PrintUtils.print_success("恭喜,成功添加ROS源,接下来可以使用apt安装ROS或者使用[1]一键安装ROS安装!") return - #add source2 - PrintUtils.print_warn("换源后更新失败,第二次开始切换源,尝试更换ROS2源为华为源!") - mirrors = self.get_mirror_by_code(osversion.get_codename(),arch=arch,first_choose="huawei") - PrintUtils.print_info("根据您的系统,为您推荐安装源为{}".format(mirrors)) - source_data = '' - for mirror in mirrors: - source_data += 'deb [arch={}] {} {} main\n'.format(arch,mirror,osversion.get_codename()) - FileUtils.delete('/etc/apt/sources.list.d/ros-fish.list') - FileUtils.new('/etc/apt/sources.list.d/',"ros-fish.list",source_data) - ros_pkg = self.get_all_instsll_ros_pkgs() - if ros_pkg and len(ros_pkg)>1: - PrintUtils.print_success("恭喜,成功添加ROS源,接下来可以使用apt安装ROS或者使用[1]一键安装ROS安装!") - return - - - PrintUtils.print_warn("换源后更新失败,第三次开始切换源,尝试使用https-ROS官方源~!") - mirrors = self.get_mirror_by_code(osversion.get_codename(),arch=arch,first_choose="https.packages.ros") - PrintUtils.print_info("根据您的系统,为您推荐安装源为{}".format(mirrors)) - source_data = '' - for mirror in mirrors: - source_data += 'deb [arch={}] {} {} main\n'.format(arch,mirror,osversion.get_codename()) - FileUtils.delete('/etc/apt/sources.list.d/ros-fish.list') - FileUtils.new('/etc/apt/sources.list.d/',"ros-fish.list",source_data) - ros_pkg = self.get_all_instsll_ros_pkgs() - if ros_pkg and len(ros_pkg)>1: - PrintUtils.print_success("恭喜,成功添加ROS源,接下来可以使用apt安装ROS或者使用[1]一键安装ROS安装!") - return - - #add source2 - PrintUtils.print_warn("换源后更新失败,第四次开始切换源,尝试更换ROS源为http-ROS官方源!") - mirrors = self.get_mirror_by_code(osversion.get_codename(),arch=arch,first_choose="packages.ros") - PrintUtils.print_info("根据您的系统,为您推荐安装源为{}".format(mirrors)) - source_data = '' - for mirror in mirrors: - source_data += 'deb [arch={}] {} {} main\n'.format(arch,mirror,osversion.get_codename()) - FileUtils.delete('/etc/apt/sources.list.d/ros-fish.list') - FileUtils.new('/etc/apt/sources.list.d/',"ros-fish.list",source_data) - ros_pkg = self.get_all_instsll_ros_pkgs() - if ros_pkg and len(ros_pkg)>1: - PrintUtils.print_success("恭喜,成功添加ROS源,接下来可以使用apt安装ROS或者使用[1]一键安装ROS安装!") - return - - # echo >>/etc/apt/apt.conf.d/99verify-peer.conf "Acquire { https::Verify-Peer false }" - if not AptUtils.checkapt(): PrintUtils.print_error("四次换源后都失败了,请及时联系小鱼获取解决方案并处理!") - + # 如果第一次尝试失败,让用户重新选择镜像源 + PrintUtils.print_warn("换源后更新失败,您可以重新选择镜像源再尝试!") + retry_mirror = self.select_mirror() + while retry_mirror != selected_mirror: + PrintUtils.print_info("您重新选择的镜像源: {}".format(retry_mirror)) + mirrors = self.get_mirror_by_code(osversion.get_codename(),arch=arch,first_choose=retry_mirror) + PrintUtils.print_info("根据您的系统和选择,为您推荐安装源为{}".format(mirrors)) + source_data = '' + for mirror in mirrors: + source_data += 'deb [arch={}] {} {} main\n'.format(arch,mirror,osversion.get_codename()) + FileUtils.delete('/etc/apt/sources.list.d/ros-fish.list') + FileUtils.new('/etc/apt/sources.list.d/',"ros-fish.list",source_data) + ros_pkg = self.get_all_instsll_ros_pkgs() + if ros_pkg and len(ros_pkg)>1: + PrintUtils.print_success("恭喜,成功添加ROS源,接下来可以使用apt安装ROS或者使用[1]一键安装ROS安装!") + return + else: + PrintUtils.print_warn("换源后更新失败,您可以重新选择镜像源再尝试!") + retry_mirror = self.select_mirror() + else: + PrintUtils.print_error("您选择了相同的镜像源,四次换源后都失败了,请及时联系小鱼获取解决方案并处理!") def add_key(self): PrintUtils.print_success(tr.tr('============正在添加ROS源密钥=================')) From 49657eb93f164370145334d8579f39bf4ec7dd69 Mon Sep 17 00:00:00 2001 From: Dennis <488132230@qq.com> Date: Fri, 29 Aug 2025 15:23:00 +0800 Subject: [PATCH 04/18] =?UTF-8?q?refactor(tools):=20=E4=BC=98=E5=8C=96=20R?= =?UTF-8?q?OS=20=E5=AE=89=E8=A3=85=E5=B7=A5=E5=85=B7=E7=9A=84=E9=80=80?= =?UTF-8?q?=E5=87=BA=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在多个 ROS 安装工具中统一退出操作的处理方式 - 使用 PrintUtils.print_error() 替换直接 print,提高错误信息的可读性 - 返回 False 以明确表示退出操作失败 - 涉及工具:tool_install_ros.py, tool_install_ros1_systemdefault.py, tool_install_ros_with_docker.py --- tools/tool_install_ros.py | 6 +++--- tools/tool_install_ros1_systemdefault.py | 4 ++-- tools/tool_install_ros_with_docker.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tools/tool_install_ros.py b/tools/tool_install_ros.py index 9b8654a..85acd84 100644 --- a/tools/tool_install_ros.py +++ b/tools/tool_install_ros.py @@ -380,13 +380,13 @@ def choose_and_install_ros(self): if code==0: PrintUtils.print_error("你选择退出") PrintUtils.print_delay('是因为没有自己想要的ROS版本吗?ROS版本和操作系统版本是有对应关系的哦,所以可能是你的系统版本{}不对!具体请查看:https://fishros.org.cn/forum/topic/96'.format(str(str(osversion.get_name())+str(osversion.get_version())))) - return + return False version_dic = {1:rosname+"桌面版",2:rosname+"基础版(小)"} code,name = ChooseTask(version_dic,"请选择安装的具体版本(如果不知道怎么选,请选1桌面版):",False).run() if code==0: - print("你选择退出。。。。") - return + PrintUtils.print_error("你选择退出。。。。") + return False install_tool = 'aptitude' install_tool_apt = 'apt' diff --git a/tools/tool_install_ros1_systemdefault.py b/tools/tool_install_ros1_systemdefault.py index ad4b83b..ee7ab47 100644 --- a/tools/tool_install_ros1_systemdefault.py +++ b/tools/tool_install_ros1_systemdefault.py @@ -31,8 +31,8 @@ def install_system_ros(self): code,name = ChooseTask(version_dic,"请选择安装的具体版本(如果不知道怎么选,请选1桌面版):",False).run() if code==0: - print("你选择退出。。。。") - return + PrintUtils.print_error("你选择退出。。。。") + return False install_tool = 'aptitude' install_tool_apt = 'apt' diff --git a/tools/tool_install_ros_with_docker.py b/tools/tool_install_ros_with_docker.py index 8be6c29..68ca263 100644 --- a/tools/tool_install_ros_with_docker.py +++ b/tools/tool_install_ros_with_docker.py @@ -132,7 +132,7 @@ def choose_image_version(self): code,result = ChooseTask(RosVersions.get_vesion_list(),"请选择你要安装的ROS版本名称(请注意ROS1和ROS2区别):",True).run() if code==0: PrintUtils.print_error("你选择退出。。。。") - return + return False version_info,rosname = RosVersions.get_version_string(result) PrintUtils.print_info("你选择了{}".format(version_info)) return rosname @@ -249,7 +249,7 @@ def install_use_tool(self): def install_ros_with_docker(self): rosname = self.choose_image_version() - if not rosname: return + if not rosname: return False self.install_docker() self.download_image(rosname) From e220cc840daddef672bbf4d9df43a5891e5cca14 Mon Sep 17 00:00:00 2001 From: Dennis <488132230@qq.com> Date: Wed, 17 Sep 2025 16:30:28 +0800 Subject: [PATCH 05/18] =?UTF-8?q?fix(tools):=20=E4=BF=AE=E5=A4=8Drosdepc?= =?UTF-8?q?=E5=87=BD=E6=95=B0=E4=B8=ADprint=5Fwarn=E7=9A=84=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E8=B0=83=E7=94=A8=20-=20=E4=BF=AE=E5=A4=8Drosdepc?= =?UTF-8?q?=E5=87=BD=E6=95=B0=E4=B8=ADprint=5Fwarn=E7=9A=84=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E8=B0=83=E7=94=A8=20-=20=E8=B0=83=E6=95=B4=E5=AE=89?= =?UTF-8?q?=E8=A3=85=E7=9A=84=E9=A1=BA=E5=BA=8F=EF=BC=8C=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E4=B8=8D=E5=B8=A6=E5=8F=82=E6=95=B0=E7=9A=84?= =?UTF-8?q?=E5=AE=89=E8=A3=85=20-=20=E5=B0=86=E9=95=9C=E5=83=8F=E6=BA=90?= =?UTF-8?q?=E7=9A=84=E5=90=8D=E7=A7=B0=E4=BF=AE=E6=94=B9=E4=B8=BA=E5=85=A8?= =?UTF-8?q?=E5=90=8D=E8=80=8C=E9=9D=9E=E7=AE=80=E7=A7=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/tool_config_rosdep.py | 67 ++++++++++++++++++++++++++----------- 1 file changed, 48 insertions(+), 19 deletions(-) diff --git a/tools/tool_config_rosdep.py b/tools/tool_config_rosdep.py index e0e357c..128817b 100644 --- a/tools/tool_config_rosdep.py +++ b/tools/tool_config_rosdep.py @@ -44,7 +44,7 @@ def choose_pip_source(self): sources = { "清华源": "https://pypi.tuna.tsinghua.edu.cn/simple", "阿里云": "https://mirrors.aliyun.com/pypi/simple", - "中国科技大学": "https://pypi.mirrors.ustc.edu.cn/simple", + "中国科学技术大学": "https://pypi.mirrors.ustc.edu.cn/simple", "华为云": "https://repo.huaweicloud.com/repository/pypi/simple" } @@ -59,36 +59,65 @@ def choose_pip_source(self): choose_index, choose_content = ChooseTask(choose_dict, "请选择pip源:").run() if choose_index == 0: - PrintUtils.print_info("未选择源,默认使用中国科技大学源") - return sources["中国科技大学"] + PrintUtils.print_info("未选择源,默认使用中国科学技术大学源") + return sources["中国科学技术大学"] try: # choose_index 已经是整数,直接使用 selected_name = list(sources.keys())[choose_index-1] return sources[selected_name] except (IndexError) as e: - PrintUtils.print_error(f"选择源时出错: {str(e)},使用中国科技大学源") - return sources["中国科技大学"] + PrintUtils.print_error(f"选择源时出错: {str(e)},使用中国科学技术大学源") + return sources["中国科学技术大学"] def install_rosdepc(self): - CmdTask("sudo apt install python3-pip -y", 0).run() + """ + 安装rosdepc工具,用于ROS依赖管理 + """ + # 安装python3-pip + pip_install_result = CmdTask("sudo apt install python3-pip -y", 0).run() + if pip_install_result[0] != 0: + PrintUtils.print_error("安装python3-pip失败") + return False - # 选择源 + # 选择pip源 selected_source = self.choose_pip_source() PrintUtils.print_success(f"您选择了: {selected_source}") - # 直接使用 --break-system-packages 参数安装,避免第一次安装失败 + # 先尝试不带参数安装rosdepc PrintUtils.print_info(f"正在使用 {selected_source} 安装 rosdepc...") - cmd_ret = CmdTask(f"sudo pip3 install -i {selected_source} rosdepc --break-system-packages").run() - if cmd_ret[0]!=0: - # 如果安装失败,尝试不带参数安装 - PrintUtils.print_warning("安装失败,尝试使用其他方式安装...") - cmd_ret = CmdTask(f"sudo pip3 install -i {selected_source} rosdepc").run() - CmdTask("sudo rosdepc init", 0).run() - CmdTask("sudo rosdepc fix-permissions", 0).run() + cmd_ret = CmdTask(f"sudo pip3 install -i {selected_source} rosdepc").run() + + # 如果不带参数安装失败,尝试使用 --break-system-packages 参数安装 + if cmd_ret[0] != 0: + PrintUtils.print_warn("安装失败,尝试使用 --break-system-packages 参数安装...") + cmd_ret = CmdTask(f"sudo pip3 install -i {selected_source} rosdepc --break-system-packages").run() + # 如果仍然失败,返回False + if cmd_ret[0] != 0: + PrintUtils.print_error("两种方式安装rosdepc均失败") + return False + + # 初始化rosdepc + init_result = CmdTask("sudo rosdepc init", 0).run() + if init_result[0] != 0: + PrintUtils.print_error("rosdepc初始化失败") + return False + + # 修复权限问题 + fix_result = CmdTask("sudo rosdepc fix-permissions", 0).run() + if fix_result[0] != 0: + PrintUtils.print_error("修复rosdepc权限失败") + return False + PrintUtils.print_info('已为您安装好rosdepc,请使用:\nrosdepc update \n进行测试更新,最后欢迎关注微信公众号《鱼香ROS》') - - + return True + def run(self): - #正式的运行 - self.install_rosdepc() \ No newline at end of file + """ + 运行rosdepc安装工具 + + Returns: + bool: 安装成功返回True,失败返回False + """ + # 正式的运行 + return self.install_rosdepc() \ No newline at end of file From a982a560a6ba69c0e80b0ef6a831696825ddd7a3 Mon Sep 17 00:00:00 2001 From: Dennis <488132230@qq.com> Date: Wed, 17 Sep 2025 16:36:40 +0800 Subject: [PATCH 06/18] =?UTF-8?q?refactor(tools):=20=E7=B2=BE=E7=AE=80=20t?= =?UTF-8?q?ool=5Fconfig=5Frosdep.py=20=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除了未使用的导入模块 - 移除了未使用的 test_source_speed 函数 --- tools/tool_config_rosdep.py | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/tools/tool_config_rosdep.py b/tools/tool_config_rosdep.py index 128817b..8faded6 100644 --- a/tools/tool_config_rosdep.py +++ b/tools/tool_config_rosdep.py @@ -1,40 +1,12 @@ # -*- coding: utf-8 -*- -import time -import http.client -from urllib.parse import urlparse from .base import BaseTool from .base import PrintUtils,CmdTask,FileUtils,AptUtils,ChooseTask -from .base import osversion -from .base import run_tool_file class Tool(BaseTool): def __init__(self): self.type = BaseTool.TYPE_CONFIG self.name = "模板工程" self.author = '小鱼' - - def test_source_speed(self, url): - """测试源的速度 - - Args: - url (str): 源的URL - - Returns: - float: 响应时间(秒) - """ - try: - start_time = time.time() - parsed_url = urlparse(url) - conn = http.client.HTTPSConnection(parsed_url.netloc, timeout=5) - conn.request("HEAD", parsed_url.path) - resp = conn.getresponse() - conn.close() - end_time = time.time() - return end_time - start_time - except Exception as e: - PrintUtils.print_error(f"测试源 {url} 失败: {str(e)}") - return float('inf') # 返回无穷大表示连接失败 - def choose_pip_source(self): """选择pip源 From c6e78127e54c6be5912211b4f5d6f87e5602549f Mon Sep 17 00:00:00 2001 From: Dennis <488132230@qq.com> Date: Wed, 17 Sep 2025 16:38:30 +0800 Subject: [PATCH 07/18] =?UTF-8?q?refactor(tools):=20=E7=B2=BE=E7=AE=80=20t?= =?UTF-8?q?ool=5Fconfig=5Frosdep.py=20=E4=B8=AD=E7=9A=84=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除了未使用的 FileUtils 和 AptUtils 模块 - 优化了代码结构,提高了代码的可读性和维护性 --- tools/tool_config_rosdep.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/tool_config_rosdep.py b/tools/tool_config_rosdep.py index 8faded6..45dc258 100644 --- a/tools/tool_config_rosdep.py +++ b/tools/tool_config_rosdep.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- from .base import BaseTool -from .base import PrintUtils,CmdTask,FileUtils,AptUtils,ChooseTask +from .base import PrintUtils,CmdTask,ChooseTask class Tool(BaseTool): def __init__(self): From b8a6c5d13f5c68e61f349f97f0b4391d5008c8a3 Mon Sep 17 00:00:00 2001 From: Dennis <488132230@qq.com> Date: Wed, 17 Sep 2025 17:34:25 +0800 Subject: [PATCH 08/18] =?UTF-8?q?refactor(install):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E7=BB=93=E6=9E=84=E5=92=8C=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - wget相关文件之前提前创建本地相关文件夹防止被记录报错 --- install.py | 6 +++--- tools/tool_config_rosdep.py | 10 +++++----- tools/translation/translator.py | 2 ++ 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/install.py b/install.py index 1f11463..a9d8623 100644 --- a/install.py +++ b/install.py @@ -53,7 +53,8 @@ tracking = None def main(): - # download base + os.system("mkdir -p /tmp/fishinstall/tools/translation/assets") + url_prefix = os.environ.get('FISHROS_URL','http://mirror.fishros.com/install') os.system("wget {} -O /tmp/fishinstall/{} --no-check-certificate".format(base_url,base_url.replace(url_prefix,''))) @@ -62,7 +63,6 @@ def main(): from tools.base import run_tool_file,download_tools from tools.base import config_helper,tr - # download translations CmdTask("wget {} -O /tmp/fishinstall/{} --no-check-certificate".format(translator_url,translator_url.replace(url_prefix,''))).run() importlib.import_module("tools.translation.translator").Linguist() @@ -149,7 +149,7 @@ def main(): for text,end in tracing.logs: print(text, file=f,end=end) # 打印输出到文件中 for text in tracing.err_logs: - print(text, file=f) # 打印输出到文件中 + print(text, file=f) # 打印输出到文件中 if tracing.need_report: print("") input('检测到本次运行出现失败命令,直接退出按Ctrl+C,按任意键上传日志并退出\n') diff --git a/tools/tool_config_rosdep.py b/tools/tool_config_rosdep.py index 45dc258..05fc944 100644 --- a/tools/tool_config_rosdep.py +++ b/tools/tool_config_rosdep.py @@ -50,7 +50,7 @@ def install_rosdepc(self): pip_install_result = CmdTask("sudo apt install python3-pip -y", 0).run() if pip_install_result[0] != 0: PrintUtils.print_error("安装python3-pip失败") - return False + return 1 # 选择pip源 selected_source = self.choose_pip_source() @@ -67,22 +67,22 @@ def install_rosdepc(self): # 如果仍然失败,返回False if cmd_ret[0] != 0: PrintUtils.print_error("两种方式安装rosdepc均失败") - return False + return 2 # 初始化rosdepc init_result = CmdTask("sudo rosdepc init", 0).run() if init_result[0] != 0: PrintUtils.print_error("rosdepc初始化失败") - return False + return 3 # 修复权限问题 fix_result = CmdTask("sudo rosdepc fix-permissions", 0).run() if fix_result[0] != 0: PrintUtils.print_error("修复rosdepc权限失败") - return False + return 4 PrintUtils.print_info('已为您安装好rosdepc,请使用:\nrosdepc update \n进行测试更新,最后欢迎关注微信公众号《鱼香ROS》') - return True + return 0 def run(self): """ diff --git a/tools/translation/translator.py b/tools/translation/translator.py index 5d935f3..50eeb57 100644 --- a/tools/translation/translator.py +++ b/tools/translation/translator.py @@ -30,6 +30,8 @@ def __init__(self): self._currentLocale = locale.getdefaultlocale()[0] # Load the translation file. self.lang = self._currentLocale + # 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() From 1ee4dc2ad71d31f5cc2108ee50e8c5f303cdd939 Mon Sep 17 00:00:00 2001 From: Dennis <488132230@qq.com> Date: Thu, 18 Sep 2025 12:36:51 +0800 Subject: [PATCH 09/18] =?UTF-8?q?refactor(base.py,=20tool=5Fconfig=5Frosde?= =?UTF-8?q?p.py):=20=E6=9B=B4=E6=96=B0=E5=AD=97=E7=AC=A6=E4=B8=B2=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F=E5=8C=96=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 `base.py` 和 `tool_config_rosdep.py` 文件中的 f-string 格式化改为 `.format()` 方法,以保持代码风格的一致性。 --- tools/base.py | 2 +- tools/tool_config_rosdep.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tools/base.py b/tools/base.py index ae4ef98..22ae6b9 100644 --- a/tools/base.py +++ b/tools/base.py @@ -1472,7 +1472,7 @@ def get_fast_url(urls,timeout=1.5): conn.close() except Exception as e: # 如果请求失败,记录为 None 或者一个很大的延时 - # print(f"Error accessing {url}: {e}") + # print("Error accessing {}: {}".format(url, e)) PrintUtils.print_info("- {}\t\t超时".format(url)) latencies[url] = float('inf') diff --git a/tools/tool_config_rosdep.py b/tools/tool_config_rosdep.py index 05fc944..0b3434d 100644 --- a/tools/tool_config_rosdep.py +++ b/tools/tool_config_rosdep.py @@ -24,7 +24,7 @@ def choose_pip_source(self): choose_dict = {} i = 1 for name, url in sources.items(): - choose_dict[i] = f"{name} - {url}" + choose_dict[i] = "{} - {}".format(name, url) i += 1 PrintUtils.print_info("请选择要使用的pip源:") @@ -39,7 +39,7 @@ def choose_pip_source(self): selected_name = list(sources.keys())[choose_index-1] return sources[selected_name] except (IndexError) as e: - PrintUtils.print_error(f"选择源时出错: {str(e)},使用中国科学技术大学源") + PrintUtils.print_error("选择源时出错: {},使用中国科学技术大学源".format(str(e))) return sources["中国科学技术大学"] def install_rosdepc(self): @@ -54,16 +54,16 @@ def install_rosdepc(self): # 选择pip源 selected_source = self.choose_pip_source() - PrintUtils.print_success(f"您选择了: {selected_source}") + PrintUtils.print_success("您选择了: {}".format(selected_source)) # 先尝试不带参数安装rosdepc - PrintUtils.print_info(f"正在使用 {selected_source} 安装 rosdepc...") - cmd_ret = CmdTask(f"sudo pip3 install -i {selected_source} rosdepc").run() + PrintUtils.print_info("正在使用 {} 安装 rosdepc...".format(selected_source)) + cmd_ret = CmdTask("sudo pip3 install -i {} rosdepc".format(selected_source)).run() # 如果不带参数安装失败,尝试使用 --break-system-packages 参数安装 if cmd_ret[0] != 0: PrintUtils.print_warn("安装失败,尝试使用 --break-system-packages 参数安装...") - cmd_ret = CmdTask(f"sudo pip3 install -i {selected_source} rosdepc --break-system-packages").run() + cmd_ret = CmdTask("sudo pip3 install -i {} rosdepc --break-system-packages".format(selected_source)).run() # 如果仍然失败,返回False if cmd_ret[0] != 0: PrintUtils.print_error("两种方式安装rosdepc均失败") From f47382243c2dbbf3808d2a382fcf26814368ebe9 Mon Sep 17 00:00:00 2001 From: Dennis <488132230@qq.com> Date: Thu, 18 Sep 2025 17:16:01 +0800 Subject: [PATCH 10/18] =?UTF-8?q?fix(tool):=E7=A7=BB=E9=99=A4=E9=83=A8?= =?UTF-8?q?=E5=88=86=E4=B8=8D=E6=94=AF=E6=8C=81ros1=E7=9A=84=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E7=BC=96=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/tool_install_ros.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/tools/tool_install_ros.py b/tools/tool_install_ros.py index 85acd84..d3fefff 100644 --- a/tools/tool_install_ros.py +++ b/tools/tool_install_ros.py @@ -120,17 +120,6 @@ def get_desktop_version(name): 'xenial':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, 'yakkety':{"packages.ros"}, 'zesty':{"packages.ros"}, - 'focal':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, - 'jammy':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, - 'kinetic':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, - 'lunar':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, - 'melodic':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, - 'noble':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, - 'trusty':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, - 'utopic':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, - 'xenial':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, - 'yakkety':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, - 'zesty':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, } From d30d8d90daa4a1c12d57b3c0c7dca159fd430d8e Mon Sep 17 00:00:00 2001 From: Dennis_Re_Yoonjiho <98302491+tangsangsimida@users.noreply.github.com> Date: Fri, 10 Oct 2025 18:51:47 +0800 Subject: [PATCH 11/18] =?UTF-8?q?feat(test):=20=E6=B7=BB=E5=8A=A0=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=8C=96=E6=B5=8B=E8=AF=95=E5=B7=A5=E4=BD=9C=E6=B5=81?= =?UTF-8?q?=E5=92=8C=E6=B5=8B=E8=AF=95=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 GitHub Actions 工作流用于测试安装脚本,支持多 Ubuntu 版本。 添加 test_runner.py 脚本,实现基于 YAML 配置的自动化测试逻辑。 新增 fish_install_test.yaml 测试配置文件,包含多个安装项的测试用例。 test_report.json和test_report.html文件作为artifact保存,便于后续查看和分析测试结果 在测试运行器中新增generate_html_report函数,用于生成带样式的HTML测试报告。 Co-authored-by: awan-deng <2152621419@qq.com> --- .github/workflows/test-install.yml | 47 ++++ .gitignore | 3 +- README.md | 2 +- install.py | 12 +- tests/fish_install_test.yaml | 13 + tests/generate_report.py | 146 +++++++++++ tests/test_runner.py | 383 +++++++++++++++++++++++++++++ tools/base.py | 32 ++- 8 files changed, 624 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/test-install.yml create mode 100644 tests/fish_install_test.yaml create mode 100644 tests/generate_report.py create mode 100644 tests/test_runner.py 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 : From f62e0d26e1922abba2eec3ade801eeebd4d33525 Mon Sep 17 00:00:00 2001 From: awan-deng <2152621419@qq.com> Date: Fri, 10 Oct 2025 19:51:09 +0800 Subject: [PATCH 12/18] =?UTF-8?q?ci(test-install):=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E5=B7=A5=E4=BD=9C=E6=B5=81=E4=B8=AD=E7=9A=84?= =?UTF-8?q?Ubuntu=E7=89=88=E6=9C=AC=E7=9F=A9=E9=98=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除Ubuntu 18.04和25.10,仅保留20.04、22.04和24.04版本用于测试安装流程 --- .github/workflows/test-install.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-install.yml b/.github/workflows/test-install.yml index 8979d8e..b1f9746 100644 --- a/.github/workflows/test-install.yml +++ b/.github/workflows/test-install.yml @@ -10,7 +10,7 @@ jobs: test-install: strategy: matrix: - os: [ubuntu-18.04, ubuntu-20.04, ubuntu-22.04, ubuntu-24.04, ubuntu-25.10] + os: [ubuntu-20.04, ubuntu-22.04, ubuntu-24.04] runs-on: ${{ matrix.os }} steps: From d15f6a7dc886852cb63f339c71d146271e3e6eff Mon Sep 17 00:00:00 2001 From: Dennis <488132230@qq.com> Date: Fri, 17 Oct 2025 23:54:09 +0800 Subject: [PATCH 13/18] =?UTF-8?q?feat(tests):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E6=97=A7=E7=9A=84=E5=AE=89=E8=A3=85=E8=84=9A=E6=9C=AC=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E7=9B=B8=E5=85=B3=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除了 GitHub Actions 工作流配置文件 `.github/workflows/test-install.yml`, 以及相关的测试配置文件 `tests/fish_install_test.yaml` 和测试运行器 `tests/test_runner.py`。这些文件已被新的测试方案替代,故予以清理。 --- .github/workflows/test-install.yml | 47 ---- tests/fish_install_test.yaml | 13 - tests/generate_report.py | 146 ----------- tests/test_runner.py | 383 ----------------------------- 4 files changed, 589 deletions(-) delete mode 100644 .github/workflows/test-install.yml delete mode 100644 tests/fish_install_test.yaml delete mode 100644 tests/generate_report.py delete mode 100644 tests/test_runner.py diff --git a/.github/workflows/test-install.yml b/.github/workflows/test-install.yml deleted file mode 100644 index b1f9746..0000000 --- a/.github/workflows/test-install.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: Test Install Script - -on: - push: - branches: [ dev, master ] - pull_request: - branches: [ dev, master ] - -jobs: - test-install: - strategy: - matrix: - os: [ubuntu-20.04, ubuntu-22.04, ubuntu-24.04] - 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/tests/fish_install_test.yaml b/tests/fish_install_test.yaml deleted file mode 100644 index 563a4d9..0000000 --- a/tests/fish_install_test.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# 测试配置文件,用于 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 deleted file mode 100644 index 3d8ee60..0000000 --- a/tests/generate_report.py +++ /dev/null @@ -1,146 +0,0 @@ -#!/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 deleted file mode 100644 index 1e70a06..0000000 --- a/tests/test_runner.py +++ /dev/null @@ -1,383 +0,0 @@ -#!/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() From e350fb2ce05cfe8c297397b22aaf0c6ebe0e4379 Mon Sep 17 00:00:00 2001 From: Dennis <488132230@qq.com> Date: Sat, 18 Oct 2025 12:43:05 +0800 Subject: [PATCH 14/18] =?UTF-8?q?fix(tools):=20=E4=BF=AE=E5=A4=8D=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=88=9B=E5=BB=BA=E6=9D=83=E9=99=90=E5=86=B2=E7=AA=81?= =?UTF-8?q?=E9=97=AE=E9=A2=98=E5=B9=B6=E6=B8=85=E7=90=86=E4=B8=B4=E6=97=B6?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 `FileUtils.new` 方法中,使用 uuid 生成唯一临时文件名以避免权限冲突 - 添加了对临时文件的清理逻辑,确保即使出现异常也能删除临时文件 - 移除了 `tool_config_system_source.py` 中多余的空行,优化代码格式 - 更新注释说明,明确指出使用已修复的 `FileUtils.new` 方法来配置系统源 --- tools/base.py | 15 +++++++++++---- tools/tool_config_system_source.py | 8 +------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/tools/base.py b/tools/base.py index c1f2b69..cc75701 100644 --- a/tools/base.py +++ b/tools/base.py @@ -1278,10 +1278,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 From 5c79f88512672a13bd7493f98b67d57704ee85e7 Mon Sep 17 00:00:00 2001 From: Dennis_Re_Yoonjiho <98302491+tangsangsimida@users.noreply.github.com> Date: Sat, 18 Oct 2025 13:49:32 +0800 Subject: [PATCH 15/18] add GitHub ci docker test (#2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat(test): 添加自动化测试工作流和测试 runner - 新增 GitHub Actions 工作流用于测试安装脚本,支持多 Ubuntu 版本。 - 添加 test_runner.py 脚本,实现基于 YAML 配置的自动化测试逻辑。 - 新增 fish_install_test.yaml 测试配置文件,包含多个安装项的测试用例。 fix(install): 跳过GitHub Actions环境中的配置文件生成和提示信息 - 在 GitHub Actions 环境中运行时,跳过需要用户交互的配置文件生成 以及相关欢迎和帮助信息的打印操作,避免因等待输入导致流程卡住。 fix(tools): 处理自动化测试环境中的 EOFError 异常 - 在 `ChooseTask` 和 `ChooseWithCategoriesTask` 类中,添加对 `input()` 函数在自动化测试环境下可能引发的 `EOFError` 异常处理。当捕获到该异常时,若存在配置文件提供的默认选项则使用它,否则默认选择退出(编号 0),并提示用户当前处于自动化环境。 feat(test): 为测试运行器添加环境变量配置支持 - 在 test_runner.py 中更新 run_install_test 函数,通过环境变量传递 FISH_INSTALL_CONFIG 配置文件路径,以支持更灵活的测试配置。 - 修改 tools/base.py 中的 ConfigHelper 类,优先从环境变量读取 配置文件路径,若未设置则使用默认值 ./fish_install.yaml。 feat(tests): 添加HTML格式测试报告生成功能 - 在测试运行器中新增generate_html_report函数,用于生成带样式的HTML测试报告。报告包含测试摘要和详细结果,支持通过/失败状态的视觉区分。在main函数中调用该功能,将报告保存为test_report.html文件。 ci(test-install): 添加测试报告上传功能 - 在GitHub Actions工作流中添加了测试报告上传步骤,确保在所有情况下都能上传test_report.json和test_report.html文件作为artifact保存,便于后续查看和分析测试结果 build(github-actions): 更新测试工作流中的上传构件操作版本 - 将 test-install.yml 工作流中的 actions/upload-artifact 操作从 v3 版本 升级到 v4 版本,以使用最新的功能和改进。 test(test_runner): 增加安装测试的超时时间至60分钟 将测试运行器中的进程超时时间从300秒增加到3600秒,以避免在较慢的环境中出现误报超时错误。 fix(test-install): 解决测试安装工作流中的编码问题 - 在 GitHub Actions 工作流中添加了 UTF-8 语言环境支持,确保测试运行器能够正确处理 中文字符。同时设置了 PYTHONIOENCODING 环境变量以避免编码错误。 - 将GitHub Actions工作流中的locale配置从C.UTF-8更改为en_US.UTF-8,以确保测试环境使用标准的英文UTF-8编码设置。 build(workflow): 添加wget依赖到测试安装工作流 - 在GitHub Actions工作流中添加wget包的安装,以支持后续的下载需求。 fix(test-install): 更新apt包列表并添加sudo依赖 - 在GitHub Actions工作流中,安装软件包前先执行apt update以确保获取最新的包信息 - 添加sudo依赖以支持需要管理员权限的操作。 添加注释说明在GitHub工作流测试中应使用官方源而非国内镜像源 * fix(install): 添加wget忽略证书检查选项以解决SSL问题 - 在安装脚本中,为wget命令添加`--no-check-certificate`参数,以解决某些环境下SSL证书验证失败导致的下载问题。 * ci(test-install): 增强测试工作流的健壮性和输出一致性 - 在shell脚本中添加set -u以提高变量检查严格性 - 设置DEBIAN_FRONTEND为oninteractive以避免交互式安装问题 - 更新pip并安装pyyaml和distro依赖包 - 添加-u参数到python3命令以确保输出不被缓冲 - 升级actions/upload-artifact到v4版本 * test(test_runner): 增加安装测试的超时时间 fix(test-install): 修复Docker容器中的时区设置问题 - 在Ubuntu Docker容器中运行测试时,tzdata包会提示交互式时区选择, 导致安装过程失败。通过预先设置时区为UTC避免该问题。 ci(test-install): 优化Docker容器中的测试运行命令格式 将Docker运行命令中的参数连接方式从多行连续输入改为使用反斜杠续行, 使YAML配置更清晰易读。同时保持原有的工作目录挂载和环境变量设置功能。 * feat(tools): 支持在GitHub Actions环境中自动覆盖配置文件 当检测到GITHUB_ACTIONS环境变量为true时,跳过用户交互确认步骤, 直接覆盖已存在的配置文件。此修改确保了在自动化测试环境中 配置文件能够被正确更新,而不会因为等待用户输入导致流程中断。 * feat(install): 支持通过环境变量跳过配置文件生成 新增对 `FISH_INSTALL_CONFIG` 环境变量的检查,当该环境变量存在时, 即使不在 GitHub Actions 环境中,也会跳过生成配置文件和后续的打印操作, 以便支持自动化测试场景。 docs(tests): 添加自动化测试说明文档 新增 `tests/README.md` 文件,详细说明了测试目录下的文件结构、 运行方式、测试配置文件格式以及工作原理,并提供了 GitHub Actions 集成说明。 * docs(tests): 更新测试文档并注释旧测试用例 新增“自动化测试与用户实际安装的区别”章节,说明两者在配置文件、环境变量、 跳过步骤及 GitHub Actions 运行环境方面的不同。同时注释掉 `fish_install_test.yaml` 中针对 focal 版本的旧 ROS 安装测试用例。 --------- Co-authored-by: awan-deng <2152621419@qq.com> --- .github/workflows/test-install.yml | 52 ++++ .gitignore | 3 +- install.py | 6 +- tests/README.md | 64 ++++ tests/fish_install_test.yaml | 56 ++++ tests/generate_report.py | 146 +++++++++ tests/test_runner.py | 467 +++++++++++++++++++++++++++++ tools/base.py | 16 +- tools/translation/translator.py | 46 ++- 9 files changed, 835 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/test-install.yml create mode 100644 tests/README.md create mode 100644 tests/fish_install_test.yaml create mode 100644 tests/generate_report.py create mode 100644 tests/test_runner.py 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 7e99e36..3bcfd2e 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -*.pyc \ No newline at end of file +*.pyc +tests/test_local \ No newline at end of file diff --git a/install.py b/install.py index 5853d9e..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': @@ -122,9 +122,9 @@ def main(): download_tools(code,tools,url_prefix) run_tool_file(tools[code]['tool'].replace("/",".")) - # 检查是否在 GitHub Actions 环境中运行 + # 检查是否在 GitHub Actions 环境中运行或使用了测试配置文件 # 如果是,则跳过生成配置文件和后续的打印操作,因为这些操作需要用户输入 - if os.environ.get('GITHUB_ACTIONS') != 'true': + 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) 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..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..09ede23 --- /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(f"加载测试配置文件失败: {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 += 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 + # 增加超时时间为 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(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() \ No newline at end of file diff --git a/tools/base.py b/tools/base.py index cc75701..b432863 100644 --- a/tools/base.py +++ b/tools/base.py @@ -64,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): 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 From 556f080665d329a9cf71a1b6616e07ef5d45f33f Mon Sep 17 00:00:00 2001 From: Dennis <488132230@qq.com> Date: Sat, 18 Oct 2025 18:49:43 +0800 Subject: [PATCH 16/18] =?UTF-8?q?refactor(tests):=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E5=B0=86f-string=E6=A0=BC=E5=BC=8F=E5=8C=96=E6=9B=BF=E6=8D=A2?= =?UTF-8?q?=E4=B8=BAstr.format=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/generate_report.py | 16 +++++++------- tests/test_runner.py | 48 ++++++++++++++++++++-------------------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/tests/generate_report.py b/tests/generate_report.py index 3d8ee60..ad77730 100644 --- a/tests/generate_report.py +++ b/tests/generate_report.py @@ -106,16 +106,16 @@ def generate_html_report(report, output_file): status_text = "通过" if test_case["success"] else "失败" status_style = "status-passed" if test_case["success"] else "status-failed" - html_content += f""" -
+ html_content += """ +
- {test_case["name"]} - {status_text} + {} + {}
输出日志:
-
{test_case["output"]}
+
{}
-""" +""".format(status_class, test_case["name"], status_style, status_text, test_case["output"]) html_content += """
@@ -141,6 +141,6 @@ def generate_html_report(report, output_file): # 生成HTML报告 html_report_file = "test_report.html" generate_html_report(report, html_report_file) - print(f"HTML测试报告已生成: {html_report_file}") + print("HTML测试报告已生成: {}".format(html_report_file)) else: - print(f"找不到测试报告文件: {report_file}") \ No newline at end of file + print("找不到测试报告文件: {}".format(report_file)) \ No newline at end of file diff --git a/tests/test_runner.py b/tests/test_runner.py index 09ede23..22d1e47 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -27,7 +27,7 @@ def load_test_cases(config_file): test_cases = yaml.safe_load(f) return test_cases except Exception as e: - print(f"加载测试配置文件失败: {e}") + print("加载测试配置文件失败: {}".format(e)) return [] def get_ubuntu_codename(): @@ -180,16 +180,16 @@ def generate_html_report(report, output_file): status_text = "通过" if test_case["success"] else "失败" status_style = "status-passed" if test_case["success"] else "status-failed" - html_content += f""" -
+ html_content += """ +
- {test_case["name"]} - {status_text} -
-
输出日志:
-
{test_case["output"]}
-
-""" + {} + {}<\/span> + <\/div> +
输出日志:<\/div> +
{}<\/div> + <\/div> +""".format(status_class, test_case["name"], status_style, status_text, test_case["output"]) html_content += """
@@ -210,7 +210,7 @@ def run_install_test(test_case): name = test_case.get('name', 'Unknown Test') chooses = test_case.get('chooses', []) - print(f"开始测试: {name}") + print("开始测试: {}".format(name)) # 创建临时配置文件路径 temp_config = "/tmp/fish_install_test_temp.yaml" @@ -223,9 +223,9 @@ def run_install_test(test_case): try: with open(temp_config, 'w', encoding='utf-8') as f: yaml.dump(config_data, f, allow_unicode=True) - print(f"已创建临时配置文件: {temp_config}") + print("已创建临时配置文件: {}".format(temp_config)) except Exception as e: - print(f"创建临时配置文件失败: {e}") + print("创建临时配置文件失败: {}".format(e)) return False, "" # 备份原始的 fish_install.yaml (如果存在) @@ -234,25 +234,25 @@ def run_install_test(test_case): if os.path.exists(original_config): try: os.rename(original_config, backup_config) - print(f"已备份原始配置文件至: {backup_config}") + print("已备份原始配置文件至: {}".format(backup_config)) except Exception as e: - print(f"备份原始配置文件失败: {e}") + print("备份原始配置文件失败: {}".format(e)) # 即使备份失败也继续执行,因为我们会在最后恢复 # 将临时配置文件复制为当前配置文件和/tmp/fishinstall/tools/fish_install.yaml try: import shutil shutil.copy(temp_config, original_config) - print(f"已将临时配置文件复制为: {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(f"已将临时配置文件复制为: {fishinstall_config}") + print("已将临时配置文件复制为: {}".format(fishinstall_config)) except Exception as e: - print(f"复制配置文件失败: {e}") + print("复制配置文件失败: {}".format(e)) # 恢复备份的配置文件 if os.path.exists(backup_config): try: @@ -405,9 +405,9 @@ def main(): failed = 0 for i, test_case in enumerate(test_cases): - print(f"\n--- 测试用例 {i+1}/{len(test_cases)} ---") + print("\n--- 测试用例 {}/{} ---".format(i+1, len(test_cases))) success, output = run_install_test(test_case) - case_name = test_case.get('name', f'Test Case {i+1}') + case_name = test_case.get('name', 'Test Case {}'.format(i+1)) result = { "name": case_name, @@ -438,17 +438,17 @@ def main(): 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}") + print("\n详细测试报告已保存至: {}".format(report_file)) except Exception as e: - print(f"保存测试报告失败: {e}") + print("保存测试报告失败: {}".format(e)) # 生成HTML格式的测试报告 html_report_file = "test_report.html" try: generate_html_report(report, html_report_file) - print(f"HTML测试报告已保存至: {html_report_file}") + print("HTML测试报告已保存至: {}".format(html_report_file)) except Exception as e: - print(f"生成HTML测试报告失败: {e}") + print("生成HTML测试报告失败: {}".format(e)) # 输出测试结果摘要 print("\n=== 测试结果摘要 ===") From 2119cc7d58331a91f2795a1a0c558b85264e83cd Mon Sep 17 00:00:00 2001 From: Dennis_Re_Yoonjiho <98302491+tangsangsimida@users.noreply.github.com> Date: Fri, 31 Oct 2025 21:05:04 +0800 Subject: [PATCH 17/18] =?UTF-8?q?fix(tools):=20=E4=BC=98=E5=8C=96=E4=BA=86?= =?UTF-8?q?GitHub=E5=B7=A5=E4=BD=9C=E6=B5=81=E6=B5=8B=E8=AF=95=E6=B5=81?= =?UTF-8?q?=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重新整理了各个源对ros1&&ros2的支持字典 新增 generate-test-matrix 任务,用于从 YAML 文件动态生成测试矩阵。 将工作流测试配置文件中安装的ros版本修改为ros1的版本(中科大的镜像无法支持ubuntu18.04安装ros2) 修改依赖安装逻辑,使用批量安装方式提高效率 --- .github/workflows/test-install.yml | 267 ++++++++++++++++++++++++++++- tests/fish_install_test.yaml | 27 +-- tests/test_runner.py | 117 +++++++++++-- tools/tool_install_ros.py | 95 +++++----- 4 files changed, 429 insertions(+), 77 deletions(-) diff --git a/.github/workflows/test-install.yml b/.github/workflows/test-install.yml index 0837f6d..afc0063 100644 --- a/.github/workflows/test-install.yml +++ b/.github/workflows/test-install.yml @@ -7,10 +7,212 @@ on: branches: [ dev, master, github_ci_docker_test ] jobs: - test-install: + generate-test-matrix-by-os: + runs-on: ubuntu-latest + outputs: + bionic_matrix: ${{ steps.set-bionic-matrix.outputs.matrix }} + focal_matrix: ${{ steps.set-focal-matrix.outputs.matrix }} + jammy_matrix: ${{ steps.set-jammy-matrix.outputs.matrix }} + noble_matrix: ${{ steps.set-noble-matrix.outputs.matrix }} + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pyyaml + - name: Generate bionic matrix + id: set-bionic-matrix + run: | + python -c " + import yaml + import json + with open('tests/fish_install_test.yaml', 'r') as f: + test_cases = yaml.safe_load(f) + + # 过滤出bionic系统的测试用例 + bionic_tests = [case for case in test_cases if case.get('target_os_version') == 'bionic'] + matrix = {'include': []} + for i, case in enumerate(bionic_tests): + matrix['include'].append({ + 'test_name': case['name'], + 'test_index': test_cases.index(case) + }) + + print('matrix=' + json.dumps(matrix)) + " >> $GITHUB_OUTPUT + - name: Generate focal matrix + id: set-focal-matrix + run: | + python -c " + import yaml + import json + with open('tests/fish_install_test.yaml', 'r') as f: + test_cases = yaml.safe_load(f) + + # 过滤出focal系统的测试用例 + focal_tests = [case for case in test_cases if case.get('target_os_version') == 'focal'] + matrix = {'include': []} + for i, case in enumerate(focal_tests): + matrix['include'].append({ + 'test_name': case['name'], + 'test_index': test_cases.index(case) + }) + + print('matrix=' + json.dumps(matrix)) + " >> $GITHUB_OUTPUT + - name: Generate jammy matrix + id: set-jammy-matrix + run: | + python -c " + import yaml + import json + with open('tests/fish_install_test.yaml', 'r') as f: + test_cases = yaml.safe_load(f) + + # 过滤出jammy系统的测试用例 + jammy_tests = [case for case in test_cases if case.get('target_os_version') == 'jammy'] + matrix = {'include': []} + for i, case in enumerate(jammy_tests): + matrix['include'].append({ + 'test_name': case['name'], + 'test_index': test_cases.index(case) + }) + + print('matrix=' + json.dumps(matrix)) + " >> $GITHUB_OUTPUT + - name: Generate noble matrix + id: set-noble-matrix + run: | + python -c " + import yaml + import json + with open('tests/fish_install_test.yaml', 'r') as f: + test_cases = yaml.safe_load(f) + + # 过滤出noble系统的测试用例 + noble_tests = [case for case in test_cases if case.get('target_os_version') == 'noble'] + matrix = {'include': []} + for i, case in enumerate(noble_tests): + matrix['include'].append({ + 'test_name': case['name'], + 'test_index': test_cases.index(case) + }) + + print('matrix=' + json.dumps(matrix)) + " >> $GITHUB_OUTPUT + + test-install-bionic: + needs: generate-test-matrix-by-os + if: ${{ needs.generate-test-matrix-by-os.outputs.bionic_matrix != '{}' }} + strategy: + matrix: ${{ fromJSON(needs.generate-test-matrix-by-os.outputs.bionic_matrix) }} + # max-parallel: 2 + fail-fast: false # 即使某些测试失败,也继续运行其他测试 + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Run tests in Docker container + run: | + TEST_CASE="bionic" + echo "Using Ubuntu version: $TEST_CASE" + + docker run --rm \ + -v ${{ github.workspace }}:${{ github.workspace }} \ + -w ${{ github.workspace }} \ + ubuntu:$TEST_CASE \ + 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 --target-os-version $TEST_CASE --test-index ${{ matrix.test_index }} + " + - name: Upload test reports + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-reports-${{ matrix.test_name }} + path: | + tests/test_report.json + tests/test_report.html + if-no-files-found: ignore + + test-install-focal: + needs: generate-test-matrix-by-os + if: ${{ needs.generate-test-matrix-by-os.outputs.focal_matrix != '{}' }} + strategy: + matrix: ${{ fromJSON(needs.generate-test-matrix-by-os.outputs.focal_matrix) }} + max-parallel: 2 + fail-fast: false # 即使某些测试失败,也继续运行其他测试 + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Run tests in Docker container + run: | + TEST_CASE="focal" + echo "Using Ubuntu version: $TEST_CASE" + + docker run --rm \ + -v ${{ github.workspace }}:${{ github.workspace }} \ + -w ${{ github.workspace }} \ + ubuntu:$TEST_CASE \ + 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 --target-os-version $TEST_CASE --test-index ${{ matrix.test_index }} + " + - name: Upload test reports + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-reports-${{ matrix.test_name }} + path: | + tests/test_report.json + tests/test_report.html + if-no-files-found: ignore + + test-install-jammy: + needs: generate-test-matrix-by-os + if: ${{ needs.generate-test-matrix-by-os.outputs.jammy_matrix != '{}' }} strategy: - matrix: - ubuntu_version: [18.04, 20.04, 22.04, 24.04] + matrix: ${{ fromJSON(needs.generate-test-matrix-by-os.outputs.jammy_matrix) }} + max-parallel: 2 + fail-fast: false # 即使某些测试失败,也继续运行其他测试 runs-on: ubuntu-latest steps: @@ -19,10 +221,13 @@ jobs: - name: Run tests in Docker container run: | + TEST_CASE="jammy" + echo "Using Ubuntu version: $TEST_CASE" + docker run --rm \ -v ${{ github.workspace }}:${{ github.workspace }} \ -w ${{ github.workspace }} \ - ubuntu:${{ matrix.ubuntu_version }} \ + ubuntu:$TEST_CASE \ bash -c " set -u && export DEBIAN_FRONTEND=noninteractive && @@ -39,14 +244,64 @@ jobs: pip install --upgrade pip && pip install pyyaml distro && cd tests && - PYTHONIOENCODING=utf-8 python3 -u test_runner.py + PYTHONIOENCODING=utf-8 python3 -u test_runner.py --target-os-version $TEST_CASE --test-index ${{ matrix.test_index }} " - name: Upload test reports uses: actions/upload-artifact@v4 if: always() with: - name: test-reports-ubuntu-${{ matrix.ubuntu_version }} + name: test-reports-${{ matrix.test_name }} path: | tests/test_report.json tests/test_report.html if-no-files-found: ignore + + test-install-noble: + needs: generate-test-matrix-by-os + if: ${{ needs.generate-test-matrix-by-os.outputs.noble_matrix != '{}' }} + strategy: + matrix: ${{ fromJSON(needs.generate-test-matrix-by-os.outputs.noble_matrix) }} + max-parallel: 2 + fail-fast: false # 即使某些测试失败,也继续运行其他测试 + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Run tests in Docker container + run: | + TEST_CASE="noble" + echo "Using Ubuntu version: $TEST_CASE" + + docker run --rm \ + -v ${{ github.workspace }}:${{ github.workspace }} \ + -w ${{ github.workspace }} \ + ubuntu:$TEST_CASE \ + 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 --target-os-version $TEST_CASE --test-index ${{ matrix.test_index }} + " + - name: Upload test reports + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-reports-${{ matrix.test_name }} + path: | + tests/test_report.json + tests/test_report.html + if-no-files-found: ignore \ No newline at end of file diff --git a/tests/fish_install_test.yaml b/tests/fish_install_test.yaml index 7bea6ee..cc592b5 100644 --- a/tests/fish_install_test.yaml +++ b/tests/fish_install_test.yaml @@ -4,15 +4,17 @@ # target_os_version: "目标系统版本代号" (可选,如果不指定则适用于所有系统) # chooses: [{choose: <选项ID>, desc: <选项描述>}] + # 为不同的 Ubuntu 版本定义具体的测试配置 -# Ubuntu 18.04 (bionic) - Melodic +################################################################################################################################################################## +# Ubuntu 18.04 (bionic) - name: "Install_ROS_bionic_source1" target_os_version: "bionic" chooses: - { choose: 1, desc: "一键安装(推荐):ROS(支持ROS/ROS2,树莓派Jetson)" } - { choose: 2, desc: "不更换系统源再继续安装" } - - { choose: 1, desc: "清华源" } - - { choose: 1, desc: "支持的第一个ros版本" } + - { choose: 1, desc: "中科大源" } + - { choose: 5, desc: "支持的第5个ros版本" } # 中科大源不支持Ubuntu18.04安装ros2-bouncy - { choose: 2, desc: "基础版(小)" } - name: "Install_ROS_bionic_source2" @@ -20,8 +22,8 @@ chooses: - { choose: 1, desc: "一键安装(推荐):ROS(支持ROS/ROS2,树莓派Jetson)" } - { choose: 2, desc: "不更换系统源再继续安装" } - - { choose: 2, desc: "中科大源" } - - { choose: 1, desc: "支持的第一个ros版本" } + - { choose: 2, desc: "清华源" } + - { choose: 5, desc: "支持的第5个ros版本" } - { choose: 2, desc: "基础版(小)" } - name: "Install_ROS_bionic_source3" @@ -30,7 +32,7 @@ - { choose: 1, desc: "一键安装(推荐):ROS(支持ROS/ROS2,树莓派Jetson)" } - { choose: 2, desc: "不更换系统源再继续安装" } - { choose: 3, desc: "华为源" } - - { choose: 1, desc: "支持的第一个ros版本" } + - { choose: 5, desc: "支持的第5个ros版本" } - { choose: 2, desc: "基础版(小)" } - name: "Install_ROS_bionic_source4" @@ -39,7 +41,7 @@ - { choose: 1, desc: "一键安装(推荐):ROS(支持ROS/ROS2,树莓派Jetson)" } - { choose: 2, desc: "不更换系统源再继续安装" } - { choose: 4, desc: "中山大学源" } - - { choose: 1, desc: "支持的第一个ros版本" } + - { choose: 5, desc: "支持的第5个ros版本" } - { choose: 2, desc: "基础版(小)" } - name: "Install_ROS_bionic_source5" @@ -48,10 +50,11 @@ - { choose: 1, desc: "一键安装(推荐):ROS(支持ROS/ROS2,树莓派Jetson)" } - { choose: 2, desc: "不更换系统源再继续安装" } - { choose: 5, desc: "ROS官方源" } - - { choose: 1, desc: "支持的第一个ros版本" } + - { choose: 5, desc: "支持的第5个ros版本" } - { choose: 2, desc: "基础版(小)" } -# Ubuntu 20.04 (focal) - Noetic 或 Foxy +################################################################################################################################################################## +# Ubuntu 20.04 (focal) - name: "Install_ROS_focal_noetic" target_os_version: "focal" chooses: @@ -78,7 +81,8 @@ # - { choose: 1, desc: "支持的第一个ros版本" } # - { choose: 2, desc: "基础版(小)" } -# Ubuntu 22.04 (jammy) - Humble +################################################################################################################################################################## +# Ubuntu 22.04 (jammy) - name: "Install_ROS_jammy" target_os_version: "jammy" chooses: @@ -88,7 +92,8 @@ - { choose: 1, desc: "支持的第一个ros版本" } - { choose: 2, desc: "基础版(小)" } -# Ubuntu 24.04 (noble) - Jazzy +################################################################################################################################################################## +# Ubuntu 24.04 (noble) - name: "Install_ROS_noble" target_os_version: "noble" chooses: diff --git a/tests/test_runner.py b/tests/test_runner.py index 39968e1..538bdc9 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -10,6 +10,13 @@ import re import argparse +# 尝试导入select模块,用于非阻塞I/O操作 +try: + import select + HAVE_SELECT = True +except ImportError: + HAVE_SELECT = False + # 将项目根目录添加到 Python 路径中,以便能找到 tools 模块 sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) @@ -285,17 +292,83 @@ def run_install_test(test_case): env={**os.environ, 'FISH_INSTALL_CONFIG': '../fish_install.yaml'} ) - # 实时打印输出 + # 实时打印输出,添加3分钟无日志超时检测 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() + last_output_time = time.time() + timeout_seconds = 180 # 3分钟超时 + + if HAVE_SELECT: + # 在支持select的系统上使用非阻塞I/O + while True: + # 检查是否超时 + if time.time() - last_output_time > timeout_seconds: + print(f"错误: 测试 {name} 超时,超过 {timeout_seconds} 秒无日志输出,自动退出") + # 终止进程 + process.terminate() + try: + process.wait(timeout=10) # 等待最多10秒优雅退出 + except subprocess.TimeoutExpired: + process.kill() # 强制杀死进程 + process.wait() + output += f"\n错误: 测试超时,超过 {timeout_seconds} 秒无日志输出,自动退出" + return False, output + + # 检查进程是否已经结束 + if process.poll() is not None: + # 读取剩余的所有输出 + remaining_output = process.stdout.read() + if remaining_output: + print(remaining_output.strip()) + output += remaining_output + last_output_time = time.time() # 更新最后输出时间 + break + + # 非阻塞读取输出 + if select.select([process.stdout], [], [], 0.1)[0]: + output_line = process.stdout.readline() + if output_line: + print(output_line.strip()) + output += output_line + last_output_time = time.time() # 更新最后输出时间 + # 确保实时刷新输出 + sys.stdout.flush() + elif process.poll() is not None: + # 进程结束且没有更多输出 + break + else: + # 没有可读取的数据,短暂休眠 + time.sleep(0.1) + else: + # 在不支持select的系统上使用标准方法(如Windows) + while True: + # 检查是否超时 + if time.time() - last_output_time > timeout_seconds: + print(f"错误: 测试 {name} 超时,超过 {timeout_seconds} 秒无日志输出,自动退出") + # 终止进程 + process.terminate() + try: + process.wait(timeout=10) # 等待最多10秒优雅退出 + except subprocess.TimeoutExpired: + process.kill() # 强制杀死进程 + process.wait() + output += f"\n错误: 测试超时,超过 {timeout_seconds} 秒无日志输出,自动退出" + return False, output + + # 检查进程是否已经结束 + if process.poll() is not None: + break + + # 使用poll方法检查是否有数据可读 + output_line = process.stdout.readline() + if output_line: + print(output_line.strip()) + output += output_line + last_output_time = time.time() # 更新最后输出时间 + # 确保实时刷新输出 + sys.stdout.flush() + else: + # 没有输出,短暂休眠 + time.sleep(0.1) print("=== 脚本输出结束 ===") # 等待进程结束,超时时间为 2 小时 @@ -356,6 +429,7 @@ def main(): # 解析命令行参数 parser = argparse.ArgumentParser(description='运行一键安装工具测试') parser.add_argument('--target-os-version', type=str, help='目标Ubuntu版本代号 (例如: bionic, focal, jammy, noble)') + parser.add_argument('--test-index', type=int, help='特定测试用例的索引') args = parser.parse_args() target_os_version = args.target_os_version @@ -398,6 +472,23 @@ def main(): print("错误: 没有找到适用于所有系统的通用测试用例") sys.exit(1) + # 如果指定了测试用例索引,则只运行该索引的测试用例 + if args.test_index is not None: + # 直接根据索引在所有测试用例中查找 + if 0 <= args.test_index < len(all_test_cases): + target_test_case = all_test_cases[args.test_index] + # 确保该测试用例适用于当前系统版本 + if (target_test_case.get('target_os_version') == target_os_version or + (target_os_version and 'target_os_version' not in target_test_case)): + test_cases = [target_test_case] + print(f"只运行指定索引的测试用例: {target_test_case.get('name', 'Unknown Test')}") + else: + print(f"错误: 索引为 {args.test_index} 的测试用例不适用于系统版本 {target_os_version}") + sys.exit(1) + else: + print(f"错误: 测试用例索引 {args.test_index} 超出范围 (0-{len(all_test_cases)-1})") + sys.exit(1) + print(f"共找到 {len(test_cases)} 个适用于当前系统版本的测试用例") # 运行所有测试用例并收集结果 @@ -457,12 +548,14 @@ def main(): print(f"失败: {failed}") print(f"总计: {len(test_cases)}") + # 根据是否有测试失败,设置适当的退出码 + # 在并行测试环境中,这将由每个单独的job处理 if failed > 0: print("部分测试失败,请检查日志和测试报告。") - sys.exit(1) + sys.exit(1) # 有测试失败,返回非零退出码 else: print("所有测试通过!") - sys.exit(0) + sys.exit(0) # 所有测试通过,返回零退出码 if __name__ == "__main__": main() \ No newline at end of file diff --git a/tools/tool_install_ros.py b/tools/tool_install_ros.py index ba43c84..18f4d4e 100644 --- a/tools/tool_install_ros.py +++ b/tools/tool_install_ros.py @@ -58,9 +58,10 @@ def get_version(name): @staticmethod def install_depend(name): depends = RosVersions.get_version(name).deps - for dep in depends: - AptUtils.install_pkg(dep) - + if depends: + # 批量安装依赖包,提高效率 + dep_string = " ".join(depends) + AptUtils.install_pkg(dep_string) @staticmethod def tip_test_command(name): @@ -90,61 +91,59 @@ def get_desktop_version(name): "ustc":{"ROS1":"https://mirrors.ustc.edu.cn/ros/ubuntu/","ROS2":"https://mirrors.ustc.edu.cn/ros2/ubuntu/"}, "huawei":{"ROS1":"https://repo.huaweicloud.com/ros/ubuntu/","ROS2":"https://repo.huaweicloud.com/ros2/ubuntu/"}, "packages.ros":{"ROS1":"http://packages.ros.org/ros/ubuntu/","ROS2":"http://packages.ros.org/ros2/ubuntu/"}, - "https.packages.ros":{"ROS1":"https://packages.ros.org/ros/ubuntu/","ROS2":"https://packages.ros.org/ros2/ubuntu/"}, - "repo-ros2":{"ROS2":"http://repo.ros2.org/ubuntu/"} } ros_dist_dic = { - 'artful':{"packages.ros"}, - 'bionic':{"tsinghua","ustc","huawei","mirrorz","packages.ros","https.packages.ros"}, - 'buster':{"packages.ros"}, - 'cosmic':{"packages.ros"}, - 'disco':{"packages.ros"}, - 'eoan':{"packages.ros"}, - 'focal':{"tsinghua","ustc","huawei","mirrorz","packages.ros","https.packages.ros"}, - 'jessie':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, - 'lucid':{"packages.ros"}, - 'maverick':{"packages.ros"}, - 'natty':{"packages.ros"}, - 'oneiric':{"packages.ros"}, - 'precise':{"packages.ros"}, - 'quantal':{"packages.ros"}, - 'raring':{"packages.ros"}, - 'saucy':{"packages.ros"}, - 'stretch':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, - 'trusty':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, - 'utopic':{"packages.ros"}, - 'vivid':{"packages.ros"}, - 'wheezy':{"packages.ros"}, - 'wily':{"packages.ros"}, - 'xenial':{"tsinghua","ustc","huawei","mirrorz","packages.ros","https.packages.ros"}, - 'yakkety':{"packages.ros"}, - 'zesty':{"packages.ros"}, + 'artful': {"tsinghua", "ustc", "huawei", "packages.ros", }, + 'bionic': {"tsinghua", "ustc", "huawei", "mirrorz", "packages.ros", }, + 'buster': {"tsinghua", "ustc", "huawei", "mirrorz", "packages.ros", }, + 'cosmic': {"tsinghua", "ustc", "huawei", "packages.ros", }, + 'disco': {"tsinghua", "ustc", "huawei", "packages.ros", }, + 'eoan': {"tsinghua", "ustc", "huawei", "packages.ros", }, + 'focal': {"tsinghua", "ustc", "huawei", "mirrorz", "packages.ros", }, + 'jessie': {"tsinghua", "ustc", "huawei", "mirrorz", "packages.ros", }, + 'lucid': {"tsinghua", "ustc", "huawei", "packages.ros", }, + 'maverick': {"tsinghua", "ustc", "huawei", "packages.ros", }, + 'natty': {"tsinghua", "ustc", "huawei", "packages.ros", }, + 'oneiric': {"tsinghua", "ustc", "huawei", "packages.ros", }, + 'precise': {"tsinghua", "ustc", "huawei", "packages.ros", }, + 'quantal': {"tsinghua", "ustc", "huawei", "packages.ros", }, + 'raring': {"tsinghua", "ustc", "huawei", "packages.ros", }, + 'saucy': {"tsinghua", "ustc", "huawei", "packages.ros", }, + 'stretch': {"tsinghua", "ustc", "huawei", "mirrorz", "packages.ros", }, + 'trusty': {"tsinghua", "ustc", "huawei", "mirrorz", "packages.ros", }, + 'utopic': {"tsinghua", "ustc", "huawei", "packages.ros", }, + 'vivid': {"tsinghua", "ustc", "huawei", "packages.ros", }, + 'wheezy': {"tsinghua", "ustc", "huawei", "packages.ros", }, + 'wily': {"tsinghua", "ustc", "huawei", "packages.ros", }, + 'xenial': {"tsinghua", "ustc", "huawei", "mirrorz", "packages.ros", }, + 'yakkety': {"tsinghua", "ustc", "huawei", "packages.ros", }, + 'zesty': {"tsinghua", "ustc", "huawei", "packages.ros", }, } ros2_dist_dic = { - 'bionic':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, - 'bullseye':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, - 'buster':{"packages.ros"}, - 'cosmic':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, - 'disco':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, - 'eoan':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, - 'focal':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, - 'jessie':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, - 'jammy':{"tsinghua","ustc","huawei","mirrorz","packages.ros","https.packages.ros"}, - 'noble':{"tsinghua","ustc","huawei","mirrorz","packages.ros","https.packages.ros"}, - 'stretch':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, - 'trusty':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, - 'utopic':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, - 'xenial':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, - 'yakkety':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, - 'zesty':{"tsinghua","ustc","huawei","packages.ros","https.packages.ros"}, + 'bionic': {"tsinghua", "mirrorz", "huawei", "packages.ros", }, + 'bookworm': {"tsinghua", "ustc", "huawei", "mirrorz", "packages.ros", }, + 'bullseye': {"tsinghua", "ustc", "huawei", "mirrorz", "packages.ros", }, + 'buster': {"tsinghua", "ustc", "huawei", "packages.ros", }, + 'cosmic': {"packages.ros", }, + 'disco': {"packages.ros", }, + 'eoan': {"packages.ros", }, + 'focal': {"tsinghua", "ustc", "huawei", "mirrorz", "packages.ros", }, + 'jessie': {"tsinghua", "ustc", "huawei", "packages.ros", }, + 'jammy': {"tsinghua", "ustc", "huawei", "mirrorz", "packages.ros", }, + 'noble': {"tsinghua", "ustc", "huawei", "mirrorz", "packages.ros", }, + 'stretch': {"tsinghua", "ustc", "huawei", "packages.ros", }, + 'trixie': {"tsinghua", "ustc", "huawei", "mirrorz", "packages.ros", }, + 'trusty': {"tsinghua", "ustc", "huawei", "packages.ros", }, + 'xenial': {"tsinghua", "ustc", "huawei", "mirrorz", "packages.ros", }, + 'utopic': {"packages.ros", }, + 'yakkety': {"packages.ros", }, + 'zesty': {"packages.ros", }, } - - class Tool(BaseTool): def __init__(self): self.name = "一键安装ROS和ROS2,支持树莓派Jetson" From db7d0e5cc4ce7626bdd71db73121f5cf43f0ddc4 Mon Sep 17 00:00:00 2001 From: dennis <488132230@qq.com> Date: Fri, 31 Oct 2025 21:15:30 +0800 Subject: [PATCH 18/18] =?UTF-8?q?fix(tests):=20=E6=9B=B4=E6=96=B0fish?= =?UTF-8?q?=E5=AE=89=E8=A3=85=E6=B5=8B=E8=AF=95=E9=85=8D=E7=BD=AE=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E4=B8=AD=E7=9A=84ROS=E7=89=88=E6=9C=AC=E6=8F=8F?= =?UTF-8?q?=E8=BF=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/fish_install_test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/fish_install_test.yaml b/tests/fish_install_test.yaml index cc592b5..ad30f5f 100644 --- a/tests/fish_install_test.yaml +++ b/tests/fish_install_test.yaml @@ -14,7 +14,7 @@ - { choose: 1, desc: "一键安装(推荐):ROS(支持ROS/ROS2,树莓派Jetson)" } - { choose: 2, desc: "不更换系统源再继续安装" } - { choose: 1, desc: "中科大源" } - - { choose: 5, desc: "支持的第5个ros版本" } # 中科大源不支持Ubuntu18.04安装ros2-bouncy + - { choose: 5, desc: "支持的第5个ros版本" } # 中科大,清华,华为,中山大学源不支持Ubuntu18.04安装ros2-bouncy 其余ros2没测试 - { choose: 2, desc: "基础版(小)" } - name: "Install_ROS_bionic_source2"