kamerling 0.0.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.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +50 -0
  3. data/.ruby-version +1 -0
  4. data/Gemfile +2 -0
  5. data/Gemfile.lock +90 -0
  6. data/LICENCE +661 -0
  7. data/README.md +6 -0
  8. data/Rakefile +18 -0
  9. data/bin/kamerling +9 -0
  10. data/config/reek.yml +18 -0
  11. data/kamerling.gemspec +30 -0
  12. data/lib/kamerling/addr.rb +18 -0
  13. data/lib/kamerling/client.rb +2 -0
  14. data/lib/kamerling/core_extensions/main.rb +17 -0
  15. data/lib/kamerling/core_extensions.rb +1 -0
  16. data/lib/kamerling/handler.rb +24 -0
  17. data/lib/kamerling/http_api.rb +38 -0
  18. data/lib/kamerling/logging.rb +20 -0
  19. data/lib/kamerling/message.rb +47 -0
  20. data/lib/kamerling/migrations/1_basic_schema.rb +45 -0
  21. data/lib/kamerling/net_dispatcher.rb +8 -0
  22. data/lib/kamerling/project.rb +2 -0
  23. data/lib/kamerling/receiver.rb +11 -0
  24. data/lib/kamerling/registrar.rb +9 -0
  25. data/lib/kamerling/registration.rb +2 -0
  26. data/lib/kamerling/repo.rb +32 -0
  27. data/lib/kamerling/repos.rb +65 -0
  28. data/lib/kamerling/result.rb +2 -0
  29. data/lib/kamerling/server/http.rb +26 -0
  30. data/lib/kamerling/server/sock.rb +32 -0
  31. data/lib/kamerling/server/tcp.rb +19 -0
  32. data/lib/kamerling/server/udp.rb +20 -0
  33. data/lib/kamerling/server_runner.rb +50 -0
  34. data/lib/kamerling/task.rb +2 -0
  35. data/lib/kamerling/task_dispatcher.rb +29 -0
  36. data/lib/kamerling/uuid.rb +17 -0
  37. data/lib/kamerling/uuid_object.rb +79 -0
  38. data/lib/kamerling/views/clients.slim +6 -0
  39. data/lib/kamerling/views/layout.slim +6 -0
  40. data/lib/kamerling/views/project.slim +11 -0
  41. data/lib/kamerling/views/projects.slim +9 -0
  42. data/lib/kamerling/views/root.slim +6 -0
  43. data/lib/kamerling.rb +29 -0
  44. data/spec/kamerling/addr_spec.rb +27 -0
  45. data/spec/kamerling/client_spec.rb +9 -0
  46. data/spec/kamerling/core_extensions/main_spec.rb +18 -0
  47. data/spec/kamerling/handler_spec.rb +30 -0
  48. data/spec/kamerling/http_api_spec.rb +93 -0
  49. data/spec/kamerling/logging_spec.rb +73 -0
  50. data/spec/kamerling/message_spec.rb +70 -0
  51. data/spec/kamerling/net_dispatcher_spec.rb +21 -0
  52. data/spec/kamerling/receiver_spec.rb +22 -0
  53. data/spec/kamerling/registrar_spec.rb +16 -0
  54. data/spec/kamerling/repo_spec.rb +60 -0
  55. data/spec/kamerling/repos_spec.rb +136 -0
  56. data/spec/kamerling/server/http_spec.rb +22 -0
  57. data/spec/kamerling/server/tcp_spec.rb +46 -0
  58. data/spec/kamerling/server/udp_spec.rb +44 -0
  59. data/spec/kamerling/server_runner_spec.rb +65 -0
  60. data/spec/kamerling/task_dispatcher_spec.rb +23 -0
  61. data/spec/kamerling/task_spec.rb +9 -0
  62. data/spec/kamerling/uuid_object_spec.rb +101 -0
  63. data/spec/kamerling/uuid_spec.rb +24 -0
  64. data/spec/spec_helper.rb +26 -0
  65. metadata +325 -0
@@ -0,0 +1,79 @@
1
+ module Kamerling
2
+ def self.UUIDObject *params
3
+ class_definition_from attrs_from params
4
+ end
5
+
6
+ private
7
+
8
+ def self.attrs_from params
9
+ { uuid: -> { UUID.new } }.tap do |attrs|
10
+ attrs.merge! params.pop if params.last.is_a? Hash
11
+ attrs.merge! raises_from params
12
+ end
13
+ end
14
+
15
+ def self.class_definition_from attrs
16
+ Class.new do
17
+ define_singleton_method(:attrs) { attrs }
18
+
19
+ def self.from_h hash, repos: Repos
20
+ args = hash.reduce({}) do |result, (key, _)|
21
+ result.merge from_h_mapping hash, key, repos
22
+ end
23
+ new args
24
+ end
25
+
26
+ def initialize args = {}
27
+ @values = Hash[self.class.attrs.map do |attr, default|
28
+ value = args.fetch attr do
29
+ default.respond_to?(:call) ? default.call : default
30
+ end
31
+ [attr, value]
32
+ end]
33
+ end
34
+
35
+ def == other
36
+ uuid == other.uuid
37
+ end
38
+
39
+ attrs.each do |attr, _|
40
+ define_method(attr) { @values[attr] }
41
+ define_method("#{attr}=") { |val| @values[attr] = val }
42
+ end
43
+
44
+ def to_h
45
+ self.class.attrs.reduce({}) do |hash, (attr, _)|
46
+ hash.merge to_h_mapping attr
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def self.from_h_mapping hash, key, repos
53
+ case key
54
+ when :host, :port, :prot
55
+ { addr: Addr[hash[:host], hash[:port], hash[:prot].to_sym] }
56
+ when :client_uuid then { client: repos[Client][hash[key]] }
57
+ when :project_uuid then { project: repos[Project][hash[key]] }
58
+ when :task_uuid then { task: repos[Task][hash[key]] }
59
+ else { key => hash[key] }
60
+ end
61
+ end
62
+
63
+ def to_h_mapping attr
64
+ case value = @values[attr]
65
+ when Addr
66
+ { host: value.host, port: value.port, prot: value.prot.to_s }
67
+ when Client then { client_uuid: client.uuid }
68
+ when Project then { project_uuid: project.uuid }
69
+ when Task then { task_uuid: task.uuid }
70
+ else { attr => value }
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ def self.raises_from params
77
+ Hash[params.map { |param| [param, -> { raise "#{param} required" }] }]
78
+ end
79
+ end
@@ -0,0 +1,6 @@
1
+ nav
2
+ ul#clients
3
+ - clients.each do |client|
4
+ li
5
+ a data-uuid=client.uuid href="/clients/#{client.uuid}" rel='client'
6
+ = client.busy
@@ -0,0 +1,6 @@
1
+ doctype html
2
+ html
3
+ head
4
+ title Kamerling
5
+ body
6
+ == yield
@@ -0,0 +1,11 @@
1
+ nav
2
+ ul#clients
3
+ - clients.each do |client|
4
+ li
5
+ a data-busy=client.busy.to_s data-uuid=client.uuid href="/clients/#{client.uuid}" rel='client'
6
+ = client.busy
7
+ ul#tasks
8
+ - tasks.each do |task|
9
+ li
10
+ a data-done=task.done.to_s data-uuid=task.uuid href="/tasks/#{task.uuid}" rel='task'
11
+ = task.done
@@ -0,0 +1,9 @@
1
+ nav
2
+ ul#projects
3
+ - projects.each do |project|
4
+ li
5
+ a data-uuid=project.uuid href="/projects/#{project.uuid}" rel='project'
6
+ = project.name
7
+
8
+ form action='/projects' method='POST'
9
+ input name='name'
@@ -0,0 +1,6 @@
1
+ nav
2
+ ul
3
+ li
4
+ a#clients href='/clients' clients
5
+ li
6
+ a#projects href='/projects' projects
data/lib/kamerling.rb ADDED
@@ -0,0 +1,29 @@
1
+ require 'socket'
2
+
3
+ require_relative 'kamerling/core_extensions'
4
+
5
+ include Kamerling::CoreExtensions::Main
6
+
7
+ require_relative 'kamerling/addr'
8
+ require_relative 'kamerling/handler'
9
+ require_relative 'kamerling/message'
10
+ require_relative 'kamerling/net_dispatcher'
11
+ require_relative 'kamerling/receiver'
12
+ require_relative 'kamerling/registrar'
13
+ require_relative 'kamerling/repo'
14
+ require_relative 'kamerling/repos'
15
+ require_relative 'kamerling/http_api'
16
+ require_relative 'kamerling/server/http'
17
+ require_relative 'kamerling/server/sock'
18
+ require_relative 'kamerling/server/tcp'
19
+ require_relative 'kamerling/server/udp'
20
+ require_relative 'kamerling/server_runner'
21
+ require_relative 'kamerling/task_dispatcher'
22
+ require_relative 'kamerling/uuid'
23
+ require_relative 'kamerling/uuid_object'
24
+ require_relative 'kamerling/client'
25
+ require_relative 'kamerling/project'
26
+ require_relative 'kamerling/registration'
27
+ require_relative 'kamerling/result'
28
+ require_relative 'kamerling/task'
29
+ require_relative 'kamerling/logging'
@@ -0,0 +1,27 @@
1
+ require_relative '../spec_helper'
2
+
3
+ module Kamerling describe Addr do
4
+ let(:addr) { Addr['localhost', 1981, :TCP] }
5
+
6
+ describe '#connectable?' do
7
+ it 'is a predicate whether the (TCP) address is connectable' do
8
+ server = TCPServer.new(*addr)
9
+ addr.must_be :connectable?
10
+ server.close
11
+ addr.wont_be :connectable?
12
+ end
13
+ end
14
+
15
+ describe '#to_a' do
16
+ it 'returns host + port for splat use' do
17
+ splat = *addr
18
+ splat.must_equal ['localhost', 1981]
19
+ end
20
+ end
21
+
22
+ describe '#to_s' do
23
+ it 'returns the Addr in ‘host:port (protocol)’ notation' do
24
+ addr.to_s.must_equal 'localhost:1981 (TCP)'
25
+ end
26
+ end
27
+ end end
@@ -0,0 +1,9 @@
1
+ require_relative '../spec_helper'
2
+
3
+ module Kamerling describe Client do
4
+ describe '#busy' do
5
+ it 'defaults to false' do
6
+ Client.new(addr: fake(:addr)).busy.must_equal false
7
+ end
8
+ end
9
+ end end
@@ -0,0 +1,18 @@
1
+ require_relative '../../spec_helper'
2
+
3
+ module Kamerling describe CoreExtensions::Main do
4
+ describe '#req' do
5
+ it 'raises a RuntimeError that a parameter is required' do
6
+ -> { CoreExtensions::Main.req(:foo) }.must_raise(RuntimeError)
7
+ .message.must_include 'param foo is required'
8
+ end
9
+ end
10
+
11
+ describe '#warn_off' do
12
+ it 'turns $VERBOSE off inside the block' do
13
+ assert $VERBOSE
14
+ CoreExtensions::Main.warn_off { refute $VERBOSE }
15
+ assert $VERBOSE
16
+ end
17
+ end
18
+ end end
@@ -0,0 +1,30 @@
1
+ require_relative '../spec_helper'
2
+
3
+ module Kamerling describe Handler do
4
+ describe '#handle' do
5
+ fakes :addr, :receiver, :registrar
6
+ let(:handler) { Handler.new receiver: receiver, registrar: registrar }
7
+
8
+ it 'handles RGST inputs' do
9
+ input = 'RGST' + "\0" * 12 + '16B client UUID16B project UUID'
10
+ handler.handle input, addr
11
+ registrar.must_have_received :register, [{ addr: addr,
12
+ client_uuid: UUID['16B client UUID'],
13
+ project_uuid: UUID['16B project UUID'] }]
14
+ end
15
+
16
+ it 'handles RSLT inputs' do
17
+ input = 'RSLT' + "\0" * 12
18
+ input << '16B client UUID16B project UUID16B task UUIDdata'
19
+ handler.handle input, addr
20
+ receiver.must_have_received :receive, [{ addr: addr,
21
+ client_uuid: UUID['16B client UUID'], data: 'data',
22
+ task_uuid: UUID['16B task UUID'] }]
23
+ end
24
+
25
+ it 'raises on unknown inputs' do
26
+ ex = -> { handler.handle 'MESS', addr }.must_raise Handler::UnknownInput
27
+ ex.message.must_equal 'MESS'
28
+ end
29
+ end
30
+ end end
@@ -0,0 +1,93 @@
1
+ require 'nokogiri'
2
+
3
+ require_relative '../spec_helper'
4
+
5
+ module Kamerling describe HTTPAPI do
6
+ let(:app) { HTTPAPI.set repos: repos }
7
+ let(:doc) { Nokogiri::HTML last_response.body }
8
+ let(:ecc) { fake :project, uuid: UUID.new }
9
+ let(:gimps) { fake :project, uuid: UUID.new }
10
+ let(:repos) { fake :repos, as: :class, projects: [gimps, ecc] }
11
+
12
+ describe 'GET /' do
13
+ it 'contains links to clients and projects' do
14
+ get '/'
15
+ doc.at('#clients')['href'].must_equal '/clients'
16
+ doc.at('#projects')['href'].must_equal '/projects'
17
+ end
18
+ end
19
+
20
+ describe 'GET /clients' do
21
+ it 'contains links to and UUIDs of clients' do
22
+ fpga = fake :client, uuid: UUID.new
23
+ stub(repos).clients { [fpga] }
24
+ get '/clients'
25
+ links = doc.css '#clients a[rel=client]'
26
+ links.size.must_equal 1
27
+ links.at("[data-uuid='#{fpga.uuid}']")['href']
28
+ .must_equal "/clients/#{fpga.uuid}"
29
+ end
30
+ end
31
+
32
+ describe 'GET /projects' do
33
+ it 'contains links to and UUIDs of projects' do
34
+ get '/projects'
35
+ links = doc.css '#projects a[rel=project]'
36
+ links.size.must_equal 2
37
+ links.at("[data-uuid='#{gimps.uuid}']")['href']
38
+ .must_equal "/projects/#{gimps.uuid}"
39
+ end
40
+ end
41
+
42
+ describe 'GET /projects/{uuid}' do
43
+ let(:cpu) { fake :client, busy: false, uuid: UUID.new }
44
+ let(:gpu) { fake :client, busy: true, uuid: UUID.new }
45
+ let(:three) { fake :task, done: false, uuid: UUID.new }
46
+ let(:seven) { fake :task, done: true, uuid: UUID.new }
47
+
48
+ before do
49
+ stub(repos).project(gimps.uuid) { gimps }
50
+ stub(repos).clients_for(gimps) { [cpu, gpu] }
51
+ stub(repos).tasks_for(gimps) { [three, seven] }
52
+ end
53
+
54
+ it 'contains links to and info on the project’s clients' do
55
+ get "/projects/#{gimps.uuid}"
56
+ links = doc.css '#clients a[rel=client]'
57
+ links.size.must_equal 2
58
+ links.at("[data-uuid='#{cpu.uuid}']")['href']
59
+ .must_equal "/clients/#{cpu.uuid}"
60
+ links.at("[data-uuid='#{cpu.uuid}']")['data-busy'].must_equal 'false'
61
+ links.at("[data-uuid='#{gpu.uuid}']")['data-busy'].must_equal 'true'
62
+ end
63
+
64
+ it 'contains links to and info on the project’s tasks' do
65
+ get "/projects/#{gimps.uuid}"
66
+ links = doc.css '#tasks a[rel=task]'
67
+ links.size.must_equal 2
68
+ links.at("[data-uuid='#{three.uuid}']")['href']
69
+ .must_equal "/tasks/#{three.uuid}"
70
+ links.at("[data-uuid='#{three.uuid}']")['data-done'].must_equal 'false'
71
+ links.at("[data-uuid='#{seven.uuid}']")['data-done'].must_equal 'true'
72
+ end
73
+ end
74
+
75
+ describe 'POST /projects' do
76
+ it 'creates a new project with the given name and UUID' do
77
+ post '/projects', name: 'ECC', uuid: uuid = UUID.new
78
+ repos.must_have_received :<<, [Project.new(name: 'ECC', uuid: uuid)]
79
+ end
80
+
81
+ it 'creates a new project with a random UUID if missing' do
82
+ post '/projects', name: 'ECC'
83
+ project = Project.new name: 'ECC', uuid: any(String)
84
+ repos.must_have_received :<<, [project]
85
+ end
86
+
87
+ it 'redirects to /projects' do
88
+ post '/projects', name: 'ECC'
89
+ follow_redirect!
90
+ URI(last_request.url).path.must_equal '/projects'
91
+ end
92
+ end
93
+ end end
@@ -0,0 +1,73 @@
1
+ require_relative '../spec_helper'
2
+
3
+ module Kamerling describe Logging do
4
+ let(:logged) { stream.tap(&:rewind).read }
5
+ let(:logger) { Logger.new stream }
6
+ let(:stream) { StringIO.new }
7
+ let(:tcp_server) { Server::TCP.new addr: Addr['localhost', 1981, :TCP] }
8
+ let(:udp_server) { Server::UDP.new addr: Addr['localhost', 1979, :UDP] }
9
+
10
+ before do
11
+ Logging.log_to logger: logger
12
+ tcp_server.start
13
+ udp_server.start
14
+ end
15
+
16
+ after do
17
+ tcp_server.stop
18
+ udp_server.stop
19
+ end
20
+
21
+ describe '.new' do
22
+ it 'logs TCP server starts' do
23
+ logged.must_include 'start localhost:1981 (TCP)'
24
+ end
25
+
26
+ it 'logs TCP server stops' do
27
+ tcp_server.stop
28
+ logged.must_include 'stop localhost:1981 (TCP)'
29
+ end
30
+
31
+ it 'logs TCP server connects' do
32
+ tcp_addr = TCPSocket.open(*tcp_server.addr) do |socket|
33
+ Addr[*socket.local_address.ip_unpack, :TCP]
34
+ end
35
+ run_all_threads
36
+ logged.must_include "connect #{tcp_addr}"
37
+ end
38
+
39
+ it 'logs TCP server receives' do
40
+ tcp_addr = TCPSocket.open(*tcp_server.addr) do |socket|
41
+ socket << 'PING'
42
+ Addr[*socket.local_address.ip_unpack, :TCP]
43
+ end
44
+ run_all_threads
45
+ logged.must_include "received #{tcp_addr} PING"
46
+ end
47
+
48
+ it 'logs UDP server starts' do
49
+ logged.must_include 'start localhost:1979 (UDP)'
50
+ end
51
+
52
+ it 'logs UDP server stops' do
53
+ udp_server.stop
54
+ logged.must_include 'stop localhost:1979 (UDP)'
55
+ end
56
+
57
+ it 'logs UDP server connects' do
58
+ udp_client = UDPSocket.new
59
+ udp_client.send 'PING', 0, *udp_server.addr
60
+ udp_addr = Addr['127.0.0.1', udp_client.addr[1], :UDP]
61
+ run_all_threads
62
+ logged.must_include "connect #{udp_addr}"
63
+ end
64
+
65
+ it 'logs TCP server receives' do
66
+ udp_client = UDPSocket.new
67
+ udp_client.send 'PING', 0, *udp_server.addr
68
+ udp_addr = Addr['127.0.0.1', udp_client.addr[1], :UDP]
69
+ run_all_threads
70
+ logged.must_include "received #{udp_addr} PING"
71
+ end
72
+ end
73
+ end end
@@ -0,0 +1,70 @@
1
+ require_relative '../spec_helper'
2
+
3
+ module Kamerling describe Message do
4
+ let(:mess) do
5
+ Message.new "DATA\0\0\0\0\0\0\0\0\0\0\0\0" \
6
+ '16B client UUID16B project UUID16B task UUIDsome payload'
7
+ end
8
+
9
+ describe '.[]' do
10
+ it 'constructs a new message' do
11
+ client = fake :client, uuid: UUID.new
12
+ project = fake :project, uuid: UUID.new
13
+ task = fake :task, uuid: UUID.new
14
+ message = Message[client: client, payload: 'pay', project: project,
15
+ task: task, type: :DATA]
16
+ message.client_uuid.must_equal client.uuid
17
+ message.project_uuid.must_equal project.uuid
18
+ message.task_uuid.must_equal task.uuid
19
+ message.payload.must_equal 'pay'
20
+ message.type.must_equal :DATA
21
+ end
22
+ end
23
+
24
+ describe '.new' do
25
+ it 'raises on unknown message types' do
26
+ -> { Message.new 'MESS age' }.must_raise Message::UnknownType
27
+ end
28
+
29
+ it 'doesn’t raise on empty messages' do
30
+ Message.new ''
31
+ end
32
+ end
33
+
34
+ describe '#client_uuid' do
35
+ it 'returns the client UUID' do
36
+ mess.client_uuid.must_equal '31364220-636c-6965-6e74-202055554944'
37
+ end
38
+ end
39
+
40
+ describe '#payload' do
41
+ it 'returns the result payload' do
42
+ mess.payload.must_equal 'some payload'
43
+ end
44
+ end
45
+
46
+ describe '#project_uuid' do
47
+ it 'returns the project UUID' do
48
+ mess.project_uuid.must_equal '31364220-7072-6f6a-6563-742055554944'
49
+ end
50
+ end
51
+
52
+ describe '#task_uuid' do
53
+ it 'returns the task UUID' do
54
+ mess.task_uuid.must_equal '31364220-7461-736b-2020-202055554944'
55
+ end
56
+ end
57
+
58
+ describe '#to_s' do
59
+ it 'returns the raw bytes' do
60
+ mess.to_s.must_equal "#{mess.type}\0\0\0\0\0\0\0\0\0\0\0\0" +
61
+ '16B client UUID16B project UUID16B task UUIDsome payload'
62
+ end
63
+ end
64
+
65
+ describe '#type' do
66
+ it 'returns the message type' do
67
+ mess.type.must_match(/\A[A-Z]{4}\z/)
68
+ end
69
+ end
70
+ end end
@@ -0,0 +1,21 @@
1
+ require_relative '../spec_helper'
2
+
3
+ module Kamerling describe NetDispatcher do
4
+ describe '#dispatch' do
5
+ it 'dispatches messages to TCP clients' do
6
+ server = TCPServer.open 0
7
+ thread = Thread.new { server.accept.read }
8
+ addr = Addr[server.addr[3], server.addr[1], :TCP]
9
+ NetDispatcher.new.dispatch addr, 'foo'
10
+ thread.value.must_equal 'foo'
11
+ end
12
+
13
+ it 'dispatches messages to UDP clients' do
14
+ server = UDPSocket.new.tap { |s| s.bind '127.0.0.1', 0 }
15
+ thread = Thread.new { server.recvfrom(2**16).first }
16
+ addr = Addr[server.addr[3], server.addr[1], :UDP]
17
+ NetDispatcher.new.dispatch addr, 'foo'
18
+ thread.value.must_equal 'foo'
19
+ end
20
+ end
21
+ end end
@@ -0,0 +1,22 @@
1
+ require_relative '../spec_helper'
2
+
3
+ module Kamerling describe Receiver do
4
+ describe '#receive' do
5
+ fakes :addr, :client, :task
6
+
7
+ it 'saves the result and updates client and task' do
8
+ repos = fake :repos, as: :class
9
+ stub(repos).<<(any_args) { repos }
10
+ stub(repos).[](Client) { fake :repo, :[] => client }
11
+ stub(repos).[](Task) { fake :repo, :[] => task }
12
+ Receiver.new.receive addr: addr, client_uuid: client.uuid, data: 'data',
13
+ repos: repos, task_uuid: task.uuid
14
+ client.must_have_received :busy=, [false]
15
+ task.must_have_received :done=, [true]
16
+ repos.must_have_received :<<, [client]
17
+ repos.must_have_received :<<, [task]
18
+ repos.must_have_received :<<, [Result.new(addr: addr, client: client,
19
+ data: 'data', task: task, uuid: anything)]
20
+ end
21
+ end
22
+ end end
@@ -0,0 +1,16 @@
1
+ require_relative '../spec_helper'
2
+
3
+ module Kamerling describe Registrar do
4
+ describe '#register' do
5
+ fakes :addr, :client, :project, :repo
6
+
7
+ it 'registers that the given client can do the given project' do
8
+ repos = { Client => { client.uuid => client },
9
+ Project => { project.uuid => project }, Registration => repo }
10
+ Registrar.new.register addr: addr, client_uuid: client.uuid,
11
+ project_uuid: project.uuid, repos: repos
12
+ repo.must_have_received :<<, [Registration.new(addr: addr,
13
+ client: client, project: project, uuid: anything)]
14
+ end
15
+ end
16
+ end end
@@ -0,0 +1,60 @@
1
+ require_relative '../spec_helper'
2
+
3
+ module Kamerling describe Repo do
4
+ Tune = Kamerling.UUIDObject :genre
5
+
6
+ describe '#<<' do
7
+ it 'passes the Hash version of an object to the source' do
8
+ tune = Tune.new genre: :chap_hop
9
+ source = fake Sequel::Dataset
10
+ mock(source) << { genre: :chap_hop, uuid: tune.uuid }
11
+ Repo.new(Tune, source) << tune
12
+ end
13
+
14
+ it 'updates the source’s version if it exists there' do
15
+ dataset = fake Sequel::Dataset
16
+ source = fake Sequel::Dataset
17
+ tune = Tune.new genre: :chap_hop
18
+ stub(source).<<(tune.to_h) { raise Sequel::UniqueConstraintViolation }
19
+ stub(source).where(uuid: tune.uuid) { dataset }
20
+ Repo.new(Tune, source) << tune
21
+ dataset.must_have_received :update, [tune.to_h]
22
+ end
23
+ end
24
+
25
+ describe '#[]' do
26
+ it 'hydrates the object found in the repo' do
27
+ uuid = UUID.new
28
+ source = { { uuid: uuid } => { genre: :chap_hop, uuid: uuid } }
29
+ Repo.new(Tune, source)[uuid]
30
+ .must_equal Tune.new genre: :chap_hop, uuid: uuid
31
+ end
32
+
33
+ it 'raises NotFound if the object is not found in the repo' do
34
+ -> { Repo.new(Tune, {})[UUID.new] }.must_raise Repo::NotFound
35
+ end
36
+ end
37
+
38
+ describe '#all' do
39
+ it 'returns all objects' do
40
+ tune = Tune.new genre: :chap_hop, uuid: UUID.new
41
+ source = fake Sequel::Dataset,
42
+ all: [{ genre: :chap_hop, uuid: tune.uuid }]
43
+ Repo.new(Tune, source).all.must_equal [tune]
44
+ end
45
+ end
46
+
47
+ describe '#related_to' do
48
+ it 'returns objects related to the given object' do
49
+ tunes = [Tune.new(genre: :ragga), Tune.new(genre: :reggae)]
50
+ project = fake :project, uuid: UUID.new
51
+ results = [
52
+ { genre: :ragga, uuid: tunes.first.uuid },
53
+ { genre: :reggae, uuid: tunes.last.uuid },
54
+ ]
55
+ source = fake Sequel::Dataset
56
+ stub(source).where(project_uuid: project.uuid) { results }
57
+ Repo.new(Tune, source).related_to(project).must_equal tunes
58
+ end
59
+ end
60
+ end end