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.
@@ -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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smartest
4
+ class Suite
5
+ attr_reader :tests, :fixture_classes
6
+
7
+ def initialize
8
+ @tests = TestRegistry.new
9
+ @fixture_classes = FixtureClassRegistry.new
10
+ end
11
+ end
12
+ 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smartest
4
+ VERSION = "0.1.0.alpha1"
5
+ 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