gopher2000 0.1.0

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.
Files changed (47) hide show
  1. data/.gitignore +4 -0
  2. data/.rvmrc +1 -0
  3. data/Gemfile +27 -0
  4. data/LICENSE.txt +14 -0
  5. data/README.markdown +344 -0
  6. data/Rakefile +38 -0
  7. data/bin/gopher2000 +51 -0
  8. data/examples/default_route.rb +22 -0
  9. data/examples/nyan.rb +62 -0
  10. data/examples/simple.rb +147 -0
  11. data/examples/twitter.rb +61 -0
  12. data/examples/weather.rb +69 -0
  13. data/gopher2000.gemspec +35 -0
  14. data/lib/gopher2000/base.rb +552 -0
  15. data/lib/gopher2000/dispatcher.rb +81 -0
  16. data/lib/gopher2000/dsl.rb +128 -0
  17. data/lib/gopher2000/errors.rb +14 -0
  18. data/lib/gopher2000/handlers/base_handler.rb +18 -0
  19. data/lib/gopher2000/handlers/directory_handler.rb +125 -0
  20. data/lib/gopher2000/rendering/abstract_renderer.rb +10 -0
  21. data/lib/gopher2000/rendering/base.rb +174 -0
  22. data/lib/gopher2000/rendering/menu.rb +129 -0
  23. data/lib/gopher2000/rendering/text.rb +10 -0
  24. data/lib/gopher2000/request.rb +21 -0
  25. data/lib/gopher2000/response.rb +25 -0
  26. data/lib/gopher2000/server.rb +85 -0
  27. data/lib/gopher2000/version.rb +4 -0
  28. data/lib/gopher2000.rb +33 -0
  29. data/scripts/god.rb +8 -0
  30. data/spec/application_spec.rb +54 -0
  31. data/spec/dispatching_spec.rb +144 -0
  32. data/spec/dsl_spec.rb +116 -0
  33. data/spec/gopher_spec.rb +1 -0
  34. data/spec/handlers/directory_handler_spec.rb +116 -0
  35. data/spec/helpers_spec.rb +16 -0
  36. data/spec/rendering/base_spec.rb +59 -0
  37. data/spec/rendering/menu_spec.rb +109 -0
  38. data/spec/rendering_spec.rb +84 -0
  39. data/spec/request_spec.rb +30 -0
  40. data/spec/response_spec.rb +33 -0
  41. data/spec/routing_spec.rb +92 -0
  42. data/spec/sandbox/old/socks.txt +0 -0
  43. data/spec/sandbox/socks.txt +0 -0
  44. data/spec/server_spec.rb +127 -0
  45. data/spec/spec_helper.rb +52 -0
  46. data/specs.watchr +60 -0
  47. metadata +211 -0
@@ -0,0 +1,85 @@
1
+ module Gopher
2
+
3
+ #
4
+ # main class which will listen on a specified port, and pass requests to an Application class
5
+ #
6
+ class Server
7
+ attr_accessor :app
8
+
9
+ #
10
+ # constructor
11
+ # @param [Application] instance of Gopher::Application we want to run
12
+ #
13
+ def initialize(a)
14
+ @app = a
15
+ end
16
+
17
+ #
18
+ # @return [String] name of the host specified in our config
19
+ #
20
+ def host
21
+ @app.config[:host] ||= '0.0.0.0'
22
+ end
23
+
24
+ #
25
+ # @return [Integer] port specified in our config
26
+ #
27
+ def port
28
+ @app.config[:port] ||= 70
29
+ end
30
+
31
+ #
32
+ # main app loop. called via at_exit block defined in DSL
33
+ #
34
+ def run!
35
+ EventMachine::run do
36
+ Signal.trap("INT") {
37
+ puts "It's a trap!"
38
+ EventMachine.stop
39
+ }
40
+ Signal.trap("TERM") {
41
+ puts "It's a trap!"
42
+ EventMachine.stop
43
+ }
44
+
45
+ EventMachine.kqueue = true if EventMachine.kqueue?
46
+ EventMachine.epoll = true if EventMachine.epoll?
47
+
48
+
49
+ STDERR.puts "start server at #{host} #{port}"
50
+ if @app.non_blocking?
51
+ STDERR.puts "Not blocking on requests"
52
+ end
53
+
54
+
55
+ EventMachine::start_server(host, port, Gopher::Dispatcher) do |conn|
56
+ #
57
+ # check if we should reload any scripts before moving along
58
+ #
59
+ @app.reload_stale
60
+
61
+ #
62
+ # roughly matching sinatra's style of duping the app to respond
63
+ # to requests, @see http://www.sinatrarb.com/intro#Request/Instance%20Scope
64
+ #
65
+ # this essentially means we have 'one instance per request'
66
+ #
67
+ conn.app = @app.dup
68
+ end
69
+ end
70
+ end
71
+
72
+
73
+ #
74
+ # don't try and parse arguments if someone already has done that
75
+ #
76
+ if ARGV.any? && ! defined?(OptionParser)
77
+ require 'optparse'
78
+ OptionParser.new { |op|
79
+ op.on('-p port', 'set the port (default is 70)') { |val| set :port, Integer(val) }
80
+ op.on('-o addr', 'set the host (default is 0.0.0.0)') { |val| set :bind, val }
81
+ op.on('-e env', 'set the environment (default is development)') { |val| set :environment, val.to_sym }
82
+ }.parse!(ARGV.dup)
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,4 @@
1
+ module Gopher
2
+ # current version of the app
3
+ VERSION = "0.1.0"
4
+ end
data/lib/gopher2000.rb ADDED
@@ -0,0 +1,33 @@
1
+ require 'logging'
2
+ require 'eventmachine'
3
+ require 'stringio'
4
+
5
+
6
+ #
7
+ # Define everything needed to run a gopher server
8
+ #
9
+ module Gopher
10
+ require "gopher2000/version"
11
+ require "gopher2000/errors"
12
+
13
+ require 'gopher2000/rendering/abstract_renderer'
14
+ require 'gopher2000/rendering/base'
15
+ require 'gopher2000/rendering/text'
16
+ require 'gopher2000/rendering/menu'
17
+
18
+ require 'gopher2000/request'
19
+ require 'gopher2000/response'
20
+
21
+ require 'gopher2000/handlers/base_handler'
22
+ require 'gopher2000/handlers/directory_handler'
23
+ require 'gopher2000/base'
24
+ require 'gopher2000/server'
25
+
26
+ require 'gopher2000/dispatcher'
27
+ end
28
+
29
+ #
30
+ # include Gopher DSL in the main object space
31
+ #
32
+ require 'gopher2000/dsl'
33
+ include Gopher::DSL
data/scripts/god.rb ADDED
@@ -0,0 +1,8 @@
1
+ God.watch do |w|
2
+ w.name = "gopher"
3
+ w.dir = "/home/colin/Projects/gopher2000"
4
+ w.start = "bundle exec /home/colin/Projects/gopher2000/examples/simple.rb"
5
+ w.keepalive
6
+ w.log = '/tmp/gopher.log'
7
+
8
+ end
@@ -0,0 +1,54 @@
1
+ require File.join(File.dirname(__FILE__), '/spec_helper')
2
+ require 'tempfile'
3
+
4
+ describe Gopher::Application do
5
+ before(:each) do
6
+ @app = Gopher::Application.new
7
+ @app.reset!
8
+ @app.scripts = []
9
+ end
10
+
11
+ it 'should have default host/port' do
12
+ @app.host.should == "0.0.0.0"
13
+ @app.port.should == 70
14
+ end
15
+
16
+ describe "should_reload?" do
17
+ it "is false if no scripts" do
18
+ @app.should_reload?.should == false
19
+ end
20
+
21
+ it "shouldn't do anything if last_reload not set" do
22
+ @app.last_reload.should be_nil
23
+ @app.scripts << "foo.rb"
24
+ @app.should_reload?.should == false
25
+ end
26
+
27
+ it "should check script date" do
28
+ now = Time.now
29
+ Time.stub!(:now).and_return(now)
30
+
31
+ @app.last_reload = Time.now - 1000
32
+ @app.scripts << "foo.rb"
33
+ File.should_receive(:mtime).with("foo.rb").and_return(now)
34
+
35
+ @app.should_reload?.should == true
36
+ end
37
+ end
38
+
39
+ describe "reload_stale" do
40
+ it "should load script and update last_reload" do
41
+ now = Time.now
42
+ Time.stub!(:now).and_return(now)
43
+
44
+ @app.should_receive(:should_reload?).and_return(true)
45
+
46
+ @app.last_reload = Time.now - 1000
47
+ @app.scripts << "foo.rb"
48
+ @app.should_receive(:load).with("foo.rb")
49
+ @app.reload_stale
50
+
51
+ @app.last_reload.should == now
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,144 @@
1
+ require File.join(File.dirname(__FILE__), '/spec_helper')
2
+
3
+ class MockApplication < Gopher::Application
4
+ attr_accessor :menus, :routes, :config, :scripts, :last_reload
5
+
6
+ def initialize
7
+ @routes = []
8
+ @menus = {}
9
+ @scripts ||= []
10
+ @config = {}
11
+
12
+ register_defaults
13
+ end
14
+ end
15
+
16
+ describe Gopher::Application do
17
+ before(:each) do
18
+ @server = MockApplication.new
19
+ end
20
+
21
+ describe "lookup" do
22
+ it "should work with simple route" do
23
+ @server.route '/about' do; end
24
+ @request = Gopher::Request.new("/about")
25
+
26
+ keys, block = @server.lookup(@request.selector)
27
+ keys.should == {}
28
+ end
29
+
30
+ it "should translate path" do
31
+ @server.route '/about/:foo/:bar' do; end
32
+ @request = Gopher::Request.new("/about/x/y")
33
+
34
+ keys, block = @server.lookup(@request.selector)
35
+ keys.should == {:foo => 'x', :bar => 'y'}
36
+ end
37
+
38
+ it "should return default route if no other route found, and default is defined" do
39
+ @server.default_route do
40
+ "DEFAULT ROUTE"
41
+ end
42
+ @request = Gopher::Request.new("/about/x/y")
43
+ @response = @server.dispatch(@request)
44
+ @response.body.should == "DEFAULT ROUTE"
45
+ @response.code.should == :success
46
+ end
47
+
48
+ it "should respond with error if no route found" do
49
+ @server.route '/about/:foo/:bar' do; end
50
+ @request = Gopher::Request.new("/junk/x/y")
51
+
52
+ @response = @server.dispatch(@request)
53
+ @response.code.should == :missing
54
+ end
55
+
56
+ it "should respond with error if invalid request" do
57
+ @server.route '/about/:foo/:bar' do; end
58
+ @request = Gopher::Request.new("x" * 256)
59
+
60
+ @response = @server.dispatch(@request)
61
+ @response.code.should == :error
62
+ end
63
+
64
+ it "should respond with error if there's an exception" do
65
+ @server.route '/x' do; raise Exception; end
66
+ @request = Gopher::Request.new("/x")
67
+
68
+ @response = @server.dispatch(@request)
69
+ @response.code.should == :error
70
+ end
71
+ end
72
+
73
+ describe "dispatch" do
74
+ before(:each) do
75
+ #@server.should_receive(:lookup).and_return({})
76
+ @server.route '/about' do
77
+ 'GOPHERTRON'
78
+ end
79
+
80
+ @request = Gopher::Request.new("/about")
81
+ end
82
+
83
+ it "should run the block" do
84
+ @response = @server.dispatch(@request)
85
+ @response.body.should == "GOPHERTRON"
86
+ end
87
+ end
88
+
89
+ describe "dispatch, with params" do
90
+ before(:each) do
91
+ @server.route '/about/:x/:y' do
92
+ params.to_a.join("/")
93
+ end
94
+
95
+ @request = Gopher::Request.new("/about/a/b")
96
+ end
97
+
98
+ it "should use incoming params" do
99
+ @response = @server.dispatch(@request)
100
+ @response.body.should == "x/a/y/b"
101
+ end
102
+ end
103
+
104
+ describe "dispatch to mount" do
105
+ before(:each) do
106
+ @h = mock(Gopher::Handlers::DirectoryHandler)
107
+ @h.should_receive(:application=).with(@server)
108
+ Gopher::Handlers::DirectoryHandler.should_receive(:new).with({:bar => :baz, :mount_point => "/foo"}).and_return(@h)
109
+
110
+ @server.mount "/foo", :bar => :baz
111
+ end
112
+
113
+ it "should work for root path" do
114
+ @request = Gopher::Request.new("/foo")
115
+ @h.should_receive(:call).with({:splat => ""}, @request)
116
+
117
+ @response = @server.dispatch(@request)
118
+ @response.code.should == :success
119
+ end
120
+
121
+ it "should work for subdir" do
122
+ @request = Gopher::Request.new("/foo/bar")
123
+ @h.should_receive(:call).with({:splat => "bar"}, @request)
124
+
125
+ @response = @server.dispatch(@request)
126
+ @response.code.should == :success
127
+ end
128
+ end
129
+
130
+
131
+ describe "globs" do
132
+ before(:each) do
133
+ @server.route '/about/*' do
134
+ params[:splat]
135
+ end
136
+ end
137
+
138
+ it "should put wildcard into param[:splat]" do
139
+ @request = Gopher::Request.new("/about/a/b")
140
+ @response = @server.dispatch(@request)
141
+ @response.body.should == "a/b"
142
+ end
143
+ end
144
+ end
data/spec/dsl_spec.rb ADDED
@@ -0,0 +1,116 @@
1
+ require File.join(File.dirname(__FILE__), '/spec_helper')
2
+
3
+ class FakeApp < Gopher::Application
4
+
5
+ end
6
+
7
+ class FakeServer < Gopher::Server
8
+
9
+ end
10
+
11
+ describe Gopher::DSL do
12
+ before(:each) do
13
+ @app = FakeApp.new
14
+
15
+ @server = FakeServer.new(@app)
16
+ @server.send :require, 'gopher2000/dsl'
17
+ @server.stub!(:application).and_return(@app)
18
+ @app.reset!
19
+ end
20
+
21
+
22
+ describe "set" do
23
+ it "should set a config var" do
24
+ @server.set :foo, 'bar'
25
+ @app.config[:foo].should == 'bar'
26
+ end
27
+ end
28
+
29
+ describe "route" do
30
+ it "should pass a lookup and block to the app" do
31
+ @app.should_receive(:route).with('/foo')
32
+ @server.route '/foo' do
33
+ "hi"
34
+ end
35
+ end
36
+ end
37
+
38
+ describe "default_route" do
39
+ it "should pass a default block to the app" do
40
+ @app.should_receive(:default_route)
41
+ @server.default_route do
42
+ "hi"
43
+ end
44
+ end
45
+ end
46
+
47
+ describe "mount" do
48
+ it "should pass a route, path, and some opts to the app" do
49
+ @app.should_receive(:mount).with('/foo', {:path => "/bar"})
50
+ @server.mount "/foo" => "/bar"
51
+ end
52
+
53
+ it "should pass a route, path, filter, and some opts to the app" do
54
+ @app.should_receive(:mount).with('/foo', {:path => "/bar", :filter => "*.jpg"})
55
+ @server.mount "/foo" => "/bar", :filter => "*.jpg"
56
+ end
57
+ end
58
+
59
+ describe "menu" do
60
+ it "should pass a menu key and block to the app" do
61
+ @app.should_receive(:menu).with('/foo')
62
+ @server.menu '/foo' do
63
+ "hi"
64
+ end
65
+ end
66
+ end
67
+
68
+ describe "text" do
69
+ it "should pass a text_template key and block to the app" do
70
+ @app.should_receive(:text).with('/foo')
71
+ @server.text '/foo' do
72
+ "hi"
73
+ end
74
+ end
75
+ end
76
+
77
+ describe "helpers" do
78
+ it "should pass a block to the app" do
79
+ @app.should_receive(:helpers)
80
+ @server.helpers do
81
+ "hi"
82
+ end
83
+ end
84
+ end
85
+
86
+ describe "watch" do
87
+ it "should pass a script app for watching" do
88
+ @app.scripts.should_receive(:<<).with("foo")
89
+ @server.watch("foo")
90
+ end
91
+ end
92
+
93
+ describe "run" do
94
+ it "should set any incoming opts" do
95
+ @server.should_receive(:set).with(:x, 1)
96
+ @server.should_receive(:set).with(:y, 2)
97
+ @server.stub!(:load)
98
+
99
+ @server.run("foo", {:x => 1, :y => 2})
100
+ end
101
+
102
+ it "should turn on script watching if in debug mode" do
103
+ @app.config[:debug] = true
104
+ @server.should_receive(:watch).with("foo.rb")
105
+ @server.should_receive(:load).with("foo.rb")
106
+
107
+ @server.run("foo.rb")
108
+ end
109
+
110
+ it "should load the script" do
111
+ @server.should_receive(:load).with("foo.rb")
112
+ @server.run("foo.rb")
113
+ end
114
+ end
115
+
116
+ end
@@ -0,0 +1 @@
1
+ require File.join(File.dirname(__FILE__), '/spec_helper')
@@ -0,0 +1,116 @@
1
+ require File.join(File.dirname(__FILE__), '..', '/spec_helper')
2
+
3
+ describe Gopher::Handlers::DirectoryHandler do
4
+ before(:each) do
5
+ app = mock(Gopher::Application,
6
+ :host => "host",
7
+ :port => 1234,
8
+ :config => {})
9
+
10
+ @h = Gopher::Handlers::DirectoryHandler.new(:path => "/tmp", :mount_point => "/xyz/123")
11
+ @h.application = app
12
+ end
13
+
14
+ describe "filtering" do
15
+ before(:each) do
16
+ File.should_receive(:directory?).with("/tmp/bar/baz").and_return(true)
17
+ File.should_receive(:directory?).with("/tmp/bar/baz/a.txt").and_return(false)
18
+ File.should_receive(:directory?).with("/tmp/bar/baz/b.exe").and_return(false)
19
+ File.should_receive(:directory?).with("/tmp/bar/baz/dir2").and_return(true)
20
+
21
+ File.should_receive(:file?).with("/tmp/bar/baz/a.txt").and_return(true)
22
+ File.should_receive(:file?).with("/tmp/bar/baz/b.exe").and_return(true)
23
+
24
+ File.should_receive(:fnmatch).with("*.txt", "/tmp/bar/baz/a.txt").and_return(true)
25
+ File.should_receive(:fnmatch).with("*.txt", "/tmp/bar/baz/b.exe").and_return(false)
26
+
27
+ Dir.should_receive(:glob).with("/tmp/bar/baz/*.*").and_return([
28
+ "/tmp/bar/baz/a.txt",
29
+ "/tmp/bar/baz/b.exe",
30
+ "/tmp/bar/baz/dir2"])
31
+
32
+ @h.filter = "*.txt"
33
+ end
34
+
35
+ it "should use right filter" do
36
+ @h.call(:splat => "bar/baz")
37
+ end
38
+ end
39
+
40
+ describe "request_path" do
41
+ it "should join existing path with incoming path" do
42
+ @h.request_path(:splat => "bar/baz").should == "/tmp/bar/baz"
43
+ end
44
+ end
45
+
46
+ describe "to_selector" do
47
+ it "should work" do
48
+ @h.to_selector("/tmp/foo/bar.html").should == "/xyz/123/foo/bar.html"
49
+ @h.to_selector("/tmp/foo/baz").should == "/xyz/123/foo/baz"
50
+ @h.to_selector("/tmp").should == "/xyz/123"
51
+ end
52
+ end
53
+
54
+ describe "contained?" do
55
+ it "should be false if not under base path" do
56
+ @h.contained?("/home/gopher").should == false
57
+ end
58
+ it "should be true if under base path" do
59
+ @h.contained?("/tmp/gopher").should == true
60
+ end
61
+ end
62
+
63
+ describe "safety checks" do
64
+ it "should raise exception for invalid directory" do
65
+ lambda {
66
+ @h.call(:splat => "../../../home/foo/bar/baz").to_s.should == "0a\t/tmp/bar/baz/a\thost\t1234"
67
+ }.should raise_error(Gopher::InvalidRequest)
68
+ end
69
+ end
70
+
71
+ describe "directories" do
72
+ before(:each) do
73
+ File.should_receive(:directory?).with("/tmp/bar/baz").and_return(true)
74
+ File.should_receive(:directory?).with("/tmp/bar/baz/a").and_return(false)
75
+ File.should_receive(:directory?).with("/tmp/bar/baz/dir2").and_return(true)
76
+
77
+ File.should_receive(:file?).with("/tmp/bar/baz/a").and_return(true)
78
+ File.should_receive(:fnmatch).with("*.*", "/tmp/bar/baz/a").and_return(true)
79
+
80
+ Dir.should_receive(:glob).with("/tmp/bar/baz/*.*").and_return([
81
+ "/tmp/bar/baz/a",
82
+ "/tmp/bar/baz/dir2"])
83
+ end
84
+
85
+ it "should work" do
86
+ @h.call(:splat => "bar/baz").to_s.should == "iBrowsing: /tmp/bar/baz\tnull\t(FALSE)\t0\r\n0a\t/xyz/123/bar/baz/a\thost\t1234\r\n1dir2\t/xyz/123/bar/baz/dir2\thost\t1234\r\n"
87
+ end
88
+ end
89
+
90
+ describe "files" do
91
+ before(:each) do
92
+ @file = mock(File)
93
+ File.should_receive(:directory?).with("/tmp/baz.txt").and_return(false)
94
+
95
+ File.should_receive(:file?).with("/tmp/baz.txt").and_return(true)
96
+ File.should_receive(:new).with("/tmp/baz.txt").and_return(@file)
97
+ end
98
+
99
+ it "should work" do
100
+ @h.call(:splat => "baz.txt").should == @file
101
+ end
102
+ end
103
+
104
+ describe "missing stuff" do
105
+ before(:each) do
106
+ File.should_receive(:directory?).with("/tmp/baz.txt").and_return(false)
107
+ File.should_receive(:file?).with("/tmp/baz.txt").and_return(false)
108
+ end
109
+
110
+ it "should return not found" do
111
+ lambda {
112
+ @h.call(:splat => "baz.txt")
113
+ }.should raise_error(Gopher::NotFoundError)
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,16 @@
1
+ require File.join(File.dirname(__FILE__), '/spec_helper')
2
+
3
+ describe Gopher::Application do
4
+ before(:each) do
5
+ @obj = Gopher::Application.new
6
+ # @obj.extend(Gopher::Helpers)
7
+ end
8
+
9
+ it 'should add code to target class' do
10
+ @obj.helpers do
11
+ def foo; "FOO"; end
12
+ end
13
+
14
+ @obj.foo.should == "FOO"
15
+ end
16
+ end
@@ -0,0 +1,59 @@
1
+ require File.join(File.dirname(__FILE__), '/../spec_helper')
2
+
3
+ describe Gopher::Rendering::Base do
4
+ before(:each) do
5
+ @ctx = Gopher::Rendering::Base.new
6
+ end
7
+
8
+ it 'should add text' do
9
+ @ctx.text("line 1")
10
+ @ctx.text("line 2")
11
+ @ctx.result.should == "line 1\r\nline 2\r\n"
12
+ end
13
+
14
+ it "should add breaks correctly" do
15
+ @ctx.spacing 2
16
+ @ctx.text("line 1")
17
+ @ctx.text("line 2")
18
+ @ctx.result.should == "line 1\r\n\r\nline 2\r\n\r\n"
19
+ end
20
+
21
+ it "br outputs a bunch of newlines" do
22
+ @ctx.br(2).should == "\r\n\r\n"
23
+ end
24
+
25
+ describe "underline" do
26
+ it "underline outputs a pretty line" do
27
+ @ctx.underline(1, 'x').should == "x\r\n"
28
+ end
29
+ it "has defaults" do
30
+ @ctx.underline.should == "=" * 70 + "\r\n"
31
+ end
32
+ end
33
+
34
+ describe "big_header" do
35
+ it "outputs a box with text" do
36
+ @ctx.width(5)
37
+ @ctx.big_header('pie').should == "\r\n=====\r\n=pie=\r\n=====\r\n\r\n"
38
+ end
39
+ end
40
+
41
+ describe "header" do
42
+ it "outputs underlined text" do
43
+ @ctx.width(5)
44
+ @ctx.header('pie').should == " pie \r\n=====\r\n"
45
+ end
46
+ end
47
+
48
+ it "uses to_s to output result" do
49
+ @ctx.text("line 1")
50
+ @ctx.to_s.should == @ctx.result
51
+ end
52
+
53
+ describe "block" do
54
+ it "wraps text" do
55
+ @ctx.should_receive(:text).twice.with "a"
56
+ @ctx.block("a a",1)
57
+ end
58
+ end
59
+ end