seeing_is_believing 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile.lock +3 -1
- data/Readme.md +28 -6
- data/bin/seeing_is_believing +2 -3
- data/features/binary.feature +89 -0
- data/features/step_definitions/steps.rb +2 -2
- data/features/support/env.rb +15 -2
- data/lib/seeing_is_believing/error.rb +7 -0
- data/lib/seeing_is_believing/evaluate_by_moving_files.rb +118 -0
- data/lib/seeing_is_believing/example_use.rb +64 -18
- data/lib/seeing_is_believing/expression_list.rb +35 -23
- data/lib/seeing_is_believing/hard_core_ensure.rb +52 -0
- data/lib/seeing_is_believing/has_exception.rb +6 -0
- data/lib/seeing_is_believing/result.rb +26 -20
- data/lib/seeing_is_believing/syntax_analyzer.rb +119 -37
- data/lib/seeing_is_believing/the_matrix.rb +19 -0
- data/lib/seeing_is_believing/tracks_line_numbers_seen.rb +18 -0
- data/lib/seeing_is_believing/version.rb +1 -1
- data/lib/seeing_is_believing.rb +54 -13
- data/seeing_is_believing.gemspec +1 -0
- data/spec/evaluate_by_moving_files_spec.rb +68 -0
- data/spec/expression_list_spec.rb +22 -4
- data/spec/hard_core_ensure_spec.rb +73 -0
- data/spec/seeing_is_believing_spec.rb +94 -11
- data/spec/syntax_analyzer_spec.rb +112 -1
- metadata +27 -6
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
seeing_is_believing (0.0.
|
4
|
+
seeing_is_believing (0.0.4)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: http://rubygems.org/
|
@@ -15,6 +15,7 @@ GEM
|
|
15
15
|
diff-lcs (1.1.3)
|
16
16
|
gherkin (2.11.5)
|
17
17
|
json (>= 1.4.6)
|
18
|
+
ichannel (5.1.1)
|
18
19
|
json (1.7.6)
|
19
20
|
rspec (2.12.0)
|
20
21
|
rspec-core (~> 2.12.0)
|
@@ -30,5 +31,6 @@ PLATFORMS
|
|
30
31
|
|
31
32
|
DEPENDENCIES
|
32
33
|
cucumber (~> 1.2.1)
|
34
|
+
ichannel (~> 5.1.1)
|
33
35
|
rspec (~> 2.12.0)
|
34
36
|
seeing_is_believing!
|
data/Readme.md
CHANGED
@@ -9,8 +9,8 @@ Reeaally rough at the moment, but it works for simple examples.
|
|
9
9
|
|
10
10
|
Also comes with a binary to show how it might be used.
|
11
11
|
|
12
|
-
Use
|
13
|
-
|
12
|
+
Use The Binary
|
13
|
+
==============
|
14
14
|
|
15
15
|
```ruby
|
16
16
|
# $ seeing_is_believing proving_grounds/basic_functionality.rb
|
@@ -21,6 +21,8 @@ end # => 5
|
|
21
21
|
def meth(n)
|
22
22
|
n # => "12", "34"
|
23
23
|
end # => nil
|
24
|
+
|
25
|
+
# some invocations
|
24
26
|
meth "12" # => "12"
|
25
27
|
meth "34" # => "34"
|
26
28
|
```
|
@@ -37,6 +39,27 @@ raise "ZOMG!" # ~> RuntimeError: ZOMG!
|
|
37
39
|
ZOMG!
|
38
40
|
```
|
39
41
|
|
42
|
+
Use The Lib
|
43
|
+
===========
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
require 'seeing_is_believing'
|
47
|
+
|
48
|
+
believer = SeeingIsBelieving.new("%w[a b c].each do |i|
|
49
|
+
i.upcase
|
50
|
+
end")
|
51
|
+
|
52
|
+
result = believer.call
|
53
|
+
result # => #<SeeingIsBelieving::Result:0x007faeed1a5b78 @max_line_number=3, @min_line_number=1, @results={2=>['"A"', '"B"', '"C"'], 3=>['["a", "b", "c"]']}>
|
54
|
+
|
55
|
+
result.to_a # => [ [1, []],
|
56
|
+
# [2, ['"A"', '"B"', '"C"']],
|
57
|
+
# [3, ['["a", "b", "c"]']]
|
58
|
+
# ]
|
59
|
+
|
60
|
+
result[2] # => ['"A"', '"B"', '"C"']
|
61
|
+
```
|
62
|
+
|
40
63
|
Install
|
41
64
|
=======
|
42
65
|
|
@@ -49,11 +72,10 @@ Or if you haven't fixed your gem home, and you aren't using any version managers
|
|
49
72
|
Known Issues
|
50
73
|
============
|
51
74
|
|
52
|
-
* comments will kill it, probably going to have to actually parse the code to fix this
|
53
|
-
* multi-line strings will probably kill it, probably going to have to actually parse the code to fix this
|
54
|
-
* I have no idea what happens if you talk to stdout/stderr directly. This should become a non-issue if we evaluate it in its own process like xmpfilter.
|
55
|
-
* If it dies, it will take your program with it. Same as above. (e.g. running the binary against itself will cause it to recursively invoke itself forever WARNING: DON'T REALLY DO THAT, ITS CRAYAZAY)
|
56
75
|
* No idea what happens if you give it a syntactically invalid file. It probably just raises an exception, but might possibly freeze up or something.
|
76
|
+
* heredocs breaks things maybe also `BEGIN/END` and `=begin/=end`
|
77
|
+
* There are expressions which continue on the next line even though the previous line is a valid expression, e.g. "3\n.times { |i| i }" which will blow up. This is a fundamental flaw in the algorithm and will either require a smarter algorithm, or some sort of more sophisticated parsing in order to handle correctly
|
78
|
+
* Probably doesn't handle stdin correctly
|
57
79
|
|
58
80
|
License
|
59
81
|
=======
|
data/bin/seeing_is_believing
CHANGED
@@ -6,9 +6,8 @@ require 'seeing_is_believing/example_use'
|
|
6
6
|
status = 0
|
7
7
|
|
8
8
|
ARGV.each do |filename|
|
9
|
-
believer = SeeingIsBelieving::ExampleUse.new
|
10
|
-
believer.call
|
11
|
-
$stdout.puts believer.output
|
9
|
+
believer = SeeingIsBelieving::ExampleUse.new File.read(filename), filename
|
10
|
+
$stdout.puts believer.call
|
12
11
|
if believer.has_exception?
|
13
12
|
$stderr.puts believer.exception.message
|
14
13
|
status = 1
|
data/features/binary.feature
CHANGED
@@ -19,6 +19,11 @@ Feature: Running the binary
|
|
19
19
|
# some invocations
|
20
20
|
meth "12"
|
21
21
|
meth "34"
|
22
|
+
|
23
|
+
# multilinezzz
|
24
|
+
"a
|
25
|
+
b
|
26
|
+
c"
|
22
27
|
"""
|
23
28
|
When I run "seeing_is_believing basic_functionality.rb"
|
24
29
|
Then stderr is empty
|
@@ -36,6 +41,11 @@ Feature: Running the binary
|
|
36
41
|
# some invocations
|
37
42
|
meth "12" # => "12"
|
38
43
|
meth "34" # => "34"
|
44
|
+
|
45
|
+
# multilinezzz
|
46
|
+
"a
|
47
|
+
b
|
48
|
+
c" # => "a\n b\n c"
|
39
49
|
"""
|
40
50
|
|
41
51
|
Scenario: Raising exceptions
|
@@ -55,7 +65,86 @@ Feature: Running the binary
|
|
55
65
|
1 + 1
|
56
66
|
"""
|
57
67
|
|
68
|
+
Scenario: Passing previous output back into input
|
69
|
+
Given the file "previous_output.rb":
|
70
|
+
"""
|
71
|
+
1 + 1 # => not 2
|
72
|
+
2 + 2 # ~> Exception, something
|
73
|
+
|
74
|
+
|
75
|
+
# >> some stdout output
|
76
|
+
|
77
|
+
# !> some stderr output
|
78
|
+
__END__
|
79
|
+
"""
|
80
|
+
When I run "seeing_is_believing previous_output.rb"
|
81
|
+
Then stderr is empty
|
82
|
+
And the exit status is 0
|
83
|
+
And stdout is:
|
84
|
+
"""
|
85
|
+
1 + 1 # => 2
|
86
|
+
2 + 2 # => 4
|
87
|
+
|
88
|
+
__END__
|
89
|
+
"""
|
90
|
+
|
58
91
|
Scenario: Printing within the file
|
92
|
+
Given the file "printing.rb":
|
93
|
+
"""
|
94
|
+
print "hel"
|
95
|
+
puts "lo!"
|
96
|
+
puts ":)"
|
97
|
+
$stderr.puts "goodbye"
|
98
|
+
"""
|
99
|
+
When I run "seeing_is_believing printing.rb"
|
100
|
+
Then stderr is empty
|
101
|
+
And the exit status is 0
|
102
|
+
And stdout is:
|
103
|
+
"""
|
104
|
+
print "hel" # => nil
|
105
|
+
puts "lo!" # => nil
|
106
|
+
puts ":)" # => nil
|
107
|
+
$stderr.puts "goodbye" # => nil
|
108
|
+
|
109
|
+
# >> hello!
|
110
|
+
# >> :)
|
111
|
+
|
112
|
+
# !> goodbye
|
113
|
+
"""
|
114
|
+
|
115
|
+
Scenario: Respects macros
|
116
|
+
Given the file "some_dir/uses_macros.rb":
|
117
|
+
"""
|
118
|
+
__FILE__
|
119
|
+
__LINE__
|
120
|
+
$stdout.puts "omg"
|
121
|
+
$stderr.puts "hi"
|
122
|
+
DATA.read
|
123
|
+
__LINE__
|
124
|
+
__END__
|
125
|
+
1
|
126
|
+
2
|
127
|
+
"""
|
128
|
+
When I run "seeing_is_believing some_dir/uses_macros.rb"
|
129
|
+
Then stderr is empty
|
130
|
+
And the exit status is 0
|
131
|
+
And stdout is:
|
132
|
+
"""
|
133
|
+
__FILE__ # => "{{CommandLineHelpers.path_to 'some_dir/uses_macros.rb'}}"
|
134
|
+
__LINE__ # => 2
|
135
|
+
$stdout.puts "omg" # => nil
|
136
|
+
$stderr.puts "hi" # => nil
|
137
|
+
DATA.read # => "1\n2"
|
138
|
+
__LINE__ # => 6
|
139
|
+
|
140
|
+
# >> omg
|
141
|
+
|
142
|
+
# !> hi
|
143
|
+
__END__
|
144
|
+
1
|
145
|
+
2
|
146
|
+
"""
|
147
|
+
|
59
148
|
Scenario: Requiring other files
|
60
149
|
Scenario: Syntactically invalid file
|
61
150
|
Scenario: Passing a nonexistent file
|
@@ -7,11 +7,11 @@ When 'I run "$command"' do |command|
|
|
7
7
|
end
|
8
8
|
|
9
9
|
Then /^(stderr|stdout) is:$/ do |stream_name, output|
|
10
|
-
@last_executed.send(stream_name).chomp.should == output
|
10
|
+
@last_executed.send(stream_name).chomp.should == eval_curlies(output)
|
11
11
|
end
|
12
12
|
|
13
13
|
Then /^(stderr|stdout) is "(.*?)"$/ do |stream_name, output|
|
14
|
-
@last_executed.send(stream_name).chomp.should == output
|
14
|
+
@last_executed.send(stream_name).chomp.should == eval_curlies(output)
|
15
15
|
end
|
16
16
|
|
17
17
|
Then 'the exit status is $status' do |status|
|
data/features/support/env.rb
CHANGED
@@ -13,14 +13,15 @@ module CommandLineHelpers
|
|
13
13
|
|
14
14
|
def write_file(filename, body)
|
15
15
|
in_proving_grounds do
|
16
|
+
FileUtils.mkdir_p File.dirname filename
|
16
17
|
File.open(filename, 'w') { |file| file.write body }
|
17
18
|
end
|
18
19
|
end
|
19
20
|
|
20
21
|
def execute(command)
|
21
22
|
in_proving_grounds do
|
22
|
-
|
23
|
-
Invocation.new *Open3.capture3(command)
|
23
|
+
bin_in_path = {'PATH' => "#{bin_dir}:#{ENV['PATH']}"}
|
24
|
+
Invocation.new *Open3.capture3(bin_in_path, command)
|
24
25
|
end
|
25
26
|
end
|
26
27
|
|
@@ -47,6 +48,18 @@ module CommandLineHelpers
|
|
47
48
|
def bin_dir
|
48
49
|
File.join root_dir, "bin"
|
49
50
|
end
|
51
|
+
|
52
|
+
def path_to(filename)
|
53
|
+
in_proving_grounds { File.join proving_grounds_dir, filename }
|
54
|
+
end
|
50
55
|
end
|
51
56
|
|
52
57
|
CommandLineHelpers.make_proving_grounds
|
58
|
+
|
59
|
+
module GeneralHelpers
|
60
|
+
def eval_curlies(string)
|
61
|
+
string.gsub(/{{(.*?)}}/) { eval $1 }
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
World GeneralHelpers
|
@@ -0,0 +1,118 @@
|
|
1
|
+
# Not sure what the best way to evaluate these is
|
2
|
+
# This approach will move the old file out of the way,
|
3
|
+
# write the program in its place, invoke it, and move it back.
|
4
|
+
#
|
5
|
+
# Another option is to replace __FILE__ macros ourselves
|
6
|
+
# and then write to a temp file but evaluate in the context
|
7
|
+
# of the expected directory. I'm not doing that just because
|
8
|
+
# I don't think the __FILE__ macro can be replaced correctly
|
9
|
+
# without parsing the code, changing the AST, and then
|
10
|
+
# regenerating it, which I'm not good enough to do. Though
|
11
|
+
# I did look at Ripper, and it will invoke on_kw("__FILE__")
|
12
|
+
# when it sees this.
|
13
|
+
|
14
|
+
require 'yaml'
|
15
|
+
require 'open3'
|
16
|
+
require 'fileutils'
|
17
|
+
require 'seeing_is_believing/error'
|
18
|
+
require 'seeing_is_believing/result'
|
19
|
+
require 'seeing_is_believing/hard_core_ensure'
|
20
|
+
|
21
|
+
class SeeingIsBelieving
|
22
|
+
class EvaluateByMovingFiles
|
23
|
+
attr_accessor :program, :filename, :error_stream
|
24
|
+
|
25
|
+
def initialize(program, filename, options={})
|
26
|
+
self.program = program
|
27
|
+
self.filename = File.expand_path(filename)
|
28
|
+
self.error_stream = options.fetch :error_stream, $stderr
|
29
|
+
end
|
30
|
+
|
31
|
+
def call
|
32
|
+
@result ||= HardCoreEnsure.call(
|
33
|
+
code: -> {
|
34
|
+
dont_overwrite_existing_tempfile!
|
35
|
+
move_file_to_tempfile
|
36
|
+
write_program_to_file
|
37
|
+
begin
|
38
|
+
evaluate_file
|
39
|
+
fail unless exitstatus.success?
|
40
|
+
deserialize_result
|
41
|
+
rescue Exception
|
42
|
+
record_error
|
43
|
+
raise $!
|
44
|
+
end
|
45
|
+
},
|
46
|
+
ensure: -> {
|
47
|
+
restore_backup
|
48
|
+
}
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
52
|
+
def file_directory
|
53
|
+
File.dirname filename
|
54
|
+
end
|
55
|
+
|
56
|
+
def temp_filename
|
57
|
+
File.join file_directory, "seeing_is_believing_backup.#{File.basename filename}"
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
attr_accessor :stdout, :stderr, :exitstatus
|
63
|
+
|
64
|
+
def dont_overwrite_existing_tempfile!
|
65
|
+
return unless File.exist? temp_filename
|
66
|
+
raise TempFileAlreadyExists,
|
67
|
+
"Trying to back up #{filename.inspect} (FILE) to #{temp_filename.inspect} (TEMPFILE) but TEMPFILE already exists."\
|
68
|
+
" You should check the contents of these files. If FILE is correct, then delete TEMPFILE."\
|
69
|
+
" Otherwise rename TEMPFILE to FILE."
|
70
|
+
end
|
71
|
+
|
72
|
+
def move_file_to_tempfile
|
73
|
+
return unless File.exist? filename
|
74
|
+
FileUtils.mv filename, temp_filename
|
75
|
+
@was_backed_up = true
|
76
|
+
end
|
77
|
+
|
78
|
+
def restore_backup
|
79
|
+
return unless @was_backed_up
|
80
|
+
FileUtils.mv temp_filename, filename
|
81
|
+
end
|
82
|
+
|
83
|
+
def write_program_to_file
|
84
|
+
File.open(filename, 'w') { |f| f.write program.to_s }
|
85
|
+
end
|
86
|
+
|
87
|
+
def evaluate_file
|
88
|
+
self.stdout, self.stderr, self.exitstatus = Open3.capture3(
|
89
|
+
'ruby', '-W0', # no warnings (b/c I hijack STDOUT/STDERR)
|
90
|
+
'-I', File.expand_path('../..', __FILE__), # fix load path
|
91
|
+
'-r', 'seeing_is_believing/the_matrix', # hijack the environment so it can be recorded
|
92
|
+
'-C', file_directory, # run in the file's directory
|
93
|
+
filename
|
94
|
+
)
|
95
|
+
end
|
96
|
+
|
97
|
+
def fail
|
98
|
+
raise "Exitstatus: #{exitstatus.inspect},\nError: #{stderr.inspect}"
|
99
|
+
end
|
100
|
+
|
101
|
+
def deserialize_result
|
102
|
+
YAML.load stdout
|
103
|
+
end
|
104
|
+
|
105
|
+
def record_error
|
106
|
+
error_stream.puts "It blew up. Not too surprising given that seeing_is_believing is pretty rough around the edges, but still this shouldn't happen."
|
107
|
+
error_stream.puts "Please log an issue at: https://github.com/JoshCheek/seeing_is_believing/issues"
|
108
|
+
error_stream.puts
|
109
|
+
error_stream.puts "Program: #{program.inspect}"
|
110
|
+
error_stream.puts
|
111
|
+
error_stream.puts "Stdout: #{stdout.inspect}"
|
112
|
+
error_stream.puts
|
113
|
+
error_stream.puts "Stderr: #{stderr.inspect}"
|
114
|
+
error_stream.puts
|
115
|
+
error_stream.puts "Status: #{exitstatus.inspect}"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -1,50 +1,96 @@
|
|
1
1
|
require 'seeing_is_believing'
|
2
|
+
require 'seeing_is_believing/has_exception'
|
2
3
|
|
3
4
|
class SeeingIsBelieving
|
4
5
|
class ExampleUse
|
5
|
-
|
6
|
+
include HasException
|
6
7
|
|
7
|
-
|
8
|
-
|
8
|
+
STDOUT_PREFIX = '# >>'
|
9
|
+
STDERR_PREFIX = '# !>'
|
10
|
+
EXCEPTION_PREFIX = '# ~>'
|
11
|
+
RESULT_PREFIX = '# =>'
|
12
|
+
|
13
|
+
def initialize(body, filename=nil)
|
14
|
+
self.body = remove_previous_output_from body
|
15
|
+
self.filename = filename
|
16
|
+
end
|
17
|
+
|
18
|
+
def new_body
|
19
|
+
@new_body ||= ''
|
9
20
|
end
|
10
21
|
|
11
22
|
def call
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
23
|
+
evaluate_program
|
24
|
+
inherit_exception
|
25
|
+
add_each_line_until_data_segment
|
26
|
+
add_stdout
|
27
|
+
add_stderr
|
28
|
+
add_data_segment
|
29
|
+
new_body
|
18
30
|
end
|
19
31
|
|
20
|
-
|
21
|
-
|
32
|
+
private
|
33
|
+
|
34
|
+
attr_accessor :body, :filename, :file_result
|
35
|
+
|
36
|
+
def evaluate_program
|
37
|
+
self.file_result = SeeingIsBelieving.new(body, filename: filename).call
|
22
38
|
end
|
23
39
|
|
24
|
-
|
40
|
+
def inherit_exception
|
41
|
+
self.exception = file_result.exception
|
42
|
+
end
|
25
43
|
|
26
|
-
|
44
|
+
def add_each_line_until_data_segment
|
45
|
+
body.each_line.with_index 1 do |line, index|
|
46
|
+
break if start_of_data_segment? line
|
47
|
+
new_body << format_line(line.chomp, file_result[index])
|
48
|
+
end
|
49
|
+
end
|
27
50
|
|
28
|
-
|
51
|
+
def add_data_segment
|
52
|
+
body.each_line
|
53
|
+
.drop_while { |line| not start_of_data_segment? line }
|
54
|
+
.each { |line| new_body << line }
|
55
|
+
end
|
29
56
|
|
30
|
-
def
|
31
|
-
|
57
|
+
def start_of_data_segment?(line)
|
58
|
+
line.chomp == '__END__'
|
32
59
|
end
|
33
60
|
|
34
61
|
# max line length of the body + 2 spaces for padding
|
35
62
|
def line_length
|
36
63
|
@line_length ||= 2 + body.each_line
|
37
64
|
.map(&:chomp)
|
65
|
+
.take_while { |line| not start_of_data_segment? line }
|
38
66
|
.reject { |line| SyntaxAnalyzer.ends_in_comment? line }
|
39
67
|
.map(&:length)
|
40
68
|
.max
|
41
69
|
end
|
42
70
|
|
71
|
+
def remove_previous_output_from(string)
|
72
|
+
string.gsub(/\s+(#{EXCEPTION_PREFIX}|#{RESULT_PREFIX}).*?$/, '')
|
73
|
+
.gsub(/(\n)?(^#{STDOUT_PREFIX}[^\n]*\r?\n?)+/m, '')
|
74
|
+
.gsub(/(\n)?(^#{STDERR_PREFIX}[^\n]*\r?\n?)+/m, '')
|
75
|
+
end
|
76
|
+
|
77
|
+
def add_stdout
|
78
|
+
return unless file_result.has_stdout?
|
79
|
+
new_body << "\n"
|
80
|
+
file_result.stdout.each_line { |line| new_body << "#{STDOUT_PREFIX} #{line}" }
|
81
|
+
end
|
82
|
+
|
83
|
+
def add_stderr
|
84
|
+
return unless file_result.has_stderr?
|
85
|
+
new_body << "\n"
|
86
|
+
file_result.stderr.each_line { |line| new_body << "#{STDERR_PREFIX} #{line}" }
|
87
|
+
end
|
88
|
+
|
43
89
|
def format_line(line, line_results)
|
44
90
|
if line_results.has_exception?
|
45
|
-
sprintf "%-#{line_length}s#
|
91
|
+
sprintf "%-#{line_length}s#{EXCEPTION_PREFIX} %s: %s\n", line, line_results.exception.class, line_results.exception.message
|
46
92
|
elsif line_results.any?
|
47
|
-
sprintf "%-#{line_length}s#
|
93
|
+
sprintf "%-#{line_length}s#{RESULT_PREFIX} %s\n", line, line_results.join(', ')
|
48
94
|
else
|
49
95
|
line + "\n"
|
50
96
|
end
|
@@ -1,6 +1,8 @@
|
|
1
1
|
require 'open3'
|
2
2
|
require 'seeing_is_believing/syntax_analyzer'
|
3
3
|
|
4
|
+
# A lot of colouring going on in this file, maybe should extract a debugging object to contain it
|
5
|
+
|
4
6
|
class SeeingIsBelieving
|
5
7
|
class ExpressionList
|
6
8
|
PendingExpression = Struct.new :expression, :children do
|
@@ -18,60 +20,70 @@ class SeeingIsBelieving
|
|
18
20
|
self.should_debug = options.fetch :debug, false
|
19
21
|
self.generator = options.fetch :generator
|
20
22
|
self.on_complete = options.fetch :on_complete
|
21
|
-
|
22
|
-
self.list = []
|
23
|
+
@line_number = 0
|
23
24
|
end
|
24
25
|
|
25
26
|
def call
|
27
|
+
expressions = []
|
26
28
|
expression = nil
|
27
29
|
begin
|
28
|
-
generate
|
29
|
-
expression
|
30
|
-
|
30
|
+
pending_expression = generate
|
31
|
+
debug { "GENERATED: #{pending_expression.expression.inspect}, ADDING IT TO #{inspected_expressions expressions}" }
|
32
|
+
expressions << pending_expression
|
33
|
+
expression = reduce expressions
|
34
|
+
end until expressions.empty?
|
31
35
|
expression
|
32
36
|
end
|
33
37
|
|
34
38
|
private
|
35
39
|
|
36
|
-
attr_accessor :debug_stream, :should_debug, :generator, :on_complete, :
|
40
|
+
attr_accessor :debug_stream, :should_debug, :generator, :on_complete, :expressions
|
37
41
|
|
38
42
|
def generate
|
39
43
|
@line_number += 1
|
40
44
|
expression = generator.call
|
41
|
-
|
42
|
-
@list << PendingExpression.new(expression, [])
|
45
|
+
PendingExpression.new(expression, [])
|
43
46
|
end
|
44
47
|
|
45
|
-
def
|
46
|
-
"[#{
|
48
|
+
def inspected_expressions(expressions)
|
49
|
+
"[#{expressions.map { |pe| pe.inspect debug? }.join(', ')}]"
|
47
50
|
end
|
48
51
|
|
49
52
|
def debug?
|
50
53
|
@should_debug
|
51
54
|
end
|
52
55
|
|
53
|
-
def debug
|
54
|
-
@debug_stream.puts
|
56
|
+
def debug
|
57
|
+
@debug_stream.puts yield if debug?
|
55
58
|
end
|
56
59
|
|
57
|
-
def
|
58
|
-
|
59
|
-
# uhm, should this expression we are checking for validity consider the children?
|
60
|
-
|
60
|
+
def reduce(expressions)
|
61
|
+
expressions.size.times do |i|
|
62
|
+
expression = expressions[i..-1].map(&:expression) # uhm, should this expression we are checking for validity consider the children?
|
63
|
+
.join("\n") # must use newline otherwise can get expressions like `a\\+b` that should be `a\\\n+b`, former is invalid
|
64
|
+
return if children_will_never_be_valid? expression
|
61
65
|
next unless valid_ruby? expression
|
62
|
-
result = on_complete.call(
|
63
|
-
|
64
|
-
|
66
|
+
result = on_complete.call(expressions[i].expression,
|
67
|
+
expressions[i].children,
|
68
|
+
expressions[i+1..-1].map { |pe| [pe.expression, pe.children] }.flatten, # hmmm, not sure this is really correct, but it allows it to work for my use cases
|
65
69
|
@line_number)
|
66
|
-
|
67
|
-
|
68
|
-
debug
|
70
|
+
expressions.replace expressions[0, i]
|
71
|
+
expressions[i-1].children << result unless expressions.empty?
|
72
|
+
debug { "REDUCED: #{result.inspect}, LIST: #{inspected_expressions expressions}" }
|
69
73
|
return result
|
70
74
|
end
|
71
75
|
end
|
72
76
|
|
73
77
|
def valid_ruby?(expression)
|
74
|
-
SyntaxAnalyzer.valid_ruby? expression
|
78
|
+
valid = SyntaxAnalyzer.valid_ruby? expression
|
79
|
+
debug { "#{valid ? "\e[31mIS NOT VALID:" : "\e[32mIS VALID:"}: #{expression.inspect}\e[0m" }
|
80
|
+
valid
|
81
|
+
end
|
82
|
+
|
83
|
+
def children_will_never_be_valid?(expression)
|
84
|
+
analyzer = SyntaxAnalyzer.new(expression)
|
85
|
+
analyzer.parse
|
86
|
+
analyzer.unclosed_string? || analyzer.unclosed_regexp?
|
75
87
|
end
|
76
88
|
end
|
77
89
|
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
class SeeingIsBelieving
|
2
|
+
class HardCoreEnsure
|
3
|
+
def self.call(options)
|
4
|
+
new(options).call
|
5
|
+
end
|
6
|
+
|
7
|
+
def initialize(options)
|
8
|
+
self.options = options
|
9
|
+
validate_options
|
10
|
+
end
|
11
|
+
|
12
|
+
def call
|
13
|
+
trap_sigint
|
14
|
+
invoke_code
|
15
|
+
ensure
|
16
|
+
invoke_ensure
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
attr_accessor :options, :ensure_invoked, :old_handler
|
22
|
+
|
23
|
+
def trap_sigint
|
24
|
+
self.old_handler = trap 'INT' do
|
25
|
+
invoke_ensure
|
26
|
+
Process.kill 'INT', $$
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def invoke_code
|
31
|
+
options[:code].call
|
32
|
+
end
|
33
|
+
|
34
|
+
def invoke_ensure
|
35
|
+
return if ensure_invoked
|
36
|
+
trap 'INT', old_handler
|
37
|
+
self.ensure_invoked = true
|
38
|
+
options[:ensure].call
|
39
|
+
end
|
40
|
+
|
41
|
+
def validate_options
|
42
|
+
raise ArgumentError, "Must pass the :code key" unless options.key? :code
|
43
|
+
raise ArgumentError, "Must pass the :ensure key" unless options.key? :ensure
|
44
|
+
unknown_keys = options.keys - [:code, :ensure]
|
45
|
+
if options.size == 3
|
46
|
+
raise ArgumentError, "Unknown key: #{unknown_keys.first.inspect}"
|
47
|
+
elsif options.size > 3
|
48
|
+
raise ArgumentError, "Unknown keys: #{unknown_keys.map(&:inspect).join(', ')}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|