jasmine-selenium-sauce 1.0.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/.gitignore +19 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/Guardfile +6 -0
- data/LICENSE +22 -0
- data/README.md +63 -0
- data/Rakefile +8 -0
- data/jasmine-selenium-sauce.gemspec +28 -0
- data/lib/jasmine-selenium-sauce.rb +24 -0
- data/lib/jasmine-selenium-sauce/jasmine_results.rb +19 -0
- data/lib/jasmine-selenium-sauce/rspec_reporter.rb +87 -0
- data/lib/jasmine-selenium-sauce/sauce_config.rb +56 -0
- data/lib/jasmine-selenium-sauce/selenium_runner.rb +91 -0
- data/lib/jasmine-selenium-sauce/selenium_saucelabs_driver.rb +52 -0
- data/lib/jasmine-selenium-sauce/tasks/railtie.rb +17 -0
- data/lib/jasmine-selenium-sauce/tasks/rake_runner.rb +17 -0
- data/lib/jasmine-selenium-sauce/tasks/sauce.rake +16 -0
- data/lib/jasmine-selenium-sauce/version.rb +7 -0
- data/spec/fixtures/vcr_cassettes/jasmine_failures.yml +319 -0
- data/spec/fixtures/vcr_cassettes/jasmine_success.yml +308 -0
- data/spec/jasmine-selenium-sauce/jasmine_results_spec.rb +40 -0
- data/spec/jasmine-selenium-sauce/sauce_config_spec.rb +120 -0
- data/spec/jasmine-selenium-sauce/selenium_runner_spec.rb +93 -0
- data/spec/jasmine-selenium-sauce/selenium_saucelabs_driver_spec.rb +106 -0
- data/spec/jasmine-selenium-sauce_spec.rb +54 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/support/reporter_fake.rb +31 -0
- data/spec/support/sample_results.rb +79 -0
- data/spec/vcr_helper.rb +11 -0
- metadata +236 -0
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'jasmine_results'
|
3
|
+
require 'sample_results'
|
4
|
+
|
5
|
+
describe Jasmine::Sauce::CI::JasmineResults do
|
6
|
+
|
7
|
+
let(:under_test) { Jasmine::Sauce::CI::JasmineResults.new(suites, suite_results) }
|
8
|
+
|
9
|
+
describe "#suites" do
|
10
|
+
subject { under_test.suites }
|
11
|
+
|
12
|
+
context "when no suites" do
|
13
|
+
let(:suites) { {} }
|
14
|
+
let(:suite_results) { {} }
|
15
|
+
it { should be_empty }
|
16
|
+
end
|
17
|
+
|
18
|
+
context "when suites" do
|
19
|
+
include_context "suites sample"
|
20
|
+
let(:suite_results) { {} }
|
21
|
+
it { should eq(suites) }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "#for_spec_id" do
|
26
|
+
let(:spec_id) { "1" }
|
27
|
+
let(:suites) { {} }
|
28
|
+
subject { under_test.for_spec_id(spec_id) }
|
29
|
+
|
30
|
+
context "when unknown spec id" do
|
31
|
+
let(:suite_results) { {} }
|
32
|
+
it { should be_nil }
|
33
|
+
end
|
34
|
+
|
35
|
+
context "when valid spec id" do
|
36
|
+
include_context "suite result sample"
|
37
|
+
it { should eq(suite_results["1"]) }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'sauce_config'
|
3
|
+
|
4
|
+
describe Jasmine::Sauce::CI::SauceConfig do
|
5
|
+
|
6
|
+
after do
|
7
|
+
ENV.delete('SAUCELABS_URL')
|
8
|
+
ENV.delete('JASMINE_URL')
|
9
|
+
ENV.delete('SAUCE_BROWSER')
|
10
|
+
end
|
11
|
+
|
12
|
+
describe "#validate" do
|
13
|
+
subject { Jasmine::Sauce::CI::SauceConfig.new.validate }
|
14
|
+
|
15
|
+
context "when valid" do
|
16
|
+
before do
|
17
|
+
ENV['SAUCELABS_URL'] = 'sauce'
|
18
|
+
ENV['JASMINE_URL'] = 'jasmine'
|
19
|
+
ENV['SAUCE_BROWSER'] = 'browser'
|
20
|
+
end
|
21
|
+
specify { expect { subject }.not_to raise_error }
|
22
|
+
end
|
23
|
+
|
24
|
+
context "when saucelabs url is not set" do
|
25
|
+
before do
|
26
|
+
ENV['JASMINE_URL'] = 'jasmine'
|
27
|
+
ENV['SAUCE_BROWSER'] = 'browser'
|
28
|
+
end
|
29
|
+
specify { expect { subject }.to raise_error(ArgumentError) }
|
30
|
+
end
|
31
|
+
|
32
|
+
context "when jasmine url is not set" do
|
33
|
+
before do
|
34
|
+
ENV['SAUCELABS_URL'] = 'sauce'
|
35
|
+
ENV['SAUCE_BROWSER'] = 'browser'
|
36
|
+
end
|
37
|
+
specify { expect { subject }.to raise_error(ArgumentError) }
|
38
|
+
end
|
39
|
+
|
40
|
+
context "when browser is not set" do
|
41
|
+
before do
|
42
|
+
ENV['SAUCELABS_URL'] = 'sauce'
|
43
|
+
ENV['JASMINE_URL'] = 'jasmine'
|
44
|
+
end
|
45
|
+
specify { expect { subject }.to raise_error(ArgumentError) }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
shared_examples_for "overridable configuration setting" do |env_setting, default|
|
50
|
+
context "when not specified" do
|
51
|
+
it { should eq(default) }
|
52
|
+
end
|
53
|
+
|
54
|
+
context "when specified" do
|
55
|
+
let(:value) { "random value" }
|
56
|
+
before { ENV[env_setting] = value }
|
57
|
+
after { ENV.delete(env_setting) }
|
58
|
+
it { should eq(value) }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
describe "#saucelabs_server_url" do
|
63
|
+
let(:url) { "http://user:password@ondemand.saucelabs.com:80/wd/hub" }
|
64
|
+
before { ENV['SAUCELABS_URL'] = url }
|
65
|
+
after { ENV.delete('SAUCELABS_URL') }
|
66
|
+
its(:saucelabs_server_url) { should eq(url)}
|
67
|
+
end
|
68
|
+
|
69
|
+
describe "#jasmine_server_url" do
|
70
|
+
let(:url) { "http://my.host.com/jasmine" }
|
71
|
+
before { ENV['JASMINE_URL'] = url }
|
72
|
+
after { ENV.delete('JASMINE_URL') }
|
73
|
+
its(:jasmine_server_url) { should eq(url)}
|
74
|
+
end
|
75
|
+
|
76
|
+
describe "#sauce_browser" do
|
77
|
+
let(:browser) { "Chrome" }
|
78
|
+
before { ENV['SAUCE_BROWSER'] = browser }
|
79
|
+
after { ENV.delete('SAUCE_BROWSER') }
|
80
|
+
its(:browser) { should eq(browser)}
|
81
|
+
end
|
82
|
+
|
83
|
+
describe "#sauce_platform" do
|
84
|
+
context "when not specified" do
|
85
|
+
its(:platform) { should eq(:VISTA) }
|
86
|
+
end
|
87
|
+
|
88
|
+
context "when specified" do
|
89
|
+
let(:platform) { "WIN_7" }
|
90
|
+
before { ENV['SAUCE_PLATFORM'] = platform }
|
91
|
+
after { ENV.delete('SAUCE_PLATFORM') }
|
92
|
+
its(:platform) { should eq(platform.to_sym) }
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
describe "#browser_version" do
|
97
|
+
subject { Jasmine::Sauce::CI::SauceConfig.new.browser_version }
|
98
|
+
it_behaves_like "overridable configuration setting", 'SAUCE_BROWSER_VERSION', nil
|
99
|
+
end
|
100
|
+
|
101
|
+
describe "#record_screenshots" do
|
102
|
+
subject { Jasmine::Sauce::CI::SauceConfig.new.record_screenshots }
|
103
|
+
it_behaves_like "overridable configuration setting", 'SAUCE_SCREENSHOTS', false
|
104
|
+
end
|
105
|
+
|
106
|
+
describe "#record_video" do
|
107
|
+
subject { Jasmine::Sauce::CI::SauceConfig.new.record_video }
|
108
|
+
it_behaves_like "overridable configuration setting", 'SAUCE_VIDEO', false
|
109
|
+
end
|
110
|
+
|
111
|
+
describe "#idle_timeout" do
|
112
|
+
subject { Jasmine::Sauce::CI::SauceConfig.new.idle_timeout }
|
113
|
+
it_behaves_like "overridable configuration setting", 'SAUCE_IDLE_TIMEOUT', 90
|
114
|
+
end
|
115
|
+
|
116
|
+
describe "#max_duration" do
|
117
|
+
subject { Jasmine::Sauce::CI::SauceConfig.new.max_duration }
|
118
|
+
it_behaves_like "overridable configuration setting", 'SAUCE_MAX_DURATION', 180
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'selenium_runner'
|
3
|
+
require 'jasmine_results'
|
4
|
+
require 'sample_results'
|
5
|
+
|
6
|
+
describe Jasmine::Sauce::CI::SeleniumRunner do
|
7
|
+
|
8
|
+
let(:driver) { double("Driver") }
|
9
|
+
let(:url) { "jasmine.url" }
|
10
|
+
let(:under_test) { Jasmine::Sauce::CI::SeleniumRunner.new(driver) }
|
11
|
+
|
12
|
+
describe "#run" do
|
13
|
+
subject { under_test.run(url) }
|
14
|
+
|
15
|
+
let(:load_timeout) { 0.01 }
|
16
|
+
before do
|
17
|
+
driver.should_receive(:connect).with(url)
|
18
|
+
under_test.should_receive(:load_jasmine_timeout).and_return(load_timeout)
|
19
|
+
end
|
20
|
+
|
21
|
+
context "when loading jasmine times out" do
|
22
|
+
before do
|
23
|
+
driver.should_receive(:evaluate_js).and_return(false, false)
|
24
|
+
end
|
25
|
+
specify { expect {subject}.to raise_error("Timed out after #{load_timeout}s waiting for Jasmine to load") }
|
26
|
+
end
|
27
|
+
|
28
|
+
context "when jasmine execution times out" do
|
29
|
+
let(:load_result) { true }
|
30
|
+
let(:suites) { {} }
|
31
|
+
let(:jasmine_finished_result) { false }
|
32
|
+
before do
|
33
|
+
under_test.should_receive(:jasmine_execution_timeout).and_return(load_timeout)
|
34
|
+
driver.should_receive(:evaluate_js).and_return(load_result, suites, jasmine_finished_result, jasmine_finished_result)
|
35
|
+
end
|
36
|
+
specify { expect {subject}.to raise_error("Timed out after #{load_timeout}s waiting for Jasmine to finish") }
|
37
|
+
end
|
38
|
+
|
39
|
+
context "when there are no suites" do
|
40
|
+
let(:load_result) { true }
|
41
|
+
let(:suites) { {} }
|
42
|
+
let(:jasmine_finished_result) { true }
|
43
|
+
let(:suite_results) { {} }
|
44
|
+
|
45
|
+
before do
|
46
|
+
under_test.should_receive(:jasmine_execution_timeout).and_return(load_timeout)
|
47
|
+
driver.should_receive(:evaluate_js).and_return(load_result, suites, jasmine_finished_result)
|
48
|
+
driver.should_receive(:disconnect)
|
49
|
+
end
|
50
|
+
|
51
|
+
it("should return no suites") { subject.suites.should be_empty }
|
52
|
+
it("should return no suite results") { subject.for_spec_id('0').should be_nil }
|
53
|
+
end
|
54
|
+
|
55
|
+
shared_context "selenium runner is successful" do
|
56
|
+
include_context "suites sample"
|
57
|
+
include_context "suite result sample"
|
58
|
+
|
59
|
+
let(:load_result) { true }
|
60
|
+
let(:jasmine_finished_result) { true }
|
61
|
+
|
62
|
+
before do
|
63
|
+
under_test.should_receive(:jasmine_execution_timeout).and_return(load_timeout)
|
64
|
+
driver.should_receive(:disconnect)
|
65
|
+
end
|
66
|
+
|
67
|
+
it("should return suites") { subject.suites.should eq(suites) }
|
68
|
+
it("should return suite results") { subject.for_spec_id('0').should eq(suite_results['0']) }
|
69
|
+
end
|
70
|
+
|
71
|
+
context "when there are suites" do
|
72
|
+
include_context "selenium runner is successful"
|
73
|
+
|
74
|
+
before {
|
75
|
+
driver.should_receive(:evaluate_js).and_return(load_result, suites, jasmine_finished_result, suite_results)
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
context "when suites exceed batch size" do
|
80
|
+
let(:batch_size) { 1 }
|
81
|
+
let(:under_test) { Jasmine::Sauce::CI::SeleniumRunner.new(driver, batch_size) }
|
82
|
+
include_context "selenium runner is successful"
|
83
|
+
|
84
|
+
before {
|
85
|
+
driver.should_receive(:evaluate_js).and_return(load_result, suites, jasmine_finished_result,
|
86
|
+
suite_results.select {|k,_| k == '0'},
|
87
|
+
suite_results.select {|k,_| k == '1'},
|
88
|
+
suite_results.select {|k,_| k == '2'})
|
89
|
+
}
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'sauce_config'
|
3
|
+
require 'selenium_saucelabs_driver'
|
4
|
+
require 'sample_results'
|
5
|
+
|
6
|
+
describe Jasmine::Sauce::CI::SeleniumSauceLabsDriver do
|
7
|
+
|
8
|
+
let(:under_test) { Jasmine::Sauce::CI::SeleniumSauceLabsDriver.new(config) }
|
9
|
+
let(:config) { Jasmine::Sauce::CI::SauceConfig.new }
|
10
|
+
let(:driver) { double("SeleniumWebDriver") }
|
11
|
+
|
12
|
+
shared_context "create driver is stubbed" do
|
13
|
+
before do
|
14
|
+
Jasmine::Sauce::CI::SeleniumSauceLabsDriver.any_instance.should_receive(:create_driver).with(config).and_return(driver)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe "#create_driver" do
|
19
|
+
let(:selenium_client) { double("Selenium::WebDriver::Remote::Http::Default") }
|
20
|
+
let(:timeout) { "client timeout" }
|
21
|
+
let(:sauce_url) { "http://sauce.url" }
|
22
|
+
let(:capabilities) { "desired capabilities" }
|
23
|
+
subject { under_test }
|
24
|
+
before do
|
25
|
+
config.should_receive(:selenium_client_timeout).and_return(timeout)
|
26
|
+
config.should_receive(:saucelabs_server_url).and_return(sauce_url)
|
27
|
+
Selenium::WebDriver::Remote::Http::Default.should_receive(:new).and_return(selenium_client)
|
28
|
+
selenium_client.should_receive(:timeout=).with(timeout)
|
29
|
+
Jasmine::Sauce::CI::SeleniumSauceLabsDriver.any_instance.should_receive(:generate_capabilities).and_return(capabilities)
|
30
|
+
Selenium::WebDriver.should_receive(:for) do | browser, options |
|
31
|
+
browser.should eq(:remote)
|
32
|
+
options[:http_client].should eq(selenium_client)
|
33
|
+
options[:url].should eq(sauce_url)
|
34
|
+
options[:desired_capabilities].should eq(capabilities)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
it("should interact with webdriver correctly") { subject }
|
39
|
+
end
|
40
|
+
|
41
|
+
describe "#connect" do
|
42
|
+
include_context "create driver is stubbed"
|
43
|
+
let(:url) { "http://jasmine.server.url/jasmine" }
|
44
|
+
let(:navigator) { double("SeleniumNavigator") }
|
45
|
+
subject { under_test.connect(url) }
|
46
|
+
before do
|
47
|
+
driver.should_receive(:navigate).and_return(navigator)
|
48
|
+
navigator.should_receive(:to).with(url)
|
49
|
+
end
|
50
|
+
|
51
|
+
specify { expect { subject }.not_to raise_error }
|
52
|
+
end
|
53
|
+
|
54
|
+
describe "#disconnect" do
|
55
|
+
include_context "create driver is stubbed"
|
56
|
+
subject { under_test.disconnect }
|
57
|
+
before do
|
58
|
+
driver.should_receive(:quit)
|
59
|
+
end
|
60
|
+
|
61
|
+
specify { expect { subject }.not_to raise_error }
|
62
|
+
end
|
63
|
+
|
64
|
+
describe "#evaluate_js" do
|
65
|
+
include_context "create driver is stubbed"
|
66
|
+
let(:script) { "the javascript" }
|
67
|
+
subject { under_test.evaluate_js(script) }
|
68
|
+
before do
|
69
|
+
driver.should_receive(:execute_script).and_return(script_result)
|
70
|
+
end
|
71
|
+
|
72
|
+
context "when simple result" do
|
73
|
+
let(:script_result) { "true" }
|
74
|
+
it { should be_true }
|
75
|
+
end
|
76
|
+
|
77
|
+
context "when json result" do
|
78
|
+
include_context "suites sample"
|
79
|
+
let(:script_result) { suites.to_json }
|
80
|
+
it { should eq(JSON.parse(script_result)) }
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe "#generate_capabilities" do
|
85
|
+
include_context "create driver is stubbed"
|
86
|
+
subject { under_test.generate_capabilities(config) }
|
87
|
+
before do
|
88
|
+
config.should_receive(:platform).and_return("platform")
|
89
|
+
config.should_receive(:browser).and_return("browser")
|
90
|
+
config.should_receive(:browser_version).and_return("version")
|
91
|
+
config.should_receive(:record_screenshots).and_return("screens")
|
92
|
+
config.should_receive(:record_video).and_return("video")
|
93
|
+
config.should_receive(:idle_timeout).and_return("idle")
|
94
|
+
config.should_receive(:max_duration).and_return("duration")
|
95
|
+
end
|
96
|
+
|
97
|
+
it { subject['platform'].should eq "platform" }
|
98
|
+
it { subject['browserName'].should eq "browser" }
|
99
|
+
it { subject['browser-version'].should eq "version" }
|
100
|
+
it { subject['record-screenshots'].should eq "screens" }
|
101
|
+
it { subject['record-video'].should eq "video" }
|
102
|
+
it { subject['idle-timeout'].should eq "idle" }
|
103
|
+
it { subject['max-duration'].should eq "duration" }
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
require_relative 'vcr_helper'
|
3
|
+
require 'jasmine-selenium-sauce'
|
4
|
+
require 'sauce_config'
|
5
|
+
require 'reporter_fake'
|
6
|
+
|
7
|
+
describe Jasmine::Sauce::CI::Main do
|
8
|
+
|
9
|
+
describe "#run" do
|
10
|
+
|
11
|
+
let(:config) { Jasmine::Sauce::CI::SauceConfig.new }
|
12
|
+
let(:reporter) { ReporterFake.new }
|
13
|
+
subject { Jasmine::Sauce::CI::Main.run(config, reporter) }
|
14
|
+
|
15
|
+
after do
|
16
|
+
ENV.delete('SAUCELABS_URL')
|
17
|
+
ENV.delete('JASMINE_URL')
|
18
|
+
ENV.delete('SAUCE_BROWSER')
|
19
|
+
end
|
20
|
+
|
21
|
+
context "when there are no failures" do
|
22
|
+
use_vcr_cassette 'jasmine_success', record: :none
|
23
|
+
before do
|
24
|
+
ENV['SAUCELABS_URL'] = 'http://username:password@ondemand.saucelabs.com:80/wd/hub'
|
25
|
+
ENV['JASMINE_URL'] = 'http://jasmine.server.com/jasmine'
|
26
|
+
ENV['SAUCE_BROWSER'] = 'chrome'
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "passing tests" do
|
30
|
+
it { subject[:passed].should eq([0,1,2]) }
|
31
|
+
end
|
32
|
+
describe "failing tests" do
|
33
|
+
it { subject[:failed].should be_empty }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context "when there are failures" do
|
38
|
+
use_vcr_cassette 'jasmine_failures', record: :none
|
39
|
+
before do
|
40
|
+
ENV['SAUCELABS_URL'] = 'http://username:password@ondemand.saucelabs.com:80/wd/hub'
|
41
|
+
ENV['JASMINE_URL'] = 'http://jasmine.server.com/jasmine'
|
42
|
+
ENV['SAUCE_BROWSER'] = 'chrome'
|
43
|
+
end
|
44
|
+
|
45
|
+
describe "passing tests" do
|
46
|
+
it { subject[:passed].should eq([0,2]) }
|
47
|
+
end
|
48
|
+
describe "failing tests" do
|
49
|
+
it { subject[:failed].should eq([1]) }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,6 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib', 'jasmine-selenium-sauce'))
|
3
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
4
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'support'))
|
5
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'fixtures'))
|
6
|
+
|
@@ -0,0 +1,31 @@
|
|
1
|
+
|
2
|
+
class ReporterFake
|
3
|
+
|
4
|
+
def report(jasmine_results)
|
5
|
+
results = { passed: [], failed: []}
|
6
|
+
jasmine_results.suites.each do |suite|
|
7
|
+
child_results = process_children(suite["children"], jasmine_results)
|
8
|
+
results[:passed].concat(child_results[:passed]) if child_results[:passed]
|
9
|
+
results[:failed].concat(child_results[:failed]) if child_results[:failed]
|
10
|
+
end
|
11
|
+
results
|
12
|
+
end
|
13
|
+
|
14
|
+
def process_children(children, jasmine_results)
|
15
|
+
results = { passed: [], failed: []}
|
16
|
+
children.each do |node|
|
17
|
+
type = node["type"]
|
18
|
+
if type == "suite"
|
19
|
+
child_results = process_children(node["children"], jasmine_results)
|
20
|
+
results[:passed].concat(child_results[:passed]) if child_results[:passed]
|
21
|
+
results[:failed].concat(child_results[:failed]) if child_results[:failed]
|
22
|
+
elsif type == "spec"
|
23
|
+
spec_result = jasmine_results.for_spec_id(node["id"].to_s)
|
24
|
+
results[:passed] << node["id"] if spec_result["result"] == "passed"
|
25
|
+
results[:failed] << node["id"] if spec_result["result"] == "failed"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
results
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|