simulacrum 0.1.1 → 0.3.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 +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
|