br-approvals 0.0.22

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 (110) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.travis.yml +15 -0
  4. data/CHANGELOG.md +71 -0
  5. data/Gemfile +6 -0
  6. data/License.txt +22 -0
  7. data/README.md +263 -0
  8. data/Rakefile +6 -0
  9. data/approvals.gemspec +29 -0
  10. data/bin/approvals +8 -0
  11. data/ext/mkrf_conf.rb +22 -0
  12. data/lib/approvals.rb +33 -0
  13. data/lib/approvals/approval.rb +144 -0
  14. data/lib/approvals/cli.rb +36 -0
  15. data/lib/approvals/configuration.rb +29 -0
  16. data/lib/approvals/dotfile.rb +33 -0
  17. data/lib/approvals/dsl.rb +7 -0
  18. data/lib/approvals/error.rb +21 -0
  19. data/lib/approvals/executable.rb +18 -0
  20. data/lib/approvals/extensions/rspec.rb +13 -0
  21. data/lib/approvals/extensions/rspec/dsl.rb +44 -0
  22. data/lib/approvals/filter.rb +45 -0
  23. data/lib/approvals/namers/default_namer.rb +20 -0
  24. data/lib/approvals/namers/directory_namer.rb +30 -0
  25. data/lib/approvals/namers/rspec_namer.rb +32 -0
  26. data/lib/approvals/reporters.rb +14 -0
  27. data/lib/approvals/reporters/diff_reporter/diffmerge_reporter.rb +18 -0
  28. data/lib/approvals/reporters/diff_reporter/opendiff_reporter.rb +18 -0
  29. data/lib/approvals/reporters/diff_reporter/tortoisediff_reporter.rb +18 -0
  30. data/lib/approvals/reporters/diff_reporter/vimdiff_reporter.rb +18 -0
  31. data/lib/approvals/reporters/filelauncher_reporter.rb +18 -0
  32. data/lib/approvals/reporters/first_working_reporter.rb +21 -0
  33. data/lib/approvals/reporters/image_reporter.rb +13 -0
  34. data/lib/approvals/reporters/image_reporter/html_image_reporter.rb +35 -0
  35. data/lib/approvals/reporters/image_reporter/image_magick_reporter.rb +20 -0
  36. data/lib/approvals/reporters/launcher.rb +49 -0
  37. data/lib/approvals/reporters/reporter.rb +30 -0
  38. data/lib/approvals/rspec.rb +4 -0
  39. data/lib/approvals/scrubber.rb +43 -0
  40. data/lib/approvals/system_command.rb +13 -0
  41. data/lib/approvals/version.rb +3 -0
  42. data/lib/approvals/writer.rb +39 -0
  43. data/lib/approvals/writers/array_writer.rb +17 -0
  44. data/lib/approvals/writers/binary_writer.rb +49 -0
  45. data/lib/approvals/writers/hash_writer.rb +20 -0
  46. data/lib/approvals/writers/html_writer.rb +15 -0
  47. data/lib/approvals/writers/json_writer.rb +31 -0
  48. data/lib/approvals/writers/text_writer.rb +21 -0
  49. data/lib/approvals/writers/xml_writer.rb +15 -0
  50. data/spec/approvals_spec.rb +172 -0
  51. data/spec/configuration_spec.rb +28 -0
  52. data/spec/dotfile_spec.rb +22 -0
  53. data/spec/executable_spec.rb +17 -0
  54. data/spec/extensions/rspec_approvals_spec.rb +105 -0
  55. data/spec/filter_spec.rb +123 -0
  56. data/spec/fixtures/approvals/approvals_custom_writer_verifies_a_complex_object.approved.txt +1 -0
  57. data/spec/fixtures/approvals/approvals_passes_approved_files_through_erb.approved.txt +1 -0
  58. data/spec/fixtures/approvals/approvals_passes_the_received_files_through_erb.approved.txt +1 -0
  59. data/spec/fixtures/approvals/approvals_supports_excluded_keys_option_also_supports_an_array_of_hashes.approved.json +10 -0
  60. data/spec/fixtures/approvals/approvals_supports_excluded_keys_option_supports_the_array_writer.approved.txt +1 -0
  61. data/spec/fixtures/approvals/approvals_supports_excluded_keys_option_supports_the_hash_writer.approved.txt +1 -0
  62. data/spec/fixtures/approvals/approvals_supports_excluded_keys_option_verifies_json_with_excluded_keys.approved.json +8 -0
  63. data/spec/fixtures/approvals/approvals_verifies_a_complex_object.approved.txt +1 -0
  64. data/spec/fixtures/approvals/approvals_verifies_a_hash.approved.txt +6 -0
  65. data/spec/fixtures/approvals/approvals_verifies_a_malformed_html_fragment.approved.html +11 -0
  66. data/spec/fixtures/approvals/approvals_verifies_a_string.approved.txt +1 -0
  67. data/spec/fixtures/approvals/approvals_verifies_an_array.approved.txt +4 -0
  68. data/spec/fixtures/approvals/approvals_verifies_an_array_as_json_when_format_is_set_to_json.approved.json +10 -0
  69. data/spec/fixtures/approvals/approvals_verifies_an_executable.approved.txt +1 -0
  70. data/spec/fixtures/approvals/approvals_verifies_html.approved.html +11 -0
  71. data/spec/fixtures/approvals/approvals_verifies_json.approved.json +7 -0
  72. data/spec/fixtures/approvals/approvals_verifies_json_and_is_newline_agnostic.approved.json +7 -0
  73. data/spec/fixtures/approvals/approvals_verifies_xml.approved.xml +9 -0
  74. data/spec/fixtures/approvals/verifications_a_string.approved.txt +1 -0
  75. data/spec/fixtures/approvals/verifies_a_complex_object.approved.txt +1 -0
  76. data/spec/fixtures/approvals/verifies_a_failure.approved.txt +1 -0
  77. data/spec/fixtures/approvals/verifies_a_failure_diff.approved.txt +1 -0
  78. data/spec/fixtures/approvals/verifies_a_string.approved.txt +1 -0
  79. data/spec/fixtures/approvals/verifies_an_array.approved.txt +4 -0
  80. data/spec/fixtures/approvals/verifies_an_executable.approved.txt +1 -0
  81. data/spec/fixtures/approvals/verifies_directory/a_complex_object.approved.txt +1 -0
  82. data/spec/fixtures/approvals/verifies_directory/a_failure.approved.txt +0 -0
  83. data/spec/fixtures/approvals/verifies_directory/a_failure_diff.approved.txt +0 -0
  84. data/spec/fixtures/approvals/verifies_directory/a_string.approved.txt +1 -0
  85. data/spec/fixtures/approvals/verifies_directory/an_array.approved.txt +4 -0
  86. data/spec/fixtures/approvals/verifies_directory/an_executable.approved.txt +1 -0
  87. data/spec/fixtures/approvals/verifies_directory/html.approved.html +11 -0
  88. data/spec/fixtures/approvals/verifies_directory/json.approved.json +7 -0
  89. data/spec/fixtures/approvals/verifies_directory/xml.approved.xml +9 -0
  90. data/spec/fixtures/approvals/verifies_html.approved.html +11 -0
  91. data/spec/fixtures/approvals/verifies_json.approved.json +7 -0
  92. data/spec/fixtures/approvals/verifies_xml.approved.xml +9 -0
  93. data/spec/fixtures/one.png +0 -0
  94. data/spec/fixtures/one.txt +1 -0
  95. data/spec/fixtures/two.png +0 -0
  96. data/spec/fixtures/two.txt +1 -0
  97. data/spec/namers/default_namer_spec.rb +37 -0
  98. data/spec/namers/directory_namer_spec.rb +31 -0
  99. data/spec/namers/rspec_namer_spec.rb +30 -0
  100. data/spec/namers_spec.rb +16 -0
  101. data/spec/reporters/first_working_reporter_spec.rb +30 -0
  102. data/spec/reporters/html_image_reporter_spec.rb +22 -0
  103. data/spec/reporters/image_magick_reporter_spec.rb +16 -0
  104. data/spec/reporters/launcher_spec.rb +24 -0
  105. data/spec/reporters/opendiff_reporter_spec.rb +15 -0
  106. data/spec/reporters/reporter_spec.rb +21 -0
  107. data/spec/scrubber_spec.rb +26 -0
  108. data/spec/spec_helper.rb +7 -0
  109. data/spec/system_command_spec.rb +13 -0
  110. metadata +196 -0
@@ -0,0 +1,22 @@
1
+ require 'rubygems/dependency_installer'
2
+
3
+ # This is how we can depend on a different version of the same gem for
4
+ # different Ruby versions.
5
+ # See https://en.wikibooks.org/wiki/Ruby_Programming/RubyGems
6
+
7
+ installer = Gem::DependencyInstaller.new
8
+
9
+ begin
10
+ if RUBY_VERSION >= '2.0'
11
+ installer.install 'json', '~> 2.0'
12
+ else
13
+ installer.install 'json', '~> 1.8'
14
+ end
15
+ rescue
16
+ exit(1)
17
+ end
18
+
19
+ # Write fake Rakefile for rake since Makefile isn't used
20
+ File.open(File.join(File.dirname(__FILE__), 'Rakefile'), 'w') do |f|
21
+ f.write("task :default\n")
22
+ end
@@ -0,0 +1,33 @@
1
+ require 'json'
2
+ require 'fileutils'
3
+ require 'nokogiri'
4
+ require 'approvals/version'
5
+ require 'approvals/configuration'
6
+ require 'approvals/approval'
7
+ require 'approvals/dsl'
8
+ require 'approvals/error'
9
+ require 'approvals/system_command'
10
+ require 'approvals/scrubber'
11
+ require 'approvals/dotfile'
12
+ require 'approvals/executable'
13
+ require 'approvals/reporters'
14
+ require 'approvals/filter'
15
+ require 'approvals/writer'
16
+ require 'approvals/namers/default_namer'
17
+
18
+ module Approvals
19
+ extend DSL
20
+
21
+ class << self
22
+
23
+ def project_dir
24
+ @project_dir ||= FileUtils.pwd
25
+ end
26
+
27
+ def reset
28
+ Dotfile.reset
29
+ end
30
+ end
31
+ end
32
+
33
+ Approvals.reset
@@ -0,0 +1,144 @@
1
+ require 'erb' # It is referenced on line 69
2
+ module Approvals
3
+ class Approval
4
+ class << self
5
+ attr_accessor :namer
6
+ end
7
+
8
+ attr_reader :subject, :namer, :failure
9
+ def initialize(subject, options = {})
10
+ @subject = subject
11
+ @namer = options[:namer] || default_namer(options[:name])
12
+ @format = options[:format] || identify_format
13
+ end
14
+
15
+ def default_namer(name)
16
+ Approvals::Approval.namer || Namers::DefaultNamer.new(name)
17
+ end
18
+
19
+ # Add a Proc that tests if subject is a kind of format
20
+ IDENTITIES = {
21
+ hash: Proc.new(){|subject|subject.respond_to? :each_pair},
22
+ array: Proc.new(){|subject|subject.respond_to? :each_with_index},
23
+ }
24
+
25
+ def identify_format
26
+ IDENTITIES.each_pair do |format, id_test|
27
+ return format if id_test.call(subject)
28
+ end
29
+ # otherwise
30
+ return :txt
31
+ end
32
+
33
+ def writer
34
+ @writer ||= Writer.for(@format)
35
+ end
36
+
37
+ def verify
38
+ unless File.exist?(namer.output_dir)
39
+ FileUtils.mkdir_p(namer.output_dir)
40
+ end
41
+
42
+ writer.write(subject, received_path)
43
+
44
+ unless approved?
45
+ fail_with "Approval file \"#{approved_path}\" not found."
46
+ end
47
+
48
+ @approved_content, @received_content = read_content
49
+
50
+ unless received_matches?
51
+ fail_with "Received file does not match approved:\n"+
52
+ "#{received_path}\n#{approved_path}\n#{diff_preview}"
53
+ end
54
+
55
+ success!
56
+ end
57
+
58
+ def diff_preview
59
+ approved, received = diff_lines
60
+ return unless approved and received
61
+ diff_index =
62
+ approved.each_char.with_index.find_index do |approved_char, i|
63
+ approved_char != received[i]
64
+ end
65
+ "approved fragment: #{approved[diff_index - 10 .. diff_index + 30]}\n"+
66
+ "received fragment: #{received[diff_index - 10 .. diff_index + 30]}"
67
+ end
68
+
69
+ def diff_lines
70
+ approved = @approved_content.split("\n")
71
+ received = @received_content.split("\n")
72
+ approved.each_with_index do |line, i|
73
+ return line, received[i] unless line == received[i]
74
+ end
75
+ end
76
+
77
+ def success!
78
+ File.delete received_path
79
+ end
80
+
81
+ def approved?
82
+ File.exist? approved_path
83
+ end
84
+
85
+ BINARY_FORMATS = [:binary]
86
+
87
+ def read_content
88
+ if BINARY_FORMATS.include?(@format) # Read without ERB
89
+ [IO.read(approved_path).chomp,
90
+ IO.read(received_path).chomp]
91
+ else
92
+ [ERB.new(IO.read(approved_path).chomp).result,
93
+ ERB.new(IO.read(received_path).chomp).result]
94
+ end
95
+ end
96
+
97
+ def received_matches?
98
+ @approved_content == @received_content
99
+ end
100
+
101
+ def fail_with(message)
102
+ Dotfile.append(diff_path)
103
+
104
+ if subject.respond_to?(:on_failure)
105
+ subject.on_failure.call(approved_text) if approved?
106
+ subject.on_failure.call(received_text)
107
+ end
108
+
109
+ error = ApprovalError.new("Approval Error: #{message}")
110
+ error.approved_path = approved_path
111
+ error.received_path = received_path
112
+
113
+ raise error
114
+ end
115
+
116
+ def diff_path
117
+ "#{approved_path} #{received_path}"
118
+ end
119
+
120
+ def full_path(state)
121
+ "#{namer.output_dir}#{namer.name}.#{state}.#{writer.extension}"
122
+ end
123
+
124
+ def name
125
+ namer.name
126
+ end
127
+
128
+ def approved_path
129
+ full_path('approved')
130
+ end
131
+
132
+ def received_path
133
+ full_path('received')
134
+ end
135
+
136
+ def approved_text
137
+ File.read(approved_path).chomp
138
+ end
139
+
140
+ def received_text
141
+ File.read(received_path).chomp
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,36 @@
1
+ # don't require the approvals library here, as it will reset the dotfile.
2
+ # or find a better way to reset the dotfile before a run.
3
+ module Approvals
4
+ class CLI < Thor
5
+
6
+ desc "verify", "Go through all failing approvals with a diff tool"
7
+ method_option :diff, :type => :string, :default => 'diff -N', :aliases => '-d', :desc => 'The difftool to use. e.g. opendiff, vimdiff, etc.'
8
+ method_option :ask, :type => :boolean, :default => true, :aliases => "-a", :desc => 'Offer to approve the received file for you.'
9
+ def verify
10
+ approvals = File.read('.approvals').split("\n")
11
+
12
+ rejected = []
13
+ approvals.each do |approval|
14
+ approved, received = approval.split(/\s+/)
15
+ if received.include?(".approved.")
16
+ received, approved = approved, received
17
+ end
18
+
19
+ diff_command = "#{options[:diff]} #{approved} #{received}"
20
+ puts diff_command
21
+ system(diff_command)
22
+
23
+ if options[:ask] && yes?("Approve? [y/N] ")
24
+ system("mv #{received} #{approved}")
25
+ else
26
+ rejected << approval
27
+ end
28
+ end
29
+
30
+ File.open('.approvals', 'w') do |f|
31
+ f.write rejected.join("\n")
32
+ end
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,29 @@
1
+ require 'singleton'
2
+
3
+ module Approvals
4
+
5
+ class << self
6
+ def configure(&block)
7
+ block.call Approvals::Configuration.instance
8
+ end
9
+
10
+ def configuration
11
+ Approvals::Configuration.instance
12
+ end
13
+ end
14
+
15
+ class Configuration
16
+ include Singleton
17
+
18
+ attr_writer :approvals_path
19
+ attr_writer :excluded_json_keys
20
+
21
+ def approvals_path
22
+ @approvals_path ||= 'fixtures/approvals/'
23
+ end
24
+
25
+ def excluded_json_keys
26
+ @excluded_json_keys ||= {}
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,33 @@
1
+ module Approvals
2
+
3
+ class Dotfile
4
+ class << self
5
+
6
+ def reset
7
+ File.truncate(path, 0) if File.exist?(path)
8
+ end
9
+
10
+ def append(text)
11
+ unless includes?(text)
12
+ write text
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def path
19
+ File.join(Approvals.project_dir, '.approvals')
20
+ end
21
+
22
+ def includes?(text)
23
+ system("cat #{path} 2> /dev/null | grep -q \"^#{text}$\"")
24
+ end
25
+
26
+ def write(text)
27
+ File.open(path, 'a+') do |f|
28
+ f.write "#{text}\n"
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,7 @@
1
+ module Approvals
2
+ module DSL
3
+ def verify(object, options = {})
4
+ Approval.new(object, options).verify
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,21 @@
1
+ module Approvals
2
+ class ApprovalError < Exception
3
+ attr_accessor :received_path, :approved_path
4
+
5
+ def received_exists?
6
+ received_path && File.exist?(received_path)
7
+ end
8
+
9
+ def received_text
10
+ received_exists? && IO.read(received_path)
11
+ end
12
+
13
+ def approved_exists?
14
+ approved_path && File.exist?(approved_path)
15
+ end
16
+
17
+ def approved_text
18
+ approved_exists? && IO.read(approved_path)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,18 @@
1
+ module Approvals
2
+ class Executable
3
+
4
+ attr_accessor :command, :on_failure
5
+ def initialize(command, &block)
6
+ self.command = command
7
+ self.on_failure = block
8
+ end
9
+
10
+ def to_s
11
+ inspect
12
+ end
13
+
14
+ def inspect
15
+ command
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,13 @@
1
+ if defined? RSpec
2
+ require 'approvals/extensions/rspec/dsl'
3
+ require 'approvals/namers/rspec_namer'
4
+ require 'approvals/namers/directory_namer'
5
+
6
+ RSpec.configure do |c|
7
+ c.include Approvals::RSpec::DSL
8
+ c.add_setting :approvals_path, :default => 'spec/fixtures/approvals/'
9
+ c.add_setting :approvals_namer_class, :default => Approvals::Namers::DirectoryNamer
10
+ c.add_setting :diff_on_approval_failure, :default => false
11
+ c.add_setting :approvals_default_format, :default => nil
12
+ end
13
+ end
@@ -0,0 +1,44 @@
1
+ require 'rspec/expectations'
2
+
3
+ module Approvals
4
+ module RSpec
5
+ module DSL
6
+ def executable(command, &block)
7
+ Approvals::Executable.new(command, &block)
8
+ end
9
+
10
+ def verify(options = {}, &block)
11
+ # Workaround to support both Rspec 2 and 3
12
+ # RSpec.current_example is the Rspec 3 way
13
+ fetch_current_example = ::RSpec.respond_to?(:current_example) ? proc { ::RSpec.current_example } : proc { |context| context.example }
14
+ # /Workaround
15
+
16
+ group = eval "self", block.binding
17
+ namer = ::RSpec.configuration.approvals_namer_class.new(fetch_current_example.call(group))
18
+ defaults = {
19
+ :namer => namer
20
+ }
21
+ format = ::RSpec.configuration.approvals_default_format
22
+ defaults[:format] = format if format
23
+ Approvals.verify(block.call, defaults.merge(options))
24
+ rescue ApprovalError => e
25
+ if diff_on_approval_failure?
26
+ ::RSpec::Expectations.fail_with(e.message, e.approved_text, e.received_text)
27
+ else
28
+ raise e
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def diff_on_approval_failure?
35
+ # Workaround to support both RSpec 2 and 3
36
+ fetch_current_example = ::RSpec.respond_to?(:current_example) ? proc { ::RSpec.current_example } : proc { |context| context.example }
37
+ # /Workaround
38
+
39
+ ::RSpec.configuration.diff_on_approval_failure? ||
40
+ fetch_current_example.call(self).metadata[:diff_on_approval_failure]
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,45 @@
1
+ module Approvals
2
+ class Filter
3
+ attr_reader :filters
4
+
5
+ def initialize(filters)
6
+ @filters = filters
7
+ @placeholder = {}
8
+ end
9
+
10
+ def apply hash_or_array
11
+ if @filters.any?
12
+ censored(hash_or_array)
13
+ else
14
+ hash_or_array
15
+ end
16
+ end
17
+
18
+ def censored value, key=nil
19
+ if value.nil?
20
+ nil
21
+ elsif key && placeholder_for(key)
22
+ "<#{placeholder_for(key)}>"
23
+ else
24
+ case value
25
+ when Array
26
+ value.map { |item| censored(item) }
27
+ when Hash
28
+ Hash[value.map { |inner_key, inner_value| [inner_key, censored(inner_value, inner_key)] }]
29
+ else
30
+ value
31
+ end
32
+ end
33
+ end
34
+
35
+ def placeholder_for key
36
+ return @placeholder[key] if @placeholder.key? key
37
+
38
+ applicable_filters = filters.select do |placeholder, pattern|
39
+ pattern && key.match(pattern)
40
+ end
41
+
42
+ @placeholder[key] = applicable_filters.keys.last
43
+ end
44
+ end
45
+ end