otaku 0.2.2 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/HISTORY.txt +8 -0
- data/README.rdoc +18 -5
- data/VERSION +1 -1
- data/examples/unittest/client.rb +17 -0
- data/examples/unittest/server.rb +56 -0
- data/examples/unittest/server2.rb +11 -0
- data/examples/unittest/tests/a_test.rb +9 -0
- data/examples/unittest/tests/b_test.rb +9 -0
- data/lib/otaku.rb +1 -0
- data/lib/otaku/handler.rb +16 -108
- data/lib/otaku/handler/context.rb +43 -0
- data/lib/otaku/handler/magic_proc.rb +116 -0
- data/lib/otaku/handler/processor.rb +78 -0
- data/otaku.gemspec +11 -3
- data/spec/handler_spec.rb +905 -23
- data/spec/integration_spec.rb +6 -1
- metadata +17 -4
data/HISTORY.txt
CHANGED
data/README.rdoc
CHANGED
@@ -4,7 +4,11 @@ Dead simple server/client service built using eventmachine.
|
|
4
4
|
|
5
5
|
== Introduction
|
6
6
|
|
7
|
-
Otaku's original intent is to support testing of cross-process stubbing in
|
7
|
+
Otaku's original intent is to support testing of cross-process stubbing in
|
8
|
+
cross-stub (http://github.com/ngty/cross-stub). It's usefulness in other
|
9
|
+
aspects of my hacking life prompts me to extract it out, & package it as a
|
10
|
+
generic solution. Its primary intent is to be dead simple to use & easy to
|
11
|
+
customize, of course, both criteria subjected to very my own tastes.
|
8
12
|
|
9
13
|
== Getting Started
|
10
14
|
|
@@ -31,7 +35,8 @@ It's hosted on rubygems.org:
|
|
31
35
|
|
32
36
|
=== Unfortunately ...
|
33
37
|
|
34
|
-
Most of the times, we won't have anything as simple as above, the following
|
38
|
+
Most of the times, we won't have anything as simple as above, the following
|
39
|
+
illustrates the problem of contextual reference:
|
35
40
|
|
36
41
|
mark = '*'
|
37
42
|
Otaku.start do |data|
|
@@ -40,7 +45,9 @@ Most of the times, we won't have anything as simple as above, the following illu
|
|
40
45
|
|
41
46
|
Otaku.process('hello') # failure !!
|
42
47
|
|
43
|
-
The reason is that the proc that we passed to Otaku.start is being marshalled
|
48
|
+
The reason is that the proc that we passed to Otaku.start is being marshalled
|
49
|
+
while being passed to the server as a handler, in the process, the contextual
|
50
|
+
references are lost. A workaround for this problem is:
|
44
51
|
|
45
52
|
Otaku.start(:mark => '*') do |data|
|
46
53
|
'%s %s %s' % [mark, data, mark]
|
@@ -48,6 +55,11 @@ The reason is that the proc that we passed to Otaku.start is being marshalled wh
|
|
48
55
|
|
49
56
|
Otaku.process('hello') # >> '* hello *'
|
50
57
|
|
58
|
+
Anything that can be marshalled can be passed as a context. The exception is
|
59
|
+
proc, which cannot be marshalled, yet it can still be passed in. However, in
|
60
|
+
the process, all contextual references within the proc are lost, so u may
|
61
|
+
want to exercise care when playing with proc.
|
62
|
+
|
51
63
|
== Configuraing It
|
52
64
|
|
53
65
|
Otaku ships with the following defaults:
|
@@ -88,8 +100,9 @@ Configuring can be done via:
|
|
88
100
|
* Make your feature addition or bug fix.
|
89
101
|
* Add tests for it. This is important so I don't break it in a
|
90
102
|
future version unintentionally.
|
91
|
-
* Commit, do not mess with rakefile, version, or history.
|
92
|
-
|
103
|
+
* Commit, do not mess with rakefile, version, or history. (if you want to have
|
104
|
+
your own version, that is fine but bump version in a commit by itself I can
|
105
|
+
ignore when I pull)
|
93
106
|
* Send me a pull request. Bonus points for topic branches.
|
94
107
|
|
95
108
|
== Copyright
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.3.0
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'lib', 'otaku')
|
2
|
+
|
3
|
+
Otaku.port = 10901
|
4
|
+
|
5
|
+
Otaku.start do |test_file|
|
6
|
+
unless $o_stdout
|
7
|
+
$o_stdout, $o_stderr = $stdout, $stderr
|
8
|
+
$stdout, $stderr = StringIO.new, StringIO.new
|
9
|
+
end
|
10
|
+
MiniTest::Unit::TestCase.reset
|
11
|
+
require File.join(File.dirname(__FILE__), test_file)
|
12
|
+
MiniTest::Unit.new.run
|
13
|
+
$stdout.rewind
|
14
|
+
$stdout.lines
|
15
|
+
end
|
16
|
+
|
17
|
+
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'pp'
|
3
|
+
require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'otaku')
|
4
|
+
|
5
|
+
Otaku.port = 10901
|
6
|
+
|
7
|
+
Otaku.start do |test_file|
|
8
|
+
|
9
|
+
unless $o_stdout
|
10
|
+
$o_stdout, $o_stderr = $stdout, $stderr
|
11
|
+
$stdout, $stderr = StringIO.new, StringIO.new
|
12
|
+
end
|
13
|
+
|
14
|
+
require 'rubygems'
|
15
|
+
require 'minitest/unit'
|
16
|
+
MiniTest::Unit::TestCase.reset
|
17
|
+
require test_file
|
18
|
+
MiniTest::Unit.new.run
|
19
|
+
|
20
|
+
results = ($stdout.rewind; $stdout).lines.inject({}) do |memo, line|
|
21
|
+
case line
|
22
|
+
when /(\d+) assertions, (\d+) failures, (\d+) errors, (\d+) skips/
|
23
|
+
a, f, e, s = [$1, $2, $3, $4].map(&:to_i)
|
24
|
+
memo[:stats] = {:assertions => a, :failures => f, :errors => e, :skips => s})
|
25
|
+
when /Finished in ([\d\.]+) seconds/
|
26
|
+
memo[:time_taken] = "#{$1} seconds"
|
27
|
+
else
|
28
|
+
(memo[:buffer] ||= []) << line
|
29
|
+
end
|
30
|
+
memo
|
31
|
+
end
|
32
|
+
|
33
|
+
unless [:errors, :failures].all?{|type| results[:stats][type].zero? }
|
34
|
+
results[:errors] = (results.delete(:buffer) || []).inject([]) do |errors, line|
|
35
|
+
latest = errors[-1]
|
36
|
+
if title = line[/^ (\d+\) (Error|Failure)\:)/,1]
|
37
|
+
errors << [title]
|
38
|
+
elsif latest && [1,2].include?(latest.size)
|
39
|
+
latest << line.strip
|
40
|
+
elsif latest && latest.size == 3 && line.include?(test_file)
|
41
|
+
latest << [test_file, line[/\:(\d+)\:/,1]].join(':')
|
42
|
+
end
|
43
|
+
errors
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
results
|
48
|
+
end
|
49
|
+
|
50
|
+
Dir.glob(File.join(File.expand_path(File.dirname(__FILE__)), 'tests', '*.rb')) do |file|
|
51
|
+
pp Otaku.process(file)
|
52
|
+
end
|
53
|
+
|
54
|
+
at_exit do
|
55
|
+
Otaku.stop
|
56
|
+
end
|
data/lib/otaku.rb
CHANGED
data/lib/otaku/handler.rb
CHANGED
@@ -1,124 +1,32 @@
|
|
1
|
+
require 'otaku/handler/magic_proc'
|
2
|
+
require 'otaku/handler/context'
|
3
|
+
require 'otaku/handler/processor'
|
4
|
+
|
1
5
|
module Otaku
|
2
6
|
class Handler
|
3
7
|
|
8
|
+
attr_reader :context, :processor
|
9
|
+
|
4
10
|
def initialize(context, handler)
|
5
|
-
@context =
|
6
|
-
|
7
|
-
@proc, @file, @line = [:code, :file, :line].map{|meth| magic_proc.send(meth) }
|
11
|
+
@context = Context.new(context)
|
12
|
+
@processor = Processor.new(handler)
|
8
13
|
end
|
9
14
|
|
10
15
|
def process(data)
|
11
|
-
|
12
|
-
instance_exec(data, &(block = eval(@proc, nil, @file, @line)))
|
16
|
+
@context.eval!.instance_exec(data, &@processor.eval!)
|
13
17
|
end
|
14
18
|
|
15
19
|
def root
|
16
|
-
File.dirname(@file)
|
20
|
+
File.dirname(@processor.file)
|
17
21
|
end
|
18
22
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
'Class.new{ %s }.new' %
|
23
|
-
methods_hash.map do |method, val|
|
24
|
-
"def #{method}; Encoder.decode(%|#{Encoder.encode(val).gsub('|','\|')}|); end"
|
25
|
-
end.sort.join('; ')
|
26
|
-
end
|
27
|
-
|
28
|
-
class MagicProc
|
29
|
-
|
30
|
-
RUBY_PARSER = RubyParser.new
|
31
|
-
RUBY_2_RUBY = Ruby2Ruby.new
|
32
|
-
|
33
|
-
attr_reader :file, :line, :code
|
34
|
-
|
35
|
-
def initialize(block)
|
36
|
-
@block = block
|
37
|
-
extract_file_and_line_and_code
|
38
|
-
end
|
39
|
-
|
40
|
-
private
|
41
|
-
|
42
|
-
def extract_file_and_line_and_code
|
43
|
-
code, remaining = code_fragments
|
44
|
-
|
45
|
-
while frag = remaining[frag_regexp,1]
|
46
|
-
begin
|
47
|
-
sexp = RUBY_PARSER.parse(replace_magic_vars(code += frag))
|
48
|
-
if sexp.inspect =~ sexp_regexp
|
49
|
-
@code = revert_magic_vars(RUBY_2_RUBY.process(sexp)).sub('proc {','lambda {')
|
50
|
-
break
|
51
|
-
end
|
52
|
-
rescue SyntaxError, Racc::ParseError, NoMethodError
|
53
|
-
remaining.sub!(frag,'')
|
54
|
-
end
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
def code_fragments
|
59
|
-
ignore, start_marker, arg =
|
60
|
-
[:ignore, :start_marker, :arg].map{|key| code_match_args[key] }
|
61
|
-
[
|
62
|
-
"proc #{start_marker} |#{arg}| ",
|
63
|
-
source_code.sub(ignore, '')
|
64
|
-
]
|
65
|
-
end
|
66
|
-
|
67
|
-
def sexp_regexp
|
68
|
-
@sexp_regexp ||= (
|
69
|
-
Regexp.new([
|
70
|
-
Regexp.quote("s(:iter, s(:call, nil, :"),
|
71
|
-
"(proc|lambda)",
|
72
|
-
Regexp.quote(", s(:arglist)), s(:lasgn, :#{code_match_args[:arg]}), s("),
|
73
|
-
].join)
|
74
|
-
)
|
75
|
-
end
|
76
|
-
|
77
|
-
def frag_regexp
|
78
|
-
@frag_regexp ||= (
|
79
|
-
end_marker = {'do' => 'end', '{' => '\}'}[code_match_args[:start_marker]]
|
80
|
-
/^(.*?\W#{end_marker})/m
|
81
|
-
)
|
82
|
-
end
|
83
|
-
|
84
|
-
def code_regexp
|
85
|
-
@code_regexp ||=
|
86
|
-
/^(.*?(Otaku\.start.*?|lambda|proc|Proc\.new)\s*(do|\{)\s*\|(\w+)\|\s*)/m
|
87
|
-
end
|
88
|
-
|
89
|
-
def code_match_args
|
90
|
-
@code_match_args ||= (
|
91
|
-
args = source_code.match(code_regexp)
|
92
|
-
{
|
93
|
-
:ignore => args[1],
|
94
|
-
:start_marker => args[3],
|
95
|
-
:arg => args[4]
|
96
|
-
}
|
97
|
-
)
|
98
|
-
end
|
99
|
-
|
100
|
-
def source_code
|
101
|
-
@source_code ||= (
|
102
|
-
file, line = /^#<Proc:0x[0-9A-Fa-f]+@(.+):(\d+).*?>$/.match(@block.inspect)[1..2]
|
103
|
-
@file = File.expand_path(file)
|
104
|
-
@line = line.to_i
|
105
|
-
File.readlines(@file)[@line.pred .. -1].join
|
106
|
-
)
|
107
|
-
end
|
108
|
-
|
109
|
-
def replace_magic_vars(code)
|
110
|
-
%w{__FILE__ __LINE__}.inject(code) do |code, var|
|
111
|
-
code.gsub(var, "%|((#{var}))|")
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
def revert_magic_vars(code)
|
116
|
-
%w{__FILE__ __LINE__}.inject(code) do |code, var|
|
117
|
-
code.gsub(%|"((#{var}))"|, var)
|
118
|
-
end
|
119
|
-
end
|
23
|
+
def marshal_dump
|
24
|
+
[@context, @processor]
|
25
|
+
end
|
120
26
|
|
121
|
-
|
27
|
+
def marshal_load(data)
|
28
|
+
@context, @processor = data
|
29
|
+
end
|
122
30
|
|
123
31
|
end
|
124
32
|
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Otaku
|
2
|
+
class Handler
|
3
|
+
class Context #:nodoc:
|
4
|
+
|
5
|
+
attr_reader :code
|
6
|
+
|
7
|
+
def initialize(methods_hash)
|
8
|
+
@code = 'Class.new{ %s }.new' %
|
9
|
+
methods_hash.map do |method, val|
|
10
|
+
'def %s; %s; end' % [method, method_body(val)]
|
11
|
+
end.sort.join('; ')
|
12
|
+
end
|
13
|
+
|
14
|
+
def eval!
|
15
|
+
eval(@code, nil, '(generated class)', 1)
|
16
|
+
end
|
17
|
+
|
18
|
+
def marshal_dump
|
19
|
+
[@code]
|
20
|
+
end
|
21
|
+
|
22
|
+
def marshal_load(data)
|
23
|
+
@code, _ = data
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def method_body(val)
|
29
|
+
if val.is_a?(Proc)
|
30
|
+
"Encoder.decode(%|#{Encoder.encode(magic_proc(val)).gsub('|','\|')}|).eval!"
|
31
|
+
else
|
32
|
+
"Encoder.decode(%|#{Encoder.encode(val).gsub('|','\|')}|)"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def magic_proc(block)
|
37
|
+
MagicProc.new(block)
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
@@ -0,0 +1,116 @@
|
|
1
|
+
module Otaku
|
2
|
+
class Handler
|
3
|
+
class MagicProc #:nodoc:
|
4
|
+
|
5
|
+
RUBY_PARSER = RubyParser.new
|
6
|
+
RUBY_2_RUBY = Ruby2Ruby.new
|
7
|
+
|
8
|
+
attr_reader :file, :line, :code
|
9
|
+
|
10
|
+
def initialize(block)
|
11
|
+
@cache = {:block => block}
|
12
|
+
extract_file_and_line_and_code
|
13
|
+
@cache = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
def eval!
|
17
|
+
eval(@code, nil, @file, @line)
|
18
|
+
end
|
19
|
+
|
20
|
+
def marshal_dump
|
21
|
+
[@code, @file, @line]
|
22
|
+
end
|
23
|
+
|
24
|
+
def marshal_load(data)
|
25
|
+
@code, @file, @line = data
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def extract_file_and_line_and_code
|
31
|
+
code, remaining = code_fragments
|
32
|
+
|
33
|
+
while frag = remaining[frag_regexp,1]
|
34
|
+
begin
|
35
|
+
sexp = RUBY_PARSER.parse(replace_magic_vars(code += frag))
|
36
|
+
if sexp.inspect =~ sexp_regexp
|
37
|
+
@code = revert_magic_vars(RUBY_2_RUBY.process(sexp)).sub('proc {','lambda {')
|
38
|
+
break
|
39
|
+
end
|
40
|
+
rescue SyntaxError, Racc::ParseError, NoMethodError
|
41
|
+
remaining.sub!(frag,'')
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def code_fragments
|
47
|
+
ignore, start_marker, arg =
|
48
|
+
[:ignore, :start_marker, :arg].map{|key| code_match_args[key] }
|
49
|
+
[
|
50
|
+
arg ? "proc #{start_marker} |#{arg}|" : "proc #{start_marker}",
|
51
|
+
source_code.sub(ignore, '')
|
52
|
+
]
|
53
|
+
end
|
54
|
+
|
55
|
+
def sexp_regexp
|
56
|
+
@cache[:sexp_regexp] ||= (
|
57
|
+
Regexp.new([
|
58
|
+
Regexp.quote("s(:iter, s(:call, nil, :"),
|
59
|
+
"(proc|lambda)",
|
60
|
+
Regexp.quote(", s(:arglist)), "),
|
61
|
+
'(%s|%s|%s)' % [
|
62
|
+
Regexp.quote('s(:masgn, s(:array, s('),
|
63
|
+
Regexp.quote('s(:lasgn, :'),
|
64
|
+
Regexp.quote('nil, s(')
|
65
|
+
]
|
66
|
+
].join)
|
67
|
+
)
|
68
|
+
end
|
69
|
+
|
70
|
+
def frag_regexp
|
71
|
+
@cache[:frag_regexp] ||= (
|
72
|
+
end_marker = {'do' => 'end', '{' => '\}'}[code_match_args[:start_marker]]
|
73
|
+
/^(.*?\W#{end_marker})/m
|
74
|
+
)
|
75
|
+
end
|
76
|
+
|
77
|
+
def code_regexp
|
78
|
+
@cache[:code_regexp] ||=
|
79
|
+
/^(.*?(lambda|proc|Proc\.new)?\s*(do|\{)\s*(\|(.*?)\|\s*)?)/m
|
80
|
+
end
|
81
|
+
|
82
|
+
def code_match_args
|
83
|
+
@cache[:code_match_args] ||= (
|
84
|
+
args = source_code.match(code_regexp)
|
85
|
+
{
|
86
|
+
:ignore => args[1],
|
87
|
+
:start_marker => args[3],
|
88
|
+
:arg => args[5]
|
89
|
+
}
|
90
|
+
)
|
91
|
+
end
|
92
|
+
|
93
|
+
def source_code
|
94
|
+
@cache[:source_code] ||= (
|
95
|
+
file, line = /^#<Proc:0x[0-9A-Fa-f]+@(.+):(\d+).*?>$/.match(@cache[:block].inspect)[1..2]
|
96
|
+
@file = File.expand_path(file)
|
97
|
+
@line = line.to_i
|
98
|
+
File.readlines(@file)[@line.pred .. -1].join
|
99
|
+
)
|
100
|
+
end
|
101
|
+
|
102
|
+
def replace_magic_vars(code)
|
103
|
+
%w{__FILE__ __LINE__}.inject(code) do |code, var|
|
104
|
+
code.gsub(var, "%|((#{var}))|")
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def revert_magic_vars(code)
|
109
|
+
%w{__FILE__ __LINE__}.inject(code) do |code, var|
|
110
|
+
code.gsub(%|"((#{var}))"|, var)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|