cuke_cataloger 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,555 @@
1
+ module CukeCataloger
2
+ class UniqueTestCaseTagger
3
+
4
+ SUB_ID_PATTERN = /^\d+\-\d+$/
5
+ SUB_ID_MATCH_PATTERN = /^\d+\-(\d+)$/
6
+
7
+
8
+ attr_accessor :tag_location
9
+
10
+
11
+ def initialize
12
+ @file_line_increases = Hash.new(0)
13
+ @tag_location = :adjacent
14
+ end
15
+
16
+ def tag_tests(feature_directory, tag_prefix, explicit_indexes = {})
17
+ warn("This script will potentially rewrite all of your feature files. Please be patient and remember to tip your source control system.")
18
+
19
+ @known_id_tags = {}
20
+
21
+ set_id_tag(tag_prefix)
22
+ set_test_suite_model(feature_directory)
23
+
24
+ @start_indexes = merge_indexes(default_start_indexes(determine_known_ids(feature_directory, tag_prefix)), explicit_indexes)
25
+ @next_index = @start_indexes[:primary]
26
+
27
+ # Analysis and output
28
+ @tests.each do |test|
29
+ case
30
+ when test.is_a?(CukeModeler::Scenario)
31
+ process_scenario(test)
32
+ when test.is_a?(CukeModeler::Outline)
33
+ process_outline(test)
34
+ else
35
+ raise("Unknown test type: #{test.class.to_s}")
36
+ end
37
+ end
38
+ end
39
+
40
+ def scan_for_tagged_tests(feature_directory, tag_prefix)
41
+ @results = []
42
+ @known_id_tags = {}
43
+
44
+ set_id_tag(tag_prefix)
45
+ set_test_suite_model(feature_directory)
46
+
47
+ @tests.each do |test|
48
+ add_to_results(test) if has_id_tag?(test)
49
+
50
+ if test.is_a?(CukeModeler::Outline)
51
+ test.examples.each do |example|
52
+ if has_id_parameter?(example)
53
+ example_rows_for(example).each do |row|
54
+ add_to_results(row) if has_row_id?(row)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ @results
62
+ end
63
+
64
+ def validate_test_ids(feature_directory, tag_prefix)
65
+ @results = []
66
+ @known_id_tags = {}
67
+
68
+ set_id_tag(tag_prefix)
69
+ set_test_suite_model(feature_directory)
70
+
71
+ @features.each { |feature| validate_feature(feature) }
72
+ @tests.each { |test| validate_test(test) }
73
+
74
+ @results
75
+ end
76
+
77
+ def determine_known_ids(feature_directory, tag_prefix)
78
+ known_ids = []
79
+
80
+ found_tagged_objects = scan_for_tagged_tests(feature_directory, tag_prefix).collect { |result| result[:object] }
81
+
82
+ found_tagged_objects.each do |element|
83
+ if element.is_a?(CukeModeler::Row)
84
+ row_id = row_id_for(element)
85
+ known_ids << row_id if well_formed_sub_id?(row_id)
86
+ else
87
+ known_ids << test_id_for(element)
88
+ end
89
+ end
90
+
91
+ known_ids
92
+ end
93
+
94
+
95
+ private
96
+
97
+
98
+ def set_id_tag(tag_prefix)
99
+ @tag_prefix = tag_prefix
100
+ #todo -should probably escape these characters
101
+ @tag_pattern = Regexp.new("#{@tag_prefix}\\d+")
102
+ end
103
+
104
+ def set_test_suite_model(feature_directory)
105
+ @directory = CukeModeler::Directory.new(feature_directory)
106
+ @model_repo = CQL::Repository.new(@directory)
107
+
108
+ @tests = @model_repo.query do
109
+ select :self
110
+ from scenarios, outlines
111
+ end.collect { |result| result[:self] }
112
+
113
+ @features = @model_repo.query do
114
+ select :self
115
+ from features
116
+ end.collect { |result| result[:self] }
117
+ end
118
+
119
+ def validate_feature(feature)
120
+ check_for_feature_level_test_tag(feature)
121
+ end
122
+
123
+ def validate_test(test)
124
+ check_for_missing_test_tag(test)
125
+ check_for_multiple_test_id_tags(test)
126
+ check_for_duplicated_test_id_tags(test)
127
+
128
+ if test.is_a?(CukeModeler::Outline)
129
+ check_for_missing_id_columns(test)
130
+ check_for_missing_row_tags(test)
131
+ check_for_duplicated_row_tags(test)
132
+ check_for_mismatched_row_tags(test)
133
+ check_for_malformed_row_tags(test)
134
+ end
135
+ end
136
+
137
+ def check_for_feature_level_test_tag(feature)
138
+ add_to_results(feature, :feature_test_tag) if has_id_tag?(feature)
139
+ end
140
+
141
+ def check_for_duplicated_test_id_tags(test)
142
+ @existing_tags ||= @model_repo.query do
143
+ select tags
144
+ from features, scenarios, outlines, examples
145
+ end.collect { |result| result['tags'] }.flatten
146
+
147
+ test_id_tag = static_id_tag_for(test)
148
+
149
+ matching_tags = @existing_tags.select { |tag| tag == test_id_tag }
150
+
151
+ add_to_results(test, :duplicate_id_tag) if matching_tags.count > 1
152
+ end
153
+
154
+ def check_for_multiple_test_id_tags(test)
155
+ id_tags_found = test.tags.select { |tag| tag =~ @tag_pattern }
156
+
157
+ add_to_results(test, :multiple_tags) if id_tags_found.count > 1
158
+ end
159
+
160
+ def check_for_missing_test_tag(test)
161
+ add_to_results(test, :missing_tag) unless has_id_tag?(test)
162
+ end
163
+
164
+ def check_for_missing_id_columns(test)
165
+ test.examples.each do |example|
166
+ add_to_results(example, :missing_id_column) unless has_id_column?(example)
167
+ end
168
+ end
169
+
170
+ def check_for_duplicated_row_tags(test)
171
+ validate_rows(test, :duplicate_row_id, false, :has_duplicate_row_id?)
172
+ end
173
+
174
+ def check_for_missing_row_tags(test)
175
+ validate_rows(test, :missing_row_id, true, :has_row_id?)
176
+ end
177
+
178
+ def check_for_mismatched_row_tags(test)
179
+ validate_rows(test, :mismatched_row_id, true, :has_matching_id?)
180
+ end
181
+
182
+ def check_for_malformed_row_tags(test)
183
+ test.examples.each do |example|
184
+ if has_id_column?(example)
185
+ example_rows_for(example).each do |row|
186
+ add_to_results(row, :malformed_sub_id) if (has_row_id?(row) && !well_formed_sub_id?(row_id_for(row)))
187
+ end
188
+ end
189
+ end
190
+ end
191
+
192
+ def validate_rows(test, rule, desired, row_check)
193
+ test.examples.each do |example|
194
+ if has_id_column?(example)
195
+ example_rows_for(example).each do |row|
196
+ if desired
197
+ add_to_results(row, rule) unless self.send(row_check, row)
198
+ else
199
+ add_to_results(row, rule) if self.send(row_check, row)
200
+ end
201
+ end
202
+ end
203
+ end
204
+ end
205
+
206
+ def process_scenario(test)
207
+ apply_tag_if_needed(test)
208
+ end
209
+
210
+ def process_outline(test)
211
+ apply_tag_if_needed(test)
212
+ update_parameters_if_needed(test)
213
+ update_rows_if_needed(test, determine_next_sub_id(test))
214
+ end
215
+
216
+ def apply_tag_if_needed(test)
217
+ unless has_id_tag?(test)
218
+ tag = "#{@tag_prefix}#{@next_index}"
219
+ @next_index += 1
220
+
221
+ tag_test(test, tag, (' ' * determine_test_indentation(test)))
222
+ end
223
+ end
224
+
225
+ def has_id_tag?(test)
226
+ !!fast_id_tag_for(test)
227
+ end
228
+
229
+ def has_id_column?(example)
230
+ example.parameters.any? { |param| param =~ /test_case_id/ }
231
+ end
232
+
233
+ def row_id_for(row)
234
+ id_index = determine_row_id_cell_index(row)
235
+
236
+ id_index && row.cells[id_index] != '' ? row.cells[id_index] : nil
237
+ end
238
+
239
+ def has_row_id?(row)
240
+ !!row_id_for(row)
241
+ end
242
+
243
+ def well_formed_sub_id?(id)
244
+ !!(id =~ SUB_ID_PATTERN)
245
+ end
246
+
247
+ def has_matching_id?(row)
248
+ row_id = row_id_for(row)
249
+
250
+ # A lack of id counts as 'matching'
251
+ return true if row_id.nil?
252
+
253
+ parent_tag = static_id_tag_for(row.get_ancestor(:test))
254
+
255
+ if parent_tag
256
+ parent_id = parent_tag.sub(@tag_prefix, '')
257
+
258
+ row_id =~ /#{parent_id}-/
259
+ else
260
+ row_id.nil?
261
+ end
262
+ end
263
+
264
+ def has_duplicate_row_id?(row)
265
+ row_id = row_id_for(row)
266
+
267
+ return false unless row_id && well_formed_sub_id?(row_id)
268
+
269
+ existing_ids = determine_used_sub_ids(row.get_ancestor(:test))
270
+ matching_ids = existing_ids.select { |id| id == row_id[/\d+$/] }
271
+
272
+ matching_ids.count > 1
273
+ end
274
+
275
+ def determine_next_sub_id(test)
276
+ parent = test_id_for(test)
277
+ explicit_index = @start_indexes[:sub][parent]
278
+
279
+ explicit_index ? explicit_index : 1
280
+ end
281
+
282
+ def determine_used_sub_ids(test)
283
+ ids = test.examples.collect do |example|
284
+ if has_id_parameter?(example)
285
+ example_rows_for(example).collect do |row|
286
+ row_id_for(row)
287
+ end
288
+ else
289
+ []
290
+ end
291
+ end
292
+
293
+ ids.flatten!
294
+ ids.delete_if { |id| !id.to_s.match(SUB_ID_PATTERN) }
295
+
296
+ ids.collect! { |id| id.match(SUB_ID_MATCH_PATTERN)[1] }
297
+
298
+ ids
299
+ end
300
+
301
+ def determine_row_id_cell_index(row)
302
+ row.get_ancestor(:example).parameters.index { |param| param =~ /test_case_id/ }
303
+ end
304
+
305
+ def tag_test(test, tag, padding_string = ' ')
306
+ feature_file = test.get_ancestor(:feature_file)
307
+ file_path = feature_file.path
308
+
309
+ index_adjustment = @file_line_increases[file_path]
310
+ tag_index = source_line_to_use(test) + index_adjustment
311
+
312
+ file_lines = File.readlines(file_path)
313
+ file_lines.insert(tag_index, "#{padding_string}#{tag}\n")
314
+
315
+ File.open(file_path, 'w') { |file| file.print file_lines.join }
316
+ @file_line_increases[file_path] += 1
317
+ test.tags << tag
318
+ end
319
+
320
+ def update_parameters_if_needed(test)
321
+ feature_file = test.get_ancestor(:feature_file)
322
+ file_path = feature_file.path
323
+ index_adjustment = @file_line_increases[file_path]
324
+
325
+ test.examples.each do |example|
326
+ unless has_id_parameter?(example)
327
+ parameter_line_index = (example.row_elements.first.source_line - 1) + index_adjustment
328
+
329
+ file_lines = File.readlines(file_path)
330
+
331
+ new_parameter = 'test_case_id'.ljust(parameter_spacing(example))
332
+ update_parameter_row(file_lines, parameter_line_index, new_parameter)
333
+ File.open(file_path, 'w') { |file| file.print file_lines.join }
334
+ end
335
+ end
336
+ end
337
+
338
+ def update_rows_if_needed(test, sub_id)
339
+ feature_file = test.get_ancestor(:feature_file)
340
+ file_path = feature_file.path
341
+ index_adjustment = @file_line_increases[file_path]
342
+
343
+ tag_index = fast_id_tag_for(test)[/\d+/]
344
+
345
+ file_lines = File.readlines(file_path)
346
+
347
+ test.examples.each do |example|
348
+ example.row_elements[1..(example.row_elements.count - 1)].each do |row|
349
+ unless has_row_id?(row)
350
+ row_id = "#{tag_index}-#{sub_id}".ljust(parameter_spacing(example))
351
+
352
+ row_line_index = (row.source_line - 1) + index_adjustment
353
+
354
+ update_value_row(file_lines, row_line_index, row, row_id)
355
+ sub_id += 1
356
+ end
357
+ end
358
+
359
+ File.open(file_path, 'w') { |file| file.print file_lines.join }
360
+ end
361
+ end
362
+
363
+
364
+ # Slowest way to get the id tag. Will check the object every time.
365
+ def current_id_tag_for(thing)
366
+ id_tag_for(thing)
367
+ end
368
+
369
+ # Faster way to get the id tag. Will skip checking the object if an id for it is already known.
370
+ def fast_id_tag_for(thing)
371
+ @known_id_tags ||= {}
372
+
373
+ id = @known_id_tags[thing.object_id]
374
+
375
+ unless id
376
+ id = current_id_tag_for(thing)
377
+ @known_id_tags[thing.object_id] = id
378
+ end
379
+
380
+ id
381
+ end
382
+
383
+ # Fastest way to get the id tag. Will skip checking the object if it has been checked before, even if no id was found.
384
+ def static_id_tag_for(thing)
385
+ @known_id_tags ||= {}
386
+ id_key = thing.object_id
387
+
388
+ return @known_id_tags[id_key] if @known_id_tags.has_key?(id_key)
389
+
390
+ id = current_id_tag_for(thing)
391
+ @known_id_tags[id_key] = id
392
+
393
+ id
394
+ end
395
+
396
+ def id_tag_for(thing)
397
+ thing.tags.select { |tag| tag =~ @tag_pattern }.first
398
+ end
399
+
400
+ def test_id_for(test)
401
+ #todo - should probably be escaping these in case regex characters used in prefix...
402
+ fast_id_tag_for(test).match(/#{@tag_prefix}(.*)/)[1]
403
+ end
404
+
405
+ def has_id_parameter?(example)
406
+ #todo - make the id column name configurable
407
+ example.parameters.any? { |parameter| parameter == 'test_case_id' }
408
+ end
409
+
410
+ def update_parameter_row(file_lines, line_index, parameter)
411
+ append_row!(file_lines, line_index, " #{parameter} |")
412
+ end
413
+
414
+ def update_value_row(file_lines, line_index, row, row_id)
415
+ case
416
+ when needs_adding?(row)
417
+ append_row!(file_lines, line_index, " #{row_id} |")
418
+ when needs_filled_in?(row)
419
+ fill_in_row(file_lines, line_index, row, row_id)
420
+ else
421
+ raise("Don't know how to update row")
422
+ end
423
+ end
424
+
425
+ def needs_adding?(row)
426
+ !has_id_parameter?(row.get_ancestor(:example))
427
+ end
428
+
429
+ def needs_filled_in?(row)
430
+ has_id_parameter?(row.get_ancestor(:example))
431
+ end
432
+
433
+ def replace_row!(file_lines, line_index, new_line)
434
+ file_lines[line_index] = new_line
435
+ end
436
+
437
+ def prepend_row!(file_lines, line_index, string)
438
+ old_row = file_lines[line_index]
439
+ new_row = string + old_row.lstrip
440
+ file_lines[line_index] = new_row
441
+ end
442
+
443
+ def append_row!(file_lines, line_index, string)
444
+ old_row = file_lines[line_index]
445
+ trailing_bits = old_row[/\s*$/]
446
+ new_row = old_row.rstrip + string + trailing_bits
447
+
448
+ file_lines[line_index] = new_row
449
+ end
450
+
451
+ def example_rows_for(example)
452
+ rows = example.row_elements.dup
453
+ rows.shift
454
+
455
+ rows
456
+ end
457
+
458
+ def add_to_results(item, issue = nil)
459
+ result = {:test => "#{item.get_ancestor(:feature_file).path}:#{item.source_line}", :object => item}
460
+ result.merge!({:problem => issue}) if issue
461
+
462
+ @results << result
463
+ end
464
+
465
+ def default_start_indexes(known_ids)
466
+ primary_ids = known_ids.select { |id| id =~ /^\d+$/ }
467
+ sub_ids = known_ids.select { |id| id =~ /^\d+-\d+$/ }
468
+
469
+ max_primary_id = primary_ids.collect { |id| id.to_i }.max || 0
470
+ default_indexes = {:primary => max_primary_id + 1,
471
+ :sub => {}}
472
+
473
+ sub_primaries = sub_ids.collect { |sub_id| sub_id[/^\d+/] }
474
+
475
+ sub_primaries.each do |primary|
476
+ default_indexes[:sub][primary] = sub_ids.select { |sub_id| sub_id[/^\d+/] == primary }.collect { |sub_id| sub_id[/\d+$/].to_i }.max + 1
477
+ end
478
+
479
+ default_indexes
480
+ end
481
+
482
+ def merge_indexes(set1, set2)
483
+ set1.merge(set2) { |key, set1_value, set2_value|
484
+ key == :sub ? set1_value.merge(set2_value) : set2_value
485
+ }
486
+ end
487
+
488
+ def parameter_spacing(example)
489
+ test = example.get_ancestor(:test)
490
+ test_id = fast_id_tag_for(test)[/\d+$/]
491
+ row_count = test.examples.reduce(0) { |sum, example| sum += example.rows.count }
492
+
493
+ max_id_length = test_id.length + 1 + row_count.to_s.length
494
+ param_length = 'test_case_id'.length
495
+
496
+ [param_length, max_id_length].max
497
+ end
498
+
499
+ def determine_test_indentation(test)
500
+ #todo - replace with 'get_most_recent_file_text'
501
+ feature_file = test.get_ancestor(:feature_file)
502
+ file_path = feature_file.path
503
+
504
+ index_adjustment = @file_line_increases[file_path]
505
+ test_index = (test.source_line - 1) + index_adjustment
506
+
507
+ file_lines = File.readlines(file_path)
508
+ indentation = file_lines[test_index][/^\s*/].length
509
+
510
+ indentation
511
+ end
512
+
513
+ def fill_in_row(file_lines, line_index, row, row_id)
514
+ old_row = file_lines[line_index]
515
+ sections = file_lines[line_index].split('|', -1)
516
+
517
+ replacement_index = determine_row_id_cell_index(row)
518
+ sections[replacement_index + 1] = " #{row_id} "
519
+
520
+ new_row = sections.join('|')
521
+
522
+ replace_row!(file_lines, line_index, new_row)
523
+ end
524
+
525
+ def source_line_to_use(test)
526
+ case @tag_location
527
+ when :above
528
+ determine_highest_tag_line(test)
529
+ when :below
530
+ determine_lowest_tag_line(test)
531
+ when :adjacent
532
+ adjacent_tag_line(test)
533
+ else
534
+ raise(ArgumentError, "Don't know where #{@tag_location} is.")
535
+ end
536
+ end
537
+
538
+ def determine_highest_tag_line(test)
539
+ return adjacent_tag_line(test) if test.tags.empty?
540
+
541
+ test.tag_elements.collect { |tag_element| tag_element.source_line }.min - 1
542
+ end
543
+
544
+ def determine_lowest_tag_line(test)
545
+ return adjacent_tag_line(test) if test.tags.empty?
546
+
547
+ test.tag_elements.collect { |tag_element| tag_element.source_line }.max
548
+ end
549
+
550
+ def adjacent_tag_line(test)
551
+ (test.source_line - 1)
552
+ end
553
+
554
+ end
555
+ end
@@ -0,0 +1,3 @@
1
+ module CukeCataloger
2
+ VERSION = '1.0.0'
3
+ end
@@ -0,0 +1,60 @@
1
+ require 'rake'
2
+ require 'cuke_modeler'
3
+ require 'cql'
4
+
5
+ require 'extensions/cucumber_analytics_extensions'
6
+ require 'cuke_cataloger/version'
7
+ require 'cuke_cataloger/unique_test_case_tagger'
8
+
9
+ module CukeCataloger
10
+
11
+ extend Rake::DSL
12
+
13
+
14
+ def self.create_tasks
15
+
16
+ desc 'Add unique id tags to tests in the given directory'
17
+ task 'tag_tests', [:directory, :prefix] do |t, args|
18
+ puts "Tagging tests in '#{args[:directory]}' with tag '#{args[:prefix]}'\n"
19
+
20
+ tagger = CukeCataloger::UniqueTestCaseTagger.new
21
+ tagger.tag_tests(args[:directory], args[:prefix])
22
+ end
23
+
24
+ desc 'Scan tests in the given directory for id problems'
25
+ task 'validate_tests', [:directory, :prefix, :out_file] do |t, args|
26
+ puts "Validating tests in '#{args[:directory]}' with tag '#{args[:prefix]}'\n"
27
+
28
+ results = CukeCataloger::UniqueTestCaseTagger.new.validate_test_ids(args[:directory], args[:prefix])
29
+ report_text = "Validation Results\nProblems found: #{results.count}\n\n"
30
+
31
+
32
+ results_by_category = Hash.new { |hash, key| hash[key] = [] }
33
+
34
+ results.each do |result|
35
+ results_by_category[result[:problem]] << result
36
+ end
37
+
38
+ results_by_category.keys.each do |problem_category|
39
+ report_text << "#{problem_category} problems: #{results_by_category[problem_category].count}\n"
40
+ end
41
+
42
+ results_by_category.keys.each do |problem_category|
43
+ report_text << "\n\n#{problem_category} problems (#{results_by_category[problem_category].count}):\n"
44
+
45
+ results_by_category[problem_category].each do |result|
46
+ report_text << "#{result[:test]}\n"
47
+ end
48
+ end
49
+
50
+ if args[:out_file]
51
+ puts "Problems found: #{results.count}"
52
+ File.write(args[:out_file], report_text)
53
+ else
54
+ puts report_text
55
+ end
56
+ end
57
+
58
+ end
59
+
60
+ end
@@ -0,0 +1,44 @@
1
+ module CukeModeler
2
+ module Parsing
3
+
4
+ class << self
5
+
6
+ def parse_text(source_text, file_name = nil)
7
+ raise(ArgumentError, "Cannot parse #{source_text.class} objects. Strings only.") unless source_text.is_a?(String)
8
+
9
+ file_name ||= 'fake_file.txt'
10
+
11
+ io = StringIO.new
12
+ formatter = Gherkin::Formatter::JSONFormatter.new(io)
13
+ parser = Gherkin::Parser::Parser.new(formatter)
14
+ begin
15
+ parser.parse(source_text, file_name, 0)
16
+
17
+ formatter.done
18
+ rescue => e
19
+ raise("Error encountered while parsing file #{file_name}: #{e.message}")
20
+ end
21
+ MultiJson.load(io.string)
22
+ end
23
+
24
+ end
25
+
26
+ end
27
+ end
28
+
29
+
30
+ module CukeModeler
31
+ class FeatureFile
32
+
33
+
34
+ private
35
+
36
+
37
+ def parse_file(file_to_parse)
38
+ source_text = IO.read(file_to_parse)
39
+
40
+ Parsing::parse_text(source_text, file_to_parse)
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,24 @@
1
+ unless RUBY_VERSION.to_s < '1.9.0'
2
+ require 'simplecov'
3
+ SimpleCov.command_name('cuke_cataloger-cucumber')
4
+ end
5
+
6
+
7
+ require 'cuke_cataloger'
8
+
9
+
10
+ RSpec.configure do |config|
11
+ config.before(:all) do
12
+ here = File.dirname(__FILE__)
13
+ @default_file_directory = "#{here}/temp_files"
14
+ @default_test_file_directory = "#{here}/test_files"
15
+ end
16
+
17
+ config.before(:each) do
18
+ FileUtils.mkpath(@default_file_directory)
19
+ end
20
+
21
+ config.after(:each) do
22
+ FileUtils.remove_dir(@default_file_directory, true)
23
+ end
24
+ end