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,9 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift File.expand_path('../../lib/', __FILE__)
|
4
|
+
|
5
|
+
require 'mountain_berry_fields'
|
6
|
+
require 'mountain_berry_fields/command_line_interaction'
|
7
|
+
MountainBerryFields.override(:interaction) { MountainBerryFields::CommandLineInteraction.new }
|
8
|
+
|
9
|
+
exit MountainBerryFields.new(ARGV).execute
|
@@ -0,0 +1,73 @@
|
|
1
|
+
Feature: Using a context block
|
2
|
+
|
3
|
+
Sometimes there is necessary code around what the user wants to show.
|
4
|
+
But including this context just distracts from the relevant piece of code.
|
5
|
+
|
6
|
+
The user can declare this context in the context block. Their code example
|
7
|
+
can state that it should be executed in that context, and its body will
|
8
|
+
replace the __CODE__ macro in the context.
|
9
|
+
|
10
|
+
This ensures it gets tested with the context, but displayed without it.
|
11
|
+
|
12
|
+
Scenario: Passing test block with a context
|
13
|
+
Given the file "example.mountain_berry_fields.md":
|
14
|
+
"""
|
15
|
+
<% context 'user spec' do %>
|
16
|
+
User = Struct.new :name
|
17
|
+
describe User do
|
18
|
+
it 'does usery things' do
|
19
|
+
__CODE__
|
20
|
+
end
|
21
|
+
end
|
22
|
+
<% end %>
|
23
|
+
|
24
|
+
Users know their names:
|
25
|
+
```ruby
|
26
|
+
<% test 'users know their name', with: :rspec, context: 'user spec' do %>
|
27
|
+
User.new('Josh').name.should == 'Josh'
|
28
|
+
<% end %>
|
29
|
+
```
|
30
|
+
"""
|
31
|
+
When I run "mountain_berry_fields example.mountain_berry_fields.md"
|
32
|
+
Then it exits with a status of 0
|
33
|
+
And I see the file "example.md":
|
34
|
+
"""
|
35
|
+
|
36
|
+
Users know their names:
|
37
|
+
```ruby
|
38
|
+
User.new('Josh').name.should == 'Josh'
|
39
|
+
```
|
40
|
+
"""
|
41
|
+
|
42
|
+
|
43
|
+
Scenario: Passing test block with a context
|
44
|
+
Given the file "example.mountain_berry_fields.md":
|
45
|
+
"""
|
46
|
+
<% context 'user spec' do %>
|
47
|
+
User = Struct.new :name
|
48
|
+
describe User do
|
49
|
+
it 'does usery things' do
|
50
|
+
__CODE__
|
51
|
+
end
|
52
|
+
end
|
53
|
+
<% end %>
|
54
|
+
|
55
|
+
Users know their names:
|
56
|
+
```ruby
|
57
|
+
<% test 'Users know their names', with: :rspec, context: 'user spec' do %>
|
58
|
+
User.new('Josh').name.should == 'Not Josh'
|
59
|
+
<% end %>
|
60
|
+
```
|
61
|
+
"""
|
62
|
+
When I run "mountain_berry_fields example.mountain_berry_fields.md"
|
63
|
+
Then it exits with a status of 1, and a stderr of:
|
64
|
+
"""
|
65
|
+
FAILURE: Users know their names
|
66
|
+
User does usery things:
|
67
|
+
expected: "Not Josh"
|
68
|
+
got: "Josh" (using ==)
|
69
|
+
|
70
|
+
backtrace:
|
71
|
+
/spec.rb:4:in `block (2 levels) in <top (required)>'
|
72
|
+
"""
|
73
|
+
And I do not see the file "example.md"
|
@@ -0,0 +1,16 @@
|
|
1
|
+
Feature: Input and output directories
|
2
|
+
The current working directory while the mbf file executes
|
3
|
+
is its own directory.
|
4
|
+
|
5
|
+
The output file is placed in the same directory.
|
6
|
+
|
7
|
+
Scenario:
|
8
|
+
Given the file "example/mah_filez.mountain_berry_fields":
|
9
|
+
"""
|
10
|
+
<% test 'what dir?', with: :magic_comments do %>
|
11
|
+
Dir.pwd # => "{{proving_grounds_dir}}/example"
|
12
|
+
<% end %>
|
13
|
+
"""
|
14
|
+
When I run "mountain_berry_fields example/mah_filez.mountain_berry_fields"
|
15
|
+
Then it exits with a status of 0
|
16
|
+
And I see the file "example/mah_filez"
|
@@ -0,0 +1,46 @@
|
|
1
|
+
Feature: Mountain Berry Fields provides a rake task
|
2
|
+
|
3
|
+
To automate usage, users may wish to add Mountain Berry Fields
|
4
|
+
to a rake task, which runs as part of their suite.
|
5
|
+
|
6
|
+
Scenario: Successful rake task
|
7
|
+
Given the file "Rakefile":
|
8
|
+
"""
|
9
|
+
require 'mountain_berry_fields/rake_task'
|
10
|
+
MountainBerryFields::RakeTask.new(:mbf, 'Readme.mountain_berry_fields.md')
|
11
|
+
"""
|
12
|
+
And the file "Readme.mountain_berry_fields.md":
|
13
|
+
"""
|
14
|
+
Use it like this:
|
15
|
+
|
16
|
+
<% test 'example', with: :always_pass do %>
|
17
|
+
the code
|
18
|
+
<% end %>
|
19
|
+
"""
|
20
|
+
When I run "rake mbf"
|
21
|
+
Then it exits with a status of 0
|
22
|
+
And I see the file "Readme.md":
|
23
|
+
"""
|
24
|
+
Use it like this:
|
25
|
+
|
26
|
+
the code
|
27
|
+
"""
|
28
|
+
|
29
|
+
|
30
|
+
Scenario: Failing rake task
|
31
|
+
Given the file "Rakefile":
|
32
|
+
"""
|
33
|
+
require 'mountain_berry_fields/rake_task'
|
34
|
+
MountainBerryFields::RakeTask.new(:mountains, 'Readme.mountain_berry_fields.md')
|
35
|
+
"""
|
36
|
+
And the file "Readme.mountain_berry_fields.md":
|
37
|
+
"""
|
38
|
+
Use it like this:
|
39
|
+
|
40
|
+
<% test 'example', with: :always_fail do %>
|
41
|
+
the code
|
42
|
+
<% end %>
|
43
|
+
"""
|
44
|
+
When I run "rake mountains"
|
45
|
+
Then it exits with a status of 1
|
46
|
+
And I do not see the file "Readme.md"
|
@@ -0,0 +1,36 @@
|
|
1
|
+
Feature: Using a setup block
|
2
|
+
|
3
|
+
Sometimes the user needs to setup the environment.
|
4
|
+
Rather than forcing the user to provide this setup in the
|
5
|
+
code block, which is visible, allow them to provide a
|
6
|
+
setup block, which will be executed before each block of code.
|
7
|
+
|
8
|
+
The current directory is set to the directory containing the
|
9
|
+
mountain_berry_field file, so file operations can be performed
|
10
|
+
relative to that directory.
|
11
|
+
|
12
|
+
Scenario: setup blocks are evaluated before each test block
|
13
|
+
Given the file "addition_helpers.rb":
|
14
|
+
"""
|
15
|
+
def plus_five(n)
|
16
|
+
n + 5
|
17
|
+
end
|
18
|
+
"""
|
19
|
+
And the file "example.mountain_berry_fields.md":
|
20
|
+
"""
|
21
|
+
<% setup do %>
|
22
|
+
require "./addition_helpers"
|
23
|
+
<% end %>
|
24
|
+
|
25
|
+
<% test 'I see the method', with: :magic_comments do %>
|
26
|
+
plus_five 2 # => 7
|
27
|
+
<% end %>
|
28
|
+
"""
|
29
|
+
When I run "mountain_berry_fields example.mountain_berry_fields.md"
|
30
|
+
Then it exits with a status of 0
|
31
|
+
And I see the file "example.md":
|
32
|
+
"""
|
33
|
+
|
34
|
+
plus_five 2 # => 7
|
35
|
+
"""
|
36
|
+
|
@@ -0,0 +1,46 @@
|
|
1
|
+
Given 'the file "$filename":' do |filename, body|
|
2
|
+
in_proving_grounds do
|
3
|
+
ensure_dir File.dirname filename
|
4
|
+
File.write filename, interpret_curlies(body)
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
When 'I run "$command"' do |command|
|
9
|
+
in_proving_grounds { cmdline command }
|
10
|
+
end
|
11
|
+
|
12
|
+
Then /^it exits with a status of (\d+)$/ do |status|
|
13
|
+
last_cmdline.exitstatus.should eq(status.to_i), "Expected #{status}, got #{last_cmdline.exitstatus}. STDERR: #{last_cmdline.stderr}"
|
14
|
+
end
|
15
|
+
|
16
|
+
Then /^it exits with a status of (\d+), and a stderr of:$/ do |status, stderr|
|
17
|
+
last_cmdline.exitstatus.should == status.to_i
|
18
|
+
last_cmdline.stderr.chomp.should == interpret_curlies(stderr).chomp
|
19
|
+
end
|
20
|
+
|
21
|
+
Then /^it prints nothing to (stdout|stderr)$/ do |descriptor|
|
22
|
+
last_cmdline.send(descriptor).should == ''
|
23
|
+
end
|
24
|
+
|
25
|
+
Then 'I see the file "$filename"' do |filename|
|
26
|
+
in_proving_grounds { File.exist?(filename).should be_true }
|
27
|
+
end
|
28
|
+
|
29
|
+
Then 'I see the file "$filename":' do |filename, body|
|
30
|
+
in_proving_grounds do
|
31
|
+
File.exist?(filename).should be_true, "#{filename} doesn't exist"
|
32
|
+
|
33
|
+
body && strip_trailing_whitespace(File.read(filename)).should ==
|
34
|
+
strip_trailing_whitespace(body)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
Then 'I do not see the file "$filename"' do |filename|
|
39
|
+
in_proving_grounds { File.exist?(filename).should be_false }
|
40
|
+
end
|
41
|
+
|
42
|
+
And 'I pry' do
|
43
|
+
require 'pry'
|
44
|
+
binding.pry
|
45
|
+
end
|
46
|
+
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'simplecov'
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
module ProvingGrounds
|
5
|
+
def proving_grounds_dir
|
6
|
+
File.expand_path "../../proving_grounds", __FILE__
|
7
|
+
end
|
8
|
+
|
9
|
+
def in_proving_grounds(&block)
|
10
|
+
make_proving_grounds
|
11
|
+
FileUtils.cd(proving_grounds_dir) { return block.call }
|
12
|
+
end
|
13
|
+
|
14
|
+
def make_proving_grounds
|
15
|
+
FileUtils.mkdir_p proving_grounds_dir
|
16
|
+
end
|
17
|
+
|
18
|
+
def remove_proving_grounds
|
19
|
+
FileUtils.rm_r proving_grounds_dir
|
20
|
+
end
|
21
|
+
|
22
|
+
def ensure_dir(dirname)
|
23
|
+
FileUtils.mkdir_p dirname
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
module Helpers
|
29
|
+
def interpret_curlies(string)
|
30
|
+
string.gsub /{{.*?}}/ do |code|
|
31
|
+
code.sub! /^{{/, ''
|
32
|
+
code.sub! /}}$/, ''
|
33
|
+
eval code
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def strip_trailing_whitespace(text)
|
38
|
+
text.gsub /\s+$/, ''
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
require 'open3'
|
43
|
+
class CommandLine
|
44
|
+
module CukeHelpers
|
45
|
+
def cmdline(command, options={})
|
46
|
+
@last_cmdline = CommandLine.new(command, options)
|
47
|
+
@last_cmdline.execute
|
48
|
+
@last_cmdline
|
49
|
+
end
|
50
|
+
|
51
|
+
def last_cmdline
|
52
|
+
@last_cmdline
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
attr_reader :command, :options, :stderr, :stdout
|
57
|
+
|
58
|
+
def initialize(command, options={})
|
59
|
+
@command, @options = command, options
|
60
|
+
end
|
61
|
+
|
62
|
+
def execute
|
63
|
+
@stdout, @stderr, @status = Open3.capture3(command, @options)
|
64
|
+
end
|
65
|
+
|
66
|
+
def exitstatus
|
67
|
+
@status.exitstatus
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
ENV['PATH'] = "#{File.expand_path "../../../bin", __FILE__}:#{ENV['PATH']}"
|
72
|
+
ENV['YO_IM_TESTING_README_SHIT_RIGHT_NOW'] = 'FOR_REALSIES'
|
73
|
+
World ProvingGrounds
|
74
|
+
World CommandLine::CukeHelpers
|
75
|
+
World Helpers
|
76
|
+
After { remove_proving_grounds }
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'deject'
|
2
|
+
|
3
|
+
require 'mountain_berry_fields/version'
|
4
|
+
require 'mountain_berry_fields/evaluator'
|
5
|
+
require 'mountain_berry_fields/parser'
|
6
|
+
|
7
|
+
# Ties everything together. It gets the file, passes it to the parser,
|
8
|
+
# passes the result to the evaluator, and writes the file. If anything
|
9
|
+
# goes wrong along the way, it declares the failure to the interaction
|
10
|
+
class MountainBerryFields
|
11
|
+
Deject self, :interaction
|
12
|
+
dependency(:evaluator_class) { Evaluator }
|
13
|
+
dependency(:parser_class) { Parser }
|
14
|
+
dependency(:file_class) { File }
|
15
|
+
dependency(:dir_class) { Dir }
|
16
|
+
|
17
|
+
def initialize(argv)
|
18
|
+
self.argv = argv
|
19
|
+
self.full_filename = File.expand_path filename if filename
|
20
|
+
end
|
21
|
+
|
22
|
+
def execute
|
23
|
+
return false if missing_input_file? || invalid_filename? || nonexistent_file?
|
24
|
+
execute!
|
25
|
+
end
|
26
|
+
|
27
|
+
def evaluator
|
28
|
+
@evaluator ||= evaluator_class.new parser.parse
|
29
|
+
end
|
30
|
+
|
31
|
+
def parser
|
32
|
+
@parser ||= parser_class.new file_class.read(full_filename),
|
33
|
+
visible: evaluator_class.visible_commands,
|
34
|
+
invisible: evaluator_class.invisible_commands
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def missing_input_file?
|
40
|
+
return if filename
|
41
|
+
interaction.declare_failure 'Please provide an input file'
|
42
|
+
true
|
43
|
+
end
|
44
|
+
|
45
|
+
def invalid_filename?
|
46
|
+
return if filename =~ filename_regex
|
47
|
+
interaction.declare_failure "#{filename.inspect} does not match #{filename_regex.inspect}"
|
48
|
+
true
|
49
|
+
end
|
50
|
+
|
51
|
+
def nonexistent_file?
|
52
|
+
return if file_class.exist? filename
|
53
|
+
interaction.declare_failure "#{File.expand_path(filename).inspect} does not exist."
|
54
|
+
true
|
55
|
+
end
|
56
|
+
|
57
|
+
def execute!
|
58
|
+
begin
|
59
|
+
if dir_class.chdir(dirname) { evaluator.tests_pass? }
|
60
|
+
file_class.write output_filename_for(filename), evaluator.document
|
61
|
+
true
|
62
|
+
else
|
63
|
+
interaction.declare_failure "FAILURE: #{evaluator.failure_name}\n#{evaluator.failure_message}"
|
64
|
+
false
|
65
|
+
end
|
66
|
+
rescue StandardError
|
67
|
+
interaction.declare_failure "#{$!.class} #{$!.message}"
|
68
|
+
false
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
attr_accessor :argv, :full_filename
|
73
|
+
|
74
|
+
def filename
|
75
|
+
argv.first
|
76
|
+
end
|
77
|
+
|
78
|
+
def dirname
|
79
|
+
File.dirname filename
|
80
|
+
end
|
81
|
+
|
82
|
+
def output_filename_for(filename)
|
83
|
+
filename.sub filename_regex, ''
|
84
|
+
end
|
85
|
+
|
86
|
+
def filename_regex
|
87
|
+
/\.mountain_berry_fields\b/
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class MountainBerryFields
|
2
|
+
|
3
|
+
# Iinteractions are used by the MountainBerryFields to let the users know what's going on.
|
4
|
+
# This one interacts with the command line.
|
5
|
+
class CommandLineInteraction
|
6
|
+
Deject self
|
7
|
+
dependency(:stderr) { $stderr }
|
8
|
+
|
9
|
+
def declare_failure(failure_message)
|
10
|
+
stderr.puts failure_message
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'mountain_berry_fields/test'
|
2
|
+
|
3
|
+
class MountainBerryFields
|
4
|
+
|
5
|
+
# This class evaluates a block of code that was emitted from the parser.
|
6
|
+
# It has a document that the evalutable code can append to.
|
7
|
+
# It implements the visible and invisible methods (see the parser for more on these)
|
8
|
+
# that MountainBerryFields supports.
|
9
|
+
#
|
10
|
+
# It also evaluates tests, and tracks whether they pass or fail (currently only tracking the last failure)
|
11
|
+
class Evaluator
|
12
|
+
attr_reader :to_evaluate
|
13
|
+
|
14
|
+
def initialize(to_evaluate)
|
15
|
+
@to_evaluate = to_evaluate
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.visible_commands
|
19
|
+
[:test]
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.invisible_commands
|
23
|
+
[:setup, :context]
|
24
|
+
end
|
25
|
+
|
26
|
+
def setup
|
27
|
+
setup_code << yield
|
28
|
+
end
|
29
|
+
|
30
|
+
def context(context_name, options={}, &block)
|
31
|
+
contexts[context_name] = in_context options[:context], block.call
|
32
|
+
return if contexts[context_name]['__CODE__']
|
33
|
+
raise ArgumentError, "Context #{context_name.inspect} does not have a __CODE__ block"
|
34
|
+
end
|
35
|
+
|
36
|
+
def contexts
|
37
|
+
@contexts ||= Hash.new
|
38
|
+
end
|
39
|
+
|
40
|
+
def setup_code
|
41
|
+
@setup_code ||= ''
|
42
|
+
end
|
43
|
+
|
44
|
+
def test(name, options={}, &block)
|
45
|
+
code = setup_code + in_context(options[:context], block.call.to_s)
|
46
|
+
test = Test.new(name, options.merge(code: code))
|
47
|
+
strategy = Test::Strategy.for(test.strategy).new(test.code)
|
48
|
+
unless strategy.pass?
|
49
|
+
@failing_strategy = strategy
|
50
|
+
@failing_test = test
|
51
|
+
end
|
52
|
+
tests << test
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
def in_context(context_name, code)
|
57
|
+
return code unless context_name
|
58
|
+
return contexts[context_name].gsub '__CODE__', code if contexts[context_name]
|
59
|
+
raise NameError, "There is no context #{context_name.inspect}, only #{contexts.keys.inspect}"
|
60
|
+
end
|
61
|
+
|
62
|
+
def tests_pass?
|
63
|
+
evaluate
|
64
|
+
!@failing_test
|
65
|
+
end
|
66
|
+
|
67
|
+
def failure_name
|
68
|
+
@failing_test.name
|
69
|
+
end
|
70
|
+
|
71
|
+
def failure_message
|
72
|
+
@failing_strategy.failure_message
|
73
|
+
end
|
74
|
+
|
75
|
+
def tests
|
76
|
+
@tests ||= []
|
77
|
+
end
|
78
|
+
|
79
|
+
def document
|
80
|
+
evaluate
|
81
|
+
@document ||= ''
|
82
|
+
end
|
83
|
+
|
84
|
+
def evaluate
|
85
|
+
return if @evaluated
|
86
|
+
@evaluated = true
|
87
|
+
instance_eval to_evaluate
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|