chefspec 7.3.3 → 7.3.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +23 -0
- data/Rakefile +77 -0
- data/chefspec.gemspec +29 -0
- data/lib/chefspec/version.rb +1 -1
- data/spec/spec_helper.rb +13 -0
- data/spec/support/hash.rb +35 -0
- data/spec/unit/cacher_spec.rb +70 -0
- data/spec/unit/coverage/filters_spec.rb +60 -0
- data/spec/unit/deprecations_spec.rb +53 -0
- data/spec/unit/errors_spec.rb +57 -0
- data/spec/unit/expect_exception_spec.rb +32 -0
- data/spec/unit/macros_spec.rb +119 -0
- data/spec/unit/matchers/do_nothing_matcher.rb +5 -0
- data/spec/unit/matchers/include_recipe_matcher_spec.rb +38 -0
- data/spec/unit/matchers/link_to_matcher_spec.rb +55 -0
- data/spec/unit/matchers/notifications_matcher_spec.rb +40 -0
- data/spec/unit/matchers/render_file_matcher_spec.rb +68 -0
- data/spec/unit/matchers/resource_matcher_spec.rb +5 -0
- data/spec/unit/matchers/state_attrs_matcher_spec.rb +68 -0
- data/spec/unit/matchers/subscribes_matcher_spec.rb +65 -0
- data/spec/unit/renderer_spec.rb +69 -0
- data/spec/unit/server_runner_spec.rb +28 -0
- data/spec/unit/solo_runner_spec.rb +171 -0
- data/spec/unit/stubs/command_registry_spec.rb +27 -0
- data/spec/unit/stubs/command_stub_spec.rb +61 -0
- data/spec/unit/stubs/data_bag_item_registry_spec.rb +39 -0
- data/spec/unit/stubs/data_bag_item_stub_spec.rb +36 -0
- data/spec/unit/stubs/data_bag_registry_spec.rb +39 -0
- data/spec/unit/stubs/data_bag_stub_spec.rb +35 -0
- data/spec/unit/stubs/registry_spec.rb +29 -0
- data/spec/unit/stubs/search_registry_spec.rb +39 -0
- data/spec/unit/stubs/search_stub_spec.rb +36 -0
- data/spec/unit/stubs/stub_spec.rb +64 -0
- metadata +34 -2
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ChefSpec::ExpectException do
|
4
|
+
context 'when there have been no `raise_error` matchers' do
|
5
|
+
subject { described_class.new(Exception) }
|
6
|
+
|
7
|
+
it 'does not match' do
|
8
|
+
allow(RSpec::Matchers::BuiltIn::RaiseError).to receive(:last_run).and_return(nil)
|
9
|
+
expect(subject.expected?).to be_falsy
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
context 'when the last error does not match the expected type' do
|
14
|
+
subject { described_class.new(RuntimeError) }
|
15
|
+
|
16
|
+
it 'does not match' do
|
17
|
+
last_error = double('last error', last_error_for_chefspec: ArgumentError)
|
18
|
+
allow(RSpec::Matchers::BuiltIn::RaiseError).to receive(:last_run).and_return(last_error)
|
19
|
+
expect(subject.expected?).to be_falsy
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
context 'when the last error matches the expected type' do
|
24
|
+
subject { described_class.new(RuntimeError) }
|
25
|
+
|
26
|
+
it 'does not match' do
|
27
|
+
last_error = double('last error', last_error_for_chefspec: RuntimeError)
|
28
|
+
allow(RSpec::Matchers::BuiltIn::RaiseError).to receive(:last_run).and_return(last_error)
|
29
|
+
expect(subject.expected?).to be_truthy
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ChefSpec::API::Stubs do
|
4
|
+
describe '#stub_command' do
|
5
|
+
let(:command_stub) { double('command') }
|
6
|
+
|
7
|
+
it 'adds the command to the command registry' do
|
8
|
+
allow(ChefSpec::Stubs::CommandStub).to receive(:new).and_return(command_stub)
|
9
|
+
stub_command('echo "hello"')
|
10
|
+
|
11
|
+
expect(ChefSpec::Stubs::CommandRegistry.stubs).to include(command_stub)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe '#stub_search' do
|
16
|
+
let(:search_stub) { double('search') }
|
17
|
+
|
18
|
+
it 'adds the query to the search registry' do
|
19
|
+
allow(ChefSpec::Stubs::SearchStub).to receive(:new).and_return(search_stub)
|
20
|
+
stub_search(:node, '*:*')
|
21
|
+
|
22
|
+
expect(ChefSpec::Stubs::SearchRegistry.stubs).to include(search_stub)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '#stub_data_bag' do
|
27
|
+
let(:data_bag_stub) { double('data_bag') }
|
28
|
+
|
29
|
+
it 'adds the query to the data_bag registry' do
|
30
|
+
allow(ChefSpec::Stubs::DataBagStub).to receive(:new).and_return(data_bag_stub)
|
31
|
+
stub_data_bag(:users)
|
32
|
+
|
33
|
+
expect(ChefSpec::Stubs::DataBagRegistry.stubs).to include(data_bag_stub)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe '#stub_data_bag_item' do
|
38
|
+
let(:data_bag_item_stub) { double('data_bag_item') }
|
39
|
+
|
40
|
+
it 'adds the query to the data_bag_item registry' do
|
41
|
+
allow(ChefSpec::Stubs::DataBagItemStub).to receive(:new).and_return(data_bag_item_stub)
|
42
|
+
stub_data_bag_item(:users, 'id')
|
43
|
+
|
44
|
+
expect(ChefSpec::Stubs::DataBagItemRegistry.stubs).to include(data_bag_item_stub)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe '#stub_node' do
|
49
|
+
it 'returns a Chef::Node' do
|
50
|
+
expect(stub_node).to be_a(Chef::Node)
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'defaults the node name to `node.example`' do
|
54
|
+
node = stub_node
|
55
|
+
expect(node.name).to eq('node.example')
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'sets the node name when given' do
|
59
|
+
node = stub_node('example.com')
|
60
|
+
expect(node.name).to eq('example.com')
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'sets the automatic attributes' do
|
64
|
+
node = stub_node
|
65
|
+
expect(node.automatic).to eq(Fauxhai.mock.data)
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'sets the automatic attributes with ohai overrides' do
|
69
|
+
node = stub_node('node.example', ohai: { ipaddress: '1.2.3.4' })
|
70
|
+
expect(node['ipaddress']).to eq('1.2.3.4')
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'sets the automatic attributes for a specific platform and version' do
|
74
|
+
node = stub_node('node.example', platform: 'ubuntu', version: '18.04')
|
75
|
+
expect(node.automatic).to eq(Fauxhai.mock(platform: 'ubuntu', version: '18.04').data)
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'sets the automatic attributes from a JSON data path' do
|
79
|
+
allow(File).to receive(:exist?).with('/path/to/json').and_return(true)
|
80
|
+
allow(File).to receive(:read).with('/path/to/json').and_return('{ "ipaddress": "1.2.3.4" }')
|
81
|
+
node = stub_node('node.example', path: '/path/to/json')
|
82
|
+
expect(node['ipaddress']).to eq('1.2.3.4')
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'yields a block' do
|
86
|
+
expect { |block| stub_node(&block) }.to yield_with_args(Chef::Node)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe 'nginx::source' do
|
92
|
+
describe '#described_cookbook' do
|
93
|
+
describe 'nginx::source' do
|
94
|
+
it 'returns the name of the cookbook' do
|
95
|
+
expect(described_cookbook).to eq('nginx')
|
96
|
+
end
|
97
|
+
|
98
|
+
context 'in a nested context' do
|
99
|
+
it 'still returns the name of the cookbook' do
|
100
|
+
expect(described_cookbook).to eq('nginx')
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
describe '#described_recipe' do
|
107
|
+
describe 'nginx::source' do
|
108
|
+
it 'returns the cookbook::recipe' do
|
109
|
+
expect(described_recipe).to eq('nginx::source')
|
110
|
+
end
|
111
|
+
|
112
|
+
context 'in a nested context' do
|
113
|
+
it 'still retrns the cookbook::recipe' do
|
114
|
+
expect(described_recipe).to eq('nginx::source')
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ChefSpec::Matchers::IncludeRecipeMatcher do
|
4
|
+
let(:chef_run) { double('chef run', run_context: { loaded_recipes: %w(one two three) }) }
|
5
|
+
subject { described_class.new('one::default') }
|
6
|
+
|
7
|
+
describe '#failure_message' do
|
8
|
+
it 'has the right value' do
|
9
|
+
subject.matches?(chef_run)
|
10
|
+
expect(subject.failure_message)
|
11
|
+
.to eq(%q(expected ["one::default", "two::default", "three::default"] to include "one::default"))
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe '#failure_message_when_negated' do
|
16
|
+
it 'has the right value' do
|
17
|
+
subject.matches?(chef_run)
|
18
|
+
expect(subject.failure_message_when_negated)
|
19
|
+
.to eq(%q(expected "one::default" to not be included))
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe '#description' do
|
24
|
+
it 'has the right value' do
|
25
|
+
subject.matches?(chef_run)
|
26
|
+
expect(subject.description).to eq(%q(include recipe "one::default"))
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'matches when the recipe is included' do
|
31
|
+
expect(subject.matches?(chef_run)).to be_truthy
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'does not match when the recipe is not included' do
|
35
|
+
failure = described_class.new('nope')
|
36
|
+
expect(failure.matches?(chef_run)).to be_falsy
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ChefSpec::Matchers::LinkToMatcher do
|
4
|
+
let(:from) { '/var/www' }
|
5
|
+
let(:to) { '/var/html' }
|
6
|
+
let(:link) do
|
7
|
+
Chef::Resource::Link.new(from).tap do |link|
|
8
|
+
link.to(to)
|
9
|
+
link.perform_action(:create)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
subject { described_class.new(to) }
|
13
|
+
|
14
|
+
describe '#failure_message' do
|
15
|
+
it 'has the right value' do
|
16
|
+
subject.matches?(link)
|
17
|
+
expect(subject.failure_message)
|
18
|
+
.to eq(%Q(expected "link[#{from}]" to link to "#{to}" but was "#{to}"))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe '#failure_message_when_negated' do
|
23
|
+
it 'has the right value' do
|
24
|
+
subject.matches?(link)
|
25
|
+
expect(subject.failure_message_when_negated)
|
26
|
+
.to eq(%Q(expected "link[#{from}]" to not link to "#{to}"))
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe '#description' do
|
31
|
+
it 'has the right value' do
|
32
|
+
subject.matches?(link)
|
33
|
+
expect(subject.description).to eq(%Q(link to "#{to}"))
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context 'when the link is correct' do
|
38
|
+
it 'matches' do
|
39
|
+
expect(subject.matches?(link)).to be_truthy
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'adds the link to the coverage report' do
|
43
|
+
expect(ChefSpec::Coverage).to receive(:cover!).with(link)
|
44
|
+
subject.matches?(link)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context 'when the link is not correct' do
|
49
|
+
subject { described_class.new('/nope/bad/path/bro') }
|
50
|
+
|
51
|
+
it 'does not match' do
|
52
|
+
expect(subject.matches?(link)).to be_falsy
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ChefSpec::Matchers::NotificationsMatcher do
|
4
|
+
subject { described_class.new('execute[install]') }
|
5
|
+
let(:package) do
|
6
|
+
double('package',
|
7
|
+
name: 'package',
|
8
|
+
to_s: 'package[foo]',
|
9
|
+
is_a?: true,
|
10
|
+
performed_action?: true,
|
11
|
+
immediate_notifications: [],
|
12
|
+
delayed_notifications: [],
|
13
|
+
before_notifications: []
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
describe '#failure_message' do
|
18
|
+
it 'has the right value' do
|
19
|
+
subject.matches?(package)
|
20
|
+
expect(subject.failure_message)
|
21
|
+
.to include %|expected "package[foo]" to notify "execute[install]", but did not.|
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe '#failure_message_when_negated' do
|
26
|
+
it 'has the right value' do
|
27
|
+
subject.matches?(package)
|
28
|
+
expect(subject.failure_message_when_negated)
|
29
|
+
.to eq %|expected "package[foo]" to not notify "execute[install]", but it did.|
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe '#description' do
|
34
|
+
it 'has the right value' do
|
35
|
+
subject.matches?(package)
|
36
|
+
expect(subject.description)
|
37
|
+
.to eq %|notify "execute[install]"|
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ChefSpec::Matchers::RenderFileMatcher do
|
4
|
+
let(:path) { '/tmp/thing' }
|
5
|
+
let(:file) { double('file', to: path, to_s: "file[#{path}]", performed_action?: true) }
|
6
|
+
let(:chef_run) { double('chef run', find_resource: file) }
|
7
|
+
subject { described_class.new(path) }
|
8
|
+
|
9
|
+
describe '#with_content' do
|
10
|
+
it 'accepts do/end syntax' do
|
11
|
+
subject.matches?(chef_run)
|
12
|
+
expect(
|
13
|
+
subject.with_content do |content|
|
14
|
+
'Does not raise ArgumentError'
|
15
|
+
end.expected_content.first.call
|
16
|
+
).to eq('Does not raise ArgumentError')
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe '#failure_message' do
|
21
|
+
it 'has the right value' do
|
22
|
+
subject.matches?(chef_run)
|
23
|
+
expect(subject.failure_message)
|
24
|
+
.to eq(%Q(expected Chef run to render "#{path}"))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe '#failure_message_when_negated' do
|
29
|
+
it 'has the right value' do
|
30
|
+
subject.matches?(chef_run)
|
31
|
+
expect(subject.failure_message_when_negated)
|
32
|
+
.to eq(%Q(expected file "#{path}" to not be in Chef run))
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe '#description' do
|
37
|
+
it 'has the right value' do
|
38
|
+
subject.matches?(chef_run)
|
39
|
+
expect(subject.description).to eq(%Q(render file "#{path}"))
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'has the right value when with_content is chained' do
|
43
|
+
subject.matches?(chef_run)
|
44
|
+
expect(
|
45
|
+
subject.with_content('foo').with_content('bar').description
|
46
|
+
).to eq(%Q(render file "#{path}" with content "foo" with content "bar"))
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context 'when the file is correct' do
|
51
|
+
it 'matches' do
|
52
|
+
expect(subject.matches?(chef_run)).to be_truthy
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'adds the resource to the coverage report' do
|
56
|
+
expect(ChefSpec::Coverage).to receive(:cover!).with(file)
|
57
|
+
subject.matches?(chef_run)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
context 'when the file is not correct' do
|
62
|
+
it 'does not match' do
|
63
|
+
allow(chef_run).to receive(:find_resource).and_return(nil)
|
64
|
+
failure = described_class.new('nope')
|
65
|
+
expect(failure.matches?(chef_run)).to be_falsy
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ChefSpec::Matchers::StateAttrsMatcher do
|
4
|
+
subject { described_class.new([:a, :b]) }
|
5
|
+
|
6
|
+
context 'when the resource does not exist' do
|
7
|
+
let(:resource) { nil }
|
8
|
+
before { subject.matches?(resource) }
|
9
|
+
|
10
|
+
it 'does not match' do
|
11
|
+
expect(subject).to_not be_matches(resource)
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'has the correct description' do
|
15
|
+
expect(subject.description).to eq('have state attributes [:a, :b]')
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'has the correct failure message for should' do
|
19
|
+
expect(subject.failure_message).to include <<-EOH.gsub(/^ {8}/, '')
|
20
|
+
expected _something_ to have state attributes, but the _something_ you gave me was nil!
|
21
|
+
Ensure the resource exists before making assertions:
|
22
|
+
|
23
|
+
expect(resource).to be
|
24
|
+
EOH
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'has the correct failure message for should not' do
|
28
|
+
expect(subject.failure_message_when_negated).to include <<-EOH.gsub(/^ {8}/, '')
|
29
|
+
expected _something_ to not have state attributes, but the _something_ you gave me was nil!
|
30
|
+
Ensure the resource exists before making assertions:
|
31
|
+
|
32
|
+
expect(resource).to be
|
33
|
+
EOH
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context 'when the resource exists' do
|
38
|
+
let(:klass) { double('class', state_attrs: [:a, :b]) }
|
39
|
+
let(:resource) { double('resource', class: klass) }
|
40
|
+
before { subject.matches?(resource) }
|
41
|
+
|
42
|
+
it 'has the correct description' do
|
43
|
+
expect(subject.description).to eq('have state attributes [:a, :b]')
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'has the correct failure message for should' do
|
47
|
+
expect(subject.failure_message).to eq('expected [:a, :b] to equal [:a, :b]')
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'has the correct failure message for should not' do
|
51
|
+
expect(subject.failure_message_when_negated).to eq('expected [:a, :b] to not equal [:a, :b]')
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'matches when the state attributes are correct' do
|
55
|
+
expect(subject).to be_matches(resource)
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'does not match when the state attributes are incorrect' do
|
59
|
+
allow(klass).to receive(:state_attrs).and_return([:c, :d])
|
60
|
+
expect(subject).to_not be_matches(resource)
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'does not match when partial state attribute are incorrect' do
|
64
|
+
allow(klass).to receive(:state_attrs).and_return([:b, :c])
|
65
|
+
expect(subject).to_not be_matches(resource)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ChefSpec::Matchers::SubscribesMatcher do
|
4
|
+
subject { described_class.new('execute[install]') }
|
5
|
+
let(:runner) { double('runner', find_resource: nil) }
|
6
|
+
let(:run_context) { double('run_context', node: node) }
|
7
|
+
let(:node) { double('node', runner: runner) }
|
8
|
+
let(:package) do
|
9
|
+
double('package',
|
10
|
+
name: 'package',
|
11
|
+
to_s: 'package[foo]',
|
12
|
+
run_context: run_context,
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
context 'when no resource is found' do
|
17
|
+
describe '#failure_message' do
|
18
|
+
it 'has the right value' do
|
19
|
+
subject.matches?(package)
|
20
|
+
expect(subject.failure_message)
|
21
|
+
.to include %|expected _something_ to notify "package[foo]", but the _something_ you gave me was nil! If you are running a test like:|
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'when the resource exists' do
|
27
|
+
let(:execute) do
|
28
|
+
double('execute',
|
29
|
+
name: 'execute',
|
30
|
+
to_s: 'execute[install]',
|
31
|
+
immediate_notifications: [],
|
32
|
+
delayed_notifications: [],
|
33
|
+
before_notifications: []
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
before do
|
38
|
+
allow(runner).to receive(:find_resource).and_return(execute)
|
39
|
+
end
|
40
|
+
|
41
|
+
describe '#failure_message' do
|
42
|
+
it 'has the right value' do
|
43
|
+
subject.matches?(package)
|
44
|
+
expect(subject.failure_message)
|
45
|
+
.to include %|expected "execute[install]" to notify "package[foo]", but did not.|
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe '#failure_message_when_negated' do
|
50
|
+
it 'has the right value' do
|
51
|
+
subject.matches?(package)
|
52
|
+
expect(subject.failure_message_when_negated)
|
53
|
+
.to eq %|expected "execute[install]" to not notify "package[foo]", but it did.|
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe '#description' do
|
59
|
+
it 'has the right value' do
|
60
|
+
subject.matches?(package)
|
61
|
+
expect(subject.description)
|
62
|
+
.to eq %|notify "package[foo]"|
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|