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,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