discourse/bin/turbo_rspec
Alan Guo Xiang Tan 71a6dd9674
DEV: Add per-test SQL/Redis/network capture RSpec formatter (#40632)
Adds `RspecPerformanceFormatter`, an RSpec formatter that captures the
SQL queries, Redis commands, and outbound network calls each example
performs and emits them as NDJSON with one object per test. It gives an
AI model (or a developer) concrete per-test context for spotting N+1s,
redundant queries, or unexpected outbound calls. Off unless the
formatter is selected, so normal runs and production are unaffected.
Works with both `bin/rspec` and `bin/turbo_rspec`.
2026-06-16 09:47:54 +08:00

138 lines
3.7 KiB
Ruby
Executable file
Vendored

#!/usr/bin/env ruby
# frozen_string_literal: true
ENV["RAILS_ENV"] ||= "test"
require "./lib/turbo_tests"
require "optparse"
requires = []
formatters = []
verbose = false
fail_fast = nil
seed = rand(2**16)
profile = false
profile_print_slowest_examples_count = 10
use_runtime_info = nil
enable_system_tests = nil
retry_and_log_flaky_tests = ENV["DISCOURSE_TURBO_RSPEC_RETRY_AND_LOG_FLAKY_TESTS"].to_s == "1"
exclude_patterns = []
OptionParser
.new do |opts|
opts.on("-r", "--require PATH", "Require a file.") { |filename| requires << filename }
opts.on("-f", "--format FORMATTER", "Choose a formatter.") do |name|
formatters << { name: name, outputs: [] }
end
opts.on("-o", "--out FILE", "Write output to a file instead of $stdout") do |filename|
formatters << { name: "progress", outputs: [] } if formatters.empty?
formatters.last[:outputs] << filename
end
opts.on("-v", "--verbose", "More output") { verbose = true }
opts.on(
"-p",
"--profile=[COUNT]",
"Benchmark the runtime of each example and list the slowest examples (default: 10)",
) do |count|
profile = true
profile_print_slowest_examples_count = count.to_i if count
end
opts.on("--fail-fast=[N]") do |n|
n =
begin
Integer(n)
rescue StandardError
nil
end
fail_fast = (n.nil? || n < 1) ? 1 : n
end
opts.on(
"--retry-and-log-flaky-tests",
"Retry failed tests and log if test is deemed to be flaky",
) { retry_and_log_flaky_tests = true }
opts.on("--seed SEED", "The seed for the random order") { |s| seed = s.to_i }
opts.on("--use-runtime-info", "Use runtime info for tests group splitting") do
use_runtime_info = true
end
opts.on("--enable-system-tests", "Run the system tests (defaults false)") do
enable_system_tests = true
end
opts.on(
"--exclude-pattern=pattern",
"Exclude files that matches against the pattern using unix-style pattern matching (comma-separated for multiple patterns)",
) { |pattern| exclude_patterns.concat(pattern.split(",").map(&:strip)) }
end
.parse!(ARGV)
# OptionParser modifies ARGV, leaving the leftover arguments in ARGV
if ARGV.empty?
files = Dir.glob("spec/**/*_spec.rb")
files.reject! { |file| File.fnmatch("*spec/system*", file) } if !enable_system_tests
use_runtime_info = true if use_runtime_info.nil?
else
STDERR.puts "Ignoring system test options since files were specified" if enable_system_tests
files =
ARGV.flat_map do |arg|
if File.directory?(arg)
Dir.glob("#{arg}/**/*_spec.rb")
else
arg
end
end
use_runtime_info = false if use_runtime_info.nil?
end
if exclude_patterns.any?
files.reject! { |file| exclude_patterns.any? { |pattern| File.fnmatch(pattern, file) } }
end
requires.each { |f| require(f) }
formatters << { name: "progress", outputs: [] } if formatters.empty?
formatters.each { |formatter| formatter[:outputs] << "-" if formatter[:outputs].empty? }
if formatters.any? { |formatter| formatter[:name] == "RspecPerformanceFormatter" }
ENV["DISCOURSE_RSPEC_PERFORMANCE_FORMATTER"] = "1"
require "./spec/support/rspec_performance_formatter"
end
puts "::group::Run turbo_rspec" if ENV["GITHUB_ACTIONS"]
if files.size > 5
puts "Running turbo_rspec on #{files.size} files"
else
puts "Running turbo_rspec on: #{files.join(", ")}"
end
puts "::endgroup::" if ENV["GITHUB_ACTIONS"]
success =
TurboTests::Runner.run(
formatters: formatters,
files: files,
verbose: verbose,
fail_fast: fail_fast,
use_runtime_info: use_runtime_info,
seed: seed.to_s,
profile:,
profile_print_slowest_examples_count:,
retry_and_log_flaky_tests:,
)
if success
exit 0
else
exit 1
end