初次创建仓库提交代码
1. 已经构建好了架子了。 2. 添加了示例的插件
This commit is contained in:
141
tests/data/customer_test.yaml
Normal file
141
tests/data/customer_test.yaml
Normal file
@ -0,0 +1,141 @@
|
||||
ConfigList:
|
||||
- ipc_iford.nor.glibc-11.1.0-squashfs.ssc029a.512.bga12_ddr4_defconfig
|
||||
- ipc-rtos_iford.spinand.glibc-11.1.0-ramdisk.ssc029a.512.bga12_ddr4_defconfig
|
||||
MiReleaseList: []
|
||||
MhalReleaseList: []
|
||||
|
||||
RtosReleaseList:
|
||||
- coremark
|
||||
- common
|
||||
- font
|
||||
- iqserver
|
||||
- disp_app
|
||||
- dualos_pipeline
|
||||
- aov_preload
|
||||
- cm4_preload
|
||||
- preload_rtos
|
||||
- preload_sample
|
||||
- lvgl
|
||||
- usb_gadget_app
|
||||
- aesdma
|
||||
- bdma
|
||||
- camclk
|
||||
- cpufreq
|
||||
- drvutil
|
||||
- emac
|
||||
- fcie
|
||||
- flash
|
||||
- fsp_qspi
|
||||
- gpio
|
||||
- i2c
|
||||
- input
|
||||
- int
|
||||
- ir
|
||||
- kernel
|
||||
- loadns
|
||||
- miu
|
||||
- mmupte
|
||||
- mspi
|
||||
- msys
|
||||
- padmux
|
||||
- pwm
|
||||
- rtc
|
||||
- rtcpwc
|
||||
- saradc
|
||||
- sdmmc
|
||||
- str
|
||||
- timer
|
||||
- tsensor
|
||||
- uart
|
||||
- voltage
|
||||
- watchdog
|
||||
- algo
|
||||
- decompress
|
||||
- freertos_plus_fat
|
||||
- lwfs
|
||||
- proxyfs
|
||||
- tcpip
|
||||
- arm
|
||||
- riscv
|
||||
- libc
|
||||
- newlib_stub
|
||||
- cam_dev_wrapper
|
||||
- cam_drv_poll
|
||||
- cam_fs_wrapper
|
||||
- cam_os_wrapper
|
||||
- cam_proc_wrapper
|
||||
- env_util
|
||||
- initcall
|
||||
- loadable_module
|
||||
- memmang
|
||||
- memmap
|
||||
- mempool
|
||||
- MsWrapper
|
||||
- newlib_stub
|
||||
- freertos_main
|
||||
- freertos_posix
|
||||
- sys_I_SW
|
||||
|
||||
|
||||
PmRtosReleaseList:
|
||||
- pm_aesdma
|
||||
- pm_bdma
|
||||
- pm_camclk
|
||||
- pm_cpufreq
|
||||
- pm_drvutil
|
||||
- pm_loadns
|
||||
- pm_emac
|
||||
- pm_flash
|
||||
- pm_fsp_qspi
|
||||
- pm_gpio
|
||||
- pm_i2c
|
||||
- pm_int
|
||||
- pm_ir
|
||||
- pm_kernel
|
||||
- pm_miu
|
||||
- pm_mmupte
|
||||
- pm_mspi
|
||||
- pm_msys
|
||||
- pm_padmux
|
||||
- pm_pcie
|
||||
- pm_pl011
|
||||
- pm_mhal_pm_clk
|
||||
- pm_pm_idle
|
||||
- pm_mhal_pm_mbx
|
||||
- pm_rtcpwc
|
||||
- pm_mhal_pm_sys
|
||||
- pm_mhal_pm_wdt
|
||||
- pm_pspi
|
||||
- pm_pwm
|
||||
- pm_pm_power
|
||||
- pm_rtc
|
||||
- pm_rtcpwc
|
||||
- pm_saradc
|
||||
- pm_sdmmc
|
||||
- pm_timer
|
||||
- pm_tsensor
|
||||
- pm_uart
|
||||
- pm_voltage
|
||||
- pm_watchdog
|
||||
- pm_decompress
|
||||
- pm_freertos_plus_fat
|
||||
- pm_lwfs
|
||||
- pm_proxyfs
|
||||
- pm_tcpip
|
||||
- pm_arm
|
||||
- pm_riscv
|
||||
- pm_cam_dev_wrapper
|
||||
- pm_cam_drv_poll
|
||||
- pm_cam_fs_wrapper
|
||||
- pm_cam_os_wrapper
|
||||
- pm_cam_proc_wrapper
|
||||
- pm_env_util
|
||||
- pm_initcall
|
||||
- pm_libc
|
||||
- pm_memmang
|
||||
- pm_memmap
|
||||
- pm_mempool
|
||||
- pm_MsWrapper
|
||||
- pm_freertos
|
||||
- pm_freertos_posix
|
||||
- pm_sys_I_SW
|
133
tests/data/test.mk
Normal file
133
tests/data/test.mk
Normal file
@ -0,0 +1,133 @@
|
||||
# 这是一个测试用的复杂 Makefile 示例
|
||||
# 包含了各种 Makefile 语法特性用于测试解析器
|
||||
|
||||
# 简单变量赋值
|
||||
SIMPLE_VAR = simple value
|
||||
|
||||
# 不同分隔符的变量赋值
|
||||
COLON_VAR := immediately expanded value
|
||||
|
||||
# 多行变量定义
|
||||
MULTILINE_VAR = first line \
|
||||
second line \
|
||||
third line
|
||||
|
||||
# 带注释的变量定义
|
||||
WITH_COMMENT = value with trailing comment # 这是一个注释
|
||||
|
||||
# 定义函数
|
||||
define MULTI_LINE_FUNCTION
|
||||
echo "This is line 1"
|
||||
echo "This is line 2"
|
||||
echo "This is line 3"
|
||||
endef
|
||||
|
||||
# 使用 += 追加变量内容
|
||||
APPEND_VAR = initial value
|
||||
APPEND_VAR += appended value
|
||||
|
||||
# 条件语句
|
||||
ifdef DEBUG
|
||||
LOG_LEVEL = debug
|
||||
else
|
||||
LOG_LEVEL = info
|
||||
endif
|
||||
|
||||
# 使用函数
|
||||
SOURCES = main.c util.c driver.c
|
||||
OBJECTS = $(patsubst %.c,%.o,$(SOURCES))
|
||||
|
||||
# 空变量定义
|
||||
EMPTY_VAR =
|
||||
|
||||
# 带特殊字符的变量定义
|
||||
SPECIAL_CHARS = $$HOME/bin:/usr/local/bin:$$PATH
|
||||
|
||||
# 引用其他变量
|
||||
COMPOUND_VAR = $(SIMPLE_VAR) and $(COLON_VAR)
|
||||
|
||||
# 内容中包含等号的变量
|
||||
EQUAL_IN_VALUE = key1=value1 key2=value2
|
||||
|
||||
# 目标定义
|
||||
all: build test
|
||||
|
||||
# 带多条命令的目标
|
||||
build:
|
||||
@echo "Building..." # 注释
|
||||
$(CC) -o output $(SOURCES)
|
||||
@echo "Build complete"
|
||||
|
||||
# 依赖于多个目标的规则
|
||||
test: unit_test integration_test
|
||||
@echo "All tests completed"
|
||||
|
||||
# 模式规则
|
||||
%.o: %.c
|
||||
$(CC) -c -o $@ $<
|
||||
|
||||
# 伪目标
|
||||
.PHONY: all clean test
|
||||
|
||||
# 复杂的多行命令
|
||||
clean:
|
||||
@echo "Cleaning up..."
|
||||
rm -f *.o
|
||||
rm -f output
|
||||
@echo "Clean complete"
|
||||
|
||||
# 变量中包含换行
|
||||
NEWLINE := line1$(shell echo)line2
|
||||
|
||||
# 带续行符的shell命令
|
||||
test_shell:
|
||||
for file in $(SOURCES); do \
|
||||
echo "Processing $$file"; \
|
||||
cat $$file | grep "TODO"; \
|
||||
done
|
||||
|
||||
# 使用 ?= 条件赋值
|
||||
CC ?= gcc
|
||||
|
||||
# 使用 != 执行命令并赋值
|
||||
CURRENT_DIR != pwd
|
||||
|
||||
# 包含其他 Makefile
|
||||
-include optional.mk
|
||||
include required.mk
|
||||
|
||||
# 导出变量到环境变量
|
||||
export PATH
|
||||
export JAVA_HOME = /usr/lib/jvm/default-java
|
||||
|
||||
# 嵌套使用变量
|
||||
NESTED = $($(VARNAME))
|
||||
|
||||
# 使用自动变量
|
||||
auto_vars: file1.c file2.c
|
||||
@echo "First prerequisite: $<"
|
||||
@echo "All prerequisites: $^"
|
||||
@echo "Target name: $@"
|
||||
|
||||
# 使用通配符
|
||||
HEADERS = $(wildcard *.h)
|
||||
|
||||
# 使用 vpath 指令
|
||||
vpath %.c src
|
||||
vpath %.h include
|
||||
|
||||
# 静态模式规则
|
||||
MODULES = mod1 mod2 mod3
|
||||
$(MODULES:%=%.o): %.o: %.c
|
||||
$(CC) -c -o $@ $<
|
||||
|
||||
# 字符串替换
|
||||
TEXT = Hello World
|
||||
REPLACED = $(subst Hello,Goodbye,$(TEXT))
|
||||
|
||||
# shell 命令输出作为变量内容
|
||||
VERSION := $(shell git describe --tags)
|
||||
|
||||
# 路径处理
|
||||
SRC_PATH = src/module/file.c
|
||||
FILE_NAME = $(notdir $(SRC_PATH))
|
52
tests/data/test_defconfig
Normal file
52
tests/data/test_defconfig
Normal file
@ -0,0 +1,52 @@
|
||||
# 这是一个测试用的复杂 defconfig 文件示例
|
||||
# 包含了各种 defconfig 语法特性用于测试解析器
|
||||
|
||||
# 简单的键值对,使用等号
|
||||
CONFIG_SIMPLE = y
|
||||
|
||||
# 使用等号前后有空格的键值对
|
||||
CONFIG_WITH_SPACES = y
|
||||
|
||||
# 没有空格的键值对
|
||||
CONFIG_NO_SPACES=y
|
||||
|
||||
# 带有数值的配置
|
||||
CONFIG_INT_VALUE = 100
|
||||
CONFIG_HEX_VALUE = 0x1000
|
||||
CONFIG_OCTAL_VALUE = 0755
|
||||
|
||||
# 带有字符串的配置
|
||||
CONFIG_STRING = "hello world"
|
||||
CONFIG_STRING_NO_QUOTES = hello world
|
||||
|
||||
# 带有特殊字符的配置
|
||||
CONFIG_PATH = /usr/local/bin:/usr/bin:/bin
|
||||
CONFIG_SPECIAL_CHARS = $HOME/.config
|
||||
|
||||
# 带有注释的配置
|
||||
CONFIG_WITH_COMMENT = y # 这是一个注释
|
||||
|
||||
# 多种格式混合
|
||||
CONFIG_MIXED = "value with spaces" # 和注释
|
||||
|
||||
# 空值配置
|
||||
CONFIG_EMPTY =
|
||||
|
||||
# 带有布尔值的配置
|
||||
CONFIG_BOOL_TRUE = y
|
||||
CONFIG_BOOL_FALSE = n
|
||||
|
||||
# 带有多行内容的配置(实际 defconfig 中不常见,但为测试解析器的健壮性添加)
|
||||
CONFIG_MULTILINE = first line \
|
||||
second line \
|
||||
third line
|
||||
|
||||
# 含有等号的值
|
||||
CONFIG_WITH_EQUAL = key=value
|
||||
|
||||
# 重复的键(后面的应该覆盖前面的)
|
||||
CONFIG_DUPLICATE = first
|
||||
CONFIG_DUPLICATE = second
|
||||
|
||||
# 长路径配置
|
||||
CONFIG_LONG_PATH = /very/long/path/to/some/file/that/might/wrap/around/in/editor/and/cause/parsing/issues/file.conf
|
41
tests/test_common.py
Normal file
41
tests/test_common.py
Normal file
@ -0,0 +1,41 @@
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
|
||||
import core.plugin as plugin
|
||||
|
||||
|
||||
def test_plugins():
|
||||
plugins = plugin.PluginManager()
|
||||
print("we found {} plugins".format(len(plugins.get_available_plugins())))
|
||||
print(plugins)
|
||||
|
||||
print("Step of BaseLine is:")
|
||||
print(plugins.BaseLine.PhaseEnv.Pre)
|
||||
print(plugins.BaseLine.PhaseEnv.Process)
|
||||
print(plugins.BaseLine.PhaseEnv.Post)
|
||||
|
||||
|
||||
def test_ReleaseStep():
|
||||
from core.defines import ReleasePhase, ProcessStep
|
||||
for release_step in ReleasePhase:
|
||||
print(release_step.value)
|
||||
for process_step in ProcessStep:
|
||||
class_name = f" _{release_step.value}_{process_step.value}"
|
||||
print(class_name)
|
||||
|
||||
from database import DirAlias
|
||||
def test_config():
|
||||
print("DirAlias.mi_sys: ", DirAlias.mi_sys)
|
||||
assert DirAlias.mi_sys
|
||||
|
||||
|
||||
def test_demo_list_default():
|
||||
from database import DemoListDefault
|
||||
DemoListDefault.check()
|
||||
print(f"DemoListDefault: {len(DemoListDefault)}")
|
||||
assert not DemoListDefault
|
||||
|
||||
|
||||
def test_config_single_item():
|
||||
from database import REPO_URL
|
||||
assert REPO_URL == "http://hcgit04:9080/manifest/alkaid"
|
232
tests/test_config_expend.py
Normal file
232
tests/test_config_expend.py
Normal file
@ -0,0 +1,232 @@
|
||||
from database import *
|
||||
|
||||
def test_black_list_expand():
|
||||
# 如下的参考数据来源于 原本的脚本里面的 BLACK_LIST_DEFAULT
|
||||
ref = """
|
||||
sdk/interface/include/aio/
|
||||
sdk/interface/include/ai/
|
||||
sdk/interface/src/ai/
|
||||
sdk/impl/ai/
|
||||
sdk/interface/src/aio
|
||||
sdk/impl/aio
|
||||
sdk/interface/include/ao/
|
||||
sdk/interface/src/ao/
|
||||
sdk/impl/ao/
|
||||
sdk/interface/include/alsa/
|
||||
sdk/interface/src/alsa/
|
||||
sdk/impl/alsa/
|
||||
sdk/interface/include/cipher/
|
||||
sdk/interface/src/cipher/
|
||||
sdk/impl/cipher/
|
||||
sdk/interface/src/common
|
||||
sdk/impl/common
|
||||
sdk/interface/include/cus3a/
|
||||
sdk/interface/src/cus3a/
|
||||
sdk/interface/include/debug
|
||||
sdk/interface/src/debug
|
||||
sdk/impl/debug
|
||||
sdk/interface/include/dummy/
|
||||
sdk/interface/src/dummy/
|
||||
sdk/impl/dummy/
|
||||
sdk/interface/include/disp
|
||||
sdk/interface/src/disp
|
||||
sdk/impl/disp
|
||||
sdk/interface/include/dpu/
|
||||
sdk/interface/src/dpu/
|
||||
sdk/impl/dpu/
|
||||
sdk/interface/include/dsp/
|
||||
sdk/interface/src/dsp/
|
||||
sdk/impl/dsp/
|
||||
sdk/interface/include/fb/
|
||||
sdk/interface/src/fb/
|
||||
sdk/impl/fb/
|
||||
sdk/interface/include/gfx/
|
||||
sdk/interface/src/gfx/
|
||||
sdk/impl/gfx/
|
||||
sdk/interface/include/hdmi/
|
||||
sdk/interface/src/hdmi/
|
||||
sdk/impl/hdmi/
|
||||
sdk/interface/include/hdmirx
|
||||
sdk/interface/src/hdmirx
|
||||
sdk/impl/hdmirx
|
||||
sdk/interface/include/hvp
|
||||
sdk/interface/src/hvp
|
||||
sdk/impl/hvp
|
||||
sdk/interface/include/ipu
|
||||
sdk/interface/src/ipu
|
||||
sdk/impl/ipu
|
||||
sdk/interface/include/iqserver/
|
||||
sdk/interface/src/iqserver/
|
||||
sdk/interface/include/ive
|
||||
sdk/interface/src/ive
|
||||
sdk/impl/ive
|
||||
sdk/interface/include/jpd/
|
||||
sdk/interface/src/jpd/
|
||||
sdk/impl/jpd
|
||||
sdk/interface/include/mipitx/
|
||||
sdk/interface/src/mipitx/
|
||||
sdk/impl/mipitx/
|
||||
sdk/interface/include/nir
|
||||
sdk/interface/src/nir
|
||||
sdk/impl/nir
|
||||
sdk/interface/include/panel/
|
||||
sdk/interface/src/panel/
|
||||
sdk/impl/panel/
|
||||
sdk/interface/include/pcie/
|
||||
sdk/interface/src/pcie/
|
||||
sdk/impl/pcie/
|
||||
sdk/interface/include/pspi
|
||||
sdk/interface/src/pspi
|
||||
sdk/impl/pspi
|
||||
sdk/interface/include/rgn
|
||||
sdk/interface/src/rgn
|
||||
sdk/impl/rgn
|
||||
sdk/interface/include/scl
|
||||
sdk/interface/src/scl
|
||||
sdk/impl/scl
|
||||
sdk/interface/include/sed/
|
||||
sdk/interface/src/sed/
|
||||
sdk/interface/include/sensor
|
||||
sdk/interface/src/sensor
|
||||
sdk/impl/sensor
|
||||
sdk/interface/include/shadow/
|
||||
sdk/interface/src/shadow/
|
||||
sdk/impl/shadow/
|
||||
sdk/impl/sys
|
||||
sdk/interface/include/vdec/
|
||||
sdk/interface/src/vdec/
|
||||
sdk/impl/vdec/
|
||||
sdk/interface/include/md/
|
||||
sdk/interface/include/od/
|
||||
sdk/interface/include/vg/
|
||||
sdk/interface/include/vdf/
|
||||
sdk/interface/src/vdf/
|
||||
sdk/interface/src/vcodec
|
||||
sdk/interface/include/vdisp/
|
||||
sdk/interface/src/vdisp/
|
||||
sdk/impl/vdisp/
|
||||
sdk/interface/include/wlan/
|
||||
sdk/interface/src/wlan/
|
||||
rtos/proj/sc/driver/sysdriver/dualos
|
||||
rtos/proj/sc/driver/sysdriver/otp
|
||||
rtos/proj/sc/driver/sysdriver/usb_gadget/udc/usb30
|
||||
rtos/proj/sc/system/rtos/freertos
|
||||
rtos/proj/sc_priv/arch/arm/cortex-a/v7_aarch32/context_switch
|
||||
rtos/proj/sc_priv/arch/arm/cortex-a/v7_aarch32/lh_monitor
|
||||
rtos/proj/sc_priv/driver/sysdriver/dualos
|
||||
rtos/proj/sc_priv/driver/sysdriver/otp
|
||||
rtos/proj/sc_priv/driver/sysdriver/usb_gadget/udc/usb30
|
||||
rtos/proj/sc_priv/system/rtos/freertos
|
||||
pm_rtos/proj/sc/driver/sysdriver/usb_gadget/udc/usb30
|
||||
pm_rtos/proj/sc/driver/camdriver/common
|
||||
pm_rtos/proj/sc/driver/camdriver/pm_aio
|
||||
pm_rtos/proj/sc/driver/camdriver/pm_idle
|
||||
pm_rtos/proj/sc/driver/camdriver/pm_jpe
|
||||
pm_rtos/proj/sc/driver/camdriver/pm_md
|
||||
pm_rtos/proj/sc/driver/camdriver/pm_radar
|
||||
pm_rtos/proj/sc/driver/camdriver/pm_usbpoc
|
||||
pm_rtos/proj/sc/driver/camdriver/pm_vif
|
||||
"""
|
||||
BlackListDefault.check()
|
||||
ret = sorted(set(config_expand(BlackListDefault)))
|
||||
ret = [_.strip('/') for _ in ret]
|
||||
# with open("temp/test_expand.txt", "w") as f:
|
||||
# for item in ret:
|
||||
# item = item.strip('/')
|
||||
# f.write(item + "\n")
|
||||
exp = [_.strip() for _ in ref.split()]
|
||||
exp = sorted(set(exp))
|
||||
exp = [_.strip('/') for _ in exp]
|
||||
# with open("temp/test_expand_ref.txt", "w") as f:
|
||||
# for item in exp:
|
||||
# f.write(item + "\n")
|
||||
assert ret == exp
|
||||
#print("BLACK_LIST_DEFAULT.expand: ", ret)
|
||||
|
||||
def test_black_list_internal_expand():
|
||||
BlackListInternal.check()
|
||||
ref = """
|
||||
sdk/interface/include/isp/
|
||||
sdk/interface/src/isp/
|
||||
sdk/impl/isp/
|
||||
sdk/interface/include/ispalgo/
|
||||
sdk/interface/src/ispalgo/
|
||||
sdk/interface/include/ldc/
|
||||
sdk/interface/src/ldc/
|
||||
sdk/impl/ldc/
|
||||
sdk/interface/include/venc
|
||||
sdk/interface/src/venc
|
||||
sdk/impl/venc
|
||||
sdk/interface/include/vif
|
||||
sdk/interface/src/vif
|
||||
sdk/impl/vif
|
||||
pm_rtos/proj/sc/driver/camdriver/pm_isp/
|
||||
pm_rtos/proj/sc/driver/camdriver/pm_ispalgo/
|
||||
pm_rtos/proj/sc/driver/camdriver/pm_radar_algo/
|
||||
"""
|
||||
ret = sorted(set(config_expand(BlackListInternal)))
|
||||
ret = [_.strip('/') for _ in ret]
|
||||
# with open("temp/test_expand_internal.txt", "w") as f:
|
||||
# for item in ret:
|
||||
# f.write(item + "\n")
|
||||
exp = [_.strip() for _ in ref.split()]
|
||||
exp = sorted(set(exp))
|
||||
exp = [_.strip('/') for _ in exp]
|
||||
# with open("temp/test_expand_ref_internal.txt", "w") as f:
|
||||
# for item in exp:
|
||||
# f.write(item + "\n")
|
||||
assert ret == exp
|
||||
|
||||
def test_config_format():
|
||||
ReleaseOptions.SWBoardAlias = "iford"
|
||||
@AutoInstanceDecorator
|
||||
class TestAlias(BaseConfig):
|
||||
_config = [
|
||||
"preload_sample",
|
||||
]
|
||||
ret = config_expand(TestAlias)
|
||||
assert ret == ["sdk/verify/sample_code/source/iford/preload_sample/rtos"]
|
||||
|
||||
|
||||
def test_config_test_item():
|
||||
@AutoInstanceDecorator
|
||||
class TestAlias(BaseConfig):
|
||||
_config = [
|
||||
"preload_sample",
|
||||
]
|
||||
TestAlias.update(["xxx/xxx/yyy", "zzz/zzz/www", "test_item"])
|
||||
ret = config_expand(TestAlias)
|
||||
# 使用list方式访问
|
||||
assert [_ for _ in ["xxx/xxx/yyy", "zzz/zzz/www", "test_item"] if _ in ret]
|
||||
# 使用dict方式访问
|
||||
assert TestAlias.get("xxx/xxx/yyy") == "xxx/xxx/yyy"
|
||||
assert TestAlias["zzz/zzz/www"] == "zzz/zzz/www"
|
||||
# 使用索引方式修改item 因为这是一个list类型的配置,所以修改了值其key也会跟着修改
|
||||
TestAlias["zzz/zzz/www"] = "zzz/zzz/www_new"
|
||||
assert TestAlias["zzz/zzz/www_new"] == "zzz/zzz/www_new"
|
||||
# 获取不存在的item
|
||||
assert TestAlias.get("zzz/zzz/www", 'default') == 'default'
|
||||
# 特殊访问方式。直接使用一个路径去访问,这样即使这个路径不存在,也不会抛出异常而是返回路径本身
|
||||
assert TestAlias["zzz/zzz/www"] == "zzz/zzz/www"
|
||||
# 通过索引的方式访问不存在的item 会抛出异常
|
||||
try:
|
||||
TestAlias["unexist_item"]
|
||||
except KeyError:
|
||||
assert True
|
||||
else:
|
||||
assert False
|
||||
# 使用点号方式访问
|
||||
assert TestAlias.test_item == "test_item"
|
||||
# 使用点号方式设置
|
||||
TestAlias.test_item = "test_item_new" # 会自动将test_item从_list中删除,并添加test_item_new
|
||||
assert TestAlias.test_item_new == "test_item_new"
|
||||
# 使用点号方式删除后,再次访问会抛出异常
|
||||
try:
|
||||
del TestAlias.test_item_new
|
||||
TestAlias.test_item_new
|
||||
except KeyError:
|
||||
assert True
|
||||
else:
|
||||
assert False
|
||||
# 删除后,使用get方式访问会返回默认值
|
||||
assert TestAlias.get("test_item", "default") == "default"
|
209
tests/test_defconfig_parser.py
Normal file
209
tests/test_defconfig_parser.py
Normal file
@ -0,0 +1,209 @@
|
||||
import sys
|
||||
import os
|
||||
import tempfile
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
|
||||
from utils.utils import DefconfigParser
|
||||
|
||||
def test_defconfig_parser_read():
|
||||
"""测试 DefconfigParser 类的读取功能"""
|
||||
defconfig_path = os.path.join(os.path.dirname(__file__), 'data', 'test_defconfig')
|
||||
parser = DefconfigParser(defconfig_path)
|
||||
temp_file1 = os.path.join(os.path.dirname(__file__), '.tmp', 'test_defconfig_parsed.json')
|
||||
with open(temp_file1, 'w') as f:
|
||||
f.write(f"{parser}")
|
||||
# 测试获取简单键值对
|
||||
assert parser.get('CONFIG_SIMPLE') == 'y'
|
||||
assert parser['CONFIG_SIMPLE'] == 'y' # 使用索引方式访问
|
||||
assert parser.get('CONFIG_WITH_SPACES') == 'y'
|
||||
assert parser.get('CONFIG_NO_SPACES') == 'y'
|
||||
|
||||
# 测试获取数值配置
|
||||
assert parser.get('CONFIG_INT_VALUE') == '100'
|
||||
assert parser.get('CONFIG_HEX_VALUE') == '0x1000'
|
||||
assert parser.get('CONFIG_OCTAL_VALUE') == '0755'
|
||||
|
||||
# 测试获取字符串配置
|
||||
assert parser.get('CONFIG_STRING') == '"hello world"'
|
||||
assert parser.get('CONFIG_STRING_NO_QUOTES') == 'hello world'
|
||||
|
||||
# 测试获取特殊字符配置
|
||||
assert parser.get('CONFIG_PATH') == '/usr/local/bin:/usr/bin:/bin'
|
||||
assert parser.get('CONFIG_SPECIAL_CHARS') == '$HOME/.config'
|
||||
|
||||
# 测试带有注释的配置
|
||||
assert parser.get('CONFIG_WITH_COMMENT') == 'y'
|
||||
|
||||
# 测试混合格式
|
||||
assert parser.get('CONFIG_MIXED') == '"value with spaces"'
|
||||
|
||||
# 测试空值配置
|
||||
assert parser.get('CONFIG_EMPTY') == ''
|
||||
|
||||
# 测试布尔值配置
|
||||
assert parser.get('CONFIG_BOOL_TRUE') == 'y'
|
||||
assert parser.get('CONFIG_BOOL_FALSE') == 'n'
|
||||
|
||||
# 测试多行内容配置 (在实际解析中可能会有所不同,取决于实现)
|
||||
multiline_value = parser.get('CONFIG_MULTILINE')
|
||||
assert 'first line' in multiline_value
|
||||
|
||||
# 测试含有等号的值
|
||||
assert parser.get('CONFIG_WITH_EQUAL') == 'key=value'
|
||||
|
||||
# 测试重复的键 (应该获取最后一个值)
|
||||
assert parser.get('CONFIG_DUPLICATE') == 'second'
|
||||
|
||||
# 测试长路径配置
|
||||
assert '/very/long/path' in parser.get('CONFIG_LONG_PATH')
|
||||
|
||||
# 测试不存在的键
|
||||
assert parser.get('CONFIG_NOT_EXIST', 'default') == 'default'
|
||||
|
||||
def test_defconfig_parser_rw():
|
||||
"""测试 DefconfigParser 类的读写功能,看是否能够正确写回文件"""
|
||||
src_file = os.path.join(os.path.dirname(__file__), 'data', 'test_defconfig')
|
||||
parser = DefconfigParser(src_file)
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
if 1:
|
||||
new_file = os.path.join(os.path.dirname(__file__), '.tmp', 'test_defconfig_copy')
|
||||
else:
|
||||
new_file = os.path.join(temp_dir, 'test_defconfig_copy')
|
||||
# temp_file1 = os.path.join(os.path.dirname(__file__), 'test_defconfig_parsed.json')
|
||||
# with open(temp_file1, 'w') as f:
|
||||
# f.write(f"{parser}")
|
||||
parser.flush(new_file)
|
||||
try:
|
||||
with open(new_file, 'r') as f:
|
||||
with open(src_file, 'r') as f2:
|
||||
# 由于一些格式控制符,导致读取的文件与原文件不一致,这里忽略空格进行比较
|
||||
src = f2.read().replace(' ', '')
|
||||
dst = f.read().replace(' ', '')
|
||||
assert src == dst
|
||||
finally:
|
||||
# 清理临时文件
|
||||
if os.path.exists(temp_dir):
|
||||
shutil.rmtree(temp_dir)
|
||||
|
||||
|
||||
def test_defconfig_parser_modify():
|
||||
"""测试 DefconfigParser 类的修改功能"""
|
||||
# 创建测试文件的副本
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
temp_file = os.path.join(temp_dir, 'test_defconfig_copy')
|
||||
original_file = os.path.join(os.path.dirname(__file__), 'data', 'test_defconfig')
|
||||
shutil.copy(original_file, temp_file)
|
||||
|
||||
try:
|
||||
parser = DefconfigParser(temp_file)
|
||||
|
||||
# 修改现有配置
|
||||
parser['CONFIG_SIMPLE'] = 'n'
|
||||
parser['CONFIG_INT_VALUE'] = '200'
|
||||
parser['CONFIG_STRING'] = '"modified string"'
|
||||
|
||||
# 添加新配置
|
||||
parser['CONFIG_NEW'] = 'new_value'
|
||||
|
||||
# 删除配置
|
||||
del parser['CONFIG_EMPTY']
|
||||
|
||||
idx = parser.index('CONFIG_SIMPLE')
|
||||
parser.insert(idx, 'CONFIG_SIMPLE_NEW', '=', 'new_value_new')
|
||||
# 刷新到文件
|
||||
parser.flush()
|
||||
|
||||
# temp_file1 = os.path.join(os.path.dirname(__file__), 'test_defconfig_parsed.json')
|
||||
# with open(temp_file1, 'w') as f:
|
||||
# f.write(f"{parser}")
|
||||
|
||||
# 重新加载验证修改
|
||||
new_parser = DefconfigParser(temp_file)
|
||||
assert new_parser.get('CONFIG_SIMPLE') == 'n'
|
||||
assert new_parser.get('CONFIG_INT_VALUE') == '200'
|
||||
assert new_parser.get('CONFIG_STRING') == '"modified string"'
|
||||
assert new_parser.get('CONFIG_NEW') == 'new_value'
|
||||
assert new_parser.get('CONFIG_EMPTY') is None # 删除的配置
|
||||
assert new_parser.get('CONFIG_SIMPLE_NEW') == 'new_value_new' # 插入的配置
|
||||
finally:
|
||||
# 清理临时文件
|
||||
if os.path.exists(temp_dir):
|
||||
shutil.rmtree(temp_dir)
|
||||
|
||||
def test_defconfig_parser_content_preservation():
|
||||
"""测试 DefconfigParser 类是否保留原始文件的注释和结构"""
|
||||
# 创建测试文件的副本
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
temp_file = os.path.join(temp_dir, 'test_defconfig_copy')
|
||||
original_file = os.path.join(os.path.dirname(__file__), 'data', 'test_defconfig')
|
||||
shutil.copy(original_file, temp_file)
|
||||
|
||||
try:
|
||||
# 记录原始文件的行数和注释行数
|
||||
with open(temp_file, 'r') as f:
|
||||
original_lines = f.readlines()
|
||||
original_line_count = len(original_lines)
|
||||
original_comment_count = sum(1 for line in original_lines if line.strip().startswith('#'))
|
||||
|
||||
# 进行一些简单修改
|
||||
parser = DefconfigParser(temp_file)
|
||||
parser['CONFIG_SIMPLE'] = 'n'
|
||||
parser.flush()
|
||||
|
||||
# 检查修改后的文件结构是否保留
|
||||
with open(temp_file, 'r') as f:
|
||||
modified_lines = f.readlines()
|
||||
modified_line_count = len(modified_lines)
|
||||
modified_comment_count = sum(1 for line in modified_lines if line.strip().startswith('#'))
|
||||
|
||||
# 行数应该大致相同
|
||||
assert abs(original_line_count - modified_line_count) < 5
|
||||
|
||||
# 注释应该完全保留
|
||||
assert original_comment_count == modified_comment_count
|
||||
finally:
|
||||
# 清理临时文件
|
||||
if os.path.exists(temp_dir):
|
||||
shutil.rmtree(temp_dir)
|
||||
|
||||
def test_edge_cases():
|
||||
"""测试 DefconfigParser 类的边缘情况处理"""
|
||||
# 创建一个临时的 defconfig 文件,包含一些边缘情况
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
temp_file = os.path.join(temp_dir, 'edge_case_defconfig')
|
||||
|
||||
try:
|
||||
# 创建一个包含边缘情况的 defconfig 文件
|
||||
with open(temp_file, 'w') as f:
|
||||
f.write("# 空文件开始\n\n")
|
||||
f.write("# 只有注释的行\n")
|
||||
f.write("CONFIG_NO_VALUE=\n") # 无值
|
||||
f.write("CONFIG_SPACES_AROUND_EQUAL = value_with_spaces_around_equal\n") # 等号周围有多个空格
|
||||
f.write("=invalid_line_no_key\n") # 无效行,没有键
|
||||
f.write("invalid_line_no_equal\n") # 无效行,没有等号
|
||||
|
||||
# 解析文件并测试
|
||||
parser = DefconfigParser(temp_file)
|
||||
|
||||
# 检查无值配置
|
||||
assert parser.get('CONFIG_NO_VALUE') == ''
|
||||
|
||||
# 检查等号周围有多个空格的配置
|
||||
assert parser.get('CONFIG_SPACES_AROUND_EQUAL') == 'value_with_spaces_around_equal'
|
||||
|
||||
# 检查无效行处理
|
||||
assert parser.get('invalid_line_no_equal', 'not_found') == 'not_found'
|
||||
|
||||
# 修改并保存
|
||||
parser['CONFIG_NO_VALUE'] = 'now_has_value'
|
||||
parser.flush()
|
||||
|
||||
# 重新加载并验证
|
||||
new_parser = DefconfigParser(temp_file)
|
||||
assert new_parser.get('CONFIG_NO_VALUE') == 'now_has_value'
|
||||
finally:
|
||||
# 清理临时文件
|
||||
if os.path.exists(temp_dir):
|
||||
shutil.rmtree(temp_dir)
|
442
tests/test_flow.py
Normal file
442
tests/test_flow.py
Normal file
@ -0,0 +1,442 @@
|
||||
from pathlib import Path
|
||||
import pytest
|
||||
import sys, os
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
|
||||
from thirdparty.py_trees.trees import BehaviourTree
|
||||
from core.behavior_tree import ReleaseFlowActionDecorator, ReleaseFlowBuilder, ReleaseFlowAction
|
||||
from thirdparty.py_trees.common import Status, Access
|
||||
from core.logger import get_logger
|
||||
|
||||
log = get_logger()
|
||||
|
||||
class ActionTest(ReleaseFlowAction):
|
||||
"""测试用的Action节点"""
|
||||
def __init__(self, name: str, return_status: Status, description: str = ""):
|
||||
super().__init__(name, description)
|
||||
self.return_status = return_status
|
||||
self.process_called = False
|
||||
self.setup_called = False
|
||||
self.shutdown_called = False
|
||||
|
||||
def setup(self):
|
||||
self.setup_called = True
|
||||
log.info(f"setup: {self.name}")
|
||||
|
||||
def process(self):
|
||||
self.process_called = True
|
||||
self.status = self.return_status
|
||||
log.info(f"process: {self.name}, status: {self.status}")
|
||||
|
||||
def update(self):
|
||||
return self.status
|
||||
|
||||
def shutdown(self):
|
||||
self.shutdown_called = True
|
||||
|
||||
class ActionTestBlackboard(ReleaseFlowAction):
|
||||
"""测试Blackboard功能的Action节点"""
|
||||
def __init__(self, name: str, description: str = ""):
|
||||
super().__init__(name, description)
|
||||
self.blackboard.register_key("test_key", access=Access.WRITE)
|
||||
self.blackboard.register_key("read_only_key", access=Access.READ)
|
||||
|
||||
def process(self):
|
||||
self.status = Status.SUCCESS
|
||||
log.info(f"process: {self.name}, status: {self.status}, test_key: {self.blackboard.test_key}")
|
||||
|
||||
def update(self):
|
||||
return self.status
|
||||
|
||||
class ActionTestBlackboard1(ReleaseFlowAction):
|
||||
"""测试Blackboard功能的Action节点"""
|
||||
def __init__(self, name: str, description: str = ""):
|
||||
super().__init__(name, description)
|
||||
self.blackboard.require_key("test_key")
|
||||
|
||||
def process(self):
|
||||
self.status = Status.SUCCESS
|
||||
log.info(f"process: {self.name}, status: {self.status}, test_key: {self.blackboard.test_key}")
|
||||
|
||||
def update(self):
|
||||
return self.status
|
||||
|
||||
@pytest.fixture
|
||||
def builder():
|
||||
"""创建ReleaseFlowBuilder实例的fixture"""
|
||||
return ReleaseFlowBuilder()
|
||||
|
||||
def test_single_action():
|
||||
"""测试单个Action节点的执行"""
|
||||
action = ActionTest("test_action", Status.SUCCESS)
|
||||
action.setup()
|
||||
assert action.setup_called
|
||||
|
||||
action.tick_once()
|
||||
assert action.process_called
|
||||
assert action.status == Status.SUCCESS
|
||||
|
||||
action.shutdown()
|
||||
assert action.shutdown_called
|
||||
|
||||
def test_sequence(builder):
|
||||
"""测试Sequence节点的执行"""
|
||||
success_action = ActionTest("success_action", Status.SUCCESS)
|
||||
failure_action = ActionTest("failure_action", Status.FAILURE)
|
||||
|
||||
with builder.sequence("test_sequence") as b:
|
||||
b.action(success_action)
|
||||
b.action(failure_action)
|
||||
|
||||
flow = builder.build()
|
||||
tree = BehaviourTree(root=flow)
|
||||
tree.setup()
|
||||
tree.tick()
|
||||
|
||||
assert failure_action.process_called
|
||||
assert flow.status == Status.FAILURE
|
||||
assert success_action.process_called
|
||||
|
||||
def test_selector(builder):
|
||||
"""测试Selector节点的执行"""
|
||||
failure_action = ActionTest("failure_action", Status.FAILURE)
|
||||
success_action = ActionTest("success_action", Status.SUCCESS)
|
||||
|
||||
with builder.selector("test_selector") as b:
|
||||
b.action(failure_action)
|
||||
b.action(success_action)
|
||||
|
||||
flow = builder.build()
|
||||
tree = BehaviourTree(root=flow)
|
||||
tree.setup()
|
||||
tree.tick()
|
||||
|
||||
assert flow.status == Status.SUCCESS
|
||||
assert failure_action.process_called
|
||||
assert success_action.process_called
|
||||
|
||||
def test_blackboard():
|
||||
"""测试Blackboard功能"""
|
||||
action = ActionTestBlackboard("test_blackboard")
|
||||
action1 = ActionTestBlackboard1("test_blackboard_read")
|
||||
|
||||
# 测试写入权限
|
||||
action.blackboard.test_key = "test_value"
|
||||
# 测试共享
|
||||
assert action1.blackboard.test_key == "test_value"
|
||||
|
||||
# 测试只读权限
|
||||
with pytest.raises(AttributeError):
|
||||
action.blackboard.read_only_key = "new_value"
|
||||
|
||||
# 测试必需key
|
||||
with pytest.raises(AttributeError):
|
||||
_ = action.blackboard.required_key
|
||||
|
||||
def test_parallel(builder):
|
||||
"""测试Parallel节点的执行"""
|
||||
success_action1 = ActionTest("success_action1", Status.SUCCESS)
|
||||
success_action2 = ActionTest("success_action2", Status.SUCCESS)
|
||||
failure_action = ActionTest("failure_action", Status.FAILURE)
|
||||
|
||||
with builder.parallel("test_parallel") as b:
|
||||
b.action(success_action1)
|
||||
b.action(success_action2)
|
||||
b.action(failure_action)
|
||||
|
||||
flow = builder.build()
|
||||
tree = BehaviourTree(root=flow)
|
||||
tree.setup()
|
||||
tree.tick()
|
||||
|
||||
assert flow.status == Status.FAILURE
|
||||
assert success_action1.process_called
|
||||
assert success_action2.process_called
|
||||
assert failure_action.process_called
|
||||
|
||||
def test_nested_behavior_tree(builder):
|
||||
"""测试嵌套的行为树结构"""
|
||||
success_action1 = ActionTest("success_action1", Status.SUCCESS)
|
||||
success_action2 = ActionTest("success_action2", Status.SUCCESS)
|
||||
failure_action = ActionTest("failure_action", Status.FAILURE)
|
||||
|
||||
with builder.sequence("root_sequence") as b:
|
||||
with b.selector("nested_selector") as s:
|
||||
s.action(failure_action)
|
||||
s.action(success_action1)
|
||||
b.action(success_action2)
|
||||
|
||||
flow = builder.build()
|
||||
tree = BehaviourTree(root=flow)
|
||||
tree.setup()
|
||||
tree.tick()
|
||||
|
||||
assert flow.status == Status.SUCCESS
|
||||
assert failure_action.process_called
|
||||
assert success_action1.process_called
|
||||
assert success_action2.process_called
|
||||
|
||||
def test_parallel_success_on_all(builder):
|
||||
"""测试并行节点SuccessOnAll策略"""
|
||||
success_action1 = ActionTest("success_action1", Status.SUCCESS)
|
||||
success_action2 = ActionTest("success_action2", Status.SUCCESS)
|
||||
success_action3 = ActionTest("success_action3", Status.SUCCESS)
|
||||
|
||||
with builder.parallel("test_parallel_success_on_all") as b:
|
||||
b.action(success_action1)
|
||||
b.action(success_action2)
|
||||
b.action(success_action3)
|
||||
|
||||
flow = builder.build()
|
||||
tree = BehaviourTree(root=flow)
|
||||
tree.setup()
|
||||
tree.tick()
|
||||
|
||||
assert flow.status == Status.SUCCESS
|
||||
assert success_action1.process_called
|
||||
assert success_action2.process_called
|
||||
assert success_action3.process_called
|
||||
|
||||
def test_parallel_success_on_one(builder):
|
||||
"""测试并行节点SuccessOnOne策略"""
|
||||
from thirdparty.py_trees.common import ParallelPolicy
|
||||
|
||||
running_action1 = ActionTest("running_action1", Status.RUNNING)
|
||||
success_action = ActionTest("success_action", Status.SUCCESS)
|
||||
running_action2 = ActionTest("running_action2", Status.RUNNING)
|
||||
|
||||
with builder.parallel("test_parallel_success_on_one",
|
||||
policy=ParallelPolicy.SuccessOnOne()) as b:
|
||||
b.action(running_action1)
|
||||
b.action(success_action)
|
||||
b.action(running_action2)
|
||||
|
||||
flow = builder.build()
|
||||
tree = BehaviourTree(root=flow)
|
||||
tree.setup()
|
||||
tree.tick()
|
||||
|
||||
assert flow.status == Status.SUCCESS
|
||||
assert running_action1.process_called
|
||||
assert success_action.process_called
|
||||
assert running_action2.process_called
|
||||
|
||||
def test_multiple_executions(builder):
|
||||
"""测试行为树的多次执行"""
|
||||
action1 = ActionTest("action1", Status.SUCCESS)
|
||||
action2 = ActionTest("action2", Status.SUCCESS)
|
||||
|
||||
with builder.sequence("test_multiple_executions") as b:
|
||||
b.action(action1)
|
||||
b.action(action2)
|
||||
|
||||
flow = builder.build()
|
||||
tree = BehaviourTree(root=flow)
|
||||
tree.setup()
|
||||
|
||||
# 第一次执行
|
||||
tree.tick()
|
||||
assert flow.status == Status.SUCCESS
|
||||
assert action1.process_called
|
||||
assert action2.process_called
|
||||
|
||||
# 重置状态
|
||||
action1.process_called = False
|
||||
action2.process_called = False
|
||||
|
||||
# 第二次执行
|
||||
tree.tick()
|
||||
assert flow.status == Status.SUCCESS
|
||||
assert action1.process_called
|
||||
assert action2.process_called
|
||||
|
||||
def test_error_handling(builder):
|
||||
"""测试行为树的错误处理"""
|
||||
# 测试空的行为树
|
||||
with pytest.raises(Exception) as excinfo:
|
||||
builder.build()
|
||||
assert "No nodes added to behavior tree" in str(excinfo.value)
|
||||
# 测试未闭合的节点
|
||||
with pytest.raises(Exception) as excinfo:
|
||||
with builder.sequence("unclosed_sequence") as b:
|
||||
b.action(ActionTest("action", Status.SUCCESS))
|
||||
b.action(ActionTest("action", Status.SUCCESS))
|
||||
# 不调用end()或使用with语句的__exit__
|
||||
builder.build()
|
||||
assert "Unclosed nodes" in str(excinfo.value)
|
||||
|
||||
|
||||
|
||||
|
||||
class ActionWithError(ReleaseFlowAction):
|
||||
"""测试异常处理的Action节点"""
|
||||
def __init__(self, name: str, description: str = ""):
|
||||
super().__init__(name, description)
|
||||
self.process_called = False
|
||||
|
||||
def process(self):
|
||||
self.process_called = True
|
||||
raise Exception("测试异常")
|
||||
|
||||
def update(self):
|
||||
return Status.FAILURE
|
||||
|
||||
def test_action_exception_handling(builder):
|
||||
"""测试Action节点异常处理"""
|
||||
error_action = ActionWithError("error_action")
|
||||
|
||||
with builder.sequence("test_error_handling") as b:
|
||||
b.action(error_action)
|
||||
|
||||
flow = builder.build()
|
||||
tree = BehaviourTree(root=flow)
|
||||
tree.setup()
|
||||
|
||||
# 执行时应该捕获异常并返回FAILURE状态
|
||||
tree.tick()
|
||||
assert error_action.process_called
|
||||
assert flow.status == Status.FAILURE
|
||||
|
||||
def test_decorator():
|
||||
"""测试ReleaseFlowActionDecorator装饰器"""
|
||||
@ReleaseFlowActionDecorator
|
||||
class DecoratedAction(ReleaseFlowAction):
|
||||
def process(self):
|
||||
self.status = Status.SUCCESS
|
||||
|
||||
def update(self):
|
||||
return self.status
|
||||
|
||||
# 测试自动生成名称
|
||||
action1 = DecoratedAction()
|
||||
assert action1.name == f"{Path(__file__).stem}.DecoratedAction"
|
||||
|
||||
# 测试自定义名称
|
||||
action2 = DecoratedAction(name="custom_name")
|
||||
assert action2.name == "custom_name"
|
||||
|
||||
# 测试功能
|
||||
action2.setup()
|
||||
action2.tick_once()
|
||||
assert action2.status == Status.SUCCESS
|
||||
|
||||
def test_complex_behavior_tree(builder):
|
||||
"""测试复杂的行为树结构"""
|
||||
success_action1 = ActionTest("success_action1", Status.SUCCESS)
|
||||
success_action2 = ActionTest("success_action2", Status.SUCCESS)
|
||||
failure_action1 = ActionTest("failure_action1", Status.FAILURE)
|
||||
success_action3 = ActionTest("success_action3", Status.SUCCESS)
|
||||
|
||||
with builder.sequence("root") as b:
|
||||
with b.parallel("parallel_group") as p:
|
||||
p.action(success_action1)
|
||||
p.action(success_action2)
|
||||
with b.selector("selector_group") as s:
|
||||
s.action(failure_action1)
|
||||
s.action(success_action3)
|
||||
|
||||
flow = builder.build()
|
||||
tree = BehaviourTree(root=flow)
|
||||
tree.setup()
|
||||
tree.tick()
|
||||
|
||||
assert flow.status == Status.SUCCESS
|
||||
assert success_action1.process_called
|
||||
assert success_action2.process_called
|
||||
assert failure_action1.process_called
|
||||
assert success_action3.process_called
|
||||
|
||||
def test_blackboard_advanced():
|
||||
"""测试Blackboard的高级功能"""
|
||||
class ActionWrite(ReleaseFlowAction):
|
||||
def __init__(self, name: str, description: str = ""):
|
||||
super().__init__(name, description)
|
||||
self.blackboard.register_key("counter", access=Access.WRITE)
|
||||
self.blackboard.counter = 0
|
||||
|
||||
def process(self):
|
||||
self.blackboard.counter += 1
|
||||
self.status = Status.SUCCESS
|
||||
|
||||
def update(self):
|
||||
return self.status
|
||||
|
||||
class ActionRead(ReleaseFlowAction):
|
||||
def __init__(self, name: str, description: str = ""):
|
||||
super().__init__(name, description)
|
||||
self.blackboard.register_key("counter", access=Access.READ)
|
||||
self.value = 0
|
||||
|
||||
def process(self):
|
||||
self.value = self.blackboard.counter
|
||||
self.status = Status.SUCCESS
|
||||
|
||||
def update(self):
|
||||
return self.status
|
||||
|
||||
# 创建动作节点
|
||||
write_action = ActionWrite("write_action")
|
||||
read_action = ActionRead("read_action")
|
||||
|
||||
# 设置初始值
|
||||
write_action.blackboard.counter = 10
|
||||
|
||||
# 测试读取
|
||||
assert read_action.blackboard.counter == 10
|
||||
|
||||
# 测试写入
|
||||
write_action.blackboard.counter = 20
|
||||
assert read_action.blackboard.counter == 20
|
||||
|
||||
# 测试只读权限
|
||||
with pytest.raises(AttributeError):
|
||||
read_action.blackboard.counter = 30
|
||||
|
||||
def test_action_exception_handling_with_sequence(builder):
|
||||
"""测试Action节点异常处理后行为树继续执行的能力"""
|
||||
error_action = ActionWithError("error_action")
|
||||
success_action = ActionTest("success_action", Status.SUCCESS)
|
||||
|
||||
with builder.sequence("test_error_handling_sequence") as b:
|
||||
b.action(error_action)
|
||||
b.action(success_action)
|
||||
|
||||
flow = builder.build()
|
||||
tree = BehaviourTree(root=flow)
|
||||
tree.setup()
|
||||
|
||||
# 执行时应该捕获异常并返回FAILURE状态,但不会中断行为树的执行
|
||||
tree.tick()
|
||||
|
||||
# 验证error_action被标记为已处理
|
||||
assert error_action.process_called
|
||||
|
||||
# 验证序列节点状态为FAILURE(因为第一个节点失败)
|
||||
assert flow.status == Status.FAILURE
|
||||
|
||||
# 验证success_action没有被执行(因为序列节点在第一个节点失败后停止)
|
||||
assert not success_action.process_called
|
||||
|
||||
def test_action_exception_handling_with_selector(builder):
|
||||
"""测试Action节点异常处理后选择器继续尝试其他节点"""
|
||||
error_action = ActionWithError("error_action")
|
||||
success_action = ActionTest("success_action", Status.SUCCESS)
|
||||
|
||||
with builder.selector("test_error_handling_selector") as b:
|
||||
b.action(error_action)
|
||||
b.action(success_action)
|
||||
|
||||
flow = builder.build()
|
||||
tree = BehaviourTree(root=flow)
|
||||
tree.setup()
|
||||
|
||||
# 执行时应该捕获异常并继续尝试下一个节点
|
||||
tree.tick()
|
||||
|
||||
# 验证error_action被标记为已处理
|
||||
assert error_action.process_called
|
||||
|
||||
# 验证success_action被执行
|
||||
assert success_action.process_called
|
||||
|
||||
# 验证选择器节点状态为SUCCESS(因为第二个节点成功)
|
||||
assert flow.status == Status.SUCCESS
|
141
tests/test_makefile_parser.py
Normal file
141
tests/test_makefile_parser.py
Normal file
@ -0,0 +1,141 @@
|
||||
import sys
|
||||
import os
|
||||
import tempfile
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
|
||||
from utils.utils import MakefileParser
|
||||
|
||||
def test_makefile_parser_read():
|
||||
"""测试 MakefileParser 类的读取功能"""
|
||||
parser = MakefileParser(os.path.join(os.path.dirname(__file__), "data", "test.mk"))
|
||||
with open(os.path.join(os.path.dirname(__file__), '.tmp', 'test_parsed.json'), 'w') as f:
|
||||
f.write(f"{parser}")
|
||||
# 测试获取简单变量
|
||||
assert parser.get('SIMPLE_VAR') == ['simple', 'value']
|
||||
|
||||
# 测试获取使用不同分隔符的变量
|
||||
assert parser.get('COLON_VAR') == ['immediately', 'expanded', 'value']
|
||||
|
||||
# 测试获取多行变量
|
||||
multiline = parser.get('MULTILINE_VAR')
|
||||
assert len(multiline) == 6
|
||||
# 测试带注释的变量
|
||||
assert parser.get('WITH_COMMENT')[0].startswith('value')
|
||||
|
||||
# 测试不存在的变量返回默认值
|
||||
assert parser.get('NON_EXISTENT', 'default') == 'default'
|
||||
|
||||
# 测试复合变量
|
||||
assert parser.get('COMPOUND_VAR') == ['$(SIMPLE_VAR)', 'and', '$(COLON_VAR)']
|
||||
|
||||
# 测试空变量
|
||||
assert parser.get('EMPTY_VAR') == []
|
||||
|
||||
def test_makefile_parser_rw():
|
||||
"""测试 MakefileParser 类的读写功能,看是否能够正确写回文件"""
|
||||
src_file = os.path.join(os.path.dirname(__file__), "data", "test.mk")
|
||||
parser = MakefileParser(src_file)
|
||||
# new_file = os.path.join(os.path.dirname(__file__), 'test_rw.mk')
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
new_file = os.path.join(temp_dir, 'test_copy.mk')
|
||||
shutil.copy(os.path.join(os.path.dirname(__file__), "data", "test.mk"), new_file)
|
||||
|
||||
parser.flush(new_file)
|
||||
try:
|
||||
with open(new_file, 'r') as f:
|
||||
with open(src_file, 'r') as f2:
|
||||
# 由于一些格式控制符,导致读取的文件与原文件不一致,这里忽略空格进行比较
|
||||
src = f2.read().replace(' ', '')
|
||||
dst = f.read().replace(' ', '')
|
||||
assert src == dst
|
||||
finally:
|
||||
# 清理临时文件
|
||||
if os.path.exists(temp_dir):
|
||||
shutil.rmtree(temp_dir)
|
||||
|
||||
def test_makefile_parser_modify():
|
||||
"""测试 MakefileParser 类的修改功能"""
|
||||
# 创建测试文件的副本
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
temp_file = os.path.join(temp_dir, 'test_copy.mk')
|
||||
shutil.copy(os.path.join(os.path.dirname(__file__), "data", "test.mk"), temp_file)
|
||||
|
||||
try:
|
||||
parser = MakefileParser(temp_file)
|
||||
|
||||
# 修改现有变量
|
||||
parser['SIMPLE_VAR'] = 'modified value'
|
||||
|
||||
# 添加新变量
|
||||
parser['NEW_VAR'] = 'new value'
|
||||
|
||||
#在SIMPLE_VAR前面插入新变量
|
||||
# idx = parser.index('SIMPLE_VAR')
|
||||
parser.insert('SIMPLE_VAR', 'NEW_VAR1', ':=', 'new value', '# comment')
|
||||
|
||||
# 添加多行变量
|
||||
parser['NEW_MULTILINE'] = ['line 1', 'line 2', 'line 3']
|
||||
|
||||
# 删除变量
|
||||
del parser['EMPTY_VAR']
|
||||
|
||||
# 使用正则
|
||||
parser['REGEX_VAR'] = 'new value'
|
||||
parser['REGEX_.* := new value'] = 'REGEX_VAR := regex value'
|
||||
|
||||
|
||||
# 刷新到文件
|
||||
parser.flush()
|
||||
with open(os.path.join(os.path.dirname(__file__), '.tmp', 'test_parsed.json'), 'w') as f:
|
||||
f.write(f"{parser}")
|
||||
# 重新加载解析器验证修改
|
||||
new_parser = MakefileParser(temp_file)
|
||||
assert new_parser.get('SIMPLE_VAR') == ['modified', 'value']
|
||||
assert new_parser.get('NEW_VAR') == ['new', 'value']
|
||||
assert new_parser.get('NEW_VAR1') == ['new', 'value']
|
||||
assert len(new_parser.get('NEW_MULTILINE')) == 6
|
||||
assert new_parser.get('EMPTY_VAR') is None
|
||||
assert new_parser.get('REGEX_VAR') == ['regex', 'value']
|
||||
del new_parser['REGEX_VAR']
|
||||
assert new_parser.get('REGEX_VAR') is None
|
||||
finally:
|
||||
# 清理临时文件
|
||||
if os.path.exists(temp_dir):
|
||||
shutil.rmtree(temp_dir)
|
||||
|
||||
def test_makefile_parser_content_preservation():
|
||||
"""测试 MakefileParser 类是否保留原始文件的注释和结构"""
|
||||
# 创建测试文件的副本
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
temp_file = os.path.join(temp_dir, 'test_copy.mk')
|
||||
shutil.copy(os.path.join(os.path.dirname(__file__), "data", "test.mk"), temp_file)
|
||||
|
||||
try:
|
||||
# 记录原始文件内容的行数和注释行数
|
||||
with open(temp_file, 'r') as f:
|
||||
original_lines = f.readlines()
|
||||
original_line_count = len(original_lines)
|
||||
original_comment_count = sum(1 for line in original_lines if line.strip().startswith('#'))
|
||||
|
||||
# 使用解析器进行一些简单修改
|
||||
parser = MakefileParser(temp_file)
|
||||
parser['SIMPLE_VAR'] = 'modified but preserving structure'
|
||||
parser.flush()
|
||||
|
||||
# 检查修改后的文件是否保留了原始结构
|
||||
with open(temp_file, 'r') as f:
|
||||
modified_lines = f.readlines()
|
||||
modified_line_count = len(modified_lines)
|
||||
modified_comment_count = sum(1 for line in modified_lines if line.strip().startswith('#'))
|
||||
|
||||
assert original_line_count == modified_line_count
|
||||
|
||||
# 注释应该完全保留
|
||||
assert original_comment_count == modified_comment_count
|
||||
|
||||
finally:
|
||||
# 清理临时文件
|
||||
if os.path.exists(temp_dir):
|
||||
shutil.rmtree(temp_dir)
|
291
tests/test_release_options.py
Normal file
291
tests/test_release_options.py
Normal file
@ -0,0 +1,291 @@
|
||||
import sys
|
||||
import os
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
|
||||
from database.options import ReleaseOptions
|
||||
|
||||
def test_basic_required_args():
|
||||
"""测试基本必需参数"""
|
||||
# 构建必需参数
|
||||
test_args = [
|
||||
'--SWBoardAlias', 'testboard',
|
||||
'--TagId', 'v1.0.0',
|
||||
'--SnapShotXml', 'snapshot.xml',
|
||||
'--SWBoardType', 'boardtype',
|
||||
'--SWProductType', 'producttype'
|
||||
]
|
||||
|
||||
# 解析参数
|
||||
ReleaseOptions.parse(test_args)
|
||||
|
||||
# 验证必需参数被正确解析 使用属性的方式访问
|
||||
assert ReleaseOptions.SWBoardAlias == 'testboard'
|
||||
assert ReleaseOptions.TagId == 'v1.0.0'
|
||||
assert ReleaseOptions.SnapShotXml == 'snapshot.xml'
|
||||
assert ReleaseOptions.SWBoardType == 'boardtype'
|
||||
assert ReleaseOptions.SWProductType == 'producttype'
|
||||
|
||||
# 验证非必需参数使用默认值
|
||||
assert ReleaseOptions['ThreadNum'] == 1
|
||||
assert ReleaseOptions['IplVersion'] == 'CN'
|
||||
assert ReleaseOptions['SkipCheckout'] is False
|
||||
assert ReleaseOptions['OpenSource'] is False
|
||||
|
||||
assert ReleaseOptions.check() is True
|
||||
|
||||
|
||||
def test_bool_args():
|
||||
"""测试布尔类型的参数"""
|
||||
test_args = [
|
||||
'--SWBoardAlias', 'testboard',
|
||||
'--TagId', 'v1.0.0',
|
||||
'--SnapShotXml', 'snapshot.xml',
|
||||
'--SWBoardType', 'boardtype',
|
||||
'--SWProductType', 'producttype',
|
||||
'--SkipCheckout',
|
||||
'--OpenSource',
|
||||
'--RiscVSupport',
|
||||
'--Cm4Support',
|
||||
'--Doc2Pdf',
|
||||
'--Doc2Html',
|
||||
'--VerifyBuild',
|
||||
'--ReduceCodeSize',
|
||||
'--SkipSyncCode'
|
||||
]
|
||||
|
||||
ReleaseOptions.parse(test_args)
|
||||
|
||||
# 验证布尔参数被正确解析
|
||||
assert ReleaseOptions['SkipCheckout'] is True
|
||||
assert ReleaseOptions['OpenSource'] is True
|
||||
assert ReleaseOptions['RiscVSupport'] is True
|
||||
assert ReleaseOptions['Cm4Support'] is True
|
||||
assert ReleaseOptions['Doc2Pdf'] is True
|
||||
assert ReleaseOptions['Doc2Html'] is True
|
||||
assert ReleaseOptions['VerifyBuild'] is True
|
||||
assert ReleaseOptions['ReduceCodeSize'] is True
|
||||
assert ReleaseOptions['SkipSyncCode'] is True
|
||||
|
||||
assert ReleaseOptions.check() is True
|
||||
|
||||
|
||||
def test_optional_args():
|
||||
"""测试可选参数"""
|
||||
test_args = [
|
||||
'--SWBoardAlias', 'testboard',
|
||||
'--TagId', 'v1.0.0',
|
||||
'--SnapShotXml', 'snapshot.xml',
|
||||
'--SWBoardType', 'boardtype',
|
||||
'--SWProductType', 'producttype',
|
||||
'--DocsPath', '/path/to/docs',
|
||||
'--HWChipType', 'chiptype',
|
||||
'--HWPackageType', 'package',
|
||||
'--OutPath', 'output_dir',
|
||||
'--ThreadNum', '8',
|
||||
'--IplVersion', 'WW'
|
||||
]
|
||||
|
||||
ReleaseOptions.parse(test_args)
|
||||
|
||||
# 验证可选参数被正确解析
|
||||
assert ReleaseOptions['DocsPath'] == '/path/to/docs'
|
||||
assert ReleaseOptions['HWChipType'] == 'chiptype'
|
||||
assert ReleaseOptions['HWPackageType'] == 'package'
|
||||
assert ReleaseOptions['OutPath'] == 'output_dir'
|
||||
assert ReleaseOptions['ThreadNum'] == 8
|
||||
assert ReleaseOptions['IplVersion'] == 'WW'
|
||||
|
||||
assert ReleaseOptions.check() is True
|
||||
|
||||
|
||||
def test_mix_args():
|
||||
"""测试混合使用短格式和长格式参数"""
|
||||
test_args = [
|
||||
'-a', 'testboard',
|
||||
'-t', 'v1.0.0',
|
||||
'-s', 'snapshot.xml',
|
||||
'-b', 'boardtype',
|
||||
'-p', 'producttype',
|
||||
'-d', '/path/to/docs',
|
||||
'-j', '4',
|
||||
'-i', # SkipCheckout
|
||||
'-f' # OpenSource
|
||||
]
|
||||
|
||||
ReleaseOptions.parse(test_args)
|
||||
|
||||
# 验证短格式参数被正确解析
|
||||
assert ReleaseOptions['SWBoardAlias'] == 'testboard'
|
||||
assert ReleaseOptions['TagId'] == 'v1.0.0'
|
||||
assert ReleaseOptions['DocsPath'] == '/path/to/docs'
|
||||
assert ReleaseOptions['ThreadNum'] == 4
|
||||
assert ReleaseOptions['SkipCheckout'] is True
|
||||
assert ReleaseOptions['OpenSource'] is True
|
||||
|
||||
assert ReleaseOptions.check() is True
|
||||
|
||||
|
||||
def test_missing_required_args():
|
||||
"""测试缺少必需参数的情况"""
|
||||
# 缺少必需参数 SWBoardType
|
||||
test_args = [
|
||||
'--SWBoardAlias', 'testboard',
|
||||
'--TagId', 'v1.0.0',
|
||||
'--SnapShotXml', 'snapshot.xml',
|
||||
'--SWProductType', 'producttype'
|
||||
]
|
||||
|
||||
try:
|
||||
ReleaseOptions.parse(test_args)
|
||||
except SystemExit as e:
|
||||
assert True
|
||||
|
||||
|
||||
def test_invalid_thread_num():
|
||||
"""测试无效的线程数"""
|
||||
test_args = [
|
||||
'--SWBoardAlias', 'testboard',
|
||||
'--TagId', 'v1.0.0',
|
||||
'--SnapShotXml', 'snapshot.xml',
|
||||
'--SWBoardType', 'boardtype',
|
||||
'--SWProductType', 'producttype',
|
||||
'--ThreadNum', '-1' # 无效的线程数
|
||||
]
|
||||
|
||||
ReleaseOptions.parse(test_args)
|
||||
|
||||
# 线程数被解析但无效
|
||||
assert ReleaseOptions['ThreadNum'] == -1
|
||||
|
||||
# 检查应该失败
|
||||
try:
|
||||
ReleaseOptions.check()
|
||||
except ValueError as e:
|
||||
assert str(e) == "ThreadNum 必须为正整数"
|
||||
|
||||
|
||||
def test_ipl_version_choices():
|
||||
"""测试 IplVersion 的选择限制"""
|
||||
# 备份原始的 ReleaseOptions 实例
|
||||
original_instance = ReleaseOptions._instance
|
||||
|
||||
try:
|
||||
# 有效的 IplVersion: 'CN'
|
||||
test_args = [
|
||||
'--SWBoardAlias', 'testboard',
|
||||
'--TagId', 'v1.0.0',
|
||||
'--SnapShotXml', 'snapshot.xml',
|
||||
'--SWBoardType', 'boardtype',
|
||||
'--SWProductType', 'producttype',
|
||||
'--IplVersion', 'CN'
|
||||
]
|
||||
|
||||
ReleaseOptions.parse(test_args)
|
||||
assert ReleaseOptions['IplVersion'] == 'CN'
|
||||
assert ReleaseOptions.check() is True
|
||||
|
||||
# 有效的 IplVersion: 'WW'
|
||||
test_args[-1] = 'WW'
|
||||
ReleaseOptions.parse(test_args)
|
||||
assert ReleaseOptions['IplVersion'] == 'WW'
|
||||
assert ReleaseOptions.check() is True
|
||||
|
||||
# 无效的 IplVersion
|
||||
test_args[-1] = 'INVALID'
|
||||
try:
|
||||
ReleaseOptions.parse(test_args)
|
||||
except SystemExit as e:
|
||||
assert True
|
||||
finally:
|
||||
# 恢复原始的 ReleaseOptions 实例
|
||||
ReleaseOptions._instance = original_instance
|
||||
|
||||
@patch('database.options.get_alkaid_root') # 修改patch路径为正确的模块路径
|
||||
def test_default_alkaid_root_path(mock_get_alkaid_root):
|
||||
"""测试默认的 AlkaidRootPath"""
|
||||
# 设置 mock 返回值
|
||||
mock_get_alkaid_root.return_value = '/mocked/alkaid/root'
|
||||
|
||||
test_args = [
|
||||
'--SWBoardAlias', 'testboard',
|
||||
'--TagId', 'v1.0.0',
|
||||
'--SnapShotXml', 'snapshot.xml',
|
||||
'--SWBoardType', 'boardtype',
|
||||
'--SWProductType', 'producttype'
|
||||
]
|
||||
|
||||
# 重置 ReleaseOptions 实例,确保使用新的 mock
|
||||
ReleaseOptions._instance = None
|
||||
|
||||
# 重新初始化 ReleaseOptions 实例
|
||||
ReleaseOptions.__class__().parse(test_args)
|
||||
|
||||
# 验证 AlkaidRootPath 使用 get_alkaid_root 的返回值
|
||||
assert ReleaseOptions['AlkaidRootPath'] == '/mocked/alkaid/root'
|
||||
mock_get_alkaid_root.assert_called_once()
|
||||
|
||||
|
||||
def test_dict_interface():
|
||||
"""测试字典接口功能"""
|
||||
test_args = [
|
||||
'--SWBoardAlias', 'testboard',
|
||||
'--TagId', 'v1.0.0',
|
||||
'--SnapShotXml', 'snapshot.xml',
|
||||
'--SWBoardType', 'boardtype',
|
||||
'--SWProductType', 'producttype'
|
||||
]
|
||||
|
||||
ReleaseOptions.parse(test_args)
|
||||
|
||||
# 测试 __getitem__
|
||||
assert ReleaseOptions['SWBoardAlias'] == 'testboard'
|
||||
|
||||
# 测试 __setitem__
|
||||
ReleaseOptions['OutPath'] = 'custom_output'
|
||||
assert ReleaseOptions['OutPath'] == 'custom_output'
|
||||
|
||||
# 测试 __contains__
|
||||
assert 'SWBoardAlias' in ReleaseOptions
|
||||
assert 'OutPath' in ReleaseOptions
|
||||
assert 'NonExistentKey' not in ReleaseOptions
|
||||
|
||||
|
||||
def test_mounriver_sdk_with_riscv():
|
||||
"""测试 MounRiverSDK 和 RiscVSupport 的组合"""
|
||||
test_args = [
|
||||
'--SWBoardAlias', 'testboard',
|
||||
'--TagId', 'v1.0.0',
|
||||
'--SnapShotXml', 'snapshot.xml',
|
||||
'--SWBoardType', 'boardtype',
|
||||
'--SWProductType', 'producttype',
|
||||
'--RiscVSupport',
|
||||
'--MounRiverSDK'
|
||||
]
|
||||
|
||||
ReleaseOptions.parse(test_args)
|
||||
|
||||
# 验证两个参数都被正确设置
|
||||
assert ReleaseOptions['RiscVSupport'] is True
|
||||
assert ReleaseOptions['MounRiverSDK'] is True
|
||||
|
||||
assert ReleaseOptions.check() is True
|
||||
|
||||
|
||||
def test_help_method():
|
||||
"""测试 help 方法"""
|
||||
# 备份原始解析器
|
||||
original_parser = getattr(ReleaseOptions, 'parser', None)
|
||||
|
||||
try:
|
||||
# 准备一个 MagicMock 来捕获 print_help 的调用
|
||||
ReleaseOptions.parser = MagicMock()
|
||||
|
||||
# 调用 help 方法
|
||||
ReleaseOptions.help()
|
||||
|
||||
# 验证 print_help 被调用
|
||||
ReleaseOptions.parser.print_help.assert_called_once()
|
||||
finally:
|
||||
# 恢复原始解析器
|
||||
ReleaseOptions.parser = original_parser
|
25
tests/test_yaml.py
Normal file
25
tests/test_yaml.py
Normal file
@ -0,0 +1,25 @@
|
||||
import sys,os
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
|
||||
from thirdparty import yaml
|
||||
|
||||
|
||||
|
||||
|
||||
def test_yaml_base():
|
||||
yaml_str = """
|
||||
name: Alice
|
||||
age: 25
|
||||
hobbies:
|
||||
- reading
|
||||
- hiking
|
||||
"""
|
||||
data = yaml.safe_load(yaml_str) # 推荐使用 safe_load 避免潜在的安全风险
|
||||
print(data, f"{type(data)}")
|
||||
|
||||
|
||||
def test_load_customer_yaml():
|
||||
file = os.path.join(os.path.dirname(__file__), "data", "customer_test.yaml")
|
||||
with open(file, "r") as fp:
|
||||
yaml_str = fp.read()
|
||||
data = yaml.safe_load(yaml_str) # 推荐使用 safe_load 避免潜在的安全风险
|
||||
print(data, f"{type(data)}")
|
Reference in New Issue
Block a user