Repository navigation
Expand file tree
/
Copy pathRakefile
More file actions
476 lines (411 loc) · 15.9 KB
/
Copy pathRakefile
File metadata and controls
476 lines (411 loc) · 15.9 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
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
# frozen_string_literal: true
require "bundler/gem_helper"
Bundler::GemHelper.install_tasks
require "rake/testtask"
require_relative "lib/graphql/rake_task/validate"
require 'rake/extensiontask'
TEST_LIBS = ["spec", "lib", "graphql-c_parser/lib"]
def graphql_test_files
exclude_integrations = []
['mongoid', 'rails'].each do |integration|
begin
require integration
rescue LoadError
exclude_integrations << integration
end
end
exclude_docs = begin
require "rdoc"
require "rdoc/generator/aliki"
false
rescue LoadError
true
end
FileList.new("spec/**/*_spec.rb") do |fl|
fl.exclude(*exclude_integrations.map { |int| "spec/integration/#{int}/**/*" })
fl.exclude("spec/docs/**/*") if exclude_docs
end
end
# After 2.7, there were not warnings for uninitialized ivars anymore
TEST_WARNINGS = RUBY_VERSION >= "3"
Rake::TestTask.new("test:serial") do |t|
t.description = "Run the test suite in a single process"
t.libs.concat(TEST_LIBS)
t.test_files = graphql_test_files
t.warning = TEST_WARNINGS
end
desc "Run the test suite (in parallel, unless TEST=..., SERIAL=1, or CI is set)"
task :test do
if ENV["TEST"] || ENV["SERIAL"] || ENV["CI"]
Rake::Task["test:serial"].invoke
else
Rake::Task["test:parallel"].invoke
end
end
namespace :test do
desc "Run the test suite with parallel_tests (PARALLEL_TEST_PROCESSORS=n to pick the number of workers)"
task :parallel do
require "parallel_tests"
# Don't merge coverage from a previous run into this one
FileUtils.rm_f("coverage/.resultset.json")
ENV["PARALLEL_TESTS_EXECUTABLE"] = ["ruby", ("-w" if TEST_WARNINGS), *TEST_LIBS.map { |l| "-I#{l}" }].compact.join(" ")
# spec_helper.rb records runtimes in tmp/parallel_runtime_test.log. When it covers enough files,
# parallel_tests uses it to group files by runtime; otherwise it groups them by file size.
ParallelTests::CLI.new.run(["--type", "test", "--serialize-stdout", "--combine-stderr", *graphql_test_files])
end
end
# gemfiles/docs.gemfile only installs what the documentation build needs
unless Bundler.default_gemfile.basename.to_s == "docs.gemfile"
require 'rubocop/rake_task'
RuboCop::RakeTask.new
end
default_tasks = [:test, :rubocop]
if ENV["SYSTEM_TESTS"]
task(default: ["test:system"] + default_tasks)
else
task(default: default_tasks)
end
def assert_dependency_version(dep_name, required_version, check_script)
version = `#{check_script}`
if !version.include?(required_version)
raise <<-ERR
build_parser requires #{dep_name} version "#{required_version}", but found:
$ #{check_script}
> #{version}
To fix this issue:
- Update #{dep_name} to the required version
- Update the assertion in `Rakefile` to match the current version
ERR
end
end
namespace :bench do
def prepare_benchmark
$LOAD_PATH << "./lib" << "./spec/support"
require_relative("./benchmark/run.rb")
end
# Tracked benchmarks measure GraphQL::Execution::Next, so load test schemas in that mode
def prepare_tracked_benchmark
ENV["GRAPHQL_FUTURE"] ||= "1"
prepare_benchmark
require_relative("./benchmark/track.rb")
end
desc "Benchmark parsing"
task :parse do
prepare_benchmark
GraphQLBenchmark.run("parse")
end
desc "Benchmark lexical analysis"
task :scan do
prepare_benchmark
GraphQLBenchmark.run("scan")
end
desc "Benchmark the introspection query"
task :query do
prepare_benchmark
GraphQLBenchmark.run("query")
end
desc "Run the benchmarks tracked over time, measuring iterations per second"
task :track_runtime do
prepare_tracked_benchmark
GraphQLBenchmark::Track.runtime
end
desc "Run the benchmarks tracked over time, measuring memory allocations"
task :track_memory do
prepare_tracked_benchmark
GraphQLBenchmark::Track.memory
end
desc "Run the benchmark tracked over time, measuring memory retained by a large schema"
task :track_schema_memory do
prepare_tracked_benchmark
GraphQLBenchmark::Track.schema_memory
end
desc "Benchmark validation of several queries"
task :validate do
prepare_benchmark
GraphQLBenchmark.run("validate")
end
desc "Profile a validation"
task :validate_memory do
prepare_benchmark
GraphQLBenchmark.validate_memory
end
desc "Generate a profile of the introspection query"
task :profile do
prepare_benchmark
GraphQLBenchmark.profile
end
desc "Run benchmarks on a very large result"
task :profile_large_result do
prepare_benchmark
GraphQLBenchmark.profile_large_result
end
desc "Run benchmarks on a small result"
task :profile_small_result do
prepare_benchmark
GraphQLBenchmark.profile_small_result
end
desc "Run introspection on a small schema"
task :profile_small_introspection do
prepare_benchmark
GraphQLBenchmark.profile_small_introspection
end
desc "Dump schema to SDL"
task :profile_to_definition do
prepare_benchmark
GraphQLBenchmark.profile_to_definition
end
desc "Load schema from SDL"
task :profile_from_definition do
prepare_benchmark
GraphQLBenchmark.profile_from_definition
end
desc "Compare GraphQL-Batch and GraphQL-Dataloader"
task :profile_batch_loaders do
prepare_benchmark
GraphQLBenchmark.profile_batch_loaders
end
desc "Run benchmarks on schema creation"
task :profile_boot do
prepare_benchmark
GraphQLBenchmark.profile_boot
end
desc "Check the memory footprint of a large schema"
task :profile_schema_memory_footprint do
prepare_benchmark
GraphQLBenchmark.profile_schema_memory_footprint
end
desc "Check the depth of the stacktrace during execution"
task :profile_stack_depth do
prepare_benchmark
GraphQLBenchmark.profile_stack_depth
end
desc "Run a very big introspection query"
task :profile_large_introspection do
prepare_benchmark
GraphQLBenchmark.profile_large_introspection
end
task :profile_small_query_on_large_schema do
prepare_benchmark
GraphQLBenchmark.profile_small_query_on_large_schema
end
desc "Run analysis on a big query"
task :profile_large_analysis do
prepare_benchmark
GraphQLBenchmark.profile_large_analysis
end
desc "Run analysis on parsing"
task :profile_parse do
prepare_benchmark
GraphQLBenchmark.profile_parse
end
end
namespace :test do
desc "Run system tests for ActionCable subscriptions"
task :system do
success = Dir.chdir("spec/dummy") do
system("bundle install")
system("bundle exec bin/rails test:system")
end
success || abort
end
task js: "js:test"
end
namespace :js do
client_dir = "./javascript_client"
desc "Run the tests for javascript_client"
task :test do
success = Dir.chdir(client_dir) do
system("yarn run test")
end
success || abort
end
desc "Install JS dependencies"
task :install do
Dir.chdir(client_dir) do
system("yarn install")
end
end
desc "Compile TypeScript to JavaScript"
task :build do
Dir.chdir(client_dir) do
system("yarn tsc")
end
end
task all: [:install, :build, :test]
end
task :build_c_lexer do
assert_dependency_version("Ragel", "7.0.4", "ragel -v")
`ragel -F1 graphql-c_parser/ext/graphql_c_parser_ext/lexer.rl`
end
Rake::ExtensionTask.new("graphql_c_parser_ext") do |t|
t.ext_dir = 'graphql-c_parser/ext/graphql_c_parser_ext'
t.lib_dir = "graphql-c_parser/lib/graphql"
end
task :build_yacc_parser do
assert_dependency_version("Bison", "3.8", "yacc --version")
`yacc graphql-c_parser/ext/graphql_c_parser_ext/parser.y -o graphql-c_parser/ext/graphql_c_parser_ext/parser.c -Wyacc`
end
task :move_binary do
# For some reason my local env doesn't respect the `lib_dir` configured above
`mv graphql-c_parser/lib/*.bundle graphql-c_parser/lib/graphql`
end
namespace :docs do
desc "Build the RDoc/Aliki documentation site"
task build: "docs:rdoc:build"
desc "Build and run documentation quality checks"
task check: "docs:rdoc:build" do
ruby "tool/docs/check.rb"
ruby "tool/docs/type_signatures.rb", "--check"
ruby "tool/docs/compatibility.rb", "--root", "tmp/rdoc-site", "--rdoc", "tmp/rdoc-site/js/search_data.js", "--strict", "--json", "tmp/rdoc-api-compatibility.json"
ruby "tool/docs/link_checker.rb", "--root", "tmp/rdoc-site", "--strict", "--json", "tmp/rdoc-link-report.json"
ruby "tool/docs/rdoc_ref_checker.rb", "--root", "tmp/rdoc-site", "--json", "tmp/rdoc-ref-report.json"
sh "node tool/docs/assets/graphql_highlighter_test.js"
end
desc "Build the documentation twice and compare generated files"
# Reuses the `docs:rdoc:build` output as the first build, so running
# `rake docs:check docs:build_twice` builds the site only twice in total.
task build_twice: "docs:rdoc:build" do
require_relative "tool/docs/build"
require "digest"
require "fileutils"
require "pathname"
first = "tmp/rdoc-site"
second = GraphQLDocs::Build.new.build_site(output: "tmp/rdoc-site-second")
digest = lambda do |root|
Dir[File.join(root, "**", "*")].select { |path| File.file?(path) }.sort.to_h do |path|
[Pathname.new(path).relative_path_from(Pathname.new(root)).to_s, Digest::SHA256.file(path).hexdigest]
end
end
raise "RDoc output is not reproducible" unless digest.call(first) == digest.call(second)
puts "RDoc output is reproducible"
ensure
FileUtils.rm_rf("tmp/rdoc-site-second")
end
namespace :rdoc do
desc "Build the shadow RDoc/Aliki documentation site"
task :build do
require_relative "tool/docs/build"
GraphQLDocs::Build.new.build_site
end
desc "Build versioned RDoc/Aliki API documentation"
task :build_version, [:version] do |_task, args|
require_relative "tool/docs/build"
version = args[:version] || ENV["GRAPHQL_VERSION"] || raise(ArgumentError, "A version is required")
GraphQLDocs::Build.new.build_version(version)
end
desc "Build the shadow RDoc site and serve it locally"
task :serve => :build do
require "webrick"
server = WEBrick::HTTPServer.new(
Port: Integer(ENV.fetch("PORT", "8808")),
DocumentRoot: File.expand_path("tmp/rdoc-site"),
)
trap("INT") { server.shutdown }
puts "Serving RDoc documentation at http://127.0.0.1:#{server.config[:Port]}"
server.start
end
end
end
desc "Build the C Extension"
task build_ext: [:build_c_lexer, :build_yacc_parser, "compile:graphql_c_parser_ext", :move_binary]
namespace :sorbet do
namespace :coverage do
snapshots_dir = "tmp/sorbet_coverage/snapshots"
report_file = "tmp/sorbet_coverage/report.html"
# Run on every push to master (see .github/workflows/benchmarks.yaml) and charted alongside the benchmarks.
# Each name is the key for that metric's history, so renaming one starts a new chart.
desc "Print Sorbet coverage of the current checkout as JSON for github-action-benchmark's customBiggerIsBetter tool"
task :track do
require "spoom"
require "json"
snapshot = Spoom::Coverage.snapshot(Spoom::Context.new(Dir.pwd))
percent = ->(part, total) { total.zero? ? 0.0 : (100.0 * part / total).round(2) }
# Leave out RBIs (gems' and generated ones), so these measure graphql-ruby's own code
sigils = snapshot.sigils_excluding_rbis
total_files = sigils.values.sum
# If Sorbet didn't write metrics, the snapshot is empty; fail instead of recording zeros
abort("Sorbet didn't report any files; check `bundle exec srb tc`") if total_files.zero?
typed_files = sigils.values_at("true", "strict", "strong").compact.sum
strict_files = sigils.values_at("strict", "strong").compact.sum
methods_with_sig = snapshot.methods_with_sig_excluding_rbis
total_methods = methods_with_sig + snapshot.methods_without_sig_excluding_rbis
metrics = {
"sorbet - files at typed: true or stricter" => percent.call(typed_files, total_files),
"sorbet - files at typed: strict or stricter" => percent.call(strict_files, total_files),
"sorbet - methods with signatures" => percent.call(methods_with_sig, total_methods),
"sorbet - typed calls" => percent.call(snapshot.calls_typed, snapshot.calls_typed + snapshot.calls_untyped),
}
puts JSON.pretty_generate(metrics.map { |name, value| { name: name, unit: "%", value: value } })
end
desc "Collect Sorbet coverage snapshots for one commit per day since Sorbet was added (cached in #{snapshots_dir})"
task :timeline do
require "spoom"
require "tmpdir"
context = Spoom::Context.new(Dir.pwd)
intro_commit = context.sorbet_intro_commit || abort("Couldn't find the commit that added sorbet/config")
# Use today's Sorbet binary for every commit, so old commits don't need their own `bundle install`
sorbet_bin = File.join(Gem.loaded_specs.fetch("sorbet-static").full_gem_path, "libexec", "sorbet")
FileUtils.mkdir_p(snapshots_dir)
# The last first-parent commit of each day since Sorbet was introduced, plus HEAD
shas = `git log --first-parent --format='%H %cs' #{intro_commit.sha}^..HEAD`.lines
.map(&:split)
.uniq { |(_sha, date)| date } # log is newest-first, so this keeps each day's last commit
.map(&:first)
# Sorbet was added on a branch, so skip mainline commits from before it was merged
.select { |sha| system("git", "cat-file", "-e", "#{sha}:sorbet/config", err: File::NULL) }
todo = shas.reject { |sha| File.exist?(File.join(snapshots_dir, "#{sha}.json")) }
puts "#{shas.size} commits to chart, #{shas.size - todo.size} already cached"
next if todo.empty?
# Check out old commits in a scratch worktree so the current checkout isn't touched
Dir.mktmpdir("graphql-ruby-sorbet-coverage") do |dir|
worktree = File.join(dir, "graphql-ruby")
system("git", "worktree", "add", "--detach", "--quiet", worktree, todo.first) || abort("git worktree add failed")
begin
worktree_context = Spoom::Context.new(worktree)
todo.each_with_index do |sha, idx|
system("git", "-C", worktree, "checkout", "--quiet", "--detach", sha) || abort("git checkout #{sha} failed")
puts "[#{idx + 1}/#{todo.size}] #{`git -C #{worktree} log -1 --format='%cs %h %s' #{sha}`.strip}"
snapshot = Spoom::Coverage.snapshot(worktree_context, sorbet_bin: sorbet_bin)
File.write(File.join(snapshots_dir, "#{sha}.json"), snapshot.to_json)
end
ensure
system("git", "worktree", "remove", "--force", worktree)
end
end
end
desc "Build an HTML report of Sorbet adoption over time (#{report_file})"
task report: :timeline do
require "spoom"
context = Spoom::Context.new(Dir.pwd)
snapshots = Dir.glob(File.join(snapshots_dir, "*.json"))
.map { |f| Spoom::Coverage::Snapshot.from_json(File.read(f)) }
.select(&:commit_timestamp)
.sort_by(&:commit_timestamp)
file_tree = Spoom::Coverage.file_tree(context)
scores = Spoom::FileTree::CollectScores.new(context)
scores.visit_tree(file_tree)
intro_commit = context.sorbet_intro_commit
report = Spoom::Coverage::Report.new(
project_name: "graphql-ruby",
palette: Spoom::Coverage::D3::ColorPalette.new(
ignore_color: Spoom::Coverage::D3::COLOR_IGNORE,
false_color: Spoom::Coverage::D3::COLOR_FALSE,
true_color: Spoom::Coverage::D3::COLOR_TRUE,
strict_color: Spoom::Coverage::D3::COLOR_STRICT,
strong_color: Spoom::Coverage::D3::COLOR_STRONG,
),
snapshots: snapshots,
file_tree: file_tree,
nodes_strictnesses: scores.strictnesses,
nodes_strictness_scores: scores.scores,
sorbet_intro_commit: intro_commit&.sha,
sorbet_intro_date: intro_commit&.time,
)
FileUtils.mkdir_p(File.dirname(report_file))
File.write(report_file, report.html)
puts "Wrote #{report_file} (#{snapshots.size} snapshots)"
end
end
desc "Generate the Sorbet adoption report (alias for sorbet:coverage:report)"
task coverage: "coverage:report"
end