abstract_feature_branch 0.7.1 → 0.8.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.
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