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