t-ruby 0.0.7 → 0.0.34
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 +4 -4
- data/README.md +6 -2
- data/lib/t_ruby/cli.rb +131 -6
- data/lib/t_ruby/compiler.rb +97 -9
- data/lib/t_ruby/config.rb +366 -24
- data/lib/t_ruby/docs_badge_generator.rb +192 -0
- data/lib/t_ruby/docs_example_extractor.rb +156 -0
- data/lib/t_ruby/docs_example_verifier.rb +222 -0
- data/lib/t_ruby/error_handler.rb +191 -13
- data/lib/t_ruby/parser.rb +33 -0
- data/lib/t_ruby/version.rb +1 -1
- data/lib/t_ruby/watcher.rb +42 -12
- data/lib/t_ruby.rb +5 -0
- metadata +4 -1
data/lib/t_ruby/config.rb
CHANGED
|
@@ -3,51 +3,393 @@
|
|
|
3
3
|
require "yaml"
|
|
4
4
|
|
|
5
5
|
module TRuby
|
|
6
|
+
# Error raised when configuration is invalid
|
|
7
|
+
class ConfigError < StandardError; end
|
|
8
|
+
|
|
6
9
|
class Config
|
|
10
|
+
# Valid strictness levels
|
|
11
|
+
VALID_STRICTNESS = %w[strict standard permissive].freeze
|
|
12
|
+
# New schema structure (v0.0.12+)
|
|
7
13
|
DEFAULT_CONFIG = {
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
14
|
+
"source" => {
|
|
15
|
+
"include" => ["src"],
|
|
16
|
+
"exclude" => [],
|
|
17
|
+
"extensions" => [".trb"]
|
|
18
|
+
},
|
|
19
|
+
"output" => {
|
|
20
|
+
"ruby_dir" => "build",
|
|
21
|
+
"rbs_dir" => nil,
|
|
22
|
+
"preserve_structure" => true,
|
|
23
|
+
"clean_before_build" => false
|
|
12
24
|
},
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
25
|
+
"compiler" => {
|
|
26
|
+
"strictness" => "standard",
|
|
27
|
+
"generate_rbs" => true,
|
|
28
|
+
"target_ruby" => "3.0",
|
|
29
|
+
"experimental" => [],
|
|
30
|
+
"checks" => {
|
|
31
|
+
"no_implicit_any" => false,
|
|
32
|
+
"no_unused_vars" => false,
|
|
33
|
+
"strict_nil" => false
|
|
34
|
+
}
|
|
16
35
|
},
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
36
|
+
"watch" => {
|
|
37
|
+
"paths" => [],
|
|
38
|
+
"debounce" => 100,
|
|
39
|
+
"clear_screen" => false,
|
|
40
|
+
"on_success" => nil
|
|
21
41
|
}
|
|
22
42
|
}.freeze
|
|
23
43
|
|
|
24
|
-
|
|
44
|
+
# Legacy keys for migration detection
|
|
45
|
+
LEGACY_KEYS = %w[emit paths strict include exclude].freeze
|
|
46
|
+
|
|
47
|
+
# Always excluded (not configurable)
|
|
48
|
+
AUTO_EXCLUDE = [".git"].freeze
|
|
49
|
+
|
|
50
|
+
attr_reader :source, :output, :compiler, :watch, :version_requirement
|
|
25
51
|
|
|
26
52
|
def initialize(config_path = nil)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
@
|
|
53
|
+
raw_config = load_raw_config(config_path)
|
|
54
|
+
config = process_config(raw_config)
|
|
55
|
+
|
|
56
|
+
@source = config["source"]
|
|
57
|
+
@output = config["output"]
|
|
58
|
+
@compiler = config["compiler"]
|
|
59
|
+
@watch = config["watch"]
|
|
60
|
+
@version_requirement = raw_config["version"]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Get output directory for compiled Ruby files
|
|
64
|
+
# @return [String] output directory path
|
|
65
|
+
def ruby_dir
|
|
66
|
+
@output["ruby_dir"] || "build"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Get output directory for RBS files
|
|
70
|
+
# @return [String] RBS output directory (defaults to ruby_dir if not specified)
|
|
71
|
+
def rbs_dir
|
|
72
|
+
@output["rbs_dir"] || ruby_dir
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Check if source directory structure should be preserved in output
|
|
76
|
+
# @return [Boolean] true if structure should be preserved
|
|
77
|
+
def preserve_structure?
|
|
78
|
+
@output["preserve_structure"] != false
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Check if output directory should be cleaned before build
|
|
82
|
+
# @return [Boolean] true if should clean before build
|
|
83
|
+
def clean_before_build?
|
|
84
|
+
@output["clean_before_build"] == true
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Get compiler strictness level
|
|
88
|
+
# @return [String] one of: strict, standard, permissive
|
|
89
|
+
def strictness
|
|
90
|
+
@compiler["strictness"] || "standard"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Check if RBS files should be generated
|
|
94
|
+
# @return [Boolean] true if RBS files should be generated
|
|
95
|
+
def generate_rbs?
|
|
96
|
+
@compiler["generate_rbs"] != false
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Get target Ruby version
|
|
100
|
+
# @return [String] target Ruby version (e.g., "3.0", "3.2")
|
|
101
|
+
def target_ruby
|
|
102
|
+
(@compiler["target_ruby"] || "3.0").to_s
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Get list of enabled experimental features
|
|
106
|
+
# @return [Array<String>] list of experimental feature names
|
|
107
|
+
def experimental_features
|
|
108
|
+
@compiler["experimental"] || []
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Check if a specific experimental feature is enabled
|
|
112
|
+
# @param feature [String] feature name to check
|
|
113
|
+
# @return [Boolean] true if feature is enabled
|
|
114
|
+
def experimental_enabled?(feature)
|
|
115
|
+
experimental_features.include?(feature)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Check if no_implicit_any check is enabled
|
|
119
|
+
# @return [Boolean] true if check is enabled
|
|
120
|
+
def check_no_implicit_any?
|
|
121
|
+
@compiler.dig("checks", "no_implicit_any") == true
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Check if no_unused_vars check is enabled
|
|
125
|
+
# @return [Boolean] true if check is enabled
|
|
126
|
+
def check_no_unused_vars?
|
|
127
|
+
@compiler.dig("checks", "no_unused_vars") == true
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Check if strict_nil check is enabled
|
|
131
|
+
# @return [Boolean] true if check is enabled
|
|
132
|
+
def check_strict_nil?
|
|
133
|
+
@compiler.dig("checks", "strict_nil") == true
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Get additional watch paths
|
|
137
|
+
# @return [Array<String>] list of additional paths to watch
|
|
138
|
+
def watch_paths
|
|
139
|
+
@watch["paths"] || []
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Get watch debounce delay in milliseconds
|
|
143
|
+
# @return [Integer] debounce delay in milliseconds
|
|
144
|
+
def watch_debounce
|
|
145
|
+
@watch["debounce"] || 100
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Check if terminal should be cleared before rebuild
|
|
149
|
+
# @return [Boolean] true if terminal should be cleared
|
|
150
|
+
def watch_clear_screen?
|
|
151
|
+
@watch["clear_screen"] == true
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Get command to run after successful compilation
|
|
155
|
+
# @return [String, nil] command to run on success
|
|
156
|
+
def watch_on_success
|
|
157
|
+
@watch["on_success"]
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Check if current T-Ruby version satisfies the version requirement
|
|
161
|
+
# @return [Boolean] true if version is satisfied or no requirement specified
|
|
162
|
+
def version_satisfied?
|
|
163
|
+
return true if @version_requirement.nil?
|
|
164
|
+
|
|
165
|
+
requirement = Gem::Requirement.new(@version_requirement)
|
|
166
|
+
current = Gem::Version.new(TRuby::VERSION)
|
|
167
|
+
requirement.satisfied_by?(current)
|
|
168
|
+
rescue ArgumentError
|
|
169
|
+
false
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Validate the configuration
|
|
173
|
+
# @raise [ConfigError] if configuration is invalid
|
|
174
|
+
def validate!
|
|
175
|
+
validate_strictness!
|
|
176
|
+
true
|
|
31
177
|
end
|
|
32
178
|
|
|
179
|
+
# Backwards compatible: alias for ruby_dir
|
|
33
180
|
def out_dir
|
|
34
|
-
|
|
181
|
+
ruby_dir
|
|
35
182
|
end
|
|
36
183
|
|
|
184
|
+
# Backwards compatible: first source.include directory
|
|
37
185
|
def src_dir
|
|
38
|
-
@
|
|
186
|
+
@source["include"].first || "src"
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Get source include directories
|
|
190
|
+
# @return [Array<String>] list of include directories
|
|
191
|
+
def source_include
|
|
192
|
+
@source["include"] || ["src"]
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Get source exclude patterns
|
|
196
|
+
# @return [Array<String>] list of exclude patterns
|
|
197
|
+
def source_exclude
|
|
198
|
+
@source["exclude"] || []
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Get source file extensions
|
|
202
|
+
# @return [Array<String>] list of file extensions (e.g., [".trb", ".truby"])
|
|
203
|
+
def source_extensions
|
|
204
|
+
@source["extensions"] || [".trb"]
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Get include patterns for file discovery
|
|
208
|
+
def include_patterns
|
|
209
|
+
extensions = @source["extensions"] || [".trb"]
|
|
210
|
+
extensions.map { |ext| "**/*#{ext}" }
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Get exclude patterns
|
|
214
|
+
def exclude_patterns
|
|
215
|
+
@source["exclude"] || []
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Find all source files matching include patterns, excluding exclude patterns
|
|
219
|
+
# @return [Array<String>] list of matching file paths
|
|
220
|
+
def find_source_files
|
|
221
|
+
files = []
|
|
222
|
+
|
|
223
|
+
@source["include"].each do |include_dir|
|
|
224
|
+
base_dir = File.expand_path(include_dir)
|
|
225
|
+
next unless Dir.exist?(base_dir)
|
|
226
|
+
|
|
227
|
+
include_patterns.each do |pattern|
|
|
228
|
+
full_pattern = File.join(base_dir, pattern)
|
|
229
|
+
files.concat(Dir.glob(full_pattern))
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Filter out excluded files
|
|
234
|
+
files.reject { |f| excluded?(f) }.uniq.sort
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Check if a file path should be excluded
|
|
238
|
+
# @param file_path [String] absolute or relative file path
|
|
239
|
+
# @return [Boolean] true if file should be excluded
|
|
240
|
+
def excluded?(file_path)
|
|
241
|
+
relative_path = relative_to_src(file_path)
|
|
242
|
+
all_exclude_patterns.any? { |pattern| matches_pattern?(relative_path, pattern) }
|
|
39
243
|
end
|
|
40
244
|
|
|
41
245
|
private
|
|
42
246
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
247
|
+
# Validate strictness value
|
|
248
|
+
def validate_strictness!
|
|
249
|
+
value = strictness
|
|
250
|
+
return if VALID_STRICTNESS.include?(value)
|
|
251
|
+
|
|
252
|
+
raise ConfigError, "Invalid compiler.strictness: '#{value}'. Must be one of: #{VALID_STRICTNESS.join(', ')}"
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def load_raw_config(config_path)
|
|
256
|
+
raw = if config_path && File.exist?(config_path)
|
|
257
|
+
YAML.safe_load_file(config_path, permitted_classes: [Symbol]) || {}
|
|
258
|
+
elsif File.exist?("trbconfig.yml")
|
|
259
|
+
YAML.safe_load_file("trbconfig.yml", permitted_classes: [Symbol]) || {}
|
|
260
|
+
else
|
|
261
|
+
{}
|
|
262
|
+
end
|
|
263
|
+
expand_env_vars(raw)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Expand environment variables in config values
|
|
267
|
+
# Supports ${VAR} and ${VAR:-default} syntax
|
|
268
|
+
def expand_env_vars(obj)
|
|
269
|
+
case obj
|
|
270
|
+
when Hash
|
|
271
|
+
obj.transform_values { |v| expand_env_vars(v) }
|
|
272
|
+
when Array
|
|
273
|
+
obj.map { |v| expand_env_vars(v) }
|
|
274
|
+
when String
|
|
275
|
+
expand_env_string(obj)
|
|
276
|
+
else
|
|
277
|
+
obj
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Expand environment variables in a single string
|
|
282
|
+
def expand_env_string(str)
|
|
283
|
+
str.gsub(/\$\{([^}]+)\}/) do |_match|
|
|
284
|
+
var_expr = ::Regexp.last_match(1)
|
|
285
|
+
if var_expr.include?(":-")
|
|
286
|
+
var_name, default_value = var_expr.split(":-", 2)
|
|
287
|
+
ENV.fetch(var_name, default_value)
|
|
288
|
+
else
|
|
289
|
+
ENV.fetch(var_expr, "")
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def process_config(raw_config)
|
|
295
|
+
if legacy_config?(raw_config)
|
|
296
|
+
warn "DEPRECATED: trbconfig.yml uses legacy format. Please migrate to new schema (source/output/compiler/watch)."
|
|
297
|
+
migrate_legacy_config(raw_config)
|
|
298
|
+
else
|
|
299
|
+
merge_with_defaults(raw_config)
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def legacy_config?(raw_config)
|
|
304
|
+
LEGACY_KEYS.any? { |key| raw_config.key?(key) }
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def migrate_legacy_config(raw_config)
|
|
308
|
+
result = deep_dup(DEFAULT_CONFIG)
|
|
309
|
+
|
|
310
|
+
# Migrate emit -> compiler.generate_rbs
|
|
311
|
+
if raw_config["emit"]
|
|
312
|
+
result["compiler"]["generate_rbs"] = raw_config["emit"]["rbs"] if raw_config["emit"].key?("rbs")
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# Migrate paths -> source.include and output.ruby_dir
|
|
316
|
+
if raw_config["paths"]
|
|
317
|
+
if raw_config["paths"]["src"]
|
|
318
|
+
src_path = raw_config["paths"]["src"].sub(%r{^\./}, "")
|
|
319
|
+
result["source"]["include"] = [src_path]
|
|
320
|
+
end
|
|
321
|
+
if raw_config["paths"]["out"]
|
|
322
|
+
out_path = raw_config["paths"]["out"].sub(%r{^\./}, "")
|
|
323
|
+
result["output"]["ruby_dir"] = out_path
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# Migrate include/exclude patterns
|
|
328
|
+
if raw_config["include"]
|
|
329
|
+
# Keep legacy include patterns as-is for now
|
|
330
|
+
result["source"]["include"] = [result["source"]["include"].first || "src"]
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
if raw_config["exclude"]
|
|
334
|
+
result["source"]["exclude"] = raw_config["exclude"]
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
result
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def merge_with_defaults(user_config)
|
|
341
|
+
result = deep_dup(DEFAULT_CONFIG)
|
|
342
|
+
deep_merge(result, user_config)
|
|
343
|
+
result
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def deep_dup(hash)
|
|
347
|
+
hash.each_with_object({}) do |(key, value), result|
|
|
348
|
+
result[key] = value.is_a?(Hash) ? deep_dup(value) : (value.is_a?(Array) ? value.dup : value)
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def deep_merge(target, source)
|
|
353
|
+
source.each do |key, value|
|
|
354
|
+
if value.is_a?(Hash) && target[key].is_a?(Hash)
|
|
355
|
+
deep_merge(target[key], value)
|
|
356
|
+
elsif !value.nil?
|
|
357
|
+
target[key] = value
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# Combine auto-excluded patterns with user-configured patterns
|
|
363
|
+
def all_exclude_patterns
|
|
364
|
+
patterns = AUTO_EXCLUDE.dup
|
|
365
|
+
patterns << out_dir.sub(%r{^\./}, "") # Add output directory
|
|
366
|
+
patterns.concat(exclude_patterns)
|
|
367
|
+
patterns.uniq
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# Convert absolute path to relative path from first src_dir
|
|
371
|
+
def relative_to_src(file_path)
|
|
372
|
+
base_dir = File.expand_path(src_dir)
|
|
373
|
+
full_path = File.expand_path(file_path)
|
|
374
|
+
|
|
375
|
+
if full_path.start_with?(base_dir)
|
|
376
|
+
full_path.sub("#{base_dir}/", "")
|
|
48
377
|
else
|
|
49
|
-
|
|
378
|
+
file_path
|
|
50
379
|
end
|
|
51
380
|
end
|
|
381
|
+
|
|
382
|
+
# Check if path matches a glob/directory pattern
|
|
383
|
+
def matches_pattern?(path, pattern)
|
|
384
|
+
# Direct directory match (e.g., "node_modules" matches "node_modules/foo.rb")
|
|
385
|
+
return true if path.start_with?("#{pattern}/") || path == pattern
|
|
386
|
+
|
|
387
|
+
# Check if any path component matches
|
|
388
|
+
path_parts = path.split("/")
|
|
389
|
+
return true if path_parts.include?(pattern)
|
|
390
|
+
|
|
391
|
+
# Glob pattern match
|
|
392
|
+
File.fnmatch?(pattern, path, File::FNM_PATHNAME | File::FNM_DOTMATCH)
|
|
393
|
+
end
|
|
52
394
|
end
|
|
53
395
|
end
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "docs_example_verifier"
|
|
4
|
+
|
|
5
|
+
module TRuby
|
|
6
|
+
# Generates badges and reports for documentation verification results.
|
|
7
|
+
#
|
|
8
|
+
# Supports:
|
|
9
|
+
# - Shields.io compatible JSON badges
|
|
10
|
+
# - SVG badge generation
|
|
11
|
+
# - Markdown report generation
|
|
12
|
+
# - JSON report generation
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# generator = DocsBadgeGenerator.new
|
|
16
|
+
# verifier = DocsExampleVerifier.new
|
|
17
|
+
# results = verifier.verify_glob("docs/**/*.md")
|
|
18
|
+
# generator.generate_badge(results, "coverage/docs_badge.json")
|
|
19
|
+
#
|
|
20
|
+
class DocsBadgeGenerator
|
|
21
|
+
# Badge colors based on pass rate
|
|
22
|
+
COLORS = {
|
|
23
|
+
excellent: "brightgreen", # 95-100%
|
|
24
|
+
good: "green", # 80-94%
|
|
25
|
+
fair: "yellow", # 60-79%
|
|
26
|
+
poor: "orange", # 40-59%
|
|
27
|
+
critical: "red", # 0-39%
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
def initialize
|
|
31
|
+
@verifier = DocsExampleVerifier.new
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Generate all outputs
|
|
35
|
+
#
|
|
36
|
+
# @param results [Array<DocsExampleVerifier::VerificationResult>] Results
|
|
37
|
+
# @param output_dir [String] Output directory
|
|
38
|
+
def generate_all(results, output_dir)
|
|
39
|
+
FileUtils.mkdir_p(output_dir)
|
|
40
|
+
|
|
41
|
+
generate_badge_json(results, File.join(output_dir, "docs_badge.json"))
|
|
42
|
+
generate_badge_svg(results, File.join(output_dir, "docs_badge.svg"))
|
|
43
|
+
generate_report_json(results, File.join(output_dir, "docs_report.json"))
|
|
44
|
+
generate_report_markdown(results, File.join(output_dir, "docs_report.md"))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Generate Shields.io compatible JSON badge
|
|
48
|
+
#
|
|
49
|
+
# @param results [Array<DocsExampleVerifier::VerificationResult>] Results
|
|
50
|
+
# @param output_path [String] Output file path
|
|
51
|
+
def generate_badge_json(results, output_path)
|
|
52
|
+
summary = @verifier.summary(results)
|
|
53
|
+
pass_rate = summary[:pass_rate]
|
|
54
|
+
|
|
55
|
+
badge = {
|
|
56
|
+
schemaVersion: 1,
|
|
57
|
+
label: "docs examples",
|
|
58
|
+
message: "#{pass_rate}%",
|
|
59
|
+
color: color_for_rate(pass_rate),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
File.write(output_path, JSON.pretty_generate(badge))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Generate SVG badge
|
|
66
|
+
#
|
|
67
|
+
# @param results [Array<DocsExampleVerifier::VerificationResult>] Results
|
|
68
|
+
# @param output_path [String] Output file path
|
|
69
|
+
def generate_badge_svg(results, output_path)
|
|
70
|
+
summary = @verifier.summary(results)
|
|
71
|
+
pass_rate = summary[:pass_rate]
|
|
72
|
+
color = svg_color_for_rate(pass_rate)
|
|
73
|
+
|
|
74
|
+
svg = <<~SVG
|
|
75
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="140" height="20">
|
|
76
|
+
<linearGradient id="b" x2="0" y2="100%">
|
|
77
|
+
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
|
78
|
+
<stop offset="1" stop-opacity=".1"/>
|
|
79
|
+
</linearGradient>
|
|
80
|
+
<mask id="a">
|
|
81
|
+
<rect width="140" height="20" rx="3" fill="#fff"/>
|
|
82
|
+
</mask>
|
|
83
|
+
<g mask="url(#a)">
|
|
84
|
+
<path fill="#555" d="M0 0h85v20H0z"/>
|
|
85
|
+
<path fill="#{color}" d="M85 0h55v20H85z"/>
|
|
86
|
+
<path fill="url(#b)" d="M0 0h140v20H0z"/>
|
|
87
|
+
</g>
|
|
88
|
+
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
|
|
89
|
+
<text x="42.5" y="15" fill="#010101" fill-opacity=".3">docs examples</text>
|
|
90
|
+
<text x="42.5" y="14">docs examples</text>
|
|
91
|
+
<text x="112" y="15" fill="#010101" fill-opacity=".3">#{pass_rate}%</text>
|
|
92
|
+
<text x="112" y="14">#{pass_rate}%</text>
|
|
93
|
+
</g>
|
|
94
|
+
</svg>
|
|
95
|
+
SVG
|
|
96
|
+
|
|
97
|
+
File.write(output_path, svg)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Generate JSON report
|
|
101
|
+
#
|
|
102
|
+
# @param results [Array<DocsExampleVerifier::VerificationResult>] Results
|
|
103
|
+
# @param output_path [String] Output file path
|
|
104
|
+
def generate_report_json(results, output_path)
|
|
105
|
+
summary = @verifier.summary(results)
|
|
106
|
+
|
|
107
|
+
report = {
|
|
108
|
+
generated_at: Time.now.iso8601,
|
|
109
|
+
summary: summary,
|
|
110
|
+
files: group_results_by_file(results),
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
File.write(output_path, JSON.pretty_generate(report))
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Generate Markdown report
|
|
117
|
+
#
|
|
118
|
+
# @param results [Array<DocsExampleVerifier::VerificationResult>] Results
|
|
119
|
+
# @param output_path [String] Output file path
|
|
120
|
+
def generate_report_markdown(results, output_path)
|
|
121
|
+
summary = @verifier.summary(results)
|
|
122
|
+
grouped = group_results_by_file(results)
|
|
123
|
+
|
|
124
|
+
markdown = <<~MD
|
|
125
|
+
# Documentation Examples Verification Report
|
|
126
|
+
|
|
127
|
+
Generated: #{Time.now.strftime("%Y-%m-%d %H:%M:%S")}
|
|
128
|
+
|
|
129
|
+
## Summary
|
|
130
|
+
|
|
131
|
+
| Metric | Value |
|
|
132
|
+
|--------|-------|
|
|
133
|
+
| Total Examples | #{summary[:total]} |
|
|
134
|
+
| Passed | #{summary[:passed]} |
|
|
135
|
+
| Failed | #{summary[:failed]} |
|
|
136
|
+
| Skipped | #{summary[:skipped]} |
|
|
137
|
+
| **Pass Rate** | **#{summary[:pass_rate]}%** |
|
|
138
|
+
|
|
139
|
+
## Results by File
|
|
140
|
+
|
|
141
|
+
MD
|
|
142
|
+
|
|
143
|
+
grouped.each do |file_path, file_results|
|
|
144
|
+
file_summary = @verifier.summary(file_results)
|
|
145
|
+
status_emoji = file_summary[:failed].zero? ? "✅" : "❌"
|
|
146
|
+
|
|
147
|
+
markdown += "### #{status_emoji} #{file_path}\n\n"
|
|
148
|
+
markdown += "Pass rate: #{file_summary[:pass_rate]}% (#{file_summary[:passed]}/#{file_summary[:total]})\n\n"
|
|
149
|
+
|
|
150
|
+
failed_results = file_results.select(&:fail?)
|
|
151
|
+
if failed_results.any?
|
|
152
|
+
markdown += "**Failed examples:**\n\n"
|
|
153
|
+
failed_results.each do |result|
|
|
154
|
+
markdown += "- Line #{result.line_number}:\n"
|
|
155
|
+
result.errors.each do |error|
|
|
156
|
+
markdown += " - #{error}\n"
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
markdown += "\n"
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
File.write(output_path, markdown)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
private
|
|
167
|
+
|
|
168
|
+
def color_for_rate(rate)
|
|
169
|
+
case rate
|
|
170
|
+
when 95..100 then COLORS[:excellent]
|
|
171
|
+
when 80...95 then COLORS[:good]
|
|
172
|
+
when 60...80 then COLORS[:fair]
|
|
173
|
+
when 40...60 then COLORS[:poor]
|
|
174
|
+
else COLORS[:critical]
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def svg_color_for_rate(rate)
|
|
179
|
+
case rate
|
|
180
|
+
when 95..100 then "#4c1" # bright green
|
|
181
|
+
when 80...95 then "#97ca00" # green
|
|
182
|
+
when 60...80 then "#dfb317" # yellow
|
|
183
|
+
when 40...60 then "#fe7d37" # orange
|
|
184
|
+
else "#e05d44" # red
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def group_results_by_file(results)
|
|
189
|
+
results.group_by(&:file_path)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|