kontrast 0.2.1
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 +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +262 -0
- data/Rakefile +2 -0
- data/bin/kontrast +111 -0
- data/lib/kontrast.rb +97 -0
- data/lib/kontrast/config/template.rb +27 -0
- data/lib/kontrast/configuration.rb +124 -0
- data/lib/kontrast/exceptions.rb +5 -0
- data/lib/kontrast/gallery/template.erb +76 -0
- data/lib/kontrast/gallery_creator.rb +164 -0
- data/lib/kontrast/image_handler.rb +119 -0
- data/lib/kontrast/runner.rb +141 -0
- data/lib/kontrast/selenium_handler.rb +84 -0
- data/lib/kontrast/test_builder.rb +25 -0
- data/lib/kontrast/version.rb +3 -0
- data/spec/configuration_spec.rb +53 -0
- data/spec/gallery_creator_spec.rb +105 -0
- data/spec/image_handler_spec.rb +52 -0
- data/spec/runner_spec.rb +37 -0
- data/spec/spec_helper.rb +44 -0
- data/spec/test_builder_spec.rb +52 -0
- metadata +202 -0
@@ -0,0 +1,141 @@
|
|
1
|
+
require "yaml"
|
2
|
+
require "net/http"
|
3
|
+
|
4
|
+
module Kontrast
|
5
|
+
class Runner
|
6
|
+
def initialize
|
7
|
+
end
|
8
|
+
|
9
|
+
def run
|
10
|
+
# Make sure the local server is running
|
11
|
+
wait_for_server
|
12
|
+
|
13
|
+
# Assign nodes
|
14
|
+
if Kontrast.configuration.run_parallel
|
15
|
+
total_nodes = Kontrast.configuration.total_nodes
|
16
|
+
current_node = Kontrast.configuration.current_node
|
17
|
+
else
|
18
|
+
# Override the config for local use
|
19
|
+
total_nodes = 1
|
20
|
+
current_node = 0
|
21
|
+
end
|
22
|
+
|
23
|
+
# Assign tests and run them
|
24
|
+
to_run = split_run(total_nodes, current_node)
|
25
|
+
parallel_run(to_run, current_node)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Given the total number of nodes and the index of the current node,
|
29
|
+
# we determine which tests the current node will run
|
30
|
+
def split_run(total_nodes, current_node)
|
31
|
+
all_tests = Kontrast.test_suite.tests
|
32
|
+
tests_to_run = Hash.new
|
33
|
+
|
34
|
+
index = 0
|
35
|
+
all_tests.each do |width, pages|
|
36
|
+
next if pages.nil?
|
37
|
+
tests_to_run[width] = {}
|
38
|
+
pages.each do |name, path|
|
39
|
+
if index % total_nodes == current_node
|
40
|
+
tests_to_run[width][name] = path
|
41
|
+
end
|
42
|
+
index += 1
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
return tests_to_run
|
47
|
+
end
|
48
|
+
|
49
|
+
# Runs tests, handles all image operations, creates manifest for current node
|
50
|
+
def parallel_run(tests, current_node)
|
51
|
+
# Load test handlers
|
52
|
+
@selenium_handler = SeleniumHandler.new
|
53
|
+
@image_handler = ImageHandler.new
|
54
|
+
|
55
|
+
begin
|
56
|
+
# Run per-page tasks
|
57
|
+
tests.each do |width, pages|
|
58
|
+
next if pages.nil?
|
59
|
+
pages.each do |name, path|
|
60
|
+
print "Processing #{name} @ #{width}... "
|
61
|
+
|
62
|
+
# Run the browser and take screenshots
|
63
|
+
@selenium_handler.run_comparison(width, path, name)
|
64
|
+
|
65
|
+
# Crop images
|
66
|
+
print "Cropping... "
|
67
|
+
@image_handler.crop_images(width, name)
|
68
|
+
|
69
|
+
# Compare images
|
70
|
+
print "Diffing... "
|
71
|
+
@image_handler.diff_images(width, name)
|
72
|
+
|
73
|
+
# Create thumbnails for gallery
|
74
|
+
print "Creating thumbnails... "
|
75
|
+
@image_handler.create_thumbnails(width, name)
|
76
|
+
|
77
|
+
# Upload to S3
|
78
|
+
if Kontrast.configuration.run_parallel
|
79
|
+
print "Uploading... "
|
80
|
+
@image_handler.upload_images(width, name)
|
81
|
+
end
|
82
|
+
|
83
|
+
puts "\n", ("=" * 85)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Log diffs
|
88
|
+
puts @image_handler.diffs
|
89
|
+
|
90
|
+
# Create manifest
|
91
|
+
puts "Creating manifest..."
|
92
|
+
if Kontrast.configuration.run_parallel
|
93
|
+
@image_handler.create_manifest(current_node, Kontrast.configuration.remote_path)
|
94
|
+
else
|
95
|
+
@image_handler.create_manifest(current_node)
|
96
|
+
end
|
97
|
+
ensure
|
98
|
+
@selenium_handler.cleanup
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
def wait_for_server
|
104
|
+
# Test server
|
105
|
+
tries = 30
|
106
|
+
uri = URI(Kontrast.configuration.test_domain)
|
107
|
+
begin
|
108
|
+
Net::HTTP.get(uri)
|
109
|
+
rescue Errno::ECONNREFUSED => e
|
110
|
+
tries -= 1
|
111
|
+
if tries > 0
|
112
|
+
puts "Waiting for test server..."
|
113
|
+
sleep 2
|
114
|
+
retry
|
115
|
+
else
|
116
|
+
raise RunnerException.new("Could not reach the test server at '#{uri}'.")
|
117
|
+
end
|
118
|
+
rescue Exception => e
|
119
|
+
raise RunnerException.new("An unexpected error occured while trying to reach the test server at '#{uri}': #{e.inspect}")
|
120
|
+
end
|
121
|
+
|
122
|
+
# Production server
|
123
|
+
tries = 30
|
124
|
+
uri = URI(Kontrast.configuration.production_domain)
|
125
|
+
begin
|
126
|
+
Net::HTTP.get(uri)
|
127
|
+
rescue Errno::ECONNREFUSED => e
|
128
|
+
tries -= 1
|
129
|
+
if tries > 0
|
130
|
+
puts "Waiting for production server..."
|
131
|
+
sleep 2
|
132
|
+
retry
|
133
|
+
else
|
134
|
+
raise RunnerException.new("Could not reach the production server at '#{uri}'.")
|
135
|
+
end
|
136
|
+
rescue Exception => e
|
137
|
+
raise RunnerException.new("An unexpected error occured while trying to reach the production server at '#{uri}': #{e.inspect}")
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require "selenium-webdriver"
|
2
|
+
require "workers"
|
3
|
+
|
4
|
+
module Kontrast
|
5
|
+
class SeleniumHandler
|
6
|
+
def initialize
|
7
|
+
@path = Kontrast.path
|
8
|
+
|
9
|
+
# Configure profile
|
10
|
+
driver_name = Kontrast.configuration.browser_driver
|
11
|
+
profile = Selenium::WebDriver.const_get(driver_name.capitalize)::Profile.new
|
12
|
+
Kontrast.configuration.browser_profile.each do |option, value|
|
13
|
+
profile[option] = value
|
14
|
+
end
|
15
|
+
|
16
|
+
# Get drivers with profile
|
17
|
+
@test_driver = {
|
18
|
+
name: "test",
|
19
|
+
driver: Selenium::WebDriver.for(driver_name.to_sym, profile: profile)
|
20
|
+
}
|
21
|
+
@production_driver = {
|
22
|
+
name: "production",
|
23
|
+
driver: Selenium::WebDriver.for(driver_name.to_sym, profile: profile)
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
def cleanup
|
28
|
+
# Make sure windows are closed
|
29
|
+
Workers.map([@test_driver, @production_driver]) do |driver|
|
30
|
+
driver[:driver].quit
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def run_comparison(width, path, name)
|
35
|
+
# Create folder for this test
|
36
|
+
current_output = FileUtils.mkdir_p("#{@path}/#{width}_#{name}").join('')
|
37
|
+
|
38
|
+
# Open test host tabs
|
39
|
+
navigate(path)
|
40
|
+
|
41
|
+
# Resize to given width and total height
|
42
|
+
resize(width)
|
43
|
+
|
44
|
+
# Take screenshot
|
45
|
+
begin
|
46
|
+
Kontrast.configuration.before_screenshot(@test_driver[:driver], @production_driver[:driver], { width: width, name: name })
|
47
|
+
screenshot(current_output)
|
48
|
+
ensure
|
49
|
+
Kontrast.configuration.after_screenshot(@test_driver[:driver], @production_driver[:driver], { width: width, name: name })
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
def navigate(path)
|
55
|
+
# Get domains
|
56
|
+
test_host = Kontrast.configuration.test_domain
|
57
|
+
production_host = Kontrast.configuration.production_domain
|
58
|
+
|
59
|
+
Workers.map([@test_driver, @production_driver]) do |driver|
|
60
|
+
if driver[:name] == "test"
|
61
|
+
driver[:driver].navigate.to("#{test_host}#{path}")
|
62
|
+
elsif driver[:name] == "production"
|
63
|
+
driver[:driver].navigate.to("#{production_host}#{path}")
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def resize(width)
|
69
|
+
Workers.map([@test_driver, @production_driver]) do |driver|
|
70
|
+
driver[:driver].manage.window.resize_to(width, driver[:driver].manage.window.size.height)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def screenshot(output_path)
|
75
|
+
Workers.map([@test_driver, @production_driver]) do |driver|
|
76
|
+
if driver[:name] == "test"
|
77
|
+
driver[:driver].save_screenshot("#{output_path}/test.png")
|
78
|
+
elsif driver[:name] == "production"
|
79
|
+
driver[:driver].save_screenshot("#{output_path}/production.png")
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Kontrast
|
2
|
+
class TestBuilder
|
3
|
+
def initialize
|
4
|
+
@tests = Hash.new
|
5
|
+
end
|
6
|
+
|
7
|
+
def add_width(width)
|
8
|
+
@tests[width] = Hash.new
|
9
|
+
@current_width = width
|
10
|
+
end
|
11
|
+
|
12
|
+
# Needed in case someone tries to name a test "tests"
|
13
|
+
def tests(param = nil)
|
14
|
+
if param
|
15
|
+
raise ConfigurationException.new("'tests' is not a valid name for a test.")
|
16
|
+
end
|
17
|
+
return @tests
|
18
|
+
end
|
19
|
+
|
20
|
+
# Adds a given test from config to the suite
|
21
|
+
def method_missing(name, *args)
|
22
|
+
@tests[@current_width][name.to_s] = args.first
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
describe Kontrast::Configuration do
|
2
|
+
it "can set basic options" do
|
3
|
+
Kontrast.configure do |config|
|
4
|
+
config.test_domain = "http://google.com"
|
5
|
+
end
|
6
|
+
expect(Kontrast.configuration.test_domain).to eql "http://google.com"
|
7
|
+
end
|
8
|
+
|
9
|
+
it "cannot set options that don't exist" do
|
10
|
+
expect {
|
11
|
+
Kontrast.configure do |config|
|
12
|
+
config.foo = "bar"
|
13
|
+
end
|
14
|
+
}.to raise_error(NoMethodError)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "sets defaults" do
|
18
|
+
# Set up a config block with no options
|
19
|
+
Kontrast.configure do |config|
|
20
|
+
end
|
21
|
+
|
22
|
+
# Check that we have some defaults
|
23
|
+
expect(Kontrast.configuration.browser_driver).to eql "firefox"
|
24
|
+
expect(Kontrast.configuration.run_parallel).to eql false
|
25
|
+
expect(Kontrast.configuration.distortion_metric).to eql "MeanAbsoluteErrorMetric"
|
26
|
+
end
|
27
|
+
|
28
|
+
it "sets up and runs blocks" do
|
29
|
+
Kontrast.configure do |config|
|
30
|
+
config.before_run do
|
31
|
+
x = 1
|
32
|
+
x += 1
|
33
|
+
# Implicit return of x
|
34
|
+
end
|
35
|
+
end
|
36
|
+
expect(Kontrast.configuration.before_run).to eql 2
|
37
|
+
end
|
38
|
+
|
39
|
+
it "passes data to some blocks" do
|
40
|
+
Kontrast.configure do |config|
|
41
|
+
config.after_gallery do |diffs, gallery|
|
42
|
+
{
|
43
|
+
diffs: diffs,
|
44
|
+
gallery: gallery
|
45
|
+
}
|
46
|
+
end
|
47
|
+
end
|
48
|
+
expect(Kontrast.configuration.after_gallery("arg1", "arg2")).to eql({
|
49
|
+
diffs: "arg1",
|
50
|
+
gallery: "arg2"
|
51
|
+
})
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
describe Kontrast::GalleryCreator do
|
2
|
+
before :all do
|
3
|
+
Kontrast.configure do |config|
|
4
|
+
end
|
5
|
+
end
|
6
|
+
|
7
|
+
before :each do
|
8
|
+
@image_handler = Kontrast::ImageHandler.new
|
9
|
+
@gallery_creator = Kontrast::GalleryCreator.new(@image_handler.path)
|
10
|
+
end
|
11
|
+
|
12
|
+
it "can parse manifests" do
|
13
|
+
# Create fake manifests
|
14
|
+
manifest_1 = {
|
15
|
+
files: ["1280_home/diff.png", "1280_home/diff_thumb.png", "1280_home/production.png", "1280_home/production_thumb.png", "1280_home/test.png", "1280_home/test_thumb.png"],
|
16
|
+
diffs: {
|
17
|
+
"1280_home" => {
|
18
|
+
width: 1280,
|
19
|
+
name: "home",
|
20
|
+
diff: 0.1337
|
21
|
+
}
|
22
|
+
}
|
23
|
+
}
|
24
|
+
|
25
|
+
manifest_2 = {
|
26
|
+
files: ["320_home/production.png", "320_home/production_thumb.png", "320_home/test.png", "320_home/test_thumb.png", "320_home/diff.png", "320_home/diff_thumb.png"],
|
27
|
+
diffs: {}
|
28
|
+
}
|
29
|
+
|
30
|
+
File.open("#{@image_handler.path}/manifest_0.json", 'w') do |outf|
|
31
|
+
outf.write(manifest_1.to_json)
|
32
|
+
end
|
33
|
+
File.open("#{@image_handler.path}/manifest_1.json", 'w') do |outf|
|
34
|
+
outf.write(manifest_2.to_json)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Test get_manifests while we're at it
|
38
|
+
manifests = @gallery_creator.get_manifests
|
39
|
+
expect(manifests).to include(@image_handler.path + "/manifest_0.json", @image_handler.path + "/manifest_1.json")
|
40
|
+
|
41
|
+
# Parse
|
42
|
+
parsed_manifests = @gallery_creator.parse_manifests(manifests)
|
43
|
+
|
44
|
+
expect(parsed_manifests[:diffs]).to eql({
|
45
|
+
"1280_home" => {
|
46
|
+
"width" => 1280,
|
47
|
+
"name" => "home",
|
48
|
+
"diff" => 0.1337
|
49
|
+
}
|
50
|
+
})
|
51
|
+
|
52
|
+
expect(parsed_manifests[:files]).to include("1280_home/diff.png",
|
53
|
+
"1280_home/diff_thumb.png",
|
54
|
+
"1280_home/production.png",
|
55
|
+
"1280_home/production_thumb.png",
|
56
|
+
"1280_home/test.png",
|
57
|
+
"1280_home/test_thumb.png",
|
58
|
+
"320_home/production.png",
|
59
|
+
"320_home/production_thumb.png",
|
60
|
+
"320_home/test.png",
|
61
|
+
"320_home/test_thumb.png",
|
62
|
+
"320_home/diff.png",
|
63
|
+
"320_home/diff_thumb.png")
|
64
|
+
end
|
65
|
+
|
66
|
+
it "can create the gallery hash" do
|
67
|
+
files = ["1280_home/diff.png",
|
68
|
+
"1280_home/diff_thumb.png",
|
69
|
+
"1280_home/production.png",
|
70
|
+
"1280_home/production_thumb.png",
|
71
|
+
"1280_home/test.png",
|
72
|
+
"1280_home/test_thumb.png",
|
73
|
+
"320_home/production.png",
|
74
|
+
"320_home/production_thumb.png",
|
75
|
+
"320_home/test.png",
|
76
|
+
"320_home/test_thumb.png",
|
77
|
+
"320_home/diff.png",
|
78
|
+
"320_home/diff_thumb.png"]
|
79
|
+
diffs = {
|
80
|
+
"1280_home" => {
|
81
|
+
"width" => 1280,
|
82
|
+
"name" => "home",
|
83
|
+
"diff" => 0.1337
|
84
|
+
}
|
85
|
+
}
|
86
|
+
|
87
|
+
dirs = @gallery_creator.parse_directories(files, diffs)
|
88
|
+
expect(dirs).to eql({
|
89
|
+
"1280" => {
|
90
|
+
"home" => {
|
91
|
+
:variants => [{:image=>"../1280_home/test.png", :thumb=>"../1280_home/test_thumb.png", :domain=>"test"},
|
92
|
+
{:image=>"../1280_home/production.png", :thumb=>"../1280_home/production_thumb.png", :domain=>"production"},
|
93
|
+
{:image=>"../1280_home/diff.png", :thumb=>"../1280_home/diff_thumb.png", :domain=>"diff", :diff_amt=>0.1337}]
|
94
|
+
}
|
95
|
+
},
|
96
|
+
"320" => {
|
97
|
+
"home" => {
|
98
|
+
:variants => [{:image=>"../320_home/test.png", :thumb=>"../320_home/test_thumb.png", :domain=>"test"},
|
99
|
+
{:image=>"../320_home/production.png", :thumb=>"../320_home/production_thumb.png", :domain=>"production"},
|
100
|
+
{:image=>"../320_home/diff.png", :thumb=>"../320_home/diff_thumb.png", :domain=>"diff", :diff_amt=>0}]
|
101
|
+
}
|
102
|
+
}
|
103
|
+
})
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
describe Kontrast::ImageHandler do
|
2
|
+
before :all do
|
3
|
+
Kontrast.configure do |config|
|
4
|
+
# Set up some tests
|
5
|
+
config.pages(1280) do |page|
|
6
|
+
page.home "/"
|
7
|
+
page.products "/"
|
8
|
+
end
|
9
|
+
config.pages(320) do |page|
|
10
|
+
page.home "/"
|
11
|
+
page.products "/"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
before :each do
|
17
|
+
@image_handler = Kontrast::ImageHandler.new
|
18
|
+
end
|
19
|
+
|
20
|
+
it "can create a manifest for the current node" do
|
21
|
+
# Create some files
|
22
|
+
Kontrast.test_suite.tests.each do |size, tests|
|
23
|
+
tests.each do |name, path|
|
24
|
+
test_name = "#{size}_#{name}"
|
25
|
+
path = FileUtils.mkdir_p(@image_handler.path + "/#{test_name}").join('')
|
26
|
+
Dir.chdir(path)
|
27
|
+
FileUtils.touch("diff_thumb.png")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Create a diff
|
32
|
+
@image_handler.diffs["1280_home"] = {
|
33
|
+
width: 1280,
|
34
|
+
name: "home",
|
35
|
+
diff: 0.1337
|
36
|
+
}
|
37
|
+
|
38
|
+
# Create the manifest
|
39
|
+
contents = @image_handler.create_manifest(0)
|
40
|
+
|
41
|
+
# Expectations
|
42
|
+
expect(contents[:diffs]).to eql({
|
43
|
+
"1280_home" => {
|
44
|
+
:width => 1280,
|
45
|
+
:name=> "home" ,
|
46
|
+
:diff => 0.1337
|
47
|
+
}
|
48
|
+
})
|
49
|
+
|
50
|
+
expect(contents[:files]).to include("1280_home/diff_thumb.png", "1280_products/diff_thumb.png", "320_home/diff_thumb.png", "320_products/diff_thumb.png")
|
51
|
+
end
|
52
|
+
end
|