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
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smartest
|
|
4
|
+
class Reporter
|
|
5
|
+
PASS_MARK = "\u2713"
|
|
6
|
+
FAIL_MARK = "\u2717"
|
|
7
|
+
|
|
8
|
+
def initialize(io = $stdout)
|
|
9
|
+
@io = io
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def start(count)
|
|
13
|
+
@io.puts "Running #{count} #{count == 1 ? 'test' : 'tests'}"
|
|
14
|
+
@io.puts
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def record(result)
|
|
18
|
+
mark = result.passed? ? PASS_MARK : FAIL_MARK
|
|
19
|
+
@io.puts "#{mark} #{result.test_case.name}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def finish(results, suite_cleanup_errors: [])
|
|
23
|
+
failures = results.select(&:failed?)
|
|
24
|
+
|
|
25
|
+
report_failures(failures) if failures.any?
|
|
26
|
+
report_suite_cleanup_errors(suite_cleanup_errors) if suite_cleanup_errors.any?
|
|
27
|
+
|
|
28
|
+
@io.puts
|
|
29
|
+
summary = "#{results.count} #{results.count == 1 ? 'test' : 'tests'}, #{results.count(&:passed?)} passed, #{failures.count} failed"
|
|
30
|
+
if suite_cleanup_errors.any?
|
|
31
|
+
cleanup_label = suite_cleanup_errors.count == 1 ? "suite cleanup" : "suite cleanups"
|
|
32
|
+
summary = "#{summary}, #{suite_cleanup_errors.count} #{cleanup_label} failed"
|
|
33
|
+
end
|
|
34
|
+
@io.puts summary
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def report_failures(failures)
|
|
40
|
+
@io.puts
|
|
41
|
+
@io.puts "Failures:"
|
|
42
|
+
@io.puts
|
|
43
|
+
|
|
44
|
+
failures.each_with_index do |result, index|
|
|
45
|
+
@io.puts "#{index + 1}) #{result.test_case.name}"
|
|
46
|
+
report_location(result.test_case.location)
|
|
47
|
+
report_error(result.error) if result.error
|
|
48
|
+
result.cleanup_errors.each { |error| report_cleanup_error(error) }
|
|
49
|
+
@io.puts
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def report_suite_cleanup_errors(errors)
|
|
54
|
+
@io.puts
|
|
55
|
+
@io.puts "Suite cleanup failures:"
|
|
56
|
+
@io.puts
|
|
57
|
+
|
|
58
|
+
errors.each_with_index do |error, index|
|
|
59
|
+
@io.puts "#{index + 1}) suite cleanup"
|
|
60
|
+
report_cleanup_error(error)
|
|
61
|
+
@io.puts
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def report_location(location)
|
|
66
|
+
return unless location
|
|
67
|
+
|
|
68
|
+
@io.puts " #{location.path}:#{location.lineno}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def report_error(error)
|
|
72
|
+
if error.is_a?(AssertionFailed)
|
|
73
|
+
@io.puts " #{error.message}"
|
|
74
|
+
else
|
|
75
|
+
@io.puts " #{error.class}: #{error.message}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
report_backtrace(error)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def report_cleanup_error(error)
|
|
82
|
+
@io.puts " cleanup failed: #{error.class}: #{error.message}"
|
|
83
|
+
report_backtrace(error)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def report_backtrace(error)
|
|
87
|
+
backtrace_line = error.backtrace&.first
|
|
88
|
+
@io.puts " #{backtrace_line}" if backtrace_line
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smartest
|
|
4
|
+
class Runner
|
|
5
|
+
def initialize(suite: Smartest.suite, reporter: Reporter.new)
|
|
6
|
+
@suite = suite
|
|
7
|
+
@reporter = reporter
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def run
|
|
11
|
+
results = []
|
|
12
|
+
suite_cleanup_errors = []
|
|
13
|
+
@suite_fixture_set = nil
|
|
14
|
+
|
|
15
|
+
@reporter.start(@suite.tests.count)
|
|
16
|
+
|
|
17
|
+
begin
|
|
18
|
+
@suite.tests.each do |test_case|
|
|
19
|
+
result = run_one(test_case)
|
|
20
|
+
results << result
|
|
21
|
+
@reporter.record(result)
|
|
22
|
+
end
|
|
23
|
+
ensure
|
|
24
|
+
suite_cleanup_errors = @suite_fixture_set.run_cleanups if @suite_fixture_set
|
|
25
|
+
@suite_fixture_set = nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
@reporter.finish(results, suite_cleanup_errors: suite_cleanup_errors)
|
|
29
|
+
|
|
30
|
+
results.any?(&:failed?) || suite_cleanup_errors.any? ? 1 : 0
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def run_one(test_case)
|
|
36
|
+
started_at = now
|
|
37
|
+
context = ExecutionContext.new
|
|
38
|
+
fixture_set = nil
|
|
39
|
+
error = nil
|
|
40
|
+
cleanup_errors = []
|
|
41
|
+
|
|
42
|
+
begin
|
|
43
|
+
fixture_set = FixtureSet.new(@suite.fixture_classes, context: context, parent: suite_fixture_set)
|
|
44
|
+
fixtures = fixture_set.resolve_keywords(test_case.fixture_names)
|
|
45
|
+
context.instance_exec(**fixtures, &test_case.block)
|
|
46
|
+
rescue Exception => rescued_error
|
|
47
|
+
raise if Smartest.fatal_exception?(rescued_error)
|
|
48
|
+
|
|
49
|
+
error = rescued_error
|
|
50
|
+
ensure
|
|
51
|
+
cleanup_errors = fixture_set.run_cleanups if fixture_set
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
duration = now - started_at
|
|
55
|
+
|
|
56
|
+
if error || cleanup_errors.any?
|
|
57
|
+
TestResult.failed(
|
|
58
|
+
test_case: test_case,
|
|
59
|
+
error: error,
|
|
60
|
+
duration: duration,
|
|
61
|
+
cleanup_errors: cleanup_errors
|
|
62
|
+
)
|
|
63
|
+
else
|
|
64
|
+
TestResult.passed(test_case: test_case, duration: duration)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def suite_fixture_set
|
|
69
|
+
@suite_fixture_set ||= FixtureSet.new(
|
|
70
|
+
@suite.fixture_classes,
|
|
71
|
+
context: ExecutionContext.new,
|
|
72
|
+
scope: :suite
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def now
|
|
77
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smartest
|
|
4
|
+
class TestCase
|
|
5
|
+
attr_reader :name, :metadata, :block, :location, :fixture_names
|
|
6
|
+
|
|
7
|
+
def initialize(name:, metadata:, block:, location:)
|
|
8
|
+
raise ArgumentError, "test name is required" if name.nil? || name.to_s.empty?
|
|
9
|
+
raise ArgumentError, "test block is required" unless block
|
|
10
|
+
|
|
11
|
+
@name = name.to_s
|
|
12
|
+
@metadata = metadata
|
|
13
|
+
@block = block
|
|
14
|
+
@location = location
|
|
15
|
+
@fixture_names = ParameterExtractor.required_keyword_names(block, usage: :test)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smartest
|
|
4
|
+
class TestRegistry
|
|
5
|
+
include Enumerable
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@tests = []
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def add(test_case)
|
|
12
|
+
@tests << test_case
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def each(&block)
|
|
16
|
+
@tests.each(&block)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def count
|
|
20
|
+
@tests.count
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
alias size count
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smartest
|
|
4
|
+
class TestResult
|
|
5
|
+
attr_reader :test_case, :status, :error, :duration, :cleanup_errors
|
|
6
|
+
|
|
7
|
+
def self.passed(test_case:, duration:, cleanup_errors: [])
|
|
8
|
+
new(
|
|
9
|
+
test_case: test_case,
|
|
10
|
+
status: :passed,
|
|
11
|
+
error: nil,
|
|
12
|
+
duration: duration,
|
|
13
|
+
cleanup_errors: cleanup_errors
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.failed(test_case:, error:, duration:, cleanup_errors: [])
|
|
18
|
+
new(
|
|
19
|
+
test_case: test_case,
|
|
20
|
+
status: :failed,
|
|
21
|
+
error: error,
|
|
22
|
+
duration: duration,
|
|
23
|
+
cleanup_errors: cleanup_errors
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def initialize(test_case:, status:, error:, duration:, cleanup_errors:)
|
|
28
|
+
@test_case = test_case
|
|
29
|
+
@status = status
|
|
30
|
+
@error = error
|
|
31
|
+
@duration = duration
|
|
32
|
+
@cleanup_errors = cleanup_errors
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def passed?
|
|
36
|
+
status == :passed
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def failed?
|
|
40
|
+
status == :failed
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
data/lib/smartest.rb
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "smartest/version"
|
|
4
|
+
require_relative "smartest/errors"
|
|
5
|
+
require_relative "smartest/parameter_extractor"
|
|
6
|
+
require_relative "smartest/test_case"
|
|
7
|
+
require_relative "smartest/test_registry"
|
|
8
|
+
require_relative "smartest/fixture_definition"
|
|
9
|
+
require_relative "smartest/fixture"
|
|
10
|
+
require_relative "smartest/fixture_class_registry"
|
|
11
|
+
require_relative "smartest/fixture_set"
|
|
12
|
+
require_relative "smartest/suite"
|
|
13
|
+
require_relative "smartest/expectations"
|
|
14
|
+
require_relative "smartest/expectation_target"
|
|
15
|
+
require_relative "smartest/matchers"
|
|
16
|
+
require_relative "smartest/execution_context"
|
|
17
|
+
require_relative "smartest/dsl"
|
|
18
|
+
require_relative "smartest/test_result"
|
|
19
|
+
require_relative "smartest/reporter"
|
|
20
|
+
require_relative "smartest/runner"
|
|
21
|
+
require_relative "smartest/init_generator"
|
|
22
|
+
|
|
23
|
+
module Smartest
|
|
24
|
+
class << self
|
|
25
|
+
def suite
|
|
26
|
+
@suite ||= Suite.new
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def reset!
|
|
30
|
+
@suite = Suite.new
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def disable_autorun!
|
|
34
|
+
@autorun_disabled = true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def autorun_disabled?
|
|
38
|
+
@autorun_disabled == true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def register_autorun!
|
|
42
|
+
return if autorun_disabled?
|
|
43
|
+
return if @autorun_registered
|
|
44
|
+
|
|
45
|
+
@autorun_registered = true
|
|
46
|
+
|
|
47
|
+
at_exit do
|
|
48
|
+
exit Runner.new.run if $ERROR_INFO.nil?
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def fatal_exception?(error)
|
|
53
|
+
error.is_a?(SystemExit) ||
|
|
54
|
+
error.is_a?(Interrupt) ||
|
|
55
|
+
error.is_a?(SignalException) ||
|
|
56
|
+
error.is_a?(NoMemoryError)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|