cake-tester 0.2.0

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,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../test_failure'
4
+ require_relative '../test_neutral'
5
+ require_relative '../test_pass'
6
+ require_relative 'context'
7
+ require_relative 'node'
8
+ require_relative 'child'
9
+
10
+ # A Test is an actionable unit of testing.
11
+ class Test < Contextual::Node
12
+ include Contextual::Child
13
+ # @return [Boolean]
14
+ attr_reader :ran_successfully
15
+
16
+ # @param title [String]
17
+ # @param assertions [Proc]
18
+ # @param action [Proc]
19
+ # @param skip [Boolean]
20
+ # @param options [TestOptions]
21
+ def initialize(
22
+ title,
23
+ assertions:,
24
+ setup: nil,
25
+ teardown: nil,
26
+ action: nil,
27
+ options: nil,
28
+ skip: false
29
+ )
30
+ super(title, setup: setup, teardown: teardown, options: options, skip: skip)
31
+ @action = action
32
+ @assertions = assertions
33
+ @assert_failures = []
34
+ end
35
+
36
+ def ran_successfully=(value)
37
+ # Once this has failed, don't allow it to recover
38
+ return if @ran_successfully == false
39
+
40
+ @ran_successfully = value
41
+ end
42
+
43
+ # Should this run with the current filter settings
44
+ # @param filter_settings [FilterSettings]
45
+ # @return [Boolean]
46
+ def should_run_with_filter(filter_settings)
47
+ return filter_settings.testSearchFor == _title if filter_settings.hasTestSearchFor
48
+ return _title.include? filter_settings.testFilterTerm if filter_settings.hasTestFilterTerm
49
+
50
+ should_run_with_search_term(filter_settings)
51
+ end
52
+
53
+ # Report results, if any
54
+ # @return [void]
55
+ def report(*)
56
+ @result.report(@parent_count)
57
+
58
+ @assert_failures.each do |assert_failure|
59
+ assert_failure.report(@parent_count)
60
+ end
61
+ end
62
+
63
+ # @param filter_settings [FilterSettings]
64
+ # @return [TestResult]
65
+ def get_result(filter_settings)
66
+ super
67
+
68
+ @result = run_setup
69
+
70
+ unless @result.nil?
71
+ # Setup failed, we want to bail out as soon as possible
72
+ @ran_successfully = false
73
+ return @result
74
+ end
75
+
76
+ run_action
77
+
78
+ run_assertions
79
+
80
+ teardown_failure = run_teardown
81
+ unless teardown_failure.nil?
82
+ @ran_successfully = false
83
+ @result = teardown_failure
84
+ end
85
+
86
+ # At this point, if nothing has failed, this test ran successfully
87
+ @ran_successfully = true if @ran_successfully.nil?
88
+ @result
89
+ end
90
+
91
+ private
92
+
93
+ def run_action
94
+ return if @action.nil?
95
+
96
+ begin
97
+ value = @action.call(@context)
98
+ @context.actual = value unless value.nil?
99
+ rescue StandardError => e
100
+ @ran_successfully = false
101
+ # We want to continue and try to teardown anything we've set up
102
+ # even if it's all haywire at this point
103
+ @result = TestFailure.new(@title, 'Failed during action', e)
104
+ end
105
+ end
106
+
107
+ # @return [TestResult]
108
+ def run_assertions
109
+ # Don't bother running assertions if we've already come up failed
110
+ return unless @result.nil?
111
+
112
+ asserts = @assertions.call(@context)
113
+ has_failed_a_test = false
114
+ assert_result = nil
115
+
116
+ asserts.each_with_index do |expect, index|
117
+ # Skip rest of assertions if an assert has failed already,
118
+ # allowing a bypass with an option flag.
119
+ if has_failed_a_test && @options.fail_on_first_expect
120
+ assert_result = AssertNeutral.new(
121
+ 'Skipped: Previous assert failed.',
122
+ index
123
+ )
124
+ @assert_failures << assert_result
125
+ next
126
+ end
127
+
128
+ begin
129
+ assert_result = expect.run
130
+ rescue StandardError => e
131
+ assert_result = TestFailure.new(
132
+ @title,
133
+ 'Failed while running assertions',
134
+ e
135
+ )
136
+ end
137
+
138
+ next unless assert_result.instance_of? AssertFailure
139
+
140
+ assert_result.index = index if asserts.length > 1
141
+ @assert_failures << assert_result
142
+ has_failed_a_test = true
143
+ end
144
+
145
+ @result = if @assert_failures.empty?
146
+ TestPass.new(@title)
147
+ else
148
+ TestFailure.new(@title, 'Assert failed.')
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../helpers/filter_settings'
4
+ require_relative '../helpers/printer'
5
+ require_relative 'node'
6
+ require_relative 'parent'
7
+
8
+ # TestRunner
9
+ #
10
+ # @param title
11
+ # @param context_builder
12
+ # @param children
13
+ # @param options
14
+ # @param skip
15
+ #
16
+ # The root node for any tests. Automatically runs once created. Do not include
17
+ # TestRunners inside other Groups.
18
+ class TestRunner < Contextual::Node
19
+ include Contextual::Parent
20
+
21
+ def initialize(
22
+ title,
23
+ children = [],
24
+ setup: nil,
25
+ teardown: nil,
26
+ options: nil,
27
+ skip: false
28
+ )
29
+ super(title, setup: setup, teardown: teardown, options: options, skip: skip)
30
+ set_parent(children)
31
+ # TODO: Fetch filter settings
32
+ @filter_settings = FilterSettings.new
33
+ run_all
34
+ end
35
+
36
+ private
37
+
38
+ def run_all
39
+ return if skip
40
+
41
+ return unless should_run_with_filter(@filter_settings)
42
+
43
+ run(@context, @filter_settings)
44
+ report(@filter_settings)
45
+ end
46
+
47
+ # @param filter_settings [FilterSettings]
48
+ def should_run_with_filter(filter_settings)
49
+ return @title == filter_settings.test_runner_search_for if filter_settings.has_test_runner_search_for
50
+ return @title.include? filter_settings.test_runner_filter_term if filter_settings.has_test_runner_filter_term
51
+
52
+ should_run_with_search_term_with_children(filter_settings)
53
+ true
54
+ end
55
+
56
+ def report(filter_settings)
57
+ @result.report
58
+ return if @skip
59
+
60
+ report_children(filter_settings)
61
+
62
+ # Get count of successes, failures, and neutrals
63
+ message = Printer.summary(total, successes, failures, neutrals)
64
+
65
+ Printer.pass(message) if @result.instance_of? TestPass
66
+
67
+ Printer.fail(message) if @result.instance_of? TestFailure
68
+
69
+ Printer.neutral(message) if @result.instance_of? TestNeutral
70
+ end
71
+ end
data/lib/expect.rb ADDED
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Contains expects in order to run assertions in a [#Test]
4
+ module Expect
5
+ # @abstract Base class for Expects. Override {#run} to implement the
6
+ # validation logic.
7
+ class ExpectBase
8
+ attr_accessor :actual, :expected
9
+
10
+ # @param [Object] actual
11
+ # @param [Object] expected
12
+ def initialize(actual:, expected: nil)
13
+ @actual = actual
14
+ @expected = expected
15
+ end
16
+
17
+ # Runs the validator and returns an AssertResult
18
+ # @return [AssertPass, AssertFailure]
19
+ def run
20
+ AssertFailure.new("No expectation defined for #{@actual}.")
21
+ end
22
+ end
23
+
24
+ # Checks if actual == expected
25
+ class Equals < ExpectBase
26
+ # Runs the validator and returns an AssertResult
27
+ # @return [AssertPass, AssertFailure]
28
+ def run
29
+ return AssertPass.new if @actual == @expected
30
+
31
+ @actual = @actual.nil? ? '<nil>' : @actual
32
+ @expected = @expected.nil? ? '<nil>' : @expected
33
+ AssertFailure.new("Equality failed: Expected #{@expected}, got #{@actual}.")
34
+ end
35
+ end
36
+
37
+ # Checks if actual != expected
38
+ class IsNotEqual < ExpectBase
39
+ def initialize(actual:, not_expected:)
40
+ super(actual: actual, expected: not_expected)
41
+ end
42
+
43
+ # Runs the validator and returns an AssertResult
44
+ # @return [AssertPass, AssertFailure]
45
+ def run
46
+ return AssertPass.new if @actual != @expected
47
+
48
+ @actual = @actual.nil? ? '<nil>' : @actual
49
+ @expected = @expected.nil? ? '<nil>' : @expected
50
+ AssertFailure.new("Inequality failed: Expected #{@expected} to not equal #{@actual}.")
51
+ end
52
+ end
53
+
54
+ # Checks if actual is nil
55
+ class IsNil < ExpectBase
56
+ def initialize(actual)
57
+ super(actual: actual)
58
+ end
59
+
60
+ # Runs the validator and returns an AssertResult
61
+ # @return [AssertPass, AssertFailure]
62
+ def run
63
+ return AssertPass.new if @actual.nil?
64
+
65
+ AssertFailure.new("IsNil failed: Expected #{@actual} to be nil.")
66
+ end
67
+ end
68
+
69
+ # Checks if actual is not nil
70
+ class IsNotNil < ExpectBase
71
+ def initialize(actual)
72
+ super(actual: actual)
73
+ end
74
+
75
+ # Runs the validator and returns an AssertResult
76
+ # @return [AssertPass, AssertFailure]
77
+ def run
78
+ return AssertPass.new unless @actual.nil?
79
+
80
+ AssertFailure.new("IsNotNil failed: Expected #{@actual} to not be nil.")
81
+ end
82
+ end
83
+
84
+ # Checks if actual is truthy
85
+ class IsTrue < ExpectBase
86
+ def initialize(actual)
87
+ super(actual: actual)
88
+ end
89
+
90
+ # Runs the validator and returns an AssertResult
91
+ # @return [AssertPass, AssertFailure]
92
+ def run
93
+ return AssertPass.new if @actual
94
+
95
+ @actual = @actual.nil? ? '<nil>' : @actual
96
+ AssertFailure.new("IsTrue failed: Expected #{@actual} to be true.")
97
+ end
98
+ end
99
+
100
+ # Checks if actual is falsey
101
+ class IsFalse < ExpectBase
102
+ def initialize(actual)
103
+ super(actual: actual, expected: true)
104
+ end
105
+
106
+ # Runs the validator and returns an AssertResult
107
+ # @return [AssertPass, AssertFailure]
108
+ def run
109
+ return AssertPass.new unless @actual
110
+
111
+ AssertFailure.new("IsFalse failed: Expected #{@actual} to be false.")
112
+ end
113
+ end
114
+
115
+ # Checks if actual responds to a method
116
+ class RespondTo < ExpectBase
117
+ def initialize(actual, method)
118
+ super(actual: actual)
119
+ @method = method
120
+ end
121
+
122
+ # Runs the validator and returns an AssertResult
123
+ # @return [AssertPass, AssertFailure]
124
+ def run
125
+ return AssertPass.new if @actual.respond_to?(@method)
126
+
127
+ @actual = @actual.nil? ? '<nil>' : @actual
128
+ AssertFailure.new("RespondsTo failed: Expected #{@actual} to respond to #{@method}.")
129
+ end
130
+ end
131
+
132
+ # Checks if actual does not respond to a method
133
+ class Undefined < ExpectBase
134
+ def initialize(actual, method)
135
+ super(actual: actual)
136
+ @method = method
137
+ end
138
+
139
+ # Runs the validator and returns an AssertResult
140
+ # @return [AssertPass, AssertFailure]
141
+ def run
142
+ return AssertPass.new unless @actual.methods.include?(@method)
143
+
144
+ @actual = @actual.nil? ? '<nil>' : @actual
145
+ AssertFailure.new("Undefined failed: Expected #{@actual} to not respond to #{@method}.")
146
+ end
147
+ end
148
+
149
+ # Checks if actual does not respond to a method
150
+ # Synonym for Expect::Undefined
151
+ # @note (see Expect::Undefined)
152
+ class DoesNotRespondTo < Undefined
153
+ end
154
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Holds a collection of filter settings. Filter settings are normally set via
4
+ # the command line as flag options.
5
+ class FilterSettings
6
+ def initialize(
7
+ general_search_term: nil,
8
+ test_filter_term: nil,
9
+ test_search_for: nil,
10
+ group_filter_term: nil,
11
+ group_search_for: nil,
12
+ test_runner_filter_term: nil,
13
+ test_runner_search_for: nil
14
+ )
15
+ @general_search_term = general_search_term || get_from_args(FilterProps::GENERAL_SEARCH_TERM)
16
+ @test_filter_term = test_filter_term || get_from_args(FilterProps::TEST_FILTER_TERM)
17
+ @test_search_for = test_search_for || get_from_args(FilterProps::TEST_SEARCH_FOR)
18
+ @group_filter_term = group_filter_term || get_from_args(FilterProps::GROUP_FILTER_TERM)
19
+ @group_search_for = group_search_for || get_from_args(FilterProps::GROUP_SEARCH_FOR)
20
+ @test_runner_filter_term = test_runner_filter_term || get_from_args(FilterProps::TEST_RUNNER_FILTER_TERM)
21
+ @test_runner_search_for = test_runner_search_for || get_from_args(FilterProps::TEST_RUNNER_SEARCH_FOR)
22
+ end
23
+
24
+ def has_general_search_term
25
+ !@general_search_term.nil? && @general_search_term.length.positive?
26
+ end
27
+
28
+ def has_test_filter_term
29
+ !@test_filter_term.nil? && @test_filter_term.length.positive?
30
+ end
31
+
32
+ def has_test_search_for
33
+ !@test_search_for.nil? && @test_search_for.length.positive?
34
+ end
35
+
36
+ def has_group_filter_term
37
+ !@group_filter_term.nil? && @group_filter_term.length.positive?
38
+ end
39
+
40
+ def has_group_search_for
41
+ !@group_search_for.nil? && @group_search_for.length.positive?
42
+ end
43
+
44
+ def has_test_runner_filter_term
45
+ !@test_runner_filter_term.nil? && @test_runner_filter_term.length.positive?
46
+ end
47
+
48
+ def has_test_runner_search_for
49
+ !@test_runner_search_for.nil? && @test_runner_search_for.length.positive?
50
+ end
51
+
52
+ def is_not_empty
53
+ hasGeneralSearchTerm ||
54
+ hasTestFilterTerm ||
55
+ hasTestSearchFor ||
56
+ hasGroupFilterTerm ||
57
+ hasGroupSearchFor ||
58
+ hasTestRunnerFilterTerm ||
59
+ hasTestRunnerSearchFor
60
+ end
61
+
62
+ def to_properties
63
+ props = []
64
+
65
+ props << [FilterProps::GENERAL_SEARCH_TERM, @general_search_term] if has_general_search_term
66
+ props << [FilterProps::TEST_FILTER_TERM, @test_filter_term] if has_test_filter_term
67
+ props << [FilterProps::TEST_SEARCH_FOR, @test_search_for] if has_test_search_for
68
+ props << [FilterProps::GROUP_FILTER_TERM, @group_filter_term] if has_group_filter_term
69
+ props << [FilterProps::GROUP_SEARCH_FOR, @group_search_for] if has_group_search_for
70
+ props << [FilterProps::TEST_RUNNER_FILTER_TERM, @test_runner_filter_term] if has_test_runner_filter_term
71
+ props << [FilterProps::TEST_RUNNER_SEARCH_FOR, @test_runner_search_for] if has_test_runner_search_for
72
+
73
+ props
74
+ end
75
+
76
+ # @param [String] flag
77
+ # @return [String, Nil]
78
+ def get_from_args(flag)
79
+ index = ARGV.find_index(flag)
80
+ return ARGV[index + 1] if index && index != ARGV.length - 1
81
+
82
+ nil
83
+ end
84
+ end
85
+
86
+ module FilterProps
87
+ GENERAL_SEARCH_TERM = 'generalSearchTerm'
88
+ TEST_FILTER_TERM = 'testFilterTerm'
89
+ TEST_SEARCH_FOR = 'testSearchFor'
90
+ GROUP_FILTER_TERM = 'groupFilterTerm'
91
+ GROUP_SEARCH_FOR = 'groupSearchFor'
92
+ TEST_RUNNER_FILTER_TERM = 'testRunnerFilterTerm'
93
+ TEST_RUNNER_SEARCH_FOR = 'testRunnerSearchFor'
94
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Printer
4
+ # Prints a message in green to the console.
5
+ #
6
+ # Parameters:
7
+ # - message: A string message to be printed.
8
+ #
9
+ # Returns:
10
+ # None
11
+ def self.pass(message)
12
+ puts "\x1B[32m#{message}\x1B[0m"
13
+ end
14
+
15
+ # Prints a neutral message in red.
16
+ #
17
+ # Params:
18
+ # - message: The error message to be displayed.
19
+ #
20
+ # Returns:
21
+ # None
22
+ def self.fail(message)
23
+ puts "\x1B[31m#{message}\x1B[0m"
24
+ end
25
+
26
+ # Prints a neutral message in gray.
27
+ #
28
+ # Parameters:
29
+ # - message: A string representing the message to be printed.
30
+ #
31
+ # Returns: None
32
+ def self.neutral(message)
33
+ puts "\x1B[90m#{message}\x1B[0m"
34
+ end
35
+
36
+ # Generates a summary message based on the total number of tests, the number of successful tests,
37
+ # the number of failed tests, and the number of neutral tests. The summary message is formatted
38
+ # as a box with the following structure:
39
+ #
40
+ # - Summary: -------------------
41
+ # | |
42
+ # | $total tests ran. |
43
+ # | - $successes passed. |
44
+ # | - $failures failed. |
45
+ # | - $neutrals skipped/inconclusive. |
46
+ # -------------------------------
47
+ #
48
+ # Parameters:
49
+ # - total (int): The total number of tests.
50
+ # - successes (int): The number of successful tests.
51
+ # - failures (int): The number of failed tests.
52
+ # - neutrals (int): The number of neutral tests.
53
+ #
54
+ # Returns:
55
+ # - message (str): The formatted summary message.
56
+ def self.summary(total, successes, failures, neutrals)
57
+ total_char_count = total.to_s.length
58
+ success_char_count = successes.to_s.length
59
+ failure_char_count = failures.to_s.length
60
+ neutral_char_count = neutrals.to_s.length
61
+ max_char_count = [total_char_count, success_char_count, failure_char_count, neutral_char_count].max
62
+
63
+ base_top_dash_spacer = 18
64
+ base_blank_spacer = 28
65
+ base_total_spacer = 14
66
+ base_success_spacer = 15
67
+ base_failure_spacer = 15
68
+ base_neutral_spacer = 1
69
+ box_length = 29
70
+
71
+ total_extra_space = max_char_count - total_char_count + base_total_spacer
72
+ success_extra_space = max_char_count - success_char_count + base_success_spacer
73
+ failure_extra_space = max_char_count - failure_char_count + base_failure_spacer
74
+ neutral_extra_space = max_char_count - neutral_char_count + base_neutral_spacer
75
+
76
+ # There are only two fields that might realistically overflow the box
77
+ # - total and neutrals. Since total will always be >= passed and failed
78
+ # count, we can just look at total and push out the space from there.
79
+ all_extra_space = 0
80
+
81
+ if neutral_extra_space == max_char_count && max_char_count > base_neutral_spacer
82
+ all_extra_space = max_char_count - base_neutral_spacer
83
+ elsif total_extra_space == max_char_count && max_char_count > base_total_spacer
84
+ all_extra_space = max_char_count - base_total_spacer
85
+ end
86
+
87
+ # Create a box that looks like this:
88
+ # - Summary: -------------------
89
+ # | |
90
+ # | $total tests ran. |
91
+ # | - $successes passed. |
92
+ # | - $failures failed. |
93
+ # | - $neutrals skipped/inconclusive. |
94
+ # -------------------------------
95
+
96
+ message = "\n"
97
+ message += ' - Summary: '
98
+ (base_top_dash_spacer + all_extra_space).times do
99
+ message += '-'
100
+ end
101
+ message += "\n"
102
+
103
+ # | | (blank spacer line)
104
+ message += '|'
105
+ (base_blank_spacer + all_extra_space).times do
106
+ message += ' '
107
+ end
108
+ message += "|\n"
109
+
110
+ # | $total tests ran. |
111
+ message += "| #{total} tests ran."
112
+ (total_extra_space + all_extra_space).times do
113
+ message += ' '
114
+ end
115
+ message += "|\n"
116
+
117
+ # | - $successes passed. |
118
+ message += "| - #{successes} passed."
119
+ (success_extra_space + all_extra_space).times do
120
+ message += ' '
121
+ end
122
+ message += "|\n"
123
+
124
+ # | - $failures failed. |
125
+ message += "| - #{failures} failed."
126
+ (failure_extra_space + all_extra_space).times do
127
+ message += ' '
128
+ end
129
+ message += "|\n"
130
+
131
+ # | - $neutrals skipped/inconclusive. |
132
+ message += "| - #{neutrals} skipped/inconclusive."
133
+ (neutral_extra_space + all_extra_space).times do
134
+ message += ' '
135
+ end
136
+ message += "|\n"
137
+
138
+ # -------------------------------
139
+ (box_length + all_extra_space).times do
140
+ message += '-'
141
+ end
142
+ message += "\n"
143
+
144
+ message
145
+ end
146
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'test_result'
4
+ require_relative 'helpers/printer'
5
+
6
+ # Test that has failed, either with a provided message or thrown error
7
+ class TestFailure < TestResult
8
+ def initialize(title, message = nil, err = nil)
9
+ super(title)
10
+ @message = message
11
+ @err = err
12
+ end
13
+
14
+ def report(spacer_count = 0)
15
+ super
16
+ Printer.fail(@spacer + format_message)
17
+ return if @err.nil?
18
+
19
+ # Extra space is to compensate for the [X]
20
+ Printer.fail("#{@spacer} #{@err}")
21
+
22
+ # Mimic the default Ruby trace. This does not need additional formatting as
23
+ # some terminals recognize this as a stack trace.
24
+ puts @err.backtrace.join("\n\t")
25
+ .sub("\n\t", ": #{@err}#{@err.class ? " (#{@err.class})" : ''}\n\t")
26
+ end
27
+
28
+ def failures
29
+ 1
30
+ end
31
+
32
+ private
33
+
34
+ def format_message
35
+ if @title.nil?
36
+ " #{@message}"
37
+ else
38
+ "[X] #{@title}: #{@message}"
39
+ end
40
+ end
41
+ end
42
+
43
+ # Assert that has failed with a message explaining why the assertion failed
44
+ class AssertFailure < AssertResult
45
+ def initialize(message)
46
+ super(message, nil)
47
+ end
48
+
49
+ def report(spacer_count)
50
+ super
51
+
52
+ Printer.fail(@spacer + format_message)
53
+ end
54
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'test_result'
4
+ require_relative 'helpers/printer'
5
+
6
+ # Prints system messages, generally ran by the CLI runner
7
+ class TestMessage < TestResult
8
+ attr_accessor :message
9
+
10
+ def initialize(title, message)
11
+ super(title)
12
+ @message = message
13
+ end
14
+
15
+ def report(spacer_count = 0)
16
+ super
17
+ if message.nil?
18
+ Printer.neutral(@spacer + @title)
19
+ else
20
+ Printer.neutral("#{@spacer}#{@title} #{@message}")
21
+ end
22
+ end
23
+ end