seeing_is_believing 0.0.3 → 0.0.4
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.
- 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
|