[i=s] 本帖最后由 begseeder 于 2025-8-15 15:54 编辑 [/i]
#申请原创# @21小跑堂
前言
很多时候我们在编写代码过程中,虽然有模块化的设计痕迹存在,但是总有些疏漏的地方让你没办法完全放心的修改某处代码,一旦发生修改,就会担心是否会影响系统的其他地方或者说系统的某些必要指标,所以会重复的进行一遍又一遍的功能测试以保证每次修改都是对现有系统的升级而不是破坏。
以我目前的了解,有一种叫做 TDD
的开发方式,遵循测试驱动开发的原则,我一开始不理解,没有实现功能怎么测试呢,现在大概了解了,正是因为功能没实现,导致测试不过,才去实现功能的,而且实现功能粒度更细,属于小步快走类型,这种开发方式肯定比先设计后测试的传统方式要效率高的。但我一时间还真转变不过来,只好先预先接触一下测试的开发,因为测试毕竟是软件质量的一种保障措施,包括初始化检测,模块功能验证,决策逻辑验证等。
这篇文章介绍的是在 STM32F103C8T6
工程中使用 Ceedling
进行的代码测试工作,简单介绍一下我使用到的一些测试策略、方式等内容。我会给出几个方面的测试用例,包括初始化、硬件驱动模拟以及协议这3个部分,其他方面的测试都是大同小异,触类旁通的。
技术要点
Ceedling
文章中介绍的测试框架来自于 Ceedling
这个构建系统,他集成了 Unity
、CMock
测试框架,实际上它的作用不只是测试,但我目前也只初步的接触它,使用它进行测试以及生成报告等功能。安装的事项我就不介绍了,官网都有介绍,不过配置的部分网上搜到的教程大多是老版本 0.30.1
,我这里使用的新版本 1.0.1
,有些差异,但配置文件 project.yml
中的注释还是挺清晰的,完全可以对照教程修改,再想深入就需要阅读相关的文档了。这个框架可以很方便的对现有工程进行测试,而且可以接入到CI/CD的体系中,对工程的唯一要求就是可测试性。
可测试的代码一般需要如下要求:
- 单一职责:每个类、模块或者函数只有一个功能,如果功能太多会导致组合过多,测试用例复杂度暴增还不好维护
- 低耦合:一个代码单元对于外部的依赖尽可能的少,不然极其不稳定,在测试的时候没办法单独测试,不好隔离
- 依赖注入:通过外部传入依赖的方式来灵活面对多变情况,同时方便完全控制依赖的行为,这样测试的时候也方便模拟
- 接口化和抽象化:在测试的时候更容易创建和使用测试替身,特别是使用到
mock
时
- 控制副作用:纯函数或方法的行为都是可预测的,并且同样的输入会产生同样的输出,但是如果有修改系统参数或者一些读写操作,无法在测试中实现
- 可观察:需要提供一些表达方式来判断代码的行为是否符合预期
所以在设计阶段就考虑如何 隔离
、模拟
和 观察
,能从根本上提高系统的可测试性,这里也说明一下,以前直接用库函数的方式不适合进行测试,必须做隔离才行,不然只能测试纯逻辑的部分。
Ceedling
通过命令行进行操作,因为是运行在PC主机上,所以不需要目标设备的编译配置,虽然主机测试可能会掩盖目标编译器的缺陷,但它也提供了仿真环境中的编译配置,这方面还有待研究。一般 Windows
情况下默认使用主机的 GCC
编译器就可以了,其他的配置就按实际情况来,比如路径,默认行为等。接下来介绍一下这篇文章中用到的两个测试框架的API方法。
API名称 |
作用 |
TEST_ASSERT_EQUAL |
比较两个值是否相等 |
TEST_ASSERT_NOT_NULL |
判断一个指针是否为空 |
TEST_ASSERT_EQUAL_MEMORY |
比较两个缓存是否一致 |
TEST_ASSERT_GREATER_THAN |
判断一个值是否超过阈值 |
Func_Ignore |
忽略Func的执行 |
Func_IgnoreAndReturn |
期望执行一次Func,忽略参数,并返回指定值 |
Func_Expect |
期望执行一次Func |
Func_ExpectAndReturn |
期望执行一次Func并且返回一个指定值 |
Func_AddCallback |
添加执行Func的回调函数 |
需要注意一点的就是,被mock的函数,只要有机会被执行到,必须显式前置添加期望,或者忽略,否则会报错。
测试策略
一般情况下,软件部分的测试还是比较容易实现的,测试的标准就是让测试结果或状态与你认为正确的、期望的一致就是通过测试,这就要求设计的测试用例要有代表性,主旨就是使用真实的函数功能,然后检查执行功能后的结果是否符合预期,同时呢,在测试时需要尽可能的使用真实功能及其流程来实现测试,而不是以偏概全式的为了通过测试而人为改变环境。以下是一些概况:
- 对于驱动,可以测试初始化后对硬件资源的配置情况是否正确,这个初始化最好是使用真实环境的初始化函数,这样才能保证测试与真实一致
- 完整功能性不好测试,但有部分是可以测试逻辑功能
- 对于直接驱动硬件的部分,需要做好隔离,也就是再封装一层函数后通过mock来模拟
- 如果完全在PC主机测试,那么不要mock
HAL
库文件,因为有些地方依赖一些目标编译器的配置
- 测试的流程尽可能遵循实际功能的执行流程
- 对于硬件的模拟,可以把硬件看作带有输入输出接口的黑盒,测试中只需要进行输入检查和输出处理即可
好了,接下来就是示例内容了,这部分是我实际测试中使用到的用例,也是我在一个工程中第一次用 Ceedling
这样的工具,后期想要继续用下去,并且集成到CI/CD的体系中,这是一个大方向,还在摸索中。
初始化部分
检查初始化流程是否正确
这里的初始化流程指的是各个模块、组件的初始化顺序,有时候会存在严格的执行顺序的要求,同时这样的执行流程检查也适用于正常运行时发生的运行路径检查。示例代码如下:
void setUp(void)
{
/* 必要的测试环境初始化 */
TestInit();
}
void tearDown(void)
{
}
void test_AppInitOrder(void){
/* 先准备好期待的顺序 */
start_module_specific_init_Expect();
other_module_specific_init_Expect();
end_module_specific_init_Expect();
/* 执行真实初始化代码 */
AppInit();
}
这里使用到了 CMock
的 _Expect
函数,视情况是否需要传参,然后这里的初始化流程并不是一定要mock相应的模块产生,而是可表达模块执行的部分函数即可,重点就是符合期望的执行顺序。
检查系统参数是否正确,检查系统环境是否正常
很多时候系统需要有默认参数,也需要一定的运行环境,比如某些对象的接口是否进行了初始化,还有就是对硬件资源的分配是否正确,比如led使用的gpio是否与实际板子使用的配置一致,所以可以检查初始化之后这些系统必需的元素是否正常,示例代码如下:
void setUp(void)
{
/* 必要的测试环境初始化 */
TestInit();
}
void tearDown(void)
{
}
void test_AppInitParam(void){
/* 先准备测试环境 */
someting_Ignore();
/* 执行真实初始化代码 */
AppInit();
/* 检查系统参数与环境 */
/* 检查系统参数 */
TEST_ASSERT_EQUAL(parameter.var1, DEFAULT_VAR1);
TEST_ASSERT_EQUAL(parameter.var2, DEFAULT_VAR2);
/* 检查接口是否初始化 */
TEST_ASSERT_NOT_NULL(module_fun1.Disable);
TEST_ASSERT_NOT_NULL(module_fun2.Enable);
/* 检查bsp资源分配情况 */
TEST_ASSERT_EQUAL(bspled.port,GPIOA);
TEST_ASSERT_EQUAL(bspled.pin,GPIO_PIN_1);
}
驱动部分
驱动部分一般都会使用mock来进行模拟依赖函数,所以隔离被测逻辑与外部依赖是早期就需要做到的,这里不得不提到 TDD
方式的先见之明。同时测试时可能出现单元测试策略可行,但是到集成测试就不好实现的情况,这就只能看情况调整吧,主要的目的就是检查驱动参数是否正确,调用驱动的逻辑部分是否和期望一致。因为我一般都会使用cubeMX生成工程,我没有测试HAL库本身的需求,所以需要对 HAL
库函数进行mock,实际中发现依赖太多,并且会涉及到目标编译器特有的配置问题,所以我的做法是再对HAL库函数封装一层,然后mock封装的函数即可。
模拟adc等传感器
这部分比较好模拟,就是把 ADC
当作一个黑盒,然后指定它的输出即可,也就是控制返回的传感器数据即可进行模拟,然后测试数据处理代码的运行情况,示例代码如下:
void setUp(void)
{
`/* 必要的测试环境初始化 */
TestInit();
}
void tearDown(void)
{
}
void test_SensorDateFilter(void){
float mock_volt = 0;
/* 设置模拟的读取adc数据 */
getADC_ExpectAndReturn(channel1,0x800);
/* 调用实际的转换函数 */
mock_volt = getVoltBaseADC(channel1);
/* 检验是否转换正确 */
TEST_ASSERT_EQUAL(mock_volt,1.65);
}
其中被mock的函数是 getADC
,是被函数 getVoltBaseADC
内部调用的。
模拟e2prom等存储设备
我这里使用的是HAL库的硬件I2C,主要对读写部分的入口参数、读写逻辑进行检测,包括使用的i2c句柄、e2prom的设备地址、读写地址以及大小等。特别是读写地址,在每一次操作过程中是否正确递增,读写次数是否是设定次数,这些都是需要测试的,并且从过程来讲,每一步的参数都是确定的,我需要验证每一步的过程是否都正确,所以通过 for
循环来生成每一步的期望,同时为了更真实的测试,这里使用到了mock回调来模拟真实e2prom的读写,其中使用一个数组表示物理存储,当调用到i2c读取函数时,比如这样 BspAPI_I2C_Read((void *)E2PROM_I2C_HANDLE, E2PROM_DEV_ADDR, SYSCFG_ADDR + i, E2PROM_MEMADD_SIZE, &buf[i], 1, 200);
时,会从模拟存储中的 SYSCFG_ADDR + i
地址处读取相应数据,写入也是同理,我在示例中分别进行了读取操作,用读取缓存与模拟存储进行对比,若一致则表示读取正确,然后设置缓存数据,进行写入操作,用模拟存储与写入缓存对比,若一致则表示写入成功,示例如下:
#define SYSCFG_ADDR 0X100
/* 模拟e2prom存储 */
uint8_t Mock_e2prom[4096] = {0};
/* 定义读e2prom测试回调 */
uint8_t e2prom_read_callback(void *hi2cx, uint16_t devaddr, uint16_t memaddr, uint16_t memaddsize, uint8_t *pdata, uint16_t size, uint32_t tmout, int num_calls)
{
/* 把模拟存储中的值复制到pdata中 */
*pdata = Mock_e2prom[memaddr];
return HAL_OK;
}
/* 定义写e2prom测试回调 */
uint8_t e2prom_write_callback(void *hi2cx, uint16_t devaddr, uint16_t memaddr, uint16_t memaddsize, uint8_t *pdata, uint16_t size, uint32_t tmout, int num_calls)
{
/* 把pdata中的值复制到模拟存储中 */
Mock_e2prom[memaddr] = *pdata;
return HAL_OK;
}
void setUp(void)
{
/* 必要的测试环境初始化 */
mock_bsp_api_base_hal_Init();
BspE2pRom.Init();
}
void tearDown(void)
{
}
void test_Bsp_E2PromRW(void)
{
/* 定义测试环境 */
/* 定义测试缓存,大小为4096字节 */
uint8_t buf[4096];
/* 定义了索引变量i,定义了读写次数控制变量size,为999次*/
uint16_t i = 0, size = 999;
/* 准备读e2prom的测试环境 */
for (i = 0; i < 4096; i++) {
buf[i] = 0;
/* 模拟存储中写入页号 */
Mock_e2prom[i] = i / E2P_ONE_PAGE_BYTES;
}
/* 添加读e2prom回调函数 */
BspAPI_I2C_Read_AddCallback(e2prom_read_callback);
/* 添加写e2prom回调函数 */
BspAPI_I2C_Write_AddCallback(e2prom_write_callback);
/* 批量准备期待的读取测试过程,顺序固定,由地址偏移以及buf偏移来表征 */
for (i = 0; i < size; i++) {
BspAPI_I2C_Read_ExpectAndReturn((void *)E2PROM_I2C_HANDLE, E2PROM_DEV_ADDR, SYSCFG_ADDR + i, E2PROM_MEMADD_SIZE, &buf[i], 1, 200, HAL_OK);
}
/* 实际读取测试,给定起始地址,读取大小以及buf */
BspE2pRom.Read(SYSCFG_ADDR, size, buf);
/* 验证缓存是否正确读取数据 */
TEST_ASSERT_EQUAL_MEMORY(&Mock_e2prom[SYSCFG_ADDR], buf, size);
/* 准备读e2prom的测试环境 */
for (i = 0; i < 4096; i++) {
buf[i] = i % 0xff;
}
/* 批量准备期待的写入测试过程,顺序固定,由地址偏移以及buf偏移来表征 */
for (i = 0; i < size; i++) {
BspAPI_I2C_Write_ExpectAndReturn((void *)E2PROM_I2C_HANDLE, E2PROM_DEV_ADDR, SYSCFG_ADDR + i, E2PROM_MEMADD_SIZE, &buf[i], 1, 200, HAL_OK);
}
/* 实际写入测试,给定起始地址,读取大小以及buf */
BspE2pRom.Write(SYSCFG_ADDR, size, buf);
/* 验证缓存是否正确写入数据 */
TEST_ASSERT_EQUAL_MEMORY(&Mock_e2prom[SYSCFG_ADDR], buf, size);
}
若是测试失败,会给出如下类似提示:

上图表示名为 memaddr
的入参 0x0103
和期望 0x0102
不一致,这时候需要去排查到底是代码逻辑问题还是测试本身有问题呢,我实际排查确实是代码问题,修改代码后重新测试,测试通过。
还可能遇到这样的报错:CMock has run out of memory. Please allocate more
,可以通过在 project.yml
文件中的 :defines:
下的 :test:
下加入 - CMOCK_MEM_DYNAMIC
来解决
测试通过,会有如下测试结果:

协议部分
这部分测试的重点就是检查协议是否正常解析与封装,我这里使用485通信,并且同时支持多个协议,所以可以使用依赖注入的方式来完成多个协议的解析与封装工作,方式类似于这样:Module_U485.GuardParse((void *)&Proto485_a1.Handler, &a1_pback);
,其中 Proto485_a1
是一个协议句柄,携带有这个协议的解析方法和封装方法。其次是通信的接收依赖于中断,这是可以模拟的,实际上就是条件触发,我使用一个中断接收函数,然后模拟串口接收的流程就可以了,其实就是循环调用中断接收函数,循环次数设置成帧长度,实际的流程也不过如此,这里演示2个协议的测试工作,通过为这2个协议分别设置了测试帧,然后按照实际的接收流程,解析流程到封装流程,对每个流程中的状态,进行验证,示例如下:
void setUp(void)
{
/* 485模块初始化 */
Module_U485.Init();
/* 协议1初始化 */
Proto485_a1.Init();
/* 协议2初始化 */
Proto485_a2.Init();
}
void tearDown(void)
{
}
/* 协议1测试 */
void test_Proto485_a1(void)
{
/* 定义协议1的反馈变量 */
int a1_pback = 0;
/* 定义485事件组变量 */
UartRecvEvntGrup_u u485_evnt = {.Block = 0};
uint16_t tmp = 0;
uint16_t i = 0;
uint16_t buf1_crc = 0;
uint16_t buf2_crc = 0;
int ret = 0;
/* 定义协议1模拟帧1,设置寄存器地址错误 */
uint8_t buf1[8] = {START_BYTE_SYMBL, FUNCODE_03, 0x00, 0x20, 0x00, 0x03, 0x04, 0x32};
/* 定义协议1模拟帧2,设置无误 */
uint8_t buf2[8] = {START_BYTE_SYMBL, FUNCODE_04, 0x75, 0x40, 0x00, 0x03, 0xab, 0xe0};
uint8_t *pbuf = NULL;
/* 获取协议1模拟帧1长度 */
tmp = sizeof(buf1);
/* 模拟串口接收中断调用 */
for (i = 0; i < tmp; i++) {
Module_U485.PrivateArea.Uart485.RecvIRQ(&(Module_U485.PrivateArea.Uart485), &buf1[i], U485_BUF_MAX);
}
/* 模拟串口接收空闲中断调用 */
Module_U485.PrivateArea.Uart485.RecvIRQ(&(Module_U485.PrivateArea.Uart485), NULL, U485_BUF_MAX);
/* 检查接收长度是否正确 */
TEST_ASSERT_EQUAL(Module_U485.GetRecvCnt(), tmp);
/* 获取接收buf */
pbuf = Module_U485.GetBuf();
/* 检查接收buf是否正常 */
TEST_ASSERT_NOT_NULL(pbuf);
/* 检查接收buf数据内容是否正常 */
TEST_ASSERT_EQUAL_MEMORY(pbuf, buf1, sizeof(buf1));
/* 获取485事件组 */
u485_evnt = Module_U485.GetEvnt();
/* 检查485事件组中接收完成是否置1,否则输出错误信息 */
TEST_ASSERT_EQUAL_MESSAGE(u485_evnt.Reg.Finished, 1, "finish is not 1");
/* 传入协议a1句柄和a1的反馈变量,进行协议a1的解析工作 */
Module_U485.GuardParse((void *)&Proto485_a1.Handler, &a1_pback);
/* 检测是否识别帧1中的寄存器地址错误问题 */
TEST_ASSERT_EQUAL(MERR_REGADDR, hd_pback);
/* 清除本次接收状态 */
Module_U485.ClearStatus();
/* 检查是否清除接收长度 */
TEST_ASSERT_EQUAL(Module_U485.GetRecvCnt(), 0);
/* 检查是否清除接收状态 */
TEST_ASSERT_EQUAL(Module_U485.GetRecvStatus(), 0);
/* 开始构造第二帧的接收,过程与上一帧一样 */
tmp = sizeof(buf2);
/* 模拟串口接收中断调用 */
for (i = 0; i < tmp; i++) {
Module_U485.PrivateArea.Uart485.RecvIRQ(&(Module_U485.PrivateArea.Uart485), &buf2[i], U485_BUF_MAX);
}
/* 模拟串口接收空闲中断调用 */
Module_U485.PrivateArea.Uart485.RecvIRQ(&(Module_U485.PrivateArea.Uart485), NULL, U485_BUF_MAX);
/* 检查接收长度是否正确 */
TEST_ASSERT_EQUAL(Module_U485.GetRecvCnt(), tmp);
/* 检查接收buf是否正常 */
TEST_ASSERT_EQUAL_MEMORY(pbuf, buf2, sizeof(buf2));
/* 获取485事件组 */
u485_evnt = Module_U485.GetEvnt();
/* 检查485事件组中接收完成是否置1,否则输出错误信息 */
TEST_ASSERT_EQUAL_MESSAGE(u485_evnt.Reg.Finished, 1, "finish is not 1");
/* 传入协议a1句柄和a1的反馈变量,进行协议a1的解析工作 */
Module_U485.GuardParse((void *)&Proto485_a1.Handler, &a1_pback);
/* 检查是否通过解析 */
TEST_ASSERT_GREATER_THAN(MREQ_VALIED, a1_pback);
/* 检查是否通过解析出从机地址 */
TEST_ASSERT_EQUAL(Proto485_a1.ReqInfo.SlaveAddr, START_BYTE_SYMBL);
/* 检查是否通过解析出功能码 */
TEST_ASSERT_EQUAL(Proto485_a1.ReqInfo.Funcode, FUNCODE_04);
/* 检查是否通过解析出寄存器地址 */
TEST_ASSERT_EQUAL(Proto485_a1.ReqInfo.RegAddr, 0X7540);
/* 检查是否通过解析出寄存器数量 */
TEST_ASSERT_EQUAL(Proto485_a1.ReqInfo.RegNum, 0X0003);
/* 使用crc函数计算帧2的CRC */
buf2_crc = crc16tablefast(buf2, sizeof(buf2) - 2);
/* 检查是否计算crc与帧中crc一致,否则输出错误信息 */
TEST_ASSERT_EQUAL_MESSAGE(buf2_crc, 0xabe0, "buf2_crc is wrong");
/* 传入协议a1句柄和a1的反馈变量,进行协议a1的封装发送工作 */
Module_U485.GuardTransfer((void *)&Proto485_a1.Handler, &a1_pback);
/* 检查封装数据起始字节是否正确 */
TEST_ASSERT_EQUAL(START_BYTE_SYMBL, pbuf[START_BYTE_OFFSET]);
/* 检查封装数据功能码是否正确 */
TEST_ASSERT_EQUAL(FUNCODE_04, pbuf[FUNCODE_BYTE_OFFSET]);
/* 清除本次接收状态 */
Module_U485.ClearStatus();
/* 检查是否清除接收长度 */
TEST_ASSERT_EQUAL(Module_U485.GetRecvCnt(), 0);
/* 检查是否清除接收状态 */
TEST_ASSERT_EQUAL(Module_U485.GetRecvStatus(), 0);
}
/* 协议2测试 */
void test_Proto485_a2(void)
{
uint16_t tmp = 0;
uint16_t i = 0;
/* 定义协议1的反馈变量 */
int a2_pback = 0;
/* 定义485事件组变量 */
UartRecvEvntGrup_u u485_evnt = {.Block = 0};
/* 定义协议2模拟帧1,设置无误 */
uint8_t buf1[] = {'^', '_', 'H', 'e', 'A', 'd', '&', 0x01, 0x00, 0x00, 0xe9, 0x78, '$', '_', 'T', 'a', 'I', 'l'};
uint8_t *pbuf = NULL;
/* 获取协议1模拟帧1长度 */
tmp = sizeof(buf1);
/* 模拟串口接收中断调用 */
for (i = 0; i < tmp; i++) {
Module_U485.PrivateArea.Uart485.RecvIRQ(&(Module_U485.PrivateArea.Uart485), &buf1[i], U485_BUF_MAX);
}
/* 模拟串口接收空闲中断调用 */
Module_U485.PrivateArea.Uart485.RecvIRQ(&(Module_U485.PrivateArea.Uart485), NULL, U485_BUF_MAX);
/* 检查接收长度是否正确 */
TEST_ASSERT_EQUAL(Module_U485.GetRecvCnt(), tmp);
/* 获取接收buf */
pbuf = Module_U485.GetBuf();
/* 检查接收buf是否正常 */
TEST_ASSERT_NOT_NULL(pbuf);
/* 检查接收buf数据内容是否正常 */
TEST_ASSERT_EQUAL_MEMORY(pbuf, buf1, sizeof(buf1));
/* 获取485事件组 */
u485_evnt = Module_U485.GetEvnt();
/* 检查485事件组中接收完成是否置1,否则输出错误信息 */
TEST_ASSERT_EQUAL_MESSAGE(u485_evnt.Reg.Finished, 1, "finish is not 1");
/* 传入协议a2句柄和a2的反馈变量,进行协议a2的解析工作 */
Module_U485.GuardParse((void *)&Proto485_a2.Handler, &a2_pback);
/* 检查是否通过解析出正确命令码 */
TEST_ASSERT_EQUAL(A2_CMD_START, a2_pback);
}
可以看到使用依赖注入的方式实现的功能,不仅在实际使用中复用能力强,在测试中也可以使用完全一致的检验方式,只需要为每个协议设计相应的测试帧就可以。
测试报告
Ceedling
提供了一些生成测试报告的功能,这里用到了代码的测试覆盖率报告功能,有3个维度的指标,行数覆盖率、函数覆盖率以及分支覆盖率,不同颜色表示不同覆盖程度,绿色就是基本全部测试到了,如下图所示:

点开某一个被测文件后,会出现具体的源码测试覆盖细节,绿色代表被执行到,如下图所示:

上图中显示了还有很多的代码没有测试,所以就写到这里吧,我去搞测试了。
总结
在实际的开发中,测试是必需的,使用 Ceedling
可以极大地简化了测试替身的创建和使用,提高了编写测试的效率和质量,并且提供了自动化测试的方式,是快速反馈和持续交付的基础,而手动测试无法满足频繁迭代的需求,且容易遗漏。这篇文章中的测试也只覆盖了一部分,并且只做了常规测试,一些边界测试、可靠性测试、压力测试这些都没有提及,这些无非是测试方法的改变,具体情况具体测。其次我还想要说的一点是,一个软件的所有bug像是一张没有边际的方格纸,我们所做的测试就是把空白的方格打上勾,也就是说测试能带给我们的是只能保证打勾的那一部分,剩余的还是无法保证,这很让人无奈。一个边界明确的方格纸,只存在于理论中,是封闭的,没有任何外界干扰,现实中想要逼近这样的形式,我觉得得屏蔽所有中断,屏蔽所有外部输入,然后闭关锁国,自成一方小世界才能达到吧,但又没实际意义了,行了,不扯了,就这样吧。