simulacrum 0.1.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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