ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MongoDB 查询引擎的 Core Property-Based Tests:基于 fast-check 的属性化测试模型设计与实践

MongoDB 查询引擎的 Core Property-Based Tests:基于 fast-check 的属性化测试模型设计与实践 MongoDB 查询引擎的 Core Property-Based Tests基于 fast-check 的属性化测试模型设计与实践【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo本篇文章系统讲解 MongoDB 仓库当前工作目录GitHub_Trending/mo/mongo中查询团队设计的Core Property-Based Tests核心属性化测试从属性决定模型的设计哲学、刻意收窄的小型 Schema、Workload工作负载建模格式、查询族Query Family生成机制到失败时的缩小shrinking与反例回放调试流程。读完你将掌握这套测试基建的设计动机、模型文件的组织方式以及如何在自己的*_pbt.js测试中复用testProperty接口与common_properties.js中的现成属性。什么是 Core PBTCore PBT 是 MongoDB 属性化测试的一个子集它们共享一套 Schema 与模型。其设计目的是为查询语言提供基础覆盖——覆盖那些其余 jstests 可能没有测到的简单场景。这意味着 Core PBT 只覆盖$project、$match、$sort等简单聚合阶段而$lookup、$facet这类复杂阶段不在 Core 集合中测试Core 集合之外的 PBT 会负责这些复杂特性。据 jstests/libs/property_test_helpers/README.md 记载这套测试在 8 个月内捕获了 24 个 bug详见 SERVER-89308 中的完整列表效果显著。这也是理解其设计取舍的重要背景它不是为了测全语法而是为了稳定地挖出交互类 bug。Core PBT 的设计建立在几条关于随机化测试的关键原则上下面逐一展开。设计原则一属性决定模型Properties Dictate the Models在 MongoDB 的通用 fuzzer 中有覆盖大部分 MQL 的语法生成器。覆盖面广的代价是能断言的属性变弱模型语法是第一位属性性质排在第二位为此甚至需要给属性添加例外exception来让它成立。这种模型决定属性的设计最终反噬了除了在属性里加例外还需要后处理生成的查询。例如往聚合管道的多处插入$sort就不再是在测试真实的 MQL而是在测试一个用户永远不会写的人工 MQL 子集。因此 Core PBT 反其道而行属性优先且属性几乎无例外。属性反过来决定采用什么模型从而无需任何后处理。这也直接导致了 Core PBT 的模型比 fuzzer 模型显著更小。设计原则二小 SchemaSmall Schema字段数量少交互更容易被发现Schema 中字段数量少是为了更容易发现字段之间的交互interaction。举个例子假设某优化只在[{$match: {*field*: 5}}, {$sort: {*field*: 1}}]中两个字段相同时才触发。若 PBT 模型里有一千个可能字段a、b、c还有a.b.c、a.a.a及各种组合随机命中该优化的概率是1/1000而只有六个字段时概率提升到1/6。另一种交互发生在查询与索引之间。由小 Schema 生成的查询和索引使索引更可能被实际使用。而 bug 往往来自交互与特殊场景一个没有优化可施、也不走索引的查询其执行逻辑简单得多通常与更少的 bug 相关。简单值规避 MQL 自身的不一致与属性决定模型呼应更简单的文档模型允许更强的属性。查询语言中存在一些被接受为既有行为、却会破坏属性化测试的不一致解决办法是对文档中允许出现的值保持谨慎。文档中列举了两个典型案例SERVER-12869null 与 missing 的编码歧义在索引格式中null 与字段缺失missing被编码成同一种形式导致 covering plan不含FETCH节点的执行计划无法区分二者。这个不一致让 fuzzer 产生大量噪音——结果中一个值的差异就会向外传播。因此 Core PBT不允许字段缺失具体约束为文档必须包含 Schema 中的全部字段只能对 Schema 中的字段建索引查询只能引用 Schema 中的字段。而null本身是允许的只禁 missing不禁 null。浮点数浮点运算结果会因运算顺序不同而不同这种差异会传播。因此 Core PBT 中只允许整数作为数值。这些约束在源码中有直接体现。在 document_models.js 的getDocModel中文档被建模为_id、t、m、array、a、b以及空字符串字段的固定记录而在 basic_models.js 的getScalarArb中标量值仅由整数、布尔、字符串、日期与常量null组成注释明确写道允许 null 字段但不允许 missing 字段以规避 SERVER-12869。Workload 建模集合与查询必须相互关联一次 PBT 运行的输入是一个workload工作负载由集合模型与聚合模型共同组成格式如下{ collSpec: { isTS: true/false to indicate if the collection should be time-series docs: a list of documents indexes: a list of indexes }, queries: a list of aggregation pipelines, extraParams: an optional list of extra values to be passed to the property function }为什么用一个统一的 workload 模型而不是分开的相互独立的集合模型和聚合模型因为统一模型允许二者相互关联。例如要建模一个每条查询都应满足 partial index 过滤条件的 PBT可以这样写fc.record({ partialFilter: partialFilterPredicateModel, docs: docsModel, indexes: indexesModel, aggs: aggsModel }).map(({partialFilter, docs, indexes, aggs}) { // Append {partialFilterExpression: partialFilter} to all index options // Prefix every query with {$match: partialFilter} // Return our workload object. });这就是一个合法的工作负载模型。若把集合模型与聚合模型分开传入它们彼此独立无法共享同一个partialFilter这样的公共 arbitrary也就无法协调生成。源码实现位于 workload_models.js 的makeWorkloadModel它接受collModel、aggModel单个聚合模型或aggsModel一次生成多条管道的模型、numQueriesPerRun、extraParamsModel与includeForeignCollection最终产出{collSpec, queries, extraParams}形式的 workload并调用fc.record组合。注意其中对参数做了类型检查typeCheckManyAggsModel防止把模型传错参数位。Core PBT 的 SchemaCore PBT 使用的文档 Schema 如下{ _id: a unique integer t: a date value m: an object with subfields m1 and m2. both are simple scalars array: an array of scalars, other arrays, or objects. this is the only field that is allowed to be an array. a: any simple scalar: integer, boolean, string, date, null b: same as a }该 Schema 目前同样适用于时间序列time-series集合中的文档此时t是时间字段m是元数据字段但未来二者可能分道扬镳。在源码层面basic_models.js 对整数使用了分层采样stratified sampling极小值[-1, 1]、较小值[-20, 20]、全 int32 范围[-2147483648, 2147483647]、以及两个角点常量NumberInt(kInt32Min)与NumberInt(kInt32Max)。分层的目的在注释中写得很清楚小范围[-1, 1]鼓励模型生成能命中文档的过滤条件生成{$match: {a: 1}}比{$match: {a: 8923741}}更容易命中{a: 1}从而让查询产生非空结果。日期也做了类似分层tiny/smallish/全范围/角点0001-01-01与9999-12-31且刻意把最小年份设为 1规避Date(year0)触发 ValidateCollections hook 的误报。字符串生成器则过滤掉$前缀字符以$开头的字符串会被解释为字段引用并可配置是否允许 unicode 与 null 字节null 字节在某些实现中表示字符串结尾属于角点场景。查询生成支持的阶段与 Query FamiliesCore PBT 的模型只覆盖有限的聚合阶段模型位于jstests/libs/property_test_helpers/models支持$project$addFields$match$sort$group$limit$skip在 query_models.js 的getAllowedStages中实际还包含$unwind与$replaceRoot等额外 arbitrary并区分deterministicBag——需要每次返回相同结果包时排除$limit/$skip时间序列集合暂不生成$group对应 TODO SERVER-83072。Query Families以族为单位生成查询查询模型并非生成单条独立查询而是生成一个查询族family of queries。在族的中子leaf处保存着该叶子可能取到的多个值。例如不再生成叶子值为1的单条查询[{$match: {a: 1}}, {$project: {b: 0}}]而是生成1,2,3作为该槽位的候选值[{$match: {a: {concreteValues: [1,2,3]}}}, {$project: {b: 0}}]随后从中抽取若干条形状相同的查询[{$match: {a: 1}}, {$project: {b: 0}}] [{$match: {a: 2}}, {$project: {b: 0}}] [{$match: {a: 3}}, {$project: {b: 0}}]这么做是为了让属性更频繁地命中计划缓存plan cache而不是靠运气。属性可以通过getQuery接口获取不同形状的查询或获取同形状、不同叶子值的查询。源码支撑basic_models.js 定义了LeafParameter类封装concreteValues数组与leafParameterArb生成 1 到leafParametersPerFamily 10个常量的数组property_testing_utils.js 的concreteQueryFromFamily(queryShape, leafId)递归遍历查询族把每个LeafParameter替换为vals[leafId % vals.length]处的具体常量query_models.js 中的各阶段 arbitrary如addFieldsConstArb、getSortArb、limitArb/skipArb、unwindArb、getMatchArb都以leafParameterArb作为叶子值来源管道长度由fc.array(oneof(...stages), {minLength: 1, maxLength: 6})控制注释说明长度 6 足以覆盖阶段间的交互。属性Property的源码级实现control 与 experiment 对比Core PBT 中最核心的一类属性是正确性对比在 experiment 集合带索引、启用优化与计划缓存上跑查询与 control 集合全表扫描、禁用优化与计划缓存上的结果做对比。其支撑函数如下runDeoptimized 通过setParameter将 control 集合上的执行强制到 classic engine、禁用计划缓存并先clear()缓存、禁用布尔表达式化简器再用$_internalInhibitOptimization前缀包裹每个阶段以关闭管道优化运行结束后在finally中恢复所有参数。createCorrectnessProperty 是正确性属性的默认实现对每种查询形状取第一个参数版本先一次性算出 control 结果再在 experiment 集合上执行并逐条对比默认比较器不排序文档内部数组compSortArrays会先排序数组compNormalized还会归一化数值。createCacheCorrectnessProperty 专测计划缓存交互把每个形状的第一条查询连跑三次使其入缓存再对比同形状、不同叶子参数的其他查询与 control 的差异——因为模型并未精确建模自动参数化规则缓存命中的查询与入缓存查询可能参数不同。其他常用属性还包括 createReplanningCacheCorrectnessProperty替换全部文档后强制 replan、createPlanStabilityProperty断言同一查询多次 explain 的 winning/rejected 计划一致可配合 CBR 的基数估算校验、以及makeBehavioralPropertyFn仅凭结果即可验证行为如$limit后结果数不超过上限。一次测试运行由 testProperty 驱动它以固定seed 4调用fc.assert(fc.property(workloadModel, ...))在每次运行时重建集合与索引createIndexesForPBT 对IndexOptionsConflict、通配符投影路径冲突、部分索引深度超限等可接受错误码静默放行避免模型过度复杂化并关闭TestData.traceExceptions以压掉无效索引等可忽略异常。失败时自定义reporter会输出属性名、fast-check 失败摘要与反例 workload。实战写一个 Core PBT——以 partial_index_pbt.js 为例jstests/core/query/partial_index_pbt.js 是文档提到的完整示例它演示了workload 内部相互关联的思想生成一个部分索引过滤谓词族partialFilterPredShape来自getPartialFilterPredicateArb可配置$eq/$ne/$lt/$lte/$gt/$gte、$exists、$in/$nin、$or/$nor/$not等见 match_models.js生成文档集、索引maxLength: 15且size: 2偏向生成更多与查询在.map阶段把第一个具体谓词注入到所有索引的partialFilterExpression并把整个谓词族以{$match: ...}前缀拼接到每条查询管道之前——这样每条查询都必然满足部分索引过滤条件索引使用率不再依赖运气最后调用testProperty(correctnessProperty, {controlColl, experimentColl}, workloadModel, numRuns100, examples)其中属性选用createCacheCorrectnessProperty专门验证相似形状的查询连续运行触发的计划缓存交互。文件头部的tags声明了该测试的约束如query_intensive_pbt、requires_getmore、multiversion_incompatible等并会在 slow builddebug 或开启 sanitizer下提前退出。文档约定 Core PBT 的文件命名模式为jstests/**/*_pbt.js当前仓库中已有 50 个这类文件覆盖聚合各阶段如aggregation/sources/project/project_pbt.js、aggregation/sources/group/group_pbt.js、计划缓存core/query/plan_cache/cache_correctness_pbt.js、cache_usage_pbt.js、时间序列core/timeseries/pbt/*、排序走索引core/index/index_for_sort_pbt.js、CBR 基数估算稳定性noPassthroughWithMongod/query/cbr/*等。调试一个失败的 PBT固定 seed 与确定性当前所有 PBT 都使用固定 seedtestProperty中const seed 4。这意味着只要 bug 在服务端是确定性的PBT 每次运行都会稳定撞上它若 bug 不确定测试则可能失败也可能不失败。缩小Shrinking / Minimizing一旦属性找到一个反例counterexamplefast-check 会自动尝试缩小它。但缩小通常到不了全局最小反例因为 fast-check 无法做某些跳跃——例如它无从得知{$and: [{a: {$eq: 1}}]}通常可以化简为{a: {$eq: 1}}甚至{a: 1}除非 fast-check 具备 MQL 的领域知识或缩小阶段对反例继续模糊fuzz否则做不出这类简化。不过实际反例通常已经足够小没有太多可缩的空间。对于非确定性问题缩小效果会打折扣——因为属性对缩小后的反例是否仍失败给出的是混合信号。失败输出与反例回放失败经过最小化后会打印反例及其调试数据包括 fast-check 找到的反例与遇到的错误。反例本身就是一个 workload见上文Workload 建模包含集合与查询的全部信息。复现方法把反例复制粘贴进失败的 PBT作为examples参数传给testProperty。fast-check 会先运行这些手工示例再开始随机生成。partial_index_pbt.js就是通过examples参数引用 pbt_resolved_bugs.js 来确保历史上失败过的 workload 每次都被执行同样可用于复现新 BFbug fix报告中的 bug。例如该文件中保存了 SERVER-102825 的反例一个带{$or: [{a: 1}, {a: {$lte: a string}}]部分索引过滤条件的场景与 SERVER-106983 的计划稳定性反例。testProperty还支持counterexamplePath参数传入后会以path模式仅回放单个反例而不跑完整属性日志会提示Remove the counterexamplePath argument to run the full property test。测试基建自检防止静默地什么都不测Core PBT 有一项重要防护self_tests/pbt_model_test.js 与pbt_minimization_test.js用来校验模型行为正确防止 PBT 静默空转——例如若模型生成了零个文档那么任何查询看起来都是正确的。自测会检查平均值集合中是否有足够多的文档是否创建了足够多的索引查询返回的结果集大小是否可接受参数化同一形状、不同叶子值是否正确工作。这再次印证了文档中强调的原则模型质量直接决定属性测试的有效性。模型目录下还有collection_models.js、index_models.js、group_models.js、collation_models.js、lookup_models.js等按阶段拆分的模型文件以及common_models.js、model_utils.js提供的组合工具均可按需复用。附录PBT 与 fast-check 简介属性化测试PBT属性化测试是一种断言属性在大量示例输入上成立的测试方法。在 MongoDB 的应用中它由两个组件构成模型model被测对象的描述用于生成对象的各种示例属性函数property function接收这些示例并断言对象具备预期特征。一个经典例子假设实现了一个整数加法函数add可以用具体值测试assert.eq(add(1, 2), 3); assert.eq(add(-1, 1), 0); ...此外还可以写一个 PBT 断言add的交换律——add(a, b)应恒等于add(b, a)function testAdd(a, b){ assert.eq(add(a, b), add(b, a)); }testAdd的输入可以用 JS 内置的Random包也可以用 fast-check 这类 PBT 库。MongoDB 查询团队的使用方式更复杂几乎总是涉及对查询语言、文档与索引的子集建模。通用 fuzzer 本质上也属于属性化测试——生成随机查询并针对不同对照旧版 mongo、无索引集合等断言正确性。fast-checkfast-check 是一个面向 JavaScript/TypeScript 的属性化测试框架位于jstests/third_party/fast_check/fc-3.1.0.js。它提供构建大型模型的积木式组件arbitrary支持对模型断言属性并内置反例缩小shrinking/minimizing逻辑。关于如何使用 fast-check 编写一个完整的属性化测试可参考 aggregation/sources/project/project_coalescing.js文档中引用的示例其内部通过fc.assert、fc.property与共享的模型 arbitrary 组织测试。总结Core PBT 是一套刻意收窄边界的属性化测试基建用小 Schema 提高交互命中率、用简单值规避 MQL 自身不一致、用属性决定模型换取无需后处理的强断言、用查询族机制让计划缓存交互可被稳定触发。配合固定 seed、自动缩小与examples反例回放它既能持续稳定复现已知 bug也能在 8 个月内发现 24 个新问题。如果你要为 MongoDB 查询语言新增覆盖遵循_pbt.js命名模式、复用models/下的 arbitrary 与common_properties.js中的现成属性即可快速产出高质量且可维护的属性化测试。【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表