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