golden_child 0.0.1
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 +7 -0
- data/.gitignore +14 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +23 -0
- data/Rakefile +2 -0
- data/golden_child.gemspec +23 -0
- data/lib/golden_child.rb +80 -0
- data/lib/golden_child/block_content_filter.rb +19 -0
- data/lib/golden_child/configuration.rb +84 -0
- data/lib/golden_child/helpers.rb +30 -0
- data/lib/golden_child/rspec.rb +43 -0
- data/lib/golden_child/scenario.rb +273 -0
- data/lib/golden_child/version.rb +3 -0
- metadata +85 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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.
|
data/Rakefile
ADDED
@@ -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
|
data/lib/golden_child.rb
ADDED
@@ -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
|
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: []
|