Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move most of spec runner's state into Spec::CLI #14170

Merged
merged 1 commit into from
Jan 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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