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.
data/bin/collector.rb ADDED
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../lib/helpers/printer'
4
+
5
+ # Collector holds totals for all collectors and prints a summary
6
+ class Collector
7
+ # @return [Integer]
8
+ attr_reader :total, :end_index
9
+
10
+ def initialize
11
+ @collectors = []
12
+ @total = 0
13
+ @successes = 0
14
+ @failures = 0
15
+ @neutrals = 0
16
+ @end_index = 0
17
+ end
18
+
19
+ # Adds a collector and updates the total, successes, failures, and neutrals
20
+ # @param collector [TestRunnerCollector]
21
+ def add_collector(collector)
22
+ @collectors << collector
23
+ @total += collector.total
24
+ @successes += collector.successes
25
+ @failures += collector.failures
26
+ @neutrals += collector.neutrals
27
+ @end_index = collector.end_index
28
+ end
29
+
30
+ def print_message(verbose)
31
+ summary = Printer.summary(@total, @successes, @failures, @neutrals)
32
+
33
+ if @failures.positive?
34
+ Printer.fail(summary)
35
+ @collectors.each(&:print_errors)
36
+ elsif @successes.zero?
37
+ Printer.neutral(summary)
38
+ else
39
+ Printer.pass(summary)
40
+ end
41
+
42
+ return unless verbose
43
+
44
+ @collectors.each(&:print_message)
45
+ end
46
+ end
47
+
48
+ # TestRunnerCollector holds totals for a single test runner
49
+ class Test_Runner_Controller
50
+ # @return [Integer]
51
+ attr_reader :total
52
+ # @return [Integer]
53
+ attr_reader :successes
54
+ # @return [Integer]
55
+ attr_reader :failures
56
+ # @return [Integer]
57
+ attr_reader :neutrals
58
+ # @return [Integer]
59
+ attr_reader :end_index
60
+
61
+ # Initializes a new instance of the class.
62
+ #
63
+ # @param output [Array<String>] Total output of the TestRunner
64
+ # @param total [Integer] Total amount of tests from the TestRunner
65
+ # @param successes [Integer] Total amount of successes from the TestRunner
66
+ # @param failures [Integer] Total amount of failures from the TestRunner
67
+ # @param neutrals [Integer] Total amount of neutrals from the TestRunner
68
+ # @param end_index [Integer] Last line given by the TestRunner
69
+ def initialize(output, total:, successes:, failures:, neutrals:, end_index:)
70
+ @output = output
71
+ @total = total
72
+ @successes = successes
73
+ @failures = failures
74
+ @neutrals = neutrals
75
+ @end_index = end_index
76
+ end
77
+
78
+ def print_message
79
+ # This already has formatting from the printer, just pass through the original message
80
+ @output.each do |message|
81
+ puts message
82
+ end
83
+ end
84
+
85
+ def print_errors
86
+ return unless @failures.positive?
87
+
88
+ print_message
89
+ end
90
+ end
data/bin/runner.rb ADDED
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'collector'
4
+
5
+ # Controls IO and output
6
+ class Runner
7
+ # Retrieves all files with a cake.rb extension in the current directory
8
+ # @return [Array<String>]
9
+ def cake_file_list
10
+ # Get all files in the current directory, including subdirectories
11
+ Dir.glob('**/*.cake.rb')
12
+ end
13
+
14
+ # Runs the tests
15
+ # @param settings [Settings]
16
+ # @param cake_list [Array<String>]
17
+ def run(settings, cake_list)
18
+ collector = Collector.new
19
+ process_args = settings.test_filter.to_properties
20
+
21
+ cake_list.each do |file|
22
+ next if settings.file_filter && !file.include?(settings.file_filter)
23
+
24
+ r, w = IO.pipe
25
+ pid = Process.spawn('ruby', file, process_args.join(' '), out: w, err: %i[child out])
26
+ w.close
27
+ pid, status = Process.wait2
28
+ output = r.read
29
+ r.close
30
+
31
+ collectors = test_runner_output_parser(output)
32
+ collectors.each do |c|
33
+ collector.add_collector(c)
34
+ end
35
+
36
+ puts "#{collector.total} tests found in #{file}..." if settings.verbose
37
+ end
38
+
39
+ collector.print_message(settings.verbose || settings.vs_code)
40
+ return if settings.vs_code
41
+
42
+ # This makes sure to clear out any color changes
43
+ Printer.neutral('')
44
+ end
45
+
46
+ def show_help
47
+ puts 'Usage: cake-tester [options]'
48
+ puts ' -h, --help Show this help message'
49
+ puts ' -i, --interactive Interactive mode'
50
+ puts ' -v, --verbose Verbose output'
51
+ puts ''
52
+ puts 'Test Filters: '
53
+ puts <<~HELP
54
+ -t General search: -t "foo" Run all tests, groups, and runners with "foo" in the title
55
+ --tt Test search: --tt "cool test" Run all tests with the phrase "cool test" in the title
56
+ --tte Test search, exact: --tte "should pass when true" Runs only the test that matches the phrase exactly.
57
+ --tg Group search: --tg "bar" Run all groups matching "bar" in the title
58
+ --tge Group search, exact: --tge "API Endpoints" Runs all groups exactly matching the phrase "API Endpoints"
59
+ --tr Test Runner search: --tr "Models" Runs all test runners with "Models" in the title
60
+ --tre Test Runner search, exact: --tre "Models - User" Runs test runners that exactly match the phrase "Models - User"
61
+ HELP
62
+ end
63
+
64
+ private
65
+
66
+ # Parses output from the process
67
+ # @param output [String]
68
+ # @return [Array<TestRunnerCollector>]
69
+ def test_runner_output_parser(output)
70
+ test_runner_collectors = []
71
+ lines = output.lines
72
+ cursor = 0
73
+
74
+ while cursor < (lines.length - 1)
75
+ line = lines[cursor]
76
+ test_runner_collector = test_runner_output_parse(lines, cursor)
77
+ cursor = test_runner_collector.end_index
78
+ test_runner_collectors << test_runner_collector
79
+ end
80
+
81
+ test_runner_collectors
82
+ end
83
+
84
+ # @param lines [Array<String>] All output lines
85
+ # @param cursor [Integer] Current index of the cursor in the output
86
+ # @return [TestRunnerCollector]
87
+ def test_runner_output_parse(lines, cursor)
88
+ test_output = []
89
+ total = 0
90
+ successes = 0
91
+ failures = 0
92
+ neutrals = 0
93
+ at_summary_line = -1
94
+ summary_line = ' - Summary: ---------------'
95
+ total_line = /(\d*) tests ran\./
96
+ success_line = /(\d*) passed\./
97
+ failed_line = /(\d*) failed\./
98
+ neutral_line = %r{(\d*) skipped/inconclusive\.}
99
+ i = cursor
100
+ while i < lines.length - 1 || at_summary_line == 7
101
+ line = lines[i]
102
+ at_summary_line = 0 if line.include? summary_line
103
+
104
+ case at_summary_line
105
+ when 2
106
+ total = total_line.match(line)[1].to_i
107
+ when 3
108
+ successes = success_line.match(line)[1].to_i
109
+ when 4
110
+ failures = failed_line.match(line)[1].to_i
111
+ when 5
112
+ neutrals = neutral_line.match(line)[1].to_i
113
+ end
114
+
115
+ if at_summary_line > -1
116
+ at_summary_line += 1
117
+ else
118
+ test_output << line
119
+ end
120
+
121
+ i += 1
122
+ end
123
+
124
+ Test_Runner_Controller.new(
125
+ test_output,
126
+ total: total,
127
+ successes: successes,
128
+ failures: failures,
129
+ neutrals: neutrals,
130
+ end_index: i
131
+ )
132
+ end
133
+ end
data/bin/settings.rb ADDED
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../lib/helpers/filter_settings'
4
+
5
+ # Converts args into easily accessible settings
6
+ class Settings
7
+ attr_reader :verbose, :file_filter, :vs_code, :interactive, :test_filter, :show_help
8
+
9
+ def initialize
10
+ @verbose = ARGV.include?('-v') || ARGV.include?('--verbose')
11
+ @file_filter = get_from_args('-f')
12
+ @vs_code = ARGV.include? '--vs-code'
13
+ @interactive = ARGV.include?('-i') || ARGV.include?('--interactive')
14
+ @show_help = ARGV.include?('-h') || ARGV.include?('--help')
15
+ @test_filter = FilterSettings.new(
16
+ general_search_term: get_from_args('-t'),
17
+ test_filter_term: get_from_args('--tt'),
18
+ test_search_for: get_from_args('--tte'),
19
+ group_filter_term: get_from_args('--tg'),
20
+ group_search_for: get_from_args('--tge'),
21
+ test_runner_filter_term: get_from_args('--tr'),
22
+ test_runner_search_for: get_from_args('--tre')
23
+ )
24
+ end
25
+
26
+ private
27
+
28
+ # @param [String] flag
29
+ # @return [String, Nil]
30
+ def get_from_args(flag)
31
+ index = ARGV.find_index(flag)
32
+ return ARGV[index + 1] if index && index != ARGV.length - 1
33
+
34
+ nil
35
+ end
36
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gem::Specification.new do |gem|
4
+ gem.name = 'cake-tester'
5
+ gem.version = '0.2.0'
6
+ gem.authors = ['Polyhedra', 'C. Lee Spruit']
7
+ gem.summary = 'The lightweight, explicit testing framework for Ruby.'
8
+ gem.description = ''
9
+ gem.homepage = 'https://github.com/Polyhedra-Studio/Cake-Ruby'
10
+ gem.license = 'MPL-2.0'
11
+ gem.required_ruby_version = '>= 2.7.0'
12
+ gem.metadata['rubygems_mfa_required'] = 'true'
13
+
14
+ gem.files = `git ls-files -z`.split("\x0")
15
+ # gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
16
+ gem.executables << 'cake'
17
+ gem.require_paths = ['lib']
18
+ end
data/lib/cake.rb ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'expect'
4
+ require_relative 'contextual/group'
5
+ require_relative 'contextual/test_runner'
6
+ require_relative 'contextual/test'
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../test_options'
4
+
5
+ # Contextual is a wrapper for Node and Node attributes
6
+ module Contextual
7
+ # A Child is a Node that has a parent
8
+ module Child
9
+ # Handles assigning parent information to child
10
+ #
11
+ # @param parent_options [TestOptions] Options from parent to copy to child
12
+ def assign_parent(parent_options)
13
+ @parent_count = 0 if @parent_count.nil?
14
+ @parent_count += 1
15
+
16
+ @options = TestOptions.new if @options.nil?
17
+ @options.map_from_parent(parent_options)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ostruct'
4
+
5
+ # The Context is used to pass information between different stages of a Test.
6
+ # Context is inherited from parent to child, but not sibling to sibling.
7
+ # The Context object is an OpenStruct, which means you can dynamically assign
8
+ # as needed.
9
+ class Context < OpenStruct
10
+ # @return [Object] actual expected object, ideally will be set during the
11
+ # Contexual::Node#run_action step
12
+ attr_accessor :actual
13
+
14
+ # @return [Object] expected
15
+ attr_accessor :expected
16
+
17
+ def initialize
18
+ super
19
+ @expected = nil
20
+ @actual = nil
21
+ end
22
+
23
+ # @param [Context] parent_context
24
+ def apply(parent_context)
25
+ @expected = parent_context.expected
26
+ @actual = parent_context.actual
27
+
28
+ parent_context.each_pair do |key, value|
29
+ self[key] = value
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'child'
4
+ require_relative 'node'
5
+ require_relative 'parent'
6
+
7
+ # A Group is a organizational class that holds other tests. You can nest as many
8
+ # groups as you like.
9
+ class Group < Contextual::Node
10
+ include Contextual::Child
11
+ include Contextual::Parent
12
+
13
+ def initialize(
14
+ title,
15
+ children = [],
16
+ setup: nil,
17
+ teardown: nil,
18
+ options: nil,
19
+ skip: false
20
+ )
21
+ super(title, setup: setup, teardown: teardown, options: options, skip: skip)
22
+ set_parent(children)
23
+ end
24
+
25
+ # Should this run with the current filter settings
26
+ # @param filter_settings [FilterSettings]
27
+ # @return [Boolean]
28
+ def should_run_with_filter(filter_settings)
29
+ # This should run failrly close to testRunner's version
30
+ return @title == filter_settings.group_search_for if filter_settings.has_group_search_for
31
+ return @title.include? filter_settings.group_filter_term if filter_settings.has_group_filter_term
32
+
33
+ should_run_with_search_term_with_children(filter_settings)
34
+ end
35
+
36
+ # Report results, if any
37
+ # @param filter_settings [FilterSettings]
38
+ # @return [TestResult, Nil]
39
+ def report(filter_settings)
40
+ @result.report(@parent_count)
41
+ return if @skip
42
+
43
+ report_children(filter_settings)
44
+ end
45
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../test_options'
4
+
5
+ module Contextual
6
+ # @abstract Base class for testing nodes, like TestRunner, Group, and Test.
7
+ class Node
8
+ attr_reader :skip, :result
9
+
10
+ def initialize(title, setup: nil, teardown: nil, options: nil, skip: false)
11
+ @title = title
12
+ @setup = setup
13
+ @teardown = teardown
14
+ @options = options.nil? ? TestOptions.new : options
15
+ @skip = skip
16
+ @context = Context.new
17
+ end
18
+
19
+ # @param current_context [Context] Parent or root context
20
+ # @param filter_settings [FilterSettings]
21
+ # @return [TestResult]
22
+ def run(current_context, filter_settings)
23
+ if @skip
24
+ @result = TestNeutral.new(@title, 'Skipped')
25
+ else
26
+ @context.apply(current_context)
27
+ @result = get_result(filter_settings)
28
+ end
29
+ @result
30
+ end
31
+
32
+ # Runs the setup hook
33
+ # @return [TestResult, Nil] Returns a TestFailure if the setup fails
34
+ def run_setup
35
+ return if @setup.nil?
36
+
37
+ begin
38
+ @setup.call(@context)
39
+ nil
40
+ rescue StandardError => e
41
+ @result = TestFailure.new(@title, 'Failed during setup', e)
42
+ end
43
+ end
44
+
45
+ # Runs the teardown hook
46
+ # @return [TestResult, Nil] Returns a TestFailure if the teardown fails
47
+ def run_teardown
48
+ return if @teardown.nil?
49
+
50
+ begin
51
+ @teardown.call(@context)
52
+ nil
53
+ rescue StandardError => e
54
+ if @result.successes.positive?
55
+ TestFailure.new(@title, 'Tests passed, but failed during teardown.', e)
56
+ else
57
+ TestFailure.new(@title, 'Failed during teardown', e)
58
+ end
59
+ end
60
+ end
61
+
62
+ # @abstract Override this method in your subclass
63
+ # @param _filter_settings [FilterSettings] not used
64
+ # @return [TestResult]
65
+ def get_result(_filter_settings)
66
+ # This has been marked as skipped - do nothing
67
+ TestNeutral.new(@title, 'Skipped') if @skip
68
+ end
69
+
70
+ # Should this run with the current filter settings, checking general search
71
+ # @param filter_settings [FilterSettings]
72
+ # @return [Boolean]
73
+ def should_run_with_search_term(filter_settings)
74
+ # Check if the general search term applies here
75
+ return @title.include?(filter_settings.general_search_term) if filter_settings.has_general_search_term
76
+
77
+ true
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,191 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../test_failure'
4
+ require_relative '../test_neutral'
5
+ require_relative '../test_pass'
6
+
7
+ module Contextual
8
+ # A Parent is a Node that can have children
9
+ module Parent
10
+ # @return [Array<Node>]
11
+ attr_reader :children
12
+
13
+ # @param children [Array<Node>]
14
+ def set_parent(children = [])
15
+ @children = children
16
+ @test_fail_count = 0
17
+ @test_success_count = 0
18
+ @test_neutral_count = 0
19
+ @filter_applies_to_children = false
20
+ assign_children
21
+ end
22
+
23
+ # @param parent_options [TestOptions]
24
+ # @return [void]
25
+ def assign_parent(parent_options)
26
+ super
27
+ @children.each do |child|
28
+ next unless child.respond_to?(:assign_parent)
29
+
30
+ child.assign_parent(parent_options)
31
+ end
32
+ end
33
+
34
+ # @param filter_settings [FilterSettings]
35
+ # @return [Boolean]
36
+ def should_run_with_search_term_with_children(filter_settings)
37
+ # Check if the general search term applies here
38
+ return true if should_run_with_search_term(filter_settings)
39
+
40
+ if filter_settings.has_test_filter_term ||
41
+ filter_settings.has_test_search_for
42
+ should_run_filter_on_children(filter_settings)
43
+ end
44
+
45
+ # No applicable filter
46
+ true
47
+ end
48
+
49
+ # @param filter_settings [FilterSettings]
50
+ # @return [void]
51
+ def report_children(filter_settings)
52
+ @children.each do |child|
53
+ if @filter_applies_to_children
54
+ child.report(filter_settings) if child.should_run_with_filter(filter_settings)
55
+ else
56
+ child.report(filter_settings)
57
+ end
58
+ end
59
+ end
60
+
61
+ # @param filter_settings [FilterSettings]
62
+ # @return [TestResult]
63
+ def get_result(filter_settings)
64
+ super
65
+
66
+ # This is just a stub if there's no children - do nothing
67
+ return TestNeutral.new(@title, 'Empty - no tests') if @children.empty?
68
+
69
+ @result = run_setup
70
+
71
+ return @result unless @result.nil?
72
+
73
+ @result = get_result_children(filter_settings)
74
+
75
+ teardown_failure = run_teardown
76
+ @result = teardown_failure unless teardown_failure.nil?
77
+
78
+ @result
79
+ end
80
+
81
+ # Counts all successes, including children
82
+ # @return [Integer]
83
+ def successes
84
+ current_success_count = @test_success_count
85
+ @children.each do |child|
86
+ current_success_count += child.successes if child.instance_of? Group
87
+ end
88
+ current_success_count
89
+ end
90
+
91
+ # Counts all failures, including children
92
+ # @return [Integer]
93
+ def failures
94
+ current_fail_count = @test_fail_count
95
+ @children.each do |child|
96
+ current_fail_count += child.failures if child.instance_of? Group
97
+ end
98
+ current_fail_count
99
+ end
100
+
101
+ # Counts all neutrals, including children
102
+ # @return [Integer]
103
+ def neutrals
104
+ current_neutral_count = @test_neutral_count
105
+ @children.each do |child|
106
+ current_neutral_count += child.neutrals if child.instance_of? Group
107
+ end
108
+ current_neutral_count
109
+ end
110
+
111
+ # Counts all tests, including children
112
+ # @return [Integer]
113
+ def total
114
+ test_count = 0
115
+ @children.each do |child|
116
+ if child.instance_of? Group
117
+ test_count += child.total
118
+ elsif child.instance_of?(Test) && child.ran_successfully
119
+ test_count += 1
120
+ end
121
+ end
122
+ test_count
123
+ end
124
+
125
+ # Handles when there is a error that makes it impossible to run
126
+ # @return [TestFailure]
127
+ def critical_inconclusive
128
+ super
129
+ @children.each do |child|
130
+ child.critical_inconclusive
131
+ @test_neutral_count += 1
132
+ end
133
+ end
134
+
135
+ private
136
+
137
+ # Notifies the children that they have a parent
138
+ # @return [void]
139
+ def assign_children
140
+ @children.each do |child|
141
+ if child.respond_to?(:assign_parent)
142
+ child.assign_parent(@options)
143
+ else
144
+ # Someone tried to add something that isn't a child into their test suite
145
+ throw 'Only objects like Group or Test can be children of another node.'
146
+ end
147
+ end
148
+ end
149
+
150
+ # @param filter_settings [FilterSettings]
151
+ # @return [Boolean]
152
+ def should_run_filter_on_children(filter_settings)
153
+ @filter_applies_to_children = true
154
+ @children.any? { |child| child.should_run_with_filter(filter_settings) }
155
+ end
156
+
157
+ # @param filter_settings [FilterSettings]
158
+ # @return [TestResult]
159
+ def get_result_children(filter_settings)
160
+ child_success_count = 0
161
+ child_fail_count = 0
162
+ @children.each do |child|
163
+ result = get_result_child(filter_settings, child)
164
+ next if result.nil?
165
+
166
+ child_success_count += result.successes
167
+ child_fail_count += result.failures
168
+
169
+ next unless child.instance_of? Test
170
+
171
+ @test_success_count += result.successes
172
+ @test_fail_count += result.failures
173
+ @test_neutral_count += result.neutrals
174
+ end
175
+
176
+ return TestFailure.new(@title, 'Some tests failed') if child_fail_count.positive?
177
+ return TestNeutral.new(@title) if child_success_count.zero?
178
+
179
+ TestPass.new(@title)
180
+ end
181
+
182
+ # @param filter_settings [FilterSettings]
183
+ # @param child [Node]
184
+ # @return [TestResult, nil]
185
+ def get_result_child(filter_settings, child)
186
+ return if @filter_applies_to_children && !child.should_run_with_filter(filter_settings)
187
+
188
+ child.run(@context.dup, filter_settings)
189
+ end
190
+ end
191
+ end