propono 0.6.3 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,15 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 299526d5c3416f58858496b984ec04ff96a74a69
4
- data.tar.gz: 8ab281343f74f9c5bfc233891311f1f21ebbf9e7
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ MTk2ZTQ1YmMxYTg4NzdiOWEyZDQ1ZWQyOTI4N2QyMGE4ZTA5OWRiOQ==
5
+ data.tar.gz: !binary |-
6
+ ZmUwY2I2M2E4ZjQ4ZTAxYzIwNzI4OTVjNWQ1OTkyYmFhYmM4Njk4OA==
5
7
  SHA512:
6
- metadata.gz: 9b3f4bbc2284bf4fad16d8c57dff469d94522b46948a5d1c882d0994b03a4c1d699680615dd653a0325671f512d84f47c165314d0e4d7cc914a8eda7871b318a
7
- data.tar.gz: 16609ea0f80a7cb985b84d4b2015bdbedd2aa83441f6dbcab2263db623bc17d412c2f0a191b0aab7d7b178ca781c98baa181a7aaba18291260a333725333c479
8
+ metadata.gz: !binary |-
9
+ ZTI1OWEzZGUwZTMzNzllNTYyNTE0NGZmOTZiOWUxZjJlYzUyNzY1NmQzMGZk
10
+ NTExYzYzNjE5NmI4NGE1Y2ZkODEzMzNkNzM2NzI1ZDAyMDFjYjQ0ZWIxYzhl
11
+ OGQ2NzBhZDlmNWQxZGYxMzUyMDJkN2I5YTAwMGM1YjIwMzYwMzU=
12
+ data.tar.gz: !binary |-
13
+ ZjFhYzMxYzgxNmZmNWM5MmQ1ODkwMTJlYzY4N2ZmNTE2NTMwYTY3MTA3YWVk
14
+ YjAxYjEyZjgwMjA4MmMxNzZjMDk5ZDA2M2RlZDc5ZmExNzRlNzQ2MTk2Mjc0
15
+ ZDY2OWU0ZjI2MmE2NWFjOWEyZDY5MGQyMTIwNTdhNWViOTI1Zjc=
@@ -1,3 +1,7 @@
1
+ # 0.7.0 / Unreleased
2
+
3
+ * [FEATURE] Add TCP publish and listen methods.
4
+
1
5
  # 0.6.3 / 2013-10-20
2
6
 
3
7
  * [FEATURE] Catch all StandardError exceptions for UDP publishes.
data/README.md CHANGED
@@ -58,34 +58,60 @@ end
58
58
  ```
59
59
  In the background, Propono is automatically setting up a queue using SQS, a notification system using SNS, and glueing them all together for you. But you don't have to worry about any of that.
60
60
 
61
+ ### Using TCP for messages
62
+
63
+ Publishing directly to SNS takes about 15x longer than publishing over a simple TCP connection. It is therefore some times favourable to publish to a seperate machine listening for TCP messages, which will then proxy them on.
64
+
65
+ To send messages this way, you need to set up a little extra config:
66
+
67
+ ```ruby
68
+ Propono.config.tcp_host = "some.host.running.a.propono.listener"
69
+ Propono.config.tcp_port = 12543
70
+ ```
71
+
72
+ You then simply pass the `:tcp` protocol into `publish`
73
+
74
+ ```ruby
75
+ Propono.publish('some-topic', message, protocol: :tcp)
76
+ ```
77
+
78
+ You'll now need another application running Propono to listen to the TCP feed. You can use the same machine or a different one, just make sure the port config is the same in both applications, and you're good to go.
79
+
80
+ ```ruby
81
+ Propono.listen_to_tcp do |topic, message|
82
+ Propono.publish(topic, message) # Proxy the message to SNS
83
+ end
84
+ ```
85
+
86
+ This proxying of TCP to SQS is used so often that there's a simple shortcut. Just run this on the machine receiving the TCP packets.
87
+
88
+ ```ruby
89
+ Propono.proxy_tcp()
90
+ ```
91
+
61
92
  ### Using UDP for messages
62
93
 
63
94
  If you want almost-zero performance impact, and don't mind the occasional message getting lost, you can use UDP. We use this for things like our live dashboard where we don't mind losing a piece of activity here and there, but any perforamnce impact on our Meducation itself is bad news.
64
95
 
65
- To send messages this way, you need to set up a little extra config:
96
+ Sending messages in this way is very similar to using TCP. First add some config:
66
97
 
67
98
  ```ruby
68
99
  Propono.config.udp_host = "some.host.running.a.propono.listener"
69
100
  Propono.config.udp_port = 12543
70
101
  ```
71
102
 
72
- You then simply pass the `:udp` protocol into `publish`
103
+ You then simply pass the `:udp` protocol into `publish`:
73
104
 
74
105
  ```ruby
75
106
  Propono.publish('some-topic', message, protocol: :udp)
76
- ```
77
107
 
78
- You'll now need another application running Propono to listen to the UDP feed. You can use the same machine or a different one, just make sure the port config is the same in both applications, and you're good to go.
108
+ As per the `listen_to_tcp` method explained above, you now listen to udp or use the proxy method:
79
109
 
80
110
  ```ruby
81
111
  Propono.listen_to_udp do |topic, message|
82
112
  Propono.publish(topic, message) # Proxy the message to SNS
83
113
  end
84
- ```
85
-
86
- This proxying of UDP to SQS is used so often that there's a simple shortcut. Just run this on the machine receiving the UDP packets.
87
114
 
88
- ```ruby
89
115
  Propono.proxy_udp()
90
116
  ```
91
117
 
@@ -100,7 +126,9 @@ Propono.config do |config|
100
126
  config.queue_region = "An AWS queue region"
101
127
  config.application_name = "A name unique in your network"
102
128
  config.udp_host = "The host of a machine used for UDP proxying"
103
- config.udp_port = "The host of a machine used for UDP proxying"
129
+ config.udp_port = "The port of a machine used for UDP proxying"
130
+ config.tcp_host = "The host of a machine used for TCP proxying"
131
+ config.tcp_port = "The port of a machine used for TCP proxying"
104
132
  config.logger = "A logger such as Log4r or Rails.logger"
105
133
  end
106
134
  ```
@@ -20,6 +20,7 @@ require "propono/services/queue_listener"
20
20
  require "propono/services/subscriber"
21
21
  require "propono/services/topic_creator"
22
22
  require "propono/services/udp_listener"
23
+ require "propono/services/tcp_listener"
23
24
 
24
25
  # Propono is a pub/sub gem built on top of Amazon Web Services (AWS).
25
26
  # It uses Simple Notification Service (SNS) and Simple Queue Service (SQS)
@@ -113,6 +114,17 @@ module Propono
113
114
  UdpListener.listen(&message_processor)
114
115
  end
115
116
 
117
+ # Listens for TCP messages and yields for each.
118
+ #
119
+ # Calling this will enter a queue-listening loop that
120
+ # yields the message_processor for each UDP message received.
121
+ #
122
+ # @param &message_processor The block to yield for each message.
123
+ # Is called with <tt>|topic, message|</tt>.
124
+ def self.listen_to_tcp(&message_processor)
125
+ TcpListener.listen(&message_processor)
126
+ end
127
+
116
128
  # Listens for UDP messages and passes them onto the queue.
117
129
  #
118
130
  # This method uses #listen_to_udp and #publish to proxy
@@ -122,4 +134,14 @@ module Propono
122
134
  Propono.publish(topic, message)
123
135
  end
124
136
  end
137
+
138
+ # Listens for TCP messages and passes them onto the queue.
139
+ #
140
+ # This method uses #listen_to_tcp and #publish to proxy
141
+ # messages from TCP onto the queue.
142
+ def self.proxy_tcp
143
+ Propono.listen_to_tcp do |topic, message|
144
+ Propono.publish(topic, message)
145
+ end
146
+ end
125
147
  end
@@ -9,6 +9,7 @@ module Propono
9
9
  :access_key, :secret_key, :queue_region,
10
10
  :application_name,
11
11
  :udp_host, :udp_port,
12
+ :tcp_host, :tcp_port,
12
13
  :logger
13
14
  ]
14
15
  attr_writer *SETTINGS
@@ -1,3 +1,5 @@
1
+ require 'socket'
2
+
1
3
  module Propono
2
4
  class PublisherError < ProponoError
3
5
  end
@@ -38,5 +40,15 @@ module Propono
38
40
  rescue => e
39
41
  Propono.config.logger.error "Propono failed to send : #{e}"
40
42
  end
43
+
44
+ def publish_via_tcp
45
+ payload = {topic: topic_id, message: message}.to_json
46
+
47
+ socket = TCPSocket.new(Propono.config.tcp_host, Propono.config.tcp_port)
48
+ socket.write payload
49
+ socket.close
50
+ rescue => e
51
+ Propono.config.logger.error "Propono failed to send : #{e}"
52
+ end
41
53
  end
42
54
  end
@@ -0,0 +1,40 @@
1
+ require 'json'
2
+
3
+ module Propono
4
+ class TcpListenerError < ProponoError
5
+ end
6
+
7
+ class TcpListener
8
+
9
+ def self.listen(&processor)
10
+ new(&processor).listen
11
+ end
12
+
13
+ def initialize(&processor)
14
+ raise TcpListenerError.new("Please provide a block to call for each message") unless block_given?
15
+ @processor = processor
16
+ end
17
+
18
+ def listen
19
+ loop { receive_and_process }
20
+ end
21
+
22
+ private
23
+
24
+ def receive_and_process
25
+ client = server.accept
26
+ tcp_data = client.recvfrom(1024)[0]
27
+ client.close
28
+ Thread.new { process_tcp_data(tcp_data) }
29
+ end
30
+
31
+ def process_tcp_data(tcp_data)
32
+ json = JSON.parse(tcp_data)
33
+ @processor.call(json['topic'], json['message'])
34
+ end
35
+
36
+ def server
37
+ @server ||= TCPServer.open(Propono.config.tcp_port)
38
+ end
39
+ end
40
+ end
@@ -1,3 +1,3 @@
1
1
  module Propono
2
- VERSION = "0.6.3"
2
+ VERSION = "0.7.0"
3
3
  end
@@ -43,6 +43,30 @@ module Propono
43
43
  assert_equal application_name, Propono.config.application_name
44
44
  end
45
45
 
46
+ def test_udp_host
47
+ val = "test-application-name"
48
+ Propono.config.udp_host = val
49
+ assert_equal val, Propono.config.udp_host
50
+ end
51
+
52
+ def test_udp_port
53
+ val = 10000
54
+ Propono.config.udp_port = val
55
+ assert_equal val, Propono.config.udp_port
56
+ end
57
+
58
+ def test_tcp_host
59
+ val = "test-application-name"
60
+ Propono.config.tcp_host = val
61
+ assert_equal val, Propono.config.tcp_host
62
+ end
63
+
64
+ def test_tcp_port
65
+ val = 9382
66
+ Propono.config.tcp_port = val
67
+ assert_equal val, Propono.config.tcp_port
68
+ end
69
+
46
70
  def test_missing_access_key_throws_exception
47
71
  assert_raises(ProponoConfigurationError) do
48
72
  Propono.config.access_key
@@ -14,6 +14,9 @@ module Propono
14
14
  break
15
15
  end
16
16
  end
17
+
18
+ sleep(2) # Make sure the listener has started
19
+
17
20
  Propono.publish(topic, text)
18
21
  flunk("Test Timeout") unless wait_for_thread(thread)
19
22
  ensure
@@ -0,0 +1,35 @@
1
+ require File.expand_path('../integration_test', __FILE__)
2
+
3
+ module Propono
4
+ class UdpToSqsTest < IntegrationTest
5
+ def test_the_message_gets_there
6
+ topic = "test-topic"
7
+ message = "This is my message"
8
+ Propono.config.tcp_host = "localhost"
9
+ Propono.config.tcp_port = 20009
10
+
11
+ Propono.subscribe_by_queue(topic)
12
+
13
+ sqs_thread = Thread.new do
14
+ Propono.listen_to_queue(topic) do |sqs_message|
15
+ assert_equal message, sqs_message
16
+ sqs_thread.terminate
17
+ end
18
+ end
19
+
20
+ tcp_thread = Thread.new do
21
+ Propono.listen_to_tcp do |tcp_topic, tcp_message|
22
+ Propono.publish(tcp_topic, tcp_message)
23
+ tcp_thread.terminate
24
+ end
25
+ end
26
+ sleep(2) # Make sure the listener has started
27
+
28
+ Propono.publish(topic, message, protocol: :tcp)
29
+ flunk("Test Timeout") unless wait_for_thread(tcp_thread) && wait_for_thread(sqs_thread)
30
+ ensure
31
+ tcp_thread.terminate
32
+ sqs_thread.terminate
33
+ end
34
+ end
35
+ end
@@ -9,10 +9,6 @@ module Propono
9
9
 
10
10
  Propono.subscribe_by_queue(topic)
11
11
 
12
- udp_thread = Thread.new do
13
- Propono.proxy_udp
14
- end
15
-
16
12
  sqs_thread = Thread.new do
17
13
  Propono.listen_to_queue(topic) do |sqs_message|
18
14
  assert_equal message, sqs_message
@@ -20,6 +16,12 @@ module Propono
20
16
  end
21
17
  end
22
18
 
19
+ udp_thread = Thread.new do
20
+ Propono.proxy_udp
21
+ end
22
+
23
+ sleep(2) # Make sure the proxy has started
24
+
23
25
  Propono.publish(topic, message, protocol: :udp)
24
26
  flunk("Test timeout") unless wait_for_thread(sqs_thread)
25
27
  ensure
@@ -9,6 +9,13 @@ module Propono
9
9
 
10
10
  Propono.subscribe_by_queue(topic)
11
11
 
12
+ sqs_thread = Thread.new do
13
+ Propono.listen_to_queue(topic) do |sqs_message|
14
+ assert_equal message, sqs_message
15
+ sqs_thread.terminate
16
+ end
17
+ end
18
+
12
19
  udp_thread = Thread.new do
13
20
  Propono.listen_to_udp do |udp_topic, udp_message|
14
21
  Propono.publish(udp_topic, udp_message)
@@ -16,12 +23,7 @@ module Propono
16
23
  end
17
24
  end
18
25
 
19
- sqs_thread = Thread.new do
20
- Propono.listen_to_queue(topic) do |sqs_message|
21
- assert_equal message, sqs_message
22
- sqs_thread.terminate
23
- end
24
- end
26
+ sleep(2) # Make sure the listener has started
25
27
 
26
28
  Propono.publish(topic, message, protocol: :udp)
27
29
  flunk("Test Timeout") unless wait_for_thread(udp_thread) && wait_for_thread(sqs_thread)
@@ -32,6 +32,11 @@ module Propono
32
32
  Propono.listen_to_udp()
33
33
  end
34
34
 
35
+ def test_listen_to_tcp_calls_tcp_listener
36
+ TcpListener.expects(:listen).with()
37
+ Propono.listen_to_tcp()
38
+ end
39
+
35
40
  def test_proxy_udp_calls_listen
36
41
  UdpListener.expects(:listen).with()
37
42
  Propono.proxy_udp()
@@ -44,5 +49,18 @@ module Propono
44
49
  Publisher.expects(:publish).with(topic, message, {})
45
50
  Propono.proxy_udp
46
51
  end
52
+
53
+ def test_proxy_tcp_calls_listen
54
+ TcpListener.expects(:listen).with()
55
+ Propono.proxy_tcp()
56
+ end
57
+
58
+ def test_proxy_tcp_calls_publish_in_the_block
59
+ topic = "foobar"
60
+ message = "message"
61
+ Propono.stubs(:listen_to_tcp).yields(topic, message)
62
+ Publisher.expects(:publish).with(topic, message, {})
63
+ Propono.proxy_tcp
64
+ end
47
65
  end
48
66
  end
@@ -122,6 +122,52 @@ module Propono
122
122
  end
123
123
  end
124
124
 
125
+ def test_tcp_uses_correct_message
126
+ Propono.config.tcp_host = "http://meducation.net"
127
+ Propono.config.tcp_port = 1234
128
+ topic_id = "my-fav-topic"
129
+ message = "foobar"
130
+ payload = {topic: topic_id, message: message}.to_json
131
+
132
+ socket = mock()
133
+ socket.expects(:write).with(payload)
134
+ socket.expects(:close)
135
+ TCPSocket.stubs(new: socket)
136
+
137
+ publisher = Publisher.new(topic_id, message)
138
+ publisher.send(:publish_via_tcp)
139
+ end
140
+
141
+ def test_tcp_uses_correct_message_host_and_port
142
+ host = "http://meducation.net"
143
+ port = 1234
144
+ Propono.config.tcp_host = host
145
+ Propono.config.tcp_port = port
146
+ topic_id = "my-fav-topic"
147
+ message = "foobar"
148
+ TCPSocket.expects(:new).with(host, port)
149
+
150
+ publisher = Publisher.new(topic_id, message)
151
+ publisher.send(:publish_via_tcp)
152
+ end
153
+
154
+ def test_exception_from_tcpsocket_caught_and_logged
155
+ host = "http://meducation.net"
156
+ port = 1234
157
+ Propono.config.tcp_host = host
158
+ Propono.config.tcp_port = port
159
+
160
+ client = Publisher.new("topic_id", "message")
161
+ Propono.config.logger.expects(:error).with() {|x| x =~ /^Propono failed to send : getaddrinfo:.*/}
162
+ client.send(:publish_via_tcp)
163
+ end
164
+
165
+ def test_publish_should_raise_exception_if_topic_is_nil
166
+ assert_raises(PublisherError, "Topic is nil") do
167
+ Publisher.publish(nil, "foobar")
168
+ end
169
+ end
170
+
125
171
  def test_publish_should_raise_exception_if_message_is_nil
126
172
  assert_raises(PublisherError, "Message is nil") do
127
173
  Publisher.publish("foobar", nil)
@@ -0,0 +1,63 @@
1
+ require File.expand_path('../../test_helper', __FILE__)
2
+
3
+ module Propono
4
+ class TcpListenerTest < Minitest::Test
5
+
6
+ def test_intialize_sets_locals
7
+ block = Proc.new {}
8
+ listener = TcpListener.new(&block)
9
+ assert_equal block, listener.instance_variable_get("@processor")
10
+ end
11
+
12
+ def test_server_is_setup_correctly
13
+ port = 1234
14
+ Propono.config.tcp_port = port
15
+
16
+ TCPServer.expects(:open).with(port)
17
+
18
+ listener = TcpListener.new() {}
19
+ server = listener.send(:server)
20
+ end
21
+
22
+ def test_initialize_should_fail_without_a_block
23
+ assert_raises(TcpListenerError) do
24
+ TcpListener.new
25
+ end
26
+ end
27
+
28
+ def test_message_is_processed
29
+ tcp_msg = "Foobar"
30
+ processor = Proc.new {}
31
+ listener = TcpListener.new(&processor)
32
+ client = mock()
33
+ client.expects(:recvfrom => [tcp_msg])
34
+ client.expects(:close)
35
+
36
+ server = mock()
37
+ server.expects(accept: client)
38
+
39
+ listener.stubs(server: server)
40
+ listener.expects(:process_tcp_data).with(tcp_msg)
41
+ thread = listener.send(:receive_and_process)
42
+ thread.join
43
+ end
44
+
45
+ def test_processor_is_called_correctly
46
+ topic = "my-topic"
47
+ message = "my-message"
48
+ processor = Proc.new {}
49
+ tcp_data = {topic: topic, message: message}.to_json
50
+ processor.expects(:call).with(topic, message)
51
+
52
+ listener = TcpListener.new(&processor)
53
+ listener.send(:process_tcp_data, tcp_data)
54
+ end
55
+
56
+ def test_listen_should_loop
57
+ listener = TcpListener.new {}
58
+ listener.expects(:loop)
59
+ listener.listen
60
+ end
61
+ end
62
+ end
63
+
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: propono
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.3
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - MalcyL
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-10-20 00:00:00.000000000 Z
12
+ date: 2013-10-23 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: fog
@@ -43,42 +43,42 @@ dependencies:
43
43
  name: rake
44
44
  requirement: !ruby/object:Gem::Requirement
45
45
  requirements:
46
- - - '>='
46
+ - - ! '>='
47
47
  - !ruby/object:Gem::Version
48
48
  version: '0'
49
49
  type: :development
50
50
  prerelease: false
51
51
  version_requirements: !ruby/object:Gem::Requirement
52
52
  requirements:
53
- - - '>='
53
+ - - ! '>='
54
54
  - !ruby/object:Gem::Version
55
55
  version: '0'
56
56
  - !ruby/object:Gem::Dependency
57
57
  name: mocha
58
58
  requirement: !ruby/object:Gem::Requirement
59
59
  requirements:
60
- - - '>='
60
+ - - ! '>='
61
61
  - !ruby/object:Gem::Version
62
62
  version: '0'
63
63
  type: :development
64
64
  prerelease: false
65
65
  version_requirements: !ruby/object:Gem::Requirement
66
66
  requirements:
67
- - - '>='
67
+ - - ! '>='
68
68
  - !ruby/object:Gem::Version
69
69
  version: '0'
70
70
  - !ruby/object:Gem::Dependency
71
71
  name: yard
72
72
  requirement: !ruby/object:Gem::Requirement
73
73
  requirements:
74
- - - '>='
74
+ - - ! '>='
75
75
  - !ruby/object:Gem::Version
76
76
  version: '0'
77
77
  type: :development
78
78
  prerelease: false
79
79
  version_requirements: !ruby/object:Gem::Requirement
80
80
  requirements:
81
- - - '>='
81
+ - - ! '>='
82
82
  - !ruby/object:Gem::Version
83
83
  version: '0'
84
84
  - !ruby/object:Gem::Dependency
@@ -125,6 +125,7 @@ files:
125
125
  - lib/propono/services/queue_creator.rb
126
126
  - lib/propono/services/queue_listener.rb
127
127
  - lib/propono/services/subscriber.rb
128
+ - lib/propono/services/tcp_listener.rb
128
129
  - lib/propono/services/topic_creator.rb
129
130
  - lib/propono/services/udp_listener.rb
130
131
  - lib/propono/version.rb
@@ -139,6 +140,7 @@ files:
139
140
  - test/configuration_test.rb
140
141
  - test/integration/integration_test.rb
141
142
  - test/integration/sns_to_sqs_test.rb
143
+ - test/integration/tcp_to_sqs_test.rb
142
144
  - test/integration/udp_proxy_test.rb
143
145
  - test/integration/udp_to_sqs_test.rb
144
146
  - test/logger_test.rb
@@ -147,6 +149,7 @@ files:
147
149
  - test/services/queue_creator_test.rb
148
150
  - test/services/queue_listener_test.rb
149
151
  - test/services/subscriber_test.rb
152
+ - test/services/tcp_listener_test.rb
150
153
  - test/services/topic_creator_test.rb
151
154
  - test/services/udp_listener_test.rb
152
155
  - test/test_helper.rb
@@ -160,12 +163,12 @@ require_paths:
160
163
  - lib
161
164
  required_ruby_version: !ruby/object:Gem::Requirement
162
165
  requirements:
163
- - - '>='
166
+ - - ! '>='
164
167
  - !ruby/object:Gem::Version
165
168
  version: '0'
166
169
  required_rubygems_version: !ruby/object:Gem::Requirement
167
170
  requirements:
168
- - - '>='
171
+ - - ! '>='
169
172
  - !ruby/object:Gem::Version
170
173
  version: '0'
171
174
  requirements: []
@@ -185,6 +188,7 @@ test_files:
185
188
  - test/configuration_test.rb
186
189
  - test/integration/integration_test.rb
187
190
  - test/integration/sns_to_sqs_test.rb
191
+ - test/integration/tcp_to_sqs_test.rb
188
192
  - test/integration/udp_proxy_test.rb
189
193
  - test/integration/udp_to_sqs_test.rb
190
194
  - test/logger_test.rb
@@ -193,6 +197,7 @@ test_files:
193
197
  - test/services/queue_creator_test.rb
194
198
  - test/services/queue_listener_test.rb
195
199
  - test/services/subscriber_test.rb
200
+ - test/services/tcp_listener_test.rb
196
201
  - test/services/topic_creator_test.rb
197
202
  - test/services/udp_listener_test.rb
198
203
  - test/test_helper.rb