lesli_testing 1.4.1 → 1.4.2

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.
@@ -30,34 +30,37 @@ Building a better future, one line of code at a time.
30
30
  // ·
31
31
  =end
32
32
 
33
+ require_relative "../environment"
34
+
33
35
  module LesliTesting
34
36
  module Reporters
35
- class CliReporter < Minitest::StatisticsReporter
37
+ # Inheriting from SummaryReporter is important even though LesliTesting
38
+ # renders its own summary. Minitest uses this type to detect a filtered
39
+ # run in which no tests matched and return a non-zero exit status.
40
+ class CliReporter < Minitest::SummaryReporter
36
41
  def start
37
- super
38
-
39
- Termline.br
40
-
41
- Termline.m(
42
- Termline::Style.colorize("Running Lesli tests...", :blue),
43
- Termline::Style.colorize("-> For a better result run test over a clean database", :blue),
44
- Termline::Style.colorize("-> You can use: rake dev:db:reset", :blue)
45
- )
42
+ run_statistics_reporter(:start)
46
43
 
47
- Termline.br 2
44
+ io.puts
45
+ header_messages.each do |message|
46
+ io.puts Termline::Style.colorize(message, :blue)
47
+ end
48
+ io.puts
49
+ io.puts
48
50
  end
49
51
 
50
52
  def record(test)
51
53
  super
52
54
 
53
55
  # Only print the line if the configuration allows it
54
- return if ENV["QUIET"]
56
+ return if LesliTesting::Environment.enabled?("QUIET")
55
57
 
56
58
  # Map result codes to styles
57
- status_map = {
58
- "." => [:green, "PASS"],
59
- "S" => [:yellow, "SKIP"],
60
- "E" => [:red, "ERROR"],
59
+ status_map = {
60
+ "." => [:green, "PASS"],
61
+ "S" => [:yellow, "SKIP"],
62
+ "W" => [:yellow, "WARN"],
63
+ "E" => [:red, "ERROR"],
61
64
  "F" => [:red, "FAIL"]
62
65
  }
63
66
  color, label = status_map[test.result_code] || [:gray, "????"]
@@ -82,85 +85,192 @@ module LesliTesting
82
85
  # Add execution time per test
83
86
  parts << Termline::Style.colorize("(%.2fs)" % test.time, :red) if test.time > 1
84
87
 
85
- puts(parts.join(" "))
88
+ io.puts(parts.join(" "))
86
89
  end
87
90
 
88
91
  def report
89
- super
92
+ run_statistics_reporter(:report)
90
93
 
91
94
  total_tests = count.to_i
92
95
  total_failures = failures.to_i
93
96
  total_errors = errors.to_i
97
+ total_warnings = warnings.to_i
94
98
  total_skips = skips.to_i
95
99
  total_assertions = assertions.to_i
96
100
  duration = total_time.to_f
97
101
 
98
- passed = total_tests - total_failures - total_errors - total_skips
102
+ passed = total_tests - total_failures - total_errors - total_warnings - total_skips
99
103
  success_rate = total_tests.zero? ? 0 : ((passed.to_f / total_tests) * 100).round(1)
100
- status = (total_failures.zero? && total_errors.zero?) ? "PASS" : "FAIL"
104
+ empty_filtered_run = total_tests.zero? && options[:include]
105
+ status = if empty_filtered_run || total_failures.positive? || total_errors.positive? || total_warnings.positive?
106
+ "FAIL"
107
+ else
108
+ "PASS"
109
+ end
101
110
 
102
- Termline.br 2
111
+ io.puts
112
+ io.puts
113
+
114
+ status_color = status == "FAIL" ? :red : :blue
115
+ failures_color = total_failures.positive? ? :red : :blue
116
+ errors_color = total_errors.positive? ? :red : :blue
117
+ warnings_color = total_warnings.positive? ? :yellow : :blue
118
+ success_rate_color = status == "FAIL" ? :red : :blue
119
+
120
+ write_summary_row(status, tag: "STATUS . . . . . . . :", color: status_color)
121
+ write_summary_row(total_tests, tag: "TESTS. . . . . . . . :")
122
+ write_summary_row(passed, tag: "PASSED . . . . . . . :")
123
+ write_summary_row(total_failures, tag: "FAILURES . . . . . . :", color: failures_color)
124
+ write_summary_row(total_errors, tag: "ERRORS . . . . . . . :", color: errors_color)
125
+ write_summary_row(total_warnings, tag: "WARNINGS . . . . . . :", color: warnings_color)
126
+ write_summary_row(total_skips, tag: "SKIPPED. . . . . . . :")
127
+ write_summary_row(total_assertions, tag: "ASSERTIONS . . . . . :")
128
+ write_summary_row("#{success_rate}%", tag: "SUCCESS RATE . . . . :", color: success_rate_color)
129
+ write_summary_row(format("%.2fs", duration), tag: "TIME . . . . . . . . :")
130
+
131
+ io.puts
132
+ io.puts
133
+
134
+ if empty_filtered_run
135
+ write_status_message(
136
+ "No tests matched filter: #{options[:include]}",
137
+ tag: "FAILURE",
138
+ color: :red,
139
+ icon: :error
140
+ )
141
+ elsif status == "PASS"
142
+ write_status_message("Test suite passed", tag: "SUCCESS:", color: :green, icon: :success)
143
+ else
144
+ write_status_message("Test suite failed", tag: "FAILURE", color: :red, icon: :error)
145
+ end
103
146
 
104
- status_color = :blue
105
- status_color = :red if status == 'FAIL'
147
+ io.puts
148
+ io.puts
106
149
 
107
- failures_color = :blue
108
- failures_color = :red if total_failures > 0
150
+ print_result_details if total_failures.positive? || total_errors.positive? || total_warnings.positive? || total_skips.positive?
151
+
152
+ end
109
153
 
110
- puts(Termline::Msg.builder(status, tag:'STATUS . . . . . . . :', color: status_color, timestamp:nil))
111
- puts(Termline::Msg.builder(total_tests, tag:'TESTS. . . . . . . . :', color: :blue, timestamp:nil))
112
- puts(Termline::Msg.builder(passed, tag:'PASSED . . . . . . . :', color: :blue, timestamp:nil))
113
- puts(Termline::Msg.builder(total_failures, tag:'FAILURES . . . . . . :', color: failures_color, timestamp:nil))
114
- puts(Termline::Msg.builder(total_errors, tag:'ERRORS . . . . . . . :', color: :blue, timestamp:nil))
115
- puts(Termline::Msg.builder(total_skips, tag:'SKIPPED. . . . . . . :', color: :blue, timestamp:nil))
116
- puts(Termline::Msg.builder(total_assertions, tag:'ASSERTIONS . . . . . :', color: :blue, timestamp:nil))
117
- puts(Termline::Msg.builder("#{success_rate}%", tag:'SUCCESS RATE . . . . :', color: failures_color, timestamp:nil))
118
- puts(Termline::Msg.builder(format("%.2fs", duration),tag:'TIME . . . . . . . . :', color: :blue, timestamp:nil))
154
+ private
155
+
156
+ # Skip SummaryReporter's default terminal output while reusing the
157
+ # statistics lifecycle implemented by its parent class.
158
+ def run_statistics_reporter(method_name)
159
+ Minitest::StatisticsReporter
160
+ .instance_method(method_name)
161
+ .bind(self)
162
+ .call
163
+ end
119
164
 
120
- Termline.br 2
165
+ def write_summary_row(value, tag:, color: :blue)
166
+ io.puts Termline::Msg.builder(value, tag: tag, color: color, time: false)
167
+ end
121
168
 
122
- if status == "PASS"
123
- Termline.success("Test suite passed")
169
+ def header_messages
170
+ project_name = options[:lesli_testing_project_name]
171
+
172
+ case options[:lesli_testing_profile].to_s
173
+ when "app"
174
+ [
175
+ header_title(project_name, "Rails application"),
176
+ "-> For a better result run tests over a clean database",
177
+ "-> You can use: rake dev:db:rebuild"
178
+ ]
179
+ when "engine"
180
+ [
181
+ header_title(project_name, "Rails engine"),
182
+ "-> Tests run through the engine's Rails test application"
183
+ ]
184
+ when "gem"
185
+ [header_title(project_name, "Ruby gem")]
124
186
  else
125
- Termline.danger("Test suite failed", tag:'FAILURE')
187
+ ["Running Lesli tests..."]
126
188
  end
189
+ end
127
190
 
128
- Termline.br 2
129
-
130
- print_failure_details if total_failures.positive? || total_errors.positive?
191
+ def header_title(project_name, project_type)
192
+ name = project_name.to_s.strip
193
+ return "Running tests for this #{project_type}..." if name.empty?
131
194
 
195
+ "Running tests for #{name} (#{project_type})..."
132
196
  end
133
197
 
134
- private
198
+ def write_status_message(message, tag:, color:, icon:)
199
+ io.puts Termline::Msg.builder(message, tag: tag, icon: icon, color: color, time: true)
200
+ end
135
201
 
136
- def print_failure_details
137
- failed_results = Array(results).select { |result| result.failure }
202
+ def print_result_details
203
+ failed_results = Array(results).select(&:failure)
138
204
 
139
205
  return if failed_results.empty?
140
206
 
141
207
  failed_results.each_with_index do |result, index|
142
208
  failure = result.failure
143
- failure_tag = failure.is_a?(Minitest::Assertion) ? "FAILURE" : "ERROR"
144
- failure_msg = "#{result.class}##{result.name} (#{result.assertions} asserts)"
209
+ failure_tag, color, icon = result_style(failure)
210
+ failure_msg = "#{result.klass}##{result.name} (#{result.assertions} asserts)"
211
+
212
+ io.puts
213
+ io.puts Termline::Space.builder("-·- ", 6)
214
+ io.puts
215
+
216
+ io.puts Termline::Msg.builder(
217
+ failure_msg,
218
+ tag: " #{Termline::Style.icon(icon)} #{failure_tag} #{index + 1}:",
219
+ color: color,
220
+ time: false
221
+ )
222
+ io.puts Termline::Msg.builder(
223
+ format_failure_location(failure),
224
+ tag: " #{Termline::Style.icon(:debug)} Location:",
225
+ color: :yellow,
226
+ time: false
227
+ )
228
+
229
+ print_result_message(failure)
230
+ end
145
231
 
146
- file, line = result.source_location
147
- location_file = file ? "#{file} (line: #{line})" : "Location unknown"
232
+ io.puts
233
+ end
148
234
 
149
- Termline.br
150
- Termline.line(6)
151
- Termline.br
235
+ def result_style(failure)
236
+ case failure
237
+ when Minitest::Skip
238
+ ["SKIP", :yellow, :warning]
239
+ when Minitest::UnexpectedWarning
240
+ ["WARNING", :yellow, :warning]
241
+ when Minitest::UnexpectedError
242
+ ["ERROR", :red, :error]
243
+ else
244
+ ["FAILURE", :red, :error]
245
+ end
246
+ end
152
247
 
153
- puts Termline::Msg.builder(failure_msg, tag: " #{Termline::Style.icon(:error)} #{failure_tag} #{index + 1}:", color: :red, timestamp:nil)
154
- puts Termline::Msg.builder(location_file, tag: " #{Termline::Style.icon(:debug)} Location:", color: :yellow, timestamp:nil)
248
+ def format_failure_location(failure)
249
+ location = failure.location.to_s
250
+ return "Location unknown" if location.empty? || location == "unknown:-1"
155
251
 
156
- if failure.is_a?(Minitest::Assertion)
157
- result.failure.message.to_s.lines.each do |message|
158
- puts(parse_minitest_assertion_messages(message))
159
- end
252
+ match = location.match(/\A(.+):(\d+)\z/)
253
+ return location unless match
254
+
255
+ "#{match[1]} (line: #{match[2]})"
256
+ end
257
+
258
+ def print_result_message(failure)
259
+ if failure.is_a?(Minitest::Skip)
260
+ io.puts Termline::Style.colorize(
261
+ " #{Termline::Style.icon(:warning)} Reason: #{failure.message}",
262
+ :yellow
263
+ )
264
+ elsif failure.is_a?(Minitest::UnexpectedWarning)
265
+ io.puts Termline::Style.colorize(
266
+ " #{Termline::Style.icon(:warning)} #{failure.message}",
267
+ :yellow
268
+ )
269
+ else
270
+ failure.message.to_s.lines.each do |message|
271
+ io.puts parse_minitest_assertion_messages(message)
160
272
  end
161
273
  end
162
-
163
- Termline.br
164
274
  end
165
275
 
166
276
  def parse_minitest_assertion_messages(message)
@@ -175,10 +285,10 @@ module LesliTesting
175
285
  # +++ actual
176
286
  # @@ -1 +1 @@
177
287
  when /^--- /
178
- Termline::Style.colorize("#{msg}:", :red)
288
+ Termline::Style.colorize(msg, :red)
179
289
 
180
290
  when /^\+\+\+ /
181
- Termline::Style.colorize("++#{msg}:", :green)
291
+ Termline::Style.colorize(msg, :green)
182
292
 
183
293
  when /^@@ /
184
294
  Termline::Style.colorize("#{msg}", :skyblue)
@@ -189,10 +299,10 @@ module LesliTesting
189
299
  # +[:lesli_support_items_tasks, "lesli_support"]
190
300
  # The negative lookahead avoids matching the diff headers above.
191
301
  when /^-(?!-)/
192
- Termline::Style.colorize(" #{Termline::Style.icon(:remove)}-#{msg}", :red)
302
+ Termline::Style.colorize(" #{Termline::Style.icon(:remove)} #{msg}", :red)
193
303
 
194
304
  when /^\+(?!\+)/
195
- Termline::Style.colorize(" #{Termline::Style.icon(:add)}+#{msg}", :green)
305
+ Termline::Style.colorize(" #{Termline::Style.icon(:add)} #{msg}", :green)
196
306
 
197
307
  # 3. Boolean/Nil Failures
198
308
  # Covers cases like:
@@ -206,10 +316,10 @@ module LesliTesting
206
316
  # Covers simple assertion output like:
207
317
  # Expected: 1
208
318
  # Actual: 2
209
- when /^Expected:?\s+/
319
+ when /^Expected:\s+/
210
320
  Termline::Style.colorize(" #{Termline::Style.icon(:add)} #{msg}", :green)
211
321
 
212
- when /^Actual:?\s+/
322
+ when /^Actual:\s+/
213
323
  Termline::Style.colorize(" #{Termline::Style.icon(:remove)} #{msg}", :red)
214
324
 
215
325
  # 5. Rails Difference Failures
@@ -224,12 +334,12 @@ module LesliTesting
224
334
  # Expected 5 elements, found 2
225
335
  # Expected exactly 1 nodes
226
336
  when /Expected (.*) elements?, found (.*)/, /Expected exactly (.*) nodes/
227
- Termline::Style.colorize(" #{Termline::Style.icon(:list)} #{msg}", :magenta)
337
+ Termline::Style.colorize(" #{Termline::Style.icon(:info)} #{msg}", :blue)
228
338
 
229
339
  # 7. Fallback for Custom or Unrecognized Messages
230
340
  # Covers anything else not matched above.
231
341
  else
232
- Termline::Style.colorize(" #{Termline::Style.icon(:arrow_right)} #{msg}", :gray)
342
+ Termline::Style.colorize(" #{Termline::Style.icon(:debug)} #{msg}", :gray)
233
343
  end
234
344
  end.join("\n")
235
345
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/lazy_load_hooks"
4
+ require_relative "testers"
5
+ require_relative "fixtures"
6
+
7
+ module LesliTesting
8
+ module RailsIntegration
9
+ class << self
10
+ def install!(profile:, project_name:, engine_target: nil, options:)
11
+ LesliTesting::Fixtures.install!(
12
+ profile: profile,
13
+ engine_name: engine_target || project_name,
14
+ options: options
15
+ )
16
+ install_engine!(engine_target || project_name) if profile == "engine"
17
+ LesliTesting::Testers.install!
18
+ end
19
+
20
+ private
21
+
22
+ def install_engine!(engine_target)
23
+ require "rails"
24
+ require "rails/engine"
25
+ require_relative "engine"
26
+
27
+ LesliTesting::Engine.configure(engine_target: engine_target)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -31,35 +31,83 @@ Building a better future, one line of code at a time.
31
31
  =end
32
32
 
33
33
 
34
- #
35
- require_relative "helpers/response_integration_helper"
36
-
37
- #
38
34
  module LesliTesting
35
+ module Testers
36
+ class << self
37
+ def install!
38
+ return if @installed
39
39
 
40
- if defined?(ActionDispatch::IntegrationTest)
41
- class IntegrationTester < ActionDispatch::IntegrationTest
42
- include ResponseIntegrationHelper
43
- end
44
- end
40
+ ActiveSupport.on_load(
41
+ :action_dispatch_integration_test,
42
+ run_once: true,
43
+ yield: true
44
+ ) do |test_case|
45
+ define_integration_tester(test_case)
46
+ end
45
47
 
46
- if defined?(ActionView::TestCase)
47
- class ViewTester < ActionView::TestCase
48
- # We use a hook to include the helpers only when the class is used.
49
- # This ensures Lesli core is fully loaded by Rails before we ask for the helpers.
50
- def self.inherited(subclass)
51
- super
52
- subclass.class_eval do
53
- include Lesli::HtmlHelper if defined?(Lesli::HtmlHelper)
54
- include Lesli::SystemHelper if defined?(Lesli::SystemHelper)
48
+ ActiveSupport.on_load(
49
+ :action_view_test_case,
50
+ run_once: true,
51
+ yield: true
52
+ ) do |test_case|
53
+ define_view_tester(test_case)
55
54
  end
55
+
56
+ ActiveSupport.on_load(
57
+ :active_support_test_case,
58
+ run_once: true,
59
+ yield: true
60
+ ) do |test_case|
61
+ define_model_tester(test_case)
62
+ load_rails_test_cases
63
+ end
64
+
65
+ @installed = true
56
66
  end
57
- end
58
- end
59
67
 
60
- if defined?(ActiveSupport::TestCase)
61
- class ModelTester < ActiveSupport::TestCase
62
- include ActiveSupport::Testing::TimeHelpers
68
+ private
69
+
70
+ def define_integration_tester(test_case)
71
+ return if LesliTesting.const_defined?(:IntegrationTester, false)
72
+
73
+ require_relative "helpers/response_integration_helper"
74
+
75
+ tester = Class.new(test_case) do
76
+ include LesliTesting::ResponseIntegrationHelper
77
+ end
78
+ LesliTesting.const_set(:IntegrationTester, tester)
79
+ end
80
+
81
+ def define_view_tester(test_case)
82
+ return if LesliTesting.const_defined?(:ViewTester, false)
83
+
84
+ tester = Class.new(test_case) do
85
+ def self.inherited(subclass)
86
+ super
87
+ subclass.class_eval do
88
+ include Lesli::HtmlHelper if defined?(Lesli::HtmlHelper)
89
+ include Lesli::SystemHelper if defined?(Lesli::SystemHelper)
90
+ end
91
+ end
92
+ end
93
+ LesliTesting.const_set(:ViewTester, tester)
94
+ end
95
+
96
+ def define_model_tester(test_case)
97
+ return if LesliTesting.const_defined?(:ModelTester, false)
98
+
99
+ require "active_support/testing/time_helpers"
100
+
101
+ tester = Class.new(test_case) do
102
+ include ActiveSupport::Testing::TimeHelpers
103
+ end
104
+ LesliTesting.const_set(:ModelTester, tester)
105
+ end
106
+
107
+ def load_rails_test_cases
108
+ require "action_dispatch/testing/integration"
109
+ require "action_view/test_case"
110
+ end
63
111
  end
64
112
  end
65
113
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LesliTesting
4
- VERSION = "1.4.1"
4
+ VERSION = "1.4.2"
5
5
  BUILD = "1791607001"
6
6
  end
data/lib/lesli_testing.rb CHANGED
@@ -33,101 +33,17 @@ Building a better future, one line of code at a time.
33
33
  =end
34
34
 
35
35
 
36
- require_relative "lesli_testing/version"
37
-
38
- begin
39
- require "rails/engine"
40
- require_relative "lesli_testing/engine"
41
- rescue LoadError
42
- # Rails is optional when LesliTesting is used by a standalone Ruby gem.
36
+ # Coverage must begin before Ruby compiles the LesliTesting implementation,
37
+ # Rails, or the project under test. The selected SimpleCov profile and
38
+ # formatters are applied later by LesliTesting.app, LesliTesting.engine, or
39
+ # LesliTesting.gem.
40
+ # simplecov:disable
41
+ require_relative "lesli_testing/environment"
42
+
43
+ if LesliTesting::Environment.enabled?("COVERAGE")
44
+ require "coverage"
45
+ Coverage.start(lines: true) unless Coverage.running?
43
46
  end
44
47
 
45
-
46
- module LesliTesting
47
- class Error < StandardError; end
48
-
49
- class << self
50
-
51
- attr_accessor :app_module;
52
- attr_accessor :app_name;
53
- attr_writer :options
54
-
55
- def app(name, options = {})
56
- configure(name, options.merge(coverage_profile: "app"))
57
- end
58
-
59
- def engine(name, options = {})
60
- configure(name, options.merge(coverage_profile: "engine"))
61
- end
62
-
63
- def gem(name, options = {})
64
- configure(name, options.merge(coverage_profile: "gem"))
65
- end
66
-
67
- private
68
-
69
- def options
70
- @options ||= options_default
71
- end
72
-
73
- def options_default
74
- {
75
- coverage_profile: "app",
76
- coverage_missing_len: 25,
77
- coverage_min_coverage: 90
78
- }
79
- end
80
-
81
- def configure(name, options)
82
- self.app_name = name
83
- self.options = options_default.merge(options)
84
- configure_gems()
85
- configure_reporter()
86
- configure_coverage()
87
- configure_fixtures()
88
- end
89
-
90
- def configure_coverage()
91
-
92
- return unless ENV["COVERAGE"]
93
- return if defined?(SimpleCov) && SimpleCov.running
94
-
95
- # Start Coverage
96
- LesliTesting::Coverage.start(
97
- self.app_name,
98
- profile: self.options[:coverage_profile],
99
- missing_len: self.options[:coverage_missing_len],
100
- min_coverage: self.options[:coverage_min_coverage]
101
- )
102
- end
103
-
104
- # Load test configuration and test helper modules
105
- def configure_fixtures()
106
- LesliTesting::Fixtures.load_fixtures_for_lesli()
107
- end
108
-
109
- def configure_reporter
110
- # Force Minitest to know about Lesli Minitest reporter plugin
111
- unless Minitest.extensions.include?("lesli_testing")
112
- Minitest.register_plugin("lesli_testing")
113
- end
114
- end
115
-
116
- def configure_gems
117
- return if @tools_loaded
118
-
119
- require "minitest"
120
-
121
- # Load minitest plugin
122
- require "minitest/lesli_testing_plugin"
123
- require "termline" unless defined?(Termline)
124
-
125
- # Load helper files
126
- require_relative "lesli_testing/testers"
127
- require_relative "lesli_testing/fixtures"
128
- require_relative "lesli_testing/coverage"
129
-
130
- @tools_loaded = true
131
- end
132
- end
133
- end
48
+ require_relative "lesli_testing/core"
49
+ # simplecov:enable
@@ -7,6 +7,10 @@ module Minitest
7
7
 
8
8
  def self.plugin_lesli_testing_init(options)
9
9
  reporter.reporters.clear if reporter.respond_to?(:reporters)
10
- reporter << LesliTesting::Reporters::CliReporter.new($stdout, options)
10
+ reporter_options = options.merge(
11
+ lesli_testing_profile: LesliTesting.profile,
12
+ lesli_testing_project_name: LesliTesting.app_name
13
+ )
14
+ reporter << LesliTesting::Reporters::CliReporter.new(options[:io] || $stdout, reporter_options)
11
15
  end
12
16
  end