yard_example_test 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/GOVERNANCE.md ADDED
@@ -0,0 +1,103 @@
1
+ <!--
2
+ # @markup markdown
3
+ # @title Governance
4
+ -->
5
+
6
+ # Governance
7
+
8
+ This document explains how we steward the project with a light, principles-first
9
+ approach: enable trusted people, minimize dormant access, and keep decisions
10
+ transparent.
11
+
12
+ ## Roles
13
+
14
+ A **Maintainer** is a trusted leader with write access who stewards the project's
15
+ health and direction. Responsibilities center on triage, review, merge, and helping
16
+ the community stay unblocked.
17
+
18
+ A **Project Lead** is a maintainer with additional administrative scope (repo Admin,
19
+ org Owner). They handle settings, secrets, access, and tie-breaks when needed.
20
+
21
+ ## Becoming a Maintainer
22
+
23
+ Maintainers invite contributors who consistently ship, review, and model our values
24
+ to become maintainers. Anyone can nominate themselves or others in an issue or via a
25
+ private note. Current maintainers discuss nominations (see [Decision
26
+ Making](#decision-making)) with a focus on contribution quality, alignment with
27
+ project goals, and communication style.
28
+
29
+ ## Access Principles
30
+
31
+ - Stewardship: Maintainer access exists to keep the project healthy and responsive.
32
+ - Least privilege: Elevated access is temporary and kept only while it’s needed.
33
+ - Continuity: Dormant access is paused to protect the project and unblock
34
+ contributors.
35
+ - Respect: Status changes are transparent, reversible, and acknowledge past
36
+ contributions.
37
+
38
+ ## How We Apply Them
39
+
40
+ - Staying active: Maintainers keep elevated access while participating (shipping,
41
+ reviewing, triaging, or governance).
42
+ - When access is paused: If there’s no project activity for about a year, we’ll check
43
+ in. If we don’t hear back after a short window, we move the maintainer to Emeritus
44
+ and pause Owner/Admin/Write/package access (including CODEOWNERS entries).
45
+ - Coming back: Emeritus maintainers can be re-added quickly after a brief period of
46
+ renewed participation to refresh context.
47
+ - Recognition: Emeritus maintainers remain listed to honor prior contributions.
48
+
49
+ Access changes are communicated openly (e.g., PRs or issues) and reflected in
50
+ [MAINTAINERS.md](MAINTAINERS.md).
51
+
52
+ ## Decision Making
53
+
54
+ Decisions are usually made by consensus among the active maintainers. If consensus
55
+ cannot be reached, the decision is made by a majority vote. If a vote results in a
56
+ tie, the Project Lead has the final say.
57
+
58
+ ## Continuity
59
+
60
+ The project must be able to ship releases and respond to security issues even if
61
+ individual maintainers become unavailable.
62
+
63
+ ### RubyGems Ownership
64
+
65
+ RubyGems ownership (the ability to push new gem versions) is granted to a subset of
66
+ active maintainers—typically the Project Lead and at least one other maintainer—to
67
+ balance security with continuity. Not all maintainers require RubyGems access.
68
+
69
+ RubyGems owners follow the same activity principles as other elevated access: if an
70
+ owner becomes inactive, their ownership is paused alongside other permissions.
71
+
72
+ ### Minimum Thresholds
73
+
74
+ To avoid single points of failure:
75
+
76
+ - At least two active maintainers should have RubyGems ownership for the
77
+ `yard_example_test` gem.
78
+ - At least two active maintainers should have GitHub org Owner or repo Admin access.
79
+
80
+ If thresholds drop below these levels, remaining maintainers should prioritize
81
+ onboarding or re-activating someone to restore redundancy.
82
+
83
+ ### Access Audits
84
+
85
+ Periodically (at least annually), maintainers review access across all systems:
86
+
87
+ - GitHub organization membership and roles
88
+ - GitHub repository admin/write permissions
89
+ - RubyGems gem ownership
90
+ - GitHub Actions release automation: PATs/OIDC tokens (e.g., `AUTO_RELEASE_TOKEN`
91
+ scope), environment protection rules/approvers for RubyGems deployments, and any
92
+ OIDC trust configuration
93
+
94
+ The Project Lead (or a delegated maintainer) schedules and drives this review so
95
+ continuity checks do not slip.
96
+
97
+ Audits ensure access reflects current activity and that continuity thresholds are
98
+ met.
99
+
100
+ ## Code of Conduct
101
+
102
+ All maintainers and contributors must adhere to the project's [Code of
103
+ Conduct](CODE_OF_CONDUCT.md).
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,23 @@
1
+ Copyright (c) 2016 Alex Rodionov
2
+ Copyright (c) 2026 James Couball
3
+
4
+ MIT License
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining
7
+ a copy of this software and associated documentation files (the
8
+ "Software"), to deal in the Software without restriction, including
9
+ without limitation the rights to use, copy, modify, merge, publish,
10
+ distribute, sublicense, and/or sell copies of the Software, and to
11
+ permit persons to whom the Software is furnished to do so, subject to
12
+ the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be
15
+ included in all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/MAINTAINERS.md ADDED
@@ -0,0 +1,16 @@
1
+ <!--
2
+ # @markup markdown
3
+ # @title Maintainers
4
+ -->
5
+
6
+ # Maintainers
7
+
8
+ See [GOVERNANCE.md](GOVERNANCE.md) for the definition of the maintainer role.
9
+
10
+ ## Active Maintainers
11
+
12
+ - [James Couball](https://github.com/jcouball) (Project Lead)
13
+
14
+ ## Emeritus Maintainers
15
+
16
+ - None currently.
data/README.md ADDED
@@ -0,0 +1,526 @@
1
+ <!--
2
+ # @markup markdown
3
+ # @title README
4
+ -->
5
+
6
+ # yard_example_test
7
+
8
+ ![Gem Version](https://img.shields.io/gem/v/yard_example_test?label=gem%20version&color=green)
9
+ [![Continuous Integration](https://github.com/main-branch/yard_example_test/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/main-branch/yard_example_test/actions/workflows/continuous-integration.yml)
10
+ [![YARD Docs](https://img.shields.io/badge/docs-rubydoc.info-green.svg)](https://www.rubydoc.info/gems/yard_example_test)
11
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE.txt)
12
+
13
+ `YardExampleTest` is a YARD plugin that automatically parses `@example` tags in
14
+ your documentation and executes them as tests ensuring code examples remain
15
+ accurate and serve as a living, executable specification.
16
+
17
+ Annotate `@example` code with **expectation operators** (`#=>`):
18
+
19
+ ```ruby
20
+ # @example
21
+ # "hello".upcase #=> "HELLO"
22
+ ```
23
+
24
+ Each expectation operator verifies the actual value on the left against the expected
25
+ value on the right.
26
+
27
+ This project is derived from [yard-doctest](https://github.com/p0deje/yard-doctest),
28
+ created by [Alex Rodionov](https://github.com/p0deje) and contributors.
29
+
30
+ - [Installation](#installation)
31
+ - [Basic usage](#basic-usage)
32
+ - [Advanced usage](#advanced-usage)
33
+ - [Verifying raised exceptions](#verifying-raised-exceptions)
34
+ - [Shared example context](#shared-example-context)
35
+ - [Hooks](#hooks)
36
+ - [Skipping examples](#skipping-examples)
37
+ - [Using matchers (optional)](#using-matchers-optional)
38
+ - [RSpec matchers](#rspec-matchers)
39
+ - [Minitest matchers](#minitest-matchers)
40
+ - [Custom matchers](#custom-matchers)
41
+ - [Rake task](#rake-task)
42
+ - [Contributing](#contributing)
43
+
44
+ ## Installation
45
+
46
+ Add `yard_example_test` as a development dependency:
47
+
48
+ ```bash
49
+ bundle add yard_example_test --group development
50
+ ```
51
+
52
+ Or add it manually to your Gemfile and run `bundle install`:
53
+
54
+ ```ruby
55
+ gem 'yard_example_test', group: :development
56
+ ```
57
+
58
+ ## Basic usage
59
+
60
+ Consider a simple geometry library:
61
+
62
+ ```text
63
+ lib/
64
+ rectangle.rb
65
+ circle.rb
66
+ ```
67
+
68
+ Each file contains a class with documented examples:
69
+
70
+ ```ruby
71
+ # rectangle.rb
72
+ class Rectangle
73
+ # @example
74
+ # Rectangle.shape_name #=> 'rectangle'
75
+ def self.shape_name
76
+ 'rectangle'
77
+ end
78
+
79
+ def initialize(width, height)
80
+ @width = width
81
+ @height = height
82
+ end
83
+
84
+ # @example Unit square
85
+ # rect = Rectangle.new(1, 1)
86
+ # rect.area #=> 1
87
+ #
88
+ # @example Standard rectangle
89
+ # rect = Rectangle.new(4, 5)
90
+ # rect.area #=> 20
91
+ #
92
+ # @example Non-integer dimensions
93
+ # rect = Rectangle.new(2.5, 4.0)
94
+ # rect.area #=> 10.0
95
+ def area
96
+ @width * @height
97
+ end
98
+ end
99
+ ```
100
+
101
+ ```ruby
102
+ # circle.rb
103
+ class Circle
104
+ # @example
105
+ # Circle.shape_name #=> 'rectangle'
106
+ def self.shape_name
107
+ 'circle'
108
+ end
109
+
110
+ # @example Unit circle
111
+ # circle = Circle.new(1)
112
+ # circle.area.round(4) #=> 3.1416
113
+ def initialize(radius)
114
+ @radius = radius
115
+ end
116
+
117
+ def area
118
+ Math::PI * @radius**2
119
+ end
120
+ end
121
+ ```
122
+
123
+ First, tell YARD to automatically load `yard_example_test` by adding it as a plugin
124
+ in your `.yardopts`:
125
+
126
+ ```text
127
+ # .yardopts
128
+ --plugin yard_example_test
129
+ ```
130
+
131
+ Next, create a test helper that loads everything your examples need to run. It serves
132
+ a similar purpose to `spec_helper.rb` in RSpec or `test_helper.rb` in Minitest:
133
+
134
+ ```bash
135
+ touch example_test_helper.rb
136
+ ```
137
+
138
+ ```ruby
139
+ # example_test_helper.rb
140
+ require 'lib/rectangle'
141
+ require 'lib/circle'
142
+ ```
143
+
144
+ Now run your examples:
145
+
146
+ ```bash
147
+ $ bundle exec yard test-examples
148
+ Run options: --seed 5974
149
+
150
+ # Running:
151
+
152
+ ..F...
153
+
154
+ Finished in 0.015488s, 387.3967 runs/s, 387.3967 assertions/s.
155
+
156
+ 1) Failure:
157
+ Circle.shape_name#test_0001_ [lib/circle.rb:3]:
158
+ Expected: "rectangle"
159
+ Actual: "circle"
160
+
161
+ 6 runs, 6 assertions, 1 failures, 0 errors, 0 skips
162
+ ```
163
+
164
+ The `Circle.shape_name` example contains a copy-paste error. Correct it and run the
165
+ command again:
166
+
167
+ ```bash
168
+ $ sed -i.bak "s/#=> 'rectangle'/#=> 'circle'/" lib/circle.rb
169
+ $ bundle exec yard test-examples
170
+ Run options: --seed 51966
171
+
172
+ # Running:
173
+
174
+ ......
175
+
176
+ Finished in 0.002712s, 2212.3894 runs/s, 2212.3894 assertions/s.
177
+
178
+ 6 runs, 6 assertions, 0 failures, 0 errors, 0 skips
179
+ ```
180
+
181
+ Each expectation operator verifies the actual value on the left against the expected
182
+ value on the right. The right-hand side can be a plain value, a regular expression, a
183
+ range, or a matcher object (such as an RSpec, Minitest, or custom matcher) — each
184
+ evaluated according to its own `===` or `matches?` semantics rather than simple `==`
185
+ equality.
186
+
187
+ A single example can contain multiple expectation operators:
188
+
189
+ ```ruby
190
+ class Rectangle
191
+ # @example
192
+ # small = Rectangle.new(1, 2)
193
+ # small.area #=> 2
194
+ # large = Rectangle.new(3, 4)
195
+ # large.area #=> 12
196
+ def area
197
+ @width * @height
198
+ end
199
+ end
200
+ ```
201
+
202
+ This runs as a single test with multiple expectation operators:
203
+
204
+ ```bash
205
+ $ bundle exec yard test-examples lib/rectangle.rb
206
+ # ...
207
+ 1 runs, 2 assertions, 0 failures, 0 errors, 0 skips
208
+ ```
209
+
210
+ Examples without any expectation operators are still executed to verify that no
211
+ exceptions are raised:
212
+
213
+ ```ruby
214
+ class Rectangle
215
+ # @example
216
+ # rect = Rectangle.new(2, 3)
217
+ # rect.area
218
+ def area
219
+ @width * @height
220
+ end
221
+ end
222
+ ```
223
+
224
+ ```bash
225
+ $ bundle exec yard test-examples lib/rectangle.rb
226
+ # ...
227
+ 1 runs, 0 assertions, 0 failures, 0 errors, 0 skips
228
+ ```
229
+
230
+ Test execution is delegated to [minitest](https://github.com/minitest/minitest). Each
231
+ example is registered as an `it` block within a dynamically generated
232
+ `Minitest::Spec` subclass.
233
+
234
+ ## Advanced usage
235
+
236
+ ### Verifying raised exceptions
237
+
238
+ To verify that an example raises an exception, use `raise` on the right-hand side of
239
+ the expectation operator, specifying the exception class and message:
240
+
241
+ ```ruby
242
+ class Calculator
243
+ # @example
244
+ # divide(1, 0) #=> raise ZeroDivisionError, "divided by 0"
245
+ def divide(one, two)
246
+ one / two
247
+ end
248
+ end
249
+ ```
250
+
251
+ The raised exception is matched by comparing a string containing its class name and
252
+ message. The expected message must **exactly** match the message raised at runtime.
253
+
254
+ For more flexible exception matching — such as matching by class only or using a
255
+ regex on the message — see [RSpec matchers](#rspec-matchers), which supports
256
+ `raise_error`.
257
+
258
+ ### Shared example context
259
+
260
+ Shared example context is about making objects and methods available to examples. The
261
+ `example_test_helper.rb` file introduced in [Basic usage](#basic-usage) is loaded
262
+ before examples execute. Place shared helper methods there (or in
263
+ `support/example_test_helper.rb`, `spec/example_test_helper.rb`, or
264
+ `test/example_test_helper.rb`).
265
+
266
+ For instance, if an example references an object without constructing it:
267
+
268
+ ```ruby
269
+ class Rectangle
270
+ # @example Area of a shared rectangle
271
+ # rect.area #=> 20
272
+ def area
273
+ @width * @height
274
+ end
275
+ end
276
+ ```
277
+
278
+ Running this will fail because `rect` is not defined in the example:
279
+
280
+ ```bash
281
+ $ bundle exec yard test-examples
282
+ # ...
283
+ 1) Error:
284
+ Rectangle#area#test_0001_Area of a shared rectangle:
285
+ NameError: undefined local variable or method `rect' for Object:Class
286
+ # ...
287
+ ```
288
+
289
+ Define `rect` as a memoized method in `example_test_helper.rb` to make it available
290
+ across all examples:
291
+
292
+ ```ruby
293
+ # example_test_helper.rb
294
+ require 'lib/rectangle'
295
+ require 'lib/circle'
296
+
297
+ def rect
298
+ @rect ||= Rectangle.new(4, 5)
299
+ end
300
+ ```
301
+
302
+ ### Hooks
303
+
304
+ Hooks are lifecycle callbacks that run around each example, providing setup and
305
+ teardown behavior. They are defined in `example_test_helper.rb` using
306
+ `YardExampleTest.configure`:
307
+
308
+ ```ruby
309
+ YardExampleTest.configure do |runner|
310
+ runner.before do
311
+ # Runs before each example.
312
+ # Evaluated in the same context as the example,
313
+ # so instance variables are shared.
314
+ end
315
+
316
+ runner.after do
317
+ # Runs after each example.
318
+ # Also evaluated in the same context as the example.
319
+ end
320
+
321
+ runner.after_run do
322
+ # Runs once after all examples have finished.
323
+ # Evaluated in a separate context; instance variables
324
+ # from individual examples are not accessible here.
325
+ end
326
+ end
327
+ ```
328
+
329
+ Hooks can be scoped to a specific class, method, or named example by passing a
330
+ qualifier string:
331
+
332
+ ```ruby
333
+ YardExampleTest.configure do |runner|
334
+ runner.before('MyClass') do
335
+ # Runs before every example in `MyClass` and its methods
336
+ # (e.g. `MyClass.foo`, `MyClass#bar`)
337
+ end
338
+
339
+ runner.after('MyClass#foo') do
340
+ # Runs after every example for `MyClass#foo`
341
+ end
342
+
343
+ runner.before('MyClass#foo@Example one') do
344
+ # Runs before only the example named "Example one" in `MyClass#foo`
345
+ end
346
+ end
347
+ ```
348
+
349
+ ### Skipping examples
350
+
351
+ Examples can be excluded from a run by passing a class or method qualifier to
352
+ `runner.skip` in `example_test_helper.rb`. The qualifier is matched as a substring
353
+ of the example's class/method path, so skipping a class also skips all of its
354
+ methods:
355
+
356
+ ```ruby
357
+ YardExampleTest.configure do |runner|
358
+ runner.skip 'MyClass' # skips all examples in `MyClass` and its methods
359
+ runner.skip 'MyClass#foo' # skips all examples for `MyClass#foo` only
360
+ end
361
+ ```
362
+
363
+ Note that `skip` matches against the class/method path only. Skipping by named
364
+ example (e.g. `MyClass#foo@Example one`) is not supported; use a scoped
365
+ [hook](#hooks) to conditionally skip at that level of granularity.
366
+
367
+ ### Using matchers (optional)
368
+
369
+ The right-hand side of an expectation operator supports any object that implements
370
+ `matches?`. Matchers that also implement `failure_message` (or
371
+ `failure_message_for_should`) produce better
372
+ failure output. This includes [RSpec
373
+ matchers](https://rspec.info/documentation/3.12/rspec-expectations/),
374
+ [minitest-matchers](https://github.com/wojtekmach/minitest-matchers) or
375
+ [minitest-matchers_vaccine](https://github.com/rmm5t/minitest-matchers_vaccine), and
376
+ any custom matcher you write yourself.
377
+
378
+ #### RSpec matchers
379
+
380
+ Add `rspec-expectations` as a development dependency:
381
+
382
+ ```ruby
383
+ gem 'rspec-expectations', group: :development
384
+ ```
385
+
386
+ Include `RSpec::Matchers` in `example_test_helper.rb`:
387
+
388
+ ```ruby
389
+ # example_test_helper.rb
390
+ require 'rspec/expectations'
391
+ require 'rspec/matchers'
392
+
393
+ YardExampleTest::Example.include RSpec::Matchers
394
+ ```
395
+
396
+ ##### Value matchers
397
+
398
+ Matchers like `eq`, `be_within`, `a_kind_of`, and `include` are compared against the
399
+ evaluated actual value:
400
+
401
+ ```ruby
402
+ class Calculator
403
+ # @example
404
+ # Calculator.pi #=> be_within(0.01).of(3.14)
405
+ def self.pi
406
+ Math::PI
407
+ end
408
+
409
+ # @example
410
+ # Calculator.describe(42) #=> a_kind_of(String) & match(/positive/)
411
+ def self.describe(n)
412
+ n > 0 ? "positive number" : "non-positive number"
413
+ end
414
+ end
415
+ ```
416
+
417
+ ##### Block matchers
418
+
419
+ Matchers like `raise_error`, `change`, and `output` receive the actual expression as
420
+ a callable block, so the matcher can invoke it and inspect side-effects:
421
+
422
+ ```ruby
423
+ class Calculator
424
+ # @example
425
+ # Calculator.divide(1, 0) #=> raise_error(ZeroDivisionError)
426
+ #
427
+ # @example with message pattern
428
+ # Calculator.divide(1, 0) #=> raise_error(ZeroDivisionError, /divided/)
429
+ def self.divide(a, b)
430
+ a / b
431
+ end
432
+ end
433
+ ```
434
+
435
+ The expected side of an expectation operator is evaluated outside the documented
436
+ class's namespace, so matchers like `include` are never shadowed by Ruby's built-in
437
+ `Module#include`.
438
+
439
+ #### Minitest matchers
440
+
441
+ If you prefer to stay within the Minitest ecosystem, gems like
442
+ [minitest-matchers](https://github.com/wojtekmach/minitest-matchers) and
443
+ [minitest-matchers_vaccine](https://github.com/rmm5t/minitest-matchers_vaccine)
444
+ can be used alongside `yard_example_test`. Add one as a development dependency:
445
+
446
+ ```ruby
447
+ gem 'minitest-matchers_vaccine', group: :development
448
+ ```
449
+
450
+ If you use `minitest-matchers_vaccine`, require it in `example_test_helper.rb`:
451
+
452
+ ```ruby
453
+ # example_test_helper.rb
454
+ require 'minitest/matchers_vaccine'
455
+ ```
456
+
457
+ `yard_example_test` does not require including a matcher module on
458
+ `YardExampleTest::Example`. Any matcher object that follows the `matches?` /
459
+ `failure_message` protocol is recognized automatically.
460
+
461
+ #### Custom matchers
462
+
463
+ Any object that responds to `matches?` works as a matcher.
464
+ No external dependencies are required:
465
+
466
+ ```ruby
467
+ # example_test_helper.rb
468
+ class BePositive
469
+ def matches?(actual)
470
+ @actual = actual
471
+ actual > 0
472
+ end
473
+
474
+ def failure_message
475
+ "expected #{@actual} to be positive"
476
+ end
477
+ end
478
+
479
+ def be_positive
480
+ BePositive.new
481
+ end
482
+ ```
483
+
484
+ ```ruby
485
+ class Counter
486
+ # @example
487
+ # Counter.total #=> be_positive
488
+ def self.total
489
+ 42
490
+ end
491
+ end
492
+ ```
493
+
494
+ Block matchers additionally respond to `supports_block_expectations?` returning
495
+ `true`, which tells `yard_example_test` to wrap the actual expression in a proc
496
+ before passing it to `matches?`.
497
+
498
+ ### Rake task
499
+
500
+ A Rake task is available for integrating example runs into your build pipeline:
501
+
502
+ ```ruby
503
+ # Rakefile
504
+ require 'yard_example_test/rake'
505
+
506
+ YardExampleTest::RakeTask.new do |task|
507
+ task.test_examples_opts = %w[-v]
508
+ task.pattern = 'lib/**/*.rb'
509
+ end
510
+ ```
511
+
512
+ ```bash
513
+ bundle exec rake yard:test-examples
514
+ ```
515
+
516
+ ## Contributing
517
+
518
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution workflow and requirements.
519
+
520
+ Minimum expectations:
521
+
522
+ 1. Fork the repository and create a feature branch.
523
+ 2. Run `bin/setup` to install development dependencies.
524
+ 3. Make your changes and ensure `bundle exec rake` passes.
525
+ 4. Use [Conventional Commits](https://www.conventionalcommits.org/) for all commits.
526
+ 5. Open a pull request with a clear description of the change.