- 数据库
- 教育
- 后端
【免费下载链接】bustub
The BusTub Relational Database Management System (Educational)
本篇技术指南以 googletest 官方 Matchers 参考文档为骨架,系统讲解 GoogleTest/Google Mock 中"匹配器"(matcher)的完整体系:它如何匹配单个参数、如何在EXPECT_THAT/ASSERT_THAT断言与ON_CALL()/EXPECT_CALL()模拟中使用,以及通配、比较、浮点、字符串、容器、成员、指针、复合、自定义匹配器等全部内建类别。BusTub(CMU 15-445 教学数据库管理系统)的测试体系通过 test/CMakeLists.txt 将每个*_test.cpp链接到gtest gmock_main,因此掌握 Matchers 可以直接提升你在本项目中的测试编写能力——读完本文,你将能写出表达力更强、失败信息更精准的单元测试,并能在需要时自定义项目专属匹配器。
一、Matchers 是什么:从EXPECT_THAT说起
一个matcher(匹配器)匹配的是单个参数。它有两种典型使用场景:
- 用于 mock 行为的设定与预期:
ON_CALL()或EXPECT_CALL()中,用 matcher 描述方法参数应当满足的条件; - 直接校验一个值:使用下面两个断言宏。
| 宏 | 说明 |
|---|---|
EXPECT_THAT(actual_value, matcher) | 断言actual_value匹配matcher(非致命失败)。 |
ASSERT_THAT(actual_value, matcher) | 与EXPECT_THAT相同,但失败是致命的(立即中止当前测试)。 |
例如 googletest 自带的 gmock-matchers-containers_test.cc 中就有这样的组合用法:
ASSERT_THAT(5, Ge(2)) << "This should succeed."; ASSERT_THAT("Foo", EndsWith("oo")); EXPECT_THAT(2, AllOf(Le(7), Ge(0))) << "This should succeed too."; EXPECT_THAT("Hello", StartsWith("Hell"));警告:直接传值做等值匹配时的隐式转换陷阱
文档特别给出了一个 Warning:EXPECT_THAT(actual_value, expected_value)这种直接传裸值做等值匹配的写法虽然受支持,但隐式转换可能带来意外结果。例如:
EXPECT_THAT(some_bool, "some string"); // 能编译,但可能"意外通过"布尔值被隐式转换后与字符串字面量比较,语义完全不是你想要的。
最佳实践:把比较写显式,二选一:
EXPECT_THAT(actual_value, Eq(expected_value)); // 显式用 Eq 匹配器 EXPECT_EQ(actual_value, expected_value); // 或直接用传统断言内建匹配器的分类总览
除特别注明外,所有内建 matcher 都定义在::testing命名空间中。它们按职责可分为:通配、通用比较、浮点、字符串、容器、成员、函数结果、指针、多参数、复合、适配器、谓词化以及自定义匹配器。下文逐类展开。
二、通配与通用比较匹配器
Wildcard(通配)
| Matcher | 说明 |
|---|---|
_ | argument可以是正确类型的任意值。 |
A<type>()或An<type>() | argument可以是type类型的任意值。 |
在 mock 场景中,_极为常用,比如"无论传入什么都行"。它的实现类AnythingMatcher定义在 gmock-matchers.h 中:MatchAndExplain恒返回true,DescribeTo输出 "is anything",DescribeNegationTo输出 "never matches"。由于它是多态 matcher,通过模板转换操作符可表现为任意Matcher<T>。A<type>()/An<type>()则要求值确实是(或可转换为)指定类型。
Generic Comparison(通用比较)
| Matcher | 说明 |
|---|---|
Eq(value)或裸value | argument == value |
Ge(value) | argument >= value |
Gt(value) | argument > value |
Le(value) | argument <= value |
Lt(value) | argument < value |
Ne(value) | argument != value |
IsFalse() | argument在布尔上下文中求值为false。 |
IsTrue() | argument在布尔上下文中求值为true。 |
IsNull() | argument是NULL指针(裸指针或智能指针)。 |
NotNull() | argument是非空指针(裸指针或智能指针)。 |
Optional(m) | argument是optional<>,且其中包含的值匹配m。若要判断optional<>是否 set,可与nullopt做等值比较;若内部类型没有==,可能需要用Eq(nullopt)。 |
VariantWith<T>(m) | argument是variant<>,且当前持有的T类型备选值匹配m。 |
Ref(variable) | argument是对variable的引用。 |
TypedEq<type>(value) | argument的类型为type且等于value。当 mock 函数被重载时,可能必须用它替代Eq(value)。 |
关于拷贝语义:除Ref()外,上述 matcher 都会在内部拷贝一份value,以防其后续被修改或析构。如果编译器报错说value没有公开拷贝构造函数,可以尝试用std::ref()包装,例如Eq(std::ref(non_copyable_value))——但此时要保证non_copyable_value之后不再被修改,否则 matcher 的语义会悄然改变。
IsNullMatcher与NotNullMatcher在 gmock-matchers.h 中通过模板实现,可同时接受裸指针与智能指针(p == nullptr/p != nullptr);而RefMatcher<T>只能以引用类型T&实例化(见 gmock-matchers.h),从而在编译期阻止Ref(x)被误用于匹配非引用参数。
IsTrue/IsFalse的使用时机:当某类型可以显式转换到布尔、但不能隐式转换到布尔,同时又必须"用 matcher 来表达"时,这两个匹配器非常有用。其余常规场景,直接使用 EXPECT_TRUE / EXPECT_FALSE 基础断言即可。
三、浮点匹配器:ULP 比较与 NaN 语义
浮点数直接==比较几乎总是不可靠的,googletest 为此提供了专用的浮点匹配器。
近似相等(Approximate Equality)
| Matcher | 说明 |
|---|---|
DoubleEq(a_double) | argument是近似等于a_double的double,两个 NaN 视为不相等。 |
FloatEq(a_float) | argument是近似等于a_float的float,两个 NaN 视为不相等。 |
NanSensitiveDoubleEq(a_double) | 同上,但两个 NaN 视为相等。 |
NanSensitiveFloatEq(a_float) | 同上,但两个 NaN 视为相等。 |
IsNan() | argument是任意浮点类型且值为 NaN。 |
这些 matcher 采用ULP(Unit in the Last Place,最后一位单位)比较,与 googletest 内部浮点断言一致。它们会根据期望值的绝对值自动选取合理的误差界。DoubleEq()与FloatEq()遵循 IEEE 标准——即两个 NaN 的相等比较必须返回false;而NanSensitive*版本把两个 NaN 视为相等,这通常更符合用户直觉。
绝对误差界限(Near)
| Matcher | 说明 |
|---|---|
DoubleNear(a_double, max_abs_error) | argument是接近a_double的double(绝对误差 <=max_abs_error),两个 NaN 视为不相等。 |
FloatNear(a_float, max_abs_error) | 同上,针对float,两个 NaN 视为不相等。 |
NanSensitiveDoubleNear(a_double, max_abs_error) | 同DoubleNear,但两个 NaN 视为相等。 |
NanSensitiveFloatNear(a_float, max_abs_error) | 同FloatNear,但两个 NaN 视为相等。 |
当你需要精确控制容差(例如数据库中对聚合结果、统计值做精度校验)时,DoubleNear/FloatNear是首选。
四、字符串匹配器
argument可以是 C 字符串,也可以是 C++std::string对象:
| Matcher | 说明 |
|---|---|
ContainsRegex(string) | argument匹配给定正则表达式。 |
EndsWith(suffix) | argument以字符串suffix结尾。 |
HasSubstr(string) | argument包含子串string。 |
IsEmpty() | argument是空字符串。 |
MatchesRegex(string) | argument匹配给定正则,且匹配从首字符开始、到末字符结束(即整个字符串完整匹配)。 |
StartsWith(prefix) | argument以字符串prefix开头。 |
StrCaseEq(string) | argument与string相等(忽略大小写)。 |
StrCaseNe(string) | argument与string不相等(忽略大小写)。 |
StrEq(string) | argument与string相等。 |
StrNe(string) | argument与string不相等。 |
WhenBase64Unescaped(m) | argument是 base-64 转义字符串,其反转义后的字符串匹配m。支持 RFC 4648 中定义的网络安全(web-safe)格式。 |
其中ContainsRegex()与MatchesRegex()会接管RE对象的所有权,使用的正则语法定义见 advanced.md 的 Regular Expression Syntax 一节。除这两个正则匹配器外,其余字符串匹配器同样适用于宽字符串。
五、容器匹配器
大多数 STL 风格容器都支持==,因此可以直接用Eq(expected_container)或裸expected_container做精确匹配。但如果你想把元素内联写出来、希望匹配得更灵活,或需要更详细的失败信息,则应使用下面的匹配器:
| Matcher | 说明 |
|---|---|
BeginEndDistanceIs(m) | argument的begin()与end()迭代器之间相隔的步数匹配m。例如BeginEndDistanceIs(2)或BeginEndDistanceIs(Lt(2))。对定义了size()的容器,SizeIs(m)通常更高效。 |
ContainerEq(container) | 与Eq(container)相同,但失败信息会额外指出哪些元素只存在于其中一个容器。 |
Contains(e) | argument包含一个匹配e的元素;e可以是值或 matcher。 |
Contains(e).Times(n) | argument包含匹配e的元素,且匹配次数为n;n可以是值或 matcher。与普通Contains/Each不同,它支持检查任意出现次数,包括用Contains(e).Times(0)断言不存在。 |
Each(e) | argument是容器,且每个元素都匹配e;e可以是值或 matcher。 |
ElementsAre(e0, e1, ..., en) | argument有n + 1个元素,第i个元素匹配ei;ei可以是值或 matcher。 |
ElementsAreArray({e0, ..., en})、ElementsAreArray(a_container)、ElementsAreArray(begin, end)、ElementsAreArray(array)、ElementsAreArray(array, count) | 与ElementsAre()相同,但期望值/matcher 来自初始化列表、STL 容器、迭代器区间或 C 风格数组。 |
IsEmpty() | argument是空容器(container.empty())。 |
IsSubsetOf(...)(接受初始化列表 / 容器 / 迭代器区间 / 数组 / 数组+count) | argument匹配UnorderedElementsAre(x0, ..., xk),其中{x0, ..., xk}是期望 matchers 的某个子集。 |
IsSupersetOf(...)(同上五种形式) | argument的某个子集匹配UnorderedElementsAre(期望 matchers)。 |
Pointwise(m, container)、Pointwise(m, {e0, ..., en}) | argument与container元素个数相同,且对每个 i,argument第 i 个元素与container第 i 个元素组成的二元组匹配m(m是作用于二元组的 matcher)。例如Pointwise(Le(), upper_bounds)校验argument中每个元素都不超过upper_bounds中对应元素。 |
SizeIs(m) | argument的 size 匹配m。例如SizeIs(2)或SizeIs(Lt(2))。 |
UnorderedElementsAre(e0, e1, ..., en) | argument有n + 1个元素,且在某种排列下每个元素恰好匹配某个ei(不同的 i);ei可以是值或 matcher。 |
UnorderedElementsAreArray(...)(五种形式) | 与UnorderedElementsAre()相同,但期望值来自初始化列表 / STL 容器 / 迭代器区间 / C 风格数组。 |
UnorderedPointwise(m, container)、UnorderedPointwise(m, {e0, ..., en}) | 类似Pointwise(m, container),但忽略元素顺序。 |
WhenSorted(m) | 将argument用<排序后,匹配容器 matcherm。例如WhenSorted(ElementsAre(1, 2, 3))校验argument包含 1、2、3 这三个元素(忽略顺序)。 |
WhenSortedBy(comparator, m) | 与WhenSorted(m)相同,但用给定比较器而非<排序。例如WhenSortedBy(std::greater(), ElementsAre(3, 2, 1))。 |
补充说明:
- 这些容器 matcher 同样能匹配:① 以引用传入的原生数组(如
Foo(const int (&a)[5]));② 以"指针 + 长度"传入的数组(如Bar(const T* buffer, int len),见下文"多参数匹配器"一节)。 - 被匹配的数组可以是多维的(即元素本身是数组)。
Pointwise/UnorderedPointwise中的m应当是::std::tuple<T, U>的 matcher,其中T、U分别是实际容器与期望容器的元素类型。例如要比较两个不支持operator==的Foo容器:
MATCHER(FooEq, "") { return std::get<0>(arg).Equals(std::get<1>(arg)); } ... EXPECT_THAT(actual_foos, Pointwise(FooEq(), expected_foos));源码印证:googletest 自己的测试 gmock-matchers-containers_test.cc 展示了这些匹配器的实战形态——ElementsAre支持在位置上一一指定不同 matcher(Eq(1), Eq(2), Lt(13), ..., Gt(1), Eq(12)),UnorderedElementsAre则忽略顺序且同样支持混用通配符_与各类比较 matcher;SizeIsTest、WhenSortedTest等测试组覆盖了 map、multimap、原生数组、多态 matcher、流式容器等边缘场景(gmock-matchers-containers_test.cc)。
六、成员与属性匹配器
| Matcher | 说明 |
|---|---|
Field(&class::field, m) | argument.field(若argument是裸指针则为argument->field)匹配 matcherm;argument是_class_类型的对象。 |
Field(field_name, &class::field, m) | 同两参数版本,但提供更好的错误信息。 |
Key(e) | argument.first匹配e,e可以是值或 matcher。例如Contains(Key(Le(5)))可校验一个map包含<= 5的键。 |
Pair(m1, m2) | argument是std::pair,其first匹配m1、second匹配m2。 |
FieldsAre(m...) | argument是兼容对象,其各字段按顺序分别匹配m...。兼容对象指任何支持std::tuple_size<Obj>+get<I>(obj)协议的类型;在 C++17 及更高版本中,还支持与结构化绑定(structured bindings)兼容的类型,如聚合类型。 |
Property(&class::property, m) | argument.property()(裸指针时为argument->property())匹配m。方法property()必须无参数且声明为const。 |
Property(property_name, &class::property, m) | 同两参数版本,但提供更好的错误信息。 |
FieldsAre()示例——它可用于匹配任何支持结构化绑定的类型,如std::tuple、std::pair、std::array及聚合类型:
std::tuple<int, std::string> my_tuple{7, "hello world"}; EXPECT_THAT(my_tuple, FieldsAre(Ge(0), HasSubstr("hello"))); struct MyStruct { int value = 42; std::string greeting = "aloha"; }; MyStruct s; EXPECT_THAT(s, FieldsAre(42, "aloha"));注意事项:不要对你不拥有的成员函数使用Property()——取函数地址是脆弱操作,通常不属于该函数的契约范围。
七、函数结果匹配器与指针匹配器
Matching the Result of a Function, Functor, or Callback
| Matcher | 说明 |
|---|---|
ResultOf(f, m) | f(argument)匹配 matcherm;f是函数或仿函数(functor)。 |
ResultOf(result_description, f, m) | 同两参数版本,但提供更好的错误信息。 |
当你想"先对值做一次变换,再断言变换结果"时,ResultOf非常顺手,例如EXPECT_THAT(value, ResultOf([](auto x) { return x.size(); }, Ge(3)))。
Pointer Matchers(指针匹配器)
| Matcher | 说明 |
|---|---|
Address(m) | std::addressof(argument)的结果匹配m。 |
Pointee(m) | argument(智能指针或裸指针)指向的值匹配 matcherm。 |
Pointer(m) | argument(智能指针或裸指针)包含的指针匹配m;m总是与裸指针匹配,与argument的类型无关。 |
WhenDynamicCastTo<T>(m) | 当argument经过dynamic_cast<T>()转换后,结果匹配 matcherm。 |
Pointee与Pointer的区别值得注意:前者匹配"指针指向的目标值",后者匹配"指针本身(剥离为裸指针后)"。googletest 的 gmock-matchers-misc_test.cc 中即有Address的直接使用示例:const Matcher<int> m = Address(Eq(&n));。
八、多参数匹配器与参数选择器
严格来说,所有 matcher 匹配的都是单个值。所谓"多参数 matcher",其实只是匹配一个元组(tuple)。下面的 matchers 用于匹配二元组(x, y):
| Matcher | 说明 |
|---|---|
Eq() | x == y |
Ge() | x >= y |
Gt() | x > y |
Le() | x <= y |
Lt() | x < y |
Ne() | x != y |
还可以用下面的选择器,挑选参数子集(或重新排序)参与匹配:
| Matcher | 说明 |
|---|---|
AllArgs(m) | 等价于m,在.With(AllArgs(m))中作为语法糖很有用。 |
Args<N1, N2, ..., Nk>(m) | 选出的 k 个参数(按 0 起始下标)组成的元组匹配m,例如Args<1, 2>(Eq())。 |
典型场景:mock 一个void method(int a, int b)时,用EXPECT_CALL(mock, method(_, _)).With(Args<0, 1>(Lt()))断言"第一个参数小于第二个参数";而当函数签名为(const T* buffer, int len)时,配合容器 matcher 可把buffer与len一起匹配。
九、复合匹配器与适配器
Composite Matchers(由一个或多个 matcher 组合出新 matcher)
| Matcher | 说明 |
|---|---|
AllOf(m1, m2, ..., mn) | argument同时匹配m1到mn全部。 |
AllOfArray(...)(初始化列表 / STL 容器 / 迭代器区间 / C 风格数组 / 数组+count 五种形式) | 同AllOf(),但 matchers 来自上述集合。 |
AnyOf(m1, m2, ..., mn) | argument至少匹配m1到mn中之一。 |
AnyOfArray(...)(五种形式) | 同AnyOf(),但 matchers 来自集合。 |
Not(m) | argument不匹配 matcherm。 |
Conditional(cond, m1, m2) | cond求值为真时匹配m1,否则匹配m2。 |
上面的示例EXPECT_THAT(2, AllOf(Le(7), Ge(0)))就是复合匹配器的典型用法;Not与容器/字符串匹配器组合(如Not(IsEmpty()))也是高频写法。
Adapters for Matchers(matcher 适配器)
| Matcher | 说明 |
|---|---|
MatcherCast<T>(m) | 把 matcherm转型为Matcher<T>。 |
SafeMatcherCast<T>(m) | 安全地转型 matcherm为Matcher<T>,详见 gmock_cook_book.md 中的 SafeMatcherCast 一节。 |
Truly(predicate) | predicate(argument)返回 C++ 视为真的值;predicate是函数或仿函数。 |
AddressSatisfies(callback)与Truly(callback)会接管callback的所有权,因此callback必须是常驻(permanent)的回调对象。
十、把 Matcher 当作谓词使用
| Matcher | 说明 |
|---|---|
Matches(m)(value) | 若value匹配m则求值为true。Matches(m)本身可以作为一元仿函数单独使用。 |
ExplainMatchResult(m, value, result_listener) | 若value匹配m则求值为true,同时把匹配过程/失败原因解释给result_listener。 |
Value(value, m) | 若value匹配m则求值为true。 |
在不需要断言、只需要"按 matcher 逻辑做分支判断"的地方(例如自定义匹配器的内部实现),这三个工具非常实用,尤其是ExplainMatchResult,它能把你嵌套使用的子 matcher 的失败原因逐层传导到外层。
十一、自定义 Matcher:MATCHER 宏家族
内置 matcher 覆盖再广,也总有项目专属语义需要表达。googletest 提供MATCHER系列宏,在命名空间作用域定义可复用的自定义 matcher:
| 宏 | 说明 |
|---|---|
MATCHER(IsEven, "") { return (arg % 2) == 0; } | 定义 matcherIsEven(),匹配偶数。 |
MATCHER_P(IsDivisibleBy, n, "") { *result_listener << "where the remainder is " << (arg % n); return (arg % n) == 0; } | 定义带一个参数的 matcherIsDivisibleBy(n),匹配能被n整除的数,并把余数写入result_listener用于失败诊断。 |
MATCHER_P2(IsBetween, a, b, absl::StrCat(negation ? "isn't" : "is", " between ", PrintToString(a), " and ", PrintToString(b))) { return a <= arg && arg <= b; } | 定义带两个参数的 matcherIsBetween(a, b),匹配位于区间[a, b]内的值,且根据negation动态生成描述文本。 |
gmock-matchers.h头部注释(gmock-matchers.h)给出了这套宏的完整使用契约:宏体内部通过arg引用被匹配的值,通过arg_type引用其类型;描述字符串用于生成失败信息——由于 matcher 通常定义在头文件里被多个.cc共享,描述要求是 C 字符串字面量以避免副作用;描述为空时,自动由 matcher 名生成(如IsEven自动生成 "is even")。
使用与设计约束:
MATCHER*宏不能用在函数或类内部,只能在命名空间作用域使用。- matcher 主体必须是纯函数——不能有副作用,结果只能依赖于被匹配的值与 matcher 参数。
- 可用
PrintToString(x)把任意类型的值x转成字符串(用于动态描述)。 - 可在自定义 matcher 中调用
ExplainMatchResult()包装另一个 matcher,例如:
MATCHER_P(NestedPropertyMatches, matcher, "") { return ExplainMatchResult(matcher, arg.nested().property(), result_listener); }- 可用
DescribeMatcher<>描述另一个 matcher,例如组合描述:
MATCHER_P(XAndYThat, matcher, "X that " + DescribeMatcher<int>(matcher, negation) + (negation ? " or" : " and") + " Y that " + DescribeMatcher<double>(matcher, negation)) { return ExplainMatchResult(matcher, arg.x(), result_listener) && ExplainMatchResult(matcher, arg.y(), result_listener); }失败时你会得到类似 "X that is > 1 and Y that is > 2" 的可读信息。
十二、在 BusTub 项目中落地使用
测试基础设施已就绪
BusTub 的测试构建系统在 test/CMakeLists.txt 中定义:file(GLOB_RECURSE BUSTUB_TEST_SOURCES "${PROJECT_SOURCE_DIR}/test/*/*test.cpp")收集全部*test.cpp,每个测试可执行文件均target_link_libraries(${bustub_test_name} bustub gtest gmock_main)(test/CMakeLists.txt),并通过gtest_discover_tests注册到 CTest。也就是说,gmock 的 matcher 能力(包括gmock-matchers.h头文件)在本项目测试中开箱即用——只需包含头文件即可使用EXPECT_THAT、AllOf、ElementsAre等全部能力。
现有测试的断言风格与可迁移写法
BusTub 现有测试大量使用基础断言,例如 count_min_sketch_test.cpp 中的ASSERT_EQ(cms.Count("test"), 0)、ASSERT_EQ(cms.Count("Welcome to CMU DB (15-445/645)"), 1)。在此基础上,你可以用 matcher 升级断言表达力:
- 对数据库引擎的返回值集合:
EXPECT_THAT(result_vector, UnorderedElementsAre(...))验证元组集合(忽略顺序); - 对字符串类型的 SQL 结果:
EXPECT_THAT(col_value, HasSubstr("hello"))或StartsWith/EndsWith校验格式; - 对
std::optional/std::variant包装的列值:Optional(Ge(0))、VariantWith<BigIntType>(...); - 对 B+Tree 等容器型结构测试返回的键集合:
WhenSorted(ElementsAre(1, 2, 3))或SizeIs(42); - 对指针型数据结构(如
RID、页指针):IsNull()/NotNull()/Pointee(...); - 对需要同时满足多条件的复合校验:
AllOf(Ge(0), Lt(100)),反向条件用Not(...)。
应用前提说明:matcher 属 GoogleTest/Google Mock 通用能力,与 BusTub 具体业务无关,上述建议基于本仓库的测试组织方式(单测链接gmock_main、使用 googletest 断言宏)推断得出,可放心在 BusTub 的单元测试中直接采用。
结语
从EXPECT_THAT的等值匹配陷阱,到覆盖数值、字符串、容器、对象成员、指针、元组乃至自定义语义的完整 matcher 体系,GoogleTest 的 Matchers 把"断言"从简单的==提升到了"用可组合的迷你 DSL 描述期望"的层次。本文以 googletest 官方 matchers.md 为骨架,结合仓库内 gmock-matchers.h 的实现与 gmock-matchers-containers_test.cc、gmock-matchers-comparisons_test.cc、gmock-matchers-misc_test.cc 的测试用例做了逐项印证。掌握这套体系后,你在 BusTub 中编写的测试将更具表达力、可读性与诊断价值——这正是大型 C++ 工程中测试质量的关键分水岭。
- 数据库
- 教育
- 后端
【免费下载链接】bustub
The BusTub Relational Database Management System (Educational)
相关推荐
ArtCraft AI 创作 IDE 2026 路线图一文讲清
ArtCraft AI 创作 IDE 2026 路线图一文讲清 ArtCraft 这款 AI 创作 IDE 的 2026 年规划就两个大动作:模型覆盖从"够用"
人工智能AI 应用媒体生成桌面应用BusTub 测试实践:GoogleTest gMock Actions 参考指南
BusTub 测试实践:GoogleTest gMock Actions 参考指南 gMock 是 GoogleTest 内置的 C++ 模拟框架,本指南基于
数据库教育后端不到 1 分钟免费激活:MAS 激活脚本 1 个脚本搞定 4 种 Windows 与 Office 激活方式
不到 1 分钟免费激活:MAS 激活脚本 1 个脚本搞定 4 种 Windows 与 Office 激活方式 没有企业 KMS、手里也没有产品密钥,却需要尽快让
操作系统
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考