noam_lemma 0.2.1

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.
@@ -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