mountain_berry_fields 1.0.0

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