kamerling 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +4 -13
- data/Gemfile +0 -1
- data/Gemfile.lock +64 -64
- data/README.md +18 -1
- data/Rakefile +5 -3
- data/bin/kamerling +1 -1
- data/config/reek.yml +20 -3
- data/kamerling.gemspec +16 -13
- data/lib/kamerling.rb +2 -28
- data/lib/kamerling/addr.rb +17 -3
- data/lib/kamerling/client.rb +10 -11
- data/lib/kamerling/core_extensions/main.rb +14 -14
- data/lib/kamerling/dispatch.rb +13 -0
- data/lib/kamerling/handler.rb +16 -25
- data/lib/kamerling/http_api.rb +50 -34
- data/lib/kamerling/logging.rb +36 -16
- data/lib/kamerling/mapper.rb +33 -0
- data/lib/kamerling/message.rb +50 -37
- data/lib/kamerling/migrations/2_results_received_at.rb +7 -0
- data/lib/kamerling/migrations/3_dispatches.rb +17 -0
- data/lib/kamerling/migrations/4_registrations_registered_at.rb +7 -0
- data/lib/kamerling/migrations/5_clients_type.rb +7 -0
- data/lib/kamerling/net_dispatcher.rb +12 -7
- data/lib/kamerling/project.rb +7 -3
- data/lib/kamerling/receiver.rb +38 -10
- data/lib/kamerling/registrar.rb +45 -8
- data/lib/kamerling/registration.rb +9 -10
- data/lib/kamerling/repo.rb +33 -26
- data/lib/kamerling/repos.rb +52 -45
- data/lib/kamerling/result.rb +10 -11
- data/lib/kamerling/server/http.rb +28 -21
- data/lib/kamerling/server/sock.rb +32 -24
- data/lib/kamerling/server/tcp.rb +23 -15
- data/lib/kamerling/server/udp.rb +24 -16
- data/lib/kamerling/server_runner.rb +30 -41
- data/lib/kamerling/settings.rb +28 -0
- data/lib/kamerling/task.rb +7 -7
- data/lib/kamerling/task_dispatcher.rb +31 -22
- data/lib/kamerling/uuid.rb +13 -11
- data/lib/kamerling/uuid_entity.rb +23 -9
- data/lib/kamerling/value.rb +13 -0
- data/lib/kamerling/views/clients.slim +3 -1
- data/lib/kamerling/views/project.slim +8 -4
- data/lib/kamerling/views/projects.slim +7 -1
- data/spec/kamerling/addr_spec.rb +32 -22
- data/spec/kamerling/client_spec.rb +9 -5
- data/spec/kamerling/core_extensions/main_spec.rb +18 -13
- data/spec/kamerling/dispatch_spec.rb +16 -0
- data/spec/kamerling/handler_spec.rb +24 -34
- data/spec/kamerling/http_api_spec.rb +94 -73
- data/spec/kamerling/logging_spec.rb +93 -62
- data/spec/kamerling/mapper_spec.rb +151 -0
- data/spec/kamerling/message_spec.rb +73 -49
- data/spec/kamerling/net_dispatcher_spec.rb +22 -16
- data/spec/kamerling/receiver_spec.rb +29 -19
- data/spec/kamerling/registrar_spec.rb +43 -15
- data/spec/kamerling/registration_spec.rb +17 -0
- data/spec/kamerling/repo_spec.rb +63 -47
- data/spec/kamerling/repos_spec.rb +121 -109
- data/spec/kamerling/result_spec.rb +16 -0
- data/spec/kamerling/server/http_spec.rb +19 -14
- data/spec/kamerling/server/tcp_spec.rb +41 -35
- data/spec/kamerling/server/udp_spec.rb +40 -34
- data/spec/kamerling/server_runner_spec.rb +62 -53
- data/spec/kamerling/settings_spec.rb +36 -0
- data/spec/kamerling/task_dispatcher_spec.rb +38 -15
- data/spec/kamerling/task_spec.rb +9 -5
- data/spec/kamerling/uuid_entity_spec.rb +53 -25
- data/spec/kamerling/uuid_spec.rb +19 -16
- data/spec/kamerling/value_spec.rb +21 -0
- data/spec/spec_helper.rb +3 -6
- metadata +54 -8
- data/lib/kamerling/core_extensions.rb +0 -1
@@ -1,9 +1,13 @@
|
|
1
1
|
require_relative '../spec_helper'
|
2
|
+
require_relative '../../lib/kamerling/addr'
|
3
|
+
require_relative '../../lib/kamerling/client'
|
2
4
|
|
3
|
-
module Kamerling
|
4
|
-
describe
|
5
|
-
|
6
|
-
|
5
|
+
module Kamerling
|
6
|
+
describe Client do
|
7
|
+
describe '#busy' do
|
8
|
+
it 'defaults to false' do
|
9
|
+
refute Client.new.busy
|
10
|
+
end
|
7
11
|
end
|
8
12
|
end
|
9
|
-
end
|
13
|
+
end
|
@@ -1,18 +1,23 @@
|
|
1
1
|
require_relative '../../spec_helper'
|
2
|
+
require_relative '../../../lib/kamerling/core_extensions/main'
|
2
3
|
|
3
|
-
module Kamerling
|
4
|
-
describe
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
4
|
+
module Kamerling
|
5
|
+
describe CoreExtensions::Main do
|
6
|
+
describe '#warn_off' do
|
7
|
+
before { @verbose = $VERBOSE }
|
8
|
+
after { $VERBOSE = @verbose }
|
9
|
+
|
10
|
+
it 'when $VERBOSE is on it turns it off inside the block and back on' do
|
11
|
+
$VERBOSE = true
|
12
|
+
CoreExtensions::Main.warn_off { refute $VERBOSE }
|
13
|
+
assert $VERBOSE
|
14
|
+
end
|
10
15
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
+
it 'when $VERBOSE is off it keeps it off' do
|
17
|
+
$VERBOSE = false
|
18
|
+
CoreExtensions::Main.warn_off { refute $VERBOSE }
|
19
|
+
refute $VERBOSE
|
20
|
+
end
|
16
21
|
end
|
17
22
|
end
|
18
|
-
end
|
23
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
require_relative '../../lib/kamerling/dispatch'
|
3
|
+
|
4
|
+
module Kamerling
|
5
|
+
describe Dispatch do
|
6
|
+
describe '#dispatched_at' do
|
7
|
+
it 'defaults to the current time' do
|
8
|
+
assert Dispatch.new.dispatched_at.between?(Time.now - 1, Time.now + 1)
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'defaults to the time of Dispatch’s creation' do
|
12
|
+
Dispatch.new.dispatched_at.wont_equal Dispatch.new.dispatched_at
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -1,41 +1,31 @@
|
|
1
1
|
require_relative '../spec_helper'
|
2
|
+
require_relative '../../lib/kamerling/addr'
|
3
|
+
require_relative '../../lib/kamerling/handler'
|
4
|
+
require_relative '../../lib/kamerling/message'
|
5
|
+
require_relative '../../lib/kamerling/receiver'
|
6
|
+
require_relative '../../lib/kamerling/registrar'
|
7
|
+
require_relative '../../lib/kamerling/uuid'
|
2
8
|
|
3
|
-
module Kamerling
|
4
|
-
describe
|
5
|
-
|
6
|
-
|
9
|
+
module Kamerling
|
10
|
+
describe Handler do
|
11
|
+
describe '#handle' do
|
12
|
+
fake :receiver, as: :class
|
13
|
+
fake :registrar, as: :class
|
7
14
|
|
8
|
-
|
9
|
-
|
10
|
-
client_uuid = UUID['16B client UUID']
|
11
|
-
project_uuid = UUID['16B project UUID']
|
12
|
-
handler.handle input, addr
|
13
|
-
args = {
|
14
|
-
addr: addr,
|
15
|
-
client_uuid: client_uuid,
|
16
|
-
project_uuid: project_uuid,
|
17
|
-
}
|
18
|
-
registrar.must_have_received :register, [args]
|
19
|
-
end
|
15
|
+
let(:addr) { Addr.new }
|
16
|
+
let(:handler) { Handler.new(receiver: receiver, registrar: registrar) }
|
20
17
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
handler.handle input, addr
|
27
|
-
args = {
|
28
|
-
addr: addr,
|
29
|
-
client_uuid: client_uuid,
|
30
|
-
data: 'data',
|
31
|
-
task_uuid: task_uuid,
|
32
|
-
}
|
33
|
-
receiver.must_have_received :receive, [args]
|
34
|
-
end
|
18
|
+
it 'handles RGST inputs' do
|
19
|
+
message = Message.parse('RGST')
|
20
|
+
handler.handle message, addr
|
21
|
+
registrar.must_have_received :register, [addr: addr, message: message]
|
22
|
+
end
|
35
23
|
|
36
|
-
|
37
|
-
|
38
|
-
|
24
|
+
it 'handles RSLT inputs' do
|
25
|
+
message = Message.parse('RSLT')
|
26
|
+
handler.handle message, addr
|
27
|
+
receiver.must_have_received :receive, [addr: addr, message: message]
|
28
|
+
end
|
39
29
|
end
|
40
30
|
end
|
41
|
-
end
|
31
|
+
end
|
@@ -1,93 +1,114 @@
|
|
1
1
|
require 'nokogiri'
|
2
|
-
|
3
2
|
require_relative '../spec_helper'
|
3
|
+
require_relative '../../lib/kamerling/client'
|
4
|
+
require_relative '../../lib/kamerling/http_api'
|
5
|
+
require_relative '../../lib/kamerling/project'
|
6
|
+
require_relative '../../lib/kamerling/repos'
|
7
|
+
require_relative '../../lib/kamerling/task'
|
8
|
+
require_relative '../../lib/kamerling/task_dispatcher'
|
9
|
+
require_relative '../../lib/kamerling/uuid'
|
4
10
|
|
5
|
-
module Kamerling
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
+
module Kamerling
|
12
|
+
describe HTTPAPI do
|
13
|
+
let(:app) { HTTPAPI.set(repos: repos, task_dispatcher: task_dispatcher) }
|
14
|
+
let(:doc) { Nokogiri::HTML(last_response.body) }
|
15
|
+
let(:ecc) { Project.new }
|
16
|
+
let(:gimps) { Project.new }
|
17
|
+
let(:repos) { fake(:repos, as: :class, projects: [gimps, ecc]) }
|
18
|
+
let(:task_dispatcher) { fake(:task_dispatcher) }
|
11
19
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
20
|
+
describe 'GET /' do
|
21
|
+
it 'contains links to clients and projects' do
|
22
|
+
get '/'
|
23
|
+
doc.at('#clients')['href'].must_equal '/clients'
|
24
|
+
doc.at('#projects')['href'].must_equal '/projects'
|
25
|
+
end
|
17
26
|
end
|
18
|
-
end
|
19
27
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
.must_equal
|
28
|
+
describe 'GET /clients' do
|
29
|
+
it 'contains information on clients' do
|
30
|
+
addr = Addr['127.0.0.1', 1981, :TCP]
|
31
|
+
fpga = Client.new(addr: addr, busy: true, type: :FPGA)
|
32
|
+
stub(repos).clients { [fpga] }
|
33
|
+
get '/clients'
|
34
|
+
links = doc.css('#clients a[data-class=client]')
|
35
|
+
links.first['data-addr'].must_equal 'tcp://127.0.0.1:1981'
|
36
|
+
links.first['data-busy'].must_equal 'true'
|
37
|
+
links.first['data-type'].must_equal 'FPGA'
|
38
|
+
links.first['data-uuid'].must_equal fpga.uuid
|
39
|
+
links.first['href'].must_equal "/clients/#{fpga.uuid}"
|
40
|
+
end
|
29
41
|
end
|
30
|
-
end
|
31
42
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
43
|
+
describe 'GET /projects' do
|
44
|
+
it 'contains links to and UUIDs of projects' do
|
45
|
+
get '/projects'
|
46
|
+
links = doc.css('#projects a[data-class=project]')
|
47
|
+
links.size.must_equal 2
|
48
|
+
links.at("[data-uuid='#{gimps.uuid}']")['href']
|
49
|
+
.must_equal "/projects/#{gimps.uuid}"
|
50
|
+
end
|
39
51
|
end
|
40
|
-
end
|
41
52
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
53
|
+
describe 'GET /projects/{uuid}' do
|
54
|
+
let(:cpu) { Client.new(busy: false, type: :CPU) }
|
55
|
+
let(:gpu) { Client.new(busy: true, type: :GPU) }
|
56
|
+
let(:three) { Task.new(done: false) }
|
57
|
+
let(:seven) { Task.new(done: true) }
|
47
58
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
59
|
+
before do
|
60
|
+
stub(repos).project(gimps.uuid) { gimps }
|
61
|
+
stub(repos).clients_for(gimps) { [cpu, gpu] }
|
62
|
+
stub(repos).tasks_for(gimps) { [three, seven] }
|
63
|
+
end
|
53
64
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
65
|
+
it 'contains links to and info on the project’s clients' do
|
66
|
+
get "/projects/#{gimps.uuid}"
|
67
|
+
links = doc.css('#clients a[data-class=client]')
|
68
|
+
links.size.must_equal 2
|
69
|
+
links.at("[data-uuid='#{cpu.uuid}']")['href']
|
70
|
+
.must_equal "/clients/#{cpu.uuid}"
|
71
|
+
links.at("[data-uuid='#{cpu.uuid}']")['data-busy'].must_equal 'false'
|
72
|
+
links.at("[data-uuid='#{gpu.uuid}']")['data-busy'].must_equal 'true'
|
73
|
+
links.at("[data-uuid='#{cpu.uuid}']")['data-type'].must_equal 'CPU'
|
74
|
+
links.at("[data-uuid='#{gpu.uuid}']")['data-type'].must_equal 'GPU'
|
75
|
+
end
|
63
76
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
77
|
+
it 'contains links to and info on the project’s tasks' do
|
78
|
+
get "/projects/#{gimps.uuid}"
|
79
|
+
links = doc.css('#tasks a[data-class=task]')
|
80
|
+
links.size.must_equal 2
|
81
|
+
links.at("[data-uuid='#{three.uuid}']")['href']
|
82
|
+
.must_equal "/tasks/#{three.uuid}"
|
83
|
+
links.at("[data-uuid='#{three.uuid}']")['data-done'].must_equal 'false'
|
84
|
+
links.at("[data-uuid='#{seven.uuid}']")['data-done'].must_equal 'true'
|
85
|
+
end
|
72
86
|
end
|
73
|
-
end
|
74
87
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
88
|
+
describe 'POST /projects' do
|
89
|
+
it 'creates a new project with the given name and UUID' do
|
90
|
+
post '/projects', name: 'ECC', uuid: uuid = UUID.new
|
91
|
+
repos.must_have_received :<<, [Project.new(name: 'ECC', uuid: uuid)]
|
92
|
+
end
|
80
93
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
94
|
+
it 'redirects to /projects' do
|
95
|
+
post '/projects', name: 'ECC', uuid: UUID.new
|
96
|
+
follow_redirect!
|
97
|
+
URI(last_request.url).path.must_equal '/projects'
|
98
|
+
end
|
85
99
|
end
|
86
100
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
101
|
+
describe 'POST /projects/dispatch' do
|
102
|
+
it 'dispatches tasks to all free clients' do
|
103
|
+
post '/projects/dispatch'
|
104
|
+
task_dispatcher.must_have_received :dispatch_all, []
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'redirects to /projects' do
|
108
|
+
post '/projects/dispatch'
|
109
|
+
follow_redirect!
|
110
|
+
URI(last_request.url).path.must_equal '/projects'
|
111
|
+
end
|
91
112
|
end
|
92
113
|
end
|
93
|
-
end
|
114
|
+
end
|
@@ -1,80 +1,111 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'socket'
|
3
|
+
require 'stringio'
|
1
4
|
require_relative '../spec_helper'
|
5
|
+
require_relative '../../lib/kamerling/addr'
|
6
|
+
require_relative '../../lib/kamerling/logging'
|
7
|
+
require_relative '../../lib/kamerling/message'
|
8
|
+
require_relative '../../lib/kamerling/net_dispatcher'
|
9
|
+
require_relative '../../lib/kamerling/server/tcp'
|
10
|
+
require_relative '../../lib/kamerling/server/udp'
|
2
11
|
|
3
|
-
module Kamerling
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
12
|
+
module Kamerling
|
13
|
+
describe Logging do
|
14
|
+
let(:logged) { stream.tap(&:rewind).read }
|
15
|
+
let(:logger) { Logger.new(stream) }
|
16
|
+
let(:stream) { StringIO.new }
|
17
|
+
let(:tcp_server) { Server::TCP.new(addr: Addr['localhost', 1981, :TCP]) }
|
18
|
+
let(:udp_server) { Server::UDP.new(addr: Addr['localhost', 1979, :UDP]) }
|
9
19
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
end
|
15
|
-
|
16
|
-
after do
|
17
|
-
tcp_server.stop
|
18
|
-
udp_server.stop
|
19
|
-
end
|
20
|
-
|
21
|
-
describe '.log_to' do
|
22
|
-
it 'logs TCP server starts' do
|
23
|
-
logged.must_include 'start localhost:1981 (TCP)'
|
20
|
+
before do
|
21
|
+
Logging.log_to logger
|
22
|
+
tcp_server.start
|
23
|
+
udp_server.start
|
24
24
|
end
|
25
25
|
|
26
|
-
|
26
|
+
after do
|
27
27
|
tcp_server.stop
|
28
|
-
|
28
|
+
udp_server.stop
|
29
29
|
end
|
30
30
|
|
31
|
-
|
32
|
-
|
33
|
-
|
31
|
+
describe '.log_to' do
|
32
|
+
it 'logs TCP server starts' do
|
33
|
+
logged.must_include 'start tcp://localhost:1981'
|
34
34
|
end
|
35
|
-
run_all_threads
|
36
|
-
logged.must_include "connect #{tcp_addr}"
|
37
|
-
end
|
38
35
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
Addr[*socket.local_address.ip_unpack, :TCP]
|
36
|
+
it 'logs TCP server stops' do
|
37
|
+
tcp_server.stop
|
38
|
+
logged.must_include 'stop tcp://localhost:1981'
|
43
39
|
end
|
44
|
-
run_all_threads
|
45
|
-
logged.must_include "received #{tcp_addr} PING"
|
46
|
-
end
|
47
40
|
|
48
|
-
|
49
|
-
|
50
|
-
|
41
|
+
it 'logs TCP server connects' do
|
42
|
+
tcp_addr = TCPSocket.open(*tcp_server.addr) do |socket|
|
43
|
+
Addr[*socket.local_address.ip_unpack, :TCP]
|
44
|
+
end
|
45
|
+
run_all_threads
|
46
|
+
logged.must_include "connect #{tcp_addr}"
|
47
|
+
end
|
51
48
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
49
|
+
it 'logs TCP server receives' do
|
50
|
+
tcp_addr = TCPSocket.open(*tcp_server.addr) do |socket|
|
51
|
+
socket << 'PING'
|
52
|
+
Addr[*socket.local_address.ip_unpack, :TCP]
|
53
|
+
end
|
54
|
+
run_all_threads
|
55
|
+
logged.must_include "received #{tcp_addr} 50 49 4e 47"
|
56
|
+
end
|
56
57
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
58
|
+
it 'logs TCP unknown message types' do
|
59
|
+
tcp_addr = TCPSocket.open(*tcp_server.addr) do |socket|
|
60
|
+
socket << 'foo'
|
61
|
+
Addr[*socket.local_address.ip_unpack, :TCP]
|
62
|
+
end
|
63
|
+
run_all_threads
|
64
|
+
logged.must_include "received #{tcp_addr} unknown message type"
|
65
|
+
end
|
64
66
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
67
|
+
it 'logs UDP server starts' do
|
68
|
+
logged.must_include 'start udp://localhost:1979'
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'logs UDP server stops' do
|
72
|
+
udp_server.stop
|
73
|
+
logged.must_include 'stop udp://localhost:1979'
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'logs UDP server connects' do
|
77
|
+
udp_client = UDPSocket.new
|
78
|
+
udp_client.send 'PING', 0, *udp_server.addr
|
79
|
+
udp_addr = Addr['127.0.0.1', udp_client.addr[1], :UDP]
|
80
|
+
run_all_threads
|
81
|
+
logged.must_include "connect #{udp_addr}"
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'logs UDP server receives' do
|
85
|
+
udp_client = UDPSocket.new
|
86
|
+
udp_client.send 'PING', 0, *udp_server.addr
|
87
|
+
udp_addr = Addr['127.0.0.1', udp_client.addr[1], :UDP]
|
88
|
+
run_all_threads
|
89
|
+
logged.must_include "received #{udp_addr} 50 49 4e 47"
|
90
|
+
end
|
72
91
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
92
|
+
it 'logs UDP unknown message types' do
|
93
|
+
udp_client = UDPSocket.new
|
94
|
+
addrs = Array.new(3) do
|
95
|
+
udp_client.send 'foo', 0, *udp_server.addr
|
96
|
+
Addr['127.0.0.1', udp_client.addr[1], :UDP]
|
97
|
+
end
|
98
|
+
run_all_threads
|
99
|
+
log_lines = addrs.map { |addr| "received #{addr} unknown message type" }
|
100
|
+
assert log_lines.any? { |line| logged.include?(line) }
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'logs packet dispatches' do
|
104
|
+
server = UDPSocket.new.tap { |s| s.bind '127.0.0.1', 0 }
|
105
|
+
addr = Addr[server.addr[3], server.addr[1], :UDP]
|
106
|
+
NetDispatcher.dispatch addr, Message.parse('PING')
|
107
|
+
logged.must_include "sent #{addr} 50 49 4e 47"
|
108
|
+
end
|
78
109
|
end
|
79
110
|
end
|
80
|
-
end
|
111
|
+
end
|