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.
- data/.gitignore +19 -0
- data/.simplecov +12 -0
- data/Gemfile +2 -0
- data/LICENSE +22 -0
- data/Rakefile +30 -0
- data/Readme.md +184 -0
- data/Readme.md.mountain_berry_fields +197 -0
- data/bin/mountain_berry_fields +9 -0
- data/features/context_block.feature +73 -0
- data/features/cwd_is_dir_of_mbf_file.feature +16 -0
- data/features/rake_task.feature +46 -0
- data/features/setup_block.feature +36 -0
- data/features/step_definitions/steps.rb +46 -0
- data/features/support/env.rb +76 -0
- data/lib/mountain_berry_fields.rb +89 -0
- data/lib/mountain_berry_fields/command_line_interaction.rb +13 -0
- data/lib/mountain_berry_fields/evaluator.rb +90 -0
- data/lib/mountain_berry_fields/parser.rb +101 -0
- data/lib/mountain_berry_fields/rake_task.rb +12 -0
- data/lib/mountain_berry_fields/test.rb +88 -0
- data/lib/mountain_berry_fields/test/always_fail.rb +21 -0
- data/lib/mountain_berry_fields/test/always_pass.rb +19 -0
- data/lib/mountain_berry_fields/version.rb +3 -0
- data/mountain_berry_fields.gemspec +29 -0
- data/readme_helper.rb +205 -0
- data/spec/command_line_interaction_spec.rb +16 -0
- data/spec/evaluator_spec.rb +170 -0
- data/spec/mock_substitutability_spec.rb +25 -0
- data/spec/mountain_berry_fields_spec.rb +147 -0
- data/spec/parser_spec.rb +139 -0
- data/spec/ruby_syntax_checker_spec.rb +27 -0
- data/spec/spec_helper.rb +104 -0
- data/spec/test/always_fail_spec.rb +25 -0
- data/spec/test/always_pass_spec.rb +15 -0
- data/spec/test/strategy_spec.rb +48 -0
- data/spec/test/test_spec.rb +22 -0
- 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,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
|
data/readme_helper.rb
ADDED
@@ -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
|
+
}
|