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.
@@ -1,50 +1,56 @@
1
- class SeeingIsBelieving
1
+ require 'seeing_is_believing/has_exception'
2
+ require 'seeing_is_believing/tracks_line_numbers_seen'
2
3
 
4
+ class SeeingIsBelieving
3
5
  class Result
4
- module HasException
5
- attr_accessor :exception
6
- alias has_exception? exception
6
+
7
+ Line = Class.new(Array) { include HasException }
8
+
9
+ include HasException
10
+ include TracksLineNumbersSeen
11
+
12
+ attr_accessor :stdout, :stderr
13
+
14
+ def has_stdout?
15
+ stdout && !stdout.empty?
7
16
  end
8
17
 
9
- attr_reader :min_line_number, :max_line_number
18
+ def has_stderr?
19
+ stderr && !stderr.empty?
20
+ end
10
21
 
11
22
  def initialize
12
23
  @min_line_number = @max_line_number = 1
13
24
  end
14
25
 
15
26
  def record_result(line_number, value)
16
- contains_line_number line_number
17
- results[line_number] << value.inspect
27
+ track_line_number line_number
28
+ results(line_number) << value.inspect
18
29
  value
19
30
  end
20
31
 
21
32
  def record_exception(line_number, exception)
22
- contains_line_number line_number
23
- results[line_number].exception = exception
33
+ self.exception = exception
34
+ track_line_number line_number
35
+ results(line_number).exception = exception
24
36
  end
25
37
 
26
38
  def [](line_number)
27
- results[line_number]
39
+ results(line_number)
28
40
  end
29
41
 
30
- # probably not really useful, just exists to satisfy the tests
42
+ # probably not really useful, just exists to satisfy the tests, which specified too simple of an interface
31
43
  def to_a
32
44
  (min_line_number..max_line_number).map do |line_number|
33
45
  [line_number, [*self[line_number], *Array(self[line_number].exception)]]
34
46
  end
35
47
  end
36
48
 
37
- def contains_line_number(line_number)
38
- @min_line_number = line_number if line_number < @min_line_number
39
- @max_line_number = line_number if line_number > @max_line_number
40
- end
41
-
42
49
  private
43
50
 
44
- def results
45
- @results ||= Hash.new do |hash, line_number|
46
- hash[line_number] = Array.new.extend HasException
47
- end
51
+ def results(line_number)
52
+ @results ||= Hash.new
53
+ @results[line_number] ||= Line.new
48
54
  end
49
55
  end
50
56
  end
@@ -1,49 +1,131 @@
1
1
  require 'ripper'
2
2
 
3
- class SyntaxAnalyzer < Ripper::SexpBuilder
4
-
5
- # I don't actually know if all of the error methods should invoke has_error!
6
- # or just parse errors. I don't actually know how to produce the other errors O.o
7
- #
8
- # Here is what it is defining as of ruby-1.9.3-p125:
9
- # on_alias_error
10
- # on_assign_error
11
- # on_class_name_error
12
- # on_param_error
13
- # on_parse_error
14
- instance_methods.grep(/error/i).each do |error_meth|
15
- class_eval "
16
- def #{error_meth}(*)
3
+ class SeeingIsBelieving
4
+ class SyntaxAnalyzer < Ripper::SexpBuilder
5
+
6
+ # HELPERS
7
+
8
+ def self.parsed(code)
9
+ instance = new code
10
+ instance.parse
11
+ instance
12
+ end
13
+
14
+ # We have to do this b/c Ripper sometimes calls on_tstring_end even when the string doesn't get ended
15
+ # e.g. SyntaxAnalyzer.new('"a').parse
16
+ def ends_match?(beginning, ending)
17
+ return false unless beginning && ending
18
+ return beginning == ending if beginning.size == 1
19
+ case beginning[-1]
20
+ when '<' then '>' == ending
21
+ when '(' then ')' == ending
22
+ when '[' then ']' == ending
23
+ when '{' then '}' == ending
24
+ else
25
+ beginning[-1] == ending
26
+ end
27
+ end
28
+
29
+ # SYNTACTIC VALIDITY
30
+
31
+ # I don't actually know if all of the error methods should set @has_error
32
+ # or just parse errors. I don't actually know how to produce the other errors O.o
33
+ #
34
+ # Here is what it is defining as of ruby-1.9.3-p125:
35
+ # on_alias_error
36
+ # on_assign_error
37
+ # on_class_name_error
38
+ # on_param_error
39
+ # on_parse_error
40
+ instance_methods.grep(/error/i).each do |error_meth|
41
+ super_meth = instance_method error_meth
42
+ define_method error_meth do |*args, &block|
17
43
  @has_error = true
18
- super
44
+ super_meth.bind(self).call(*args, &block)
19
45
  end
20
- "
21
- end
46
+ end
22
47
 
23
- def on_comment(*)
24
- @has_comment = true
25
- super
26
- end
48
+ def self.valid_ruby?(code)
49
+ parsed(code).valid_ruby?
50
+ end
27
51
 
28
- def self.parsed(code)
29
- instance = new code
30
- instance.parse
31
- instance
32
- end
52
+ def valid_ruby?
53
+ !invalid_ruby?
54
+ end
33
55
 
34
- def self.valid_ruby?(code)
35
- !parsed(code).has_error?
36
- end
56
+ def invalid_ruby?
57
+ @has_error || unclosed_string? || unclosed_regexp?
58
+ end
37
59
 
38
- def self.ends_in_comment?(code)
39
- parsed(code.lines.to_a.last.to_s).has_comment?
40
- end
60
+ # STRINGS
41
61
 
42
- def has_error?
43
- @has_error
44
- end
62
+ def self.unclosed_string?(code)
63
+ parsed(code).unclosed_string?
64
+ end
65
+
66
+ def string_opens
67
+ @string_opens ||= []
68
+ end
69
+
70
+ def on_tstring_beg(beginning)
71
+ string_opens.push beginning
72
+ super
73
+ end
74
+
75
+ def on_tstring_end(ending)
76
+ string_opens.pop if ends_match? string_opens.last, ending
77
+ super
78
+ end
79
+
80
+ def unclosed_string?
81
+ string_opens.any?
82
+ end
83
+
84
+ # REGEXPS
85
+
86
+ def self.unclosed_regexp?(code)
87
+ parsed(code).unclosed_regexp?
88
+ end
89
+
90
+ def regexp_opens
91
+ @regexp_opens ||= []
92
+ end
93
+
94
+ def on_regexp_beg(beginning)
95
+ regexp_opens.push beginning
96
+ super
97
+ end
98
+
99
+ def on_regexp_end(ending)
100
+ regexp_opens.pop if ends_match? regexp_opens.last, ending
101
+ super
102
+ end
103
+
104
+ def unclosed_regexp?
105
+ regexp_opens.any?
106
+ end
107
+
108
+ # COMMENTS
109
+
110
+ def self.ends_in_comment?(code)
111
+ parsed(code.lines.to_a.last.to_s).has_comment?
112
+ end
113
+
114
+ def has_comment?
115
+ @has_comment
116
+ end
117
+
118
+ def on_comment(*)
119
+ @has_comment = true
120
+ super
121
+ end
122
+
123
+ # RETURNS
45
124
 
46
- def has_comment?
47
- @has_comment
125
+ # this is conspicuosuly inferior, but I can't figure out how to actually parse it
126
+ # see: http://www.ruby-forum.com/topic/4409633
127
+ def self.will_return?(code)
128
+ /(^|\s)return.*?\Z$/ =~ code
129
+ end
48
130
  end
49
131
  end
@@ -0,0 +1,19 @@
1
+ # WARNING: DO NOT REQUIRE THIS FILE, IT WILL FUCK YOU UP!!!!!!
2
+
3
+
4
+ require 'stringio'
5
+ real_stdout = STDOUT
6
+ real_stderr = STDERR
7
+ STDOUT = $stdout = fake_stdout = StringIO.new
8
+ STDERR = $stderr = fake_stderr = StringIO.new
9
+
10
+ require 'yaml'
11
+ require 'seeing_is_believing/result'
12
+ $seeing_is_believing_current_result = SeeingIsBelieving::Result.new
13
+
14
+ at_exit do
15
+ $seeing_is_believing_current_result.stdout = fake_stdout.string
16
+ $seeing_is_believing_current_result.stderr = fake_stderr.string
17
+
18
+ real_stdout.write YAML.dump $seeing_is_believing_current_result
19
+ end
@@ -0,0 +1,18 @@
1
+ class SeeingIsBelieving
2
+ module TracksLineNumbersSeen
3
+ INITIAL_LINE_NUMBER = 1 # uhm, should this change to 0?
4
+
5
+ def track_line_number(line_number)
6
+ @min_line_number = line_number if line_number < min_line_number
7
+ @max_line_number = line_number if line_number > max_line_number
8
+ end
9
+
10
+ def min_line_number
11
+ @min_line_number || INITIAL_LINE_NUMBER
12
+ end
13
+
14
+ def max_line_number
15
+ @max_line_number || INITIAL_LINE_NUMBER
16
+ end
17
+ end
18
+ end
@@ -1,3 +1,3 @@
1
1
  class SeeingIsBelieving
2
- VERSION = '0.0.3'
2
+ VERSION = '0.0.4'
3
3
  end
@@ -1,22 +1,28 @@
1
1
  require 'stringio'
2
+ require 'tmpdir'
2
3
 
3
4
  require 'seeing_is_believing/result'
4
5
  require 'seeing_is_believing/expression_list'
6
+ require 'seeing_is_believing/evaluate_by_moving_files'
5
7
 
6
8
  # might not work on windows b/c of assumptions about line ends
7
9
  class SeeingIsBelieving
8
- def initialize(string_or_stream)
9
- @stream = to_stream string_or_stream
10
- @result = Result.new
10
+ include TracksLineNumbersSeen
11
+ BLANK_REGEX = /\A\s*\Z/
12
+
13
+ def initialize(string_or_stream, options={})
14
+ @string = string_or_stream
15
+ @stream = to_stream string_or_stream
16
+ @filename = options[:filename]
11
17
  end
12
18
 
13
19
  def call
14
20
  @memoized_result ||= begin
15
21
  program = ''
16
- program << expression_list.call until stream.eof?
17
- $seeing_is_believing_current_result = @result # can we make this a threadlocal var on the class?
18
- TOPLEVEL_BINDING.eval record_exceptions_in(program), 'program.rb', 1
19
- @result
22
+ program << expression_list.call until eof? || data_segment?
23
+ program = record_exceptions_in program
24
+ program << "\n" << the_rest_of_the_stream if data_segment?
25
+ result_for program, min_line_number, max_line_number
20
26
  end
21
27
  end
22
28
 
@@ -25,11 +31,11 @@ class SeeingIsBelieving
25
31
  attr_reader :stream
26
32
 
27
33
  def expression_list
28
- @expression_list ||= ExpressionList.new generator: lambda { stream.gets.chomp },
34
+ @expression_list ||= ExpressionList.new generator: lambda { get_next_line },
29
35
  on_complete: lambda { |line, children, completions, line_number|
30
- @result.contains_line_number line_number
31
- expression = [line, *children, *completions].join("\n")
32
- if expression == '' || ends_in_comment?(expression)
36
+ track_line_number line_number
37
+ expression = [line, *children, *completions].map(&:chomp).join("\n")
38
+ if expression =~ BLANK_REGEX || SyntaxAnalyzer.ends_in_comment?(expression) || SyntaxAnalyzer.will_return?(expression)
33
39
  expression + "\n"
34
40
  else
35
41
  record_yahself(expression, line_number) + "\n"
@@ -56,7 +62,42 @@ class SeeingIsBelieving
56
62
  "end"
57
63
  end
58
64
 
59
- def ends_in_comment?(expression)
60
- SyntaxAnalyzer.ends_in_comment? expression
65
+ def result_for(program, min_line_number, max_line_number)
66
+ Dir.mktmpdir "seeing_is_believing_temp_dir" do |dir|
67
+ filename = @filename || File.join(dir, 'program.rb')
68
+ EvaluateByMovingFiles.new(program, filename).call.tap do |result|
69
+ result.track_line_number min_line_number
70
+ result.track_line_number max_line_number
71
+ end
72
+ end
73
+ end
74
+
75
+ def eof?
76
+ peek_next_line.nil?
77
+ end
78
+
79
+ def data_segment?
80
+ peek_next_line == '__END__'
81
+ end
82
+
83
+ def peek_next_line
84
+ @next_line ||= begin
85
+ line = stream.gets
86
+ line && line.chomp
87
+ end
88
+ end
89
+
90
+ def get_next_line
91
+ if @next_line
92
+ line = peek_next_line
93
+ @next_line = nil
94
+ line
95
+ else
96
+ peek_next_line && get_next_line
97
+ end
98
+ end
99
+
100
+ def the_rest_of_the_stream
101
+ get_next_line << "\n" << stream.read
61
102
  end
62
103
  end
@@ -20,4 +20,5 @@ Gem::Specification.new do |s|
20
20
 
21
21
  s.add_development_dependency "rspec", "~> 2.12.0"
22
22
  s.add_development_dependency "cucumber", "~> 1.2.1"
23
+ s.add_development_dependency "ichannel", "~> 5.1.1"
23
24
  end
@@ -0,0 +1,68 @@
1
+ require 'seeing_is_believing/evaluate_by_moving_files'
2
+
3
+ describe SeeingIsBelieving::EvaluateByMovingFiles do
4
+ let(:filedir) { File.expand_path '../../proving_grounds', __FILE__ }
5
+ let(:filename) { File.join filedir, 'some_filename' }
6
+
7
+ def invoke(program)
8
+ evaluator = described_class.new(program, filename)
9
+ FileUtils.rm_f evaluator.temp_filename
10
+ evaluator.call
11
+ end
12
+
13
+ it 'evaluates the code when the file DNE' do
14
+ FileUtils.rm_f filename
15
+ invoke('print 1').stdout.should == '1'
16
+ end
17
+
18
+ it 'evaluates the code when the file Exists' do
19
+ FileUtils.touch filename
20
+ invoke('print 1').stdout.should == '1'
21
+ end
22
+
23
+ it 'raises an error when the temp file already exists' do
24
+ evaluator = described_class.new('', filename)
25
+ FileUtils.touch evaluator.temp_filename
26
+ expect { evaluator.call }.to raise_error SeeingIsBelieving::TempFileAlreadyExists
27
+ end
28
+
29
+ it 'evaluates the code as the given file' do
30
+ invoke('print __FILE__').stdout.should == filename
31
+ end
32
+
33
+ it 'does not change the original file' do
34
+ File.open(filename, 'w') { |f| f.write "ORIGINAL" }
35
+ invoke '1 + 1'
36
+ File.read(filename).should == "ORIGINAL"
37
+ end
38
+
39
+ it 'uses HardCoreEnsure to move the file back' do
40
+ evaluator = described_class.new 'PROGRAM', filename, error_stream: StringIO.new
41
+ File.open(filename, 'w') { |f| f.write 'ORIGINAL' }
42
+ FileUtils.rm_rf evaluator.temp_filename
43
+ SeeingIsBelieving::HardCoreEnsure.should_receive(:call) do |options|
44
+ # initial state
45
+ File.exist?(evaluator.temp_filename).should == false
46
+ File.read(filename).should == 'ORIGINAL'
47
+
48
+ # after code
49
+ options[:code].call rescue nil
50
+ File.read(evaluator.temp_filename).should == 'ORIGINAL'
51
+ File.read(filename).should == 'PROGRAM'
52
+
53
+ # after ensure
54
+ options[:ensure].call
55
+ File.read(filename).should == 'ORIGINAL'
56
+ File.exist?(evaluator.temp_filename).should == false
57
+ end
58
+ evaluator.call
59
+ end
60
+
61
+ it 'prints some error handling code to stderr if it fails' do
62
+ stderr = StringIO.new
63
+ evaluator = described_class.new 'raise "omg"', filename, error_stream: stderr
64
+ FileUtils.rm_f evaluator.temp_filename
65
+ expect { evaluator.call }.to raise_error
66
+ stderr.string.should include "It blew up"
67
+ end
68
+ end
@@ -105,11 +105,29 @@ describe SeeingIsBelieving::ExpressionList do
105
105
  "end end"\
106
106
  end
107
107
 
108
- example "example; smoke test debug option" do
109
- stream = StringIO.new
110
- result = call %w[a+ b], debug: true, debug_stream: stream do |line, children, completions, line_number|
111
- [line, *children, *completions].join("\n")
108
+ example 'example: multiline strings with valid code in them' do
109
+ block_invocations = 0
110
+ call ["'", "1", "'"] do |*expressions, line_number|
111
+ expressions.join('').should == "'1'"
112
+ line_number.should == 3
113
+ block_invocations += 1
114
+ end
115
+ block_invocations.should == 1
116
+ end
117
+
118
+ example 'example: multiline regexps with valid code in them' do
119
+ block_invocations = 0
120
+ call ['/', '1', '/'] do |*expressions, line_number|
121
+ expressions.join('').should == "/1/"
122
+ line_number.should == 3
123
+ block_invocations += 1
112
124
  end
125
+ block_invocations.should == 1
126
+ end
127
+
128
+ example "example: smoke test debug option" do
129
+ stream = StringIO.new
130
+ call(%w[a+ b], debug: true, debug_stream: stream) { |*expressions, _| expressions.join("\n") }
113
131
  stream.string.should include "GENERATED"
114
132
  stream.string.should include "REDUCED"
115
133
  end
@@ -0,0 +1,73 @@
1
+ require 'ichannel'
2
+ require 'seeing_is_believing/hard_core_ensure'
3
+
4
+ describe SeeingIsBelieving::HardCoreEnsure do
5
+ def call(options)
6
+ described_class.new(options).call
7
+ end
8
+
9
+ it "raises an argument error if it doesn't get a code proc" do
10
+ expect { call ensure: -> {} }.to raise_error ArgumentError, "Must pass the :code key"
11
+ end
12
+
13
+ it "raises an argument error if it doesn't get an ensure proc" do
14
+ expect { call code: -> {} }.to raise_error ArgumentError, "Must pass the :ensure key"
15
+ end
16
+
17
+ it "raises an argument error if it gets any other keys" do
18
+ expect { call code: -> {}, ensure: -> {}, other: 123 }.to \
19
+ raise_error ArgumentError, "Unknown key: :other"
20
+
21
+ expect { call code: -> {}, ensure: -> {}, other1: 123, other2: 456 }.to \
22
+ raise_error ArgumentError, "Unknown keys: :other1, :other2"
23
+ end
24
+
25
+ it 'invokes the code and returns the value' do
26
+ call(code: -> { :result }, ensure: -> {}).should == :result
27
+ end
28
+
29
+ it 'invokes the ensure after the code' do
30
+ seen = []
31
+ call code: -> { seen << :code }, ensure: -> { seen << :ensure }
32
+ seen.should == [:code, :ensure]
33
+ end
34
+
35
+ it 'invokes the ensure even if an exception is raised' do
36
+ ensure_invoked = false
37
+ expect do
38
+ call code: -> { raise Exception, 'omg!' }, ensure: -> { ensure_invoked = true }
39
+ end.to raise_error Exception, 'omg!'
40
+ ensure_invoked.should == true
41
+ end
42
+
43
+ it 'invokes the code even if an interrupt is sent and there is a default handler' do
44
+ channel = IChannel.new Marshal
45
+ pid = fork do
46
+ old_handler = trap('INT') { channel.put "old handler invoked" }
47
+ call code: -> { sleep 0.1 }, ensure: -> { channel.put "ensure invoked" }
48
+ trap 'INT', old_handler
49
+ end
50
+ sleep 0.05
51
+ Process.kill 'INT', pid
52
+ Process.wait pid
53
+ channel.get.should == "ensure invoked"
54
+ channel.get.should == "old handler invoked"
55
+ channel.should_not be_readable
56
+ end
57
+
58
+ it 'invokes the code even if an interrupt is sent and interrupts are set to ignore' do
59
+ channel = IChannel.new Marshal
60
+ pid = fork do
61
+ old_handler = trap 'INT', 'IGNORE'
62
+ result = call code: -> { sleep 0.1; 'code result' }, ensure: -> { channel.put "ensure invoked" }
63
+ channel.put result
64
+ trap 'INT', old_handler
65
+ end
66
+ sleep 0.05
67
+ Process.kill 'INT', pid
68
+ Process.wait pid
69
+ channel.get.should == "ensure invoked"
70
+ channel.get.should == 'code result'
71
+ channel.should_not be_readable
72
+ end
73
+ end