capybara-screenshot-diff 1.3.0 → 1.5.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.
- checksums.yaml +4 -4
- data/.gitattributes +4 -0
- data/.github/workflows/test.yml +138 -0
- data/.gitignore +1 -1
- data/.standard.yml +11 -0
- data/Dockerfile +60 -0
- data/README.md +87 -3
- data/Rakefile +10 -8
- data/bin/console +3 -3
- data/bin/install-vips +11 -0
- data/bin/standardrb +29 -0
- data/capybara-screenshot-diff.gemspec +18 -26
- data/gemfiles/rails42.gemfile +4 -2
- data/gemfiles/rails50.gemfile +3 -2
- data/gemfiles/rails51.gemfile +3 -2
- data/gemfiles/rails52.gemfile +3 -2
- data/gemfiles/rails60_gems.rb +8 -0
- data/gemfiles/rails61_gems.rb +7 -0
- data/gems.rb +29 -0
- data/lib/capybara/screenshot/diff.rb +24 -14
- data/lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb +355 -0
- data/lib/capybara/screenshot/diff/drivers/utils.rb +24 -0
- data/lib/capybara/screenshot/diff/drivers/vips_driver.rb +180 -0
- data/lib/capybara/screenshot/diff/image_compare.rb +144 -288
- data/lib/capybara/screenshot/diff/os.rb +7 -7
- data/lib/capybara/screenshot/diff/stabilization.rb +89 -37
- data/lib/capybara/screenshot/diff/test_methods.rb +46 -55
- data/lib/capybara/screenshot/diff/vcs.rb +8 -3
- data/lib/capybara/screenshot/diff/version.rb +1 -1
- data/matrix_test.rb +20 -22
- metadata +21 -112
- data/.rubocop.yml +0 -62
- data/.rubocop_todo.yml +0 -52
- data/.travis.yml +0 -30
- data/Gemfile +0 -6
- data/gemfiles/common.gemfile +0 -12
- data/gemfiles/rails60.gemfile +0 -5
@@ -3,16 +3,16 @@
|
|
3
3
|
module Capybara
|
4
4
|
module Screenshot
|
5
5
|
module Os
|
6
|
-
ON_WINDOWS = !!(RbConfig::CONFIG[
|
7
|
-
ON_MAC = !!(RbConfig::CONFIG[
|
8
|
-
ON_LINUX = !!(RbConfig::CONFIG[
|
6
|
+
ON_WINDOWS = !!(RbConfig::CONFIG["host_os"] =~ /mswin|mingw|cygwin/)
|
7
|
+
ON_MAC = !!(RbConfig::CONFIG["host_os"] =~ /darwin/)
|
8
|
+
ON_LINUX = !!(RbConfig::CONFIG["host_os"] =~ /linux/)
|
9
9
|
|
10
10
|
def os_name
|
11
|
-
return
|
12
|
-
return
|
13
|
-
return
|
11
|
+
return "windows" if ON_WINDOWS
|
12
|
+
return "macos" if ON_MAC
|
13
|
+
return "linux" if ON_LINUX
|
14
14
|
|
15
|
-
|
15
|
+
"unknown"
|
16
16
|
end
|
17
17
|
end
|
18
18
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative
|
3
|
+
require_relative "os"
|
4
4
|
|
5
5
|
module Capybara
|
6
6
|
module Screenshot
|
@@ -20,16 +20,23 @@ module Stabilization
|
|
20
20
|
}()
|
21
21
|
JS
|
22
22
|
|
23
|
-
|
24
|
-
|
25
|
-
|
23
|
+
HIDE_CARET_SCRIPT = <<~JS
|
24
|
+
if (!document.getElementById('csdHideCaretStyle')) {
|
25
|
+
let style = document.createElement('style');
|
26
|
+
style.setAttribute('id', 'csdHideCaretStyle');
|
27
|
+
document.head.appendChild(style);
|
28
|
+
let styleSheet = style.sheet;
|
29
|
+
styleSheet.insertRule("* { caret-color: transparent !important; }", 0);
|
30
|
+
}
|
31
|
+
JS
|
32
|
+
|
33
|
+
def take_stable_screenshot(comparison, stability_time_limit:, wait:)
|
26
34
|
previous_file_name = comparison.old_file_name
|
27
35
|
screenshot_started_at = last_image_change_at = Time.now
|
36
|
+
clean_stabilization_images(comparison.new_file_name)
|
37
|
+
|
28
38
|
1.step do |i|
|
29
39
|
take_right_size_screenshot(comparison)
|
30
|
-
|
31
|
-
break unless stability_time_limit
|
32
|
-
|
33
40
|
if comparison.quick_equal?
|
34
41
|
clean_stabilization_images(comparison.new_file_name)
|
35
42
|
break
|
@@ -37,10 +44,11 @@ def take_stable_screenshot(comparison, color_distance_limit:, shift_distance_lim
|
|
37
44
|
comparison.reset
|
38
45
|
|
39
46
|
if previous_file_name
|
40
|
-
stabilization_comparison =
|
41
|
-
|
42
|
-
|
43
|
-
|
47
|
+
stabilization_comparison = make_stabilization_comparison_from(
|
48
|
+
comparison,
|
49
|
+
comparison.new_file_name,
|
50
|
+
previous_file_name
|
51
|
+
)
|
44
52
|
if stabilization_comparison.quick_equal?
|
45
53
|
if (Time.now - last_image_change_at) > stability_time_limit
|
46
54
|
clean_stabilization_images(comparison.new_file_name)
|
@@ -52,37 +60,54 @@ def take_stable_screenshot(comparison, color_distance_limit:, shift_distance_lim
|
|
52
60
|
end
|
53
61
|
end
|
54
62
|
|
55
|
-
previous_file_name = "#{comparison.new_file_name.chomp(
|
63
|
+
previous_file_name = "#{comparison.new_file_name.chomp(".png")}" \
|
64
|
+
"_x#{format("%02i", i)}_#{(Time.now - screenshot_started_at).round(1)}s" \
|
65
|
+
"_#{stabilization_comparison.difference_region&.to_s&.gsub(", ", "_") || :initial}.png~"
|
56
66
|
FileUtils.mv comparison.new_file_name, previous_file_name
|
57
67
|
|
58
|
-
check_max_wait_time(
|
59
|
-
|
68
|
+
check_max_wait_time(
|
69
|
+
comparison,
|
70
|
+
screenshot_started_at,
|
71
|
+
max_wait_time: max_wait_time(comparison.shift_distance_limit, wait)
|
72
|
+
)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def notice_how_to_avoid_this
|
77
|
+
unless @_csd_retina_warned
|
78
|
+
warn "Halving retina screenshot. " \
|
79
|
+
'You should add "force-device-scale-factor=1" to your Chrome chromeOptions args.'
|
80
|
+
@_csd_retina_warned = true
|
60
81
|
end
|
61
|
-
ensure
|
62
|
-
blurred_input&.click
|
63
82
|
end
|
64
83
|
|
65
84
|
private
|
66
85
|
|
67
|
-
def
|
86
|
+
def make_stabilization_comparison_from(comparison, new_file_name, previous_file_name)
|
87
|
+
ImageCompare.new(new_file_name, previous_file_name, **comparison.driver_options)
|
88
|
+
end
|
89
|
+
|
90
|
+
def reduce_retina_image_size(file_name, driver)
|
68
91
|
return if !ON_MAC || !selenium? || !Capybara::Screenshot.window_size
|
69
92
|
|
70
|
-
|
71
|
-
|
72
|
-
return if saved_image
|
93
|
+
expected_image_width = Capybara::Screenshot.window_size[0]
|
94
|
+
saved_image = driver.from_file(file_name)
|
95
|
+
return if driver.width_for(saved_image) < expected_image_width * 2
|
73
96
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
97
|
+
notice_how_to_avoid_this
|
98
|
+
|
99
|
+
new_height = expected_image_width * driver.height_for(saved_image) / driver.width_for(saved_image)
|
100
|
+
resized_image = driver.resize_image_to(saved_image, expected_image_width, new_height)
|
101
|
+
|
102
|
+
Dir.mktmpdir do |dir|
|
103
|
+
resized_image_file = "#{dir}/resized.png"
|
104
|
+
driver.save_image_to(resized_image, resized_image_file)
|
105
|
+
FileUtils.mv(resized_image_file, file_name)
|
78
106
|
end
|
79
|
-
height = (width * saved_image.height) / saved_image.width
|
80
|
-
resized_image = saved_image.resample_bilinear(width, height)
|
81
|
-
resized_image.save(file_name)
|
82
107
|
end
|
83
108
|
|
84
109
|
def stabilization_images(base_file)
|
85
|
-
Dir["#{base_file.chomp(
|
110
|
+
Dir["#{base_file.chomp(".png")}_x*.png~"].sort
|
86
111
|
end
|
87
112
|
|
88
113
|
def clean_stabilization_images(base_file)
|
@@ -102,7 +127,7 @@ def prepare_page_for_screenshot(timeout:)
|
|
102
127
|
JS
|
103
128
|
blurred_input = page.driver.send :unwrap_script_result, active_element
|
104
129
|
end
|
105
|
-
execute_script(
|
130
|
+
execute_script(HIDE_CARET_SCRIPT) if Capybara::Screenshot.hide_caret
|
106
131
|
blurred_input
|
107
132
|
end
|
108
133
|
|
@@ -110,16 +135,40 @@ def take_right_size_screenshot(comparison)
|
|
110
135
|
save_screenshot(comparison.new_file_name)
|
111
136
|
|
112
137
|
# TODO(uwe): Remove when chromedriver takes right size screenshots
|
113
|
-
reduce_retina_image_size(comparison.new_file_name)
|
138
|
+
reduce_retina_image_size(comparison.new_file_name, comparison.driver)
|
114
139
|
# ODOT
|
115
140
|
end
|
116
141
|
|
117
|
-
def check_max_wait_time(comparison, screenshot_started_at,
|
142
|
+
def check_max_wait_time(comparison, screenshot_started_at, max_wait_time:)
|
143
|
+
return if (Time.now - screenshot_started_at) < max_wait_time
|
144
|
+
|
145
|
+
annotate_stabilization_images(comparison)
|
146
|
+
# FIXME(uwe): Change to store the failure and only report if the test succeeds functionally.
|
147
|
+
fail("Could not get stable screenshot within #{max_wait_time}s\n" \
|
148
|
+
"#{stabilization_images(comparison.new_file_name).join("\n")}")
|
149
|
+
end
|
150
|
+
|
151
|
+
def annotate_stabilization_images(comparison)
|
152
|
+
previous_file = comparison.old_file_name
|
153
|
+
stabilization_images(comparison.new_file_name).each do |file_name|
|
154
|
+
if File.exist? previous_file
|
155
|
+
stabilization_comparison = make_stabilization_comparison_from(
|
156
|
+
comparison,
|
157
|
+
file_name,
|
158
|
+
previous_file
|
159
|
+
)
|
160
|
+
if stabilization_comparison.different?
|
161
|
+
FileUtils.mv stabilization_comparison.annotated_new_file_name, file_name
|
162
|
+
end
|
163
|
+
FileUtils.rm stabilization_comparison.annotated_old_file_name
|
164
|
+
end
|
165
|
+
previous_file = file_name
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def max_wait_time(shift_distance_limit, wait)
|
118
170
|
shift_factor = shift_distance_limit ? (shift_distance_limit * 2 + 1) ^ 2 : 1
|
119
|
-
|
120
|
-
assert((Time.now - screenshot_started_at) < max_wait_time,
|
121
|
-
"Could not get stable screenshot within #{max_wait_time}s\n" \
|
122
|
-
"#{stabilization_images(comparison.new_file_name).join("\n")}")
|
171
|
+
wait * shift_factor
|
123
172
|
end
|
124
173
|
|
125
174
|
def assert_images_loaded(timeout:)
|
@@ -130,8 +179,11 @@ def assert_images_loaded(timeout:)
|
|
130
179
|
pending_image = evaluate_script IMAGE_WAIT_SCRIPT
|
131
180
|
break unless pending_image
|
132
181
|
|
133
|
-
assert(
|
134
|
-
|
182
|
+
assert(
|
183
|
+
(Time.now - start) < timeout,
|
184
|
+
"Images not loaded after #{timeout}s: #{pending_image.inspect}"
|
185
|
+
)
|
186
|
+
|
135
187
|
sleep 0.1
|
136
188
|
end
|
137
189
|
end
|
@@ -1,13 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require_relative
|
9
|
-
require_relative
|
10
|
-
require_relative
|
3
|
+
require "English"
|
4
|
+
require "capybara"
|
5
|
+
require "action_controller"
|
6
|
+
require "action_dispatch"
|
7
|
+
require "active_support/core_ext/string/strip"
|
8
|
+
require_relative "image_compare"
|
9
|
+
require_relative "stabilization"
|
10
|
+
require_relative "vcs"
|
11
11
|
|
12
12
|
# Add the `screenshot` method to ActionDispatch::IntegrationTest
|
13
13
|
module Capybara
|
@@ -42,19 +42,13 @@ def screenshot_dir
|
|
42
42
|
end
|
43
43
|
|
44
44
|
def current_capybara_driver_class
|
45
|
-
Capybara.
|
45
|
+
Capybara.current_session.driver.class
|
46
46
|
end
|
47
47
|
|
48
48
|
def selenium?
|
49
49
|
current_capybara_driver_class <= Capybara::Selenium::Driver
|
50
50
|
end
|
51
51
|
|
52
|
-
def poltergeist?
|
53
|
-
return false unless defined?(Capybara::Poltergeist::Driver)
|
54
|
-
|
55
|
-
current_capybara_driver_class <= Capybara::Poltergeist::Driver
|
56
|
-
end
|
57
|
-
|
58
52
|
def screenshot_section(name)
|
59
53
|
@screenshot_section = name.to_s
|
60
54
|
end
|
@@ -68,48 +62,59 @@ def screenshot_group(name)
|
|
68
62
|
end
|
69
63
|
|
70
64
|
# @return [Boolean] wether a screenshot was taken
|
71
|
-
def screenshot(
|
65
|
+
def screenshot(
|
66
|
+
name,
|
67
|
+
stability_time_limit: Screenshot.stability_time_limit,
|
68
|
+
wait: Capybara.default_max_wait_time,
|
69
|
+
**driver_options
|
70
|
+
)
|
71
|
+
return false unless Screenshot.active?
|
72
|
+
return false if window_size_is_wrong?
|
73
|
+
|
74
|
+
driver_options = {
|
75
|
+
area_size_limit: Diff.area_size_limit,
|
72
76
|
color_distance_limit: Diff.color_distance_limit,
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
77
|
+
driver: Diff.driver,
|
78
|
+
shift_distance_limit: Diff.shift_distance_limit,
|
79
|
+
skip_area: Diff.skip_area,
|
80
|
+
tolerance: Diff.tolerance
|
81
|
+
}.merge(driver_options)
|
82
|
+
|
83
|
+
# Allow nil or single or multiple areas
|
84
|
+
if driver_options[:skip_area]
|
85
|
+
driver_options[:skip_area] = driver_options[:skip_area].compact.flatten&.each_cons(4)&.to_a
|
86
|
+
end
|
80
87
|
|
81
88
|
if @screenshot_counter
|
82
|
-
name = "#{format(
|
89
|
+
name = "#{format("%02i", @screenshot_counter)}_#{name}"
|
83
90
|
@screenshot_counter += 1
|
84
91
|
end
|
85
92
|
name = full_name(name)
|
86
93
|
file_name = "#{Screenshot.screenshot_area_abs}/#{name}.png"
|
87
94
|
|
88
95
|
FileUtils.mkdir_p File.dirname(file_name)
|
89
|
-
comparison = ImageCompare.new(file_name,
|
90
|
-
dimensions: Screenshot.window_size, color_distance_limit: color_distance_limit,
|
91
|
-
area_size_limit: area_size_limit, shift_distance_limit: shift_distance_limit,
|
92
|
-
skip_area: skip_area)
|
96
|
+
comparison = ImageCompare.new(file_name, **driver_options)
|
93
97
|
checkout_vcs(name, comparison)
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
98
|
+
begin
|
99
|
+
blurred_input = prepare_page_for_screenshot(timeout: wait)
|
100
|
+
if stability_time_limit
|
101
|
+
take_stable_screenshot(comparison, stability_time_limit: stability_time_limit, wait: wait)
|
102
|
+
else
|
103
|
+
take_right_size_screenshot(comparison)
|
104
|
+
end
|
105
|
+
ensure
|
106
|
+
blurred_input&.click
|
107
|
+
end
|
108
|
+
|
109
|
+
return false unless comparison.old_file_exists?
|
101
110
|
|
102
111
|
(@test_screenshots ||= []) << [caller(1..1).first, name, comparison]
|
112
|
+
|
103
113
|
true
|
104
114
|
end
|
105
115
|
|
106
116
|
def window_size_is_wrong?
|
107
117
|
selenium? && Screenshot.window_size &&
|
108
|
-
|
109
|
-
# FIXME(uwe): This happens with headless chrome. Why?!
|
110
|
-
page.driver.browser.manage.window.size.width &&
|
111
|
-
# EMXIF
|
112
|
-
|
113
118
|
page.driver.browser.manage.window.size !=
|
114
119
|
::Selenium::WebDriver::Dimension.new(*Screenshot.window_size)
|
115
120
|
end
|
@@ -117,21 +122,7 @@ def window_size_is_wrong?
|
|
117
122
|
def assert_image_not_changed(caller, name, comparison)
|
118
123
|
return unless comparison.different?
|
119
124
|
|
120
|
-
|
121
|
-
max_color_distance = if RUBY_VERSION >= '2.4'
|
122
|
-
comparison.max_color_distance.ceil(1)
|
123
|
-
else
|
124
|
-
comparison.max_color_distance.ceil
|
125
|
-
end
|
126
|
-
# ODOT
|
127
|
-
|
128
|
-
max_shift_distance = comparison.max_shift_distance
|
129
|
-
"Screenshot does not match for '#{name}' (area: #{comparison.size}px #{comparison.dimensions}" \
|
130
|
-
", max_color_distance: #{max_color_distance}" \
|
131
|
-
"#{", max_shift_distance: #{max_shift_distance}" if max_shift_distance})\n" \
|
132
|
-
"#{comparison.new_file_name}\n#{comparison.annotated_old_file_name}\n" \
|
133
|
-
"#{comparison.annotated_new_file_name}\n" \
|
134
|
-
"at #{caller}"
|
125
|
+
"Screenshot does not match for '#{name}' #{comparison.error_message}\nat #{caller}"
|
135
126
|
end
|
136
127
|
end
|
137
128
|
end
|
@@ -1,15 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative
|
3
|
+
require_relative "os"
|
4
4
|
module Capybara
|
5
5
|
module Screenshot
|
6
6
|
module Diff
|
7
7
|
module Vcs
|
8
|
-
SILENCE_ERRORS = Os::ON_WINDOWS ?
|
8
|
+
SILENCE_ERRORS = Os::ON_WINDOWS ? "2>nul" : "2>/dev/null"
|
9
9
|
|
10
10
|
def restore_git_revision(name, target_file_name)
|
11
11
|
redirect_target = "#{target_file_name} #{SILENCE_ERRORS}"
|
12
|
-
|
12
|
+
show_command = "git show HEAD~0:./#{Capybara::Screenshot.screenshot_area}/#{name}.png"
|
13
|
+
if Capybara::Screenshot.use_lfs
|
14
|
+
`#{show_command} | git lfs smudge > #{redirect_target}`
|
15
|
+
else
|
16
|
+
`#{show_command} > #{redirect_target}`
|
17
|
+
end
|
13
18
|
FileUtils.rm_f(target_file_name) unless $CHILD_STATUS == 0
|
14
19
|
end
|
15
20
|
|
data/matrix_test.rb
CHANGED
@@ -1,31 +1,29 @@
|
|
1
1
|
#!/usr/bin/env ruby -w
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
|
4
|
+
update_gemfiles = ARGV.delete("--update")
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
require 'yaml'
|
9
|
-
travis = YAML.safe_load(File.read('.travis.yml'))
|
6
|
+
require "yaml"
|
7
|
+
travis = YAML.safe_load(File.read(".travis.yml"))
|
10
8
|
|
11
9
|
def run_script(ruby, env, gemfile)
|
12
10
|
env.scan(/\b(?<key>[A-Z_]+)="(?<value>.+?)"/) do |key, value|
|
13
11
|
ENV[key] = value
|
14
12
|
end
|
15
|
-
puts
|
13
|
+
puts "*" * 80
|
16
14
|
puts "Testing #{ruby} #{gemfile} #{env}"
|
17
15
|
puts
|
18
16
|
system("chruby-exec #{ruby} -- bundle exec rake") || exit(1)
|
19
17
|
puts "Testing #{ruby} #{gemfile} OK"
|
20
|
-
puts
|
18
|
+
puts "*" * 80
|
21
19
|
end
|
22
20
|
|
23
21
|
def use_gemfile(ruby, gemfile, update_gemfiles)
|
24
|
-
puts
|
25
|
-
ENV[
|
22
|
+
puts "$" * 80
|
23
|
+
ENV["BUNDLE_GEMFILE"] = gemfile
|
26
24
|
|
27
25
|
bundler_version = `grep -A1 "BUNDLED WITH" #{gemfile}.lock | tail -n 1`
|
28
|
-
bundler_version =
|
26
|
+
bundler_version = "~> 2.0" if bundler_version.strip.empty?
|
29
27
|
|
30
28
|
version_arg = "-v '#{bundler_version}'"
|
31
29
|
bundler_gem_check_cmd = "chruby-exec #{ruby} -- gem query -i -n bundler #{version_arg} >/dev/null"
|
@@ -37,33 +35,33 @@ def use_gemfile(ruby, gemfile, update_gemfiles)
|
|
37
35
|
system "chruby-exec #{ruby} -- bundle check >/dev/null || chruby-exec #{ruby} -- bundle install"
|
38
36
|
end || exit(1)
|
39
37
|
yield
|
40
|
-
puts
|
38
|
+
puts "$" * 80
|
41
39
|
end
|
42
40
|
|
43
|
-
travis[
|
44
|
-
next if
|
41
|
+
travis["rvm"].each do |ruby|
|
42
|
+
next if /head/.match?(ruby) # ruby-install does not support HEAD installation
|
45
43
|
|
46
|
-
puts
|
44
|
+
puts "#" * 80
|
47
45
|
puts "Testing #{ruby}"
|
48
46
|
puts
|
49
47
|
system "ruby-install --no-reinstall #{ruby}" || exit(1)
|
50
|
-
travis[
|
51
|
-
if travis[
|
52
|
-
(travis[
|
53
|
-
.any? { |f| f[
|
54
|
-
puts
|
48
|
+
travis["gemfile"].each do |gemfile|
|
49
|
+
if travis["matrix"] &&
|
50
|
+
(travis["matrix"]["exclude"].to_a + travis["matrix"]["allow_failures"].to_a)
|
51
|
+
.any? { |f| f["rvm"] == ruby && (f["gemfile"].nil? || f["gemfile"] == gemfile) }
|
52
|
+
puts "Skipping known failure."
|
55
53
|
next
|
56
54
|
end
|
57
55
|
use_gemfile(ruby, gemfile, update_gemfiles) do
|
58
|
-
travis[
|
56
|
+
travis["env"].each do |env|
|
59
57
|
run_script(ruby, env, gemfile)
|
60
58
|
end
|
61
59
|
end
|
62
60
|
end
|
63
61
|
puts "Testing #{ruby} OK"
|
64
|
-
puts
|
62
|
+
puts "#" * 80
|
65
63
|
end
|
66
64
|
|
67
65
|
print "\033[0;32m"
|
68
|
-
print
|
66
|
+
print " TESTS PASSED OK!"
|
69
67
|
puts "\033[0m"
|