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,16 @@
1
+ require 'spec_helper'
2
+
3
+ describe MountainBerryFields::CommandLineInteraction do
4
+ let(:interaction) { described_class.new }
5
+ let(:stderr) { interaction.stderr.string }
6
+
7
+ it 'implements the interaction interface' do
8
+ Mock::Interaction.should substitute_for MountainBerryFields::CommandLineInteraction, subset: true
9
+ end
10
+
11
+ specify '#declare_failure(message) writes messages to stderr, with newlines' do
12
+ interaction.declare_failure "failure one"
13
+ interaction.declare_failure "failure two"
14
+ stderr.should == "failure one\nfailure two\n"
15
+ end
16
+ end
@@ -0,0 +1,170 @@
1
+ require 'spec_helper'
2
+
3
+ describe MountainBerryFields::Evaluator do
4
+ it 'implements the evaluator interface' do
5
+ Mock::Evaluator.should substitute_for described_class, subset: true
6
+ end
7
+
8
+ let(:to_evaluate) { '' }
9
+ let(:evaluator) { described_class.new to_evaluate }
10
+
11
+ it 'has a document which is initialized to empty string and memoized' do
12
+ evaluator = described_class.new ''
13
+ evaluator.document.should == ''
14
+ evaluator.document << 'abc'
15
+ evaluator.document.should == 'abc'
16
+ end
17
+
18
+ def should_evaluate
19
+ evaluator = described_class.new('def meth;end')
20
+ evaluator.should_not respond_to :meth
21
+ yield evaluator
22
+ evaluator.should respond_to :meth
23
+ end
24
+
25
+ it 'ensures evaluation when asked for its document' do
26
+ should_evaluate { |evaluator| evaluator.document }
27
+ end
28
+
29
+ it 'ensures evaluation when asked if tests pass' do
30
+ should_evaluate { |evaluator| evaluator.tests_pass? }
31
+ end
32
+
33
+ describe 'evaluation' do
34
+ it 'instance evals the string only once' do
35
+ evaluator = described_class.new('def meth;end; document << "abc"')
36
+ evaluator.evaluate
37
+ evaluator.evaluate
38
+ evaluator.document.should == 'abc'
39
+ end
40
+ end
41
+
42
+
43
+ describe '#tests_pass?' do
44
+ test_class = MountainBerryFields::Test
45
+
46
+ it 'evaluates the text if not evaluated' do
47
+ should_evaluate { |evaluator| evaluator.tests_pass? }
48
+ end
49
+
50
+ it 'returns true if all its tests pass' do
51
+ evaluator = described_class.new ''
52
+ evaluator.test('Passing test', with: :always_pass) {''}
53
+ evaluator.tests_pass?.should == true
54
+ end
55
+
56
+ it 'tracks the failure name and message, if any of its tests fail' do
57
+ evaluator = described_class.new ''
58
+ evaluator.test('Failbert', with: :always_fail) {}
59
+ evaluator.tests_pass?.should == false
60
+ evaluator.failure_name.should == 'Failbert'
61
+ evaluator.failure_message.should == MountainBerryFields::Test::Strategy.for(:always_fail).new('').failure_message
62
+ end
63
+ end
64
+
65
+ describe 'visible and invisble commands' do
66
+ specify '#visible_commands is an array of commands whose output should be displayed' do
67
+ described_class.visible_commands.should be_a_kind_of Array
68
+ end
69
+
70
+ specify '#invisible_commands is an array of commands whose output should be omitted from the final document' do
71
+ described_class.invisible_commands.should be_a_kind_of Array
72
+ end
73
+ end
74
+
75
+
76
+ describe '#test' do
77
+ it 'is visible' do
78
+ described_class.visible_commands.should include :test
79
+ end
80
+
81
+ it 'adds a test with the given name, and options' do
82
+ options = { code: 'some code', with: :always_fail }
83
+ evaluator = described_class.new ''
84
+ evaluator.tests.size.should == 0
85
+ evaluator.test('some name', options) { '' }
86
+ evaluator.tests.size.should == 1
87
+ evaluator.tests.first.name.should == 'some name'
88
+ evaluator.tests.first.strategy.should == :always_fail
89
+ end
90
+
91
+ it 'immediately evaluates the test' do
92
+ $abc = ''
93
+ evaluator.test 'whatev', with: :always_pass do
94
+ '$abc = "abc"'
95
+ end
96
+ $abc.should == 'abc'
97
+ end
98
+ end
99
+
100
+
101
+ describe '#setup' do
102
+ it 'is invisible' do
103
+ described_class.invisible_commands.should include :setup
104
+ end
105
+
106
+ it 'is prepended before each test added after it' do
107
+ evaluator.test('name', with: :always_pass) { "test1\n" }
108
+ evaluator.setup { "setup\n" }
109
+ evaluator.test('name', with: :always_pass) { "test2\n" }
110
+ evaluator.test('name', with: :always_pass) { "test3\n" }
111
+ evaluator.tests[0].code.should == "test1\n"
112
+ evaluator.tests[1].code.should == "setup\ntest2\n"
113
+ evaluator.tests[2].code.should == "setup\ntest3\n"
114
+ end
115
+
116
+ it 'appends to the setup code if invoked multiple times' do
117
+ evaluator.setup { "setup1\n" }
118
+ evaluator.setup { "setup2\n" }
119
+ evaluator.test('name', with: :always_pass) { "test1\n" }
120
+ evaluator.tests.first.code.should == "setup1\nsetup2\ntest1\n"
121
+ end
122
+ end
123
+
124
+
125
+ describe '#context' do
126
+ let(:context_name) { 'some context name' }
127
+ let(:test_name) { 'some test name' }
128
+
129
+ it 'is invisible' do
130
+ described_class.invisible_commands.should include :context
131
+ end
132
+
133
+ it 'must have at least one __CODE__ section in it' do
134
+ expect {
135
+ evaluator.context(context_name) { 'no code section' }
136
+ }.to raise_error ArgumentError, /#{context_name}.*__CODE__/
137
+ end
138
+
139
+ it 'replaces all __CODE__ section with the test using it' do
140
+ evaluator.context(context_name) { 'a __CODE__ c __CODE__' }
141
+ evaluator.test(test_name, with: :always_pass, context: context_name) { 'b' }
142
+ evaluator.tests.first.code.should == 'a b c b'
143
+ end
144
+
145
+ it 'raises an error when a test references a nonexistent context' do
146
+ evaluator.context(context_name) { '__CODE__' }
147
+ expect {
148
+ evaluator.test(test_name, with: :always_pass, context: context_name.reverse) {''}
149
+ }.to raise_error NameError, /#{context_name.reverse}.*#{context_name}/
150
+ end
151
+
152
+ it 'is applied after setup' do
153
+ evaluator.setup { 'a' }
154
+ evaluator.context(context_name) { ' b __CODE__' }
155
+ evaluator.test(test_name, context: context_name, with: :always_pass) { 'c' }
156
+ evaluator.tests.first.code.should == 'a b c'
157
+ end
158
+
159
+ let(:context_name1) { 'context name 1' }
160
+ let(:context_name2) { 'context name 2' }
161
+ specify 'contexts can have contexts of their own' do
162
+ evaluator.context(context_name1) { 'a __CODE__ b' }
163
+ evaluator.context(context_name2, context: context_name1) { 'c __CODE__ d' }
164
+ evaluator.test(test_name, context: context_name1, with: :always_pass) { 'e' }
165
+ evaluator.test(test_name, context: context_name2, with: :always_pass) { 'f' }
166
+ evaluator.tests[0].code.should == 'a e b'
167
+ evaluator.tests[1].code.should == 'a c f d b'
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+
3
+ describe Mock::File do
4
+ it 'is substitutable for ::File' do
5
+ Mock::File.should substitute_for ::File, subset: true
6
+ end
7
+ end
8
+
9
+ describe Mock::Dir do
10
+ it 'is substitutable for ::Dir' do
11
+ Mock::Dir.should substitute_for Dir, subset: true
12
+ end
13
+ end
14
+
15
+ describe Mock::Process::Status do
16
+ it 'is substitutable for ::Process::Status' do
17
+ Mock::Process::Status.should substitute_for ::Process::Status, subset: true
18
+ end
19
+ end
20
+
21
+ describe Mock::Open3 do
22
+ it 'is substitutable for ::Open3' do
23
+ Mock::Open3.should substitute_for ::Open3, subset: true
24
+ end
25
+ end
@@ -0,0 +1,147 @@
1
+ require 'spec_helper'
2
+
3
+ describe MountainBerryFields do
4
+ let(:dir_class) { mbf.dir_class }
5
+ let(:file_class) { mbf.file_class }
6
+ let(:stderr) { mbf.stderr.string }
7
+ let(:interaction) { mbf.interaction }
8
+ let(:evaluator) { mbf.evaluator }
9
+ let(:parser) { mbf.parser }
10
+
11
+ shared_examples 'a failure' do
12
+ it 'returns false' do
13
+ mbf.execute.should == false
14
+ end
15
+
16
+ it 'does not write any files' do
17
+ file_class.should_not have_been_told_to :write
18
+ end
19
+ end
20
+
21
+
22
+ context 'when no input file is provided' do
23
+ nonexistence_message = 'Please provide an input file'
24
+ let(:mbf) { described_class.new [] }
25
+
26
+ it_behaves_like 'a failure'
27
+
28
+ it "declares the error '#{nonexistence_message}'" do
29
+ mbf.execute
30
+ interaction.should have_been_told_to(:declare_failure).with(nonexistence_message)
31
+ end
32
+ end
33
+
34
+
35
+ context 'when the input file does not exist' do
36
+ nonexistent_filename = '/some/bullshit/file.mountain_berry_fields.md'.freeze
37
+ nonexistence_message = "#{nonexistent_filename.inspect} does not exist.".freeze
38
+
39
+ let(:mbf) { described_class.new [nonexistent_filename] }
40
+ before { file_class.will_exist? false }
41
+
42
+ it_behaves_like 'a failure'
43
+
44
+ it "declares the error '#{nonexistent_filename}'" do
45
+ mbf.execute
46
+ file_class.should have_been_asked_for_its(:exist?).with(nonexistent_filename)
47
+ interaction.should have_been_told_to(:declare_failure).with(nonexistence_message)
48
+ end
49
+ end
50
+
51
+
52
+ context 'when the input file does not match /\.mountain_berry_fields\b/' do
53
+ invalid_filename = "invalid_filename.md"
54
+ invalid_filename_message = "#{invalid_filename.inspect} does not match /\\.mountain_berry_fields\\b/"
55
+ let(:mbf) { described_class.new [invalid_filename] }
56
+
57
+ it "declares the error '#{invalid_filename_message}'" do
58
+ mbf.execute
59
+ interaction.should have_been_told_to(:declare_failure).with(invalid_filename_message)
60
+ end
61
+ end
62
+
63
+ context 'when unsuccessfully parsing a file' do
64
+ let(:input_filename) { 'some_invalid_file.mountain_berry_fields.md' }
65
+ let(:mbf) { described_class.new [input_filename] }
66
+ before { parser.will_parse StandardError.new "some error message" }
67
+
68
+ it_behaves_like 'a failure'
69
+
70
+ it 'writes the exception class and message as the error' do
71
+ mbf.execute
72
+ interaction.should have_been_told_to(:declare_failure).with("StandardError some error message")
73
+ end
74
+ end
75
+
76
+ context 'when successfully parsing a file' do
77
+ let(:input_filepath) { '/a/b/c' }
78
+ let(:input_filename) { input_filepath + '/some_valid_file.mountain_berry_fields.md' }
79
+ let(:output_filename) { input_filepath + '/some_valid_file.md' }
80
+ let(:mbf) { described_class.new [input_filename] }
81
+ let(:file_body) { 'SOME FILE BODY' }
82
+ let(:parsed_body) { 'PARSED BODY' }
83
+ let(:document) { 'EVALUATED DOCUMENT' }
84
+
85
+ before { file_class.will_read file_body }
86
+ before { parser.will_parse parsed_body }
87
+
88
+ it 'declares no errors' do
89
+ mbf.execute
90
+ interaction.should_not have_been_told_to :declare_failure
91
+ end
92
+
93
+ it 'passes the file contents to the parser' do
94
+ visible_commands = mbf.evaluator_class.visible_commands
95
+ invisible_commands = mbf.evaluator_class.invisible_commands
96
+ mbf.execute
97
+ file_class.should have_been_told_to(:read).with(/#{input_filename}$/)
98
+ parser.should have_been_initialized_with file_body, visible: visible_commands, invisible: invisible_commands
99
+ end
100
+
101
+ it "evaluates the results of the parsing, in the input file's directory" do
102
+ dir_class.will_chdir false
103
+ mbf.execute
104
+ dir_class.should have_been_told_to(:chdir).with(input_filepath) { |block|
105
+ block.before { evaluator.should_not have_been_asked_if :tests_pass? }
106
+ block.after { evaluator.should have_been_asked_if :tests_pass? }
107
+ }
108
+ evaluator.should have_been_initialized_with parsed_body
109
+ end
110
+
111
+
112
+ context 'when the tests pass' do
113
+ before { evaluator.will_tests_pass? true }
114
+ before { evaluator.will_have_document document }
115
+
116
+ it 'returns true' do
117
+ mbf.execute.should == true
118
+ end
119
+
120
+ it 'writes the interpreted file to the new filename (the same name, but with mountain_berry_fields segment removed)' do
121
+ mbf.execute
122
+ file_class.should have_been_told_to(:write).with(output_filename, document)
123
+ end
124
+ end
125
+
126
+ context 'when the tests fail' do
127
+ before { evaluator.will_tests_pass? false }
128
+ it_behaves_like 'a failure'
129
+
130
+ it "declares the failure with the evaluator's failure's name and message" do
131
+ evaluator.will_have_failure_name "sir"
132
+ evaluator.will_have_failure_message "failsalot"
133
+ mbf.execute
134
+ interaction.should have_been_told_to(:declare_failure).with("FAILURE: sir\nfailsalot")
135
+ end
136
+ end
137
+
138
+ context 'when evaluation raises an exception' do
139
+ before { evaluator.will_tests_pass? StandardError.new('blah') }
140
+ it_behaves_like 'a failure'
141
+ it 'writes the exception class and message as the error' do
142
+ mbf.execute
143
+ interaction.should have_been_told_to(:declare_failure).with("StandardError blah")
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,139 @@
1
+ require 'spec_helper'
2
+
3
+ describe MountainBerryFields::Parser do
4
+ let :evaluator_class do
5
+ Class.new do
6
+ attr_reader :document, :visible_saw, :invisible_saw
7
+
8
+ def initialize
9
+ @document = ''
10
+ @visible_saw = []
11
+ @invisible_saw = []
12
+ end
13
+
14
+ def visible
15
+ @visible_saw << yield
16
+ 'visible'
17
+ end
18
+
19
+ def invisible
20
+ @invisible_saw << yield
21
+ 'invisible'
22
+ end
23
+
24
+ def record_args(*args)
25
+ @recorded_args = args
26
+ end
27
+
28
+ def recorded_args
29
+ @recorded_args
30
+ end
31
+
32
+ def inspect
33
+ "#<evaluator_class>"
34
+ end
35
+ end
36
+ end
37
+
38
+ attr_reader :evaluator
39
+
40
+ def visible_in(code)
41
+ parsed code
42
+ evaluator.visible_saw
43
+ end
44
+
45
+ def invisible_in(code)
46
+ parsed code
47
+ evaluator.invisible_saw
48
+ end
49
+
50
+ def recorded_args(code)
51
+ parsed code
52
+ evaluator.recorded_args
53
+ end
54
+
55
+ def parsed(text)
56
+ program = described_class.new(text, visible: [:visible], invisible: [:invisible]).parse
57
+ @evaluator = evaluator_class.new
58
+ @evaluator.instance_eval program
59
+ @evaluator.document
60
+ end
61
+
62
+ it 'parses normal erb code' do
63
+ parsed("a\n<% if true %>\ndo shit\n<% end %>b").should == "a\ndo shit\nb\n"
64
+ parsed("a\n<% if false %>do shit<% end %>b").should == "a\nb\n"
65
+ parsed("a<% if true %>b<% end %>c").should == "abc\n"
66
+ parsed("a<%= 1 + 2 %>b").should == "a3b\n"
67
+ parsed("a<%# comment %>b").should == "ab\n"
68
+ parsed("a<%% whatev %>b").should == "a<% whatev %>b\n"
69
+ parsed("a<%%= whatev %>b").should == "a<%= whatev %>b\n"
70
+ parsed("a<% if 'I' %>b<% end %>c").should == "abc\n"
71
+ recorded_args("a<% record_args 'I' %>").should == ['I']
72
+ recorded_args("a<%= record_args 'I' %>").should == ['I']
73
+ end
74
+
75
+ specify 'results always end with a newline' do
76
+ parsed("abc").should == "abc\n"
77
+ end
78
+
79
+ describe 'visible methods' do
80
+ specify 'are provided in a list to the constructor and converted to strings' do
81
+ described_class.new('', visible: [:a]).visible_commands.should == ['a']
82
+ end
83
+
84
+ specify 'default to an empty array' do
85
+ described_class.new('').visible_commands.should == []
86
+ end
87
+
88
+ specify 'show up in the document when evaluated' do
89
+ parsed("a<% visible do %>b<% end %>c").should == "abc\n"
90
+ end
91
+
92
+ specify 'are returned when the block is invoked' do
93
+ visible_in("a<% visible do %>b<% end %>c").should == ['b']
94
+ end
95
+
96
+ specify "support basic nesting (can't do complex nesting without editing parse trees -- not a big deal, these are things that even erubis can't do)" do
97
+ visible_in("a<%visible {%>b<%visible {%>c<% } %>d<% } %>e").should == ['c', 'bd']
98
+ visible_in("a<%visible {%>b<%if false %>c<% end %>d<% } %>e").should == ['bd']
99
+ visible_in("<%# visible %>").should == []
100
+ visible_in('<% visible do %>\'a\'\\<% end %>').should == ['\'a\'\\']
101
+ end
102
+ end
103
+
104
+ describe 'invisible methods' do
105
+ specify 'are provided in a list to the constructor and converted to strings' do
106
+ described_class.new('', invisible: [:a]).invisible_commands.should == ['a']
107
+ end
108
+
109
+ specify 'default to an empty array' do
110
+ described_class.new('').invisible_commands.should == []
111
+ end
112
+
113
+ specify 'do not show up in the document when evaluated' do
114
+ parsed("a<% invisible do %>b<% end %>c").should == "ac\n"
115
+ end
116
+
117
+ specify 'are returned when the block is invoked' do
118
+ invisible_in("a<% invisible do %>b<% end %>c").should == ['b']
119
+ end
120
+
121
+ specify "support basic nesting (can't do complex nesting without editing parse trees -- not a big deal, these are things that even erubis can't do)" do
122
+ invisible_in("a<%invisible {%>b<%invisible {%>c<% } %>d<% } %>e").should == ['c', 'bd']
123
+ invisible_in("a<%invisible {%>b<%if false %>c<% end %>d<% } %>e").should == ['bd']
124
+ invisible_in("<%# invisible %>").should == []
125
+ invisible_in('<% invisible do %>\'a\'\\<% end %>').should == ['\'a\'\\']
126
+ end
127
+ end
128
+
129
+ # Unfortunately, this problem is in Erubis.
130
+ # It might be possible to fix it in the subclass,
131
+ # but that seems like a lot of work for an incredibly rare use case
132
+ # so just going to leave this pending for now.
133
+ #
134
+ # Erubis::Eruby.new('<% a = "<% b %>"; a * 2 %>').src # => "_buf = ''; a = \"<% b ; _buf << '\"; a * 2 %>';\n_buf.to_s\n"
135
+ xit 'ignores erb tags inside erb blocks' do
136
+ parse("a<% s='<% visible { %>b<% } %>' %>c").should == "ac\n"
137
+ parsed("a<% s='<% invisible { %>b<% } %>' %>c").should == "ac\n"
138
+ end
139
+ end