flog 1.2.0 → 2.0.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.
data/spec/flog_spec.rb ADDED
@@ -0,0 +1,1123 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+ require 'flog'
3
+ require 'sexp_processor'
4
+
5
+ class Flog
6
+ attr_writer :total_score
7
+ end
8
+
9
+ describe Flog do
10
+ before :each do
11
+ @options = { }
12
+ @flog = Flog.new(@options)
13
+ end
14
+
15
+ describe 'when initializing' do
16
+ it 'should not reference the parse tree' do
17
+ ParseTree.expects(:new).never
18
+ Flog.new(@options)
19
+ end
20
+ end
21
+
22
+ describe 'after initializing' do
23
+ it 'should have options set' do
24
+ @flog.options.should == @options
25
+ end
26
+
27
+ it 'should return an SexpProcessor' do
28
+ @flog.should be_a_kind_of(SexpProcessor)
29
+ end
30
+
31
+ it 'should be initialized like all SexpProcessors' do
32
+ # less than ideal means of insuring the Flog instance was initialized properly, imo -RB
33
+ @flog.context.should == []
34
+ end
35
+
36
+ it 'should have no current class' do
37
+ @flog.klass_name.should == :main
38
+ end
39
+
40
+ it 'should have no current method' do
41
+ @flog.method_name.should == :none
42
+ end
43
+
44
+ it 'should not have any calls yet' do
45
+ @flog.calls.should == {}
46
+ end
47
+
48
+ it 'should have a means of accessing its parse tree' do
49
+ @flog.should respond_to(:parse_tree)
50
+ end
51
+
52
+ it 'should not have any totals yet' do
53
+ @flog.totals.should == {}
54
+ end
55
+
56
+ it 'should have a 0 total score' do
57
+ @flog.total.should == 0.0
58
+ end
59
+
60
+ it 'should have a multiplier of 1' do
61
+ @flog.multiplier.should == 1.0
62
+ end
63
+
64
+ currently "should have 'auto shift type' set to true" do
65
+ @flog.auto_shift_type.should be_true
66
+ end
67
+
68
+ currently "should have 'require empty' set to false" do
69
+ @flog.require_empty.should be_false
70
+ end
71
+ end
72
+
73
+ describe 'options' do
74
+ it 'should return the current options settings' do
75
+ @flog.should respond_to(:options)
76
+ end
77
+ end
78
+
79
+ describe 'when accessing the parse tree' do
80
+ before :each do
81
+ @parse_tree = stub('parse tree')
82
+ end
83
+
84
+ describe 'for the first time' do
85
+ it 'should create a new ParseTree' do
86
+ ParseTree.expects(:new)
87
+ @flog.parse_tree
88
+ end
89
+
90
+ currently 'should leave newlines off when creating the ParseTree instance' do
91
+ ParseTree.expects(:new).with(false)
92
+ @flog.parse_tree
93
+ end
94
+
95
+ it 'should return a ParseTree instance' do
96
+ ParseTree.stubs(:new).returns(@parse_tree)
97
+ @flog.parse_tree.should == @parse_tree
98
+ end
99
+ end
100
+
101
+ describe 'after the parse tree has been initialized' do
102
+ it 'should not attempt to create a new ParseTree instance' do
103
+ @flog.parse_tree
104
+ ParseTree.expects(:new).never
105
+ @flog.parse_tree
106
+ end
107
+
108
+ it 'should return a ParseTree instance' do
109
+ ParseTree.stubs(:new).returns(@parse_tree)
110
+ @flog.parse_tree
111
+ @flog.parse_tree.should == @parse_tree
112
+ end
113
+ end
114
+ end
115
+
116
+ describe "when flogging a list of files" do
117
+ describe 'when no files are specified' do
118
+ currently 'should not raise an exception' do
119
+ lambda { @flog.flog_files }.should_not raise_error
120
+ end
121
+
122
+ it 'should never call flog_file' do
123
+ @flog.expects(:flog_file).never
124
+ @flog.flog_files
125
+ end
126
+ end
127
+
128
+ describe 'when files are specified' do
129
+ before :each do
130
+ @files = [1, 2, 3, 4]
131
+ @flog.stubs(:flog_file)
132
+ end
133
+
134
+ it 'should do a flog for each individual file' do
135
+ @flog.expects(:flog_file).times(@files.size)
136
+ @flog.flog_files(@files)
137
+ end
138
+
139
+ it 'should provide the filename when flogging a file' do
140
+ @files.each do |file|
141
+ @flog.expects(:flog_file).with(file)
142
+ end
143
+ @flog.flog_files(@files)
144
+ end
145
+ end
146
+
147
+ describe 'when flogging a single file' do
148
+ before :each do
149
+ @flog.stubs(:flog)
150
+ end
151
+
152
+ describe 'when the filename is "-"' do
153
+ before :each do
154
+ @stdin = $stdin # HERE: working through the fact that zenspider is using $stdin in the middle of the system
155
+ $stdin = stub('stdin', :read => 'data')
156
+ end
157
+
158
+ after :each do
159
+ $stdin = @stdin
160
+ end
161
+
162
+ describe 'when reporting blame information' do
163
+ before :each do
164
+ @flog = Flog.new(:blame => true)
165
+ @flog.stubs(:flog)
166
+ end
167
+
168
+ it 'should fail' do
169
+ lambda { @flog.flog_file('-') }.should raise_error(RuntimeError)
170
+ end
171
+ end
172
+
173
+ it 'should not raise an exception' do
174
+ lambda { @flog.flog_file('-') }.should_not raise_error
175
+ end
176
+
177
+ it 'should read the data from stdin' do
178
+ $stdin.expects(:read).returns('data')
179
+ @flog.flog_file('-')
180
+ end
181
+
182
+ it 'should flog the read data' do
183
+ @flog.expects(:flog).with('data', '-')
184
+ @flog.flog_file('-')
185
+ end
186
+
187
+ describe 'when the verbose flag is on' do
188
+ before :each do
189
+ @flog = Flog.new(:verbose => true)
190
+ end
191
+
192
+ it 'should note which file is being flogged' do
193
+ @flog.expects(:warn)
194
+ @flog.flog_file('-')
195
+ end
196
+ end
197
+
198
+ describe 'when the verbose flag is off' do
199
+ before :each do
200
+ @flog = Flog.new({})
201
+ end
202
+
203
+ it 'should not note which file is being flogged' do
204
+ @flog.expects(:warn).never
205
+ @flog.flog_file('-')
206
+ end
207
+ end
208
+ end
209
+
210
+ describe 'when the filename points to a directory' do
211
+ before :each do
212
+ @flog.stubs(:flog_directory)
213
+ @file = File.dirname(__FILE__)
214
+ end
215
+
216
+ it 'should expand the files under the directory' do
217
+ @flog.expects(:flog_directory)
218
+ @flog.flog_file(@file)
219
+ end
220
+
221
+ it 'should not read data from stdin' do
222
+ $stdin.expects(:read).never
223
+ @flog.flog_file(@file)
224
+ end
225
+
226
+ it 'should not flog any data' do
227
+ @flog.expects(:flog).never
228
+ @flog.flog_file(@file)
229
+ end
230
+ end
231
+
232
+ describe 'when the filename points to a non-existant file' do
233
+ before :each do
234
+ @file = '/adfasdfasfas/fasdfaf-#{rand(1000000).to_s}'
235
+ end
236
+
237
+ it 'should raise an exception' do
238
+ lambda { @flog.flog_file(@file) }.should raise_error(Errno::ENOENT)
239
+ end
240
+ end
241
+
242
+ describe 'when the filename points to an existing file' do
243
+ before :each do
244
+ @file = __FILE__
245
+ File.stubs(:read).returns('data')
246
+ end
247
+
248
+ it 'should read the contents of the file' do
249
+ File.expects(:read).with(@file).returns('data')
250
+ @flog.flog_file(@file)
251
+ end
252
+
253
+ it 'should flog the contents of the file' do
254
+ @flog.expects(:flog).with('data', @file)
255
+ @flog.flog_file(@file)
256
+ end
257
+
258
+ describe 'when the verbose flag is on' do
259
+ before :each do
260
+ @flog = Flog.new(:verbose => true)
261
+ end
262
+
263
+ it 'should note which file is being flogged' do
264
+ @flog.expects(:warn)
265
+ @flog.flog_file(@file)
266
+ end
267
+ end
268
+
269
+ describe 'when the verbose flag is off' do
270
+ before :each do
271
+ @flog = Flog.new({})
272
+ end
273
+
274
+ it 'should not note which file is being flogged' do
275
+ @flog.expects(:warn).never
276
+ @flog.flog_file(@file)
277
+ end
278
+ end
279
+ end
280
+ end
281
+ end
282
+
283
+ describe 'when flogging a directory' do
284
+ before :each do
285
+ @files = ['a.rb', '/foo/b.rb', '/foo/bar/c.rb', '/foo/bar/baz/d.rb']
286
+ @dir = File.dirname(__FILE__)
287
+ Dir.stubs(:[]).returns(@files)
288
+ end
289
+
290
+ it 'should get the list of ruby files under the directory' do
291
+ @flog.stubs(:flog_file)
292
+ Dir.expects(:[]).returns(@files)
293
+ @flog.flog_directory(@dir)
294
+ end
295
+
296
+ it "should call flog_file once for each file in the directory" do
297
+ @files.each {|f| @flog.expects(:flog_file).with(f) }
298
+ @flog.flog_directory(@dir)
299
+ end
300
+ end
301
+
302
+ describe 'when flogging a Ruby string' do
303
+ it 'should require both a Ruby string and a filename' do
304
+ lambda { @flog.flog('string') }.should raise_error(ArgumentError)
305
+ end
306
+
307
+ describe 'when reporting blame information' do
308
+ before :each do
309
+ @flog = Flog.new(:blame => true)
310
+ end
311
+
312
+ it 'should gather blame information for the file' do
313
+ @flog.expects(:collect_blame).with('filename')
314
+ @flog.flog('string', 'filename')
315
+ end
316
+ end
317
+
318
+ describe 'when not reporting blame information' do
319
+ it 'should not gather blame information for the file' do
320
+ @flog.expects(:collect_blame).never
321
+ @flog.flog('string', 'filename')
322
+ end
323
+ end
324
+
325
+ describe 'when the string has a syntax error' do
326
+ before :each do
327
+ @flog.stubs(:warn)
328
+ @flog.stubs(:process_parse_tree).raises(SyntaxError.new("<% foo %>"))
329
+ end
330
+
331
+ describe 'when the string has erb snippets' do
332
+ currently 'should warn about skipping' do
333
+ @flog.expects(:warn)
334
+ @flog.flog('string', 'filename')
335
+ end
336
+
337
+ it 'should not raise an exception' do
338
+ lambda { @flog.flog('string', 'filename') }.should_not raise_error
339
+ end
340
+
341
+ it 'should not process the failing code' do
342
+ @flog.expects(:process).never
343
+ @flog.flog('string', 'filename')
344
+ end
345
+ end
346
+
347
+ describe 'when the string has no erb snippets' do
348
+ before :each do
349
+ @flog.stubs(:process_parse_tree).raises(SyntaxError)
350
+ end
351
+
352
+ it 'should raise a SyntaxError exception' do
353
+ lambda { @flog.flog('string', 'filename') }.should raise_error(SyntaxError)
354
+ end
355
+
356
+ it 'should not process the failing code' do
357
+ @flog.expects(:process).never
358
+ lambda { @flog.flog('string', 'filename') }
359
+ end
360
+ end
361
+ end
362
+
363
+ describe 'when the string contains valid Ruby' do
364
+ before :each do
365
+ @flog.stubs(:process_parse_tree)
366
+ end
367
+
368
+ it 'should process the parse tree for the string' do
369
+ @flog.expects(:process_parse_tree)
370
+ @flog.flog('string', 'filename')
371
+ end
372
+
373
+ it 'should provide the string and the filename to the parse tree processor' do
374
+ @flog.expects(:process_parse_tree).with('string', 'filename')
375
+ @flog.flog('string', 'filename')
376
+ end
377
+ end
378
+ end
379
+
380
+ describe 'when processing a ruby parse tree' do
381
+ before :each do
382
+ @flog.stubs(:process)
383
+ @sexp = stub('s-expressions')
384
+ @parse_tree = stub('parse tree', :parse_tree_for_string => @sexp)
385
+ ParseTree.stubs(:new).returns(@parse_tree)
386
+ end
387
+
388
+ it 'should require both a ruby string and a filename' do
389
+ lambda { @flog.process_parse_tree('string') }.should raise_error(ArgumentError)
390
+ end
391
+
392
+ it 'should compute the parse tree for the ruby string' do
393
+ Sexp.stubs(:from_array).returns(['1', '2'])
394
+ @parse_tree.expects(:parse_tree_for_string).returns(@sexp)
395
+ @flog.process_parse_tree('string', 'file')
396
+ end
397
+
398
+ it 'should use both the ruby string and the filename when computing the parse tree' do
399
+ Sexp.stubs(:from_array).returns(['1', '2'])
400
+ @parse_tree.expects(:parse_tree_for_string).with('string', 'file').returns(@sexp)
401
+ @flog.process_parse_tree('string', 'file')
402
+ end
403
+
404
+ describe 'if the ruby string is valid' do
405
+ before :each do
406
+ @parse_tree = stub('parse tree', :parse_tree_for_string => @sexp)
407
+ @flog.stubs(:process)
408
+ @flog.stubs(:parse_tree).returns(@parse_tree)
409
+ end
410
+
411
+ it 'should convert the parse tree into a list of S-expressions' do
412
+ Sexp.expects(:from_array).with(@sexp).returns(['1', '2'])
413
+ @flog.process_parse_tree('string', 'file')
414
+ end
415
+
416
+ it 'should process the list of S-expressions' do
417
+ @flog.expects(:process)
418
+ @flog.process_parse_tree('string', 'file')
419
+ end
420
+
421
+ it 'should start processing at the first S-expression' do
422
+ Sexp.stubs(:from_array).returns(['1', '2'])
423
+ @flog.expects(:process).with('1')
424
+ @flog.process_parse_tree('string', 'file')
425
+ end
426
+ end
427
+
428
+ describe 'if the ruby string is invalid' do
429
+ before :each do
430
+ @parse_tree = stub('parse tree')
431
+ @flog.stubs(:parse_tree).returns(@parse_tree)
432
+ @parse_tree.stubs(:parse_tree_for_string).raises(SyntaxError)
433
+ end
434
+
435
+ it 'should fail' do
436
+ lambda { @flog.process_parse_tree('string', 'file') }.should raise_error(SyntaxError)
437
+ end
438
+
439
+ it 'should not attempt to process the parse tree' do
440
+ @flog.expects(:process).never
441
+ lambda { @flog.process_parse_tree('string', 'file') }
442
+ end
443
+ end
444
+ end
445
+
446
+ describe 'when collecting blame information from a file' do
447
+ it 'should require a filename' do
448
+ lambda { @flog.collect_blame }.should raise_error(ArgumentError)
449
+ end
450
+
451
+ it 'should not fail when given a filename' do
452
+ @flog.collect_blame('filename')
453
+ end
454
+
455
+ # TODO: talk to Rick and see what he was planning for
456
+ # this... otherwise I'm thinking it should be ripped out
457
+
458
+ # it 'should have more specs'
459
+ end
460
+
461
+ describe 'multiplier' do
462
+ it 'should be possible to determine the current value of the multiplier' do
463
+ @flog.should respond_to(:multiplier)
464
+ end
465
+
466
+ currently 'should be possible to set the current value of the multiplier' do
467
+ @flog.multiplier = 10
468
+ @flog.multiplier.should == 10
469
+ end
470
+ end
471
+
472
+ describe 'class_stack' do
473
+ it 'should be possible to determine the current value of the class stack' do
474
+ @flog.should respond_to(:class_stack)
475
+ end
476
+
477
+ currently 'should be possible to set the current value of the class stack' do
478
+ @flog.class_stack << 'name'
479
+ @flog.class_stack.should == [ 'name' ]
480
+ end
481
+ end
482
+
483
+ describe 'method_stack' do
484
+ it 'should be possible to determine the current value of the method stack' do
485
+ @flog.should respond_to(:method_stack)
486
+ end
487
+
488
+ currently 'should be possible to set the current value of the method stack' do
489
+ @flog.method_stack << 'name'
490
+ @flog.method_stack.should == [ 'name' ]
491
+ end
492
+ end
493
+
494
+ describe 'when adding to the current flog score' do
495
+ before :each do
496
+ @flog.multiplier = 1
497
+ @flog.stubs(:klass_name).returns('foo')
498
+ @flog.stubs(:method_name).returns('bar')
499
+ @flog.calls['foo#bar'] = { :alias => 0 }
500
+ end
501
+
502
+ it 'should require an operation name' do
503
+ lambda { @flog.add_to_score() }.should raise_error(ArgumentError)
504
+ end
505
+
506
+ it 'should update the score for the current class, method, and operation' do
507
+ @flog.add_to_score(:alias)
508
+ @flog.calls['foo#bar'][:alias].should_not == 0
509
+ end
510
+
511
+ it 'should use the multiplier when updating the current call score' do
512
+ @flog.multiplier = 10
513
+ @flog.add_to_score(:alias)
514
+ @flog.calls['foo#bar'][:alias].should == 10*Flog::OTHER_SCORES[:alias]
515
+ end
516
+ end
517
+
518
+ describe 'when computing the average per-call flog score' do
519
+ it 'should not allow arguments' do
520
+ lambda { @flog.average('foo') }.should raise_error(ArgumentError)
521
+ end
522
+
523
+ it 'should return the total flog score divided by the number of calls' do
524
+ @flog.stubs(:total).returns(100.0)
525
+ @flog.stubs(:calls).returns({ :bar => {}, :foo => {} })
526
+ @flog.average.should be_close(100.0/2, 0.00000000001)
527
+ end
528
+ end
529
+
530
+ describe 'when recursively analyzing the complexity of code' do
531
+ it 'should require a complexity modifier value' do
532
+ lambda { @flog.penalize_by }.should raise_error(ArgumentError)
533
+ end
534
+
535
+ it 'should require a block, for code to recursively analyze' do
536
+ lambda { @flog.penalize_by(42) }.should raise_error(LocalJumpError)
537
+ end
538
+
539
+ it 'should recursively analyze the provided code block' do
540
+ @flog.penalize_by(42) do
541
+ @foo = true
542
+ end
543
+
544
+ @foo.should be_true
545
+ end
546
+
547
+ it 'should update the complexity multiplier when recursing' do
548
+ @flog.multiplier = 1
549
+ @flog.penalize_by(42) do
550
+ @flog.multiplier.should == 43
551
+ end
552
+ end
553
+
554
+ it 'when it is done it should restore the complexity multiplier to its original value' do
555
+ @flog.multiplier = 1
556
+ @flog.penalize_by(42) do
557
+ end
558
+ @flog.multiplier.should == 1
559
+ end
560
+ end
561
+
562
+ describe 'when computing complexity of all remaining opcodes' do
563
+ it 'should require a list of opcodes' do
564
+ lambda { @flog.analyze_list }.should raise_error(ArgumentError)
565
+ end
566
+
567
+ it 'should process each opcode' do
568
+ @opcodes = [ :foo, :bar, :baz ]
569
+ @opcodes.each do |opcode|
570
+ @flog.expects(:process).with(opcode)
571
+ end
572
+
573
+ @flog.analyze_list @opcodes
574
+ end
575
+ end
576
+
577
+ describe 'when recording the current class being analyzed' do
578
+ it 'should require a class name' do
579
+ lambda { @flog.in_klass }.should raise_error(ArgumentError)
580
+ end
581
+
582
+ it 'should require a block during which the class name is in effect' do
583
+ lambda { @flog.in_klass('name') }.should raise_error(LocalJumpError)
584
+ end
585
+
586
+ it 'should recursively analyze the provided code block' do
587
+ @flog.in_klass 'name' do
588
+ @foo = true
589
+ end
590
+
591
+ @foo.should be_true
592
+ end
593
+
594
+ it 'should update the class stack when recursing' do
595
+ @flog.class_stack.clear
596
+ @flog.in_klass 'name' do
597
+ @flog.class_stack.should == ['name']
598
+ end
599
+ end
600
+
601
+ it 'when it is done it should restore the class stack to its original value' do
602
+ @flog.class_stack.clear
603
+ @flog.in_klass 'name' do
604
+ end
605
+ @flog.class_stack.should == []
606
+ end
607
+ end
608
+
609
+ describe 'when looking up the name of the class currently under analysis' do
610
+ it 'should not take any arguments' do
611
+ lambda { @flog.klass_name('foo') }.should raise_error(ArgumentError)
612
+ end
613
+
614
+ it 'should return the most recent class entered' do
615
+ @flog.class_stack << :foo << :bar << :baz
616
+ @flog.klass_name.should == :foo
617
+ end
618
+
619
+ it 'should return the default class if no classes entered' do
620
+ @flog.class_stack.clear
621
+ @flog.klass_name.should == :main
622
+ end
623
+ end
624
+
625
+ describe 'when recording the current method being analyzed' do
626
+ it 'should require a method name' do
627
+ lambda { @flog.in_method }.should raise_error(ArgumentError)
628
+ end
629
+
630
+ it 'should require a block during which the class name is in effect' do
631
+ lambda { @flog.in_method('name') }.should raise_error(LocalJumpError)
632
+ end
633
+
634
+ it 'should recursively analyze the provided code block' do
635
+ @flog.in_method 'name' do
636
+ @foo = true
637
+ end
638
+
639
+ @foo.should be_true
640
+ end
641
+
642
+ it 'should update the class stack when recursing' do
643
+ @flog.method_stack.clear
644
+ @flog.in_method 'name' do
645
+ @flog.method_stack.should == ['name']
646
+ end
647
+ end
648
+
649
+ it 'when it is done it should restore the class stack to its original value' do
650
+ @flog.method_stack.clear
651
+ @flog.in_method 'name' do
652
+ end
653
+ @flog.method_stack.should == []
654
+ end
655
+ end
656
+
657
+ describe 'when looking up the name of the method currently under analysis' do
658
+ it 'should not take any arguments' do
659
+ lambda { @flog.method_name('foo') }.should raise_error(ArgumentError)
660
+ end
661
+
662
+ it 'should return the most recent method entered' do
663
+ @flog.method_stack << :foo << :bar << :baz
664
+ @flog.method_name.should == :foo
665
+ end
666
+
667
+ it 'should return the default method if no methods entered' do
668
+ @flog.method_stack.clear
669
+ @flog.method_name.should == :none
670
+ end
671
+ end
672
+
673
+ describe 'when resetting state' do
674
+ it 'should not take any arguments' do
675
+ lambda { @flog.reset('foo') }.should raise_error(ArgumentError)
676
+ end
677
+
678
+ it 'should clear any recorded totals data' do
679
+ @flog.totals['foo'] = 'bar'
680
+ @flog.reset
681
+ @flog.totals.should == {}
682
+ end
683
+
684
+ it 'should clear the total score' do
685
+ # the only way I know to do this is to force the total score to be computed for actual code, then reset it
686
+ @flog.flog_files(fixture_files('/simple/simple.rb'))
687
+ @flog.reset
688
+ @flog.total.should == 0
689
+ end
690
+
691
+ it 'should set the multiplier to 1.0' do
692
+ @flog.multiplier = 20.0
693
+ @flog.reset
694
+ @flog.multiplier.should == 1.0
695
+ end
696
+
697
+ it 'should set clear any calls data' do
698
+ @flog.calls['foobar'] = 'yoda'
699
+ @flog.reset
700
+ @flog.calls.should == {}
701
+ end
702
+
703
+ it 'should ensure that new recorded calls will get 0 counts without explicit initialization' do
704
+ @flog.reset
705
+ @flog.calls['foobar']['baz'] += 20
706
+ @flog.calls['foobar']['baz'].should == 20
707
+ end
708
+ end
709
+
710
+ describe 'when retrieving the total score' do
711
+ it 'should take no arguments' do
712
+ lambda { @flog.total('foo') }.should raise_error(ArgumentError)
713
+ end
714
+
715
+ it 'should return 0 if nothing has been analyzed' do
716
+ @flog.total.should == 0
717
+ end
718
+
719
+ it 'should compute totals data when called the first time' do
720
+ @flog.expects(:totals)
721
+ @flog.total
722
+ end
723
+
724
+ it 'should not recompute totals data when called after the first time' do
725
+ @flog.total
726
+ @flog.expects(:totals).never
727
+ @flog.total
728
+ end
729
+
730
+ it 'should return the score from the analysis once files have been analyzed' do
731
+ @flog.flog_files(fixture_files('/simple/simple.rb'))
732
+ @flog.total.should_not == 0
733
+ end
734
+ end
735
+
736
+ describe 'when computing a score for a method' do
737
+ it 'should require a hash of call tallies' do
738
+ lambda { @flog.score_method }.should raise_error(ArgumentError)
739
+ end
740
+
741
+ it 'should return a score of 0 if no tallies are provided' do
742
+ @flog.score_method({}).should == 0.0
743
+ end
744
+
745
+ it 'should compute the sqrt of summed squares for assignments, branches, and other tallies' do
746
+ @flog.score_method({
747
+ :assignment => 7,
748
+ :branch => 23,
749
+ :crap => 37
750
+ }).should be_close(Math.sqrt(7*7 + 23*23 + 37*37), 0.0000000001)
751
+ end
752
+ end
753
+
754
+ describe 'when recording a total for a method' do
755
+ # guess what, @totals and @calls could be refactored to be first-class objects
756
+ it 'should require a method and a score' do
757
+ lambda { @flog.record_method_score('foo') }.should raise_error(ArgumentError)
758
+ end
759
+
760
+ it 'should set the total score for the provided method' do
761
+ @flog.record_method_score('foo', 20)
762
+ @flog.totals['foo'].should == 20
763
+ end
764
+ end
765
+
766
+ describe 'when updating the total flog score' do
767
+ it 'should require an amount to update by' do
768
+ lambda { @flog.increment_total_score_by }.should raise_error(ArgumentError)
769
+ end
770
+
771
+ it 'should update the total flog score' do
772
+ @flog.total_score = 0
773
+ @flog.increment_total_score_by 42
774
+ @flog.total.should == 42
775
+ end
776
+ end
777
+
778
+ describe 'when compiling summaries for a method' do
779
+ before :each do
780
+ @tally = { :foo => 0.0 }
781
+ @method = 'foo'
782
+ @score = 42.0
783
+
784
+ @flog.stubs(:score_method).returns(@score)
785
+ @flog.stubs(:record_method_score)
786
+ @flog.stubs(:increment_total_score_by)
787
+ end
788
+
789
+ it 'should require a method name and a tally' do
790
+ lambda { @flog.summarize_method('foo') }.should raise_error(ArgumentError)
791
+ end
792
+
793
+ it 'should compute a score for the method, based on the tally' do
794
+ @flog.expects(:score_method).with(@tally)
795
+ @flog.summarize_method(@method, @tally)
796
+ end
797
+
798
+ it 'should record the score for the method' do
799
+ @flog.expects(:record_method_score).with(@method, @score)
800
+ @flog.summarize_method(@method, @tally)
801
+ end
802
+
803
+ it 'should update the overall flog score' do
804
+ @flog.expects(:increment_total_score_by).with(@score)
805
+ @flog.summarize_method(@method, @tally)
806
+ end
807
+
808
+ describe 'ignoring non-method code and given a non-method tally' do
809
+ it 'should not compute a score for the tally' do
810
+ @flog.expects(:score_method).never
811
+ @flog.summarize_method(@method, @tally)
812
+ end
813
+
814
+ it 'should not record a score based on the tally' do
815
+ @flog.expects(:record_method_score).never
816
+ @flog.summarize_method(@method, @tally)
817
+ end
818
+
819
+ it 'should not update the overall flog score' do
820
+ @flog.expects(:increment_total_score_by).never
821
+ @flog.summarize_method(@method, @tally)
822
+ end
823
+ end
824
+ end
825
+
826
+ describe 'when requesting totals' do
827
+ it 'should not accept any arguments' do
828
+ lambda { @flog.totals('foo') }.should raise_error(ArgumentError)
829
+ end
830
+
831
+ describe 'when called the first time' do
832
+ it 'should access calls data' do
833
+ @flog.expects(:calls).returns({})
834
+ @flog.totals
835
+ end
836
+
837
+ it "will compile a summary for each method from the method's tally" do
838
+ @calls = { :foo => 1.0, :bar => 2.0, :baz => 3.0 }
839
+ @flog.stubs(:calls).returns(@calls)
840
+ @calls.each do |meth, tally|
841
+ @flog.expects(:summarize_method).with(meth, tally)
842
+ end
843
+ @flog.totals
844
+ end
845
+
846
+ it 'should return the totals data' do
847
+ @flog.totals.should == {}
848
+ end
849
+ end
850
+
851
+ describe 'when called after the first time' do
852
+ before :each do
853
+ @flog.totals
854
+ end
855
+
856
+ it 'should not access calls data' do
857
+ @flog.expects(:calls).never
858
+ @flog.totals
859
+ end
860
+
861
+ it 'should not compile method summaries' do
862
+ @flog.expects(:summarize_method).never
863
+ @flog.totals
864
+ end
865
+
866
+ it 'should return the totals data' do
867
+ @flog.totals.should == {}
868
+ end
869
+ end
870
+ end
871
+
872
+ describe 'when producing a report summary' do
873
+ before :each do
874
+ @handle = stub('io handle)', :puts => nil)
875
+ @flog.stubs(:total).returns(@total_score = 42.0)
876
+ @flog.stubs(:average).returns(@average_score = 1.0)
877
+ end
878
+
879
+ it 'should require an io handle' do
880
+ lambda { @flog.output_summary }.should raise_error(ArgumentError)
881
+ end
882
+
883
+ it 'computes the total flog score' do
884
+ @flog.expects(:total).returns 42.0
885
+ @flog.output_summary(@handle)
886
+ end
887
+
888
+ it 'computes the average flog score' do
889
+ @flog.expects(:average).returns 1.0
890
+ @flog.output_summary(@handle)
891
+ end
892
+
893
+ it 'outputs the total flog score to the handle' do
894
+ @handle.expects(:puts).with do |string|
895
+ string =~ Regexp.new(Regexp.escape("%.1f" % @total_score))
896
+ end
897
+ @flog.output_summary(@handle)
898
+ end
899
+
900
+ it 'outputs the average flog score to the handle' do
901
+ @handle.expects(:puts).with do |string|
902
+ string =~ Regexp.new(Regexp.escape("%.1f" % @average_score))
903
+ end
904
+ @flog.output_summary(@handle)
905
+ end
906
+ end
907
+
908
+ describe 'when producing a detailed call summary report' do
909
+ before :each do
910
+ @handle = stub('io handle)', :puts => nil)
911
+ @calls = { :foo => {}, :bar => {}, :baz => {} }
912
+ @totals = { :foo => 1, :bar => 2, :baz => 3 }
913
+
914
+ @flog.stubs(:calls).returns(@calls)
915
+ @flog.stubs(:totals).returns(@totals)
916
+ @flog.stubs(:output_method_details).returns(5)
917
+ end
918
+
919
+ it 'should require an i/o handle' do
920
+ lambda { @flog.output_details }.should raise_error(ArgumentError)
921
+ end
922
+
923
+ it 'should allow a threshold on the amount of detail to report' do
924
+ lambda { @flog.output_details(@handle, 300) }.should_not raise_error(ArgumentError)
925
+ end
926
+
927
+ it 'retrieves the set of total statistics' do
928
+ @flog.expects(:totals).returns(@totals)
929
+ @flog.output_details(@handle)
930
+ end
931
+
932
+ it 'retrieves the set of call statistics' do
933
+ @flog.expects(:calls).returns({})
934
+ @flog.output_details(@handle)
935
+ end
936
+
937
+ it 'should output a method summary for each located method' do
938
+ @calls.each do |meth, list|
939
+ @flog.expects(:output_method_details).with(@handle, meth, list).returns(5)
940
+ end
941
+ @flog.output_details(@handle)
942
+ end
943
+
944
+ describe 'if a threshold is provided' do
945
+ it 'should only output details for methods until the threshold is reached' do
946
+ @flog.expects(:output_method_details).with(@handle, :baz, {}).returns(5)
947
+ @flog.expects(:output_method_details).with(@handle, :bar, {}).returns(5)
948
+ @flog.expects(:output_method_details).with(@handle, :foo, {}).never
949
+ @flog.output_details(@handle, 10)
950
+ end
951
+ end
952
+
953
+ describe 'if no threshold is provided' do
954
+ it 'should output details for all methods' do
955
+ @calls.each do |class_method, call_list|
956
+ @flog.expects(:output_method_details).with(@handle, class_method, call_list).returns(5)
957
+ end
958
+ @flog.output_details(@handle)
959
+ end
960
+ end
961
+ end
962
+
963
+ describe 'when reporting the details for a specific method' do
964
+ before :each do
965
+ @handle = stub('i/o handle', :puts => nil)
966
+ @totals = { 'foo#foo' => 42.0, 'foo#none' => 12.0 }
967
+ @data = { :assign => 10, :branch => 5, :case => 3 }
968
+ @flog.stubs(:totals).returns(@totals)
969
+ end
970
+
971
+ it 'should require an i/o handle, a method name, and method details' do
972
+ lambda { @flog.output_method_details('foo', 'bar') }.should raise_error(ArgumentError)
973
+ end
974
+
975
+ describe 'and ignoring non-method code' do
976
+ before :each do
977
+ @flog = Flog.new(:methods => true)
978
+ @flog.stubs(:totals).returns(@totals)
979
+ end
980
+
981
+ describe 'and given non-method data to summarize' do
982
+ it 'should not generate any output on the i/o handle' do
983
+ @handle.expects(:puts).never
984
+ @flog.output_method_details(@handle, 'foo#none', @data)
985
+ end
986
+
987
+ it 'should return 0' do
988
+ @flog.output_method_details(@handle, 'foo#none', @data).should == 0.0
989
+ end
990
+ end
991
+
992
+ describe 'and given method data to summarize' do
993
+ it 'should return the total complexity for the method' do
994
+ @flog.output_method_details(@handle, 'foo#foo', @data).should == 42.0
995
+ end
996
+
997
+ it 'should output the overall total for the method' do
998
+ @handle.expects(:puts).with do |string|
999
+ string =~ Regexp.new(Regexp.escape("%.1f" % 42.0))
1000
+ end
1001
+ @flog.output_method_details(@handle, 'foo#foo', @data)
1002
+ end
1003
+
1004
+ it 'should output call details for each call for the method' do
1005
+ @data.each do |call, count|
1006
+ @handle.expects(:puts).with do |string|
1007
+ string =~ Regexp.new(Regexp.escape("%6.1f: %s" % [ count, call ]))
1008
+ end
1009
+ end
1010
+ @flog.output_method_details(@handle, 'foo#foo', @data)
1011
+ end
1012
+ end
1013
+ end
1014
+
1015
+ describe 'and not excluding non-method code' do
1016
+ it 'should return the total complexity for the method' do
1017
+ @flog.output_method_details(@handle, 'foo#foo', @data).should == 42.0
1018
+ end
1019
+
1020
+ it 'should output the overall total for the method' do
1021
+ @handle.expects(:puts).with do |string|
1022
+ string =~ Regexp.new(Regexp.escape("%.1f" % 42.0))
1023
+ end
1024
+ @flog.output_method_details(@handle, 'foo#foo', @data)
1025
+ end
1026
+
1027
+ it 'should output call details for each call for the method' do
1028
+ @data.each do |call, count|
1029
+ @handle.expects(:puts).with do |string|
1030
+ string =~ Regexp.new(Regexp.escape("%6.1f: %s" % [ count, call ]))
1031
+ end
1032
+ end
1033
+ @flog.output_method_details(@handle, 'foo#foo', @data)
1034
+ end
1035
+ end
1036
+ end
1037
+
1038
+ describe 'when generating a report' do
1039
+ before :each do
1040
+ @flog.stubs(:output_summary)
1041
+ end
1042
+
1043
+ it 'allows specifying an i/o handle' do
1044
+ lambda { @flog.report 'handle' }.should_not raise_error(ArgumentError)
1045
+ end
1046
+
1047
+ it 'allows running the report without a specified i/o handle' do
1048
+ lambda { @flog.report }.should_not raise_error(ArgumentError)
1049
+ end
1050
+
1051
+ describe 'and no i/o handle is specified' do
1052
+ it 'defaults the io handle to stdout' do
1053
+ @flog.expects(:output_summary).with($stdout)
1054
+ @flog.report
1055
+ end
1056
+ end
1057
+
1058
+ describe 'and producing a summary report' do
1059
+ before :each do
1060
+ @flog = Flog.new(:score => true)
1061
+ @flog.stubs(:output_summary)
1062
+ end
1063
+
1064
+ it 'produces an output summary on the i/o handle' do
1065
+ @flog.expects(:output_summary).with('handle')
1066
+ @flog.report('handle')
1067
+ end
1068
+
1069
+ it 'does not output a detailed report' do
1070
+ @flog.expects(:output_details).never
1071
+ @flog.report('handle')
1072
+ end
1073
+
1074
+ it 'should reset statistics when finished' do
1075
+ @flog.expects(:reset)
1076
+ @flog.report('handle')
1077
+ end
1078
+ end
1079
+
1080
+ describe 'and producing a full report' do
1081
+ before :each do
1082
+ @flog.stubs(:output_summary)
1083
+ @flog.stubs(:output_details)
1084
+ end
1085
+
1086
+ it 'produces an output summary on the i/o handle' do
1087
+ @flog.expects(:output_summary).with('handle')
1088
+ @flog.report('handle')
1089
+ end
1090
+
1091
+ it 'should generate a detailed report of method complexity on the i/o handle' do
1092
+ @flog.expects(:output_details).with {|handle, max| handle == 'handle' }
1093
+ @flog.report('handle')
1094
+ end
1095
+
1096
+ describe 'when flogging all methods in the system' do
1097
+ before :each do
1098
+ @flog = Flog.new(:all => true)
1099
+ @flog.stubs(:output_summary)
1100
+ @flog.stubs(:output_details)
1101
+ end
1102
+
1103
+ it 'should not limit the detailed report' do
1104
+ @flog.expects(:output_details).with('handle')
1105
+ @flog.report('handle')
1106
+ end
1107
+ end
1108
+
1109
+ describe 'when flogging only the most expensive methods in the system' do
1110
+ it 'should limit the detailed report to the Flog threshold' do
1111
+ @flog.stubs(:total).returns(3.45)
1112
+ @flog.expects(:output_details).with('handle', 3.45 * 0.60)
1113
+ @flog.report('handle')
1114
+ end
1115
+ end
1116
+
1117
+ it 'should reset statistics when finished' do
1118
+ @flog.expects(:reset)
1119
+ @flog.report(@handle)
1120
+ end
1121
+ end
1122
+ end
1123
+ end