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