golden_child 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3b5bd24b65d42730252fa72b8c65690b45376497
4
+ data.tar.gz: 764d9fc4dbffc4d99be6deaad9c92b90d5b63802
5
+ SHA512:
6
+ metadata.gz: 13feb14a993bdb4611918dc97f21e39eff26fab99ed5924de3dad980becdd51c5f5bbd649cf75c941e9504238b4878fab28a1aad5fcf8ba879b835df5a9406cc
7
+ data.tar.gz: d3c5b0fdd7884fdf710b73d0cf4211520ef96e0f469bb187789bb8c040fa81bfc90eda2878ad55b603b7faecf86e3d8cdb22cb79acc45a3539ae19a0fc3a3cd7
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in golden_child.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Avdi Grimm
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,23 @@
1
+ # GoldenChild
2
+
3
+ Some helpers for doing golden master testing in Ruby.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'golden_child'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install golden_child
20
+
21
+ ## Usage
22
+
23
+ TBD.
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,23 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'golden_child/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "golden_child"
8
+ spec.version = GoldenChild::VERSION
9
+ spec.authors = ["Avdi Grimm"]
10
+ spec.email = ["avdi@avdi.org"]
11
+ spec.summary = %q{Some helpers for golden master testing in Ruby.}
12
+ spec.description = %q{}
13
+ spec.homepage = "https://github.com/avdi/golden_master"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.6"
22
+ spec.add_development_dependency "rake", "~> 10.0"
23
+ end
@@ -0,0 +1,80 @@
1
+ require "golden_child/version"
2
+ require "golden_child/helpers"
3
+ require "golden_child/scenario"
4
+ require "golden_child/configuration"
5
+ require "fileutils"
6
+ require "pathname"
7
+ require "yaml/store"
8
+ require "forwardable"
9
+
10
+ module GoldenChild
11
+ class Error < StandardError;
12
+ end
13
+ class UserError < Error;
14
+ end
15
+
16
+ extend FileUtils
17
+ extend SingleForwardable
18
+
19
+ def_delegators :configuration, :golden_path, :project_root, :master_root,
20
+ :actual_root
21
+ def_delegators :configuration, :get_path_for_shortcode
22
+
23
+ # @return [GoldenChild::Configuration]
24
+ def self.configuration
25
+ @configuration ||= Configuration.new
26
+ end
27
+
28
+ # @yield [GoldenChild::Configuration] the global configuration
29
+ def self.configure
30
+ yield configuration
31
+ end
32
+
33
+ # @param [Array<String, Pathname>] paths or shortcodes for files to accept
34
+ def self.accept(*filenames)
35
+ filenames.each do |fn|
36
+ accept_file(fn)
37
+ end
38
+ end
39
+
40
+ def self.remove(*filenames)
41
+ filenames.each do |fn|
42
+ remove_master_file(fn)
43
+ end
44
+ end
45
+
46
+ def self.accept_file(path_or_shortcode)
47
+ path = resolve_path(path_or_shortcode)
48
+ master_path = find_master_for(path)
49
+ mkpath master_path.dirname
50
+ cp path, master_path
51
+ end
52
+
53
+
54
+ def self.remove_master_file(path_or_shortcode)
55
+ path = resolve_path(path_or_shortcode)
56
+ master_path = find_master_for(path)
57
+ rm master_path
58
+ end
59
+
60
+ # @return [Pathname]
61
+ def self.resolve_path(path_or_shortcode)
62
+ path = case path_or_shortcode
63
+ when /^@\d+$/
64
+ get_path_for_shortcode(path_or_shortcode)
65
+ else
66
+ path_or_shortcode
67
+ end
68
+ Pathname(path)
69
+ end
70
+
71
+ def self.find_master_for(path)
72
+ raise UserError, "No such file #{path}" unless path.exist?
73
+ raise UserError, "Not a file: #{path}" unless path.file?
74
+ rel_path = path.relative_path_from(actual_root)
75
+ unless rel_path
76
+ raise UserError, "File #{path} is not in #{actual_root}"
77
+ end
78
+ master_root + rel_path
79
+ end
80
+ end
@@ -0,0 +1,19 @@
1
+ module GoldenChild
2
+ BlockContentFilter = Struct.new(:patterns, :block) do
3
+ # @return [true, false] whether any of the patterns match
4
+ def ===(filename)
5
+ patterns.any?{|pattern|
6
+ case pattern
7
+ when String
8
+ File.fnmatch(pattern, filename.to_s)
9
+ else
10
+ pattern === filename.to_s
11
+ end
12
+ }
13
+ end
14
+
15
+ def call(file_content)
16
+ block.call(file_content)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,84 @@
1
+ require "golden_child/block_content_filter"
2
+
3
+ module GoldenChild
4
+ class Configuration
5
+ # @return [Pathname] the directory in which "actual" results will be generated
6
+ def actual_root
7
+ golden_path + "actual"
8
+ end
9
+
10
+ # @return [Pathname] the base directory for gold master dirs
11
+ def master_root
12
+ golden_path + "master"
13
+ end
14
+
15
+ # @return [Pathname] the root directory for everything GoldenChild does
16
+ def golden_path
17
+ Pathname("spec/golden")
18
+ end
19
+
20
+ # @return [Pathname] the root directory of the current project
21
+ def project_root
22
+ @project_root ||= Pathname.pwd
23
+ end
24
+
25
+ # @param [String, Pathname] new_root set the project root directory
26
+ def project_root=(new_root)
27
+ @project_root = Pathname(new_root).expand_path
28
+ end
29
+
30
+ # @return [Hash] The global, editable set of default env vars
31
+ def env
32
+ @env ||= {}
33
+ end
34
+
35
+ # @return [Enumerable<BlockContentFilter>] the configured
36
+ # content filters
37
+ def content_filters
38
+ @content_filters ||= []
39
+ end
40
+
41
+ # Add a filter for a given file pattern. Filters are useful for removing
42
+ # volatile information (like timestamps) from the files to be compared.
43
+ # **NOTE:** filters are currently **not** suitable for removing sensitive
44
+ # information, since they are only applied when diffing files. The files
45
+ # on disk are not filtered.
46
+ #
47
+ # @param [Array<String, [#===]>] patterns Filename patterns that
48
+ # determine which files this filter should be applied to. Strings are
49
+ # treated as file glob patterns.
50
+ # @yieldparam [String] file_content the contents of the file
51
+ # @yieldreturn [String] the filtered file contents
52
+ def add_content_filter(*patterns, &filter)
53
+ content_filters << BlockContentFilter.new(patterns, filter)
54
+ end
55
+
56
+ # @param [String] code
57
+ # @return [String] path to the corresponding file
58
+ # @raise [UserError] if the shortcode is not found
59
+ #
60
+ # TODO: Move this out of Configuration
61
+ def get_path_for_shortcode(code)
62
+ value = code[/\d+/].to_i
63
+ state_transaction(read_only: true) do |store|
64
+ store[:shortcode_map].invert.fetch(value) do
65
+ fail UserError, "Shortcode not found: #{code}"
66
+ end
67
+ end
68
+ end
69
+
70
+ # @yield [YAML::Store]
71
+ #
72
+ # TODO: Move this out of Configuration
73
+ def state_transaction(read_only: false)
74
+ config_dir = project_root + ".golden_child"
75
+ mkpath config_dir unless config_dir.exist?
76
+ state_db = config_dir + "state.yaml"
77
+ store = YAML::Store.new(state_db)
78
+ store.transaction(read_only) do
79
+ yield store
80
+ end
81
+ end
82
+
83
+ end
84
+ end
@@ -0,0 +1,30 @@
1
+ require "fileutils"
2
+
3
+ module GoldenChild
4
+ module Helpers
5
+ include FileUtils
6
+
7
+ attr_writer :scenario
8
+
9
+ # @return [Scenario] the currently active scenario
10
+ def scenario
11
+ @scenario or fail "You must set the scenario first"
12
+ end
13
+
14
+ # (see Scenario#populate_from)
15
+ def populate_from(source_dir)
16
+ scenario.populate_from(source_dir, caller)
17
+ end
18
+
19
+ # (see Scenario#run)
20
+ def run(*args, ** options, &block)
21
+ scenario.run(*args, caller: caller, ** options)
22
+ end
23
+
24
+ # (see Scenario#within_zip)
25
+ def within_zip(*args, &block)
26
+ scenario.within_zip(*args, &block)
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,43 @@
1
+ require "golden_child"
2
+
3
+ module GoldenChild::RspecMatchers
4
+ extend RSpec::Matchers::DSL
5
+
6
+ matcher :match_master do |**options|
7
+ match do |actual|
8
+ @result = scenario.validate(*actual, **options)
9
+ @result.passed?
10
+ end
11
+
12
+ failure_message do |actual|
13
+ @result.message
14
+ end
15
+ end
16
+ end
17
+
18
+ RSpec.configure do |config|
19
+ config.include GoldenChild::Helpers, golden: true
20
+ config.include GoldenChild::RspecMatchers, golden: true
21
+
22
+ config.before(:example, golden: true) do |example|
23
+ # If it looks like an RSpec-Given example, use the example group name.
24
+ #
25
+ # In RSpec-Given the group names the scenario,and the example names are
26
+ # messy source code.
27
+ #
28
+ # TODO: Determine if this is a problem for nested RSpec-Given groups
29
+ scenario_name = if example.description =~ /^\s*Then\b/
30
+ example.example_group.description
31
+ else
32
+ example.full_description
33
+ end
34
+ self.scenario = GoldenChild::Scenario.new(name: scenario_name)
35
+ example.metadata[:golden_child_scenario] = scenario
36
+ scenario.setup
37
+ end
38
+
39
+ config.after(:example, golden: true) do |example|
40
+ scenario.teardown
41
+ end
42
+ end
43
+
@@ -0,0 +1,273 @@
1
+ require "fileutils"
2
+ require "yaml"
3
+ require "open3"
4
+ require "rake/file_list"
5
+ require "forwardable"
6
+ require "rspec/support"
7
+
8
+ module GoldenChild
9
+ class Scenario
10
+ include FileUtils
11
+ extend Forwardable
12
+
13
+ attr_reader :name, :command_history, :configuration
14
+
15
+ # @!method project_root
16
+ # (see Configuration#project_root)
17
+ def_delegators :configuration, :golden_path, :actual_root, :project_root,
18
+ :content_filters
19
+
20
+ # @abstract
21
+ Validation = Struct.new(:message)
22
+
23
+ class FailedValidation < Validation
24
+ def passed?
25
+ false
26
+ end
27
+ end
28
+ class PassedValidation < Validation
29
+ def passed?
30
+ true
31
+ end
32
+ end
33
+
34
+ # @param [String] name The name of the scenario (e.g. RSpec example group)
35
+ def initialize(name:, configuration: ::GoldenChild.configuration)
36
+ @name = name
37
+ @command_history = []
38
+ @configuration = configuration
39
+ end
40
+
41
+ # Recursively populate the current scenario with copies of files from
42
+ # `source_dir`
43
+ #
44
+ # @param [String, Pathname] source_dir
45
+ # @param [Array] caller
46
+ def populate_from(source_dir, caller=caller)
47
+ Dir.chdir(project_root) do
48
+ raise "Scenario has not been set up" unless actual_path.exist?
49
+ source_dir = Pathname(source_dir)
50
+ unless source_dir.directory?
51
+ fail RuntimeError, "No such directory #{source_dir}", caller
52
+ end
53
+
54
+ Dir.foreach(source_dir) do |entry|
55
+ next if %w[. ..].include?(entry)
56
+ copy_entry source_dir + entry, actual_path + entry
57
+ end
58
+ end
59
+ end
60
+
61
+ # Run a command in the context of the current scenario.
62
+ #
63
+ # @param [Array] args The command. See {Process.spawn} for the various
64
+ # forms supported. Note that any environment should be passed via the
65
+ # `env` option below.
66
+ # @param [true, false] allow_fail (false) Whether to raise an exception
67
+ # if the command fails.
68
+ # @param [Hash] env Environment variables for the command
69
+ # @param [Array] caller
70
+ # @param [Hash] options
71
+ def run(*args, allow_fail: false, env: self.env, caller: caller, ** options)
72
+ options[:chdir] ||= actual_path.to_s
73
+ env = env.map { |k, v| [k.to_s, v.to_s] }.to_h
74
+ stdout, stderr, status = Open3.capture3(env, *args, ** options)
75
+ command_history.push(
76
+ command: args, status: status, stdout: stdout, stderr: stderr)
77
+ command_log = ""
78
+ command_log << "\nCommand: #{args}"
79
+ command_log << "\nEnvironment:"
80
+ env.each_pair do |key, value|
81
+ command_log << "\n #{key}=#{value}"
82
+ end
83
+ command_log << "\nExited with status #{status.exitstatus}"
84
+ command_log << "\n========== Command STDOUT ==========\n"
85
+ command_log << stdout
86
+ command_log << "\n========== End STDOUT ==========\n"
87
+ command_log << "\n========== Command STDERR ==========\n"
88
+ command_log << stderr
89
+ command_log << "\n========== End STDERR ==========\n"
90
+ (control_dir + "commands.log").open("a") do |f|
91
+ f.write(command_log)
92
+ end
93
+ unless status.success? || allow_fail
94
+ fail RuntimeError, command_log, caller
95
+ end
96
+ end
97
+
98
+ # Unzip a zip file, and execute commands in the context of the unzipped
99
+ # directory.
100
+ #
101
+ # You must have the `unzip(1)` program installed for this to work.helper
102
+ #
103
+ # @param [String, Pathname] relative_filename The path of the zip file
104
+ def within_zip(relative_filename)
105
+ relative_filename = Pathname(relative_filename)
106
+ filename = actual_path + relative_filename
107
+ raise "Zip file not found: #{filename}" unless filename.exist?
108
+ unzip_dir = unzip_dir_for(relative_filename)
109
+ mkpath unzip_dir
110
+ unzip_succeeded =
111
+ system(*%W[unzip -qq #{filename} -d #{unzip_dir}])
112
+ raise "Could not unzip #{filename}" unless unzip_succeeded
113
+ push_working_dir(unzip_dir.relative_path_from(current_actual_path)) do
114
+ yield(unzip_dir)
115
+ end
116
+ end
117
+
118
+ # @return [Pathname]
119
+ def unzip_dir_for(relative_filename)
120
+ current_actual_path + (relative_filename.to_s + ".golden_child_unzip")
121
+ end
122
+
123
+ # Verify that `files` are identical to their corresponding gold master
124
+ # files.
125
+ def validate(*files, ** options)
126
+ paths = Rake::FileList[*files.map(&:to_s)]
127
+ pass = true
128
+ message = "No files to validate"
129
+ Dir.chdir(project_root) do
130
+ paths.each do |path|
131
+ master_file = current_master_path + path
132
+ actual_file = current_actual_path + path
133
+ shortcode = get_shortcode_for(actual_file)
134
+ approval_cmd = "golden accept #{shortcode}"
135
+ message = ""
136
+ file_pass = false
137
+ if !actual_file.exist?
138
+ message << "Expected file: #{actual_file}"
139
+ message << "\nto be created, but it was not."
140
+ elsif !actual_file.file?
141
+ message << "Expected: #{actual_file}"
142
+ message << "\n to be a file, but it is a #{actual_file.ftype}."
143
+ elsif !master_file.exist?
144
+ message << "Master: #{master_file}"
145
+ message << "\ndoes not yet exist."
146
+ message << "\nActual file: #{actual_file}"
147
+ message << "\nhas the following content:\n\n"
148
+ message << filter_file(actual_file)
149
+ message << "\n\nIf this looks correct, run `#{approval_cmd}`"
150
+ elsif !master_file.file?
151
+ message << "Master: #{master_file}"
152
+ message << "must be a file, but it is a #{master_file.ftype}."
153
+ elsif filtered_files_differ?(master_file, actual_file)
154
+ message << "Actual: #{actual_file}"
155
+ message << "\ndiffers from master: #{master_file}"
156
+ message << "\n"
157
+ message << diff(master_file, actual_file)
158
+ message << "\n\nIf the changes look correct, run `#{approval_cmd}`"
159
+ else
160
+ message << "Actual file #{actual_file} matches master #{master_file}"
161
+ file_pass = true
162
+ end
163
+ unless file_pass
164
+ pass = false
165
+ break
166
+ end
167
+ end
168
+ end
169
+ if pass
170
+ PassedValidation.new(message)
171
+ else
172
+ FailedValidation.new(message)
173
+ end
174
+ end
175
+
176
+ def filtered_files_differ?(master_file, actual_file)
177
+ filter_file(master_file) != filter_file(actual_file)
178
+ end
179
+
180
+ def diff(master_file, actual_file)
181
+ differ = RSpec::Support::Differ.new
182
+ differences = differ.diff(filter_file(actual_file), filter_file(master_file))
183
+ differences.empty? ? nil : differences
184
+ end
185
+
186
+ def filter_file(filename)
187
+ @filtered_files ||= {} # memoization
188
+ @filtered_files.fetch(filename) {
189
+ @filtered_files[filename] = content_filters.reduce(filename.read) {
190
+ |content, filter|
191
+ filter.call(content)
192
+ }
193
+ }
194
+ end
195
+
196
+ def get_shortcode_for(actual_file)
197
+ code = state_transaction do |store|
198
+ shortcode_map = (store[:shortcode_map] ||= {})
199
+ shortcode_map.fetch(actual_file.to_s) {
200
+ new_code = shortcode_map.values.max.to_i + 1
201
+ shortcode_map[actual_file.to_s] = new_code
202
+ }
203
+ end
204
+ "@#{code}"
205
+ end
206
+
207
+ def setup
208
+ mkpath master_path.parent
209
+ rmtree actual_path
210
+ mkpath actual_path
211
+ mkpath control_dir
212
+ end
213
+
214
+ def teardown
215
+ end
216
+
217
+ def actual_path
218
+ actual_root + relative_path
219
+ end
220
+
221
+ alias_method :root, :actual_path
222
+
223
+ def master_path
224
+ golden_path + "master" + relative_path
225
+ end
226
+
227
+ def current_actual_path
228
+ actual_path + current_working_dir
229
+ end
230
+
231
+ def current_master_path
232
+ master_path + current_working_dir
233
+ end
234
+
235
+ def control_dir
236
+ actual_path + ".golden_child"
237
+ end
238
+
239
+ def relative_path
240
+ slug
241
+ end
242
+
243
+ def slug
244
+ name.downcase.tr_s("^a-z0-9", "-")[0..255]
245
+ end
246
+
247
+ # @return [Hash] editable env var hash, defaults to {#configuration}
248
+ def env
249
+ @env ||= configuration.env.dup
250
+ end
251
+
252
+ private
253
+
254
+ def_delegators :configuration, :state_transaction, :get_path_for_shortcode
255
+
256
+ def push_working_dir(new_dir)
257
+ dir_stack = working_dir_stack
258
+ dir_stack.push(new_dir)
259
+ yield
260
+ ensure
261
+ dir_stack.pop
262
+ end
263
+
264
+ def working_dir_stack
265
+ Thread.current[:golden_child_working_dir] ||= []
266
+ end
267
+
268
+ def current_working_dir
269
+ last_dir = working_dir_stack.last
270
+ last_dir || "."
271
+ end
272
+ end
273
+ end
@@ -0,0 +1,3 @@
1
+ module GoldenChild
2
+ VERSION = "0.0.1"
3
+ end
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: golden_child
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Avdi Grimm
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-08-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.6'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ description: ''
42
+ email:
43
+ - avdi@avdi.org
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".gitignore"
49
+ - Gemfile
50
+ - LICENSE.txt
51
+ - README.md
52
+ - Rakefile
53
+ - golden_child.gemspec
54
+ - lib/golden_child.rb
55
+ - lib/golden_child/block_content_filter.rb
56
+ - lib/golden_child/configuration.rb
57
+ - lib/golden_child/helpers.rb
58
+ - lib/golden_child/rspec.rb
59
+ - lib/golden_child/scenario.rb
60
+ - lib/golden_child/version.rb
61
+ homepage: https://github.com/avdi/golden_master
62
+ licenses:
63
+ - MIT
64
+ metadata: {}
65
+ post_install_message:
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ requirements: []
80
+ rubyforge_project:
81
+ rubygems_version: 2.2.2
82
+ signing_key:
83
+ specification_version: 4
84
+ summary: Some helpers for golden master testing in Ruby.
85
+ test_files: []