kamerling 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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