diffux_ci 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d71ccdac8e66bd426e7ddb84f4d09423780606d7
4
- data.tar.gz: 0adf694bcf203f34bb8dc87b08e79c596c68d80f
3
+ metadata.gz: b444d088ff7fdcfd876e26252d4a9180b7e4aa71
4
+ data.tar.gz: cfd65df089cee0611a3fe98a6555d1275a3285da
5
5
  SHA512:
6
- metadata.gz: 778314460045c284beeb1eab8446e58c8f95d5619220c2d2c6887ff121b095cb73bbfbd7891055c32c3dc5ff4e2454551447119c2b2aeacfa3c164d81b0d89d0
7
- data.tar.gz: 4f242e480d4a70d4aa098b2a1ea29ec0bc8cf32128b952c48e419ec7f8b1e77092b38a50a57e9df23358ca5e92c402cad3afa0c158f46d4e2d48cad00577b041
6
+ metadata.gz: 3a65d2651905017d4c1a7a032e1483ecfce10c28028c6380ea3a036251d78213dde25e24b7980e001bd7a677288b50eb93f23600ad42535d1869ce48348c0a7f
7
+ data.tar.gz: 5575fb5c88d9769503a9e8f7d9e796e7e35560dec30f5f064334c6b4332cff989cdee8d887147a77d7bf64a36ec32ac3c62c2250538322c1d6339eca7171d6bf
@@ -0,0 +1,47 @@
1
+ require 'set'
2
+ module Diffux
3
+ # This class finds clusters in a diff. A cluster is defined as rows that are
4
+ # different, and are closer than DIFF_ROW_THRESHOLD pixels to its neighboring
5
+ # diff row.
6
+ class DiffClusterFinder
7
+ MAXIMUM_ADJACENCY_GAP = 5
8
+
9
+ # @param number_of_rows [Numeric]
10
+ def initialize(number_of_rows)
11
+ @number_of_rows = number_of_rows
12
+ @rows_with_diff = SortedSet.new
13
+ end
14
+
15
+ # Tell the DiffClusterFinder about a row that is different.
16
+ #
17
+ # @param row [Numeric]
18
+ def row_is_different(row)
19
+ @rows_with_diff.add row
20
+ end
21
+
22
+ # @return [Float] the percent of rows that are different
23
+ def percent_of_rows_different
24
+ @rows_with_diff.length.to_f / @number_of_rows * 100
25
+ end
26
+
27
+ # Calculate clusters from diff-rows that are close to each other.
28
+ #
29
+ # @return [Array<Hash>] a list of clusters modeled as hashes:
30
+ # `{ start: x, finish: y }`
31
+ def clusters
32
+ results = []
33
+ @rows_with_diff.each do |row|
34
+ current = results.last
35
+ if !current || current[:finish] + MAXIMUM_ADJACENCY_GAP < row
36
+ results << {
37
+ start: row,
38
+ finish: row,
39
+ }
40
+ else
41
+ current[:finish] = row
42
+ end
43
+ end
44
+ results
45
+ end
46
+ end
47
+ end
@@ -38,6 +38,21 @@ module DiffuxCI
38
38
  end
39
39
  end
40
40
 
41
+ get '/*' do
42
+ config = DiffuxCI::Utils.config
43
+ file = params[:splat].first
44
+ if File.exist?(file)
45
+ send_file file
46
+ else
47
+ config['public_directories'].each do |pub_dir|
48
+ filepath = File.join(Dir.pwd, pub_dir, file)
49
+ if File.exist?(filepath)
50
+ send_file filepath
51
+ end
52
+ end
53
+ end
54
+ end
55
+
41
56
  post '/reject' do
42
57
  DiffuxCI::Action.new(params[:description], params[:viewport]).reject
43
58
  redirect back
@@ -0,0 +1,88 @@
1
+ require 'oily_png'
2
+ require 'diff-lcs'
3
+ require_relative 'diff_cluster_finder'
4
+
5
+ module DiffuxCI
6
+ # This class is responsible for comparing two Snapshots and generating a diff.
7
+ class SnapshotComparer
8
+ # @param png_before [ChunkyPNG::Image]
9
+ # @param png_after [ChunkyPNG::Image]
10
+ def initialize(png_before, png_after)
11
+ @png_after = png_after
12
+ @png_before = png_before
13
+ end
14
+
15
+ # @return [Hash]
16
+ def compare!
17
+ no_diff = {
18
+ diff_in_percent: 0,
19
+ diff_image: nil,
20
+ diff_clusters: []
21
+ }
22
+
23
+ # If these images are totally identical, we don't need to do any more
24
+ # work.
25
+ return no_diff if @png_before == @png_after
26
+
27
+ array_before = to_array_of_arrays(@png_before)
28
+ array_after = to_array_of_arrays(@png_after)
29
+
30
+ # If the arrays of arrays of colors are identical, we don't need to do any
31
+ # more work. This might happen if some of the headers are different.
32
+ return no_diff if array_before == array_after
33
+
34
+ sdiff = Diff::LCS.sdiff(array_before, array_after)
35
+ cluster_finder = DiffClusterFinder.new(sdiff.size)
36
+ sprite, all_comparisons = initialize_comparison_images(
37
+ [@png_after.width, @png_before.width].max, sdiff.size)
38
+
39
+ sdiff.each_with_index do |row, y|
40
+ # each row is a Diff::LCS::ContextChange instance
41
+ all_comparisons.each { |image| image.render_row(y, row) }
42
+ cluster_finder.row_is_different(y) unless row.unchanged?
43
+ end
44
+
45
+ percent_changed = cluster_finder.percent_of_rows_different
46
+ {
47
+ diff_in_percent: percent_changed,
48
+ diff_image: (sprite if percent_changed > 0),
49
+ diff_clusters: cluster_finder.clusters,
50
+ }
51
+ end
52
+
53
+ private
54
+
55
+ # @param [ChunkyPNG::Image]
56
+ # @return [Array<Array<Integer>>]
57
+ def to_array_of_arrays(chunky_png)
58
+ array_of_arrays = []
59
+ chunky_png.height.times do |y|
60
+ array_of_arrays << chunky_png.row(y)
61
+ end
62
+ array_of_arrays
63
+ end
64
+
65
+ # @param canvas [ChunkyPNG::Image] The output image to draw pixels on
66
+ # @return [Array<SnapshotComparisonImage>]
67
+ def initialize_comparison_images(width, height)
68
+ gutter_width = SnapshotComparisonImage::Gutter::WIDTH
69
+ total_width = (width * 3) + (gutter_width * 3)
70
+
71
+ sprite = ChunkyPNG::Image.new(total_width, height)
72
+ offset, comparison_images = 0, []
73
+ comparison_images << SnapshotComparisonImage::Gutter.new(offset, sprite)
74
+ offset += gutter_width
75
+ comparison_images << SnapshotComparisonImage::Before.new(offset, sprite)
76
+ offset += width
77
+ comparison_images << SnapshotComparisonImage::Gutter.new(offset, sprite)
78
+ offset += gutter_width
79
+ comparison_images << SnapshotComparisonImage::Overlayed.new(offset, sprite)
80
+ offset += width
81
+ comparison_images << SnapshotComparisonImage::Gutter.new(offset, sprite)
82
+ offset += gutter_width
83
+ comparison_images << SnapshotComparisonImage::After.new(offset, sprite)
84
+
85
+ [sprite, comparison_images]
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,21 @@
1
+ module Diffux
2
+ module SnapshotComparisonImage
3
+ # This subclass of `SnapshotComparisonImage` knows how to draw the
4
+ # representation of the "after" image.
5
+ class After < SnapshotComparisonImage::Base
6
+ # @param y [Integer]
7
+ # @param row [Diff::LCS:ContextChange]
8
+ def render_changed_row(y, row)
9
+ render_added_row(y, row)
10
+ end
11
+
12
+ # @param y [Integer]
13
+ # @param row [Diff::LCS:ContextChange]
14
+ def render_added_row(y, row)
15
+ row.new_element.each_with_index do |pixel_after, x|
16
+ render_pixel(x, y, pixel_after)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,107 @@
1
+ require 'chunky_png'
2
+ module Diffux
3
+ module SnapshotComparisonImage
4
+ # This model represents a "comparison image". Basically it's just a wrapper
5
+ # around a ChunkyPNG image with some nice methods to make life easier in the
6
+ # world of diffs.
7
+ #
8
+ # This model is never persisted.
9
+ class Base
10
+ include ChunkyPNG::Color
11
+
12
+ BASE_OPACITY = 0.1
13
+ BASE_ALPHA = (255 * BASE_OPACITY).round
14
+ BASE_DIFF_ALPHA = BASE_ALPHA * 2
15
+
16
+ MAGENTA = ChunkyPNG::Color.from_hex '#b33682'
17
+ RED = ChunkyPNG::Color.from_hex '#dc322f'
18
+ GREEN = ChunkyPNG::Color.from_hex '#859900'
19
+
20
+ # @param offset [Integer] the x-offset that this comparison image should
21
+ # use when rendering on the canvas image.
22
+ # @param canvas [ChunkyPNG::Image] The canvas image to render pixels on.
23
+ def initialize(offset, canvas)
24
+ @offset = offset
25
+ @canvas = canvas
26
+ end
27
+
28
+ # @param y [Integer]
29
+ # @param row [Diff::LCS:ContextChange]
30
+ def render_row(y, row)
31
+ if row.unchanged?
32
+ render_unchanged_row(y, row)
33
+ elsif row.deleting?
34
+ render_deleted_row(y, row)
35
+ elsif row.adding?
36
+ render_added_row(y, row)
37
+ else # changing?
38
+ render_changed_row(y, row)
39
+ end
40
+ end
41
+
42
+ # @param y [Integer]
43
+ # @param row [Diff::LCS:ContextChange]
44
+ def render_unchanged_row(y, row)
45
+ row.new_element.each_with_index do |pixel, x|
46
+ # Render the unchanged pixel as-is
47
+ render_pixel(x, y, pixel)
48
+ end
49
+ end
50
+
51
+ # @param y [Integer]
52
+ # @param row [Diff::LCS:ContextChange]
53
+ def render_changed_row(y, row)
54
+ # no default implementation
55
+ end
56
+
57
+ # @param y [Integer]
58
+ # @param row [Diff::LCS:ContextChange]
59
+ def render_added_row(y, row)
60
+ # no default implementation
61
+ end
62
+
63
+ # @param y [Integer]
64
+ # @param row [Diff::LCS:ContextChange]
65
+ def render_deleted_row(y, row)
66
+ # no default implementation
67
+ end
68
+
69
+ # Compute a score that represents the difference between 2 pixels
70
+ #
71
+ # This method simply takes the Euclidean distance between the RGBA channels
72
+ # of 2 colors over the maximum possible Euclidean distance. This gives us a
73
+ # percentage of how different the two colors are.
74
+ #
75
+ # Although it would be more perceptually accurate to calculate a proper
76
+ # Delta E in Lab colorspace, we probably don't need perceptual accuracy for
77
+ # this application, and it is nice to avoid the overhead of converting RGBA
78
+ # to Lab.
79
+ #
80
+ # @param pixel_after [Integer]
81
+ # @param pixel_before [Integer]
82
+ # @return [Float] number between 0 and 1 where 1 is completely different
83
+ # and 0 is no difference
84
+ def pixel_diff_score(pixel_after, pixel_before)
85
+ ChunkyPNG::Color::euclidean_distance_rgba(pixel_after, pixel_before) /
86
+ ChunkyPNG::Color::MAX_EUCLIDEAN_DISTANCE_RGBA
87
+ end
88
+
89
+ # @param diff_score [Float]
90
+ # @return [Integer] a number between 0 and 255 that represents the alpha
91
+ # channel of of the difference
92
+ def diff_alpha(diff_score)
93
+ (BASE_DIFF_ALPHA + ((255 - BASE_DIFF_ALPHA) * diff_score)).round
94
+ end
95
+
96
+ # Renders a pixel on the specified x and y position. Uses the offset that
97
+ # the comparison image has been configured with.
98
+ #
99
+ # @param x [Integer]
100
+ # @param y [Integer]
101
+ # @param pixel [Integer]
102
+ def render_pixel(x, y, pixel)
103
+ @canvas.set_pixel(x + @offset, y, pixel)
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,21 @@
1
+ module Diffux
2
+ module SnapshotComparisonImage
3
+ # This subclass of `SnapshotComparisonImage` knows how to draw the
4
+ # representation of the "before" image.
5
+ class Before < SnapshotComparisonImage::Base
6
+ # @param y [Integer]
7
+ # @param row [Diff::LCS:ContextChange]
8
+ def render_changed_row(y, row)
9
+ render_deleted_row(y, row)
10
+ end
11
+
12
+ # @param y [Integer]
13
+ # @param row [Diff::LCS:ContextChange]
14
+ def render_deleted_row(y, row)
15
+ row.old_element.each_with_index do |pixel_before, x|
16
+ render_pixel(x, y, pixel_before)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,34 @@
1
+ module Diffux
2
+ module SnapshotComparisonImage
3
+ # This class renders a gutter-column with a color representing the type of
4
+ # change that has happened.
5
+ class Gutter < SnapshotComparisonImage::Base
6
+ WIDTH = 10
7
+ GRAY = ChunkyPNG::Color.from_hex '#cccccc'
8
+
9
+ def render_row(y, row)
10
+ WIDTH.times do |x|
11
+ render_pixel(x, y, gutter_color(row))
12
+ end
13
+ # render a two-pixel empty column
14
+ 2.times do |x|
15
+ render_pixel(WIDTH - 1 - x, y, WHITE)
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def gutter_color(row)
22
+ if row.unchanged?
23
+ WHITE
24
+ elsif row.deleting?
25
+ RED
26
+ elsif row.adding?
27
+ GREEN
28
+ else # changed?
29
+ GRAY
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,88 @@
1
+ module Diffux
2
+ module SnapshotComparisonImage
3
+ # This subclass of `SnapshotComparisonImage` knows how to overlay the
4
+ # after-image on top of the before-image, and render the difference in a
5
+ # scaled magenta color.
6
+ class Overlayed < SnapshotComparisonImage::Base
7
+ WHITE_OVERLAY = ChunkyPNG::Color.fade(WHITE, 1 - BASE_ALPHA)
8
+
9
+ # @param offset [Integer]
10
+ # @param canvas [ChunkyPNG::Image]
11
+ # @see SnapshotComparisonImage::Base
12
+ def initialize(offset, canvas)
13
+ @diff_pixels = {}
14
+ @faded_pixels = {}
15
+ super
16
+ end
17
+
18
+ # @param y [Integer]
19
+ # @param row [Diff::LCS:ContextChange]
20
+ def render_unchanged_row(y, row)
21
+ # Render translucent original pixels
22
+ row.new_element.each_with_index do |pixel, x|
23
+ render_faded_pixel(x, y, pixel)
24
+ end
25
+ end
26
+
27
+ # @param y [Integer]
28
+ # @param row [Diff::LCS:ContextChange]
29
+ def render_deleted_row(y, row)
30
+ row.old_element.each_with_index do |pixel_before, x|
31
+ render_faded_magenta_pixel(TRANSPARENT, pixel_before, x, y)
32
+ end
33
+ end
34
+
35
+ # @param y [Integer]
36
+ # @param row [Diff::LCS:ContextChange]
37
+ def render_added_row(y, row)
38
+ row.new_element.each_with_index do |pixel_after, x|
39
+ render_faded_magenta_pixel(pixel_after, TRANSPARENT, x, y)
40
+ end
41
+ end
42
+
43
+ # @param y [Integer]
44
+ # @param row [Diff::LCS:ContextChange]
45
+ def render_changed_row(y, row)
46
+ row.old_element.zip(row.new_element).each_with_index do |pixels, x|
47
+ pixel_before, pixel_after = pixels
48
+ render_faded_magenta_pixel(
49
+ pixel_after || TRANSPARENT,
50
+ pixel_before || TRANSPARENT,
51
+ x, y)
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ # @param pixel_after [Integer]
58
+ # @param pixel_before [Integer]
59
+ # @param x [Integer]
60
+ # @param y [Integer]
61
+ def render_faded_magenta_pixel(pixel_after, pixel_before, x, y)
62
+ score = pixel_diff_score(pixel_after, pixel_before)
63
+ if score > 0
64
+ render_diff_pixel(x, y, score)
65
+ else
66
+ render_faded_pixel(x, y, pixel_after)
67
+ end
68
+ end
69
+
70
+ # @param x [Integer]
71
+ # @param y [Integer]
72
+ # @param score [Float]
73
+ def render_diff_pixel(x, y, score)
74
+ @diff_pixels[score] ||= compose_quick(fade(MAGENTA, diff_alpha(score)),
75
+ WHITE)
76
+ render_pixel(x, y, @diff_pixels[score])
77
+ end
78
+
79
+ # @param x [Integer]
80
+ # @param y [Integer]
81
+ # @param pixel [Integer]
82
+ def render_faded_pixel(x, y, pixel)
83
+ @faded_pixels[pixel] ||= compose_quick(WHITE_OVERLAY, pixel)
84
+ render_pixel(x, y, @faded_pixels[pixel])
85
+ end
86
+ end
87
+ end
88
+ end
@@ -10,6 +10,7 @@ module DiffuxCI
10
10
  'snapshots_folder' => './snapshots',
11
11
  'source_files' => [],
12
12
  'stylesheets' => [],
13
+ 'public_directories' => [],
13
14
  'port' => 4567,
14
15
  'driver' => :firefox,
15
16
  'viewports' => {
@@ -35,7 +36,7 @@ module DiffuxCI
35
36
  end
36
37
 
37
38
  def self.normalize_description(description)
38
- Base64.encode64(description).strip
39
+ Base64.strict_encode64(description).strip
39
40
  end
40
41
 
41
42
  def self.path_to(description, viewport_name, file_name)
@@ -1,4 +1,4 @@
1
1
  # Defines the gem version.
2
2
  module DiffuxCI
3
- VERSION = '0.5.0'
3
+ VERSION = '0.6.0'
4
4
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: diffux_ci
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Henric Trotzig
@@ -137,12 +137,19 @@ files:
137
137
  - bin/diffux
138
138
  - lib/diffux_ci.rb
139
139
  - lib/diffux_ci/action.rb
140
+ - lib/diffux_ci/diff_cluster_finder.rb
140
141
  - lib/diffux_ci/diffs.html.erb
141
142
  - lib/diffux_ci/logger.rb
142
143
  - lib/diffux_ci/public/diffux_ci-runner.js
143
144
  - lib/diffux_ci/public/diffux_ci-styles.css
144
145
  - lib/diffux_ci/runner.rb
145
146
  - lib/diffux_ci/server.rb
147
+ - lib/diffux_ci/snapshot_comparer.rb
148
+ - lib/diffux_ci/snapshot_comparison_image/after.rb
149
+ - lib/diffux_ci/snapshot_comparison_image/base.rb
150
+ - lib/diffux_ci/snapshot_comparison_image/before.rb
151
+ - lib/diffux_ci/snapshot_comparison_image/gutter.rb
152
+ - lib/diffux_ci/snapshot_comparison_image/overlayed.rb
146
153
  - lib/diffux_ci/uploader.rb
147
154
  - lib/diffux_ci/utils.rb
148
155
  - lib/diffux_ci/version.rb