js-test-server 0.2.0
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.
- data/CHANGES +31 -0
- data/Gemfile +17 -0
- data/README.markdown +9 -0
- data/Rakefile +63 -0
- data/bin/jasmine-server +9 -0
- data/bin/js-test-client +8 -0
- data/bin/js-test-server +9 -0
- data/bin/screw-unit-server +9 -0
- data/lib/js_test_server.rb +29 -0
- data/lib/js_test_server/client.rb +23 -0
- data/lib/js_test_server/client/runner.rb +129 -0
- data/lib/js_test_server/configuration.rb +69 -0
- data/lib/js_test_server/server.rb +14 -0
- data/lib/js_test_server/server/app.rb +10 -0
- data/lib/js_test_server/server/representations.rb +12 -0
- data/lib/js_test_server/server/representations/dir.html.rb +22 -0
- data/lib/js_test_server/server/representations/frameworks.rb +3 -0
- data/lib/js_test_server/server/representations/not_found.html.rb +13 -0
- data/lib/js_test_server/server/representations/page.html.rb +32 -0
- data/lib/js_test_server/server/representations/remote_control_subscriber.rb +17 -0
- data/lib/js_test_server/server/representations/suite.html.rb +54 -0
- data/lib/js_test_server/server/representations/suites.rb +6 -0
- data/lib/js_test_server/server/representations/suites/jasmine.html.rb +32 -0
- data/lib/js_test_server/server/representations/suites/screw_unit.html.rb +45 -0
- data/lib/js_test_server/server/resources.rb +14 -0
- data/lib/js_test_server/server/resources/file.rb +58 -0
- data/lib/js_test_server/server/resources/framework_file.rb +15 -0
- data/lib/js_test_server/server/resources/implementations_deprecation.rb +8 -0
- data/lib/js_test_server/server/resources/not_found.rb +25 -0
- data/lib/js_test_server/server/resources/remote_control.rb +80 -0
- data/lib/js_test_server/server/resources/resource.rb +12 -0
- data/lib/js_test_server/server/resources/spec_file.rb +47 -0
- data/lib/js_test_server/server/resources/web_root.rb +17 -0
- data/lib/js_test_server/server/runner.rb +62 -0
- data/scratch.rb +8 -0
- data/spec/frameworks/jasmine/cruise_config.rb +21 -0
- data/spec/frameworks/jasmine/spec/jasmine_helper.rb +44 -0
- data/spec/frameworks/jasmine/spec/jasmine_spec.rb +31 -0
- data/spec/functional/functional_spec_helper.rb +55 -0
- data/spec/functional/functional_spec_server_starter.rb +69 -0
- data/spec/functional/jasmine/jasmine_functional_spec.rb +27 -0
- data/spec/functional/screw-unit/screw_unit_functional_spec.rb +27 -0
- data/spec/functional_suite.rb +16 -0
- data/spec/spec_helpers/be_http.rb +32 -0
- data/spec/spec_helpers/example_group.rb +41 -0
- data/spec/spec_helpers/fake_deferrable.rb +3 -0
- data/spec/spec_helpers/fake_selenium_driver.rb +16 -0
- data/spec/spec_helpers/mock_session.rb +30 -0
- data/spec/spec_helpers/show_test_exceptions.rb +22 -0
- data/spec/spec_helpers/wait_for.rb +11 -0
- data/spec/spec_suite.rb +3 -0
- data/spec/unit/js_test_core/client/runner_spec.rb +198 -0
- data/spec/unit/js_test_core/configuration_spec.rb +44 -0
- data/spec/unit/js_test_core/resources/file_spec.rb +79 -0
- data/spec/unit/js_test_core/resources/framework_file_spec.rb +58 -0
- data/spec/unit/js_test_core/resources/implementations_deprecation_spec.rb +16 -0
- data/spec/unit/js_test_core/resources/not_found_spec.rb +49 -0
- data/spec/unit/js_test_core/resources/remote_control_spec.rb +117 -0
- data/spec/unit/js_test_core/resources/spec_file_spec.rb +147 -0
- data/spec/unit/js_test_core/resources/web_root_spec.rb +26 -0
- data/spec/unit/js_test_core/server/server_spec.rb +67 -0
- data/spec/unit/unit_spec_helper.rb +34 -0
- data/spec/unit_suite.rb +10 -0
- metadata +220 -0
@@ -0,0 +1,32 @@
|
|
1
|
+
module BeHttp
|
2
|
+
include Spec::Matchers
|
3
|
+
def be_http(status, headers, body)
|
4
|
+
SimpleMatcher.new(nil) do |given, matcher|
|
5
|
+
description = (<<-DESC).gsub(/^ +/, "")
|
6
|
+
be an http of
|
7
|
+
expected status: #{status.inspect}
|
8
|
+
actual status : #{given.status.inspect}
|
9
|
+
|
10
|
+
expected headers containing: #{headers.inspect}
|
11
|
+
actual headers : #{given.headers.inspect}
|
12
|
+
|
13
|
+
expected body containing: #{body.inspect}
|
14
|
+
actual body : #{given.body.inspect}
|
15
|
+
DESC
|
16
|
+
matcher.failure_message = description
|
17
|
+
matcher.negative_failure_message = "not #{description}"
|
18
|
+
|
19
|
+
passed = true
|
20
|
+
unless given.status == status
|
21
|
+
passed = false
|
22
|
+
end
|
23
|
+
unless headers.all?{|k, v| given.headers[k] == headers[k]}
|
24
|
+
passed = false
|
25
|
+
end
|
26
|
+
unless body.is_a?(Regexp) ? given.body =~ body : given.body.include?(body)
|
27
|
+
passed = false
|
28
|
+
end
|
29
|
+
passed
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
class Spec::ExampleGroup
|
2
|
+
class << self
|
3
|
+
def macro(name, &block)
|
4
|
+
eigen do
|
5
|
+
define_method(name, &block)
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def eigen(&block)
|
10
|
+
eigen_class = (class << self; self; end)
|
11
|
+
eigen_class.class_eval(&block)
|
12
|
+
eigen_class
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
include Rack::Test::Methods
|
17
|
+
include BeHttp
|
18
|
+
include WaitFor
|
19
|
+
attr_reader :framework_path, :spec_path, :root_path, :server, :connection
|
20
|
+
before(:all) do
|
21
|
+
dir = File.dirname(__FILE__)
|
22
|
+
@framework_path = File.expand_path("#{LIBRARY_ROOT_DIR}/spec/example_framework")
|
23
|
+
@spec_path = File.expand_path("#{LIBRARY_ROOT_DIR}/spec/example_spec")
|
24
|
+
@root_path = File.expand_path("#{LIBRARY_ROOT_DIR}/spec/example_root")
|
25
|
+
stub(Thread).start.yields
|
26
|
+
end
|
27
|
+
|
28
|
+
before(:each) do
|
29
|
+
JsTestServer::Configuration.instance.spec_path = spec_path
|
30
|
+
JsTestServer::Configuration.instance.root_path = root_path
|
31
|
+
JsTestServer::Configuration.instance.framework_path = framework_path
|
32
|
+
end
|
33
|
+
|
34
|
+
after(:each) do
|
35
|
+
JsTestServer::Server::Resources::RemoteControl.queue = nil
|
36
|
+
end
|
37
|
+
|
38
|
+
def app
|
39
|
+
Sinatra::Application
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class Rack::MockSession
|
2
|
+
attr_writer :last_response
|
3
|
+
|
4
|
+
def last_response
|
5
|
+
@last_response
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class Rack::Test::Session
|
10
|
+
class << self
|
11
|
+
def http_action_with_async_catch(method_name)
|
12
|
+
alias_method "#{method_name}_without_async_catch", method_name
|
13
|
+
class_eval((<<-RUBY), __FILE__, __LINE__)
|
14
|
+
def #{method_name}_with_async_catch(*args, &block)
|
15
|
+
catch(:async) {return #{method_name}_without_async_catch(*args, &block)}
|
16
|
+
nil
|
17
|
+
end
|
18
|
+
RUBY
|
19
|
+
alias_method method_name, "#{method_name}_with_async_catch"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def_delegators :@rack_mock_session, :last_response=
|
24
|
+
|
25
|
+
http_action_with_async_catch :get
|
26
|
+
http_action_with_async_catch :put
|
27
|
+
http_action_with_async_catch :post
|
28
|
+
http_action_with_async_catch :delete
|
29
|
+
end
|
30
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class ShowTestExceptions
|
2
|
+
attr_reader :app
|
3
|
+
|
4
|
+
def initialize(app)
|
5
|
+
@app = app
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
app.call(env)
|
10
|
+
rescue StandardError, LoadError, SyntaxError => e
|
11
|
+
body = [
|
12
|
+
e.message,
|
13
|
+
e.backtrace.join("\n\t")
|
14
|
+
].join("\n")
|
15
|
+
[
|
16
|
+
500,
|
17
|
+
{"Content-Type" => "text",
|
18
|
+
"Content-Length" => body.size.to_s},
|
19
|
+
body
|
20
|
+
]
|
21
|
+
end
|
22
|
+
end
|
data/spec/spec_suite.rb
ADDED
@@ -0,0 +1,198 @@
|
|
1
|
+
require File.expand_path("#{File.dirname(__FILE__)}/../../unit_spec_helper")
|
2
|
+
|
3
|
+
module JsTestServer
|
4
|
+
module Client
|
5
|
+
describe Runner do
|
6
|
+
describe '.run' do
|
7
|
+
attr_reader :stdout, :request
|
8
|
+
before do
|
9
|
+
@stdout = StringIO.new
|
10
|
+
Runner.const_set(:STDOUT, stdout)
|
11
|
+
stub.instance_of(Runner).sleep
|
12
|
+
end
|
13
|
+
|
14
|
+
after do
|
15
|
+
Runner.__send__(:remove_const, :STDOUT)
|
16
|
+
end
|
17
|
+
|
18
|
+
describe "#start" do
|
19
|
+
attr_reader :driver, :browser_host, :spec_url, :selenium_browser_start_command, :selenium_host, :selenium_port
|
20
|
+
|
21
|
+
context "with default runner parameters" do
|
22
|
+
before do
|
23
|
+
@driver = FakeSeleniumDriver.new
|
24
|
+
|
25
|
+
expected_selenium_client_params = {
|
26
|
+
:host => "0.0.0.0", :port => 4444, :browser => "*firefox", :url => "http://localhost:8080"
|
27
|
+
}
|
28
|
+
|
29
|
+
mock.proxy(Selenium::Client::Driver).new(expected_selenium_client_params) do
|
30
|
+
driver
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
context "when the suite fails" do
|
35
|
+
it "returns false" do
|
36
|
+
mock.proxy(driver).start.ordered
|
37
|
+
mock.proxy(driver).open("/specs").ordered
|
38
|
+
|
39
|
+
mock.strong(driver).get_eval("window.JsTestServer.status()") do
|
40
|
+
{"runner_state" => RUNNING_RUNNER_STATE, "console" => ""}.to_json
|
41
|
+
end
|
42
|
+
|
43
|
+
mock.strong(driver).get_eval("window.JsTestServer.status()") do
|
44
|
+
{"runner_state" => RUNNING_RUNNER_STATE, "console" => ".."}.to_json
|
45
|
+
end
|
46
|
+
|
47
|
+
mock.strong(driver).get_eval("window.JsTestServer.status()") do
|
48
|
+
{"runner_state" => RUNNING_RUNNER_STATE, "console" => "...F."}.to_json
|
49
|
+
end
|
50
|
+
|
51
|
+
mock.strong(driver).get_eval("window.JsTestServer.status()") do
|
52
|
+
{"runner_state" => FAILED_RUNNER_STATE, "console" => "...F..\n\nFailure\n/specs/foo_spec.js"}.to_json
|
53
|
+
end
|
54
|
+
|
55
|
+
Runner.run.should be_false
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
context "when the suite passes" do
|
60
|
+
it "returns true" do
|
61
|
+
mock.proxy(driver).start.ordered
|
62
|
+
mock.proxy(driver).open("/specs").ordered
|
63
|
+
|
64
|
+
mock.strong(driver).get_eval("window.JsTestServer.status()") do
|
65
|
+
{"runner_state" => RUNNING_RUNNER_STATE, "console" => ""}.to_json
|
66
|
+
end
|
67
|
+
|
68
|
+
mock.strong(driver).get_eval("window.JsTestServer.status()") do
|
69
|
+
{"runner_state" => RUNNING_RUNNER_STATE, "console" => ".."}.to_json
|
70
|
+
end
|
71
|
+
|
72
|
+
mock.strong(driver).get_eval("window.JsTestServer.status()") do
|
73
|
+
{"runner_state" => RUNNING_RUNNER_STATE, "console" => "....."}.to_json
|
74
|
+
end
|
75
|
+
|
76
|
+
mock.strong(driver).get_eval("window.JsTestServer.status()") do
|
77
|
+
{"runner_state" => PASSED_RUNNER_STATE, "console" => "......\n\nPassed"}.to_json
|
78
|
+
end
|
79
|
+
|
80
|
+
Runner.run.should be_true
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
describe "with overridden runner parameters" do
|
86
|
+
it "allows overrides for :host, :port, :browser, and :url" do
|
87
|
+
driver = FakeSeleniumDriver.new
|
88
|
+
|
89
|
+
host = "myhost"
|
90
|
+
port = 9999
|
91
|
+
browser = "*iexplore"
|
92
|
+
spec_url = "http://myspechost:7777/specs/path"
|
93
|
+
expected_selenium_client_params = {
|
94
|
+
:host => host, :port => port, :browser => browser, :url => "http://myspechost:7777"
|
95
|
+
}
|
96
|
+
mock.proxy(Selenium::Client::Driver).new(expected_selenium_client_params) do
|
97
|
+
driver
|
98
|
+
end
|
99
|
+
|
100
|
+
mock.proxy(driver).start.ordered
|
101
|
+
mock.proxy(driver).open("/specs/path").ordered
|
102
|
+
|
103
|
+
mock.strong(driver).get_eval("window.JsTestServer.status()") do
|
104
|
+
{"runner_state" => RUNNING_RUNNER_STATE, "console" => ""}.to_json
|
105
|
+
end
|
106
|
+
|
107
|
+
mock.strong(driver).get_eval("window.JsTestServer.status()") do
|
108
|
+
{"runner_state" => RUNNING_RUNNER_STATE, "console" => ".."}.to_json
|
109
|
+
end
|
110
|
+
|
111
|
+
mock.strong(driver).get_eval("window.JsTestServer.status()") do
|
112
|
+
{"runner_state" => RUNNING_RUNNER_STATE, "console" => "....."}.to_json
|
113
|
+
end
|
114
|
+
|
115
|
+
mock.strong(driver).get_eval("window.JsTestServer.status()") do
|
116
|
+
{"runner_state" => PASSED_RUNNER_STATE, "console" => "......\n\nPassed"}.to_json
|
117
|
+
end
|
118
|
+
|
119
|
+
Runner.run(:selenium_host => host, :selenium_port => port, :selenium_browser => browser, :spec_url => spec_url).
|
120
|
+
should be_true
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
describe ".run_argv" do
|
127
|
+
attr_reader :request, :response
|
128
|
+
before do
|
129
|
+
stub(Runner).puts
|
130
|
+
end
|
131
|
+
|
132
|
+
context "when passed-in Hash contains :selenium_browser" do
|
133
|
+
it "passes the spec_url as a post parameter" do
|
134
|
+
selenium_browser = '*iexplore'
|
135
|
+
mock(Runner).run(satisfy do |params|
|
136
|
+
default_params.merge(:selenium_browser => selenium_browser).
|
137
|
+
to_set.subset?(params.to_set)
|
138
|
+
end)
|
139
|
+
client = Runner.run_argv(['--selenium-browser', selenium_browser])
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
context "when passed-in Hash contains :spec_url" do
|
144
|
+
it "passes the spec_url as a post parameter" do
|
145
|
+
spec_url = 'http://foobar.com/foo'
|
146
|
+
mock(Runner).run(satisfy do |params|
|
147
|
+
default_params.merge(:spec_url => spec_url).
|
148
|
+
to_set.subset?(params.to_set)
|
149
|
+
end)
|
150
|
+
client = Runner.run_argv(['--spec-url', spec_url])
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
context "when passed-in Hash contains :selenium_host" do
|
155
|
+
it "passes the selenium_host as a post parameter" do
|
156
|
+
selenium_host = 'test-runner'
|
157
|
+
mock(Runner).run(satisfy do |params|
|
158
|
+
default_params.merge(:selenium_host => selenium_host).
|
159
|
+
to_set.subset?(params.to_set)
|
160
|
+
end)
|
161
|
+
client = Runner.run_argv(['--selenium-host', selenium_host])
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
context "when passed-in Hash contains :selenium_port" do
|
166
|
+
it "passes the selenium_port as a post parameter" do
|
167
|
+
selenium_port = 5000
|
168
|
+
mock(Runner).run(satisfy do |params|
|
169
|
+
default_params.merge(:selenium_port => selenium_port).
|
170
|
+
to_set.subset?(params.to_set)
|
171
|
+
end)
|
172
|
+
client = Runner.run_argv(['--selenium-port', selenium_port.to_s])
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
context "when passed-in Hash contains :timeout" do
|
177
|
+
it "passes the timeout as a post parameter" do
|
178
|
+
mock(Runner).run(satisfy do |params|
|
179
|
+
default_params.merge(:timeout => 5).
|
180
|
+
to_set.subset?(params.to_set)
|
181
|
+
end)
|
182
|
+
client = Runner.run_argv(['--timeout', "5"])
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def default_params
|
187
|
+
{
|
188
|
+
:selenium_browser => "*firefox",
|
189
|
+
:selenium_host => "0.0.0.0",
|
190
|
+
:selenium_port => 4444,
|
191
|
+
:spec_url => "http://localhost:8080/specs",
|
192
|
+
:timeout => 60
|
193
|
+
}
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require File.expand_path("#{File.dirname(__FILE__)}/../unit_spec_helper")
|
2
|
+
|
3
|
+
module JsTestServer
|
4
|
+
describe Configuration do
|
5
|
+
attr_reader :result
|
6
|
+
|
7
|
+
before do
|
8
|
+
@result = ""
|
9
|
+
end
|
10
|
+
|
11
|
+
describe "#spec_path" do
|
12
|
+
it "returns the Dir " do
|
13
|
+
Configuration.spec_path.should == spec_path
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "#spec_path" do
|
18
|
+
it "returns the absolute path of the specs root directory" do
|
19
|
+
Configuration.spec_path.should == spec_path
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "#root_path" do
|
24
|
+
it "returns the expanded path of the public path" do
|
25
|
+
Configuration.root_path.should == root_path
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "#framework_path" do
|
30
|
+
it "returns the expanded path to the JsTestServer core directory" do
|
31
|
+
Configuration.framework_path.should == framework_path
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "#root_url" do
|
36
|
+
it "returns the url of the site's root" do
|
37
|
+
configuration = Configuration.new
|
38
|
+
configuration.host = "localhost"
|
39
|
+
configuration.port = 9999
|
40
|
+
configuration.root_url.should == "http://localhost:9999"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require File.expand_path("#{File.dirname(__FILE__)}/../../unit_spec_helper")
|
2
|
+
|
3
|
+
module JsTestServer::Server::Resources
|
4
|
+
describe File do
|
5
|
+
describe "Files" do
|
6
|
+
describe "GET /stylesheets/example.css" do
|
7
|
+
it "returns the example.css file content as a css file" do
|
8
|
+
path = "#{root_path}/stylesheets/example.css"
|
9
|
+
response = get("/stylesheets/example.css")
|
10
|
+
response.should be_http(
|
11
|
+
200,
|
12
|
+
{
|
13
|
+
'Content-Length' => ::File.size(path).to_s,
|
14
|
+
'Content-Type' => "text/css",
|
15
|
+
'Last-Modified' => ::File.mtime(path).rfc822
|
16
|
+
},
|
17
|
+
::File.read(path)
|
18
|
+
)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "GET /javascripts/foo.js" do
|
23
|
+
it "returns the foo.js file content as a javascript file" do
|
24
|
+
path = "#{root_path}/javascripts/foo.js"
|
25
|
+
response = get("/javascripts/foo.js")
|
26
|
+
response.should be_http(
|
27
|
+
200,
|
28
|
+
{
|
29
|
+
'Content-Length' => ::File.size(path).to_s,
|
30
|
+
'Content-Type' => "text/javascript",
|
31
|
+
'Last-Modified' => ::File.mtime(path).rfc822
|
32
|
+
},
|
33
|
+
::File.read(path)
|
34
|
+
)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe "GET /javascripts/subdir/bar.js - Subdirectory" do
|
39
|
+
it "returns the subdir/bar.js file content as a javascript file" do
|
40
|
+
path = "#{root_path}/javascripts/subdir/bar.js"
|
41
|
+
response = get("/javascripts/subdir/bar.js")
|
42
|
+
response.should be_http(
|
43
|
+
200,
|
44
|
+
{
|
45
|
+
'Content-Length' => ::File.size(path).to_s,
|
46
|
+
'Content-Type' => "text/javascript",
|
47
|
+
'Last-Modified' => ::File.mtime(path).rfc822
|
48
|
+
},
|
49
|
+
::File.read(path)
|
50
|
+
)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe "Directories" do
|
56
|
+
describe "GET /stylesheets - Top level directory" do
|
57
|
+
it "returns a page with a of files in the directory" do
|
58
|
+
response = get("/stylesheets")
|
59
|
+
response.should be_http(
|
60
|
+
200,
|
61
|
+
{},
|
62
|
+
%r(<a href="/stylesheets/example.css">example.css</a>)
|
63
|
+
)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe "GET /javascripts/subdir - Subdirectory" do
|
68
|
+
it "returns a page with a of files in the directory" do
|
69
|
+
response = get("/javascripts/subdir")
|
70
|
+
response.should be_http(
|
71
|
+
200,
|
72
|
+
{},
|
73
|
+
%r(<a href="/javascripts/subdir/bar.js">bar.js</a>)
|
74
|
+
)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|