diffux_ci 0.5.0 → 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.
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