当前位置: 首页 > Web前端 > HTML

百度单测生成技术如何召回在线服务异常问题?

时间:2023-04-02 18:04:22 HTML

简介:在线系统的异常问题一直很“吓人”,传统方法在解决此类问题时也面临着相应的技术瓶颈。在此基础上,探索了基于单元测试的异常问题召回方法,实现了通用的、无人值守的单元测试生成系统,在百余个模块中取得了一定的成果。以近码法单元测试为出发点,围绕基于单元测试生成技术召回异常问题的应用实践展开。主要介绍方案从0到1的整体构建思路,从理解代码、构建高覆盖测试用例数据、生成测试用例代码、分析失败用例四个方面进行介绍。简介本文提出了一种基于白盒方法召回异常问题的通用方法,并以C/C++语言为例介绍了该方法在百度服务器上的实现。1背景在线稳定性问题一直备受大家关注。在影响产品收益或用户体验的同时,也影响了QA的声誉。为了防止此类问题在线上发生,测试人员有一系列的异常测试召回方法可以采用。常见的有:基于压力测试、基于功能测试、基于单元测试或基于静态扫描的异常召回方法。但是,在召回能力足够完善的情况下,仍然存在网上泄密的问题。为什么是这样?2.问题分析针对以上问题,本文从高成本和低召回两个问题维度对业界现有的异常检测方法进行对比分析。比较结果如下表2.1所示。表2.1业界常用异常测试方法缺点对比分析总体来看,目前的召回方法存在滞后或成本高的问题,如基于压力测试的异常召回方法资源消耗高,基于异常召回方法的异常召回方法资源消耗大功能测试除了成本高,还存在异常场景难以构建等滞后性问题。基于单元测试和静态检查发现代码问题在这些方法中缺点相对较少。接下来,我们将更详细地比较单元测试和静态检查这两种召回异常的方法。目前静态检测是最常用的异常发现手段,接入成本低且轻量级。以静态扫描方式检测,无需编译运行,不占用资源。但它存在以下问题:滞后性:只有在线出现问题才能转化为规则,避免类似问题的再次发生。准确率低:规则是人为设计的,在某些场景下可能存在遗漏或误报,需要具体情况具体解决。不可持续:缺乏围绕规则的生态建设,可能存在规则重复开发、规则贡献者缺乏、规则上线后无法有效评估等问题。基于单元测试召回异常问题有两个缺点:开发成本高和依赖人的意识。开发者针对该功能中的重要功能编写相应的单元测试代码进行测试。选择哪些功能,验证哪些异常场景,取决于开发者的经验和主动性。但它也有以下优点:测试最小单元,易于构造数据,验证正确性便于后续功能回归,资源消耗小,可以更早发现问题,定位解决成本低。经过上面的分析,可以得出单元测试的优点远大于其缺点的结论,于是大胆假设:能否最大限度地发挥其优势,解决依赖“写作”和“经验”的问题.——即自动编写异常单元测试代码,主动发现代码健壮性问题。在此假设下,针对稳定性问题提出了一个可持续的、主动-被动组合、高ROI召回漏斗。SmartUT作为静态代码检查环节后的主动召回手段,动态分析召回问题。图2.1稳定性召回漏斗图3解决思路2019年初,在强烈的用例代码生成需求下,我们调研了C/C++语言行业中比较常见且优秀的单测生成工具:C++测试和Wings。从三个方面对召回能力和开源进行了比较,比较结果如下表3.1所示。显然,无论是C++测试还是Wings,都不能满足业务线的需求和扩展,在复杂的业务场景下,对复杂类型的函数进行全自动化单元测试代码的生成。因此需要自建单元测试代码生成能力。表3.1业界C/C++单元测试生成工具调查比较拆解开发者为一个功能编写单元测试代码的过程。关键步骤如图3.1所示。整个过程抽象为确认待测功能->分析代码->构建测试数据->生成测试代码这4个过程。确认要测试的函数:在本次提交的代码中,并不是所有的改动或者新增的功能都需要测试,可以结合函数属性(比如构造函数、析构函数等)、修改的内容(比如test-相关代码、日志逻辑等无风险功能)。分析代码:总结被测函数的代码,如参数(入参、内部依赖等依赖参数)、返回值等信息。构建测试数据:主动构建被测功能所需的用例数据,无需人工参与。生成测试代码:主动生成代码,对被测功能进行测试,无需人工参与。解决方案的关键是利用代码分析等白盒技术,实现一键异常单元测试代码的能力,真正模拟开发人员编写单元测试代码。图3.1开发者实现单测代码编写流程4实现方案基于上一节的分析。整个技术方案设计如下图所示。本节重点介绍代码分析、测试用例生成、代码生成能力、执行分析的实现思路。图4.1技术方案设计图4.1代码分析能力代码分析的目标是通过静态代码扫描,将复杂的函数代码抽象成结构化的函数特征数据,类比编译符号表。基于这种结构化数据,可以直接感知函数调用方法、变量声明和赋值方法等行为。4.1.1代码特点在C/C++语言中,尤其是C++等面向对象语言中,函数调用和类声明的创建方式不同于普通变量,有更丰富的语法多样性。首先,需要明确语言在代码分析过程中需要获取的信息内容。重点考虑如下:函数调用:普通函数调用,类的成员函数调用。在调用类的普通成员函数之前,需要先实例化类的对象。,非成员函数可以直接调用。变量声明实现:普通变量、类或结构变量、stl变量等变量声明有不同的赋值方式。需要能够区分普通变量还是class、struct、stl变量。修饰符:const、static、virtual、inline等被修饰带有符号的变量或函数会影响其调用、实例化、赋值方式文件级信息:头文件、命名空间头文件和命名空间不完整或缺失会影响编译测试代码影响赋值和实例化语法的其他属性。如类是否禁用复制/赋值构造函数等。基于以上思路,初步确定并得到如下代码特征信息:表4.1代码特征信息4.1.2特征存储以xml文件格式存储特征存储为代码结构数据(CodeStructData,CSD),并确保外围模块可以根据此输出捕获函数如何被调用,变量如何声明,以及值如何赋值。schema是根据不同的类型和赋值方式约定的,比如type、baseType1、parmType等属性。Demo如下图所示。type:实际类型baseType1:变量实际属于类别,如内置类型、数组类型、STL类型等parmType:声明类型,生成时可直接使用该字段作为变量的声明类型代码。![]图4.2源码分析得到的CSD样例4.1.3特征采集部分,希望通过静态代码扫描的方式提取代码信息,无需编译,工具要轻量级、高效、支持开源,所以以便于后续的需求迭代。综合比较后,最终选择了开源的静态代码检查工具cppcheck。此外,还可以根据其符号表进行二次开发。为了收集函数调用链信息等全局信息,内部对cppcheck进行了修改,后面会单独介绍,本文不再赘述。采集流程如下图4.3所示。图4.3基于cppcheck的代码分析方案示意图4.2用例数据生成能力4.2.1解决思路用例数据生成能力是Fuzzing技术领域的关键环节。常见的模糊数据方法包括基于生成和基于变异的方法。一般用覆盖率来衡量模糊能力,例如函数覆盖率、线路覆盖率或分支覆盖率。基于变异的方法:基于已知数据样本通过变异生成测试用例。例如著名的AFL-fuzz技术,其主要处理流程如下图4.4所示:图4.4AFL-Fuzz处理流程基于生成式:根据已知协议或接口规范进行建模,生成测试用例.例如,libfuzzer可以在不指定初始数据集的情况下,通过被测目标的接口类型随机生成字节数据,馈送到被测目标。在生成用例数据时,避免用例爆炸也是生成的条件之一。用例过多会导致用例无效,运行时效率低下。本文在传统的基于生成法构造用例数据的方法的基础上,除了目标接口协议外,充分利用路径和分支信息来引导模糊数据,覆盖更多分支中的逻辑,以及引入其他白盒特性,如Variablediffusioncorrelation等,减少无效用例的产生,最终以函数覆盖率和分支覆盖率作为fuzz能力的衡量指标。解决思路如下图4.5所示。数据生成层由CSD处理模块、路径选择模块、参数选择模块、生成筛选模块组成。针对不同类型的变量,选取不同的异常候选集生成初始测试用例集,再通过用例筛选策略得到最终测试用例集。图4.5测试用例生成方案示意图4.2.2路径选择路径选择模块包括表达式约束求解、路径可达性分析、路径合并。本节的目的是指导分支机构的数据生成覆盖率。路径提取主要是遍历上一节提取的程序控制流数据,在不影响结果的情况下可以采用深度优化遍历或者广度优先遍历。为避免路径爆炸,可以先提取预期测试的目标,每次遍历时选择一条能覆盖待测试目标的路径。1)约束求解是指求解计算路径上的分支表达式,分别计算表达式为真和假时的符号值。这里需要先替换表达式,比如把函数调用替换成变量,方便计算。替换后的表达式可以使用开源库求解,比如z3。2)路径可达性分析是指以if、while、for、switch等分支为节点,计算在该节点求解的变量值或变量范围。函数内部的节点连接起来后,就得到了一个图。结合每个节点变量的作用域,消除图中路径,删除不可达路径。3)路径合并是指将有交集的节点合并为一条路径,以减少后续产生的用例数量。如下图4.6构造_index_i和_index_j的用例时,构造{_index_i=1,_index_j=2}以满足同时覆盖第17行和第22行两个分支的数据。处理时需要分析分支内部是否存在return、continue、break等jump或return关键字,避免badcase。图4.6程序示例4.2.3候选数据来源各类候选异常数据可分为静态数据和动态数据。静态数据是指通过历史经验维护的类型边界值和业务边界值的数据库。动态数据是指基于模块日志、流量等数据源,通过插桩,通过业务数据采集和变异算法变异得到的业务值或异常边界值。4.2.4Usecasegeneration&screening根据以上步骤得到每个参数的候选值集合后,就可以将参数组合起来得到一组用例。参数组合的方式直接影响用例的大小。这个阶段重点是如何避免用例爆炸,减少不降低质量。据统计,70%以上的软件问题都是由一两个参数引起的。因此,参数因素成对组合成为软件测试中比较实用和有效的方法。如果采用全排列组合法,在某个业务场景下,使用某类classA作为函数参数,假设classA有1000个成员变量,都是v类型,v类型有4个values,v=[-1,0,1,-2147483649],那么全排列组合后的用例数据量高达4^1000。可以看出,简单的全排列组合可以保证目前的两两因子组合覆盖最丰富的场景,但是会面临案例爆炸的问题,不符合实际应用背景。事实上,生成最小测试用例集是一个NPC问题,因此学术界普遍把找到一组尽可能小的测试用例集以覆盖所有可能的对作为研究目标。本文依次使用两个步骤来降低用例的量级。剔除无用属性:基于代码分析减少无用属性的数据构建。通过分析自定义类型参数成员属性的扩散,只为类/结构中实际使用的成员属性构造数据。图4.7函数中的变量及其成员变量示例消除冗余测试用例:采用基于生成的方法,选择参数组合算法,生成合适的测试用例。常见的生成技术大致可分为组合设计法、启发式算法和元启发式探索法。组合设计法:一般围绕正交表或其他代数思想生成测试用例。启发式算法:一般情况下,测试用例是逐项或逐因子展开生成的。例如经典的AETG算法:首先根据贪心算法生成一定数量的N个测试用例,然后从N个测试用例中选择一个能够覆盖未覆盖配对集中参数对的测试用例,以及这个测试用例被添加到形成的测试用例中。在用例集T中,直到达到覆盖目标。例如,IPO算法先横向扩展用例,然后纵向扩展。元启发式算法:如遗传算法、模拟退火、蚁群算法等。大致流程如下图4.8所示。图4.8元启发式用例探索的一般流程启发式和元启发式都是局部搜索算法,不能保证最优性,但可以保证处理时间。也可以将逐项生成方法与元启发式方法相结合,引入错误风险系数、组合约束、参数优先级等信息,进一步优化组合方法。本文前期在基本的成对方法的基础上,采用逐项生成的方法来减少重复和无效的输入。举个例子简单介绍一下本文使用的2-Wisetestingpairwise方法的思路(其原理请参考文末提供的信息):假设有三个输入变量,X,Y,和Z,取值分别为D(X)={x1,x2,x3},D(Y)={y1,y2},D(Z)={z1,z2};如果采用全排列法,得到的测试用例集有3X2X2=12个case,具体测试用例如下左图4.9所示,2-Wise测试后只得到6个用例。图4.9测试用例全排与成对法算法流程本文通过上述方法有效剔除90%以上无用测试用例数据。最后将保留的测试用例以json格式存储,作为测试数据集合,方便其他场景的扩展使用。数据demo如下图4.10所示,以函数名、func_data、变量名为key,以具体参数值为value。图4.10测试用例集演示图目前的生成方式是基于参数与参数相互独立的假设,思想简单。但是在实际的业务场景中,参数之间可能会相互关联。在生成方式方面还有很大的改进空间。后期在目前逐项生成的能力下,会引入元启发式探索算法,比如遗传算法或者模拟退火算法,在这个领域效果比较显着,探索算法每次生成测试用例时都会调用。以提高覆盖率和重要覆盖要素为目标,生成有效的测试用例集也是智能UT最重要的“智能”场景之一,而数据是错误发现的根源。4.3代码生成能力4.3.1解决方案目前代码生成领域主要有两个方向:程序生成和代码补全。生成测试代码属于程序生成方向。利用深度学习算法生成代码是目前学术界的一个重要研究方向。基于一些开源代码作为语料库,已经取得了一些技术突破。但由于泛化能力弱的问题,目前还未能在业界落地。在实际技术实现中,程序生成的正确性直接影响测试任务的稳定性。考虑到这一约束,本文目前采用基于语法规则和模板的生成方法来生成测试用例代码。正确的语法规则和代码结构数据可以保证生成的代码语法正确,达到生成后立即编译的目的。具体实现方案如下图4.11所示。将上述步骤得到的代码结构数据和测试用例集合数据发送给代码生成处理模块。模块通过控制层选择不同语言对应的生成器,然后根据不同的类型选择相应的生成器。生成运算符。针对变量内容,深度遍历代码结构数据的各个功能节点、参数节点、全局节点,为每个节点下的代码信息获取对应的语法适配生成算子生成目标代码,从而得到测试用例代码,结合模板中固定的源代码,打包成一个可以编译运行的测试代码。这个过程可以比作编译器组合语法树生成目标代码的过程。和C/C++语言一样,基于Gtest的死亡测试包生成测试用例代码,测试被测函数是否意外死亡。基于目前的生成框架,可以方便地扩展其他语法规则,生成不同语言、不同形式的用例代码。图4.11代码生成方案示意图4.3.2完整demo展示下图是对被测源码的exlore_filter函数进行代码分析和用例数据生成后获取测试用例代码的过程示例.图4.12测试用例采集demo图4.4失败用例分析基于上一节介绍的代码生成能力,可以得到可编译的测试用例代码,编译适配模块生成编译命令,可执行测试编译程序后即可得到。如何保证在运行测试程序后能够快速获取故障信息,降低人为干预的分析成本是本节的重点。整个分析过程可能存在的问题如下:可读性差:一个测试用例失败后,其堆栈/崩溃不完整,或者无用信息过多。和c/c++语言的gtest死亡测试一样,用例崩溃后没有打印出堆栈信息。常规的方法是通过gdb获取栈内容。当堆栈文件太大超过3G时,读取速度会很慢。Duplicatestack/crash相同功能、同一行代码重复出现的问题,主要是不同用例之间hit问题的重复出现。例如,当输入case为{arr=nullptr,len=1}和{arr=nullptr,len=2}时,以下场景中的find函数将在sum+=arr[0]行崩溃。图4.13被测不同代码片段出现重复代码行的问题,主要是由于相似代码语义阶段分配引起的重复问题。add_to_dest和get_from_dest分别在write_dest->write和read_dest->read行中崩溃。代码行的内容不同,但是crash的语义是一样的,都是利用空指针_dest导致程序崩溃。图4.14测试代码片段A图4.15测试代码片段B定位成本高对于新手或者不熟悉堆栈文件的人来说,即使测试用例代码、CR、堆栈信息完整,后续也无头绪调查问题。维修标准不统一。哪些问题必须修复,哪些问题可以忽略。不同业务线缺乏统一标准。本文采用栈内容存储、栈内容分析、去重、故障原因预测、故障问题分类等方法解决上述问题。解决思路如下图所示。每个阶段都有很多细节,本文不重点介绍。图4.16Stack分析流程4.5技术架构面向业务的实现需要考虑如何把工具的能力带到合适的阶段,恰到好处,结合研发和开发习惯,我们考虑了以下两个因素:存量问题修复周期:业务模块直接扫描会因为历史问题太多而产生较大的修复成本,需要一定的时间消化。迭代时只需要关注变更的影响:在变更管道上扫描全量代码对于全量代码生成用例会造成资源浪费和执行效率低下。基于以上考虑,我们将落地方式分为存量和增量两种模式。盘点:建议先运行完整版新接入模块,扫描盘点问题,并请研发团队指定负责人统一修复,统一修复,消除盘点中的隐患。您还可以在每日任务或完整回归任务中运行库存扫描模式。Increment:指仅针对变更的代码,通过白盒分析的方法,分析其影响的代码范围,如直接影响(变更功能),间接影响(无变更但逻辑影响),仅在要测试的影响函数的范围。提交修改后的代码后,可以触发流水线以增量方式运行任务。这里也可以引入风险考虑,评估功能修改内容是否需要测试,剔除无风险功能。基于以上思路,将代码分析、用例数据生成和代码生成能力集成到如下技术架构中,结合百度内部的战略中台、数据中台、可视化平台等能力,实现测试准备、测试问题执行、测试分析定位这四个维度,完成基于单次测试生成的异常召回工具的构建与实现。图4.17落地架构图部分任务结果如下图所示。研发人员在本地开发提交代码后,自动触发流水线绑定的智能UT测试任务。可以通过报告查看崩溃问题的详细信息,包括失败的原因和失败堆栈的内容。图4.18任务执行展示示例图5效果一、工程效果实践:探索出一种基于单体测试解决异常问题的通用方案,已用C/C++语言实现,超过千万行测试累计生成代码,其他语言覆盖率中高:冷启动函数覆盖率50%+,分支覆盖率20%+资源低:机器资源消耗与系统级测试相比可以忽略不计人力消耗低:自动化适配UT和测试代码编译能力,无需手动搭建单一测试Framework和维护2.业务效果落地:覆盖140+关键后端模块,lib盘点召回:库存问题召回900余例增量召回:200多例增量召回问题参考1.cppcheck:https://github.com/danmar/cpp...2.Fuzzing:https://baike.baidu-com/item/...3.z3:https://github.com/Z3Prover/z34.all-pairs_testing:https://en.wikipedia.org/wiki...5.死亡测试:https://github.com/google/goo...6.traceback:实现思路参考https://github.com/zsummer/tr。..7.addresssanitizer:https://github.com/google/san...------------END----------百度架构师百度官方技术公众号已上线!技术干货·行业资讯·在线沙龙·行业会议招聘信息·介绍资料·技术书籍·百度周边欢迎各位同学关注!