simulacrum 0.1.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.cane +9 -0
- data/.env.example +2 -0
- data/.gitignore +10 -0
- data/.rspec +3 -0
- data/.rubocop.yml +14 -0
- data/.travis.yml +28 -0
- data/Gemfile +2 -0
- data/README.md +78 -19
- data/Rakefile +20 -0
- data/examples/README.md +3 -0
- data/examples/basic/Gemfile +4 -0
- data/examples/basic/README.md +13 -0
- data/examples/basic/config.ru +3 -0
- data/examples/basic/example_app.rb +7 -0
- data/examples/basic/public/button.html +13 -0
- data/examples/basic/public/index.html +22 -0
- data/examples/basic/public/panel.html +13 -0
- data/examples/basic/public/stylesheets/button.css +15 -0
- data/examples/basic/public/stylesheets/main.css +3 -0
- data/examples/basic/public/stylesheets/normalize.css +425 -0
- data/examples/basic/script/start +4 -0
- data/examples/basic/spec/simulacrum_helper.rb +9 -0
- data/examples/basic/spec/ui/button_spec.rb +10 -0
- data/exe/simulacrum +5 -0
- data/features/command_line/help.feature +8 -0
- data/features/exit_codes/failing.feature +24 -0
- data/features/exit_codes/passing.feature +24 -0
- data/features/exit_codes/pending.feature +22 -0
- data/features/output/candidate.feature +32 -0
- data/features/output/diff.feature +5 -0
- data/features/step_definitions/dummy_steps.rb +15 -0
- data/features/step_definitions/file_steps.rb +19 -0
- data/features/support/env.rb +15 -0
- data/fixtures/a1.png +0 -0
- data/fixtures/app/fixture_app.rb +12 -0
- data/fixtures/app/public/images/a1.png +0 -0
- data/fixtures/app/public/ui_component.html +10 -0
- data/fixtures/app/spec/component_spec.rb +9 -0
- data/fixtures/app/spec/simulacrum_helper.rb +37 -0
- data/fixtures/app/spec/ui/references/ui_component/test_driver/candidate.png +0 -0
- data/fixtures/diff.png +0 -0
- data/lib/simulacrum.rb +74 -15
- data/lib/simulacrum/cli.rb +38 -0
- data/lib/simulacrum/cli/parser.rb +152 -0
- data/lib/simulacrum/comparator.rb +15 -15
- data/lib/simulacrum/component.rb +22 -11
- data/lib/simulacrum/configuration.rb +20 -13
- data/lib/simulacrum/diff.rb +2 -0
- data/lib/simulacrum/diff/rmagick.rb +8 -6
- data/lib/simulacrum/driver.rb +45 -0
- data/lib/simulacrum/matchers.rb +6 -16
- data/lib/simulacrum/methods.rb +1 -0
- data/lib/simulacrum/renderer.rb +23 -8
- data/lib/simulacrum/runner.rb +44 -0
- data/lib/simulacrum/version.rb +2 -1
- data/rubocop-todo.yml +29 -0
- data/script/bootstrap +3 -0
- data/script/quality +7 -0
- data/script/spec +10 -0
- data/simulacrum.gemspec +52 -0
- data/spec/lib/simulacrum/cli/parser_spec.rb +113 -0
- data/spec/lib/simulacrum/cli_spec.rb +18 -0
- data/spec/lib/simulacrum/comparator_spec.rb +75 -0
- data/spec/lib/simulacrum/component_spec.rb +208 -0
- data/spec/lib/simulacrum/driver/local_spec.rb +11 -0
- data/spec/lib/simulacrum/version_spec.rb +12 -0
- data/spec/lib/simulacrum_spec.rb +53 -0
- data/spec/spec_helper.rb +13 -8
- data/spec/use_codeclimate.rb +3 -0
- data/spec/use_simplecov.rb +5 -12
- metadata +217 -32
- data/lib/simulacrum/diff/pdiff.rb +0 -47
- data/spec/fixtures/a.png +0 -0
- data/spec/fixtures/a2.png +0 -0
- data/spec/use_coveralls.rb +0 -2
@@ -0,0 +1,152 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require 'optparse'
|
3
|
+
require 'ostruct'
|
4
|
+
require 'simulacrum'
|
5
|
+
|
6
|
+
module Simulacrum
|
7
|
+
module CLI
|
8
|
+
# Option parser for handling options passed into the Simulacrum CLI
|
9
|
+
#
|
10
|
+
# This class is mostly borrowed from Cane's Parser class. Thanks Xav! <3
|
11
|
+
class Parser
|
12
|
+
attr_reader :stdout
|
13
|
+
|
14
|
+
# Exception to indicate that no further processing is required and the
|
15
|
+
# program can exit. This is used to handle --help and --version flags.
|
16
|
+
class OptionsHandled < RuntimeError; end
|
17
|
+
|
18
|
+
def self.parse(args)
|
19
|
+
new.parse(args)
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(stdout = $stdout)
|
23
|
+
@stdout = stdout
|
24
|
+
|
25
|
+
add_banner
|
26
|
+
add_runner_options
|
27
|
+
add_format_options
|
28
|
+
add_separator
|
29
|
+
add_version
|
30
|
+
add_help
|
31
|
+
end
|
32
|
+
|
33
|
+
def parse(args, _return = true)
|
34
|
+
parser.parse!(args)
|
35
|
+
options['files'] = args if args.size > 0
|
36
|
+
OpenStruct.new(default_options.merge(options))
|
37
|
+
rescue OptionParser::InvalidOption,
|
38
|
+
OptionParser::AmbiguousOption
|
39
|
+
args = %w(--help)
|
40
|
+
_return = false
|
41
|
+
retry
|
42
|
+
rescue OptionsHandled
|
43
|
+
_return
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def default_options
|
49
|
+
read_options_from_file.merge(username: ENV['SIMULACRUM_USERNAME'],
|
50
|
+
apikey: ENV['SIMULACRUM_APIKEY'])
|
51
|
+
end
|
52
|
+
|
53
|
+
def read_options_from_file
|
54
|
+
if Simulacrum.config_file?
|
55
|
+
filter_file_options(Simulacrum.config_file)
|
56
|
+
else
|
57
|
+
{}
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def filter_file_options(file_options)
|
62
|
+
file_options.tap do |hash|
|
63
|
+
hash.delete('username')
|
64
|
+
hash.delete('apikey')
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def add_banner
|
69
|
+
parser.banner = 'Usage: simulacrum [options] [files or directories]'
|
70
|
+
add_separator
|
71
|
+
end
|
72
|
+
|
73
|
+
def add_separator
|
74
|
+
parser.separator ''
|
75
|
+
end
|
76
|
+
|
77
|
+
def add_version
|
78
|
+
parser.on_tail('--version', 'Show version') do
|
79
|
+
stdout.puts Simulacrum::VERSION
|
80
|
+
fail OptionsHandled
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def add_help
|
85
|
+
parser.on_tail('-h', '--help', 'Show this message') do
|
86
|
+
stdout.puts parser
|
87
|
+
fail OptionsHandled
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def add_runner_options
|
92
|
+
parser.on('--runner [RUNNER]',
|
93
|
+
[:browserstack],
|
94
|
+
'Runner to use for executing specs (browserstack).') do |runner|
|
95
|
+
options['runner'] = runner
|
96
|
+
end
|
97
|
+
|
98
|
+
parser.on('--username [USERNAME]',
|
99
|
+
'Username for authenticating when using the Browserstack runner.') do |username|
|
100
|
+
options['username'] = username
|
101
|
+
end
|
102
|
+
|
103
|
+
parser.on('--apikey [APIKEY]',
|
104
|
+
'API key for authenticating when using the Browserstack runner.') do |apikey|
|
105
|
+
options['apikey'] = apikey
|
106
|
+
end
|
107
|
+
|
108
|
+
parser.on('--max-processes [N]',
|
109
|
+
Integer,
|
110
|
+
'Number of parallel proceses the runner should use.') do |max_processes|
|
111
|
+
options['max_processes'] = max_processes.to_i
|
112
|
+
end
|
113
|
+
|
114
|
+
parser.on('--browser [BROWSER]',
|
115
|
+
'Browser configuration to use') do |browser|
|
116
|
+
options['browser'] = browser.to_sym
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def add_format_options
|
121
|
+
add_separator
|
122
|
+
|
123
|
+
parser.on('-c',
|
124
|
+
'--[no-]color',
|
125
|
+
'--[no-]colour',
|
126
|
+
'Enable color in the output.') do |value|
|
127
|
+
options['color'] = value
|
128
|
+
end
|
129
|
+
|
130
|
+
parser.on('-v',
|
131
|
+
'--verbose',
|
132
|
+
'Be more shouty.') do |value|
|
133
|
+
options['verbose'] = value
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def options
|
138
|
+
@options ||= begin
|
139
|
+
options = {}
|
140
|
+
options['files'] = ['spec/ui']
|
141
|
+
options['color'] = false
|
142
|
+
options['verbose'] = false
|
143
|
+
options
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def parser
|
148
|
+
@parser ||= OptionParser.new
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# encoding: UTF-8
|
1
2
|
require 'rspec'
|
2
3
|
require_relative './diff/rmagick'
|
3
4
|
|
@@ -7,20 +8,20 @@ module Simulacrum
|
|
7
8
|
class Comparator
|
8
9
|
include RSpec::Core::Pending
|
9
10
|
|
10
|
-
|
11
|
+
attr_reader :component, :diff
|
11
12
|
|
12
13
|
def initialize(component)
|
13
14
|
@component = component
|
14
|
-
|
15
|
+
component.render
|
15
16
|
end
|
16
17
|
|
17
18
|
def test
|
18
19
|
# If the component has a reference then we should diff the candidate
|
19
20
|
# image against the reference
|
20
|
-
if
|
21
|
+
if component.reference?
|
21
22
|
# If there is a diff between the candidate and the reference then we
|
22
23
|
# should save both the candidate and diff images and fail the test
|
23
|
-
|
24
|
+
(pass?) ? pass : fail
|
24
25
|
|
25
26
|
# Otherwise we should just write the captured candidate to disk, and mark
|
26
27
|
# the spec as being pending until the user works out if the candidate is
|
@@ -30,16 +31,21 @@ module Simulacrum
|
|
30
31
|
end
|
31
32
|
end
|
32
33
|
|
34
|
+
def diff
|
35
|
+
@diff ||= Simulacrum::RMagicDiff.new(component.reference_path,
|
36
|
+
component.candidate_path)
|
37
|
+
end
|
38
|
+
|
33
39
|
private
|
34
40
|
|
35
41
|
def pass
|
36
|
-
|
37
|
-
|
42
|
+
component.remove_candidate
|
43
|
+
component.remove_diff
|
38
44
|
true
|
39
45
|
end
|
40
46
|
|
41
47
|
def fail
|
42
|
-
|
48
|
+
diff.save(component.diff_path)
|
43
49
|
false
|
44
50
|
end
|
45
51
|
|
@@ -47,14 +53,8 @@ module Simulacrum
|
|
47
53
|
nil
|
48
54
|
end
|
49
55
|
|
50
|
-
def
|
51
|
-
|
52
|
-
@component.candidate_path)
|
53
|
-
diff_delta_percent_is_acceptable
|
54
|
-
end
|
55
|
-
|
56
|
-
def diff_delta_percent_is_acceptable
|
57
|
-
@diff.delta_percent < @component.acceptable_delta
|
56
|
+
def pass?
|
57
|
+
diff.delta <= component.delta_threshold
|
58
58
|
end
|
59
59
|
end
|
60
60
|
end
|
data/lib/simulacrum/component.rb
CHANGED
@@ -1,12 +1,15 @@
|
|
1
|
+
# encoding: UTF-8
|
1
2
|
require 'capybara/dsl'
|
2
3
|
require 'fileutils'
|
3
4
|
require 'RMagick'
|
4
5
|
require_relative 'renderer'
|
5
6
|
|
6
7
|
module Simulacrum
|
8
|
+
# Component Class is responsible for managing the testing of a component
|
9
|
+
# defined in a test suite
|
7
10
|
class Component
|
8
|
-
attr_reader :name, :browser
|
9
11
|
attr_accessor :options
|
12
|
+
attr_reader :name
|
10
13
|
|
11
14
|
def initialize(name, options = {})
|
12
15
|
@name = name
|
@@ -14,42 +17,47 @@ module Simulacrum
|
|
14
17
|
@renderer = Simulacrum::Renderer.new(options.url)
|
15
18
|
end
|
16
19
|
|
17
|
-
# Load up the component url and capture an image, returns a File object
|
18
20
|
def render
|
19
21
|
ensure_example_path
|
20
|
-
|
22
|
+
tmp_path = @renderer.render
|
23
|
+
save_candidate(tmp_path)
|
21
24
|
crop_candidate_to_selector
|
25
|
+
true
|
22
26
|
ensure
|
23
27
|
cleanup
|
24
28
|
end
|
25
29
|
|
26
30
|
def reference?
|
27
|
-
File.
|
31
|
+
File.exist?(reference_path)
|
28
32
|
end
|
29
33
|
|
30
34
|
def candidate?
|
31
|
-
File.
|
35
|
+
File.exist?(candidate_path)
|
32
36
|
end
|
33
37
|
|
34
38
|
def diff?
|
35
|
-
File.
|
39
|
+
File.exist?(diff_path)
|
36
40
|
end
|
37
41
|
|
38
|
-
def
|
39
|
-
|
42
|
+
def delta_threshold
|
43
|
+
# TODO: These should probably be `configuration.defaults....`
|
44
|
+
options.delta_threshold || Simulacrum.configuration.delta_threshold
|
40
45
|
end
|
41
46
|
|
42
47
|
def reference_path
|
48
|
+
# TODO: These should probably be `configuration.defaults....`
|
43
49
|
filename = Simulacrum.configuration.reference_filename
|
44
50
|
File.join(root_path, "#{filename}.png")
|
45
51
|
end
|
46
52
|
|
47
53
|
def candidate_path
|
54
|
+
# TODO: These should probably be `configuration.defaults....`
|
48
55
|
filename = Simulacrum.configuration.candidate_filename
|
49
56
|
File.join(root_path, "#{filename}.png")
|
50
57
|
end
|
51
58
|
|
52
59
|
def diff_path
|
60
|
+
# TODO: These should probably be `configuration.defaults....`
|
53
61
|
filename = Simulacrum.configuration.diff_filename
|
54
62
|
File.join(root_path, "#{filename}.png")
|
55
63
|
end
|
@@ -66,7 +74,6 @@ module Simulacrum
|
|
66
74
|
|
67
75
|
def cleanup
|
68
76
|
@renderer.cleanup
|
69
|
-
# FileUtils.remove_entry(root_path) unless reference? || candidate? || diff?
|
70
77
|
end
|
71
78
|
|
72
79
|
def save_candidate(tmp_image_path)
|
@@ -75,7 +82,7 @@ module Simulacrum
|
|
75
82
|
|
76
83
|
def crop_candidate_to_selector
|
77
84
|
unless capture_selector.nil?
|
78
|
-
candidate_image = Magick::Image
|
85
|
+
candidate_image = Magick::Image.read(candidate_path).first
|
79
86
|
bounds = @renderer.get_bounds_for_selector(capture_selector)
|
80
87
|
candidate_image.crop!(*bounds)
|
81
88
|
candidate_image.write(candidate_path)
|
@@ -83,7 +90,11 @@ module Simulacrum
|
|
83
90
|
end
|
84
91
|
|
85
92
|
def root_path
|
86
|
-
File.join(
|
93
|
+
File.join(
|
94
|
+
Simulacrum.configuration.references_path,
|
95
|
+
name.to_s,
|
96
|
+
driver_path
|
97
|
+
)
|
87
98
|
end
|
88
99
|
|
89
100
|
def driver_path
|
@@ -1,36 +1,35 @@
|
|
1
|
+
# encoding: UTF-8
|
1
2
|
require 'ostruct'
|
2
3
|
|
3
4
|
module Simulacrum
|
5
|
+
# Configuration Class for managing config of the suite
|
4
6
|
class Configuration
|
5
|
-
|
6
7
|
COMPONENT_DEFAULTS = {
|
7
|
-
|
8
|
+
delta_threshold: 1,
|
8
9
|
window_size: [1024, 768],
|
9
10
|
capture_delay: nil,
|
10
|
-
capture_selector: :html
|
11
|
+
capture_selector: :html
|
11
12
|
}
|
12
13
|
|
13
14
|
attr_reader :references_path, :reference_filename, :candidate_filename,
|
14
|
-
|
15
|
-
|
15
|
+
:diff_filename, :delta_threshold, :capture_delay,
|
16
|
+
:window_size, :capture_selector, :default_browser
|
16
17
|
|
17
18
|
def initialize
|
18
|
-
@config = OpenStruct.new(
|
19
|
+
@config = OpenStruct.new(component: OpenStruct.new(COMPONENT_DEFAULTS))
|
19
20
|
end
|
20
21
|
|
21
22
|
def configure(config)
|
22
23
|
@config = OpenStruct.new(@config.to_h.merge!(config))
|
23
24
|
end
|
24
25
|
|
25
|
-
def default_browser
|
26
|
-
@config.default_browser || :selenium
|
27
|
-
end
|
28
|
-
|
29
26
|
def references_path
|
30
27
|
if @config.references_path
|
31
28
|
@config.references_path
|
32
29
|
elsif defined?(Rails)
|
33
30
|
File.join(Rails.root, 'spec/ui/references')
|
31
|
+
else
|
32
|
+
'spec/ui/references'
|
34
33
|
end
|
35
34
|
end
|
36
35
|
|
@@ -46,12 +45,20 @@ module Simulacrum
|
|
46
45
|
@config.diff_filename || 'diff'
|
47
46
|
end
|
48
47
|
|
49
|
-
def
|
50
|
-
@config.
|
48
|
+
def delta_threshold
|
49
|
+
@config.component.delta_threshold || 0.0
|
51
50
|
end
|
52
51
|
|
53
52
|
def capture_selector
|
54
|
-
@config.
|
53
|
+
@config.component.capture_selector || nil
|
54
|
+
end
|
55
|
+
|
56
|
+
def build_name
|
57
|
+
@config.build_name || ''
|
58
|
+
end
|
59
|
+
|
60
|
+
def project_name
|
61
|
+
@config.project_name || 'Simulacrum'
|
55
62
|
end
|
56
63
|
end
|
57
64
|
end
|
data/lib/simulacrum/diff.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
|
+
# encoding: UTF-8
|
1
2
|
require 'RMagick'
|
2
3
|
require_relative '../diff'
|
3
4
|
|
4
5
|
module Simulacrum
|
5
|
-
class
|
6
|
-
|
6
|
+
# The RMagicDiff class implements image diffing using ImageMagick
|
7
|
+
class RMagicDiff < Simulacrum::Diff
|
8
|
+
def delta
|
7
9
|
@delta * 100
|
8
10
|
end
|
9
11
|
|
@@ -12,15 +14,15 @@ module Simulacrum
|
|
12
14
|
def compare
|
13
15
|
a = Magick::Image.read(@a_path)
|
14
16
|
b = Magick::Image.read(@b_path)
|
15
|
-
@image, @delta =
|
17
|
+
@image, @delta = square_root_mean_squared(a, b)
|
16
18
|
end
|
17
19
|
|
18
|
-
def
|
20
|
+
def square_root_mean_squared(a, b)
|
19
21
|
# Calculate the Square Root Mean Squared Error for the comparison of the
|
20
22
|
# two images.
|
21
23
|
#
|
22
|
-
# Gets the color difference for each pixel, and square it, average all
|
23
|
-
# squared differences, then return the square root.
|
24
|
+
# Gets the color difference for each pixel, and square it, average all
|
25
|
+
# the squared differences, then return the square root.
|
24
26
|
#
|
25
27
|
# http://www.imagemagick.org/discourse-server/viewtopic.php?f=1&t=17284
|
26
28
|
a[0].compare_channel(b[0], Magick::MeanSquaredErrorMetric)
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
module Simulacrum
|
3
|
+
# Base class for Drivers to inherit
|
4
|
+
class Driver
|
5
|
+
def self.use
|
6
|
+
new.use
|
7
|
+
end
|
8
|
+
|
9
|
+
def use
|
10
|
+
register_driver
|
11
|
+
configure_capybara
|
12
|
+
self
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def capabilities
|
18
|
+
end
|
19
|
+
|
20
|
+
def configuration
|
21
|
+
{ browser: :firefox }
|
22
|
+
end
|
23
|
+
|
24
|
+
def register_driver
|
25
|
+
Capybara.register_driver driver_name do |app|
|
26
|
+
Capybara::Selenium::Driver.new(app, configuration)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def configure_capybara
|
31
|
+
Capybara.default_driver = driver_name
|
32
|
+
Capybara.default_wait_time = 10
|
33
|
+
Capybara.server_host = 'localhost'
|
34
|
+
Capybara.server_port = app_server_port
|
35
|
+
end
|
36
|
+
|
37
|
+
def app_server_port
|
38
|
+
ENV['APP_SERVER_PORT'].to_i if ENV['APP_SERVER_PORT']
|
39
|
+
end
|
40
|
+
|
41
|
+
def driver_name
|
42
|
+
'default'
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|