mountain_berry_fields 1.0.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 (37) hide show
  1. data/.gitignore +19 -0
  2. data/.simplecov +12 -0
  3. data/Gemfile +2 -0
  4. data/LICENSE +22 -0
  5. data/Rakefile +30 -0
  6. data/Readme.md +184 -0
  7. data/Readme.md.mountain_berry_fields +197 -0
  8. data/bin/mountain_berry_fields +9 -0
  9. data/features/context_block.feature +73 -0
  10. data/features/cwd_is_dir_of_mbf_file.feature +16 -0
  11. data/features/rake_task.feature +46 -0
  12. data/features/setup_block.feature +36 -0
  13. data/features/step_definitions/steps.rb +46 -0
  14. data/features/support/env.rb +76 -0
  15. data/lib/mountain_berry_fields.rb +89 -0
  16. data/lib/mountain_berry_fields/command_line_interaction.rb +13 -0
  17. data/lib/mountain_berry_fields/evaluator.rb +90 -0
  18. data/lib/mountain_berry_fields/parser.rb +101 -0
  19. data/lib/mountain_berry_fields/rake_task.rb +12 -0
  20. data/lib/mountain_berry_fields/test.rb +88 -0
  21. data/lib/mountain_berry_fields/test/always_fail.rb +21 -0
  22. data/lib/mountain_berry_fields/test/always_pass.rb +19 -0
  23. data/lib/mountain_berry_fields/version.rb +3 -0
  24. data/mountain_berry_fields.gemspec +29 -0
  25. data/readme_helper.rb +205 -0
  26. data/spec/command_line_interaction_spec.rb +16 -0
  27. data/spec/evaluator_spec.rb +170 -0
  28. data/spec/mock_substitutability_spec.rb +25 -0
  29. data/spec/mountain_berry_fields_spec.rb +147 -0
  30. data/spec/parser_spec.rb +139 -0
  31. data/spec/ruby_syntax_checker_spec.rb +27 -0
  32. data/spec/spec_helper.rb +104 -0
  33. data/spec/test/always_fail_spec.rb +25 -0
  34. data/spec/test/always_pass_spec.rb +15 -0
  35. data/spec/test/strategy_spec.rb +48 -0
  36. data/spec/test/test_spec.rb +22 -0
  37. metadata +209 -0
@@ -0,0 +1,101 @@
1
+ require 'erubis'
2
+
3
+ class MountainBerryFields
4
+
5
+ # This class takes a document with erb in it
6
+ # It parses the erb and gives you back a file of source code
7
+ # When that source code is evaluated, you get the file out.
8
+ # The evaluation environment is expected to provide a `document` method or local variable,
9
+ # which is a string that the template will be appended to.
10
+ #
11
+ # It differs from normal ERB in that you can pass it :visible and :invisible commands lists.
12
+ # Code inside of a block around an invisible command (method name) will not be added to the document.
13
+ # Any command invoked at the start of an erb block (<% and <%=) will be checked against the
14
+ # visible and invisible lists, if they are on either list, their block will return the
15
+ # text in the template that was written in the block.
16
+ class Parser < Erubis::Eruby
17
+ class Recording
18
+ def initialize(is_command, is_visible=true)
19
+ @is_command = is_command
20
+ @is_visible = is_visible
21
+ end
22
+
23
+ def command?
24
+ @is_command
25
+ end
26
+
27
+ def visible?
28
+ @is_visible
29
+ end
30
+
31
+ def recorded
32
+ @recorded ||= ''
33
+ end
34
+
35
+ def record(text)
36
+ recorded << text if command?
37
+ end
38
+ end
39
+
40
+ attr_accessor :visible_commands, :invisible_commands, :known_commands, :recordings
41
+
42
+ def init_generator(properties={})
43
+ self.recordings = [Recording.new(false)]
44
+ self.visible_commands = (properties.delete(:visible) || []).map &:to_s
45
+ self.invisible_commands = (properties.delete(:invisible) || []).map &:to_s
46
+ self.known_commands = visible_commands + invisible_commands
47
+ super
48
+ end
49
+
50
+ def add_preamble(src)
51
+ super
52
+ end
53
+
54
+ def add_postamble(src)
55
+ src << "#{@bufvar} << %(\\n) unless #{@bufvar}.end_with? %(\\n);"
56
+ src << "document << #{@bufvar};"
57
+ end
58
+
59
+ def parse
60
+ src
61
+ end
62
+
63
+ def add_text(src, text)
64
+ recordings.last.record text
65
+ super if recordings.last.visible?
66
+ end
67
+
68
+ def known_command?(code_with_command)
69
+ known_commands.include? code_with_command[/\w+/]
70
+ end
71
+
72
+ def visible_command?(code_with_command)
73
+ visible_commands.include? code_with_command[/\w+/]
74
+ end
75
+
76
+ def end_command?(code_with_command)
77
+ code_with_command =~ /\A\s*(end|})/
78
+ end
79
+
80
+ def add_stmt(src, code)
81
+ manage_recording src, code
82
+ super
83
+ end
84
+
85
+ def add_expr_literal(src, code)
86
+ manage_recording src, code
87
+ super
88
+ end
89
+
90
+ def manage_recording(src, code)
91
+ if known_command? code
92
+ recordings << Recording.new(true, visible_command?(code))
93
+ elsif end_command? code
94
+ recording = recordings.pop
95
+ src << recording.recorded.inspect << ";" if recording.command?
96
+ else
97
+ recordings << Recording.new(false)
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,12 @@
1
+ class MountainBerryFields
2
+ class RakeTask
3
+ include Rake::DSL
4
+
5
+ def initialize(task_name, input_file_name)
6
+ desc("Test and generate #{input_file_name}") unless ::Rake.application.last_comment
7
+ task task_name do
8
+ sh "mountain_berry_fields #{input_file_name}"
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,88 @@
1
+ require 'open3'
2
+
3
+
4
+ class MountainBerryFields
5
+
6
+ # Data structure to store the shit passed to the test method
7
+ class Test
8
+ attr_accessor :name, :options
9
+
10
+ def initialize(name, options)
11
+ self.name, self.options = name, options
12
+ end
13
+
14
+ def strategy
15
+ options[:with]
16
+ end
17
+
18
+ def code
19
+ options[:code]
20
+ end
21
+ end
22
+
23
+ # You want to run tests, amirite? So you need something that runs them.
24
+ # Right now that's called a strategy (expect that to change). Strategies
25
+ # take the code to test as a string, and then test them and return if they
26
+ # pass or fail.
27
+ #
28
+ # Strategies should probably be registered so that the evaluator can find them.
29
+ module Test::Strategy
30
+ @registered = {}
31
+
32
+ def self.for(name)
33
+ @registered.fetch name.to_s do
34
+ raise NameError, "#{name.inspect} is not a registered strategy, should have been in #{@registered.keys.inspect}"
35
+ end
36
+ end
37
+
38
+ def self.register(name, strategy)
39
+ @registered[name.to_s] = strategy
40
+ end
41
+
42
+ def self.unregister(name)
43
+ @registered.delete name.to_s
44
+ end
45
+
46
+ def self.registered?(name)
47
+ @registered.has_key? name.to_s
48
+ end
49
+
50
+ attr_reader :code_to_test
51
+
52
+ def initialize(code_to_test)
53
+ @code_to_test = code_to_test.to_s
54
+ end
55
+
56
+ def pass?
57
+ raise "unimplemented"
58
+ end
59
+
60
+ def failure_message
61
+ raise "unimplemented"
62
+ end
63
+ end
64
+
65
+
66
+ # checks syntax of a code example
67
+ class Test::RubySyntaxChecker
68
+ def initialize(code_to_test)
69
+ @code_to_test = code_to_test
70
+ end
71
+
72
+ def valid?
73
+ return @valid if defined? @valid
74
+ out, err, status = Open3.capture3 'ruby -c', stdin_data: @code_to_test
75
+ @stderr = err
76
+ @valid = status.exitstatus.zero?
77
+ end
78
+
79
+ def invalid_message
80
+ valid?
81
+ "#{@stderr.chomp}\n\noriginal file:\n#@code_to_test"
82
+ end
83
+ end
84
+ end
85
+
86
+ require 'mountain_berry_fields/test/always_fail'
87
+ require 'mountain_berry_fields/test/always_pass'
88
+ Gem.find_files("mountain_berry_fields/test/*").each { |path| require path }
@@ -0,0 +1,21 @@
1
+ class MountainBerryFields
2
+ class Test
3
+
4
+ # Red. Red. Refactor anyway, cuz fuck it.
5
+ class AlwaysFail
6
+ Strategy.register :always_fail, self
7
+
8
+ include Strategy
9
+
10
+ def pass?
11
+ eval code_to_test
12
+ ensure
13
+ return false
14
+ end
15
+
16
+ def failure_message
17
+ "THIS STRATEGY ALWAYS FAILS"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ class MountainBerryFields
2
+ class Test
3
+
4
+ # Ask it if it passes
5
+ #
6
+ # (it does)
7
+ class AlwaysPass
8
+ Strategy.register :always_pass, self
9
+
10
+ include Strategy
11
+
12
+ def pass?
13
+ eval code_to_test
14
+ ensure
15
+ return true
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,3 @@
1
+ class MountainBerryFields
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/mountain_berry_fields/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Josh Cheek"]
6
+ gem.email = ["josh.cheek@gmail.com"]
7
+ gem.description = %q{Test code samples embedded in files like readmes}
8
+ gem.summary = %q{Test code samples embedded in files like readmes}
9
+ gem.homepage = ""
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "mountain_berry_fields"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = MountainBerryFields::VERSION
17
+
18
+ gem.add_runtime_dependency 'erubis', '= 2.7.0'
19
+ gem.add_runtime_dependency 'deject', '~> 0.2.2'
20
+
21
+ gem.add_development_dependency 'mountain_berry_fields-magic_comments', '~> 1.0.0'
22
+ gem.add_development_dependency 'mountain_berry_fields-rspec', '~> 1.0.0'
23
+ gem.add_development_dependency 'surrogate', '~> 0.5.1'
24
+ gem.add_development_dependency 'rspec', '~> 2.10.0'
25
+ gem.add_development_dependency 'cucumber', '~> 1.2.0'
26
+ gem.add_development_dependency 'simplecov', '~> 0.6.4'
27
+ gem.add_development_dependency 'rake'
28
+ gem.add_development_dependency 'pry'
29
+ end
@@ -0,0 +1,205 @@
1
+ # Okay look, I know this code is scary, and you're like "I don't want to
2
+ # have to do that shit for my project".
3
+ #
4
+ # But it's not likely that you will, I didn't have to when I did Deject or Surrogate,
5
+ # it's just that this code is embedding readmes in readmes, so they're not really code samples.
6
+
7
+ Strategy = MountainBerryFields::Test::Strategy
8
+
9
+ # maybe this should be pulled into its own gem? it seems generally useful
10
+ Strategy.register :install_dep, Class.new {
11
+ def initialize(install_string)
12
+ @prompt, @gem_command, @install_command, @dependency_name = install_string.split
13
+ @gemspec = Gem::Specification.load 'mountain_berry_fields.gemspec'
14
+ end
15
+
16
+ def pass?
17
+ !failure_message
18
+ end
19
+
20
+ def failure_message
21
+ return %'prompt marker is "$", not #{@prompt.inspect}' unless @prompt == '$'
22
+ return %'gem command is "gem", not #{@gem_command.inspect}' unless @gem_command == 'gem'
23
+ return %'install command is "install", not #{@install_command.inspect}' unless @install_command == 'install'
24
+ return %'development dependencies are #{development_dependencies.inspect} WTF is #{@dependency_name.inspect}?' unless is_dependency?
25
+ end
26
+
27
+ def development_dependencies
28
+ @gemspec.development_dependencies.map &:name
29
+ end
30
+
31
+ def is_dependency?
32
+ development_dependencies.include? @dependency_name
33
+ end
34
+ }
35
+
36
+
37
+ require 'tmpdir'
38
+ require 'open3'
39
+ Strategy.register :mbf_example, Class.new {
40
+ include Strategy
41
+
42
+ attr_accessor :input_filename, :input_code, :command_line_invocation, :output_filename, :output_code, :expected_failure, :failure_message
43
+
44
+ def pass?
45
+ parse
46
+ happy_path && sad_path
47
+ end
48
+
49
+ def failure_message
50
+ @failure_message
51
+ end
52
+
53
+ def happy_path
54
+ happy_lib_code = '
55
+ <% setup do %>
56
+ MyLibName = Struct.new :data do
57
+ def result
58
+ "some cool result"
59
+ end
60
+ end
61
+ <% end %>
62
+ '
63
+ Dir.mktmpdir 'happy_path' do |dir|
64
+ Dir.chdir dir do
65
+ File.write input_filename, happy_lib_code + input_code
66
+ out, err, status = Open3.capture3 command_line_invocation
67
+ @failure_message = err
68
+ status.success? && File.exist?(dir + "/" + output_filename)
69
+ end
70
+ end
71
+ end
72
+
73
+ def sad_path
74
+ sad_lib_code = '
75
+ <% setup do %>
76
+ MyLibName = Struct.new :data do
77
+ def result
78
+ "some unexpected result"
79
+ end
80
+ end
81
+ <% end %>
82
+ '
83
+ Dir.mktmpdir 'happy_path' do |dir|
84
+ Dir.chdir dir do
85
+ File.write input_filename, sad_lib_code + input_code
86
+ oout, err, status = Open3.capture3 command_line_invocation
87
+ self.failure_message = "Error should have been #{expected_failure.inspect}, but was #{err.inspect}"
88
+ result =( expected_failure == err)
89
+ result
90
+ end
91
+ end
92
+ end
93
+
94
+ # all this parsing is overly simple and a bit fragile, but good enough
95
+ # revisit it after v2, should have something in place so we don't have to parse
96
+ # the text, but can instead talk directly to the test or something
97
+ def parse
98
+ results = code_to_test.split(/^(?=\S)/)
99
+ parse_setup results.shift
100
+ parse_happy_path results.shift
101
+ parse_sad_path results.shift
102
+ end
103
+
104
+ def parse_setup(raw_setup)
105
+ lines = raw_setup.lines.to_a
106
+ self.input_filename = lines.shift[/`(.*?)`/, 1]
107
+ lines.shift
108
+ lines.pop
109
+ self.input_code = lines.join.gsub(/^ {4}/, '')
110
+ end
111
+
112
+ def parse_happy_path(raw_happy_path)
113
+ lines = raw_happy_path.lines.to_a
114
+ first_line = lines.shift
115
+ self.command_line_invocation, self.output_filename = first_line.scan(/`[^`]*`/).map { |text| text[1...-1] }
116
+ command_line_invocation.sub! /^\$\s+/, ''
117
+ lines.shift
118
+ lines.pop
119
+ self.output_code = lines.join.gsub(/^ {4}/, '')
120
+ end
121
+
122
+ def parse_sad_path(raw_sad_path)
123
+ self.expected_failure = raw_sad_path.lines.drop(2).join.gsub(/^ {4}/, '')
124
+ end
125
+ }
126
+
127
+
128
+ Strategy.register :generic_mbf, Class.new {
129
+ attr_reader :failure_message
130
+
131
+ def initialize(code_to_test, filename='f.mountain_berry_fields', invocation="mountain_berry_fields #{filename}", &do_in_dir)
132
+ @code_to_test, @filename, @invocation, @do_in_dir =
133
+ code_to_test, filename, invocation, do_in_dir
134
+ end
135
+
136
+ def pass?
137
+ @pass ||= Dir.mktmpdir 'setup_block' do |dir|
138
+ Dir.chdir dir do
139
+ @do_in_dir.call if @do_in_dir
140
+ File.write @filename, @code_to_test
141
+ out, @failure_message, status = Open3.capture3 @invocation
142
+ status.success?
143
+ end
144
+ end
145
+ end
146
+ }
147
+
148
+
149
+ Strategy.register :requires_lib, Class.new {
150
+ attr_accessor :setup_block
151
+
152
+ def initialize(setup_block)
153
+ self.setup_block = setup_block
154
+ end
155
+
156
+ def pass?
157
+ strategy.pass?
158
+ end
159
+
160
+ def failure_message
161
+ strategy.failure_message
162
+ end
163
+
164
+ def strategy
165
+ @strategy ||= Strategy.for(:generic_mbf).new(code_to_test) do
166
+ Dir.mkdir 'lib'
167
+ File.write 'lib/my_lib_name.rb', 'MyLibName = 12'
168
+ end
169
+ end
170
+
171
+ def code_to_test
172
+ %'#{setup_block}
173
+ <% test "loaded", with: :magic_comments do %>
174
+ MyLibName # => 12
175
+ <% end %>'
176
+ end
177
+ }
178
+
179
+ Strategy.register :task_named_mbf, Class.new {
180
+ def initialize(task_definition)
181
+ @task_definition = task_definition
182
+ end
183
+
184
+ def pass?
185
+ require 'rake'
186
+ eval @task_definition
187
+ Rake::Task[:mbf] rescue nil
188
+ end
189
+
190
+ def failure_message
191
+ "No task named :mbf"
192
+ end
193
+ }
194
+
195
+ Strategy.register :register_your_strategy, Class.new {
196
+ def initialize(registration_code)
197
+ @registration_code = registration_code
198
+ end
199
+
200
+ def pass?
201
+ eval "YourStrategy = Object.new"
202
+ eval @registration_code
203
+ Strategy.for(:your_strategy) == YourStrategy
204
+ end
205
+ }