asir 0.2.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 (121) hide show
  1. data/.gitignore +11 -0
  2. data/Gemfile +16 -0
  3. data/README.textile +50 -0
  4. data/Rakefile +83 -0
  5. data/VERSION +1 -0
  6. data/asir.gemspec +36 -0
  7. data/asir.riterate.yml +114 -0
  8. data/bin/asir +6 -0
  9. data/doc/Rakefile +8 -0
  10. data/doc/asir-sequence.pic +84 -0
  11. data/doc/asir-sequence.svg +1559 -0
  12. data/doc/sequence.pic +430 -0
  13. data/example/asir_control.sh +24 -0
  14. data/example/asir_control_client_http.rb +14 -0
  15. data/example/asir_control_client_zmq.rb +15 -0
  16. data/example/config/asir_config.rb +63 -0
  17. data/example/delayed_service.rb +15 -0
  18. data/example/ex01.rb +12 -0
  19. data/example/ex02.rb +12 -0
  20. data/example/ex03.rb +19 -0
  21. data/example/ex04.rb +33 -0
  22. data/example/ex05.rb +16 -0
  23. data/example/ex06.rb +26 -0
  24. data/example/ex07.rb +28 -0
  25. data/example/ex08.rb +30 -0
  26. data/example/ex09.rb +25 -0
  27. data/example/ex10.rb +24 -0
  28. data/example/ex11.rb +48 -0
  29. data/example/ex12.rb +34 -0
  30. data/example/ex13.rb +35 -0
  31. data/example/ex14.rb +30 -0
  32. data/example/ex15.rb +13 -0
  33. data/example/ex16.rb +33 -0
  34. data/example/ex17.rb +41 -0
  35. data/example/ex18.rb +62 -0
  36. data/example/ex19.rb +32 -0
  37. data/example/ex20.rb +28 -0
  38. data/example/ex21.rb +28 -0
  39. data/example/ex22.rb +15 -0
  40. data/example/ex23.rb +20 -0
  41. data/example/ex24.rb +35 -0
  42. data/example/example_helper.rb +51 -0
  43. data/example/sample_service.rb +162 -0
  44. data/example/unsafe_service.rb +12 -0
  45. data/hack_night/README.txt +18 -0
  46. data/hack_night/exercise/prob-1.rb +18 -0
  47. data/hack_night/exercise/prob-2.rb +21 -0
  48. data/hack_night/exercise/prob-3.rb +16 -0
  49. data/hack_night/exercise/prob-4.rb +36 -0
  50. data/hack_night/exercise/prob-5.rb +36 -0
  51. data/hack_night/exercise/prob-6.rb +95 -0
  52. data/hack_night/exercise/prob-7.rb +34 -0
  53. data/hack_night/solution/math_service.rb +11 -0
  54. data/hack_night/solution/prob-1.rb +12 -0
  55. data/hack_night/solution/prob-2.rb +15 -0
  56. data/hack_night/solution/prob-3.rb +17 -0
  57. data/hack_night/solution/prob-4.rb +37 -0
  58. data/hack_night/solution/prob-5.rb +21 -0
  59. data/hack_night/solution/prob-6.rb +33 -0
  60. data/hack_night/solution/prob-7.rb +36 -0
  61. data/lab/phony_proc.rb +31 -0
  62. data/lib/asir.rb +253 -0
  63. data/lib/asir/additional_data.rb +25 -0
  64. data/lib/asir/channel.rb +130 -0
  65. data/lib/asir/client.rb +111 -0
  66. data/lib/asir/code_block.rb +57 -0
  67. data/lib/asir/code_more.rb +50 -0
  68. data/lib/asir/coder.rb +26 -0
  69. data/lib/asir/coder/base64.rb +19 -0
  70. data/lib/asir/coder/chain.rb +30 -0
  71. data/lib/asir/coder/identity.rb +23 -0
  72. data/lib/asir/coder/json.rb +30 -0
  73. data/lib/asir/coder/marshal.rb +17 -0
  74. data/lib/asir/coder/null.rb +23 -0
  75. data/lib/asir/coder/proc.rb +22 -0
  76. data/lib/asir/coder/sign.rb +48 -0
  77. data/lib/asir/coder/xml.rb +213 -0
  78. data/lib/asir/coder/yaml.rb +33 -0
  79. data/lib/asir/coder/zlib.rb +21 -0
  80. data/lib/asir/configuration.rb +32 -0
  81. data/lib/asir/error.rb +34 -0
  82. data/lib/asir/identity.rb +36 -0
  83. data/lib/asir/initialization.rb +23 -0
  84. data/lib/asir/log.rb +82 -0
  85. data/lib/asir/main.rb +396 -0
  86. data/lib/asir/message.rb +31 -0
  87. data/lib/asir/message/delay.rb +35 -0
  88. data/lib/asir/object_resolving.rb +15 -0
  89. data/lib/asir/result.rb +39 -0
  90. data/lib/asir/retry_behavior.rb +54 -0
  91. data/lib/asir/transport.rb +241 -0
  92. data/lib/asir/transport/beanstalk.rb +217 -0
  93. data/lib/asir/transport/broadcast.rb +34 -0
  94. data/lib/asir/transport/buffer.rb +115 -0
  95. data/lib/asir/transport/composite.rb +19 -0
  96. data/lib/asir/transport/connection_oriented.rb +180 -0
  97. data/lib/asir/transport/delay.rb +38 -0
  98. data/lib/asir/transport/delegation.rb +53 -0
  99. data/lib/asir/transport/fallback.rb +36 -0
  100. data/lib/asir/transport/file.rb +88 -0
  101. data/lib/asir/transport/http.rb +54 -0
  102. data/lib/asir/transport/local.rb +21 -0
  103. data/lib/asir/transport/null.rb +14 -0
  104. data/lib/asir/transport/payload_io.rb +52 -0
  105. data/lib/asir/transport/rack.rb +73 -0
  106. data/lib/asir/transport/retry.rb +41 -0
  107. data/lib/asir/transport/stream.rb +35 -0
  108. data/lib/asir/transport/subprocess.rb +30 -0
  109. data/lib/asir/transport/tcp_socket.rb +34 -0
  110. data/lib/asir/transport/webrick.rb +50 -0
  111. data/lib/asir/transport/zmq.rb +110 -0
  112. data/lib/asir/uuid.rb +32 -0
  113. data/lib/asir/version.rb +3 -0
  114. data/spec/const_get_speed_spec.rb +33 -0
  115. data/spec/debug_helper.rb +20 -0
  116. data/spec/example_spec.rb +88 -0
  117. data/spec/json_spec.rb +128 -0
  118. data/spec/spec_helper.rb +3 -0
  119. data/spec/xml_spec.rb +144 -0
  120. data/stylesheets/slides.css +105 -0
  121. metadata +173 -0
@@ -0,0 +1,14 @@
1
+ require 'example_helper'
2
+ require 'asir/transport/http'
3
+ require 'asir/coder/marshal'
4
+ begin
5
+ Email.client.transport = t =
6
+ ASIR::Transport::HTTP.new(:uri => "http://localhost:30000/asir")
7
+ t.encoder = ASIR::Coder::Marshal.new
8
+ pr Email.client.send_email(:pdf_invoice,
9
+ :to => "user@email.com",
10
+ :customer => @customer)
11
+ ensure
12
+ t.close rescue nil
13
+ end
14
+
@@ -0,0 +1,15 @@
1
+ require 'example_helper'
2
+ require 'asir/transport/zmq'
3
+ require 'asir/coder/marshal'
4
+ begin
5
+ Email.client.transport = t =
6
+ ASIR::Transport::Zmq.new :uri => "tcp://localhost:31000" # "/asir"
7
+ t.one_way = true
8
+ t.encoder = ASIR::Coder::Marshal.new
9
+ pr Email.client.send_email(:pdf_invoice,
10
+ :to => "user@email.com",
11
+ :customer => @customer)
12
+ ensure
13
+ t.close rescue nil
14
+ end
15
+
@@ -0,0 +1,63 @@
1
+ # Used by asir/bin/asir.
2
+ # Configures asir worker transport and error logging.
3
+ # asir object is bound to ASIR::Main instance.
4
+
5
+ $stderr.puts "asir.verb = #{asir.verb.inspect}"
6
+ case asir.verb
7
+ when :config
8
+ # NOTHING
9
+ true
10
+ when :environment
11
+ require 'asir'
12
+ require 'asir/transport/file'
13
+ require 'asir/coder/marshal'
14
+ require 'asir/coder/yaml'
15
+
16
+ $:.unshift File.expand_path('..')
17
+ require 'example_helper'
18
+ require 'sample_service'
19
+ require 'unsafe_service'
20
+ when :transport
21
+ # Compose with Marshal for final coding.
22
+ coder = ASIR::Coder::Marshal.new
23
+
24
+ # Logger for worker-side Exceptions.
25
+ error_log_file = asir.log_file.sub(/\.log$/, '-error.log')
26
+ error_transport =
27
+ ASIR::Transport::File.new(:file => error_log_file,
28
+ :mode => 'a+',
29
+ :perms => 0666)
30
+ error_transport.encoder = ASIR::Coder::Yaml.new
31
+
32
+ # Setup requested Transport.
33
+ case asir.adjective
34
+ when :beanstalk
35
+ transport = ASIR::Transport::Beanstalk.new
36
+ transport[:worker_processes] = 3
37
+ when :http, :webrick
38
+ transport = ASIR::Transport::Webrick.new
39
+ transport.uri = "http://localhost:#{30000 + asir.identifier.to_s.to_i}/asir"
40
+ when :rack
41
+ transport = ASIR::Transport::Rack.new
42
+ transport.uri = "http://localhost:#{30000 + asir.identifier.to_s.to_i}/asir"
43
+ when :zmq
44
+ transport = ASIR::Transport::Zmq.new
45
+ transport.one_way = true
46
+ transport.uri = "tcp://localhost:#{31000 + asir.identifier.to_s.to_i}" # /asir"
47
+ else
48
+ raise "Cannot configure Transport for #{asir.adjective}"
49
+ end
50
+
51
+ transport.encoder = coder
52
+ transport._logger = STDERR
53
+ transport._log_enabled = true
54
+ transport.verbose = 3
55
+ transport.on_exception =
56
+ lambda { | transport, exc, phase, message, *rest |
57
+ error_transport.send_request(message)
58
+ }
59
+
60
+ transport
61
+ else
62
+ $stderr.puts "Warning: unhandled asir.verb: #{asir.verb.inspect}"
63
+ end
@@ -0,0 +1,15 @@
1
+ require 'asir'
2
+ require 'time'
3
+
4
+ module DelayedService
5
+ include ASIR::Client
6
+ def self.do_it(t0)
7
+ dt = Time.now - t0
8
+ result = 5 <= dt && dt <= 6 ? :ok : :not_delayed
9
+ puts "DelayedService.do_it(#{t0.iso8601}) dt=#{dt.inspect} #{result.inspect}"
10
+ $stderr.puts "DelayedService.do_it => #{result.inspect}"
11
+ raise "Failed" unless result == :ok
12
+ result
13
+ end
14
+ end
15
+
@@ -0,0 +1,12 @@
1
+ # !SLIDE :capture_code_output true
2
+ # Call service directly
3
+
4
+ require 'example_helper' # !COMMENT
5
+ pr Email.send_email(:pdf_invoice,
6
+ :to => "user@email.com",
7
+ :customer => @customer)
8
+
9
+ # !SLIDE END
10
+ # EXPECT: : client process
11
+ # EXPECT: : Email.send_mail :pdf_invoice
12
+ # EXPECT: : pr: :ok
@@ -0,0 +1,12 @@
1
+ # !SLIDE :capture_code_output true
2
+ # In-core, in-process service
3
+
4
+ require 'example_helper'
5
+ pr Email.client.send_email(:pdf_invoice,
6
+ :to => "user@email.com",
7
+ :customer => @customer)
8
+
9
+ # !SLIDE END
10
+ # EXPECT: : client process
11
+ # EXPECT: : Email.send_mail :pdf_invoice
12
+ # EXPECT: : pr: :ok
@@ -0,0 +1,19 @@
1
+ #
2
+ # !SLIDE :capture_code_output true
3
+ # One-way, asynchronous subprocess service
4
+
5
+ require 'example_helper'
6
+ begin
7
+ Email.client.transport = t =
8
+ ASIR::Transport::Subprocess.new
9
+
10
+ pr Email.client.send_email(:pdf_invoice,
11
+ :to => "user@email.com",
12
+ :customer => @customer)
13
+ end
14
+
15
+ # !SLIDE END
16
+ # EXPECT: : client process
17
+ # EXPECT: : Email.send_mail :pdf_invoice
18
+ # EXPECT: : pr: nil
19
+
@@ -0,0 +1,33 @@
1
+ # !SLIDE :capture_code_output true
2
+ # One-way, file log service
3
+
4
+ require 'example_helper'
5
+ begin
6
+ File.unlink(service_log = "#{__FILE__}.service.log") rescue nil
7
+ Email.client.transport = t =
8
+ ASIR::Transport::File.new(:file => service_log)
9
+ t.encoder =
10
+ ASIR::Coder::Yaml.new
11
+ pr Email.client.send_email(:pdf_invoice,
12
+ :to => "user@email.com",
13
+ :customer => @customer)
14
+ ensure
15
+ t.close
16
+ puts "\x1a\n#{service_log.inspect} contents:"
17
+ puts File.read(service_log)
18
+ end
19
+
20
+ # !SLIDE END
21
+ # EXPECT: : client process
22
+ # EXPECT: service.log" contents:
23
+ # EXPECT: pr: nil
24
+ # EXPECT: --- !ruby/object:ASIR::Message
25
+ # EXPECT: --- !ruby/object:ASIR::Message
26
+ # EXPECT: arguments:
27
+ # EXPECT: - :pdf_invoice
28
+ # EXPECT: :to: user@email.com
29
+ # EXPECT: :customer: 123
30
+ # EXPECT: receiver: Email
31
+ # EXPECT: receiver_class: Module
32
+ # EXPECT: selector: :send_email
33
+
@@ -0,0 +1,16 @@
1
+ # !SLIDE :capture_code_output true
2
+ # Replay file log
3
+
4
+ require 'example_helper'
5
+ begin
6
+ service_log = "#{__FILE__.sub('ex05', 'ex04')}.service.log"
7
+ Email.client.transport = t =
8
+ ASIR::Transport::File.new(:file => service_log)
9
+ t.encoder =
10
+ ASIR::Coder::Yaml.new
11
+ t.serve_file!
12
+ end
13
+
14
+ # !SLIDE END
15
+ # EXPECT: : client process
16
+ # EXPECT: : Email.send_mail :pdf_invoice
@@ -0,0 +1,26 @@
1
+ # !SLIDE :capture_code_output true
2
+ # One-way, named pipe service
3
+
4
+ require 'example_helper'
5
+ begin
6
+ File.unlink(service_pipe = "service.pipe") rescue nil
7
+ Email.client.transport = t =
8
+ ASIR::Transport::File.new(:file => service_pipe)
9
+ t.encoder =
10
+ ASIR::Coder::Yaml.new
11
+ t.prepare_pipe_server!
12
+ server_process do
13
+ t.run_pipe_server!
14
+ end
15
+ sleep 1
16
+ pr Email.client.send_email(:pdf_invoice, :to => "user@email.com", :customer => @customer)
17
+ ensure
18
+ t.close; sleep 1; server_kill
19
+ end
20
+
21
+ # !SLIDE END
22
+ # EXPECT: : client process
23
+ # EXPECT: : server process
24
+ # EXPECT: : Email.send_mail :pdf_invoice
25
+ # EXPECT: : pr: nil
26
+
@@ -0,0 +1,28 @@
1
+ # !SLIDE :capture_code_output true
2
+ # One-way, named pipe service with signature
3
+
4
+ require 'example_helper'
5
+ begin
6
+ File.unlink(service_pipe = "service.pipe") rescue nil
7
+ Email.client.transport = t =
8
+ ASIR::Transport::File.new(:file => service_pipe)
9
+ t.encoder =
10
+ ASIR::Coder::Chain.new(:encoders =>
11
+ [ ASIR::Coder::Marshal.new,
12
+ s = ASIR::Coder::Sign.new(:secret => 'abc123'),
13
+ ASIR::Coder::Yaml.new,
14
+ ])
15
+ t.prepare_pipe_server!
16
+ server_process do
17
+ t.run_pipe_server!
18
+ end
19
+ pr Email.client.send_email(:pdf_invoice, :to => "user@email.com", :customer => @customer)
20
+ ensure
21
+ t.close; sleep 1; server_kill
22
+ end
23
+
24
+ # !SLIDE END
25
+ # EXPECT: : client process
26
+ # EXPECT: : server process
27
+ # EXPECT: : Email.send_mail :pdf_invoice
28
+ # EXPECT: : pr: nil
@@ -0,0 +1,30 @@
1
+ # !SLIDE :capture_code_output true
2
+ # One-way, named pipe service with invalid signature
3
+
4
+ require 'example_helper'
5
+ begin
6
+ File.unlink(service_pipe = "service.pipe") rescue nil
7
+ Email.client.transport = t =
8
+ ASIR::Transport::File.new(:file => service_pipe)
9
+ t.encoder =
10
+ ASIR::Coder::Chain.new(:encoders =>
11
+ [ ASIR::Coder::Marshal.new,
12
+ s = ASIR::Coder::Sign.new(:secret => 'abc123'),
13
+ ASIR::Coder::Yaml.new,
14
+ ])
15
+ t.prepare_pipe_server!
16
+ server_process do
17
+ t.run_pipe_server!
18
+ end
19
+ s.secret = 'I do not know the secret! :('
20
+ pr Email.client.send_email(:pdf_invoice, :to => "user@email.com", :customer => @customer)
21
+ ensure
22
+ t.close; sleep 1; server_kill
23
+ end
24
+
25
+ # !SLIDE END
26
+ # EXPECT: : client process
27
+ # EXPECT: : server process
28
+ # EXPECT!: : Email.send_mail :pdf_invoice
29
+ # EXPECT: : pr: nil
30
+
@@ -0,0 +1,25 @@
1
+ # !SLIDE :capture_code_output true
2
+ # Socket service
3
+
4
+ require 'example_helper'
5
+ begin
6
+ Email.client.transport = t =
7
+ ASIR::Transport::TcpSocket.new(:port => 30909)
8
+ t.encoder =
9
+ ASIR::Coder::Marshal.new
10
+ t.prepare_server!
11
+ server_process do
12
+ t.run_server!
13
+ end
14
+ pr Email.client.send_email(:pdf_invoice,
15
+ :to => "user@email.com", :customer => @customer)
16
+ ensure
17
+ t.close; sleep 1; server_kill
18
+ end
19
+
20
+ # !SLIDE END
21
+ # EXPECT: : client process
22
+ # EXPECT: : server process
23
+ # EXPECT: : Email.send_mail :pdf_invoice
24
+ # EXPECT: : pr: :ok
25
+
@@ -0,0 +1,24 @@
1
+ # !SLIDE :capture_code_output true
2
+ # Socket service with forwarded exception.
3
+
4
+ require 'example_helper'
5
+ begin
6
+ Email.client.transport = t =
7
+ ASIR::Transport::TcpSocket.new(:port => 30910)
8
+ t.encoder =
9
+ ASIR::Coder::Marshal.new
10
+ t.prepare_server!
11
+ server_process do
12
+ t.run_server!
13
+ end
14
+ pr Email.client.do_raise("Raise Me!")
15
+ rescue Exception => err
16
+ pr [ :exception, err ]
17
+ ensure
18
+ t.close; sleep 1; server_kill
19
+ end
20
+
21
+ # !SLIDE END
22
+ # EXPECT: : client process
23
+ # EXPECT: : server process
24
+ # EXPECT: : pr: [:exception, #<RuntimeError: Raise Me!>]
@@ -0,0 +1,48 @@
1
+ # !SLIDE :capture_code_output true
2
+ # Socket service with local fallback.
3
+
4
+ require 'example_helper'
5
+ begin
6
+ File.unlink(service_log = "#{__FILE__}.service.log") rescue nil
7
+ Email.client.transport = t =
8
+ ASIR::Transport::Fallback.new(:transports => [
9
+ tcp = ASIR::Transport::TcpSocket.new(:port => 31911,
10
+ :encoder => ASIR::Coder::Marshal.new),
11
+ ASIR::Transport::Broadcast.new(:transports => [
12
+ file = ASIR::Transport::File.new(:file => service_log,
13
+ :encoder => ASIR::Coder::Yaml.new),
14
+ ASIR::Transport::Subprocess.new,
15
+ ]),
16
+ ])
17
+ pr Email.client.send_email(:pdf_invoice,
18
+ :to => "user@email.com", :customer => @customer)
19
+ server_process do
20
+ tcp.prepare_server!
21
+ tcp.run_server!
22
+ end; sleep 2
23
+ pr Email.client.send_email(:pdf_invoice,
24
+ :to => "user2@email.com", :customer => @customer)
25
+ ensure
26
+ file.close rescue nil;
27
+ tcp.close rescue nil; sleep 1; server_kill
28
+ puts "\x1a\n#{service_log.inspect} contents:"
29
+ puts File.read(service_log)
30
+ end
31
+
32
+ # !SLIDE END
33
+ # EXPECT: : client process
34
+ # EXPECT: : server process
35
+ # EXPECT/: : Email.send_mail :pdf_invoice .*:to=>"user@email.com"
36
+ # EXPECT/: : Email.send_mail :pdf_invoice .*:to=>"user2@email.com"
37
+ # EXPECT: : pr: :ok
38
+ # EXPECT: service.log" contents:
39
+ # EXPECT: --- !ruby/object:ASIR::Message
40
+ # EXPECT: :transport_exceptions:
41
+ # EXPECT: ASIR::Error: Cannot connect to ASIR::Transport::TcpSocket tcp://127.0.0.1:
42
+ # EXPECT: arguments:
43
+ # EXPECT: - :pdf_invoice
44
+ # EXPECT/: :to: user@email.com
45
+ # EXPECT/: :customer: 123
46
+ # EXPECT: receiver: Email
47
+ # EXPECT: receiver_class: Module
48
+ # EXPECT: selector: :send_email
@@ -0,0 +1,34 @@
1
+ # !SLIDE :capture_code_output true
2
+ # Asynchronous beanstalkd service
3
+
4
+ require 'example_helper'
5
+ require 'asir/transport/beanstalk'
6
+ require 'asir/coder/zlib'
7
+ begin
8
+ Email.client.transport = t =
9
+ ASIR::Transport::Beanstalk.new(:address => '127.0.0.1', :port => 30904)
10
+ t.encoder =
11
+ ASIR::Coder::Chain.new(:encoders =>
12
+ [ ASIR::Coder::Marshal.new,
13
+ ASIR::Coder::Zlib.new, ])
14
+ t.start_beanstalkd!; sleep 1
15
+ pr Email.client.send_email(:pdf_invoice,
16
+ :to => "user@email.com", :customer => @customer)
17
+ sleep 2
18
+ server_process do
19
+ t.prepare_beanstalk_server!
20
+ t.run_beanstalk_server!
21
+ end
22
+ rescue Object => err
23
+ $stderr.puts "#{err.inspect}\n#{err.backtrace * "\n"}"
24
+ ensure
25
+ t.close; sleep 3; server_kill; sleep 2
26
+ t.stop_beanstalkd!
27
+ end
28
+
29
+ # !SLIDE END
30
+ # EXPECT: : client process
31
+ # EXPECT: : server process
32
+ # EXPECT: : Email.send_mail :pdf_invoice
33
+ # EXPECT: : pr: nil
34
+
@@ -0,0 +1,35 @@
1
+ # !SLIDE :capture_code_output true
2
+ # Synchronous HTTP service
3
+
4
+ require 'example_helper'
5
+ require 'asir/transport/webrick'
6
+ require 'asir/coder/base64'
7
+ require 'asir/coder/zlib'
8
+ begin
9
+ Email.client.transport = t =
10
+ ASIR::Transport::Webrick.new(:uri => "http://localhost:31913/")
11
+ t.encoder =
12
+ ASIR::Coder::Chain.new(:encoders =>
13
+ [ASIR::Coder::Marshal.new,
14
+ ASIR::Coder::Base64.new, ])
15
+ server_process do
16
+ t.prepare_server!
17
+ t.run_server!
18
+ end; sleep 2
19
+ pr Email.client.send_email(:pdf_invoice,
20
+ :to => "user@email.com",
21
+ :customer => @customer)
22
+ sleep 2
23
+ rescue Object => err
24
+ $stderr.puts "#{err.inspect}\n#{err.backtrace * "\n"}"
25
+ ensure
26
+ t.close rescue nil; sleep 3
27
+ server_kill; sleep 2
28
+ end
29
+
30
+ # !SLIDE END
31
+ # EXPECT: : client process
32
+ # EXPECT: : server process
33
+ # EXPECT: : Email.send_mail :pdf_invoice
34
+ # EXPECT: : pr: :ok
35
+