yard_example_runner 0.1.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_runner` 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,516 @@
1
+ <!--
2
+ # @markup markdown
3
+ # @title README
4
+ -->
5
+
6
+ # yard_example_runner
7
+
8
+ [![Gem
9
+ Version](https://badge.fury.io/rb/yard_example_runner.svg)](https://badge.fury.io/rb/yard_example_runner)
10
+
11
+ `YardExampleRunner` is a YARD plugin that automatically parses `@example` tags in
12
+ your documentation and executes them as tests. It ensures code examples remain
13
+ accurate and serve as a living, executable specification.
14
+
15
+ This project is derived from [yard-doctest](https://github.com/p0deje/yard-doctest),
16
+ created by [Alex Rodionov](https://github.com/p0deje) and contributors.
17
+
18
+ - [Installation](#installation)
19
+ - [Basic usage](#basic-usage)
20
+ - [Advanced usage](#advanced-usage)
21
+ - [Asserting raised exceptions](#asserting-raised-exceptions)
22
+ - [Shared example context](#shared-example-context)
23
+ - [Hooks](#hooks)
24
+ - [Skipping examples](#skipping-examples)
25
+ - [Using matchers (optional)](#using-matchers-optional)
26
+ - [RSpec matchers](#rspec-matchers)
27
+ - [Minitest matchers](#minitest-matchers)
28
+ - [Custom matchers](#custom-matchers)
29
+ - [Rake](#rake)
30
+ - [Contributing](#contributing)
31
+
32
+ ## Installation
33
+
34
+ Add `yard_example_runner` as a development dependency:
35
+
36
+ ```bash
37
+ bundle add yard_example_runner --group development
38
+ ```
39
+
40
+ Or add it manually to your Gemfile and run `bundle install`:
41
+
42
+ ```ruby
43
+ gem 'yard_example_runner', group: :development
44
+ ```
45
+
46
+ ## Basic usage
47
+
48
+ Consider a simple geometry library:
49
+
50
+ ```text
51
+ lib/
52
+ rectangle.rb
53
+ circle.rb
54
+ ```
55
+
56
+ Each file contains a class with documented examples:
57
+
58
+ ```ruby
59
+ # rectangle.rb
60
+ class Rectangle
61
+ # @example
62
+ # Rectangle.shape_name #=> 'rectangle'
63
+ def self.shape_name
64
+ 'rectangle'
65
+ end
66
+
67
+ def initialize(width, height)
68
+ @width = width
69
+ @height = height
70
+ end
71
+
72
+ # @example Unit square
73
+ # rect = Rectangle.new(1, 1)
74
+ # rect.area #=> 1
75
+ #
76
+ # @example Standard rectangle
77
+ # rect = Rectangle.new(4, 5)
78
+ # rect.area #=> 20
79
+ #
80
+ # @example Non-integer dimensions
81
+ # rect = Rectangle.new(2.5, 4.0)
82
+ # rect.area #=> 10.0
83
+ def area
84
+ @width * @height
85
+ end
86
+ end
87
+ ```
88
+
89
+ ```ruby
90
+ # circle.rb
91
+ class Circle
92
+ # @example
93
+ # Circle.shape_name #=> 'rectangle'
94
+ def self.shape_name
95
+ 'circle'
96
+ end
97
+
98
+ # @example Unit circle
99
+ # circle = Circle.new(1)
100
+ # circle.area.round(4) #=> 3.1416
101
+ def initialize(radius)
102
+ @radius = radius
103
+ end
104
+
105
+ def area
106
+ Math::PI * @radius**2
107
+ end
108
+ end
109
+ ```
110
+
111
+ First, tell YARD to automatically load `yard_example_runner` by adding it as a plugin
112
+ in your `.yardopts`:
113
+
114
+ ```bash
115
+ # .yardopts
116
+ --plugin yard_example_runner
117
+ ```
118
+
119
+ Next, create a test helper that loads everything your examples need to run. It serves
120
+ a similar purpose to `spec_helper.rb` in RSpec or `test_helper.rb` in Minitest:
121
+
122
+ ```bash
123
+ touch example_runner_helper.rb
124
+ ```
125
+
126
+ ```ruby
127
+ # example_runner_helper.rb
128
+ require 'lib/rectangle'
129
+ require 'lib/circle'
130
+ ```
131
+
132
+ Now run your examples:
133
+
134
+ ```bash
135
+ $ bundle exec yard run-examples
136
+ Run options: --seed 5974
137
+
138
+ # Running:
139
+
140
+ ..F...
141
+
142
+ Finished in 0.015488s, 387.3967 runs/s, 387.3967 assertions/s.
143
+
144
+ 1) Failure:
145
+ Circle.shape_name#test_0001_ [lib/circle.rb:3]:
146
+ Expected: "rectangle"
147
+ Actual: "circle"
148
+
149
+ 6 runs, 6 assertions, 1 failures, 0 errors, 0 skips
150
+ ```
151
+
152
+ The `Circle.shape_name` example contains a copy-paste error. Correct it and run the
153
+ command again:
154
+
155
+ ```bash
156
+ $ sed -i.bak "s/#=> 'rectangle'/#=> 'circle'/" lib/circle.rb
157
+ $ bundle exec yard run-examples
158
+ Run options: --seed 51966
159
+
160
+ # Running:
161
+
162
+ ......
163
+
164
+ Finished in 0.002712s, 2212.3894 runs/s, 2212.3894 assertions/s.
165
+
166
+ 6 runs, 6 assertions, 0 failures, 0 errors, 0 skips
167
+ ```
168
+
169
+ The `#=>` operator is an equality assertion: the expression on the left is the actual
170
+ value, the value on the right is the expected value, and they are compared using Ruby's
171
+ case equality operator (`===`). This means the right-hand side can be a plain value,
172
+ a regular expression, a range, or a matcher object (such as an RSpec, Minitest, or
173
+ custom matcher) — each evaluated according to its own `===` or `matches?` semantics
174
+ rather than simple `==` equality.
175
+
176
+ A single example can contain multiple assertions:
177
+
178
+ ```ruby
179
+ class Rectangle
180
+ # @example
181
+ # small = Rectangle.new(1, 2)
182
+ # small.area #=> 2
183
+ # large = Rectangle.new(3, 4)
184
+ # large.area #=> 12
185
+ def area
186
+ @width * @height
187
+ end
188
+ end
189
+ ```
190
+
191
+ This runs as a single test with multiple assertions:
192
+
193
+ ```bash
194
+ $ bundle exec yard run-examples lib/rectangle.rb
195
+ # ...
196
+ 1 runs, 2 assertions, 0 failures, 0 errors, 0 skips
197
+ ```
198
+
199
+ Examples without any assertions are still executed to verify that no exceptions are
200
+ raised:
201
+
202
+ ```ruby
203
+ class Rectangle
204
+ # @example
205
+ # rect = Rectangle.new(2, 3)
206
+ # rect.area
207
+ def area
208
+ @width * @height
209
+ end
210
+ end
211
+ ```
212
+
213
+ ```bash
214
+ $ bundle exec yard run-examples lib/rectangle.rb
215
+ # ...
216
+ 1 runs, 0 assertions, 0 failures, 0 errors, 0 skips
217
+ ```
218
+
219
+ Test execution is delegated to [minitest](https://github.com/minitest/minitest). Each
220
+ example is registered as an `it` block within a dynamically generated
221
+ `Minitest::Spec` subclass.
222
+
223
+ ## Advanced usage
224
+
225
+ ### Asserting raised exceptions
226
+
227
+ To assert that an example raises an exception, use `raise` on the right-hand side of
228
+ `#=>`, specifying the exception class and message:
229
+
230
+ ```ruby
231
+ class Calculator
232
+ # @example
233
+ # divide(1, 0) #=> raise ZeroDivisionError, "divided by 0"
234
+ def divide(one, two)
235
+ one / two
236
+ end
237
+ end
238
+ ```
239
+
240
+ The raised exception is matched by comparing a string containing its class name and
241
+ message. The message in the assertion must **exactly** match the message raised at
242
+ runtime.
243
+
244
+ For more flexible exception matching — such as matching by class only or using a
245
+ regex on the message — see [RSpec matchers](#rspec-matchers), which supports
246
+ `raise_error`.
247
+
248
+ ### Shared example context
249
+
250
+ Shared example context is about making objects and methods available to examples. The
251
+ `example_runner_helper.rb` file introduced in [Basic usage](#basic-usage) is loaded
252
+ before examples execute. Place shared helper methods there (or in
253
+ `support/example_runner_helper.rb`, `spec/example_runner_helper.rb`, or
254
+ `test/example_runner_helper.rb`).
255
+
256
+ Use hooks to set shared instance-variable state for examples:
257
+
258
+ For instance, if an example references an object without constructing it:
259
+
260
+ ```ruby
261
+ class Rectangle
262
+ # @example Area of a shared rectangle
263
+ # rect.area #=> 20
264
+ def area
265
+ @width * @height
266
+ end
267
+ end
268
+ ```
269
+
270
+ Running this will fail because `rect` is not defined in the example:
271
+
272
+ ```bash
273
+ $ bundle exec yard run-examples
274
+ # ...
275
+ 1) Error:
276
+ Rectangle#area#test_0001_Area of a shared rectangle:
277
+ NameError: undefined local variable or method `rect' for Object:Class
278
+ # ...
279
+ ```
280
+
281
+ Define `rect` as a memoized method in `example_runner_helper.rb` to make it available
282
+ across all examples:
283
+
284
+ ```ruby
285
+ # example_runner_helper.rb
286
+ require 'lib/rectangle'
287
+ require 'lib/circle'
288
+
289
+ def rect
290
+ @rect ||= Rectangle.new(4, 5)
291
+ end
292
+ ```
293
+
294
+ ### Hooks
295
+
296
+ Hooks are lifecycle callbacks that run around each example, providing setup and
297
+ teardown behavior. They are defined in `example_runner_helper.rb` using
298
+ `YardExampleRunner.configure`:
299
+
300
+ ```ruby
301
+ YardExampleRunner.configure do |runner|
302
+ runner.before do
303
+ # Runs before each example.
304
+ # Evaluated in the same context as the example,
305
+ # so instance variables are shared.
306
+ end
307
+
308
+ runner.after do
309
+ # Runs after each example.
310
+ # Also evaluated in the same context as the example.
311
+ end
312
+
313
+ runner.after_run do
314
+ # Runs once after all examples have finished.
315
+ # Evaluated in a separate context; instance variables
316
+ # from individual examples are not accessible here.
317
+ end
318
+ end
319
+ ```
320
+
321
+ Hooks can be scoped to a specific class, method, or named example by passing a
322
+ qualifier string:
323
+
324
+ ```ruby
325
+ YardExampleRunner.configure do |runner|
326
+ runner.before('MyClass') do
327
+ # Runs before every example in `MyClass` and its methods
328
+ # (e.g. `MyClass.foo`, `MyClass#bar`)
329
+ end
330
+
331
+ runner.after('MyClass#foo') do
332
+ # Runs after every example for `MyClass#foo`
333
+ end
334
+
335
+ runner.before('MyClass#foo@Example one') do
336
+ # Runs before only the example named "Example one" in `MyClass#foo`
337
+ end
338
+ end
339
+ ```
340
+
341
+ ### Skipping examples
342
+
343
+ Examples can be excluded from a run by passing a class or method qualifier to
344
+ `runner.skip` in `example_runner_helper.rb`. The qualifier is matched as a substring
345
+ of the example's class/method path, so skipping a class also skips all of its
346
+ methods:
347
+
348
+ ```ruby
349
+ YardExampleRunner.configure do |runner|
350
+ runner.skip 'MyClass' # skips all examples in `MyClass` and its methods
351
+ runner.skip 'MyClass#foo' # skips all examples for `MyClass#foo` only
352
+ end
353
+ ```
354
+
355
+ Note that `skip` matches against the class/method path only. Skipping by named
356
+ example (e.g. `MyClass#foo@Example one`) is not supported; use a scoped
357
+ [hook](#hooks) to conditionally skip at that level of granularity.
358
+
359
+ ### Using matchers (optional)
360
+
361
+ The right-hand side of `#=>` supports any object that implements `matches?`. Matchers
362
+ that also implement `failure_message` (or `failure_message_for_should`) produce better
363
+ failure output. This includes [RSpec
364
+ matchers](https://rspec.info/documentation/3.12/rspec-expectations/),
365
+ [minitest-matchers](https://github.com/wojtekmach/minitest-matchers) or
366
+ [minitest-matchers_vaccine](https://github.com/rmm5t/minitest-matchers_vaccine), and
367
+ any custom matcher you write yourself.
368
+
369
+ #### RSpec matchers
370
+
371
+ Add `rspec-expectations` as a development dependency:
372
+
373
+ ```ruby
374
+ gem 'rspec-expectations', group: :development
375
+ ```
376
+
377
+ Include `RSpec::Matchers` in `example_runner_helper.rb`:
378
+
379
+ ```ruby
380
+ # example_runner_helper.rb
381
+ require 'rspec/expectations'
382
+ require 'rspec/matchers'
383
+
384
+ YardExampleRunner::Example.include RSpec::Matchers
385
+ ```
386
+
387
+ ##### Value matchers
388
+
389
+ Matchers like `eq`, `be_within`, `a_kind_of`, and `include` are compared against the
390
+ evaluated actual value:
391
+
392
+ ```ruby
393
+ class Calculator
394
+ # @example
395
+ # Calculator.pi #=> be_within(0.01).of(3.14)
396
+ def self.pi
397
+ Math::PI
398
+ end
399
+
400
+ # @example
401
+ # Calculator.describe(42) #=> a_kind_of(String) & match(/positive/)
402
+ def self.describe(n)
403
+ n > 0 ? "positive number" : "non-positive number"
404
+ end
405
+ end
406
+ ```
407
+
408
+ ##### Block matchers
409
+
410
+ Matchers like `raise_error`, `change`, and `output` receive the actual expression as
411
+ a callable block, so the matcher can invoke it and inspect side-effects:
412
+
413
+ ```ruby
414
+ class Calculator
415
+ # @example
416
+ # Calculator.divide(1, 0) #=> raise_error(ZeroDivisionError)
417
+ #
418
+ # @example with message pattern
419
+ # Calculator.divide(1, 0) #=> raise_error(ZeroDivisionError, /divided/)
420
+ def self.divide(a, b)
421
+ a / b
422
+ end
423
+ end
424
+ ```
425
+
426
+ The expected side of `#=>` is evaluated outside the documented class's namespace,
427
+ so matchers like `include` are never shadowed by Ruby's built-in `Module#include`.
428
+
429
+ #### Minitest matchers
430
+
431
+ If you prefer to stay within the Minitest ecosystem, gems like
432
+ [minitest-matchers](https://github.com/wojtekmach/minitest-matchers) and
433
+ [minitest-matchers_vaccine](https://github.com/rmm5t/minitest-matchers_vaccine)
434
+ can be used alongside `yard_example_runner`. Add one as a development dependency:
435
+
436
+ ```ruby
437
+ gem 'minitest-matchers_vaccine', group: :development
438
+ ```
439
+
440
+ If you use `minitest-matchers_vaccine`, require it in `example_runner_helper.rb`:
441
+
442
+ ```ruby
443
+ # example_runner_helper.rb
444
+ require 'minitest/matchers_vaccine'
445
+ ```
446
+
447
+ `yard_example_runner` does not require including a matcher module on
448
+ `YardExampleRunner::Example`. Any matcher object that follows the `matches?` /
449
+ `failure_message` protocol is recognised automatically.
450
+
451
+ #### Custom matchers
452
+
453
+ Any object that responds to `matches?` works as a matcher.
454
+ No external dependencies are required:
455
+
456
+ ```ruby
457
+ # example_runner_helper.rb
458
+ class BePositive
459
+ def matches?(actual)
460
+ @actual = actual
461
+ actual > 0
462
+ end
463
+
464
+ def failure_message
465
+ "expected #{@actual} to be positive"
466
+ end
467
+ end
468
+
469
+ def be_positive
470
+ BePositive.new
471
+ end
472
+ ```
473
+
474
+ ```ruby
475
+ class Counter
476
+ # @example
477
+ # Counter.total #=> be_positive
478
+ def self.total
479
+ 42
480
+ end
481
+ end
482
+ ```
483
+
484
+ Block matchers additionally respond to `supports_block_expectations?` returning
485
+ `true`, which tells `yard_example_runner` to wrap the actual expression in a proc
486
+ before passing it to `matches?`.
487
+
488
+ ### Rake
489
+
490
+ A Rake task is available for integrating example runs into your build pipeline:
491
+
492
+ ```ruby
493
+ # Rakefile
494
+ require 'yard_example_runner/rake'
495
+
496
+ YardExampleRunner::RakeTask.new do |task|
497
+ task.run_examples_opts = %w[-v]
498
+ task.pattern = 'lib/**/*.rb'
499
+ end
500
+ ```
501
+
502
+ ```bash
503
+ bundle exec rake yard:run-examples
504
+ ```
505
+
506
+ ## Contributing
507
+
508
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution workflow and requirements.
509
+
510
+ Minimum expectations:
511
+
512
+ 1. Fork the repository and create a feature branch.
513
+ 2. Run `bin/setup` to install development dependencies.
514
+ 3. Make your changes and ensure `bundle exec rake` passes.
515
+ 4. Use [Conventional Commits](https://www.conventionalcommits.org/) for all commits.
516
+ 5. Open a pull request with a clear description of the change.