smartest 0.1.0.alpha1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +16 -0
- data/DEVELOPMENT.md +774 -0
- data/Gemfile +5 -0
- data/LICENSE +21 -0
- data/README.md +518 -0
- data/Rakefile +12 -0
- data/SMARTEST_DESIGN.md +1137 -0
- data/exe/smartest +63 -0
- data/lib/smartest/autorun.rb +8 -0
- data/lib/smartest/dsl.rb +22 -0
- data/lib/smartest/errors.rb +52 -0
- data/lib/smartest/execution_context.rb +8 -0
- data/lib/smartest/expectation_target.rb +21 -0
- data/lib/smartest/expectations.rb +9 -0
- data/lib/smartest/fixture.rb +78 -0
- data/lib/smartest/fixture_class_registry.rb +27 -0
- data/lib/smartest/fixture_definition.rb +31 -0
- data/lib/smartest/fixture_set.rb +119 -0
- data/lib/smartest/init_generator.rb +70 -0
- data/lib/smartest/matchers.rb +109 -0
- data/lib/smartest/parameter_extractor.rb +51 -0
- data/lib/smartest/reporter.rb +91 -0
- data/lib/smartest/runner.rb +80 -0
- data/lib/smartest/suite.rb +12 -0
- data/lib/smartest/test_case.rb +18 -0
- data/lib/smartest/test_registry.rb +25 -0
- data/lib/smartest/test_result.rb +43 -0
- data/lib/smartest/version.rb +5 -0
- data/lib/smartest.rb +59 -0
- data/smartest/smartest_test.rb +634 -0
- data/smartest.gemspec +48 -0
- metadata +95 -0
data/exe/smartest
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
|
5
|
+
|
|
6
|
+
require "smartest"
|
|
7
|
+
|
|
8
|
+
usage = <<~USAGE
|
|
9
|
+
Usage:
|
|
10
|
+
smartest [paths...]
|
|
11
|
+
smartest --init
|
|
12
|
+
smartest --version
|
|
13
|
+
smartest --help
|
|
14
|
+
|
|
15
|
+
When no paths are given, Smartest loads smartest/**/*_test.rb.
|
|
16
|
+
USAGE
|
|
17
|
+
|
|
18
|
+
command = :run
|
|
19
|
+
|
|
20
|
+
begin
|
|
21
|
+
if ARGV.include?("--help") || ARGV.include?("-h")
|
|
22
|
+
puts usage
|
|
23
|
+
exit 0
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
if ARGV.include?("--version") || ARGV.include?("-v")
|
|
27
|
+
puts Smartest::VERSION
|
|
28
|
+
exit 0
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
if ARGV.include?("--init")
|
|
32
|
+
command = :init
|
|
33
|
+
exit Smartest::InitGenerator.new.run
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
Smartest.disable_autorun!
|
|
37
|
+
Kernel.prepend Smartest::DSL
|
|
38
|
+
test_load_path = File.expand_path("smartest", Dir.pwd)
|
|
39
|
+
$LOAD_PATH.unshift(test_load_path) if Dir.exist?(test_load_path) && !$LOAD_PATH.include?(test_load_path)
|
|
40
|
+
|
|
41
|
+
files =
|
|
42
|
+
if ARGV.empty?
|
|
43
|
+
Dir["smartest/**/*_test.rb"]
|
|
44
|
+
else
|
|
45
|
+
ARGV.flat_map do |pattern|
|
|
46
|
+
matches = Dir[pattern]
|
|
47
|
+
matches.empty? ? [pattern] : matches
|
|
48
|
+
end.uniq
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
files.each do |file|
|
|
52
|
+
load File.expand_path(file)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
exit Smartest::Runner.new.run
|
|
56
|
+
rescue Exception => error
|
|
57
|
+
raise if Smartest.fatal_exception?(error)
|
|
58
|
+
|
|
59
|
+
warn(command == :init ? "Error initializing Smartest:" : "Error loading tests:")
|
|
60
|
+
warn "#{error.class}: #{error.message}"
|
|
61
|
+
warn error.backtrace&.first
|
|
62
|
+
exit 1
|
|
63
|
+
end
|
data/lib/smartest/dsl.rb
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smartest
|
|
4
|
+
module DSL
|
|
5
|
+
def test(name, **metadata, &block)
|
|
6
|
+
Smartest.suite.tests.add(
|
|
7
|
+
TestCase.new(
|
|
8
|
+
name: name,
|
|
9
|
+
metadata: metadata,
|
|
10
|
+
block: block,
|
|
11
|
+
location: caller_locations(1, 1).first
|
|
12
|
+
)
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def use_fixture(klass)
|
|
17
|
+
Smartest.suite.fixture_classes.add(klass)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private :test, :use_fixture
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smartest
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class FixtureNotFoundError < Error
|
|
7
|
+
def initialize(name)
|
|
8
|
+
super("fixture not found: #{name}")
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class DuplicateFixtureError < Error
|
|
13
|
+
def initialize(name, fixture_classes)
|
|
14
|
+
class_names = fixture_classes.map { |klass| klass.name || klass.inspect }
|
|
15
|
+
|
|
16
|
+
super(<<~MESSAGE.chomp)
|
|
17
|
+
duplicate fixture: #{name}
|
|
18
|
+
defined in:
|
|
19
|
+
#{class_names.map { |class_name| " #{class_name}" }.join("\n")}
|
|
20
|
+
MESSAGE
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class CircularFixtureDependencyError < Error
|
|
25
|
+
def initialize(path)
|
|
26
|
+
super("circular fixture dependency: #{path.join(' -> ')}")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class InvalidFixtureScopeError < Error
|
|
31
|
+
def initialize(scope)
|
|
32
|
+
super("invalid fixture scope: #{scope.inspect}; supported scopes: test, suite")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class InvalidFixtureScopeDependencyError < Error
|
|
37
|
+
def initialize(dependent_name:, dependent_scope:, dependency_name:, dependency_scope:)
|
|
38
|
+
message =
|
|
39
|
+
if dependent_name
|
|
40
|
+
"#{dependent_scope}-scoped fixture #{dependent_name} cannot depend on #{dependency_scope}-scoped fixture #{dependency_name}"
|
|
41
|
+
else
|
|
42
|
+
"cannot resolve #{dependency_scope}-scoped fixture #{dependency_name} from #{dependent_scope} fixture scope"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
super(message)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
class InvalidFixtureParameterError < Error; end
|
|
50
|
+
|
|
51
|
+
class AssertionFailed < Error; end
|
|
52
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smartest
|
|
4
|
+
class ExpectationTarget
|
|
5
|
+
def initialize(actual)
|
|
6
|
+
@actual = actual
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def to(matcher)
|
|
10
|
+
return self if matcher.matches?(@actual)
|
|
11
|
+
|
|
12
|
+
raise AssertionFailed, matcher.failure_message
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def not_to(matcher)
|
|
16
|
+
return self unless matcher.matches?(@actual)
|
|
17
|
+
|
|
18
|
+
raise AssertionFailed, matcher.negated_failure_message
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smartest
|
|
4
|
+
class Fixture
|
|
5
|
+
class << self
|
|
6
|
+
def fixture(name, scope: :test, &block)
|
|
7
|
+
define_fixture(
|
|
8
|
+
name,
|
|
9
|
+
scope: scope,
|
|
10
|
+
block: block,
|
|
11
|
+
location: caller_locations(1, 1).first
|
|
12
|
+
)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def suite_fixture(name, &block)
|
|
16
|
+
define_fixture(
|
|
17
|
+
name,
|
|
18
|
+
scope: :suite,
|
|
19
|
+
block: block,
|
|
20
|
+
location: caller_locations(1, 1).first
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def fixture_definitions
|
|
25
|
+
inherited =
|
|
26
|
+
if superclass.respond_to?(:fixture_definitions)
|
|
27
|
+
superclass.fixture_definitions
|
|
28
|
+
else
|
|
29
|
+
{}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
inherited.merge(own_fixture_definitions)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def define_fixture(name, scope:, block:, location:)
|
|
38
|
+
definition = FixtureDefinition.new(
|
|
39
|
+
name: name,
|
|
40
|
+
block: block,
|
|
41
|
+
location: location,
|
|
42
|
+
scope: scope
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
own_fixture_definitions[definition.name] = definition
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def own_fixture_definitions
|
|
49
|
+
@fixture_definitions ||= {}
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def initialize(fixture_set:, context:)
|
|
54
|
+
@fixture_set = fixture_set
|
|
55
|
+
@context = context
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def cleanup(&block)
|
|
61
|
+
raise ArgumentError, "cleanup block is required" unless block
|
|
62
|
+
|
|
63
|
+
@fixture_set.add_cleanup(&block)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def method_missing(method_name, *args, &block)
|
|
67
|
+
if @context.respond_to?(method_name, true)
|
|
68
|
+
@context.__send__(method_name, *args, &block)
|
|
69
|
+
else
|
|
70
|
+
super
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
75
|
+
@context.respond_to?(method_name, true) || super
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smartest
|
|
4
|
+
class FixtureClassRegistry
|
|
5
|
+
include Enumerable
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@fixture_classes = []
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def add(klass)
|
|
12
|
+
unless klass.is_a?(Class) && klass <= Fixture
|
|
13
|
+
raise ArgumentError, "fixture class must inherit from Smartest::Fixture"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
@fixture_classes << klass unless @fixture_classes.include?(klass)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def each(&block)
|
|
20
|
+
@fixture_classes.each(&block)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def to_a
|
|
24
|
+
@fixture_classes.dup
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smartest
|
|
4
|
+
class FixtureDefinition
|
|
5
|
+
VALID_SCOPES = %i[test suite].freeze
|
|
6
|
+
|
|
7
|
+
attr_reader :name, :block, :dependencies, :location, :scope
|
|
8
|
+
|
|
9
|
+
def initialize(name:, block:, location:, scope: :test)
|
|
10
|
+
raise ArgumentError, "fixture name is required" if name.nil? || name.to_s.empty?
|
|
11
|
+
raise ArgumentError, "fixture block is required" unless block
|
|
12
|
+
|
|
13
|
+
@name = name.to_sym
|
|
14
|
+
@block = block
|
|
15
|
+
@location = location
|
|
16
|
+
@scope = normalize_scope(scope)
|
|
17
|
+
@dependencies = ParameterExtractor.required_keyword_names(block, usage: :fixture)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def normalize_scope(scope)
|
|
23
|
+
symbol_scope = scope.to_sym
|
|
24
|
+
return symbol_scope if VALID_SCOPES.include?(symbol_scope)
|
|
25
|
+
|
|
26
|
+
raise InvalidFixtureScopeError, scope
|
|
27
|
+
rescue NoMethodError
|
|
28
|
+
raise InvalidFixtureScopeError, scope
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smartest
|
|
4
|
+
class FixtureSet
|
|
5
|
+
def initialize(fixture_classes, context:, scope: :test, parent: nil)
|
|
6
|
+
@fixture_classes = fixture_classes.to_a
|
|
7
|
+
@context = context
|
|
8
|
+
@scope = normalize_scope(scope)
|
|
9
|
+
@parent = parent
|
|
10
|
+
@cache = {}
|
|
11
|
+
@setup_errors = {}
|
|
12
|
+
@cleanups = []
|
|
13
|
+
@resolving = []
|
|
14
|
+
|
|
15
|
+
build_fixture_index
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def resolve_keywords(names)
|
|
19
|
+
names.to_h do |name|
|
|
20
|
+
symbol_name = name.to_sym
|
|
21
|
+
[symbol_name, resolve(symbol_name)]
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def resolve(name)
|
|
26
|
+
symbol_name = name.to_sym
|
|
27
|
+
|
|
28
|
+
if @resolving.include?(symbol_name)
|
|
29
|
+
cycle_start = @resolving.index(symbol_name)
|
|
30
|
+
raise CircularFixtureDependencyError, @resolving[cycle_start..] + [symbol_name]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
definition = @definitions[symbol_name]
|
|
34
|
+
raise FixtureNotFoundError, symbol_name unless definition
|
|
35
|
+
|
|
36
|
+
return resolve_from_parent(symbol_name, definition) unless definition.scope == @scope
|
|
37
|
+
return @cache[symbol_name] if @cache.key?(symbol_name)
|
|
38
|
+
raise @setup_errors[symbol_name] if @setup_errors.key?(symbol_name)
|
|
39
|
+
|
|
40
|
+
@resolving << symbol_name
|
|
41
|
+
dependencies = resolve_keywords(definition.dependencies)
|
|
42
|
+
@cache[symbol_name] = @instances[symbol_name].instance_exec(**dependencies, &definition.block)
|
|
43
|
+
rescue Exception => error
|
|
44
|
+
raise if Smartest.fatal_exception?(error)
|
|
45
|
+
|
|
46
|
+
@setup_errors[symbol_name] = error if definition&.scope == @scope
|
|
47
|
+
raise
|
|
48
|
+
ensure
|
|
49
|
+
@resolving.pop if @resolving.last == symbol_name
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def add_cleanup(&block)
|
|
53
|
+
raise ArgumentError, "cleanup block is required" unless block
|
|
54
|
+
|
|
55
|
+
@cleanups << block
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def run_cleanups
|
|
59
|
+
errors = []
|
|
60
|
+
|
|
61
|
+
@cleanups.reverse_each do |cleanup|
|
|
62
|
+
cleanup.call
|
|
63
|
+
rescue Exception => error
|
|
64
|
+
raise if Smartest.fatal_exception?(error)
|
|
65
|
+
|
|
66
|
+
errors << error
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
errors
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def normalize_scope(scope)
|
|
75
|
+
symbol_scope = scope.to_sym
|
|
76
|
+
return symbol_scope if FixtureDefinition::VALID_SCOPES.include?(symbol_scope)
|
|
77
|
+
|
|
78
|
+
raise InvalidFixtureScopeError, scope
|
|
79
|
+
rescue NoMethodError
|
|
80
|
+
raise InvalidFixtureScopeError, scope
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def resolve_from_parent(name, definition)
|
|
84
|
+
return @parent.resolve(name) if @parent && definition.scope == :suite
|
|
85
|
+
|
|
86
|
+
raise InvalidFixtureScopeDependencyError.new(
|
|
87
|
+
dependent_name: @resolving.last,
|
|
88
|
+
dependent_scope: @scope,
|
|
89
|
+
dependency_name: name,
|
|
90
|
+
dependency_scope: definition.scope
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def build_fixture_index
|
|
95
|
+
definitions_by_name = Hash.new { |hash, key| hash[key] = [] }
|
|
96
|
+
instances_by_class = {}
|
|
97
|
+
|
|
98
|
+
@fixture_classes.each do |fixture_class|
|
|
99
|
+
instances_by_class[fixture_class] = fixture_class.new(fixture_set: self, context: @context)
|
|
100
|
+
|
|
101
|
+
fixture_class.fixture_definitions.each_value do |definition|
|
|
102
|
+
definitions_by_name[definition.name] << [fixture_class, definition]
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
duplicate = definitions_by_name.find { |_name, entries| entries.length > 1 }
|
|
107
|
+
raise DuplicateFixtureError.new(duplicate.first, duplicate.last.map(&:first)) if duplicate
|
|
108
|
+
|
|
109
|
+
@definitions = {}
|
|
110
|
+
@instances = {}
|
|
111
|
+
|
|
112
|
+
definitions_by_name.each do |name, entries|
|
|
113
|
+
fixture_class, definition = entries.first
|
|
114
|
+
@definitions[name] = definition
|
|
115
|
+
@instances[name] = instances_by_class[fixture_class]
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Smartest
|
|
6
|
+
class InitGenerator
|
|
7
|
+
FILES = {
|
|
8
|
+
"smartest/test_helper.rb" => <<~RUBY,
|
|
9
|
+
# frozen_string_literal: true
|
|
10
|
+
|
|
11
|
+
require "smartest/autorun"
|
|
12
|
+
|
|
13
|
+
Dir[File.join(__dir__, "fixtures", "**", "*.rb")].sort.each do |fixture_file|
|
|
14
|
+
require fixture_file
|
|
15
|
+
end
|
|
16
|
+
RUBY
|
|
17
|
+
"smartest/example_test.rb" => <<~RUBY
|
|
18
|
+
# frozen_string_literal: true
|
|
19
|
+
|
|
20
|
+
require "test_helper"
|
|
21
|
+
|
|
22
|
+
test("example") do
|
|
23
|
+
expect(1 + 1).to eq(2)
|
|
24
|
+
end
|
|
25
|
+
RUBY
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
def initialize(root: Dir.pwd, output: $stdout)
|
|
29
|
+
@root = root
|
|
30
|
+
@output = output
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def run
|
|
34
|
+
create_directory("smartest")
|
|
35
|
+
create_directory("smartest/fixtures")
|
|
36
|
+
FILES.each { |path, contents| create_file(path, contents) }
|
|
37
|
+
|
|
38
|
+
@output.puts
|
|
39
|
+
@output.puts "Run your test suite with: bundle exec smartest"
|
|
40
|
+
|
|
41
|
+
0
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def create_directory(path)
|
|
47
|
+
absolute_path = File.join(@root, path)
|
|
48
|
+
|
|
49
|
+
if Dir.exist?(absolute_path)
|
|
50
|
+
@output.puts "exist #{path}"
|
|
51
|
+
return
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
FileUtils.mkdir_p(absolute_path)
|
|
55
|
+
@output.puts "create #{path}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def create_file(path, contents)
|
|
59
|
+
absolute_path = File.join(@root, path)
|
|
60
|
+
|
|
61
|
+
if File.exist?(absolute_path)
|
|
62
|
+
@output.puts "exist #{path}"
|
|
63
|
+
return
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
File.write(absolute_path, contents)
|
|
67
|
+
@output.puts "create #{path}"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smartest
|
|
4
|
+
module Matchers
|
|
5
|
+
def eq(expected)
|
|
6
|
+
EqMatcher.new(expected)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def include(expected)
|
|
10
|
+
IncludeMatcher.new(expected)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def be_nil
|
|
14
|
+
BeNilMatcher.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def raise_error(expected_error = StandardError)
|
|
18
|
+
RaiseErrorMatcher.new(expected_error)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class EqMatcher
|
|
23
|
+
def initialize(expected)
|
|
24
|
+
@expected = expected
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def matches?(actual)
|
|
28
|
+
@actual = actual
|
|
29
|
+
actual == @expected
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def failure_message
|
|
33
|
+
"expected #{@actual.inspect} to eq #{@expected.inspect}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def negated_failure_message
|
|
37
|
+
"expected #{@actual.inspect} not to eq #{@expected.inspect}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class IncludeMatcher
|
|
42
|
+
def initialize(expected)
|
|
43
|
+
@expected = expected
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def matches?(actual)
|
|
47
|
+
@actual = actual
|
|
48
|
+
actual.include?(@expected)
|
|
49
|
+
rescue NoMethodError
|
|
50
|
+
false
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def failure_message
|
|
54
|
+
"expected #{@actual.inspect} to include #{@expected.inspect}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def negated_failure_message
|
|
58
|
+
"expected #{@actual.inspect} not to include #{@expected.inspect}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
class BeNilMatcher
|
|
63
|
+
def matches?(actual)
|
|
64
|
+
@actual = actual
|
|
65
|
+
actual.nil?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def failure_message
|
|
69
|
+
"expected #{@actual.inspect} to be nil"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def negated_failure_message
|
|
73
|
+
"expected #{@actual.inspect} not to be nil"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
class RaiseErrorMatcher
|
|
78
|
+
def initialize(expected_error)
|
|
79
|
+
@expected_error = expected_error
|
|
80
|
+
@actual_error = nil
|
|
81
|
+
@callable = true
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def matches?(actual)
|
|
85
|
+
@actual_error = nil
|
|
86
|
+
@callable = actual.respond_to?(:call)
|
|
87
|
+
return false unless @callable
|
|
88
|
+
|
|
89
|
+
actual.call
|
|
90
|
+
false
|
|
91
|
+
rescue Exception => error
|
|
92
|
+
raise if Smartest.fatal_exception?(error)
|
|
93
|
+
|
|
94
|
+
@actual_error = error
|
|
95
|
+
error.is_a?(@expected_error)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def failure_message
|
|
99
|
+
return "expected a block to raise #{@expected_error}" unless @callable
|
|
100
|
+
return "expected block to raise #{@expected_error}, but nothing was raised" unless @actual_error
|
|
101
|
+
|
|
102
|
+
"expected block to raise #{@expected_error}, but raised #{@actual_error.class}: #{@actual_error.message}"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def negated_failure_message
|
|
106
|
+
"expected block not to raise #{@expected_error}, but raised #{@actual_error.class}: #{@actual_error.message}"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smartest
|
|
4
|
+
class ParameterExtractor
|
|
5
|
+
POSITIONAL_PARAMETER_TYPES = %i[req opt rest].freeze
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
def required_keyword_names(block, usage:)
|
|
9
|
+
raise ArgumentError, "block is required" unless block
|
|
10
|
+
|
|
11
|
+
parameters = block.parameters
|
|
12
|
+
positional = parameters.select { |type, _name| POSITIONAL_PARAMETER_TYPES.include?(type) }
|
|
13
|
+
|
|
14
|
+
raise InvalidFixtureParameterError, positional_parameter_message(usage) if positional.any?
|
|
15
|
+
|
|
16
|
+
parameters.filter_map do |type, name|
|
|
17
|
+
name if type == :keyreq
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def positional_parameter_message(usage)
|
|
24
|
+
case usage
|
|
25
|
+
when :test
|
|
26
|
+
<<~MESSAGE.chomp
|
|
27
|
+
Positional fixture parameters are not supported.
|
|
28
|
+
|
|
29
|
+
Use keyword fixture injection:
|
|
30
|
+
|
|
31
|
+
test("bad") do |user:|
|
|
32
|
+
...
|
|
33
|
+
end
|
|
34
|
+
MESSAGE
|
|
35
|
+
when :fixture
|
|
36
|
+
<<~MESSAGE.chomp
|
|
37
|
+
Positional fixture dependencies are not supported.
|
|
38
|
+
|
|
39
|
+
Use keyword fixture dependencies:
|
|
40
|
+
|
|
41
|
+
fixture :client do |server:|
|
|
42
|
+
...
|
|
43
|
+
end
|
|
44
|
+
MESSAGE
|
|
45
|
+
else
|
|
46
|
+
"Positional fixture parameters are not supported."
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|