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.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/.cane +9 -0
  3. data/.env.example +2 -0
  4. data/.gitignore +10 -0
  5. data/.rspec +3 -0
  6. data/.rubocop.yml +14 -0
  7. data/.travis.yml +28 -0
  8. data/Gemfile +2 -0
  9. data/README.md +78 -19
  10. data/Rakefile +20 -0
  11. data/examples/README.md +3 -0
  12. data/examples/basic/Gemfile +4 -0
  13. data/examples/basic/README.md +13 -0
  14. data/examples/basic/config.ru +3 -0
  15. data/examples/basic/example_app.rb +7 -0
  16. data/examples/basic/public/button.html +13 -0
  17. data/examples/basic/public/index.html +22 -0
  18. data/examples/basic/public/panel.html +13 -0
  19. data/examples/basic/public/stylesheets/button.css +15 -0
  20. data/examples/basic/public/stylesheets/main.css +3 -0
  21. data/examples/basic/public/stylesheets/normalize.css +425 -0
  22. data/examples/basic/script/start +4 -0
  23. data/examples/basic/spec/simulacrum_helper.rb +9 -0
  24. data/examples/basic/spec/ui/button_spec.rb +10 -0
  25. data/exe/simulacrum +5 -0
  26. data/features/command_line/help.feature +8 -0
  27. data/features/exit_codes/failing.feature +24 -0
  28. data/features/exit_codes/passing.feature +24 -0
  29. data/features/exit_codes/pending.feature +22 -0
  30. data/features/output/candidate.feature +32 -0
  31. data/features/output/diff.feature +5 -0
  32. data/features/step_definitions/dummy_steps.rb +15 -0
  33. data/features/step_definitions/file_steps.rb +19 -0
  34. data/features/support/env.rb +15 -0
  35. data/fixtures/a1.png +0 -0
  36. data/fixtures/app/fixture_app.rb +12 -0
  37. data/fixtures/app/public/images/a1.png +0 -0
  38. data/fixtures/app/public/ui_component.html +10 -0
  39. data/fixtures/app/spec/component_spec.rb +9 -0
  40. data/fixtures/app/spec/simulacrum_helper.rb +37 -0
  41. data/fixtures/app/spec/ui/references/ui_component/test_driver/candidate.png +0 -0
  42. data/fixtures/diff.png +0 -0
  43. data/lib/simulacrum.rb +74 -15
  44. data/lib/simulacrum/cli.rb +38 -0
  45. data/lib/simulacrum/cli/parser.rb +152 -0
  46. data/lib/simulacrum/comparator.rb +15 -15
  47. data/lib/simulacrum/component.rb +22 -11
  48. data/lib/simulacrum/configuration.rb +20 -13
  49. data/lib/simulacrum/diff.rb +2 -0
  50. data/lib/simulacrum/diff/rmagick.rb +8 -6
  51. data/lib/simulacrum/driver.rb +45 -0
  52. data/lib/simulacrum/matchers.rb +6 -16
  53. data/lib/simulacrum/methods.rb +1 -0
  54. data/lib/simulacrum/renderer.rb +23 -8
  55. data/lib/simulacrum/runner.rb +44 -0
  56. data/lib/simulacrum/version.rb +2 -1
  57. data/rubocop-todo.yml +29 -0
  58. data/script/bootstrap +3 -0
  59. data/script/quality +7 -0
  60. data/script/spec +10 -0
  61. data/simulacrum.gemspec +52 -0
  62. data/spec/lib/simulacrum/cli/parser_spec.rb +113 -0
  63. data/spec/lib/simulacrum/cli_spec.rb +18 -0
  64. data/spec/lib/simulacrum/comparator_spec.rb +75 -0
  65. data/spec/lib/simulacrum/component_spec.rb +208 -0
  66. data/spec/lib/simulacrum/driver/local_spec.rb +11 -0
  67. data/spec/lib/simulacrum/version_spec.rb +12 -0
  68. data/spec/lib/simulacrum_spec.rb +53 -0
  69. data/spec/spec_helper.rb +13 -8
  70. data/spec/use_codeclimate.rb +3 -0
  71. data/spec/use_simplecov.rb +5 -12
  72. metadata +217 -32
  73. data/lib/simulacrum/diff/pdiff.rb +0 -47
  74. data/spec/fixtures/a.png +0 -0
  75. data/spec/fixtures/a2.png +0 -0
  76. 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
- attr_accessor :component, :candidate, :diff
11
+ attr_reader :component, :diff
11
12
 
12
13
  def initialize(component)
13
14
  @component = component
14
- @component.render
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 @component.reference?
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
- perform_diff ? pass : fail
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
- @component.remove_candidate
37
- @component.remove_diff
42
+ component.remove_candidate
43
+ component.remove_diff
38
44
  true
39
45
  end
40
46
 
41
47
  def fail
42
- @diff.save(@component.diff_path)
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 perform_diff
51
- @diff = Simulacrum::RmagicDiff.new(@component.reference_path,
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
@@ -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
- save_candidate(@renderer.render)
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.exists?(reference_path)
31
+ File.exist?(reference_path)
28
32
  end
29
33
 
30
34
  def candidate?
31
- File.exists?(candidate_path)
35
+ File.exist?(candidate_path)
32
36
  end
33
37
 
34
38
  def diff?
35
- File.exists?(diff_path)
39
+ File.exist?(diff_path)
36
40
  end
37
41
 
38
- def acceptable_delta
39
- options.acceptable_delta || Simulacrum.configuration.acceptable_delta
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::read(candidate_path).first
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(Simulacrum.configuration.references_path, name.to_s, driver_path)
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
- acceptable_delta: 1,
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
- :diff_filename, :acceptable_delta, :capture_delay, :window_size,
15
- :capture_selector, :default_browser
15
+ :diff_filename, :delta_threshold, :capture_delay,
16
+ :window_size, :capture_selector, :default_browser
16
17
 
17
18
  def initialize
18
- @config = OpenStruct.new(defaults: OpenStruct.new(COMPONENT_DEFAULTS))
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 acceptable_delta
50
- @config.defaults.acceptable_delta || 0.0
48
+ def delta_threshold
49
+ @config.component.delta_threshold || 0.0
51
50
  end
52
51
 
53
52
  def capture_selector
54
- @config.defaults.capture_selector || nil
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
@@ -1,4 +1,6 @@
1
+ # encoding: UTF-8
1
2
  module Simulacrum
3
+ # Base class for implementing diffing strategies
2
4
  class Diff
3
5
  attr_accessor :a_path, :b_path, :delta, :image
4
6
 
@@ -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 RmagicDiff < Simulacrum::Diff
6
- def delta_percent
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 = compare_images(a, b)
17
+ @image, @delta = square_root_mean_squared(a, b)
16
18
  end
17
19
 
18
- def compare_images(a, b)
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 the
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