yard-rspec-coverage 0.1.3
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +13 -0
- data/LICENSE +21 -0
- data/README.md +185 -0
- data/lib/tasks/yard_rspec.rake +75 -0
- data/lib/yard-rspec-coverage.rb +12 -0
- data/lib/yard_rspec/config.rb +27 -0
- data/lib/yard_rspec/coverage.rb +37 -0
- data/lib/yard_rspec/describe_problems.rb +24 -0
- data/lib/yard_rspec/example_extractor.rb +148 -0
- data/lib/yard_rspec/handler.rb +158 -0
- data/lib/yard_rspec/method_description.rb +66 -0
- data/lib/yard_rspec/rake_tasks.rb +9 -0
- data/lib/yard_rspec/stats.rb +24 -0
- data/templates/default/fulldoc/html/css/rspec.css +33 -0
- data/templates/default/fulldoc/html/js/rspec.js +11 -0
- data/templates/default/layout/html/setup.rb +11 -0
- data/templates/default/method_details/html/rspec_specs.erb +14 -0
- data/templates/default/method_details/html/setup.rb +35 -0
- metadata +79 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f4579697cc4b66cd8e8f2542925a108797ed295a4ef8c1584a669f9d6a5e0a79
|
|
4
|
+
data.tar.gz: 04766fba38fc646ad5682f4c7e5d2a797059074e5b83bda4812743d2fa1f186f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 786fe926ae8c6260b343c25b918fd2f9b568fac0a9fe2e508a8fedb2c9dbae578a4a7303ddc34ed2bcd1942837c58f16d83704b1203a0b64b95b16c88eb4b623
|
|
7
|
+
data.tar.gz: c99f4df51422c71ddec7c5931662c6ad375c7a9f53b5dd9359956a69bb4096a0ff499f4d083373c48591676992fdf43aae5950cf5119b0c925afcf24c2fb3fd5
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.3
|
|
4
|
+
|
|
5
|
+
First public release as `yard-rspec-coverage`.
|
|
6
|
+
|
|
7
|
+
- The RSpec examples in the documentation have a new look:
|
|
8
|
+
- Example code is syntax-highlighted, like the method source.
|
|
9
|
+
- The heading shows the number of examples, with an "expand all" link when there is more
|
|
10
|
+
than one.
|
|
11
|
+
- Pending examples (`xit`, `xspecify`, `xexample`) get a "pending" label.
|
|
12
|
+
- Contexts are grouped with a line along their examples.
|
|
13
|
+
- A public method without specs gets a red "no specs" label.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 vdbijl
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# yard-rspec-coverage
|
|
2
|
+
|
|
3
|
+
A YARD plugin that embeds RSpec examples alongside method documentation in the generated YARD
|
|
4
|
+
documentation.
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
|
|
8
|
+
This plugin allows you to see RSpec test examples directly in your YARD-generated documentation.
|
|
9
|
+
When you document a method and have corresponding RSpec tests, those tests will automatically appear
|
|
10
|
+
in a "RSpec Examples" section in the method's documentation.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
Add this line to your application's Gemfile:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
gem 'yard-rspec-coverage'
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Or install it yourself:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
gem install yard-rspec-coverage
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
### Basic Usage
|
|
29
|
+
|
|
30
|
+
1. Write your Ruby code with YARD documentation comments
|
|
31
|
+
2. Write RSpec tests using `describe` and `it` blocks
|
|
32
|
+
3. Generate documentation with the plugin loaded:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
yard doc --plugin rspec-coverage 'lib/**/*.rb' 'spec/**/*.rb'
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
List the spec files explicitly: without file arguments (or files in `.yardopts`) YARD only parses
|
|
39
|
+
`lib/` and `app/`, so it finds no examples.
|
|
40
|
+
|
|
41
|
+
### Rake tasks
|
|
42
|
+
|
|
43
|
+
Declare the gem with `require: false` in your Gemfile, so you choose which YARD runs use the
|
|
44
|
+
plugin; `Bundler.require` would load it into every YARD run in the process. Then require the tasks
|
|
45
|
+
from your project's `Rakefile`:
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
require 'yard_rspec/rake_tasks'
|
|
49
|
+
|
|
50
|
+
YardRspec::Config.minimum_rspec_coverage = 80.0 # default: 100.0
|
|
51
|
+
YardRspec::Config.files = ['{lib,spec}/**/*.rb'] # default
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
This defines two tasks:
|
|
55
|
+
|
|
56
|
+
- `rake yard:doc` generates the documentation with the `rspec-coverage` plugin. It writes to `doc/`, like
|
|
57
|
+
other YARD tasks such as `yard:coverage`, so run it last.
|
|
58
|
+
- `rake yard:rspec_coverage` lists the public methods without RSpec examples and fails when the
|
|
59
|
+
coverage is below `minimum_rspec_coverage`. Attribute readers/writers and aliases are not
|
|
60
|
+
counted. It also lists describe blocks that name a class or method that cannot be found, such
|
|
61
|
+
as a typo, without failing.
|
|
62
|
+
|
|
63
|
+
The plugin never logs YARD warnings, so it is safe in YARD runs with `--fail-on-warning`.
|
|
64
|
+
|
|
65
|
+
Or use the provided build script:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
./build.sh
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Example
|
|
72
|
+
|
|
73
|
+
Given this code:
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
class String
|
|
77
|
+
# Converts a string to pig latin
|
|
78
|
+
#
|
|
79
|
+
# @return [String] the pig latin version of the string
|
|
80
|
+
def pig_latin
|
|
81
|
+
self[1..-1] + self[0] + "ay"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
describe String, '#pig_latin' do
|
|
86
|
+
it "converts 'hello' to pig latin" do
|
|
87
|
+
"hello".pig_latin.should == "ellohay"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it "works with single character strings" do
|
|
91
|
+
"a".pig_latin.should == "aay"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The generated documentation for `String#pig_latin` will include an "RSpec Examples" section showing both test cases.
|
|
97
|
+
|
|
98
|
+
## How It Works
|
|
99
|
+
|
|
100
|
+
The plugin consists of four main components:
|
|
101
|
+
|
|
102
|
+
1. **Handler** (`lib/yard_rspec/handler.rb`): Parses RSpec `describe` blocks and extracts `it` examples
|
|
103
|
+
2. **Template** (`templates/default/method_details/html/rspec_specs.erb`): Displays the examples in the documentation
|
|
104
|
+
3. **Setup** (`templates/default/method_details/html/setup.rb`): Integrates the template into YARD's rendering pipeline
|
|
105
|
+
4. **Assets** (`templates/default/fulldoc/html/`): A stylesheet and a script for the "expand all" link
|
|
106
|
+
|
|
107
|
+
Each example shows its description and its syntax-highlighted code. Pending
|
|
108
|
+
examples are labelled "pending", and a public method without specs gets a red "no specs" label.
|
|
109
|
+
|
|
110
|
+
## RSpec Format Support
|
|
111
|
+
|
|
112
|
+
The plugin supports the following RSpec syntax:
|
|
113
|
+
|
|
114
|
+
- `describe ClassName, '#method_name'` for instance methods
|
|
115
|
+
- `describe ClassName, '.method_name'` for class methods
|
|
116
|
+
- `it "description"` blocks within describe blocks
|
|
117
|
+
- `specify "description"` and `example "description"` blocks (aliases for `it`)
|
|
118
|
+
- `xit`, `xspecify` and `xexample` blocks (excluded examples), shown with a "(pending)" suffix.
|
|
119
|
+
They do not count as coverage: a method with only pending examples is reported as without
|
|
120
|
+
specs.
|
|
121
|
+
- nested `context "..."` and `describe "..."` blocks, shown as groups of examples
|
|
122
|
+
|
|
123
|
+
### Not supported
|
|
124
|
+
|
|
125
|
+
These RSpec constructs are not recognized. Examples written with them do not appear in the
|
|
126
|
+
documentation and do not count towards RSpec coverage.
|
|
127
|
+
|
|
128
|
+
- Examples that are not written as a command without parentheses followed by a block: `it('x') {}`
|
|
129
|
+
the one-liner `it {is_expected.to be_ok}` and `it do ... end` without a description.
|
|
130
|
+
- Focused examples: `fit`, `fspecify` and `fexample`. A focused example makes RSpec skip the rest
|
|
131
|
+
of the suite, so it should not be committed; the plugin does not document it.
|
|
132
|
+
- The `pending` and `skip` keywords.
|
|
133
|
+
- Capybara's `scenario` (and `feature` as a group), and the `its` method of rspec-its.
|
|
134
|
+
- Shared examples: `it_behaves_like`, `it_should_behave_like`, `include_examples` and
|
|
135
|
+
`include_context` are ignored, so the examples of a `shared_examples` group are not attributed to
|
|
136
|
+
the methods that use it. The examples inside a `shared_examples` block are attributed to the
|
|
137
|
+
enclosing method, as if they were written directly in the describe block.
|
|
138
|
+
- Focused and excluded groups: `fdescribe`, `xdescribe`, `fcontext`, `xcontext` and `example_group`
|
|
139
|
+
are not recognized as groups. Their examples are still collected, but without the group, so the
|
|
140
|
+
group's description is lost. A `describe '#method'` written with one of these keywords is not
|
|
141
|
+
linked to the method.
|
|
142
|
+
|
|
143
|
+
Limitations of the supported constructs:
|
|
144
|
+
|
|
145
|
+
- Only the text before the first interpolation of a description is kept: `it "loop #{index}"`
|
|
146
|
+
is shown as `loop `. Examples generated in a loop are listed once.
|
|
147
|
+
- Examples whose description is not a string literal are shown as `example`.
|
|
148
|
+
|
|
149
|
+
## Development
|
|
150
|
+
|
|
151
|
+
To test the plugin with the example code:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
bundle install
|
|
155
|
+
./build.sh
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Then open `doc/index.html` in your browser to see the generated documentation.
|
|
159
|
+
|
|
160
|
+
Run all checks (RuboCop, a dependency vulnerability audit, the specs with SimpleCov, YARD
|
|
161
|
+
documentation coverage and RSpec coverage) with:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
bundle exec rake
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Releasing
|
|
168
|
+
|
|
169
|
+
Update the version in `yard-rspec-coverage.gemspec` and `CHANGELOG.md`, then push a matching tag:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
git tag v0.1.3
|
|
173
|
+
git push origin v0.1.3
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The release workflow runs all checks and publishes the gem to rubygems.org with
|
|
177
|
+
[Trusted Publishing](https://guides.rubygems.org/trusted-publishing/), so no API key is stored.
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
|
|
181
|
+
MIT License
|
|
182
|
+
|
|
183
|
+
## Credits
|
|
184
|
+
|
|
185
|
+
Inspired by the [yard-spec-plugin](https://github.com/lsegal/yard-spec-plugin) by Loren Segal.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The tasks load the plugin when they run; loading it here would affect every YARD run in Rake.
|
|
4
|
+
require 'yard'
|
|
5
|
+
require_relative '../yard_rspec/config'
|
|
6
|
+
|
|
7
|
+
# @param [<YARD::CodeObjects::MethodObject>] uncovered
|
|
8
|
+
def uncovered_methods_report(uncovered)
|
|
9
|
+
return if uncovered.empty?
|
|
10
|
+
|
|
11
|
+
puts "\nMethods without RSpec specs:"
|
|
12
|
+
uncovered.sort_by(&:path).each do |method|
|
|
13
|
+
puts " #{method.file}:#{method.line}: #{method.path}"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @param [<YARD::CodeObjects::MethodObject>] public_lib_methods
|
|
18
|
+
# @return [Float] The coverage percentage.
|
|
19
|
+
def rspec_coverage_report(public_lib_methods)
|
|
20
|
+
total = public_lib_methods.size
|
|
21
|
+
uncovered = public_lib_methods.reject {|method| YardRspec::Coverage.covered?(method)}
|
|
22
|
+
covered = total - uncovered.size
|
|
23
|
+
coverage = total.positive? ? (covered.to_f / total * 100).round(2) : 100.0
|
|
24
|
+
|
|
25
|
+
puts format('RSpec coverage: %<covered>d/%<total>d (%<coverage>.2f%%)',
|
|
26
|
+
covered:, total:, coverage:)
|
|
27
|
+
uncovered_methods_report(uncovered)
|
|
28
|
+
|
|
29
|
+
coverage
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Applies .yardopts like `yard doc` does, so --exclude works. --no-output skips the markup check.
|
|
33
|
+
# @param [<String>] files Glob patterns; they replace the files in .yardopts.
|
|
34
|
+
# @return [YARD::CLI::Yardoc]
|
|
35
|
+
def yardoc_with_yardopts(files)
|
|
36
|
+
yardoc = YARD::CLI::Yardoc.new
|
|
37
|
+
yardoc.parse_arguments('--no-output', *files)
|
|
38
|
+
yardoc
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# These do not fail the coverage check.
|
|
42
|
+
# @param [<String>] describe_problems
|
|
43
|
+
def describe_problems_report(describe_problems)
|
|
44
|
+
return if describe_problems.empty?
|
|
45
|
+
|
|
46
|
+
puts "\nDescribe blocks without a matching class or method:"
|
|
47
|
+
describe_problems.each {|describe_problem| puts " #{describe_problem}"}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
namespace :yard do
|
|
51
|
+
desc 'Generate YARD documentation with the yard-rspec-coverage plugin'
|
|
52
|
+
task :doc do
|
|
53
|
+
# YARD ignores --plugin in-process.
|
|
54
|
+
require_relative '../yard-rspec-coverage'
|
|
55
|
+
YARD::CLI::Yardoc.run(*YardRspec::Config.files)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
desc 'Verify RSpec coverage of public methods (without attributes and aliases)'
|
|
59
|
+
task :rspec_coverage do
|
|
60
|
+
require_relative '../yard-rspec-coverage'
|
|
61
|
+
YARD::Registry.clear
|
|
62
|
+
yardoc = yardoc_with_yardopts(YardRspec::Config.files)
|
|
63
|
+
YARD.parse(yardoc.files, yardoc.excluded, YARD::Logger::ERROR)
|
|
64
|
+
|
|
65
|
+
coverage = rspec_coverage_report(YardRspec::Coverage.countable_methods)
|
|
66
|
+
describe_problems_report(YardRspec::DescribeProblems.all)
|
|
67
|
+
minimum = YardRspec::Config.minimum_rspec_coverage
|
|
68
|
+
|
|
69
|
+
if coverage < minimum
|
|
70
|
+
warn format("\nRSpec coverage too low: %<coverage>.2f%% (minimum is %<minimum>.2f%%)",
|
|
71
|
+
coverage:, minimum:)
|
|
72
|
+
exit 1
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yard'
|
|
4
|
+
require_relative 'yard_rspec/config'
|
|
5
|
+
require_relative 'yard_rspec/coverage'
|
|
6
|
+
require_relative 'yard_rspec/describe_problems'
|
|
7
|
+
require_relative 'yard_rspec/example_extractor'
|
|
8
|
+
require_relative 'yard_rspec/method_description'
|
|
9
|
+
require_relative 'yard_rspec/handler'
|
|
10
|
+
require_relative 'yard_rspec/stats'
|
|
11
|
+
|
|
12
|
+
YARD::Templates::Engine.register_template_path File.expand_path('../templates', __dir__)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The yard-rspec-coverage plugin.
|
|
4
|
+
module YardRspec
|
|
5
|
+
# Set in a project's Rakefile, e.g. `YardRspec::Config.minimum_rspec_coverage = 80.0`.
|
|
6
|
+
module Config
|
|
7
|
+
class << self
|
|
8
|
+
attr_writer :minimum_rspec_coverage, :files
|
|
9
|
+
|
|
10
|
+
# @return [Float] A percentage.
|
|
11
|
+
def minimum_rspec_coverage
|
|
12
|
+
@minimum_rspec_coverage || DEFAULT_MINIMUM_RSPEC_COVERAGE
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @return [<String>] Glob patterns of the source and spec files.
|
|
16
|
+
def files
|
|
17
|
+
@files || DEFAULT_FILES
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @return [Float]
|
|
22
|
+
DEFAULT_MINIMUM_RSPEC_COVERAGE = 100.0
|
|
23
|
+
|
|
24
|
+
# @return [<String>]
|
|
25
|
+
DEFAULT_FILES = ['{lib,spec}/**/*.rb'].freeze
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YardRspec
|
|
4
|
+
# Which methods count for RSpec coverage.
|
|
5
|
+
module Coverage
|
|
6
|
+
# @return [Regexp] Spec and helper files, whose methods need no specs.
|
|
7
|
+
SPEC_FILE_PATTERN = %r{(?:^|/)spec/|_spec\.rb$|_helper\.rb$}
|
|
8
|
+
|
|
9
|
+
# @return [<YARD::CodeObjects::MethodObject>]
|
|
10
|
+
def self.countable_methods
|
|
11
|
+
YARD::Registry.all(:method).select {|method| countable?(method)}
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Attributes and aliases have no behaviour of their own to spec.
|
|
15
|
+
# @param [YARD::CodeObjects::MethodObject] method
|
|
16
|
+
# @return [Boolean]
|
|
17
|
+
def self.countable?(method)
|
|
18
|
+
method.visibility == :public &&
|
|
19
|
+
!method.file.to_s.match?(SPEC_FILE_PATTERN) &&
|
|
20
|
+
!method.is_attribute? &&
|
|
21
|
+
!method.is_alias?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Pending examples (`xit`) do not run, so they do not count.
|
|
25
|
+
# @param [YARD::CodeObjects::MethodObject] method
|
|
26
|
+
# @return [Boolean]
|
|
27
|
+
def self.covered?(method)
|
|
28
|
+
examples(method[:rspec_examples] || []).any? {|example| !example[:pending]}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @param [<Hash>] nodes Example and context nodes, see `method[:rspec_examples]`.
|
|
32
|
+
# @return [<Hash>] The examples at any depth.
|
|
33
|
+
def self.examples(nodes)
|
|
34
|
+
nodes.flat_map {|node| node[:children] ? examples(node[:children]) : [node]}
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YardRspec
|
|
4
|
+
# Describe blocks naming a class or method that cannot be found. Not logged as YARD warnings,
|
|
5
|
+
# which would fail `yard --fail-on-warning`; `yard:rspec_coverage` lists them instead.
|
|
6
|
+
# Kept on the registry root, so YARD::Registry.clear also clears them.
|
|
7
|
+
module DescribeProblems
|
|
8
|
+
# @return [Symbol]
|
|
9
|
+
REGISTRY_KEY = :rspec_describe_problems
|
|
10
|
+
|
|
11
|
+
# @param [String] problem The location and description of the problem, for example
|
|
12
|
+
# "spec/widget_spec.rb:4: could not find method '#wobble' on 'Widget'".
|
|
13
|
+
def self.add(problem)
|
|
14
|
+
root = YARD::Registry.root
|
|
15
|
+
root[REGISTRY_KEY] ||= []
|
|
16
|
+
root[REGISTRY_KEY] << problem
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @return [<String>] Sorted by location.
|
|
20
|
+
def self.all
|
|
21
|
+
(YARD::Registry.root[REGISTRY_KEY] || []).sort
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YardRspec
|
|
4
|
+
# Extracts the RSpec examples of a describe block as a tree of example and context nodes.
|
|
5
|
+
# Excluded examples (xit/xspecify/xexample) get `pending: true`.
|
|
6
|
+
class ExampleExtractor
|
|
7
|
+
# @return [<String>] The keywords of examples that RSpec runs.
|
|
8
|
+
EXAMPLE_KEYWORDS = %w[it specify example].freeze
|
|
9
|
+
|
|
10
|
+
# @return [<String>] The keywords of examples that RSpec excludes from the run.
|
|
11
|
+
PENDING_KEYWORDS = %w[xit xspecify xexample].freeze
|
|
12
|
+
|
|
13
|
+
# @param [YARD::Parser::Ruby::AstNode, nil] block_node The body of a describe block.
|
|
14
|
+
def initialize(block_node)
|
|
15
|
+
@block_node = block_node
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @return [<Hash>] The example and context nodes.
|
|
19
|
+
def extract
|
|
20
|
+
return [] if @block_node.nil?
|
|
21
|
+
|
|
22
|
+
examples = []
|
|
23
|
+
traverse(@block_node, examples)
|
|
24
|
+
examples
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def traverse(node, nodes)
|
|
30
|
+
return if !ast_node?(node)
|
|
31
|
+
|
|
32
|
+
if example_call?(node)
|
|
33
|
+
nodes << build_example(node)
|
|
34
|
+
elsif context_call?(node)
|
|
35
|
+
add_context(node, nodes)
|
|
36
|
+
else
|
|
37
|
+
node.children.each {|child| traverse(child, nodes)}
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# A context without examples is left out.
|
|
42
|
+
def add_context(node, nodes)
|
|
43
|
+
children = []
|
|
44
|
+
traverse(node.last&.last, children)
|
|
45
|
+
return if children.empty?
|
|
46
|
+
|
|
47
|
+
nodes << { description: extract_context_description(node), children: children }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Includes the `x` variants.
|
|
51
|
+
# @return [Boolean]
|
|
52
|
+
def example_call?(node)
|
|
53
|
+
return false if node.type != :command
|
|
54
|
+
|
|
55
|
+
name_node = node.children[0]
|
|
56
|
+
name_node&.type == :ident && (EXAMPLE_KEYWORDS + PENDING_KEYWORDS).include?(name_node.source)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @return [Boolean]
|
|
60
|
+
def context_call?(node)
|
|
61
|
+
return false if node.type != :command
|
|
62
|
+
|
|
63
|
+
name_node = node.children[0]
|
|
64
|
+
name_node&.type == :ident && %w[context describe].include?(name_node.source)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @return [String, nil]
|
|
68
|
+
def extract_context_description(node)
|
|
69
|
+
find_string_literal(node.parameters.first)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# @return [Hash] With `pending: true` only for the `x` variants.
|
|
73
|
+
def build_example(node)
|
|
74
|
+
example = {
|
|
75
|
+
description: extract_description(node.children[1]) || 'example',
|
|
76
|
+
code: extract_code(node.children[2])
|
|
77
|
+
}
|
|
78
|
+
example[:pending] = true if PENDING_KEYWORDS.include?(node.children[0].source)
|
|
79
|
+
example
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @return [String, nil]
|
|
83
|
+
def extract_description(args_node)
|
|
84
|
+
return nil if !ast_node?(args_node)
|
|
85
|
+
|
|
86
|
+
find_string_literal(args_node)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# @return [String, nil] The first one at any depth.
|
|
90
|
+
def find_string_literal(node)
|
|
91
|
+
return nil if !ast_node?(node)
|
|
92
|
+
return extract_string_content(node) if node.type == :string_literal
|
|
93
|
+
|
|
94
|
+
node.children.each do |child|
|
|
95
|
+
result = find_string_literal(child)
|
|
96
|
+
return result if result
|
|
97
|
+
end
|
|
98
|
+
nil
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# @return [String, nil]
|
|
102
|
+
def extract_string_content(string_literal_node)
|
|
103
|
+
string_literal_node.children.each do |child|
|
|
104
|
+
next if !ast_node?(child) || child.type != :string_content
|
|
105
|
+
|
|
106
|
+
child.children.each do |grandchild|
|
|
107
|
+
return grandchild.source if grandchild.is_a?(YARD::Parser::Ruby::AstNode) &&
|
|
108
|
+
grandchild.type == :tstring_content
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# @return [String]
|
|
115
|
+
def extract_code(block_node)
|
|
116
|
+
return '' if !ast_node?(block_node)
|
|
117
|
+
return '' if !%i[do_block brace_block].include?(block_node.type)
|
|
118
|
+
|
|
119
|
+
body = block_node.children.first
|
|
120
|
+
return '' if !ast_node?(body)
|
|
121
|
+
|
|
122
|
+
# The source starts at the first token; restore its indentation before dedenting.
|
|
123
|
+
dedent((' ' * column_of(body)) + body.source.strip)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# @return [Integer] 0 if unknown.
|
|
127
|
+
def column_of(node)
|
|
128
|
+
file_source = node.full_source
|
|
129
|
+
start = node.source_range&.first
|
|
130
|
+
return 0 if file_source.nil? || start.nil? || start.zero?
|
|
131
|
+
|
|
132
|
+
newline_before = file_source.rindex("\n", start - 1)
|
|
133
|
+
newline_before.nil? ? start : start - newline_before - 1
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# @return [Boolean]
|
|
137
|
+
def ast_node?(node)
|
|
138
|
+
node.is_a?(YARD::Parser::Ruby::AstNode)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# @return [String]
|
|
142
|
+
def dedent(source)
|
|
143
|
+
lines = source.lines.map(&:rstrip)
|
|
144
|
+
min_indent = lines.reject(&:empty?).map {|line| line[/^\s*/].length}.min || 0
|
|
145
|
+
lines.map {|line| line[min_indent..] || line}.join("\n").strip
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YardRspec
|
|
4
|
+
# Attaches the examples of RSpec describe blocks to the described methods, as :rspec_examples.
|
|
5
|
+
# When parsing a file list, describes are processed after all files: YARD postpones files with
|
|
6
|
+
# unknown namespaces, so a spec may be parsed before its class.
|
|
7
|
+
class DescribeHandler < YARD::Handlers::Ruby::Base
|
|
8
|
+
handles method_call(:describe)
|
|
9
|
+
handles method_call(:context)
|
|
10
|
+
namespace_only
|
|
11
|
+
|
|
12
|
+
# Deferred while parsing a file list, see the callbacks at the end of this file.
|
|
13
|
+
def process
|
|
14
|
+
@describe_namespace = namespace
|
|
15
|
+
if globals.yard_rspec_deferred_describes
|
|
16
|
+
globals.yard_rspec_deferred_describes << -> {process_describe}
|
|
17
|
+
else
|
|
18
|
+
process_describe
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def process_describe
|
|
25
|
+
obj_name = unquote(statement.parameters.first.source)
|
|
26
|
+
|
|
27
|
+
# Method-only describes (describe '#eql?') are handled by their outer describe.
|
|
28
|
+
target_namespace = resolve_namespace(obj_name)
|
|
29
|
+
return if target_namespace.nil?
|
|
30
|
+
|
|
31
|
+
description = extract_method_description
|
|
32
|
+
if description
|
|
33
|
+
document_method(target_namespace, obj_name, description, statement.last&.last,
|
|
34
|
+
location_of)
|
|
35
|
+
else
|
|
36
|
+
process_nested_describes(target_namespace, obj_name)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @return [MethodDescription, nil] From the second describe parameter.
|
|
41
|
+
def extract_method_description
|
|
42
|
+
second_param = statement.parameters[1]
|
|
43
|
+
return nil if !ast_node?(second_param)
|
|
44
|
+
|
|
45
|
+
MethodDescription.parse(unquote(second_param.source))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Records a describe problem for an unknown class, unless it is loaded at runtime: a class
|
|
49
|
+
# from another gem (e.g. YARD::CLI::Stats) is not a typo.
|
|
50
|
+
# @return [YARD::CodeObjects::Base, nil]
|
|
51
|
+
def resolve_namespace(obj_name)
|
|
52
|
+
# Not a constant, e.g. describe 'Partial Order Reduction semantics'.
|
|
53
|
+
return nil if !obj_name.match?(/\A[A-Z][A-Za-z0-9_:]*\z/)
|
|
54
|
+
|
|
55
|
+
return @describe_namespace if obj_name == @describe_namespace.name.to_s
|
|
56
|
+
|
|
57
|
+
resolved = YARD::Registry.resolve(@describe_namespace, obj_name, true)
|
|
58
|
+
if resolved.nil? && !loaded_constant?(obj_name)
|
|
59
|
+
DescribeProblems.add("#{location_of}: could not resolve class '#{obj_name}'")
|
|
60
|
+
end
|
|
61
|
+
resolved
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @return [Boolean]
|
|
65
|
+
def loaded_constant?(obj_name)
|
|
66
|
+
Object.const_defined?(obj_name)
|
|
67
|
+
rescue NameError
|
|
68
|
+
false
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# A missing method is a describe problem, unless the description is imprecise.
|
|
72
|
+
def document_method(target_namespace, obj_name, description, block_body, location)
|
|
73
|
+
method_obj = description.find_in(target_namespace)
|
|
74
|
+
if method_obj
|
|
75
|
+
attach_examples_to(method_obj, block_body)
|
|
76
|
+
elsif !description.imprecise?
|
|
77
|
+
add_missing_method_problem(description, obj_name, location)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def add_missing_method_problem(description, obj_name, location)
|
|
82
|
+
DescribeProblems.add("#{location}: could not find method '#{description}' " \
|
|
83
|
+
"on '#{obj_name}'")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def attach_examples_to(method_obj, block_body)
|
|
87
|
+
examples = ExampleExtractor.new(block_body).extract
|
|
88
|
+
method_obj[:rspec_examples] ||= []
|
|
89
|
+
method_obj[:rspec_examples].concat(examples)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def process_nested_describes(target_namespace, obj_name)
|
|
93
|
+
block_body = statement.last&.last
|
|
94
|
+
return if block_body.nil?
|
|
95
|
+
|
|
96
|
+
each_method_describe(block_body) do |description, nested_block_body, node_line|
|
|
97
|
+
document_method(target_namespace, obj_name, description, nested_block_body,
|
|
98
|
+
location_of(node_line))
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Yields the description, block body and line of each nested method describe.
|
|
103
|
+
def each_method_describe(node, &)
|
|
104
|
+
return if !ast_node?(node)
|
|
105
|
+
|
|
106
|
+
if method_describe_node?(node)
|
|
107
|
+
description = MethodDescription.parse(unquote(node.parameters.first.source))
|
|
108
|
+
yield(description, node.last&.last, node.line)
|
|
109
|
+
else
|
|
110
|
+
node.children.each {|child| each_method_describe(child, &)}
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# @return [Boolean]
|
|
115
|
+
def method_describe_node?(node)
|
|
116
|
+
return false if node.type != :command
|
|
117
|
+
|
|
118
|
+
name_node = node.children[0]
|
|
119
|
+
return false if !describe_or_context_call?(name_node)
|
|
120
|
+
|
|
121
|
+
first_param = node.parameters.first
|
|
122
|
+
return false if !ast_node?(first_param)
|
|
123
|
+
|
|
124
|
+
unquote(first_param.source).start_with?('#', '.')
|
|
125
|
+
rescue StandardError
|
|
126
|
+
false
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# @return [Boolean]
|
|
130
|
+
def describe_or_context_call?(name_node)
|
|
131
|
+
name_node&.type == :ident && %w[describe context].include?(name_node.source)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# @return [Boolean]
|
|
135
|
+
def ast_node?(node)
|
|
136
|
+
node.is_a?(YARD::Parser::Ruby::AstNode)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# @return [String]
|
|
140
|
+
def unquote(source)
|
|
141
|
+
source.gsub(/['"]/, '')
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# @return [String] 'file:line', by default the line of the statement.
|
|
145
|
+
def location_of(line = statement.line || '?')
|
|
146
|
+
"#{statement.file || '(unknown)'}:#{line}"
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
YARD::Parser::SourceParser.before_parse_list do |_files, global_state|
|
|
152
|
+
global_state.yard_rspec_deferred_describes = []
|
|
153
|
+
nil # Returning false would cancel the parse.
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
YARD::Parser::SourceParser.after_parse_list do |_files, global_state|
|
|
157
|
+
global_state.yard_rspec_deferred_describes.each(&:call)
|
|
158
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YardRspec
|
|
4
|
+
# The method named by a describe string such as '#run', '.build' or '#initialize via Factory'.
|
|
5
|
+
class MethodDescription
|
|
6
|
+
# @return [Regexp] '#' (instance) or '.' (class), followed by the method name.
|
|
7
|
+
PATTERN = /^([#.])(\S+)/
|
|
8
|
+
|
|
9
|
+
# @return [String]
|
|
10
|
+
attr_reader :name
|
|
11
|
+
|
|
12
|
+
# @return [:instance, :class]
|
|
13
|
+
attr_reader :scope
|
|
14
|
+
|
|
15
|
+
# @param [String] text The describe string, without quotes.
|
|
16
|
+
# @return [MethodDescription, nil] Nil if the text names no method.
|
|
17
|
+
def self.parse(text)
|
|
18
|
+
match = text.match(PATTERN)
|
|
19
|
+
return nil if match.nil?
|
|
20
|
+
|
|
21
|
+
new(name: match[2], scope: match[1] == '#' ? :instance : :class,
|
|
22
|
+
imprecise: text.length > match[0].length)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @param [String] name
|
|
26
|
+
# @param [:instance, :class] scope
|
|
27
|
+
# @param [Boolean] imprecise
|
|
28
|
+
def initialize(name:, scope:, imprecise:)
|
|
29
|
+
@name = name
|
|
30
|
+
@scope = scope
|
|
31
|
+
@imprecise = imprecise
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Whether text follows the method name, as in '#initialize via SomeFactory'. A missing method
|
|
35
|
+
# is then not reported.
|
|
36
|
+
# @return [Boolean]
|
|
37
|
+
def imprecise?
|
|
38
|
+
@imprecise
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @return [String] '#name' or '.name'.
|
|
42
|
+
def to_s
|
|
43
|
+
"#{@scope == :instance ? '#' : '.'}#{name}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Falls back to the other scope, because specs sometimes write '#' for class methods.
|
|
47
|
+
# @param [YARD::CodeObjects::NamespaceObject] namespace
|
|
48
|
+
# @return [YARD::CodeObjects::MethodObject, nil] Also searched in superclasses and mixins.
|
|
49
|
+
def find_in(namespace)
|
|
50
|
+
find_with_scope(namespace, @scope) || find_with_scope(namespace, other_scope)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# Includes mixins, so examples end up on the module's method. Own methods come first.
|
|
56
|
+
def find_with_scope(namespace, scope)
|
|
57
|
+
namespace
|
|
58
|
+
.meths(scope: scope, visibility: %i[public protected private], included: true)
|
|
59
|
+
.find {|method| method.name.to_s == name}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def other_scope
|
|
63
|
+
@scope == :instance ? :class : :instance
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Defines the yard:doc and yard:rspec_coverage Rake tasks. Usage in a Rakefile:
|
|
4
|
+
#
|
|
5
|
+
# require 'yard_rspec/rake_tasks'
|
|
6
|
+
# YardRspec::Config.minimum_rspec_coverage = 80.0
|
|
7
|
+
require 'rake'
|
|
8
|
+
|
|
9
|
+
load File.expand_path('../tasks/yard_rspec.rake', __dir__)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
YARD::CLI::Stats.class_eval do
|
|
4
|
+
# Prints the RSpec specs row. Leaves @total/@undocumented alone, so "% documented" is unaffected.
|
|
5
|
+
def stats_for_rspec
|
|
6
|
+
public_lib_methods = YardRspec::Coverage.countable_methods
|
|
7
|
+
|
|
8
|
+
total = public_lib_methods.size
|
|
9
|
+
covered = public_lib_methods.count {|method| YardRspec::Coverage.covered?(method)}
|
|
10
|
+
|
|
11
|
+
log.puts "RSpec specs: #{rspec_specs_data(covered, total - covered)}"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
# @return [String]
|
|
17
|
+
def rspec_specs_data(covered, uncovered)
|
|
18
|
+
if uncovered.positive?
|
|
19
|
+
format('%<covered>5d (%<uncovered>5d without specs)', covered:, uncovered:)
|
|
20
|
+
else
|
|
21
|
+
format('%<covered>5d', covered:)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
.rspec_examples ul.rspec {
|
|
2
|
+
list-style: none;
|
|
3
|
+
padding-left: 0;
|
|
4
|
+
}
|
|
5
|
+
.rspec_context > ul.rspec {
|
|
6
|
+
margin: 3px 0 6px 4px;
|
|
7
|
+
padding-left: 12px;
|
|
8
|
+
border-left: 2px solid #e1e1e8;
|
|
9
|
+
}
|
|
10
|
+
.rspec_examples summary {
|
|
11
|
+
cursor: pointer;
|
|
12
|
+
}
|
|
13
|
+
.rspec_context_name {
|
|
14
|
+
font-style: italic;
|
|
15
|
+
color: #555;
|
|
16
|
+
}
|
|
17
|
+
.rspec_toggle {
|
|
18
|
+
margin-left: 6px;
|
|
19
|
+
font-size: 11px;
|
|
20
|
+
font-weight: normal;
|
|
21
|
+
}
|
|
22
|
+
.rspec_label {
|
|
23
|
+
padding: 1px 4px;
|
|
24
|
+
font-size: 11px;
|
|
25
|
+
font-weight: normal;
|
|
26
|
+
}
|
|
27
|
+
.rspec_pending {
|
|
28
|
+
background: #d5d5d5;
|
|
29
|
+
}
|
|
30
|
+
.rspec_missing {
|
|
31
|
+
color: #fff;
|
|
32
|
+
background: #cc0000;
|
|
33
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Opens or closes all examples of a method at once.
|
|
2
|
+
document.addEventListener('click', function(event) {
|
|
3
|
+
const toggle = event.target.closest('.rspec_toggle');
|
|
4
|
+
if (!toggle) return;
|
|
5
|
+
|
|
6
|
+
event.preventDefault();
|
|
7
|
+
const all_details = toggle.closest('.rspec_examples').querySelectorAll('details');
|
|
8
|
+
const open = !Array.from(all_details).every(details => details.open);
|
|
9
|
+
all_details.forEach(details => { details.open = open; });
|
|
10
|
+
toggle.textContent = open ? 'collapse all' : 'expand all';
|
|
11
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<% examples = object[:rspec_examples].to_a %>
|
|
2
|
+
<% if examples.any? || YardRspec::Coverage.countable?(object) %>
|
|
3
|
+
<div class="tags rspec_examples">
|
|
4
|
+
<% if examples.any? %>
|
|
5
|
+
<% count = YardRspec::Coverage.examples(examples).size %>
|
|
6
|
+
<p class="tag_title">RSpec Examples (<%= count %>):<% if count > 1 %> <a href="#" class="rspec_toggle">expand all</a><% end %></p>
|
|
7
|
+
<ul class="rspec">
|
|
8
|
+
<%= rspec_nodes_html(examples) %>
|
|
9
|
+
</ul>
|
|
10
|
+
<% else %>
|
|
11
|
+
<p class="tag_title">RSpec Examples: <span class="rspec_label rspec_missing">no specs</span></p>
|
|
12
|
+
<% end %>
|
|
13
|
+
</div>
|
|
14
|
+
<% end %>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Shows the examples before the source.
|
|
4
|
+
def init
|
|
5
|
+
super
|
|
6
|
+
sections.place(:rspec_specs).before(:source, true)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# @return [String]
|
|
10
|
+
def rspec_specs
|
|
11
|
+
erb(:rspec_specs)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @param [<Hash>] nodes Example and context nodes, see `method[:rspec_examples]`.
|
|
15
|
+
# @return [String] HTML list items.
|
|
16
|
+
def rspec_nodes_html(nodes)
|
|
17
|
+
nodes.map {|node| node[:children] ? rspec_group_html(node) : rspec_example_html(node)}.join
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @param [{description: String, code: String, pending: Boolean}] example
|
|
21
|
+
# @return [String]
|
|
22
|
+
def rspec_example_html(example)
|
|
23
|
+
pending = example[:pending] ? ' <span class="rspec_label rspec_pending">pending</span>' : ''
|
|
24
|
+
"<li><details><summary>#{h(example[:description])}#{pending}</summary>" \
|
|
25
|
+
"<pre class=\"example code\">#{html_syntax_highlight(example[:code], :ruby)}</pre>" \
|
|
26
|
+
'</details></li>'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @param [{description: String, children: [<Hash>]}] group
|
|
30
|
+
# @return [String]
|
|
31
|
+
def rspec_group_html(group)
|
|
32
|
+
'<li class="rspec_context">' \
|
|
33
|
+
"<span class=\"rspec_context_name\">#{h(group[:description])}</span>" \
|
|
34
|
+
"<ul class=\"rspec\">#{rspec_nodes_html(group[:children])}</ul></li>"
|
|
35
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: yard-rspec-coverage
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.3
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- vdbijl
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-10-31 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: yard
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0.9'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0.9'
|
|
27
|
+
description: A YARD plugin that embeds RSpec examples alongside method documentation,
|
|
28
|
+
and reports public methods without specs
|
|
29
|
+
email:
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- CHANGELOG.md
|
|
35
|
+
- LICENSE
|
|
36
|
+
- README.md
|
|
37
|
+
- lib/tasks/yard_rspec.rake
|
|
38
|
+
- lib/yard-rspec-coverage.rb
|
|
39
|
+
- lib/yard_rspec/config.rb
|
|
40
|
+
- lib/yard_rspec/coverage.rb
|
|
41
|
+
- lib/yard_rspec/describe_problems.rb
|
|
42
|
+
- lib/yard_rspec/example_extractor.rb
|
|
43
|
+
- lib/yard_rspec/handler.rb
|
|
44
|
+
- lib/yard_rspec/method_description.rb
|
|
45
|
+
- lib/yard_rspec/rake_tasks.rb
|
|
46
|
+
- lib/yard_rspec/stats.rb
|
|
47
|
+
- templates/default/fulldoc/html/css/rspec.css
|
|
48
|
+
- templates/default/fulldoc/html/js/rspec.js
|
|
49
|
+
- templates/default/layout/html/setup.rb
|
|
50
|
+
- templates/default/method_details/html/rspec_specs.erb
|
|
51
|
+
- templates/default/method_details/html/setup.rb
|
|
52
|
+
homepage: https://github.com/vdbijl/yard-rspec-coverage
|
|
53
|
+
licenses:
|
|
54
|
+
- MIT
|
|
55
|
+
metadata:
|
|
56
|
+
rubygems_mfa_required: 'true'
|
|
57
|
+
source_code_uri: https://github.com/vdbijl/yard-rspec-coverage
|
|
58
|
+
changelog_uri: https://github.com/vdbijl/yard-rspec-coverage/blob/main/CHANGELOG.md
|
|
59
|
+
bug_tracker_uri: https://github.com/vdbijl/yard-rspec-coverage/issues
|
|
60
|
+
post_install_message:
|
|
61
|
+
rdoc_options: []
|
|
62
|
+
require_paths:
|
|
63
|
+
- lib
|
|
64
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '3.3'
|
|
69
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - ">="
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: '0'
|
|
74
|
+
requirements: []
|
|
75
|
+
rubygems_version: 3.4.22
|
|
76
|
+
signing_key:
|
|
77
|
+
specification_version: 4
|
|
78
|
+
summary: YARD plugin to display RSpec examples in documentation and report RSpec coverage
|
|
79
|
+
test_files: []
|