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.
@@ -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