kontrast 0.2.1 → 0.6.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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +135 -96
  3. data/bin/kontrast +9 -5
  4. data/lib/kontrast.rb +17 -5
  5. data/lib/kontrast/api_client.rb +56 -0
  6. data/lib/kontrast/api_endpoint_comparator.rb +126 -0
  7. data/lib/kontrast/api_endpoint_runner.rb +99 -0
  8. data/lib/kontrast/api_endpoint_test.rb +8 -0
  9. data/lib/kontrast/configuration.rb +36 -7
  10. data/lib/kontrast/exceptions.rb +2 -1
  11. data/lib/kontrast/gallery/template.erb +152 -21
  12. data/lib/kontrast/gallery_creator.rb +105 -47
  13. data/lib/kontrast/global_runner.rb +121 -0
  14. data/lib/kontrast/image_helper.rb +63 -0
  15. data/lib/kontrast/image_uploader.rb +18 -0
  16. data/lib/kontrast/page_comparator.rb +46 -0
  17. data/lib/kontrast/page_runner.rb +95 -0
  18. data/lib/kontrast/page_test.rb +32 -0
  19. data/lib/kontrast/selenium_handler.rb +18 -7
  20. data/lib/kontrast/spec.rb +21 -0
  21. data/lib/kontrast/spec_builder.rb +54 -0
  22. data/lib/kontrast/test.rb +27 -0
  23. data/lib/kontrast/test_builder.rb +25 -9
  24. data/lib/kontrast/test_suite.rb +42 -0
  25. data/lib/kontrast/thumbnail_creator.rb +18 -0
  26. data/lib/kontrast/version.rb +1 -1
  27. data/spec/api_endpoint_comparator_spec.rb +125 -0
  28. data/spec/configuration_spec.rb +19 -0
  29. data/spec/gallery_creator_spec.rb +26 -20
  30. data/spec/{image_handler_spec.rb → global_runner_spec.rb} +17 -12
  31. data/spec/page_comparator_spec.rb +31 -0
  32. data/spec/page_runner_spec.rb +45 -0
  33. data/spec/spec_builder_spec.rb +32 -0
  34. data/spec/support/fixtures/image.jpg +0 -0
  35. data/spec/support/fixtures/image_clone.jpg +0 -0
  36. data/spec/support/fixtures/img1.jpg +0 -0
  37. data/spec/support/fixtures/img2.jpg +0 -0
  38. data/spec/support/fixtures/other_image.jpg +0 -0
  39. data/spec/test_builder_spec.rb +6 -3
  40. data/spec/test_spec.rb +53 -0
  41. data/spec/test_suite_spec.rb +56 -0
  42. metadata +91 -30
  43. data/lib/kontrast/image_handler.rb +0 -119
  44. data/lib/kontrast/runner.rb +0 -141
  45. data/spec/runner_spec.rb +0 -37
@@ -12,7 +12,7 @@ module Kontrast
12
12
  def create_gallery(output_dir)
13
13
  begin
14
14
  @gallery_dir = FileUtils.mkdir_p("#{output_dir}/gallery").join('')
15
- rescue Exception => e
15
+ rescue StandardError => e
16
16
  raise GalleryException.new("An unexpected error occurred while trying to create the gallery's output directory: #{e.inspect}")
17
17
  end
18
18
 
@@ -46,7 +46,7 @@ module Kontrast
46
46
 
47
47
  def generate_html(files, diffs)
48
48
  # Template variables
49
- directories = parse_directories(files, diffs)
49
+ groups, without_diffs, with_diffs = parse_directories(files, diffs)
50
50
 
51
51
  # HTML
52
52
  template = File.read(Kontrast.root + '/lib/kontrast/gallery/template.erb')
@@ -83,66 +83,124 @@ module Kontrast
83
83
  }
84
84
  end
85
85
 
86
+ def test_name_from_dir(dir)
87
+ # dir is a string prefixed with a group name:
88
+ # '1280_home' or '2x_home_screen'
89
+ return dir.split('_')[1..-1].join('_')
90
+ end
91
+
92
+ def base_path
93
+ # This determines where to display images from in the gallery
94
+ if Kontrast.configuration.run_parallel
95
+ # Build the remote path to S3
96
+ return "https://#{Kontrast.configuration.aws_bucket}.s3.amazonaws.com/#{Kontrast.configuration.remote_path}"
97
+ else
98
+ return ".."
99
+ end
100
+ end
101
+
102
+ def variants_for_page(directory, diffs)
103
+ # Return a hash that will be used in the erb template to show the
104
+ # diffs for a given test.
105
+ variants = []
106
+ ['test', 'production', 'diff'].each do |type|
107
+ variant = {
108
+ image: "#{base_path}/#{directory}/" + type + ".png",
109
+ thumb: "#{base_path}/#{directory}/" + type + "_thumb.png",
110
+ domain: type,
111
+ type: 'page',
112
+ }
113
+ if type == 'diff' && diffs[directory]
114
+ variant[:diff_amt] = diffs[directory]["diff"]
115
+ end
116
+ variants << variant
117
+ end
118
+
119
+ return variants
120
+ end
121
+
122
+ def variants_for_api_endpoint(directory, diffs, files)
123
+ # Return a hash that will be used in the erb template to show the
124
+ # diffs for a given test.
125
+ variants = []
126
+ ['test', 'production', 'diff'].each do |type|
127
+ variant = {
128
+ file: "#{base_path}/#{directory}/#{type}.json",
129
+ domain: type,
130
+ type: 'api_endpoint',
131
+ diff_amt: 0,
132
+ }
133
+ if diffs[directory]
134
+ variant[:diff_amt] = 1
135
+ end
136
+
137
+ # Get all images
138
+ image_files = files.select { |file_name|
139
+ file_name.match /#{directory}\/#{type}_\d+.(jpg|png)/
140
+ }.map { |file_name|
141
+ name_without_extension = file_name.split('.')[0..-2].join('.')
142
+ {
143
+ image: "#{base_path}/#{file_name}",
144
+ thumb: "#{base_path}/#{name_without_extension}_thumb.png",
145
+ }
146
+ }
147
+ variant[:images] = image_files
148
+ variants << variant
149
+ end
150
+
151
+ return variants
152
+ end
153
+
86
154
  # This function just turns the list of files and diffs into a hash that the gallery
87
155
  # creator can insert into a template. See an example of the created hash below.
88
156
  def parse_directories(files, diffs)
89
157
  files.sort!
90
158
 
91
- dirs = {}
92
- directories = files.map { |f| f.split('/').first }.uniq
159
+ # Initialize those hashes, where each key will map to hash, in wich
160
+ # each key maps to an array:
161
+ # {
162
+ # key1: {
163
+ # },
164
+ # key2: {
165
+ # },
166
+ # }
167
+ #
168
+ without_diffs = Hash.new { |h,k| h[k] = {} }
169
+ with_diffs = Hash.new { |h,k| h[k] = {} }
93
170
 
94
- # Get all sizes
95
- sizes = directories.map { |d| d.split('_').first }
96
- sizes.each { |size|
97
- dirs[size] = {}
98
-
99
- # Get all directories for this size
100
- tests_for_size = directories.select { |d| d.index(size + "_") == 0 }
101
- tests_for_size.each do |dir|
102
- array = dir.split('_')
103
- array.delete_at(0)
104
- test_name = array.join('_')
105
- dirs[size][test_name] = {
106
- variants: []
107
- }
108
- end
109
- }
171
+ directories = files.map { |f| f.split('/').first }.uniq
172
+ groups = directories.map { |dir| dir.split('_').first }.uniq
110
173
 
111
- # This determines where to display images from in the gallery
112
- if Kontrast.configuration.run_parallel
113
- # Build the remote path to S3
114
- base_path = "https://#{Kontrast.configuration.aws_bucket}.s3.amazonaws.com/#{Kontrast.configuration.remote_path}"
115
- else
116
- base_path = ".."
117
- end
118
174
 
119
175
  # Fill in the files as variants
120
176
  directories.each do |directory|
121
- array = directory.split('_')
122
- size = array.first
123
- array.delete_at(0)
124
- test_name = array.join('_')
125
-
126
- # Add variations
127
- ['test', 'production', 'diff'].each_with_index do |type, i|
128
- dirs[size][test_name][:variants] << {
129
- image: "#{base_path}/#{size}_#{test_name}/" + type + ".png",
130
- thumb: "#{base_path}/#{size}_#{test_name}/" + type + "_thumb.png",
131
- domain: type
132
- }
133
- if type == 'diff'
134
- dirs[size][test_name][:variants][i][:diff_amt] = (diffs["#{size}_#{test_name}"]) ? diffs["#{size}_#{test_name}"]["diff"] : 0
135
- end
177
+ group = directory.split('_')[0]
178
+ test_name = test_name_from_dir(directory)
179
+
180
+ # Determines the type of test by the presence of the diff.png
181
+ # file in the folder.
182
+ # Ideally the manifest file format would be different and
183
+ # include the test type with
184
+ if files.select { |file_name| file_name.start_with?(directory) }.any? { |file_name| file_name.include?('diff.png') }
185
+ variants = variants_for_page(directory, diffs)
186
+ else
187
+ variants = variants_for_api_endpoint(directory, diffs, files)
188
+ end
189
+
190
+ if diffs[directory]
191
+ with_diffs[group][test_name] = variants
192
+ else
193
+ without_diffs[group][test_name] = variants
136
194
  end
137
195
  end
138
196
 
139
- return dirs
197
+ return groups, without_diffs, with_diffs
140
198
 
141
199
  # For reference
142
200
  # gallery_format = {
143
201
  # "1080" => {
144
- # "name" => {
145
- # variants: [{
202
+ # "name" => [
203
+ # {
146
204
  # image: "full_img_src",
147
205
  # thumb: "thumb_src",
148
206
  # domain: "production"
@@ -154,8 +212,8 @@ module Kontrast
154
212
  # image: "diff_src",
155
213
  # thumb: "diff_thumb_src",
156
214
  # domain: "diff",
157
- # size: 0.1
158
- # }]
215
+ # diff_amt: 0.1
216
+ # }
159
217
  # }
160
218
  # }
161
219
  # }
@@ -0,0 +1,121 @@
1
+ require "yaml"
2
+ require "net/http"
3
+
4
+ module Kontrast
5
+ class GlobalRunner
6
+
7
+ def initialize
8
+ @page_runner = PageRunner.new
9
+ @api_endpont_runner = ApiEndpointRunner.new
10
+ @path = Kontrast.path
11
+ @current_node = 0
12
+ end
13
+
14
+ def run
15
+ # Make sure the local server is running
16
+ wait_for_server
17
+
18
+ # Assign nodes
19
+ if Kontrast.configuration.run_parallel
20
+ total_nodes = Kontrast.configuration.total_nodes
21
+ @current_node = Kontrast.configuration.current_node
22
+ else
23
+ # Override the config for local use
24
+ total_nodes = 1
25
+ end
26
+
27
+ # Run both runners
28
+ @page_runner.run(total_nodes, @current_node)
29
+ @api_endpont_runner.run(total_nodes, @current_node)
30
+
31
+ # Create manifest
32
+ create_manifest
33
+ end
34
+
35
+ # The manifest is a per-node .json file that is used to create the gallery
36
+ # without having to download all assets from S3 to the test environment
37
+ def create_manifest
38
+ # Create manifest
39
+ puts "Creating manifest..."
40
+ if Kontrast.configuration.run_parallel
41
+ build = Kontrast.configuration.remote_path
42
+ else
43
+ build = nil
44
+ end
45
+
46
+ diffs = {}
47
+ diffs.merge!(@page_runner.diffs)
48
+ diffs.merge!(@api_endpont_runner.diffs)
49
+
50
+ # Set up structure
51
+ manifest = {
52
+ diffs: diffs,
53
+ files: []
54
+ }
55
+
56
+ # Dump directories
57
+ Dir.foreach(@path) do |subdir|
58
+ next if ['.', '..'].include?(subdir)
59
+ next if subdir.index('manifest_')
60
+ Dir.foreach("#{@path}/#{subdir}") do |img|
61
+ next if ['.', '..'].include?(img)
62
+ manifest[:files] << "#{subdir}/#{img}"
63
+ end
64
+ end
65
+
66
+ if Kontrast.configuration.run_parallel
67
+ # Upload manifest
68
+ Kontrast.fog.directories.get(Kontrast.configuration.aws_bucket).files.create(
69
+ key: "#{build}/manifest_#{@current_node}.json",
70
+ body: manifest.to_json
71
+ )
72
+ else
73
+ # Write manifest
74
+ File.open("#{@path}/manifest_#{@current_node}.json", 'w') do |outf|
75
+ outf.write(manifest.to_json)
76
+ end
77
+ end
78
+
79
+ return manifest
80
+ end
81
+
82
+ private
83
+ def wait_for_server
84
+ # Test server
85
+ tries = 30
86
+ uri = URI(Kontrast.configuration.test_domain)
87
+ begin
88
+ Net::HTTP.get(uri)
89
+ rescue Errno::ECONNREFUSED => e
90
+ tries -= 1
91
+ if tries > 0
92
+ puts "Waiting for test server..."
93
+ sleep 2
94
+ retry
95
+ else
96
+ raise RunnerException.new("Could not reach the test server at '#{uri}'.")
97
+ end
98
+ rescue StandardError => e
99
+ raise RunnerException.new("An unexpected error occured while trying to reach the test server at '#{uri}': #{e.inspect}")
100
+ end
101
+
102
+ # Production server
103
+ tries = 30
104
+ uri = URI(Kontrast.configuration.production_domain)
105
+ begin
106
+ Net::HTTP.get(uri)
107
+ rescue Errno::ECONNREFUSED => e
108
+ tries -= 1
109
+ if tries > 0
110
+ puts "Waiting for production server..."
111
+ sleep 2
112
+ retry
113
+ else
114
+ raise RunnerException.new("Could not reach the production server at '#{uri}'.")
115
+ end
116
+ rescue StandardError => e
117
+ raise RunnerException.new("An unexpected error occured while trying to reach the production server at '#{uri}': #{e.inspect}")
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,63 @@
1
+ require "RMagick"
2
+ require "workers"
3
+
4
+ module Kontrast
5
+
6
+ class ImageHelper
7
+ def initialize(img1_path, img2_path)
8
+ @img1_path, @img2_path = img1_path, img2_path
9
+ @img1 = load_image(@img1_path)
10
+ @img2 = load_image(@img2_path)
11
+ @path = Kontrast.path
12
+ end
13
+
14
+ def reload_images
15
+ @img1 = load_image(@img1_path)
16
+ @img2 = load_image(@img2_path)
17
+ end
18
+
19
+ def load_image(path)
20
+ return Magick::Image.read(path).first
21
+ end
22
+
23
+ def crop(width)
24
+ # Let's not do anything if the images are already the same size
25
+ return if @img1.rows == @img2.rows
26
+
27
+ # Get max height of both images
28
+ max_height = [@img1.rows, @img2.rows].max
29
+
30
+ # Crop
31
+ Workers.map([@img1, @img2]) do |image|
32
+ image.extent(width, max_height).write(image.filename)
33
+ end
34
+ reload_images
35
+ end
36
+
37
+ # Uses the compare_channel function to highlight the differences between
38
+ # two images Docs:
39
+ # http://www.rubydoc.info/github/gemhome/rmagick/Magick%2FImage%3Acompare_channel
40
+ def compare(output_dir, output_file_name)
41
+ begin
42
+ distortion_metric = Magick.const_get(Kontrast.configuration.distortion_metric)
43
+ diff = @img1.compare_channel(@img2, distortion_metric) do |options|
44
+ options.highlight_color = Kontrast.configuration.highlight_color
45
+ options.lowlight_color = Kontrast.configuration.lowlight_color
46
+ end
47
+
48
+ output_path = "#{Kontrast.path}/#{output_dir}"
49
+ FileUtils.mkdir_p(output_path) # Just in case
50
+ diff.first.write(File.join(output_path, output_file_name))
51
+
52
+ # diff is an array, the last (second) value is the diff value,
53
+ # a float between 0 and 1, 0 being the same image, 1 being an
54
+ # entirely different image
55
+ return diff.last
56
+ rescue Magick::ImageMagickError => e
57
+ puts "Error comparing images: #{e.message}"
58
+ # 1 means that both images are different
59
+ return 1
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,18 @@
1
+ module Kontrast
2
+ module ImageUploader
3
+ def upload_images(test)
4
+ worker_pool = Workers::Pool.new
5
+ worker_pool.resize(Kontrast.configuration.workers_pool_size)
6
+
7
+ Workers.map(Dir.entries("#{Kontrast.path}/#{test}"), pool: worker_pool) do |file|
8
+ next if ['.', '..'].include?(file)
9
+ Kontrast.fog.directories.get(Kontrast.configuration.aws_bucket).files.create(
10
+ key: "#{Kontrast.configuration.remote_path}/#{test}/#{file}",
11
+ body: File.open("#{Kontrast.path}/#{test}/#{file}"),
12
+ public: true
13
+ )
14
+ end
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,46 @@
1
+
2
+ module Kontrast
3
+ class PageComparator
4
+ include Magick
5
+ attr_reader :diffs, :path
6
+
7
+ def initialize
8
+ @path = Kontrast.path
9
+
10
+ # This is where failed diffs will be stored
11
+ @diffs = {}
12
+ end
13
+
14
+ def test_image_path(test)
15
+ return "#{@path}/#{test}/test.png"
16
+ end
17
+
18
+ def production_image_path(test)
19
+ return "#{@path}/#{test}/production.png"
20
+ end
21
+
22
+ def diff(test)
23
+
24
+ image_helper = Kontrast::ImageHelper.new(
25
+ test_image_path(test),
26
+ production_image_path(test),
27
+ )
28
+
29
+ # In order for images to be diff'ed, they need to have the same dimensions
30
+ print "Cropping... "
31
+ image_helper.crop(test.width)
32
+
33
+ diff = image_helper.compare(test.to_s, "diff.png")
34
+
35
+ # If the images are different, let the class know about it so that it gets added to the manifest
36
+ if diff > 0
37
+ @diffs["#{test}"] = {
38
+ type: 'page',
39
+ width: test.width,
40
+ name: test.name,
41
+ diff: diff,
42
+ }
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,95 @@
1
+ module Kontrast
2
+ class PageRunner
3
+ include ImageUploader
4
+ include ThumbnailCreator
5
+
6
+ attr_reader :diffs
7
+
8
+ def initialize
9
+ @diffs = {}
10
+ @selenium_handler = nil
11
+ @page_comparator = nil
12
+ end
13
+
14
+ def run(total_nodes, current_node)
15
+ # Nothing to run
16
+ return if Kontrast.page_test_suite.nil?
17
+
18
+ # Load & bind specs
19
+ Kontrast.page_test_suite.bind_specs
20
+
21
+ # Assign tests and run them
22
+ suite = split_run(total_nodes, current_node)
23
+ parallel_run(suite, current_node)
24
+ end
25
+
26
+ # Given the total number of nodes and the index of the current node,
27
+ # we determine which tests the current node will run
28
+ def split_run(total_nodes, current_node)
29
+ test_suite = Kontrast.page_test_suite
30
+ tests_to_run = []
31
+
32
+ index = 0
33
+ test_suite.tests.each do |test|
34
+ if index % total_nodes == current_node
35
+ tests_to_run << test
36
+ end
37
+ index += 1
38
+ end
39
+
40
+ return tests_to_run
41
+ end
42
+
43
+ # Runs tests, handles all image operations, creates manifest for current node
44
+ def parallel_run(suite, current_node)
45
+ # Load test handlers
46
+ @selenium_handler = SeleniumHandler.new
47
+ @page_comparator = PageComparator.new
48
+
49
+ # Run per-page tasks
50
+ suite.each do |test|
51
+ begin
52
+ print "Processing #{test.name} @ #{test.width}... "
53
+
54
+ # Run the browser and take screenshots
55
+ @selenium_handler.run_comparison(test)
56
+
57
+ # Compare images
58
+ print "Diffing... "
59
+ @page_comparator.diff(test)
60
+
61
+ # Create thumbnails for gallery
62
+ print "Creating thumbnails... "
63
+ create_thumbnails(test, ['test.png', 'production.png', 'diff.png'])
64
+
65
+ # Upload to S3
66
+ if Kontrast.configuration.run_parallel
67
+ print "Uploading... "
68
+ upload_images(test)
69
+ end
70
+
71
+ puts "\n", ("=" * 85)
72
+ rescue Net::ReadTimeout => e
73
+ puts "Test timed out. Message: #{e.inspect}"
74
+ if Kontrast.configuration.fail_build
75
+ raise e
76
+ end
77
+ rescue StandardError => e
78
+ puts "Exception: #{e.inspect}"
79
+ puts e.backtrace.inspect
80
+ if Kontrast.configuration.fail_build
81
+ raise e
82
+ end
83
+ end
84
+ end
85
+ ensure
86
+ # Log diffs
87
+ puts @page_comparator.diffs
88
+
89
+ # We need the diff at the runner level to create the manifest
90
+ @diffs = @page_comparator.diffs
91
+
92
+ @selenium_handler.cleanup
93
+ end
94
+ end
95
+ end