split 3.3.0 → 4.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/.eslintrc +1 -1
  3. data/.github/FUNDING.yml +1 -0
  4. data/.github/ISSUE_TEMPLATE/bug_report.md +24 -0
  5. data/.rspec +1 -0
  6. data/.rubocop.yml +71 -1044
  7. data/.rubocop_todo.yml +226 -0
  8. data/.travis.yml +18 -39
  9. data/Appraisals +4 -0
  10. data/CHANGELOG.md +110 -0
  11. data/CODE_OF_CONDUCT.md +3 -3
  12. data/Gemfile +2 -0
  13. data/README.md +58 -23
  14. data/Rakefile +2 -0
  15. data/gemfiles/{4.2.gemfile → 6.0.gemfile} +1 -1
  16. data/lib/split.rb +16 -3
  17. data/lib/split/algorithms/block_randomization.rb +2 -0
  18. data/lib/split/algorithms/weighted_sample.rb +2 -1
  19. data/lib/split/algorithms/whiplash.rb +3 -2
  20. data/lib/split/alternative.rb +4 -3
  21. data/lib/split/cache.rb +28 -0
  22. data/lib/split/combined_experiments_helper.rb +3 -2
  23. data/lib/split/configuration.rb +15 -14
  24. data/lib/split/dashboard.rb +19 -1
  25. data/lib/split/dashboard/helpers.rb +3 -2
  26. data/lib/split/dashboard/pagination_helpers.rb +4 -4
  27. data/lib/split/dashboard/paginator.rb +1 -0
  28. data/lib/split/dashboard/public/dashboard.js +10 -0
  29. data/lib/split/dashboard/public/style.css +5 -0
  30. data/lib/split/dashboard/views/_controls.erb +13 -0
  31. data/lib/split/dashboard/views/layout.erb +1 -1
  32. data/lib/split/encapsulated_helper.rb +3 -2
  33. data/lib/split/engine.rb +7 -4
  34. data/lib/split/exceptions.rb +1 -0
  35. data/lib/split/experiment.rb +98 -65
  36. data/lib/split/experiment_catalog.rb +1 -3
  37. data/lib/split/extensions/string.rb +1 -0
  38. data/lib/split/goals_collection.rb +2 -0
  39. data/lib/split/helper.rb +30 -10
  40. data/lib/split/metric.rb +2 -1
  41. data/lib/split/persistence.rb +4 -2
  42. data/lib/split/persistence/cookie_adapter.rb +1 -0
  43. data/lib/split/persistence/dual_adapter.rb +54 -12
  44. data/lib/split/persistence/redis_adapter.rb +5 -0
  45. data/lib/split/persistence/session_adapter.rb +1 -0
  46. data/lib/split/redis_interface.rb +9 -28
  47. data/lib/split/trial.rb +25 -17
  48. data/lib/split/user.rb +19 -3
  49. data/lib/split/version.rb +2 -4
  50. data/lib/split/zscore.rb +1 -0
  51. data/spec/alternative_spec.rb +1 -1
  52. data/spec/cache_spec.rb +88 -0
  53. data/spec/configuration_spec.rb +1 -14
  54. data/spec/dashboard/pagination_helpers_spec.rb +3 -1
  55. data/spec/dashboard_helpers_spec.rb +2 -2
  56. data/spec/dashboard_spec.rb +78 -17
  57. data/spec/encapsulated_helper_spec.rb +2 -2
  58. data/spec/experiment_spec.rb +116 -12
  59. data/spec/goals_collection_spec.rb +1 -1
  60. data/spec/helper_spec.rb +191 -112
  61. data/spec/persistence/cookie_adapter_spec.rb +1 -1
  62. data/spec/persistence/dual_adapter_spec.rb +160 -68
  63. data/spec/persistence/redis_adapter_spec.rb +9 -0
  64. data/spec/redis_interface_spec.rb +0 -69
  65. data/spec/spec_helper.rb +5 -6
  66. data/spec/trial_spec.rb +65 -19
  67. data/spec/user_spec.rb +28 -0
  68. data/split.gemspec +9 -9
  69. metadata +34 -28
@@ -52,7 +52,7 @@ describe Split::Persistence::CookieAdapter do
52
52
  it "puts multiple experiments in a single cookie" do
53
53
  subject["foo"] = "FOO"
54
54
  subject["bar"] = "BAR"
55
- expect(context.response.headers["Set-Cookie"]).to match(/\Asplit=%7B%22foo%22%3A%22FOO%22%2C%22bar%22%3A%22BAR%22%7D; path=\/; expires=[a-zA-Z]{3}, \d{2} [a-zA-Z]{3} \d{4} \d{2}:\d{2}:\d{2} -0000\Z/)
55
+ expect(context.response.headers["Set-Cookie"]).to match(/\Asplit=%7B%22foo%22%3A%22FOO%22%2C%22bar%22%3A%22BAR%22%7D; path=\/; expires=[a-zA-Z]{3}, \d{2} [a-zA-Z]{3} \d{4} \d{2}:\d{2}:\d{2} [A-Z]{3}\Z/)
56
56
  end
57
57
 
58
58
  it "ensure other added cookies are not overriden" do
@@ -1,102 +1,194 @@
1
1
  # frozen_string_literal: true
2
- require "spec_helper"
2
+
3
+ require 'spec_helper'
3
4
 
4
5
  describe Split::Persistence::DualAdapter do
6
+ let(:context) { 'some context' }
5
7
 
6
- let(:context){ "some context" }
7
-
8
- let(:just_adapter){ Class.new }
9
- let(:selected_adapter_instance){ double }
10
- let(:selected_adapter){
11
- c = Class.new
12
- expect(c).to receive(:new){ selected_adapter_instance }
13
- c
14
- }
15
- let(:not_selected_adapter){
16
- c = Class.new
17
- expect(c).not_to receive(:new)
18
- c
19
- }
20
-
21
- shared_examples_for "forwarding calls" do
22
- it "#[]=" do
23
- expect(selected_adapter_instance).to receive(:[]=).with('my_key', 'my_value')
24
- expect_any_instance_of(not_selected_adapter).not_to receive(:[]=)
25
- subject["my_key"] = "my_value"
26
- end
8
+ let(:logged_in_adapter_instance) { double }
9
+ let(:logged_in_adapter) do
10
+ Class.new.tap { |c| allow(c).to receive(:new) { logged_in_adapter_instance } }
11
+ end
12
+ let(:logged_out_adapter_instance) { double }
13
+ let(:logged_out_adapter) do
14
+ Class.new.tap { |c| allow(c).to receive(:new) { logged_out_adapter_instance } }
15
+ end
27
16
 
28
- it "#[]" do
29
- expect(selected_adapter_instance).to receive(:[]).with('my_key'){'my_value'}
30
- expect_any_instance_of(not_selected_adapter).not_to receive(:[])
31
- expect(subject["my_key"]).to eq('my_value')
32
- end
17
+ context 'when fallback_to_logged_out_adapter is false' do
18
+ context 'when logged in' do
19
+ subject do
20
+ described_class.with_config(
21
+ logged_in: lambda { |context| true },
22
+ logged_in_adapter: logged_in_adapter,
23
+ logged_out_adapter: logged_out_adapter,
24
+ fallback_to_logged_out_adapter: false
25
+ ).new(context)
26
+ end
27
+
28
+ it '#[]=' do
29
+ expect(logged_in_adapter_instance).to receive(:[]=).with('my_key', 'my_value')
30
+ expect_any_instance_of(logged_out_adapter).not_to receive(:[]=)
31
+ subject['my_key'] = 'my_value'
32
+ end
33
+
34
+ it '#[]' do
35
+ expect(logged_in_adapter_instance).to receive(:[]).with('my_key') { 'my_value' }
36
+ expect_any_instance_of(logged_out_adapter).not_to receive(:[])
37
+ expect(subject['my_key']).to eq('my_value')
38
+ end
33
39
 
34
- it "#delete" do
35
- expect(selected_adapter_instance).to receive(:delete).with('my_key'){'my_value'}
36
- expect_any_instance_of(not_selected_adapter).not_to receive(:delete)
37
- expect(subject.delete("my_key")).to eq('my_value')
40
+ it '#delete' do
41
+ expect(logged_in_adapter_instance).to receive(:delete).with('my_key') { 'my_value' }
42
+ expect_any_instance_of(logged_out_adapter).not_to receive(:delete)
43
+ expect(subject.delete('my_key')).to eq('my_value')
44
+ end
45
+
46
+ it '#keys' do
47
+ expect(logged_in_adapter_instance).to receive(:keys) { ['my_value'] }
48
+ expect_any_instance_of(logged_out_adapter).not_to receive(:keys)
49
+ expect(subject.keys).to eq(['my_value'])
50
+ end
38
51
  end
39
52
 
40
- it "#keys" do
41
- expect(selected_adapter_instance).to receive(:keys){'my_value'}
42
- expect_any_instance_of(not_selected_adapter).not_to receive(:keys)
43
- expect(subject.keys).to eq('my_value')
53
+ context 'when logged out' do
54
+ subject do
55
+ described_class.with_config(
56
+ logged_in: lambda { |context| false },
57
+ logged_in_adapter: logged_in_adapter,
58
+ logged_out_adapter: logged_out_adapter,
59
+ fallback_to_logged_out_adapter: false
60
+ ).new(context)
61
+ end
62
+
63
+ it '#[]=' do
64
+ expect_any_instance_of(logged_in_adapter).not_to receive(:[]=)
65
+ expect(logged_out_adapter_instance).to receive(:[]=).with('my_key', 'my_value')
66
+ subject['my_key'] = 'my_value'
67
+ end
68
+
69
+ it '#[]' do
70
+ expect_any_instance_of(logged_in_adapter).not_to receive(:[])
71
+ expect(logged_out_adapter_instance).to receive(:[]).with('my_key') { 'my_value' }
72
+ expect(subject['my_key']).to eq('my_value')
73
+ end
74
+
75
+ it '#delete' do
76
+ expect_any_instance_of(logged_in_adapter).not_to receive(:delete)
77
+ expect(logged_out_adapter_instance).to receive(:delete).with('my_key') { 'my_value' }
78
+ expect(subject.delete('my_key')).to eq('my_value')
79
+ end
80
+
81
+ it '#keys' do
82
+ expect_any_instance_of(logged_in_adapter).not_to receive(:keys)
83
+ expect(logged_out_adapter_instance).to receive(:keys) { ['my_value', 'my_value2'] }
84
+ expect(subject.keys).to eq(['my_value', 'my_value2'])
85
+ end
44
86
  end
45
87
  end
46
88
 
47
- context "when logged in" do
48
- subject {
49
- described_class.with_config(
50
- logged_in: lambda { |context| true },
51
- logged_in_adapter: selected_adapter,
52
- logged_out_adapter: not_selected_adapter
89
+ context 'when fallback_to_logged_out_adapter is true' do
90
+ context 'when logged in' do
91
+ subject do
92
+ described_class.with_config(
93
+ logged_in: lambda { |context| true },
94
+ logged_in_adapter: logged_in_adapter,
95
+ logged_out_adapter: logged_out_adapter,
96
+ fallback_to_logged_out_adapter: true
53
97
  ).new(context)
54
- }
98
+ end
55
99
 
56
- it_should_behave_like "forwarding calls"
57
- end
100
+ it '#[]=' do
101
+ expect(logged_in_adapter_instance).to receive(:[]=).with('my_key', 'my_value')
102
+ expect(logged_out_adapter_instance).to receive(:[]=).with('my_key', 'my_value')
103
+ expect(logged_out_adapter_instance).to receive(:[]).with('my_key') { nil }
104
+ subject['my_key'] = 'my_value'
105
+ end
106
+
107
+ it '#[]' do
108
+ expect(logged_in_adapter_instance).to receive(:[]).with('my_key') { 'my_value' }
109
+ expect_any_instance_of(logged_out_adapter).not_to receive(:[])
110
+ expect(subject['my_key']).to eq('my_value')
111
+ end
112
+
113
+ it '#delete' do
114
+ expect(logged_in_adapter_instance).to receive(:delete).with('my_key') { 'my_value' }
115
+ expect(logged_out_adapter_instance).to receive(:delete).with('my_key') { 'my_value' }
116
+ expect(subject.delete('my_key')).to eq('my_value')
117
+ end
118
+
119
+ it '#keys' do
120
+ expect(logged_in_adapter_instance).to receive(:keys) { ['my_value'] }
121
+ expect(logged_out_adapter_instance).to receive(:keys) { ['my_value', 'my_value2'] }
122
+ expect(subject.keys).to eq(['my_value', 'my_value2'])
123
+ end
124
+ end
58
125
 
59
- context "when not logged in" do
60
- subject {
61
- described_class.with_config(
62
- logged_in: lambda { |context| false },
63
- logged_in_adapter: not_selected_adapter,
64
- logged_out_adapter: selected_adapter
126
+ context 'when logged out' do
127
+ subject do
128
+ described_class.with_config(
129
+ logged_in: lambda { |context| false },
130
+ logged_in_adapter: logged_in_adapter,
131
+ logged_out_adapter: logged_out_adapter,
132
+ fallback_to_logged_out_adapter: true
65
133
  ).new(context)
66
- }
134
+ end
135
+
136
+ it '#[]=' do
137
+ expect_any_instance_of(logged_in_adapter).not_to receive(:[]=)
138
+ expect(logged_out_adapter_instance).to receive(:[]=).with('my_key', 'my_value')
139
+ expect(logged_out_adapter_instance).to receive(:[]).with('my_key') { nil }
140
+ subject['my_key'] = 'my_value'
141
+ end
142
+
143
+ it '#[]' do
144
+ expect_any_instance_of(logged_in_adapter).not_to receive(:[])
145
+ expect(logged_out_adapter_instance).to receive(:[]).with('my_key') { 'my_value' }
146
+ expect(subject['my_key']).to eq('my_value')
147
+ end
148
+
149
+ it '#delete' do
150
+ expect(logged_in_adapter_instance).to receive(:delete).with('my_key') { 'my_value' }
151
+ expect(logged_out_adapter_instance).to receive(:delete).with('my_key') { 'my_value' }
152
+ expect(subject.delete('my_key')).to eq('my_value')
153
+ end
67
154
 
68
- it_should_behave_like "forwarding calls"
155
+ it '#keys' do
156
+ expect(logged_in_adapter_instance).to receive(:keys) { ['my_value'] }
157
+ expect(logged_out_adapter_instance).to receive(:keys) { ['my_value', 'my_value2'] }
158
+ expect(subject.keys).to eq(['my_value', 'my_value2'])
159
+ end
160
+ end
69
161
  end
70
162
 
71
- describe "when errors in config" do
72
- before{
73
- described_class.config.clear
74
- }
75
- let(:some_proc){ ->{} }
76
- it "when no logged in adapter" do
163
+ describe 'when errors in config' do
164
+ before { described_class.config.clear }
165
+ let(:some_proc) { ->{} }
166
+
167
+ it 'when no logged in adapter' do
77
168
  expect{
78
169
  described_class.with_config(
79
170
  logged_in: some_proc,
80
- logged_out_adapter: just_adapter
81
- ).new(context)
171
+ logged_out_adapter: logged_out_adapter
172
+ ).new(context)
82
173
  }.to raise_error(StandardError, /:logged_in_adapter/)
83
174
  end
84
- it "when no logged out adapter" do
175
+
176
+ it 'when no logged out adapter' do
85
177
  expect{
86
178
  described_class.with_config(
87
179
  logged_in: some_proc,
88
- logged_in_adapter: just_adapter
89
- ).new(context)
180
+ logged_in_adapter: logged_in_adapter
181
+ ).new(context)
90
182
  }.to raise_error(StandardError, /:logged_out_adapter/)
91
183
  end
92
- it "when no logged in detector" do
184
+
185
+ it 'when no logged in detector' do
93
186
  expect{
94
187
  described_class.with_config(
95
- logged_in_adapter: just_adapter,
96
- logged_out_adapter: just_adapter
97
- ).new(context)
188
+ logged_in_adapter: logged_in_adapter,
189
+ logged_out_adapter: logged_out_adapter
190
+ ).new(context)
98
191
  }.to raise_error(StandardError, /:logged_in$/)
99
192
  end
100
193
  end
101
-
102
194
  end
@@ -60,6 +60,15 @@ describe Split::Persistence::RedisAdapter do
60
60
  end
61
61
  end
62
62
 
63
+ describe '#find' do
64
+ before { Split::Persistence::RedisAdapter.with_config(:lookup_by => proc{'frag'}, :namespace => 'a_namespace') }
65
+
66
+ it "should create and user from a given key" do
67
+ adapter = Split::Persistence::RedisAdapter.find(2)
68
+ expect(adapter.redis_key).to eq("a_namespace:2")
69
+ end
70
+ end
71
+
63
72
  context 'functional tests' do
64
73
  before { Split::Persistence::RedisAdapter.with_config(:lookup_by => 'lookup') }
65
74
 
@@ -29,75 +29,6 @@ describe Split::RedisInterface do
29
29
  end
30
30
  end
31
31
 
32
- describe '#add_to_list' do
33
- subject(:add_to_list) do
34
- interface.add_to_list(list_name, 'y')
35
- interface.add_to_list(list_name, 'z')
36
- end
37
-
38
- specify do
39
- add_to_list
40
- expect(Split.redis.lindex(list_name, 0)).to eq 'y'
41
- expect(Split.redis.lindex(list_name, 1)).to eq 'z'
42
- expect(Split.redis.llen(list_name)).to eq 2
43
- end
44
- end
45
-
46
- describe '#set_list_index' do
47
- subject(:set_list_index) do
48
- interface.add_to_list(list_name, 'y')
49
- interface.add_to_list(list_name, 'z')
50
- interface.set_list_index(list_name, 0, 'a')
51
- end
52
-
53
- specify do
54
- set_list_index
55
- expect(Split.redis.lindex(list_name, 0)).to eq 'a'
56
- expect(Split.redis.lindex(list_name, 1)).to eq 'z'
57
- expect(Split.redis.llen(list_name)).to eq 2
58
- end
59
- end
60
-
61
- describe '#list_length' do
62
- subject(:list_length) do
63
- interface.add_to_list(list_name, 'y')
64
- interface.add_to_list(list_name, 'z')
65
- interface.list_length(list_name)
66
- end
67
-
68
- specify do
69
- expect(list_length).to eq 2
70
- end
71
- end
72
-
73
- describe '#remove_last_item_from_list' do
74
- subject(:remove_last_item_from_list) do
75
- interface.add_to_list(list_name, 'y')
76
- interface.add_to_list(list_name, 'z')
77
- interface.remove_last_item_from_list(list_name)
78
- end
79
-
80
- specify do
81
- remove_last_item_from_list
82
- expect(Split.redis.lindex(list_name, 0)).to eq 'y'
83
- expect(Split.redis.llen(list_name)).to eq 1
84
- end
85
- end
86
-
87
- describe '#make_list_length' do
88
- subject(:make_list_length) do
89
- interface.add_to_list(list_name, 'y')
90
- interface.add_to_list(list_name, 'z')
91
- interface.make_list_length(list_name, 1)
92
- end
93
-
94
- specify do
95
- make_list_length
96
- expect(Split.redis.lindex(list_name, 0)).to eq 'y'
97
- expect(Split.redis.llen(list_name)).to eq 1
98
- end
99
- end
100
-
101
32
  describe '#add_to_set' do
102
33
  subject(:add_to_set) do
103
34
  interface.add_to_set(set_name, 'something')
@@ -13,17 +13,16 @@ require 'yaml'
13
13
 
14
14
  Dir['./spec/support/*.rb'].each { |f| require f }
15
15
 
16
- require "fakeredis"
17
-
18
- G_fakeredis = Redis.new
19
-
20
16
  module GlobalSharedContext
21
17
  extend RSpec::SharedContext
22
18
  let(:mock_user){ Split::User.new(double(session: {})) }
19
+
23
20
  before(:each) do
24
21
  Split.configuration = Split::Configuration.new
25
- Split.redis = G_fakeredis
26
- Split.redis.flushall
22
+ Split.redis = Redis.new
23
+ Split.redis.select(10)
24
+ Split.redis.flushdb
25
+ Split::Cache.clear
27
26
  @ab_user = mock_user
28
27
  params = nil
29
28
  end
@@ -176,6 +176,14 @@ describe Split::Trial do
176
176
 
177
177
  expect_alternative(trial, 'basket')
178
178
  end
179
+
180
+ context "when alternative is not found" do
181
+ it "falls back on next_alternative" do
182
+ user[experiment.key] = 'notfound'
183
+ expect(experiment).to receive(:next_alternative).and_call_original
184
+ expect_alternative(trial, alternatives)
185
+ end
186
+ end
179
187
  end
180
188
 
181
189
  context "when user is a new participant" do
@@ -190,42 +198,68 @@ describe Split::Trial do
190
198
  expect(trial.alternative.name).to_not be_empty
191
199
  Split.configuration.on_trial_choose = nil
192
200
  end
201
+
202
+ it "assigns user to an alternative" do
203
+ trial.choose! context
204
+
205
+ expect(alternatives).to include(user[experiment.name])
206
+ end
207
+
208
+ context "when cohorting is disabled" do
209
+ before(:each) { allow(experiment).to receive(:cohorting_disabled?).and_return(true) }
210
+
211
+ it "picks the control and does not run on_trial callbacks" do
212
+ Split.configuration.on_trial = :on_trial_callback
213
+
214
+ expect(experiment).to_not receive(:next_alternative)
215
+ expect(context).not_to receive(:on_trial_callback)
216
+ expect_alternative(trial, 'basket')
217
+
218
+ Split.configuration.enabled = true
219
+ Split.configuration.on_trial = nil
220
+ end
221
+
222
+ it "user is not assigned an alternative" do
223
+ trial.choose! context
224
+
225
+ expect(user[experiment]).to eq(nil)
226
+ end
227
+ end
193
228
  end
194
229
  end
195
230
 
196
231
  describe "#complete!" do
197
- let(:trial) { Split::Trial.new(:user => user, :experiment => experiment) }
198
232
  context 'when there are no goals' do
233
+ let(:trial) { Split::Trial.new(:user => user, :experiment => experiment) }
199
234
  it 'should complete the trial' do
200
235
  trial.choose!
201
236
  old_completed_count = trial.alternative.completed_count
202
237
  trial.complete!
203
- expect(trial.alternative.completed_count).to be(old_completed_count+1)
238
+ expect(trial.alternative.completed_count).to eq(old_completed_count + 1)
204
239
  end
205
240
  end
206
241
 
207
- context 'when there are many goals' do
208
- let(:goals) { ['first', 'second'] }
242
+ context "when there are many goals" do
243
+ let(:goals) { [ "goal1", "goal2" ] }
209
244
  let(:trial) { Split::Trial.new(:user => user, :experiment => experiment, :goals => goals) }
210
- shared_examples_for "goal completion" do
211
- it 'should not complete the trial' do
212
- trial.choose!
213
- old_completed_count = trial.alternative.completed_count
214
- trial.complete!(goal)
215
- expect(trial.alternative.completed_count).to_not be(old_completed_count+1)
216
- end
217
- end
218
245
 
219
- describe 'Array of Goals' do
220
- let(:goal) { [goals.first] }
221
- it_behaves_like 'goal completion'
246
+ it "increments the completed count corresponding to the goals" do
247
+ trial.choose!
248
+ old_completed_counts = goals.map{ |goal| [goal, trial.alternative.completed_count(goal)] }.to_h
249
+ trial.complete!
250
+ goals.each { | goal | expect(trial.alternative.completed_count(goal)).to eq(old_completed_counts[goal] + 1) }
222
251
  end
252
+ end
223
253
 
224
- describe 'String of Goal' do
225
- let(:goal) { goals.first }
226
- it_behaves_like 'goal completion'
254
+ context "when there is 1 goal of type string" do
255
+ let(:goal) { "goal" }
256
+ let(:trial) { Split::Trial.new(:user => user, :experiment => experiment, :goals => goal) }
257
+ it "increments the completed count corresponding to the goal" do
258
+ trial.choose!
259
+ old_completed_count = trial.alternative.completed_count(goal)
260
+ trial.complete!
261
+ expect(trial.alternative.completed_count(goal)).to eq(old_completed_count + 1)
227
262
  end
228
-
229
263
  end
230
264
  end
231
265
 
@@ -275,5 +309,17 @@ describe Split::Trial do
275
309
  trial.choose!
276
310
  end
277
311
  end
312
+
313
+ context 'when experiment has winner' do
314
+ let(:trial) do
315
+ experiment.winner = 'cart'
316
+ Split::Trial.new(:user => user, :experiment => experiment)
317
+ end
318
+
319
+ it 'does not store' do
320
+ expect(user).to_not receive("[]=")
321
+ trial.choose!
322
+ end
323
+ end
278
324
  end
279
325
  end