Skip to content

Commit

Permalink
Move most of spec runner's state into Spec::CLI (#14170)
Browse files Browse the repository at this point in the history
  • Loading branch information
HertzDevil committed Jan 12, 2024
1 parent 7867cd8 commit 6bfcfe8
Show file tree
Hide file tree
Showing 10 changed files with 353 additions and 327 deletions.
24 changes: 13 additions & 11 deletions spec/compiler/interpreter/spec_helper.cr
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,20 @@ end
# In a nutshell, `interpret_in_separate_process` below calls this same process with an extra option that causes
# the interpretation of the code from stdin, reading the output from stdout. That string is used as the result of
# the program being tested.
def Spec.option_parser
option_parser = previous_def
option_parser.on("", "--interpret-code PRELUDE", "Execute interpreted code") do |prelude|
code = STDIN.gets_to_end

repl = Crystal::Repl.new
repl.prelude = prelude

print repl.run_code(code)
exit
class Spec::CLI
def option_parser
option_parser = previous_def
option_parser.on("", "--interpret-code PRELUDE", "Execute interpreted code") do |prelude|
code = STDIN.gets_to_end

repl = Crystal::Repl.new
repl.prelude = prelude

print repl.run_code(code)
exit
end
option_parser
end
option_parser
end

def interpret_in_separate_process(code, prelude, file = __FILE__, line = __LINE__)
Expand Down
2 changes: 1 addition & 1 deletion spec/std/log/env_config_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ end
describe "Log.setup_from_env" do
after_all do
# Setup logging in specs (again) since these specs perform Log.setup
Spec.log_setup
Spec.cli.log_setup
end

describe "backend" do
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/crystal/command/spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class Crystal::Command
puts opts
puts

runtime_options = Spec.option_parser
runtime_options = Spec::CLI.new.option_parser
runtime_options.banner = "Runtime options (passed to spec runner):"
puts runtime_options
exit
Expand Down
74 changes: 40 additions & 34 deletions src/spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -91,45 +91,51 @@ require "./spec/cli"
# value can be used to rerun the specs in that same order by passing the seed
# value to `--order`.
module Spec
end
# :nodoc:
class CLI
# :nodoc:
#
# Implement formatter configuration.
def configure_formatter(formatter, output_path = nil)
case formatter
when "junit"
junit_formatter = Spec::JUnitFormatter.file(Path.new(output_path.not_nil!))
add_formatter(junit_formatter)
when "verbose"
override_default_formatter(Spec::VerboseFormatter.new)
when "tap"
override_default_formatter(Spec::TAPFormatter.new)
end
end

Colorize.on_tty_only!
def main(args)
Colorize.on_tty_only!

# :nodoc:
#
# Implement formatter configuration.
def Spec.configure_formatter(formatter, output_path = nil)
case formatter
when "junit"
junit_formatter = Spec::JUnitFormatter.file(Path.new(output_path.not_nil!))
Spec.add_formatter(junit_formatter)
when "verbose"
Spec.override_default_formatter(Spec::VerboseFormatter.new)
when "tap"
Spec.override_default_formatter(Spec::TAPFormatter.new)
end
end
begin
option_parser.parse(args)
rescue e : OptionParser::InvalidOption
abort("Error: #{e.message}")
end

begin
Spec.option_parser.parse(ARGV)
rescue e : OptionParser::InvalidOption
abort("Error: #{e.message}")
end
unless args.empty?
STDERR.puts "Error: unknown argument '#{args.first}'"
exit 1
end

unless ARGV.empty?
STDERR.puts "Error: unknown argument '#{ARGV.first}'"
exit 1
end
if ENV["SPEC_VERBOSE"]? == "1"
override_default_formatter(Spec::VerboseFormatter.new)
end

if ENV["SPEC_VERBOSE"]? == "1"
Spec.override_default_formatter(Spec::VerboseFormatter.new)
end
add_split_filter ENV["SPEC_SPLIT"]?

Spec.add_split_filter ENV["SPEC_SPLIT"]?
{% unless flag?(:wasm32) %}
# TODO(wasm): Enable this once `Process.on_interrupt` is implemented
Process.on_interrupt { abort! }
{% end %}

{% unless flag?(:wasm32) %}
# TODO(wasm): Enable this once `Process.on_interrupt` is implemented
Process.on_interrupt { Spec.abort! }
{% end %}
run
end
end
end

Spec.run
Spec.cli.main(ARGV)
223 changes: 107 additions & 116 deletions src/spec/cli.cr
Original file line number Diff line number Diff line change
@@ -1,139 +1,130 @@
require "option_parser"
require "colorize"

# This file is included in the compiler to add usage instructions for the
# spec runner on `crystal spec --help`.

module Spec
# :nodoc:
class_property pattern : Regex?

# :nodoc:
class_property line : Int32?

# :nodoc:
class_property slowest : Int32?

# :nodoc:
class_property? fail_fast = false

# :nodoc:
class_property? focus = false

# :nodoc:
class_property? dry_run = false

# :nodoc:
class_property? list_tags = false

# :nodoc:
def self.add_location(file, line)
locations = @@locations ||= {} of String => Array(Int32)
locations.put_if_absent(File.expand_path(file)) { [] of Int32 } << line
end
#
# Configuration for a spec runner. More global state is defined in `./dsl.cr`.
class CLI
getter pattern : Regex?
getter line : Int32?
getter slowest : Int32?
getter? fail_fast = false
property? focus = false
getter? dry_run = false
getter? list_tags = false

# :nodoc:
def self.add_tag(tag)
if anti_tag = tag.lchop?('~')
(@@anti_tags ||= Set(String).new) << anti_tag
else
(@@tags ||= Set(String).new) << tag
def add_location(file, line)
locations = @locations ||= {} of String => Array(Int32)
locations.put_if_absent(File.expand_path(file)) { [] of Int32 } << line
end
end

# :nodoc:
class_getter randomizer_seed : UInt64?
class_getter randomizer : Random::PCG32?

# :nodoc:
def self.order=(mode)
seed =
case mode
when "default"
nil
when "random"
Random::Secure.rand(1..99999).to_u64 # 5 digits or less for simplicity
when UInt64
mode
def add_tag(tag)
if anti_tag = tag.lchop?('~')
(@anti_tags ||= Set(String).new) << anti_tag
else
raise ArgumentError.new("Order must be either 'default', 'random', or a numeric seed value")
(@tags ||= Set(String).new) << tag
end
end

@@randomizer_seed = seed
@@randomizer = seed ? Random::PCG32.new(seed) : nil
end
getter randomizer_seed : UInt64?
getter randomizer : Random::PCG32?

# :nodoc:
class_property option_parser : OptionParser = begin
OptionParser.new do |opts|
opts.banner = "crystal spec runner"
opts.on("-e", "--example STRING", "run examples whose full nested names include STRING") do |pattern|
Spec.pattern = Regex.new(Regex.escape(pattern))
end
opts.on("-l", "--line LINE", "run examples whose line matches LINE") do |line|
Spec.line = line.to_i
end
opts.on("-p", "--profile", "Print the 10 slowest specs") do
Spec.slowest = 10
end
opts.on("--fail-fast", "abort the run on first failure") do
Spec.fail_fast = true
end
opts.on("--location file:line", "run example at line 'line' in file 'file', multiple allowed") do |location|
if location =~ /\A(.+?)\:(\d+)\Z/
Spec.add_location $1, $2.to_i
def order=(mode)
seed =
case mode
when "default"
nil
when "random"
Random::Secure.rand(1..99999).to_u64 # 5 digits or less for simplicity
when UInt64
mode
else
STDERR.puts "location #{location} must be file:line"
exit 1
raise ArgumentError.new("Order must be either 'default', 'random', or a numeric seed value")
end
end
opts.on("--tag TAG", "run examples with the specified TAG, or exclude examples by adding ~ before the TAG.") do |tag|
Spec.add_tag tag
end
opts.on("--list-tags", "lists all the tags used.") do
Spec.list_tags = true
end
opts.on("--order MODE", "run examples in random order by passing MODE as 'random' or to a specific seed by passing MODE as the seed value") do |mode|
if mode.in?("default", "random")
Spec.order = mode
elsif seed = mode.to_u64?
Spec.order = seed
else
abort("order must be either 'default', 'random', or a numeric seed value")

@randomizer_seed = seed
@randomizer = seed ? Random::PCG32.new(seed) : nil
end

def option_parser : OptionParser
@option_parser ||= OptionParser.new do |opts|
opts.banner = "crystal spec runner"
opts.on("-e", "--example STRING", "run examples whose full nested names include STRING") do |pattern|
@pattern = Regex.new(Regex.escape(pattern))
end
opts.on("-l", "--line LINE", "run examples whose line matches LINE") do |line|
@line = line.to_i
end
opts.on("-p", "--profile", "Print the 10 slowest specs") do
@slowest = 10
end
opts.on("--fail-fast", "abort the run on first failure") do
@fail_fast = true
end
opts.on("--location file:line", "run example at line 'line' in file 'file', multiple allowed") do |location|
if location =~ /\A(.+?)\:(\d+)\Z/
add_location $1, $2.to_i
else
STDERR.puts "location #{location} must be file:line"
exit 1
end
end
opts.on("--tag TAG", "run examples with the specified TAG, or exclude examples by adding ~ before the TAG.") do |tag|
add_tag tag
end
opts.on("--list-tags", "lists all the tags used.") do
@list_tags = true
end
opts.on("--order MODE", "run examples in random order by passing MODE as 'random' or to a specific seed by passing MODE as the seed value") do |mode|
if mode.in?("default", "random")
self.order = mode
elsif seed = mode.to_u64?
self.order = seed
else
abort("order must be either 'default', 'random', or a numeric seed value")
end
end
opts.on("--junit_output OUTPUT_PATH", "generate JUnit XML output within the given OUTPUT_PATH") do |output_path|
configure_formatter("junit", output_path)
end
opts.on("-h", "--help", "show this help") do |pattern|
puts opts
exit
end
opts.on("-v", "--verbose", "verbose output") do
configure_formatter("verbose")
end
opts.on("--tap", "Generate TAP output (Test Anything Protocol)") do
configure_formatter("tap")
end
opts.on("--color", "Enabled ANSI colored output") do
Colorize.enabled = true
end
opts.on("--no-color", "Disable ANSI colored output") do
Colorize.enabled = false
end
opts.on("--dry-run", "Pass all tests without execution") do
@dry_run = true
end
opts.unknown_args do |args|
end
end
opts.on("--junit_output OUTPUT_PATH", "generate JUnit XML output within the given OUTPUT_PATH") do |output_path|
configure_formatter("junit", output_path)
end
opts.on("-h", "--help", "show this help") do |pattern|
puts opts
exit
end
opts.on("-v", "--verbose", "verbose output") do
configure_formatter("verbose")
end
opts.on("--tap", "Generate TAP output (Test Anything Protocol)") do
configure_formatter("tap")
end
opts.on("--color", "Enabled ANSI colored output") do
Colorize.enabled = true
end
opts.on("--no-color", "Disable ANSI colored output") do
Colorize.enabled = false
end
opts.on("--dry-run", "Pass all tests without execution") do
Spec.dry_run = true
end
opts.unknown_args do |args|
end
end

# Blank implementation to reduce the interface of spec's option parser for
# inclusion in the compiler. This avoids depending on more of `Spec`
# module.
# The real implementation in `../spec.cr` overrides this for actual use.
def configure_formatter(formatter, output_path = nil)
end
end

# :nodoc:
#
# Blank implementation to reduce the interface of spec's option parser for
# inclusion in the compiler. This avoids depending on more of `Spec`
# module.
# The real implementation in `../spec.cr` overrides this for actual use.
def self.configure_formatter(formatter, output_path = nil)
@[Deprecated("This is an internal API.")]
def self.randomizer : Random::PCG32?
@@cli.randomizer
end
end

0 comments on commit 6bfcfe8

Please sign in to comment.