完全指南:用可执行的代码描述 Ruby 行为规范)
Ruby Spec Suiteruby/spec完全指南用可执行的代码描述 Ruby 行为规范【免费下载链接】rubyThe Ruby Programming Language项目地址: https://gitcode.com/GitHub_Trending/ru/ruby导读本文以仓库内 spec/ruby/README.md 为主干系统讲解 Ruby Spec Suite简称 ruby/spec这一用可执行代码描述 Ruby 语言行为的测试套件它的定位与动机、目录组织方式、MSpec 运行器的使用、在不同 Ruby 实现上运行与同步的机制以及编写规范matchers、guards、shared specs的完整实践。读完本文你将掌握如何在本仓库CRuby/MRI 源码树中运行、筛选与编写 Ruby 行为规范并理解语言、核心库、标准库与 C 扩展 API 四大规范体系是如何在 spec/ruby/default.mspec 中组织起来的。Ruby Spec Suite 是什么Ruby Spec Suite缩写为ruby/spec是一套用于描述Ruby 编程语言行为的测试套件。它不是类似 ISO 那样的标准化规范也不以成为正式标准为目标——而是一个用代码描述、测试 Ruby 行为的实用工具。每条示例代码都带文本描述套件中的每一个示例example都附带一段文字描述这带来三方面优势更容易理解作者的意图阅读者不必靠猜就能知道这段断言想验证什么文档化最新版 Ruby 的行为规范本身即是可执行的文档描述“当前 Ruby 应当如何表现”帮助各 Ruby 实现达成行为一致MRI、JRuby、TruffleRuby 等实现都以其为共同行为基线。规范采用与RSpec 2相似的语法书写并通过MSpec——为运行 Ruby Spec Suite 而专门构建的测试框架——来执行。本仓库在 spec/mspec 目录下自带了一份 MSpec 的 vendored 副本包含lib/、spec/、tool/等子目录便于直接在 CRuby 源码树内运行规范。覆盖范围与分组方式规范覆盖了以下五个领域分别对应仓库中的目录领域目录内容示例语言语法spec/ruby/languageif、def、A::B、for、while、rescue、正则/字符串字面量等核心库spec/ruby/coreInteger#、String#upcase等无需 require 即可使用的方法标准库spec/ruby/libraryCSV.new、YAML.parse等需要 require stdlib 的方法C 扩展 APIspec/ruby/optional/capiC 扩展可调用的 Ruby C API 函数命令行参数spec/ruby/command_lineruby 可执行文件的-v、-e等命令行标志语言规范按关键字分组例如if_spec.rb、def_spec.rb、class_spec.rb核心库与标准库规范按类和方法分组例如core/kernel/、library/csv/。关于语言侧的分类哲学可参见 spec/ruby/language/README它主张“与其用计算理论的概念组织规范不如直接用 Ruby 语言的实体字面量、保留字、变量来组织”并指出false/true/nil/self归入predefined_spec.rb、in归入for_spec.rb、then/elsif归入if_spec.rb、when归入case_spec.rb、catch归入throw_spec.rb等合并规则。与各 Ruby 实现的 CI 关系README 声明ruby/spec 在每次提交时都会经过以下实现的测试MRI在 30 个平台、4 个版本上测试JRuby1.7 与 9.xTruffleRubyOpalArtichoke。ruby/spec 描述的是Ruby 3.3 及更新版本的行为。更精确地说每一个最新稳定版 MRI 发行版3.3.x、3.4.x 等都应在 CI 中通过 ruby/spec 的全部规范。与 Ruby 实现的月度双向同步ruby/spec 与 MRI、JRuby、TruffleRuby 之间每月进行一次双向同步各仓库都在自己的spec/ruby目录下保留一份规范的完整副本以方便就地编辑。这意味着想为某个实现测试开发版时应当使用该实现自身spec/ruby下的副本——那才是其 CI 真正测试的版本本仓库ruby/spec 的上游来源之一不一定包含 MRI 最新的规范变更同步是月度的也不包含 tags标记为在该实现上失败的规范。在某个 Ruby 实现上运行规范的通用方式是$ cd ruby_implementation/spec/ruby # 将 ../ruby_implementation/bin 加入 PATH或用 -t /path/to/bin/ruby 指定 $ ../mspec/bin/mspec在 CRuby 源码树中这套规范位于 spec/ruby与源码根目录的default.mspec见下文“在 CRuby 构建树中运行”配合使用。运行规范从零开始第一步获取 ruby/spec 与 MSpecREADME 给出的最小启动步骤是$ git clone https://github.com/ruby/spec.git $ cd spec $ git clone https://github.com/ruby/mspec.git ../mspec $ ../mspec/bin/mspec最后一条命令会用当前PATH中名为ruby的可执行文件运行全部规范。指定具体 Ruby 实现用-t选项指定运行规范所用的 Ruby 实现参数可以是 Ruby 二进制文件的完整路径也可以是$PATH中的可执行名$ ../mspec/bin/mspec -t /path/to/some/bin/ruby这在使用miniruby、ruby-debug或自定义构建版本做回归验证时尤其有用。运行选定的规范mspec接受文件、目录与分组三种粒度# 单个规范文件 $ ../mspec/bin/mspec core/kernel/kind_of_spec.rb # 整个目录 $ ../mspec/bin/mspec core/kernel # 按 default.mspec 中定义的分组运行 $ ../mspec/bin/mspec :language $ ../mspec/bin/mspec :core $ ../mspec/bin/mspec :library $ ../mspec/bin/mspec :capi分组在 spec/ruby/default.mspec 中定义其中不仅包含 README 提到的四组还扩展了更多分组键目录说明:languagelanguage语言特性规范:corecore核心库规范:librarylibrary标准库规范:command_linecommand_line命令行规范:securitysecurity安全相关规范:capioptional/capiC 扩展 API 规范:thread_safetyoptional/thread_safety线程安全规范:optionalcapi thread_safety全部可选规范:files以上全部实际运行的目录顺序command_line → language → core → library → security → optional:ci_files同:filesmspec ci运行时使用的文件集合此外default.mspec还设定了set :target, ruby默认实现、tags_patterns将language/、core/、library/等路径映射到tags/下的 tag 文件将_spec.rb映射到_tags.txt以及toplevel_constants_excludes运行泄漏检查时豁免\wSpecs?$、^CS_CONST、^CSL_CONST、^Prism等顶层常量。泄漏检查Sanity Checks运行规范时可以对多种“泄漏”开启检查文件描述符、临时文件、线程、子进程、ENV、ARGV、全局编码、顶层常量。启用方式$ CHECK_LEAKStrue ../mspec/bin/mspec关于顶层常量的规范新的顶层常量只在必要时引入或遵循ClassBeingTestedSpecs模式例如module StringSpecs其他用于测试的常量应嵌套在这样的模块之下例外情况记录在 spec/ruby/.mspec.constants 文件中可以用CHECK_LEAKSsave让 MSpec自动把新增的顶层常量追加进该文件$ CHECK_LEAKSsave mspec ../mspec/bin/mspec files390x 架构上的 zlib 相关问题在 s390x CPU 架构上如果看到与 zlib 库相关的失败规范可以加上DFLTCC0运行。这类失败可能源于 zlib 应用了 madler/zlib#410 补丁后deflate 算法产出了不同的压缩字节流$ DFLTCC0 ../mspec/bin/mspec运行所需的外部依赖规范运行依赖以下命令行可执行文件echostat用于core/file/*time_spec.rbfind用于core/file/fixtures/file_types.rb来自findutils包Windows 上不需要socket 相关规范还需要文件/etc/servicesDebian 上来自netbase包Windows 上不需要。在 CRuby 构建树中运行规范本仓库作为 CRuby/MRI 源码树其构建系统集成了 ruby/spec 的运行配置。根目录的 default.mspec 与 spec/default.mspec 展示了与上游 ruby/spec 略有差异的 MRI 化配置将默认:target设置为构建目录下的miniruby通过runruby.rb与--archdir/--extout传递构建目录与扩展输出路径确保规范针对当前构建而不是系统安装的 ruby 运行动态构造:library分组把 gems/bundled_gems 中列出的捆绑 gem 对应的规范openstruct会映射为ostruct从标准库规范中剔除分别归入:bundled_gems与:stdlibs默认开启常量泄漏检查ENV[CHECK_CONSTANT_LEAKS] || true并注入-W:no-experimental以确保子进程输出按原样断言内置MSpecScript::JobServer可借用测试框架的 jobserver 并行调度cores通过 prepend 定制DottedFormatter在终端上按固定列宽打印文件进度与点数。也就是说在本仓库构建完成后你可以直接在源码根目录运行make test-spec之类的目标参见 common.mk 与 defs/gmake.mk或手动以 default.mspec 作为配置启动 MSpec。规范自身的引导逻辑见 spec/ruby/spec_helper.rb它会校验VersionGuard::FULL_RUBY_VERSION SpecVersion.new(3.3)低于 3.3 会直接abort并支持在未设置MSPEC_RUNNER时直接用ruby some_spec.rb的方式运行单个规范此时会加载mspec/commands/mspec-run并执行MSpecRun.main。如何编写规范从 CONTRIBUTING.md 看最佳实践编写与贡献规范的完整文档见 spec/ruby/CONTRIBUTING.md。以下要点均出自该文件可直接套用。文件组织按方法的 owner 决定归属规范分为 5 个顶层分组command_line、language、core、library、optional/capi而某个方法归属哪个文件由其#owner决定。例如 [].method(:group_by) #Method: Array(Enumerable)#group_by [].method(:group_by).owner Enumerable因此group_by应写在core/enumerable/group_by_spec.rb而不是core/array/下。用 mkspec 生成规范骨架MSpec 附带的mkspec工具可用来生成规范结构$ ../mspec/bin/mkspec -h为尚未规范的模块或类创建文件例如为forwardable生成规范$ ../mspec/bin/mkspec -b library -rforwardable -c Forwardable-b指定core或library作为基准分组。查找尚未覆盖的核心方法也很简单在spec目录下执行ruby需为较新的 MRI$ ruby --disable-gem ../mspec/bin/mkspec也可以搜索it needs to be reviewed for spec completeness——该文案表示文件已生成但方法尚未被规范覆盖。Matchersshould语法规范的基本理念是在期望为真的谓词前加上.should即可。这套语法直接调用 Ruby 原有的比较方法失败时能给出清晰的错误也无需像 RSpec 那样记忆eq/的映射关系。比较类 matcher(1 2).should 3 # 调用 # (1 2).should_not 5 File.should.equal?(File) # 调用 #equal?测试同一性 (1 2).should.eql?(3) # 调用 #eql?Hash 相等性 1.should 2 2.should 2 3.should 3 4.should 3 Hello.should ~ /l{2}/ # 调用 #~正则匹配谓词类 matcher[].should.empty? [1,2,3].should.include?(2) hello.should.start_with?(h) hello.should.end_with?(o) (0.1 0.2).should be_close(0.3, TOLERANCE) # (0.2-0.1).abs TOLERANCE (0.0/0.0).should.nan? 3.14.should.instance_of?(Float) # 调用 #instance_of? 3.14.should.is_a?(Numeric) # 调用 #is_a? 3.14.should.respond_to?(:to_i) Integer.should.method_defined?(:, false)异常类 matcher- { raise oops }.should.raise(RuntimeError, /oops/) - { raise oops }.should.raise(RuntimeError) { |e| # 对 Exception 对象做自定义检查 e.message.should.include?(oops) e.cause.should nil }需要注意should_not.raise应当尽量避免与其断言“不抛异常”不如断言 lambda 中代码的实际结果一旦真的抛异常示例本来就会失败。警告 matcher- { Fixnum }.should complain(/constant ::Fixnum is deprecated/) # 期望产生警告一个真实的例子是 core/kernel/kind_of_spec.rb它用should 验证kind_of?是is_a?的别名require_relative ../../spec_helper describe Kernel#kind_of? do it is an alias of Kernel#is_a? do Kernel.instance_method(:kind_of?).should Kernel.instance_method(:is_a?) end endGuards按版本、平台与 bug 情况裁剪规范规范中使用各种 guard 来限定适用范围最常见的有版本 guardruby_version_is ...3.2 do # RUBY_VERSION 3.2 的规范 end ruby_version_is 3.2 do # RUBY_VERSION 3.2 的规范 end平台 guardplatform_is :windows do # 仅 Windows 有效 end platform_is_not :windows do # Windows 之外都有效 end platform_is :linux, :darwin do # OR 语义 end platform_is_not :linux, :darwin do # 既不是 Linux 也不是 Darwin end platform_is pointer_size: 64 do # 64 位平台 end big_endian do # 大端平台 endbug guardruby_bug仅当 MRI 存在 bug、且修复会被 backport 到旧版本时使用。使用前先在 https://bugs.ruby-lang.org/ 提交 bug。其语义等价于guard_not { RUBY_ENGINE ruby ruby_version_is ...X.Y }即在存在 bug 的指定 MRI 版本上跳过而在替代实现上执行ruby_bug #13669, ...3.2 do it works like this do # 这里应描述预期行为而不是 bug 本身 end end组合 guard 与自定义 guardguard - { platform_is :windows and ruby_version_is ...3.2 } do # Windows 且 RUBY_VERSION 3.2 end guard_not - { platform_is :windows and ruby_version_is ...3.2 } do # 相反情况 end max_uint (1 32) - 1 guard - { max_uint fixnum_max } do end自定义 guard 优于普通if因为 guard 能让mspec的命令如 tag 相关命令正常工作。CONTRIBUTING.md 特别强调没有用于定义“实现特有行为”的 guard——Ruby Spec Suite 定义的是共同行为而非实现细节实现特有行为应放到各实现自己的测试套件若某实现不支持某特性把相关规范打上 failing tag 即可。Shared Specs消除重复规范当多个方法/模块具有相同行为时用 shared specs 复用规范避免重复。仅在本模块内复用的 shared spec放在该模块目录下的shared/子目录中例如core/hash/shared/iteration.rb跨模块/类复用的放在顶层 spec/ruby/shared例如shared/file/socket.rb被core/file/socket_spec.rb、core/filetest/socket_spec.rb等共同使用。定义时在顶层describe上加shared: true选项表示该块不被 runner 直接执行shared spec 通过实例变量method和object接收调用方传入的参数# core/hash/shared/iteration.rb describe :hash_iteration_no_block, shared: true do it returns an Enumerator if called on a non-empty hash without a block do { 1 2 }.send(method).should.instance_of?(Enumerator) end end # core/hash/select_spec.rb describe Hash#select do it_behaves_like :hash_iteration_no_block, :select end # core/hash/reject_spec.rb describe Hash#reject do it_behaves_like :hash_iteration_no_block, :reject end当 shared spec 需要比“一个对象”更多的上下文时可以传入 lambda它会拥有实现方 spec 的作用域describe :kernel_sprintf, shared: true do it raises TypeError exception if cannot convert to Integer do - { method.call(%b, Object.new) }.should.raise(TypeError) end end describe Kernel#sprintf do it_behaves_like :kernel_sprintf, - format, *args { sprintf(format, *args) } end风格上CONTRIBUTING.md 要求不遗留行尾空格并遵循现有风格。历史沿革从 Rubinius 到 RubySpec 再到 Ruby Spec Suite项目最初源自Rubinius的测试被改写为 spec 风格这些规范后来被独立出来成为RubySpec项目拥有自己的愿景与原则2014 年底RubySpec 的创建者 Brian Shirai 宣布终止 RubySpec几个月后多个相关仓库被合并项目得以复活2016 年 1 月 12 日项目更名为 “The Ruby Spec Suite”让 RubySpec 的旧意识形态成为历史。另外spec/ruby/library 下的大多数 socket 规范源自rubysl-socket项目已不在 GitHub 上。该项目的三位版权持有者 Yorick Peterse、Chuck Remes 与 Brian Shirai 已同意将这些规范在 MIT 许可下重新授权给 ruby/spec因此可以在本仓库中继续使用与修改。结语Ruby Spec Suite 既是测试套件也是一份始终与代码同步的可执行语言文档。在 CRuby 源码树中spec/ruby 目录、default.mspec 分组配置与 CONTRIBUTING.md 编写规范构成了一个完整的闭环用 MSpec 运行、用 tag 管理实现差异、用 shared specs 消除重复、用 matchers 与 guards 精确表达预期。无论你是要验证某个 Ruby 实现的行为一致性还是想为语言新特性补充规范这套方法论都值得直接复用。【免费下载链接】rubyThe Ruby Programming Language项目地址: https://gitcode.com/GitHub_Trending/ru/ruby创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考