abstract_feature_branch 0.7.1 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +138 -37
  3. data/VERSION +1 -1
  4. data/abstract_feature_branch.gemspec +26 -11
  5. data/config/features/admin.local.yml +2 -1
  6. data/config/features/admin.yml +2 -1
  7. data/config/features/internal/wiki.local.yml +2 -1
  8. data/config/features/internal/wiki.yml +2 -1
  9. data/config/features/public.local.yml +2 -1
  10. data/config/features/public.yml +2 -1
  11. data/lib/abstract_feature_branch.rb +14 -3
  12. data/lib/abstract_feature_branch/file_beautifier.rb +63 -0
  13. data/lib/generators/abstract_feature_branch/install_generator.rb +1 -0
  14. data/lib/generators/templates/config/features.local.yml +1 -0
  15. data/lib/generators/templates/config/initializers/abstract_feature_branch.rb +3 -0
  16. data/lib/generators/templates/lib/tasks/abstract_feature_branch.rake +9 -0
  17. data/spec/abstract_feature_branch/file_beautifier_spec.rb +384 -0
  18. data/spec/ext/feature_branch__feature_branch_spec.rb +145 -0
  19. data/spec/ext/feature_branch__feature_enabled_spec.rb +84 -0
  20. data/spec/{application_no_config → fixtures/application_no_config}/no_config +0 -0
  21. data/spec/{application_rails_config → fixtures/application_rails_config}/config/features.local.yml +1 -0
  22. data/spec/{application_rails_config → fixtures/application_rails_config}/config/features.yml +0 -0
  23. data/spec/fixtures/application_ugly_config_reference/config/another_application_configuration.yml +31 -0
  24. data/spec/fixtures/application_ugly_config_reference/config/database.yml +17 -0
  25. data/spec/fixtures/application_ugly_config_reference/config/features.local.yml +44 -0
  26. data/spec/fixtures/application_ugly_config_reference/config/features.yml +49 -0
  27. data/spec/fixtures/application_ugly_config_reference/config/features/admin.local.yml +44 -0
  28. data/spec/fixtures/application_ugly_config_reference/config/features/admin.yml +44 -0
  29. data/spec/fixtures/application_ugly_config_reference/config/features/empty.local.yml +0 -0
  30. data/spec/fixtures/application_ugly_config_reference/config/features/feature_empty_config.local.yml +13 -0
  31. data/spec/fixtures/application_ugly_config_reference/config/features/including_comments.local.yml +52 -0
  32. data/spec/fixtures/application_ugly_config_reference/config/features/internal/wiki.local.yml +44 -0
  33. data/spec/fixtures/application_ugly_config_reference/config/features/internal/wiki.yml +44 -0
  34. data/spec/fixtures/application_ugly_config_reference/config/features/public.local.yml +44 -0
  35. data/spec/fixtures/application_ugly_config_reference/config/features/public.yml +44 -0
  36. metadata +46 -32
@@ -5,6 +5,7 @@ module AbstractFeatureBranch
5
5
 
6
6
  desc "Installs Abstract Feature Branch by generating basic configuration files, including git ignored local one."
7
7
  def copy_config
8
+ template "lib/tasks/abstract_feature_branch.rake", "lib/tasks/abstract_feature_branch.rake"
8
9
  template "config/initializers/abstract_feature_branch.rb", "config/initializers/abstract_feature_branch.rb"
9
10
  template "config/features.example.yml", "config/features.yml"
10
11
  template "config/features.local.yml", "config/features.local.yml"
@@ -2,6 +2,7 @@
2
2
  # It is recommended to use this file only for temporary overrides. Once done, make final change in main .yml
3
3
  defaults: &defaults
4
4
 
5
+
5
6
  development:
6
7
  <<: *defaults
7
8
 
@@ -4,5 +4,8 @@ AbstractFeatureBranch.application_root = Rails.root
4
4
  # Application environment (e.g. "development", "staging" or "production")
5
5
  AbstractFeatureBranch.application_environment = Rails.env.to_s
6
6
 
7
+ # Abstract Feature Branch logger
8
+ AbstractFeatureBranch.logger = Rails.logger
9
+
7
10
  # Pre-loads application features to improve performance of first web-page hit
8
11
  AbstractFeatureBranch.load_application_features
@@ -0,0 +1,9 @@
1
+ require 'abstract_feature_branch'
2
+ namespace :abstract_feature_branch do
3
+
4
+ desc "Beautify YAML of specified feature file via file_path argument or all feature files when no argument specified (config/features.yml, config/features.local.yml, and config/features/**/*.yml) by sorting features by name and eliminating extra empty lines"
5
+ task :beautify_files, :file_path do |_, args|
6
+ AbstractFeatureBranch::FileBeautifier.process(args[:file_path])
7
+ end
8
+
9
+ end
@@ -0,0 +1,384 @@
1
+ require 'spec_helper'
2
+
3
+ describe AbstractFeatureBranch::FileBeautifier do
4
+ describe '#process' do
5
+ before do
6
+ @ugly_config_application_root = File.expand_path(File.join(__FILE__, '..', '..', 'fixtures', 'application_ugly_config'))
7
+ @ugly_config_application_reference_root = File.expand_path(File.join(__FILE__, '..', '..', 'fixtures', 'application_ugly_config_reference'))
8
+ FileUtils.rm_rf( @ugly_config_application_root)
9
+ FileUtils.cp_r(@ugly_config_application_reference_root, @ugly_config_application_root)
10
+ end
11
+ after do
12
+ FileUtils.rm_rf( @ugly_config_application_root)
13
+ end
14
+
15
+ context "a file is specified" do
16
+ it 'gets rid of extra empty lines' do
17
+ feature_file_path = File.expand_path(File.join(__FILE__, '..', '..', 'fixtures', 'application_ugly_config', 'config', 'features.yml'))
18
+ AbstractFeatureBranch::FileBeautifier.process(feature_file_path)
19
+ File.open(feature_file_path, 'r') do |file|
20
+ file.readlines.join.should == <<-EXPECTED_FILE_CONTENT
21
+ defaults: &defaults
22
+ FEATURE1: true
23
+ Feature2: true
24
+ feature3: false
25
+ feature4: true
26
+ feature4a: true
27
+
28
+ development:
29
+ <<: *defaults
30
+ FEATURE1: true
31
+ Feature2: true
32
+ feature3: false
33
+ feature4: true
34
+ feature4a: true
35
+
36
+ test:
37
+ <<: *defaults
38
+ FEATURE1: true
39
+ Feature2: true
40
+ feature3: false
41
+ feature4: true
42
+ feature4a: true
43
+
44
+ staging:
45
+ <<: *defaults
46
+ FEATURE1: true
47
+ Feature2: true
48
+ feature3: false
49
+ feature4: true
50
+ feature4a: true
51
+
52
+ production:
53
+ <<: *defaults
54
+ FEATURE1: true
55
+ Feature2: true
56
+ feature3: false
57
+ feature4: true
58
+ feature4a: true
59
+
60
+ EXPECTED_FILE_CONTENT
61
+ end
62
+ end
63
+
64
+ it 'sorts features by name under each section (e.g. environment)' do
65
+ local_feature_file_path = File.expand_path(File.join(__FILE__, '..', '..', 'fixtures', 'application_ugly_config', 'config', 'features.local.yml'))
66
+ AbstractFeatureBranch::FileBeautifier.process(local_feature_file_path)
67
+ File.open(local_feature_file_path, 'r') do |file|
68
+ file.readlines.join.should == <<-EXPECTED_FILE_CONTENT
69
+ defaults: &defaults
70
+ FEATURE1: true
71
+ Feature2: true
72
+ feature3: false
73
+ feature4: true
74
+ feature4a: true
75
+
76
+ development:
77
+ <<: *defaults
78
+ FEATURE1: true
79
+ Feature2: true
80
+ feature3: false
81
+ feature4: true
82
+ feature4a: true
83
+
84
+ test:
85
+ <<: *defaults
86
+ FEATURE1: true
87
+ Feature2: true
88
+ feature3: false
89
+ feature4: true
90
+ feature4a: true
91
+
92
+ staging:
93
+ <<: *defaults
94
+ FEATURE1: true
95
+ Feature2: true
96
+ feature3: false
97
+ feature4: true
98
+ feature4a: true
99
+
100
+ production:
101
+ <<: *defaults
102
+ FEATURE1: true
103
+ Feature2: true
104
+ feature3: false
105
+ feature4: true
106
+ feature4a: true
107
+
108
+ EXPECTED_FILE_CONTENT
109
+ end
110
+ end
111
+
112
+ it 'handles comments by ignoring comments on top and deleting comments in the middle' do
113
+ feature_file_path = File.expand_path(File.join(__FILE__, '..', '..', 'fixtures', 'application_ugly_config', 'config', 'features', 'including_comments.local.yml'))
114
+ AbstractFeatureBranch::FileBeautifier.process(feature_file_path)
115
+ File.open(feature_file_path, 'r') do |file|
116
+ file.readlines.join.should == <<-EXPECTED_FILE_CONTENT
117
+ # This file allows you to override any feature configuration locally without it being committed to git
118
+ # It is recommended to use this file only for temporary overrides. Once done, make final change in main .yml
119
+ defaults: &defaults
120
+ FEATURE1: true
121
+ Feature2: true
122
+ feature3: false
123
+ feature4: true
124
+ feature4a: true
125
+
126
+ development:
127
+ <<: *defaults
128
+ FEATURE1: true
129
+ Feature2: true
130
+ feature3: false
131
+ feature4: true
132
+ feature4a: true
133
+
134
+ test:
135
+ <<: *defaults
136
+ FEATURE1: true
137
+ Feature2: true
138
+ feature3: false
139
+ feature4: true
140
+ feature4a: true
141
+
142
+ staging:
143
+ <<: *defaults
144
+ FEATURE1: true
145
+ Feature2: true
146
+ feature3: false
147
+ feature4: true
148
+ feature4a: true
149
+
150
+ production:
151
+ <<: *defaults
152
+ FEATURE1: true
153
+ Feature2: true
154
+ feature3: false
155
+ feature4: true
156
+ feature4a: true
157
+
158
+ EXPECTED_FILE_CONTENT
159
+ end
160
+ end
161
+
162
+ it 'processes a feature empty config file' do
163
+ feature_file_path = File.expand_path(File.join(__FILE__, '..', '..', 'fixtures', 'application_ugly_config', 'config', 'features', 'feature_empty_config.local.yml'))
164
+ AbstractFeatureBranch::FileBeautifier.process(feature_file_path)
165
+ File.open(feature_file_path, 'r') do |file|
166
+ file.readlines.join.should == <<-EXPECTED_FILE_CONTENT
167
+ defaults: &defaults
168
+
169
+
170
+ development:
171
+ <<: *defaults
172
+
173
+ test:
174
+ <<: *defaults
175
+
176
+ staging:
177
+ <<: *defaults
178
+
179
+ production:
180
+ <<: *defaults
181
+
182
+ EXPECTED_FILE_CONTENT
183
+ end
184
+ end
185
+
186
+ it 'processes an empty file without change or exceptions' do
187
+ feature_file_path = File.expand_path(File.join(__FILE__, '..', '..', 'fixtures', 'application_ugly_config', 'config', 'features', 'empty.local.yml'))
188
+ AbstractFeatureBranch::FileBeautifier.process(feature_file_path)
189
+ File.open(feature_file_path, 'r') do |file|
190
+ file.readlines.join.should be_empty
191
+ end
192
+ end
193
+
194
+ end
195
+
196
+ context "a directory is specified" do
197
+ it 'beautifies all YAML files under specified directory recursively' do
198
+ feature_directory_path = File.expand_path(File.join(__FILE__, '..', '..', 'fixtures', 'application_ugly_config', 'config', 'features'))
199
+ AbstractFeatureBranch::FileBeautifier.process(feature_directory_path)
200
+
201
+ ['public.yml', 'public.local.yml', 'admin.yml', 'admin.local.yml', 'internal/wiki.yml', 'internal/wiki.local.yml'].each do |file_path_suffix|
202
+ file_path = File.join(feature_directory_path, file_path_suffix)
203
+ File.open(file_path, 'r') do |file|
204
+ file.readlines.join.should == <<-EXPECTED_FILE_CONTENT
205
+ defaults: &defaults
206
+ FEATURE1: true
207
+ Feature2: true
208
+ feature3: false
209
+ feature4: true
210
+ feature4a: true
211
+
212
+ development:
213
+ <<: *defaults
214
+ FEATURE1: true
215
+ Feature2: true
216
+ feature3: false
217
+ feature4: true
218
+ feature4a: true
219
+
220
+ test:
221
+ <<: *defaults
222
+ FEATURE1: true
223
+ Feature2: true
224
+ feature3: false
225
+ feature4: true
226
+ feature4a: true
227
+
228
+ staging:
229
+ <<: *defaults
230
+ FEATURE1: true
231
+ Feature2: true
232
+ feature3: false
233
+ feature4: true
234
+ feature4a: true
235
+
236
+ production:
237
+ <<: *defaults
238
+ FEATURE1: true
239
+ Feature2: true
240
+ feature3: false
241
+ feature4: true
242
+ feature4a: true
243
+
244
+ EXPECTED_FILE_CONTENT
245
+ end
246
+ end
247
+ end
248
+
249
+ context "no file or directory is specified (process all feature files)" do
250
+ after do
251
+ AbstractFeatureBranch.initialize_application_root
252
+ AbstractFeatureBranch.load_application_features
253
+ end
254
+ it 'beautifies all feature files in the application' do
255
+ AbstractFeatureBranch.application_root = File.expand_path(File.join(__FILE__, '..', '..', 'fixtures', 'application_ugly_config'))
256
+ AbstractFeatureBranch.load_application_features
257
+ AbstractFeatureBranch::FileBeautifier.process
258
+
259
+ [
260
+ 'features.yml',
261
+ 'features.local.yml',
262
+ 'features/public.yml',
263
+ 'features/public.local.yml',
264
+ 'features/admin.yml',
265
+ 'features/admin.local.yml',
266
+ 'features/internal/wiki.yml',
267
+ 'features/internal/wiki.local.yml'
268
+ ].each do |file_path_suffix|
269
+ file_path = File.join(AbstractFeatureBranch.application_root, 'config', file_path_suffix)
270
+ File.open(file_path, 'r') do |file|
271
+ file.readlines.join.should == <<-EXPECTED_FILE_CONTENT
272
+ defaults: &defaults
273
+ FEATURE1: true
274
+ Feature2: true
275
+ feature3: false
276
+ feature4: true
277
+ feature4a: true
278
+
279
+ development:
280
+ <<: *defaults
281
+ FEATURE1: true
282
+ Feature2: true
283
+ feature3: false
284
+ feature4: true
285
+ feature4a: true
286
+
287
+ test:
288
+ <<: *defaults
289
+ FEATURE1: true
290
+ Feature2: true
291
+ feature3: false
292
+ feature4: true
293
+ feature4a: true
294
+
295
+ staging:
296
+ <<: *defaults
297
+ FEATURE1: true
298
+ Feature2: true
299
+ feature3: false
300
+ feature4: true
301
+ feature4a: true
302
+
303
+ production:
304
+ <<: *defaults
305
+ FEATURE1: true
306
+ Feature2: true
307
+ feature3: false
308
+ feature4: true
309
+ feature4a: true
310
+
311
+ EXPECTED_FILE_CONTENT
312
+ end
313
+ end
314
+ end
315
+
316
+ it 'does not beautify non-feature files in the application' do
317
+ AbstractFeatureBranch.application_root = File.expand_path(File.join(__FILE__, '..', '..', 'fixtures', 'application_ugly_config'))
318
+ AbstractFeatureBranch.load_application_features
319
+ AbstractFeatureBranch::FileBeautifier.process
320
+
321
+ file_path = File.join(AbstractFeatureBranch.application_root, 'config', 'another_application_configuration.yml')
322
+ File.open(file_path, 'r') do |file|
323
+ file.readlines.join.should == <<-ANOTHER_APPLICATION_CONFIGURATION_CONTENT
324
+ common: &default_settings
325
+ license_key: <%= ENV["LICENSE_KEY"] %>
326
+ app_name: <%= ENV["APP_NAME"] %>
327
+ monitor_mode: true
328
+ developer_mode: false
329
+ log_level: info
330
+
331
+ browser_monitoring:
332
+ auto_instrument: true
333
+
334
+ audit_log:
335
+ enabled: false
336
+
337
+
338
+ development:
339
+ <<: *default_settings
340
+ monitor_mode: false
341
+ developer_mode: true
342
+
343
+ test:
344
+ <<: *default_settings
345
+ monitor_mode: false
346
+
347
+ production:
348
+ <<: *default_settings
349
+ monitor_mode: true
350
+
351
+ staging:
352
+ <<: *default_settings
353
+ monitor_mode: true
354
+ app_name: <%= ENV["APP_NAME"] %> (Staging)
355
+ ANOTHER_APPLICATION_CONFIGURATION_CONTENT
356
+ end
357
+
358
+ file_path = File.join(AbstractFeatureBranch.application_root, 'config', 'database.yml')
359
+ File.open(file_path, 'r') do |file|
360
+ file.readlines.join.should == <<-DATABASE_CONFIGURATION_CONTENT
361
+ development:
362
+ adapter: sqlite3
363
+ database: db/development.sqlite3
364
+ pool: 5
365
+ timeout: 5000
366
+
367
+ test:
368
+ adapter: sqlite3
369
+ database: db/test.sqlite3
370
+ pool: 5
371
+ timeout: 5000
372
+
373
+ production:
374
+ adapter: sqlite3
375
+ database: db/production.sqlite3
376
+ pool: 5
377
+ timeout: 5000
378
+ DATABASE_CONFIGURATION_CONTENT
379
+ end
380
+ end
381
+ end
382
+ end
383
+ end
384
+ end
@@ -0,0 +1,145 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'feature_branch object extensions' do
4
+ before do
5
+ @app_env_backup = AbstractFeatureBranch.application_environment
6
+ AbstractFeatureBranch.logger.warn 'Environment variable ABSTRACT_FEATURE_BRANCH_FEATURE1 already set, potentially conflicting with another test' if ENV.keys.include?('ABSTRACT_FEATURE_BRANCH_FEATURE1')
7
+ AbstractFeatureBranch.logger.warn 'Environment variable Abstract_Feature_Branch_Feature2 already set, potentially conflicting with another test' if ENV.keys.include?('Abstract_Feature_Branch_Feature2')
8
+ AbstractFeatureBranch.logger.warn 'Environment variable abstract_feature_branch_feature3 already set, potentially conflicting with another test' if ENV.keys.include?('abstract_feature_branch_feature3')
9
+ end
10
+ after do
11
+ ENV.delete('ABSTRACT_FEATURE_BRANCH_FEATURE1')
12
+ ENV.delete('Abstract_Feature_Branch_Feature2')
13
+ ENV.delete('abstract_feature_branch_feature3')
14
+ AbstractFeatureBranch.initialize_application_root
15
+ AbstractFeatureBranch.load_application_features
16
+ AbstractFeatureBranch.application_environment = @app_env_backup
17
+ end
18
+ describe '#feature_branch' do
19
+ context 'class level behavior (case-insensitive string or symbol feature names)' do
20
+ {
21
+ 'Feature1' => true,
22
+ :FEATURE2 => true,
23
+ :feature3 => false,
24
+ :admin_feature1 => true,
25
+ :admin_feature2 => false,
26
+ :public_feature1 => true,
27
+ :public_feature2 => false,
28
+ :wiki_feature1 => true,
29
+ :wiki_feature2 => false,
30
+ }.each do |feature_name, expected_branch_run|
31
+ it "feature branches correctly for feature #{feature_name} with expected branch run #{expected_branch_run}" do
32
+ feature_branch_run = false
33
+ feature_branch feature_name do
34
+ feature_branch_run = true
35
+ end
36
+ feature_branch_run.should == expected_branch_run
37
+ end
38
+ end
39
+ end
40
+ it 'returns nil and does not execute block for an invalid feature name' do
41
+ return_value = feature_branch :invalid_feature_that_does_not_exist do
42
+ fail 'feature branch block must not execute, but did.'
43
+ end
44
+ return_value.should be_nil
45
+ end
46
+ it 'supports an alternate branch of behavior for turned off features' do
47
+ feature_behaviors = []
48
+ feature_branch :feature1,
49
+ :true => lambda {feature_behaviors << :feature1_true},
50
+ :false => lambda {feature_behaviors << :feature1_false}
51
+ feature_branch :feature3,
52
+ :true => lambda {feature_behaviors << :feature3_true},
53
+ :false => lambda {feature_behaviors << :feature3_false}
54
+ feature_behaviors.should include(:feature1_true)
55
+ feature_behaviors.should_not include(:feature1_false)
56
+ feature_behaviors.should_not include(:feature3_true)
57
+ feature_behaviors.should include(:feature3_false)
58
+ end
59
+ it 'executes alternate branch for an invalid feature name' do
60
+ feature_behaviors = []
61
+ feature_branch :invalid_feature_that_does_not_exist,
62
+ :true => lambda {feature_behaviors << :main_branch},
63
+ :false => lambda {feature_behaviors << :alternate_branch}
64
+ feature_behaviors.should_not include(:main_branch)
65
+ feature_behaviors.should include(:alternate_branch)
66
+ end
67
+ it 'allows environment variables (case-insensitive booleans) to override configuration file' do
68
+ ENV['ABSTRACT_FEATURE_BRANCH_FEATURE1'] = 'FALSE'
69
+ ENV['Abstract_Feature_Branch_Feature2'] = 'False'
70
+ ENV['abstract_feature_branch_feature3'] = 'true'
71
+ AbstractFeatureBranch.load_application_features
72
+ features_enabled = []
73
+ feature_branch :feature1 do
74
+ features_enabled << :feature1
75
+ end
76
+ feature_branch :feature2 do
77
+ features_enabled << :feature2
78
+ end
79
+ feature_branch :feature3 do
80
+ features_enabled << :feature3
81
+ end
82
+ features_enabled.should_not include(:feature1)
83
+ features_enabled.should_not include(:feature2)
84
+ features_enabled.should include(:feature3)
85
+ end
86
+ it 'allows local configuration file to override main configuration file' do
87
+ features_enabled = []
88
+ feature_branch :feature4 do
89
+ features_enabled << :feature4
90
+ end
91
+ feature_branch :feature5 do
92
+ features_enabled << :feature5
93
+ end
94
+ feature_branch :admin_feature3 do
95
+ features_enabled << :admin_feature3
96
+ end
97
+ feature_branch :public_feature3 do
98
+ features_enabled << :public_feature3
99
+ end
100
+ feature_branch :wiki_feature3 do
101
+ features_enabled << :wiki_feature3
102
+ end
103
+ features_enabled.should_not include(:feature4)
104
+ features_enabled.should include(:feature5)
105
+ features_enabled.should include(:admin_feature3)
106
+ features_enabled.should include(:public_feature3)
107
+ features_enabled.should include(:wiki_feature3)
108
+ end
109
+ it 'works with an application that has no configuration files' do
110
+ AbstractFeatureBranch.application_root = File.join(__FILE__, '..', '..', 'fixtures', 'application_no_config')
111
+ AbstractFeatureBranch.load_application_features
112
+ feature_branch :feature1 do
113
+ fail 'feature branch block must not execute, but did.'
114
+ end
115
+ end
116
+ end
117
+ describe 'self#feature_branch' do
118
+ after do
119
+ Object.send(:remove_const, :TestObject)
120
+ end
121
+ # No need to retest all instance test cases, just a spot check due to implementation reuse
122
+ it 'feature branches instance level behavior (case-insensitive feature names)' do
123
+ class TestObject
124
+ def self.features_enabled
125
+ @features_enabled ||= []
126
+ end
127
+ def self.hit_me
128
+ feature_branch :feature1 do
129
+ self.features_enabled << :feature1
130
+ end
131
+ feature_branch :feature2 do
132
+ self.features_enabled << :feature2
133
+ end
134
+ feature_branch :feature3 do
135
+ self.features_enabled << :feature3
136
+ end
137
+ end
138
+ end
139
+ TestObject.hit_me
140
+ TestObject.features_enabled.should include(:feature1)
141
+ TestObject.features_enabled.should include(:feature2)
142
+ TestObject.features_enabled.should_not include(:feature3)
143
+ end
144
+ end
145
+ end