noam_lemma 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,78 @@
1
+ require 'thread'
2
+
3
+ module Noam
4
+ class NoamPlayerException < Exception; end
5
+
6
+ class Player
7
+ def initialize(remote_host, remote_port)
8
+ begin
9
+ @socket = TCPSocket.new(remote_host, remote_port)
10
+ rescue Errno::ECONNREFUSED
11
+ raise NoamPlayerException.new("Unable to connect to the Noam server at #{remote_host}:#{remote_port}. Is it running?")
12
+ end
13
+
14
+ @queue = Queue.new
15
+ manage_queue_on_thread
16
+ end
17
+
18
+ def put(message)
19
+ @queue.push(message)
20
+ end
21
+
22
+ def stop
23
+ put(:soft_exit)
24
+ @thread.join
25
+ end
26
+
27
+ def stop!
28
+ put(:hard_exit)
29
+ @thread.join
30
+ end
31
+
32
+ private
33
+
34
+ def manage_queue_on_thread
35
+ @thread = Thread.new do |t|
36
+ begin
37
+ loop do
38
+ message = @queue.pop
39
+ break if exit?(message)
40
+ process(message)
41
+ end
42
+ ensure
43
+ @socket.close
44
+ end
45
+ end
46
+ end
47
+
48
+ def process(message)
49
+ case message
50
+ when :soft_exit
51
+ finish_queue
52
+ when :hard_exit
53
+ else
54
+ @socket.print(message.noam_encode)
55
+ @socket.flush
56
+ end
57
+ end
58
+
59
+ def exit?(message)
60
+ message == :hard_exit || message == :soft_exit
61
+ end
62
+
63
+ def finish_queue
64
+ queue_to_array.each do |message|
65
+ @socket.print(message.noam_encode)
66
+ @socket.flush
67
+ end
68
+ end
69
+
70
+ def queue_to_array
71
+ result = []
72
+ while(@queue.size > 0) do
73
+ result << @queue.pop
74
+ end
75
+ result
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,3 @@
1
+ module NoamLemma
2
+ VERSION = "0.2.1"
3
+ end
data/lib/noam_lemma.rb ADDED
@@ -0,0 +1,16 @@
1
+ Thread.abort_on_exception = true
2
+
3
+ module Noam
4
+ BEACON_PORT = 1030
5
+
6
+ VERSION = '0.2.1'
7
+ DEVICE_TYPE = 'ruby-script'
8
+
9
+ class NoamThreadCancelled < Exception; end
10
+ end
11
+
12
+ require 'noam_lemma/beacon'
13
+ require 'noam_lemma/lemma'
14
+ require 'noam_lemma/listener'
15
+ require 'noam_lemma/message'
16
+ require 'noam_lemma/player'
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'noam_lemma/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "noam_lemma"
8
+ spec.version = NoamLemma::VERSION
9
+ spec.authors = ["John Van Enk"]
10
+ spec.email = ["vanenkj@gmail.com"]
11
+ spec.description = %q{A lemma factory for the Noam pub-sub system.}
12
+ spec.summary = %q{A lemma factory for the Noam pub-sub system.}
13
+ spec.homepage = "https://github.com/noam-io/lemma-ruby"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "require_all"
22
+ spec.add_development_dependency "bundler", "~> 1.3"
23
+ spec.add_development_dependency "rake", "~> 10.3"
24
+ spec.add_development_dependency "rspec", "~> 2.14"
25
+ spec.add_development_dependency "mocha", "~> 1.1"
26
+ end
@@ -0,0 +1,26 @@
1
+ describe Noam::Beacon do
2
+ describe "#new" do
3
+ it "creates a new beacon" do
4
+ beacon = Noam::Beacon.new(:name, :host, :noam)
5
+ beacon.name.should == :name
6
+ beacon.host.should == :host
7
+ beacon.port.should == :noam
8
+ end
9
+ end
10
+
11
+ describe "::discover" do
12
+ before do
13
+ FakeManager.start
14
+ end
15
+
16
+ after do
17
+ FakeManager.stop
18
+ end
19
+
20
+ it "creates a Beacon based on server beacons" do
21
+ beacon = Noam::Beacon.discover
22
+ beacon.should be_a(Noam::Beacon)
23
+ beacon.port.should == NoamTest::FakeServer::PORT
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,88 @@
1
+ describe Noam::Lemma do
2
+ SERVER_DELAY = 0.05
3
+
4
+ before(:each) do
5
+ FakeManager.start
6
+ @server = FakeManager.server
7
+ @lemma = Noam::Lemma.new("Example Lemma", ["event1"], ["event1"])
8
+ @lemma.discover
9
+ sleep(SERVER_DELAY)
10
+ end
11
+
12
+ after do
13
+ @lemma.stop
14
+ FakeManager.stop
15
+ end
16
+
17
+ describe "#new" do
18
+ context "with provided arguments" do
19
+ let(:lemma) { Noam::Lemma.new("Example Lemma", ["event1"], ["event1"]) }
20
+
21
+ it "sets #name to the given name" do
22
+ lemma.name.should == "Example Lemma"
23
+ end
24
+
25
+ it "sets #hears to the given hears" do
26
+ lemma.hears.should == ["event1"]
27
+ end
28
+
29
+ it "sets #speaks to the given speaks" do
30
+ lemma.speaks.should == ["event1"]
31
+ end
32
+ end
33
+
34
+ context "with default arguments" do
35
+ let(:lemma) { Noam::Lemma.new("Example Lemma") }
36
+
37
+ it "sets #hears to an empty array" do
38
+ lemma.hears.should == []
39
+ end
40
+
41
+ it "sets #speaks to an empty array" do
42
+ lemma.speaks.should == []
43
+ end
44
+ end
45
+ end
46
+
47
+ describe "#hears" do
48
+ let(:lemma) { Noam::Lemma.new("Example Lemma", ["example_event"]) }
49
+
50
+ it "delegates to the MessageFilter when set" do
51
+ message_filter = Noam::MessageFilter.new
52
+ message_filter.hear("sample_event") {}
53
+ lemma.set_message_filter(message_filter)
54
+ lemma.hears.should == message_filter.hears
55
+ end
56
+ end
57
+
58
+ describe "#discover" do
59
+ it "sends a registration message" do
60
+ @server.clients.length.should == 1
61
+ @server.clients.first.port.should be_an(Integer)
62
+ @server.clients.first.port.should_not == 0
63
+ end
64
+
65
+ it "initializes listener and player" do
66
+ @lemma.listener.should_not be_nil
67
+ @lemma.player.should_not be_nil
68
+ end
69
+ end
70
+
71
+ describe "#speak" do
72
+ it "sends an event to the server" do
73
+ @lemma.speak("an event", "some value")
74
+ sleep(SERVER_DELAY)
75
+ @server.messages.map{|m| m[2]}.include?("an event").should be_true
76
+ end
77
+ end
78
+
79
+ describe "#listen" do
80
+ it "returns a message from the server" do
81
+ @server.send_message(["event", "test-server", "event1", "noam event"])
82
+ message = @lemma.listen
83
+ message.source.should == "test-server"
84
+ message.event.should == "event1"
85
+ message.value.should == "noam event"
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,19 @@
1
+ describe Noam::Message::Heard do
2
+ describe "#new" do
3
+ it "creates a new Heard message" do
4
+ h = Noam::Message::Heard.new("source", "event", "value")
5
+ h.source.should == "source"
6
+ h.event.should == "event"
7
+ h.value.should == "value"
8
+ end
9
+ end
10
+
11
+ describe "::from_noam" do
12
+ it "ceates a new Heard message from the noam event structure" do
13
+ h = Noam::Message::Heard.from_noam(["event", "source", "event", "value"].to_json)
14
+ h.source.should == "source"
15
+ h.event.should == "event"
16
+ h.value.should == "value"
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ describe Noam::Message::Playable do
2
+ describe "#new" do
3
+ it "can be built" do
4
+ Noam::Message::Playable.new(
5
+ :host, :event, :value
6
+ ).class.should == Noam::Message::Playable
7
+ end
8
+ end
9
+
10
+ describe "#noam_encode" do
11
+ it "encodes the Playable" do
12
+ Noam::Message::Playable.new(
13
+ "host", "event", "value"
14
+ ).noam_encode.should == '000032["event","host","event","value"]'
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ describe Noam::Message::Register do
2
+ describe "#new" do
3
+ it "creates a new Register object" do
4
+ message = Noam::Message::Register.new(:devid, :port, :hears, :speaks)
5
+ message.should be_a(Noam::Message::Register)
6
+ end
7
+ end
8
+
9
+ describe "#noam_encode" do
10
+ it "encodes the Register message" do
11
+ message = Noam::Message::Register.new("an_id", 1234, ["e1"], ["e2", "e3"]).noam_encode
12
+ expected = '000066["register","an_id",1234,["e1"],["e2","e3"],"' +
13
+ Noam::DEVICE_TYPE + '","' + Noam::VERSION + '"]'
14
+ message.should == expected
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,68 @@
1
+ describe Noam::MessageFilter do
2
+ let(:filter) { Noam::MessageFilter.new }
3
+ let(:message) { stub_message("example_event") }
4
+
5
+ describe "#hear" do
6
+ it "registers event names with the filter" do
7
+ filter.hear("example_event") {}
8
+ filter.hear("sample_event") {}
9
+ filter.hears.should == ["example_event", "sample_event"]
10
+ end
11
+ end
12
+
13
+ describe "#hears" do
14
+ it "includes single entries for event names registered multiple times" do
15
+ filter.hear("example_event") {}
16
+ filter.hear("example_event") {}
17
+ filter.hears.should == ["example_event"]
18
+ end
19
+ end
20
+
21
+ describe "#receive" do
22
+ it "calls blocks associated with the given event name" do
23
+ messages_received = 0
24
+ filter.hear("example_event") {|message| messages_received += 1}
25
+ filter.receive(message)
26
+ messages_received.should == 1
27
+ end
28
+
29
+ it "calls multiple blocks associated with the given event name" do
30
+ message_one = nil, message_two = nil
31
+ filter.hear("example_event") {|message| message_one = message}
32
+ filter.hear("example_event") {|message| message_two = message}
33
+ filter.receive(message)
34
+ message_one.should be(message)
35
+ message_two.should be(message)
36
+ end
37
+
38
+ it "ignores blocks associated with other event names" do
39
+ example_received = nil, sample_received = nil
40
+ filter.hear("example_event") {|message| example_received = true}
41
+ filter.hear("sample_event") {|message| sample_received = true}
42
+ filter.receive(stub_message("example_event"))
43
+ example_received.should be_true
44
+ sample_received.should be_false
45
+ end
46
+
47
+ it "returns the given message" do
48
+ message = stub_message("example_event")
49
+ result = filter.receive(message)
50
+ result.should be(message)
51
+ end
52
+
53
+ it "ignores event names with no associations" do
54
+ message = stub_message("example_event")
55
+ lambda { filter.receive(message) }.should_not raise_error
56
+ end
57
+
58
+ it "ignores event names with empty blocks" do
59
+ message = stub_message("example_event")
60
+ filter.hear("example_event") {}
61
+ lambda { filter.receive(message) }.should_not raise_error
62
+ end
63
+ end
64
+
65
+ def stub_message(event_name, value = "")
66
+ stub("message", event: event_name, value: value)
67
+ end
68
+ end
@@ -0,0 +1,8 @@
1
+ describe Noam::Message do
2
+ describe "::encode_length" do
3
+ it "expands the length out to 6 digits" do
4
+ Noam::Message.encode_length(6).should == "000006"
5
+ Noam::Message.encode_length(123456).should == "123456"
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,30 @@
1
+ require 'require_all'
2
+
3
+ libs = Dir[File.dirname(File.expand_path(__FILE__)) + "/../lib/**/*.rb"]
4
+ require_all(libs)
5
+
6
+ support_libs = Dir[File.dirname(File.expand_path(__FILE__)) + "/support/**/*.rb"]
7
+ require_all(support_libs)
8
+
9
+ class FakeManager
10
+ def self.start
11
+ @@beacon = NoamTest::FakeBeacon.new(Noam::BEACON_PORT)
12
+ @@beacon.start
13
+
14
+ @@server = NoamTest::FakeServer.new
15
+ @@server.start
16
+ end
17
+
18
+ def self.stop
19
+ @@beacon.stop
20
+ @@server.stop
21
+ end
22
+
23
+ def self.server
24
+ @@server
25
+ end
26
+ end
27
+
28
+ RSpec.configure do |config|
29
+ config.mock_framework = :mocha
30
+ end
@@ -0,0 +1,187 @@
1
+ require 'json'
2
+ require 'socket'
3
+
4
+ class FakeBeaconThreadCancelled < Exception; end
5
+ class FakeServerThreadCancelled < Exception; end
6
+
7
+ class UnexpectedRegisterText < Exception; end
8
+ class UnexpectedSystemVersion < Exception; end
9
+
10
+ module NoamTest
11
+ class FakeBeacon
12
+ LOOP_DELAY = 0.001
13
+
14
+ def initialize(udp_broadcast_port)
15
+ @socket = UDPSocket.new
16
+ @socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_BROADCAST, true)
17
+ end
18
+
19
+ def start
20
+ @thread = Thread.new do |t|
21
+ begin
22
+ loop do
23
+ msg = ["beacon", "fake_beacon", NoamTest::FakeServer::PORT].to_json
24
+ @socket.send(msg, 0, "255.255.255.255", Noam::BEACON_PORT)
25
+
26
+ # This is normally at 5.0 seconds, but we run faster in order to
27
+ # make tests faster.
28
+ sleep(LOOP_DELAY)
29
+ end
30
+ rescue FakeBeaconThreadCancelled
31
+ # going down
32
+ ensure
33
+ @socket.close
34
+ end
35
+ end
36
+ end
37
+
38
+ def stop
39
+ @thread.raise(FakeBeaconThreadCancelled)
40
+ @thread.join
41
+ @thread = nil
42
+ end
43
+ end
44
+
45
+ class FakeServer
46
+ PORT = 7733
47
+
48
+ def initialize
49
+ @sock = TCPServer.new(FakeServer::PORT)
50
+ end
51
+
52
+ def start
53
+ @clients = []
54
+ @thread = Thread.new do |t|
55
+ begin
56
+ loop do
57
+ s = @sock.accept
58
+ @clients << (c = Client.new(s))
59
+ c.start
60
+ end
61
+ rescue FakeServerThreadCancelled
62
+ # going down
63
+ ensure
64
+ @sock.close
65
+ end
66
+ end
67
+ end
68
+
69
+ def stop
70
+ @thread.raise(FakeServerThreadCancelled)
71
+ @thread.join
72
+ @thread = nil
73
+
74
+ @clients.each do |c|
75
+ c.stop
76
+ end
77
+ @clients = nil
78
+ end
79
+
80
+ def clients
81
+ @clients.reject {|c| c.closed}
82
+ end
83
+
84
+ def messages
85
+ clients.map {|c| c.messages}.flatten(1)
86
+ end
87
+
88
+ def send_message(m)
89
+ s = m.to_json
90
+ l = "%06u" % s.length
91
+
92
+ msg = l + s
93
+
94
+ clients.each do |c|
95
+ c.send_message(msg)
96
+ end
97
+ end
98
+ end
99
+
100
+ class Client
101
+ attr_reader :closed, :responder, :port, :hears, :speaks
102
+
103
+ def initialize(client_socket)
104
+ @sock = client_socket
105
+ @client_host = @sock.peeraddr[2]
106
+ @queue = Queue.new
107
+ end
108
+
109
+ def start
110
+ @thread = Thread.new do |t|
111
+ begin
112
+ read_register_msg
113
+ @responder = ClientResponder.new(@client_host, @port)
114
+
115
+ loop do
116
+ # Ignoring bad message.
117
+ #
118
+ # It seems the order in which sockets get shut down are a little
119
+ # weird. The following two checks give us a means to try and bail
120
+ # out in the circumstance that a socket dies. In that case, it
121
+ # *should* have been shut down and we're probably just spinning
122
+ # until the exception finally bubbles up.
123
+ if (len = @sock.read(6)) == ""
124
+ next
125
+ end
126
+ if (str = @sock.read(len.to_i)) == ""
127
+ next
128
+ end
129
+
130
+ msg = JSON.parse(str)
131
+ @queue.push(msg)
132
+ end
133
+ rescue FakeServerThreadCancelled
134
+ # going down
135
+ @closed = true
136
+ @responder.stop
137
+ ensure
138
+ @sock.close
139
+ end
140
+ end
141
+ end
142
+
143
+ def stop
144
+ @thread.raise(FakeServerThreadCancelled)
145
+ @thread.join
146
+ end
147
+
148
+ def messages
149
+ @queue.length.times.map { @queue.pop }
150
+ end
151
+
152
+ def send_message(m)
153
+ @responder.send_message(m)
154
+ end
155
+
156
+ private
157
+
158
+ def read_register_msg
159
+ len = @sock.read(6)
160
+ m = JSON.parse(@sock.read(len.to_i))
161
+ txt, _, @port, @hears, @speaks, _, ver = m
162
+
163
+ unless txt == "register"
164
+ raise UnexpectedRegisterText.new(txt)
165
+ end
166
+
167
+ unless ver == Noam::VERSION
168
+ raise UnexpectedSystemVersion.new(ver)
169
+ end
170
+ end
171
+ end
172
+
173
+ class ClientResponder
174
+ def initialize(host, port)
175
+ @sock = TCPSocket.new(host, port)
176
+ end
177
+
178
+ def send_message(m)
179
+ @sock.write(m)
180
+ @sock.flush
181
+ end
182
+
183
+ def stop
184
+ @sock.close
185
+ end
186
+ end
187
+ end