kontrast 0.2.1 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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