-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathRakefile
More file actions
336 lines (283 loc) · 12 KB
/
Copy pathRakefile
File metadata and controls
336 lines (283 loc) · 12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
# frozen_string_literal: true
require "bundler/gem_tasks"
require "fileutils"
require "json"
require "rubocop/rake_task"
require "rspec/core/rake_task"
require "rbconfig"
require "tmpdir"
require_relative "tools/yard_coverage"
RSpec::Core::RakeTask.new(:spec)
RuboCop::RakeTask.new(:lint) do |task|
task.options = ["--cache", "false"]
end
namespace :types do
desc "Validate RBS signatures"
task :validate do
ruby = RbConfig.ruby
success = system(ruby, "-S", "bundle", "exec", "rbs", "validate")
abort("RBS validation failed") unless success
end
end
namespace :spec do
desc "Generate audio fixtures from YAML definitions"
task :create_fixtures do
ruby = RbConfig.ruby
success = system(ruby, File.expand_path("tools/fixture_writer.rb", __dir__))
abort("fixture generation failed") unless success
end
desc "Run specs with line, branch, and per-file SimpleCov coverage gates"
task :coverage do
ruby = RbConfig.ruby
env = {
"COVERAGE" => "1",
"SIMPLECOV_BRANCH" => ENV.fetch("SIMPLECOV_BRANCH", "1")
}
%w[
COVERAGE_MINIMUM
COVERAGE_BRANCH_MINIMUM
COVERAGE_MINIMUM_PER_FILE
COVERAGE_BRANCH_MINIMUM_PER_FILE
].each do |name|
env[name] = ENV[name] if ENV[name]
end
success = system(env, ruby, "-S", "bundle", "exec", "rspec")
abort("coverage run failed") unless success
end
end
desc "Run the local quality gate (specs, lint, signatures, and docs)"
task check: [:spec, :lint, "types:validate", "docs:check"]
task default: :check
namespace :bench do
def benchmark_scripts
{
wav_io: "benchmarks/wav_io_benchmark.rb",
dsp: "benchmarks/dsp_effects_benchmark.rb",
flac: "benchmarks/flac_benchmark.rb",
stream: "benchmarks/streaming_memory_benchmark.rb"
}.freeze
end
def run_benchmark_script(path, env: {})
ruby = RbConfig.ruby
success = system(env, ruby, File.expand_path(path, __dir__))
abort("benchmark failed: #{path}") unless success
end
def benchmark_report_dir(name)
File.expand_path(ENV.fetch("BENCH_DIR", "tmp/benchmarks/#{name}"), __dir__)
end
def run_benchmark_reports(dir)
FileUtils.mkdir_p(dir)
benchmark_scripts.each do |name, path|
run_benchmark_script(path, env: { "BENCH_JSON" => File.join(dir, "#{name}.json") })
end
end
def compare_benchmark_reports!(baseline_dir, current_dir)
threshold = Float(ENV.fetch("BENCH_THRESHOLD", "1.2"))
benchmark_scripts.each_key do |name|
baseline = JSON.parse(File.read(File.join(baseline_dir, "#{name}.json")))
current = JSON.parse(File.read(File.join(current_dir, "#{name}.json")))
baseline_measurements = baseline.fetch("measurements").to_h { |item| [item.fetch("label"), item] }
current_measurements = current.fetch("measurements").to_h { |item| [item.fetch("label"), item] }
missing = baseline_measurements.keys - current_measurements.keys
added = current_measurements.keys - baseline_measurements.keys
unless missing.empty? && added.empty?
abort("benchmark labels changed for #{name}: removed=#{missing.inspect}, added=#{added.inspect}")
end
current_measurements.each do |label, item|
baseline_item = baseline_measurements.fetch(label)
baseline_median = baseline_item.fetch("median_seconds", baseline_item.fetch("elapsed_seconds"))
elapsed = item.fetch("median_seconds", item.fetch("elapsed_seconds"))
allowed = baseline_median * threshold
abort("benchmark regression: #{name} #{label} #{elapsed}s > #{allowed.round(6)}s") if elapsed > allowed
end
end
end
desc "Run WAV I/O benchmark"
task :wav_io do
run_benchmark_script("benchmarks/wav_io_benchmark.rb")
end
desc "Run DSP effects benchmark"
task :dsp do
run_benchmark_script("benchmarks/dsp_effects_benchmark.rb")
end
desc "Run FLAC encode/decode benchmark"
task :flac do
run_benchmark_script("benchmarks/flac_benchmark.rb")
end
desc "Run streaming memory benchmark"
task :stream do
run_benchmark_script("benchmarks/streaming_memory_benchmark.rb")
end
desc "Run all benchmarks"
task all: %i[wav_io dsp flac stream]
desc "Run all benchmarks and write JSON baseline reports"
task :baseline do
dir = benchmark_report_dir("baseline")
run_benchmark_reports(dir)
puts "benchmark baseline written to #{dir}"
end
desc "Run all benchmarks and compare with BENCH_BASELINE (default: tmp/benchmarks/baseline)"
task :compare do
baseline_dir = File.expand_path(ENV.fetch("BENCH_BASELINE", "tmp/benchmarks/baseline"), __dir__)
current_dir = benchmark_report_dir("current")
abort("missing benchmark baseline directory: #{baseline_dir}") unless Dir.exist?(baseline_dir)
run_benchmark_reports(current_dir)
compare_benchmark_reports!(baseline_dir, current_dir)
puts "benchmark compare ok: #{current_dir} <= #{ENV.fetch('BENCH_THRESHOLD', '1.2')}x baseline"
end
end
namespace :docs do
def example_scripts
Dir.glob(File.join(__dir__, "examples", "*.rb")).map do |path|
path.delete_prefix("#{__dir__}/")
end.freeze
end
def smoke_example_scripts
%w[
examples/format_convert.rb
examples/synth_pad.rb
examples/drum_machine.rb
examples/audio_processing.rb
].freeze
end
def run_ruby_script(path)
ruby = RbConfig.ruby
success = system(ruby, File.expand_path(path, __dir__))
abort("script failed: #{path}") unless success
end
def check_ruby_script(path)
ruby = RbConfig.ruby
success = system(ruby, "-c", File.expand_path(path, __dir__), out: File::NULL)
abort("script syntax failed: #{path}") unless success
end
desc "Generate YARD docs into doc/"
task :yard do
ruby = RbConfig.ruby
success = system(ruby, "-S", "bundle", "exec", "yard", "doc")
abort("yard doc failed") unless success
end
desc "Print YARD documentation coverage stats"
task :stats do
report = Wavify::Tools::YardCoverage.calculate
puts format(
"YARD objects: %<total>d (%<undocumented>d undocumented)\n%<percent>.2f%% documented",
**report
)
end
desc "Enforce minimum YARD documentation percentage (YARD_MINIMUM, default: 85)"
task :check do
minimum = ENV.fetch("YARD_MINIMUM", "85").to_f
report = Wavify::Tools::YardCoverage.calculate
percent = report.fetch(:percent)
puts format("YARD objects: %<total>d (%<undocumented>d undocumented)", **report)
abort("documentation coverage #{percent}% is below minimum #{minimum}%") if percent < minimum
puts format("docs check ok: %.2f%% >= %.2f%%", percent, minimum)
end
desc "Check all examples and run smoke examples (EXAMPLES=all runs every script)"
task :examples do
example_scripts.each { |script| check_ruby_script(script) }
scope = ENV.fetch("EXAMPLES", "smoke")
scripts = case scope
when "smoke" then smoke_example_scripts
when "all" then example_scripts
else abort("EXAMPLES must be smoke or all")
end
scripts.each do |script|
puts "== running #{script}"
run_ruby_script(script)
end
end
desc "Run docs-related checks and generators"
task all: %i[examples yard check]
end
namespace :release do
def load_release_spec!
spec = Gem::Specification.load(File.expand_path("wavify.gemspec", __dir__))
abort("failed to load wavify.gemspec") unless spec
spec
end
def assert_release_check!(condition, message)
return if condition
abort("release check failed: #{message}")
end
desc "Validate gemspec metadata and packaged file list"
task :check_gemspec do
spec = load_release_spec!
assert_release_check!(!spec.name.to_s.strip.empty?, "gemspec.name is empty")
assert_release_check!(!spec.version.to_s.strip.empty?, "gemspec.version is empty")
assert_release_check!(!spec.summary.to_s.strip.empty?, "gemspec.summary is empty")
assert_release_check!(!spec.description.to_s.strip.empty?, "gemspec.description is empty")
assert_release_check!(!spec.homepage.to_s.strip.empty?, "gemspec.homepage is empty")
assert_release_check!(!spec.license.to_s.strip.empty?, "gemspec.license is empty")
assert_release_check!(!spec.required_ruby_version.to_s.strip.empty?, "gemspec.required_ruby_version is empty")
required_metadata_keys = %w[allowed_push_host homepage_uri source_code_uri changelog_uri rubygems_mfa_required]
missing_metadata = required_metadata_keys.select { |key| spec.metadata[key].to_s.strip == "" }
assert_release_check!(missing_metadata.empty?, "missing gemspec metadata keys: #{missing_metadata.join(', ')}")
files = spec.files || []
assert_release_check!(files.include?("lib/wavify.rb"), "gemspec.files does not include lib/wavify.rb")
assert_release_check!(files.include?("README.md"), "gemspec.files does not include README.md")
assert_release_check!(files.any? { |f| ["LICENSE", "LICENSE.txt"].include?(f) }, "gemspec.files does not include license file")
assert_release_check!(files.none? { |f| f.start_with?(".idea/") }, ".idea files should not be packaged")
assert_release_check!(files.none? { |f| f.start_with?("benchmarks/", "docs/", "tools/") }, "development utilities should not be packaged")
puts "release check ok: gemspec metadata and package file list"
puts " name: #{spec.name}"
puts " version: #{spec.version}"
puts " packaged files: #{files.length}"
end
desc "Validate CHANGELOG structure and unreleased section"
task :check_changelog do
changelog_path = File.expand_path("CHANGELOG.md", __dir__)
assert_release_check!(File.file?(changelog_path), "CHANGELOG.md is missing")
changelog = File.read(changelog_path)
assert_release_check!(changelog.include?("## [Unreleased]"), "CHANGELOG.md must include an [Unreleased] section")
assert_release_check!(changelog.include?("Keep a Changelog"), "CHANGELOG.md should mention Keep a Changelog format")
has_standard_subsection = changelog.match?(/^### (Added|Changed|Fixed|Removed|Security)$/)
warn("release check warning: CHANGELOG.md has no standard subsection heading") unless has_standard_subsection
puts "release check ok: CHANGELOG.md structure"
end
desc "Build gem package locally (same artifact as release flow)"
task :build_package do
Rake::Task["build"].reenable
Rake::Task["build"].invoke
end
desc "Install the built gem into an isolated GEM_HOME and smoke-test require and CLI"
task smoke_package: :build_package do
spec = load_release_spec!
package = File.expand_path("pkg/#{spec.file_name}", __dir__)
assert_release_check!(File.file?(package), "built gem is missing: #{package}")
Dir.mktmpdir("wavify-gem-smoke-") do |gem_home|
bindir = File.join(gem_home, "bin")
env = { "GEM_HOME" => gem_home, "GEM_PATH" => gem_home }
ruby = RbConfig.ruby
Bundler.with_unbundled_env do
installed = system(
env,
ruby,
"-S",
"gem",
"install",
"--local",
"--no-document",
"--install-dir",
gem_home,
"--bindir",
bindir,
package
)
assert_release_check!(installed, "failed to install built gem")
require_ok = system(env, ruby, "-e", 'require "wavify"; abort unless Wavify::VERSION')
assert_release_check!(require_ok, 'installed gem failed to require "wavify"')
executable = File.join(bindir, "wavify")
assert_release_check!(File.file?(executable), "installed CLI executable is missing")
assert_release_check!(system(env, ruby, executable, "--help", out: File::NULL), "installed CLI --help failed")
end
gem_root = File.join(gem_home, "gems", spec.full_name)
%w[lib/wavify.rb sig/wavify.rbs README.md CHANGELOG.md LICENSE].each do |relative_path|
assert_release_check!(File.file?(File.join(gem_root, relative_path)), "installed gem is missing #{relative_path}")
end
end
puts "release check ok: isolated gem install, require, and CLI smoke test"
end
desc "Run release readiness checks (changelog, gemspec, build, install, require, CLI)"
task check: %i[check_changelog check_gemspec smoke_package]
end