attestify 0.1.0.pre.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,62 @@
1
+ module Attestify
2
+ module Assertions
3
+ # A helper class for Attestify::Assertions#assert_output.
4
+ class OutputAssertion
5
+ def initialize(expected_stdout, expected_stderr, stdout, stderr, message)
6
+ @expected_stdout = expected_stdout
7
+ @expected_stderr = expected_stderr
8
+ @stdout = stdout
9
+ @stderr = stderr
10
+ @message = message
11
+ end
12
+
13
+ def assert
14
+ assert_stdout && assert_stderr
15
+ end
16
+
17
+ def message
18
+ return @message if @message
19
+ messages = [stdout_message, stderr_message]
20
+ "Expected #{messages.compact.join(", and ")}"
21
+ end
22
+
23
+ private
24
+
25
+ def assert_stdout
26
+ assert_output(@expected_stdout, @stdout)
27
+ end
28
+
29
+ def assert_stderr
30
+ assert_output(@expected_stderr, @stderr)
31
+ end
32
+
33
+ def assert_output(expected, actual)
34
+ return true unless expected
35
+
36
+ if expected.is_a?(String)
37
+ expected == actual
38
+ else
39
+ actual =~ expected
40
+ end
41
+ end
42
+
43
+ def stdout_message
44
+ output_message("$stdout", @expected_stdout, @stdout)
45
+ end
46
+
47
+ def stderr_message
48
+ output_message("$stderr", @expected_stderr, @stderr)
49
+ end
50
+
51
+ def output_message(label, expected, actual)
52
+ return nil unless expected
53
+
54
+ if expected.is_a?(String)
55
+ "#{label}: #{expected.inspect} == #{actual.inspect}"
56
+ else
57
+ "#{label}: #{actual.inspect} =~ #{expected.inspect}"
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,84 @@
1
+ require "attestify"
2
+ require "optparse"
3
+
4
+ module Attestify
5
+ # Command Line Interface for running Attestify tests.
6
+ class CLI
7
+ def initialize(args = ARGV)
8
+ @args = args
9
+ @exit_code = true
10
+ end
11
+
12
+ def self.start
13
+ new.start
14
+ end
15
+
16
+ def test_list
17
+ @test_list ||= Attestify::TestList.new(@args, dir: options[:directory])
18
+ end
19
+
20
+ def reporter
21
+ @reporter ||=
22
+ if options[:color]
23
+ Attestify::ColorReporter.new
24
+ else
25
+ Attestify::Reporter.new
26
+ end
27
+ end
28
+
29
+ def options
30
+ @options ||= {
31
+ color: true
32
+ }
33
+ end
34
+
35
+ def option_parser # rubocop:disable Metrics/MethodLength
36
+ @option_parser ||= OptionParser.new do |opts|
37
+ opts.banner = "Usage: attestify [options] [test_files ...]"
38
+
39
+ opts.on("-c", "--color", "Run with color") do
40
+ options[:color] = true
41
+ end
42
+
43
+ opts.on("-C", "--no-color", "Run without color") do
44
+ options[:color] = false
45
+ end
46
+
47
+ opts.on("-d", "--directory [DIR]", "Run the tests in the provided DIR") do |dir|
48
+ options[:directory] = dir
49
+ end
50
+
51
+ opts.on("-h", "--help", "Output this help") do
52
+ puts opts
53
+ ignore_reporting
54
+ exit
55
+ end
56
+ end
57
+ end
58
+
59
+ def ignore_reporting
60
+ @ignore_reporting = true
61
+ end
62
+
63
+ def parse_arguments
64
+ option_parser.parse!(@args)
65
+ end
66
+
67
+ def start
68
+ timer = Attestify::Timer.time { run }
69
+ rescue => e
70
+ @exit_code = 2
71
+ STDERR.puts("Error running tests: #{e}\n #{e.backtrace.join("\n ")}")
72
+ ensure
73
+ reporter.timer = timer
74
+ reporter.report unless @ignore_reporting
75
+ exit(@exit_code)
76
+ end
77
+
78
+ def run
79
+ parse_arguments
80
+ Attestify::TestRunner.new(test_list, reporter).run
81
+ @exit_code = 1 unless reporter.passed?
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,127 @@
1
+ require "attestify"
2
+
3
+ module Attestify
4
+ # Reports results to the console, with color!
5
+ class ColorReporter < Attestify::Reporter # rubocop:disable Metrics/ClassLength
6
+ private
7
+
8
+ def puts_failure_header(result, number)
9
+ print color_code(color_for(result))
10
+ super
11
+ print color_code(:reset)
12
+ end
13
+
14
+ def puts_failure_detail(failure_detail, number, sub_number)
15
+ print color_code(color_for_detail(failure_detail))
16
+ super
17
+ print color_code(:reset)
18
+ end
19
+
20
+ def print_result_code(result)
21
+ print color_code(color_for(result))
22
+ super
23
+ print color_code(:reset)
24
+ end
25
+
26
+ def total_tests
27
+ colorize_from_totals(super)
28
+ end
29
+
30
+ def total_failures
31
+ colorize_if_positive(super, @total_failures, :red)
32
+ end
33
+
34
+ def total_errors
35
+ colorize_if_positive(super, @total_errors, :bold_red)
36
+ end
37
+
38
+ def total_skips
39
+ colorize_if_positive(super, @total_skips, :yellow)
40
+ end
41
+
42
+ def total_assertions
43
+ colorize_from_totals(super)
44
+ end
45
+
46
+ def total_failed_assertions
47
+ colorize_if_positive(super, @total_failed_assertions, :red)
48
+ end
49
+
50
+ def rerun_test_command(result)
51
+ colorize(super, color_for(result))
52
+ end
53
+
54
+ def comment(message)
55
+ colorize(super, :cyan)
56
+ end
57
+
58
+ def colorize_from_totals(text) # rubocop:disable Metrics/MethodLength
59
+ color =
60
+ if @total_errors > 0
61
+ :bold_red
62
+ elsif @total_failures > 0
63
+ :red
64
+ elsif @total_skips > 0
65
+ :yellow
66
+ else
67
+ :green
68
+ end
69
+
70
+ colorize(text, color)
71
+ end
72
+
73
+ def colorize_if_positive(text, amount, color)
74
+ if amount > 0
75
+ colorize(text, color)
76
+ else
77
+ text
78
+ end
79
+ end
80
+
81
+ def colorize(text, color)
82
+ "#{color_code(color)}#{text}#{color_code(:reset)}"
83
+ end
84
+
85
+ def color_code(color) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
86
+ case color
87
+ when :reset
88
+ "\e[0m"
89
+ when :bold_red
90
+ "\e[1;31m"
91
+ when :red
92
+ "\e[31m"
93
+ when :yellow
94
+ "\e[33m"
95
+ when :green
96
+ "\e[32m"
97
+ when :cyan
98
+ "\e[36m"
99
+ end
100
+ end
101
+
102
+ def color_for(result) # rubocop:disable Metrics/MethodLength
103
+ if result.skipped?
104
+ :yellow
105
+ elsif result.passed?
106
+ :green
107
+ elsif result.errored?
108
+ :bold_red
109
+ elsif result.failed?
110
+ :red
111
+ else
112
+ :none
113
+ end
114
+ end
115
+
116
+ def color_for_detail(failure_detail)
117
+ case failure_detail.type
118
+ when :error
119
+ :bold_red
120
+ when :failure
121
+ :red
122
+ else
123
+ :none
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,118 @@
1
+ module Attestify
2
+ # Simple class for mocking objects.
3
+ class Mock
4
+ def initialize(test_or_assertions)
5
+ @assertions = test_or_assertions
6
+ @assertions = test_or_assertions.assertions if test_or_assertions.respond_to?(:assertions)
7
+ @expectations_hash = Hash.new { |hash, key| hash[key] = [] }
8
+ @expectations = []
9
+ @called_expectations = []
10
+ end
11
+
12
+ def expect(name, return_value, args = [], &block)
13
+ name = name.to_sym
14
+ expectation = Attestify::Mock::ExpectedCall.new(name, return_value, args, block)
15
+ @expectations << expectation
16
+ @expectations_hash[name.to_sym] << expectation
17
+ self
18
+ end
19
+
20
+ def verify
21
+ @called_expectations.each { |x| x.verify(@assertions) }
22
+ @expectations.reject(&:called?).each { |x| x.verify(@assertions) }
23
+ end
24
+
25
+ def method_missing(method, *args, &block)
26
+ expectation =
27
+ if @expectations_hash[method].empty?
28
+ UnexpectedCall.new(method, args, block)
29
+ else
30
+ @expectations_hash[method].shift
31
+ end
32
+
33
+ @called_expectations << expectation
34
+ expectation.call(args, block)
35
+ end
36
+
37
+ # A base class for both ExpectedCall and UnexpectedCall.
38
+ class CallExpectation
39
+ attr_reader :name, :return_value, :args, :block, :actual_args, :actual_block
40
+
41
+ def initialize(name, return_value, args, block)
42
+ @called = false
43
+ @name = name
44
+ @return_value = return_value
45
+ @args = args
46
+ @block = block
47
+ end
48
+
49
+ def called?
50
+ @called
51
+ end
52
+
53
+ def call(actual_args, actual_block)
54
+ @called = true
55
+ @actual_args = actual_args
56
+ @actual_block = actual_block
57
+ block.call(*actual_args, &actual_block) if block
58
+ return_value
59
+ end
60
+
61
+ def to_s(style = :expected)
62
+ if style == :expected
63
+ "#{name}(#{args.map(&:inspect).join(", ")})"
64
+ else
65
+ with_block = " { ... }" if actual_block
66
+ "#{name}(#{actual_args.map(&:inspect).join(", ")})#{with_block}"
67
+ end
68
+ end
69
+ end
70
+
71
+ # Contains a mock's method call expectation.
72
+ class ExpectedCall < CallExpectation
73
+ def call(args, block)
74
+ result = super
75
+ @caller_locations = caller_locations(2) unless arguments_valid?
76
+ result
77
+ end
78
+
79
+ def verify(assertions)
80
+ if !called?
81
+ assertions.record(false, "Missing expected call to mock: #{self}", @caller_locations)
82
+ elsif !arguments_valid?
83
+ assertions.record(false, "Expected call to mock: #{self}, but got: #{to_s(:actual)}", @caller_locations)
84
+ else
85
+ assertions.record(true)
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ def arguments_valid?
92
+ return false unless args.size == actual_args.size
93
+
94
+ args.each_with_index do |arg, i|
95
+ return false unless arg === actual_args[i] # rubocop:disable Style/CaseEquality
96
+ end
97
+
98
+ true
99
+ end
100
+ end
101
+
102
+ # A method call that wasn't expected.
103
+ class UnexpectedCall < CallExpectation
104
+ def initialize(name, args, block)
105
+ super(name, nil, args, block)
106
+ end
107
+
108
+ def call(args, block)
109
+ @caller_locations = caller_locations(2)
110
+ super
111
+ end
112
+
113
+ def verify(assertions)
114
+ assertions.record(false, "Unexpected call to mock: #{to_s(:actual)}", @caller_locations)
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,29 @@
1
+ require "rake"
2
+ require "rake/tasklib"
3
+
4
+ module Attestify
5
+ # Rake task to run Attestify tests.
6
+ class RakeTask < Rake::TaskLib
7
+ attr_reader :name
8
+
9
+ def initialize(*args, &block)
10
+ @name = args.shift || :test
11
+ define(args, &block)
12
+ end
13
+
14
+ def run_task
15
+ Attestify::CLI.new([]).start
16
+ end
17
+
18
+ private
19
+
20
+ def define(args)
21
+ desc "Run Attestify tests" unless Rake.application.last_description
22
+
23
+ task name, *args do |_, task_args|
24
+ yield(self, task_args) if block_given?
25
+ run_task
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,171 @@
1
+ require "pathname"
2
+
3
+ module Attestify
4
+ # Reports results to the console.
5
+ class Reporter # rubocop:disable Metrics/ClassLength
6
+ attr_accessor :timer
7
+
8
+ def initialize
9
+ @failures = []
10
+ @total_tests = 0
11
+ @total_assertions = 0
12
+ @total_failed_assertions = 0
13
+ @total_failures = 0
14
+ @total_errors = 0
15
+ @total_skips = 0
16
+ end
17
+
18
+ def passed?
19
+ @total_failures + @total_errors == 0
20
+ end
21
+
22
+ def record(result)
23
+ add_to_totals(result)
24
+ @failures << result if !result.skipped? && !result.passed?
25
+ print_result_code(result)
26
+ end
27
+
28
+ def report
29
+ puts_failures
30
+ puts_footer
31
+ end
32
+
33
+ private
34
+
35
+ def add_to_totals(result)
36
+ @total_tests += 1
37
+ @total_assertions += result.assertions.total
38
+ @total_failed_assertions += result.assertions.failed
39
+
40
+ if result.skipped?
41
+ @total_skips += 1
42
+ elsif result.errored?
43
+ @total_errors += 1
44
+ elsif result.failed?
45
+ @total_failures += 1
46
+ end
47
+ end
48
+
49
+ def print_result_code(result)
50
+ print result.result_code
51
+ end
52
+
53
+ def record_failure(result)
54
+ @failures << result
55
+
56
+ if result.assertions.errored?
57
+ @total_errors += 1
58
+ else
59
+ @total_failures += 1
60
+ end
61
+ end
62
+
63
+ def puts_failures
64
+ puts
65
+
66
+ @failures.each_with_index do |failure, i|
67
+ puts_failure(failure, i + 1)
68
+ end
69
+ end
70
+
71
+ def puts_failure(failure, number)
72
+ puts
73
+ puts_failure_header(failure, number)
74
+ puts_failure_details(failure, number)
75
+ end
76
+
77
+ def puts_failure_header(failure, number)
78
+ puts "#{number}) #{failure.name}"
79
+ end
80
+
81
+ def puts_failure_details(failure, number)
82
+ failure.assertions.failure_details.each_with_index do |failure_detail, i|
83
+ puts_failure_detail(failure_detail, number, i + 1)
84
+ end
85
+ end
86
+
87
+ def puts_failure_detail(failure_detail, number, sub_number)
88
+ puts
89
+ puts " #{number}.#{sub_number}) #{failure_detail.message}"
90
+ puts " #{failure_detail.backtrace_locations.join("\n ")}"
91
+ end
92
+
93
+ def puts_footer
94
+ puts
95
+ puts "Finished in #{elapsed_time}, #{tests_per_second}, #{assertions_per_second}"
96
+ puts "#{total_tests}, #{total_failures}, #{total_errors}, #{total_skips}, " \
97
+ "#{total_assertions}, #{total_failed_assertions}"
98
+ puts_failure_reruns
99
+ end
100
+
101
+ def elapsed_time
102
+ timer || "?"
103
+ end
104
+
105
+ def tests_per_second
106
+ if timer
107
+ format("%.1f tests/second", @total_tests.to_f / timer.duration)
108
+ else
109
+ "? tests/second"
110
+ end
111
+ end
112
+
113
+ def assertions_per_second
114
+ if timer
115
+ format("%.1f assertions/second", @total_assertions.to_f / timer.duration)
116
+ else
117
+ "? assertions/second"
118
+ end
119
+ end
120
+
121
+ def total_tests
122
+ "#{@total_tests} tests"
123
+ end
124
+
125
+ def total_failures
126
+ "#{@total_failures} failures"
127
+ end
128
+
129
+ def total_errors
130
+ "#{@total_errors} errors"
131
+ end
132
+
133
+ def total_skips
134
+ "#{@total_skips} skips"
135
+ end
136
+
137
+ def total_assertions
138
+ "#{@total_assertions} assertions"
139
+ end
140
+
141
+ def total_failed_assertions
142
+ "#{@total_failed_assertions} failed assertions"
143
+ end
144
+
145
+ def puts_failure_reruns
146
+ puts
147
+ puts "Failed tests:"
148
+ puts
149
+
150
+ @failures.each do |failure|
151
+ puts_failure_rerun(failure)
152
+ end
153
+ end
154
+
155
+ def puts_failure_rerun(failure)
156
+ puts "#{rerun_test_command(failure)} #{comment(failure.name)}"
157
+ end
158
+
159
+ def rerun_test_command(failure)
160
+ # TODO: Should I create a new method to get the test method...?
161
+ test_method = failure.instance_variable_get(:@_test_method)
162
+ source = failure.method(test_method).source_location
163
+ source[0] = Pathname.new(File.realpath(source[0])).relative_path_from(Pathname.new(File.realpath(".")))
164
+ "attestify #{source.join(":")}"
165
+ end
166
+
167
+ def comment(message)
168
+ "# #{message}"
169
+ end
170
+ end
171
+ end