ngmoco-request-log-analyzer 1.4.2

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 (112) hide show
  1. data/.gitignore +10 -0
  2. data/DESIGN.rdoc +41 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +39 -0
  5. data/Rakefile +8 -0
  6. data/bin/request-log-analyzer +114 -0
  7. data/lib/cli/command_line_arguments.rb +301 -0
  8. data/lib/cli/database_console.rb +26 -0
  9. data/lib/cli/database_console_init.rb +43 -0
  10. data/lib/cli/progressbar.rb +213 -0
  11. data/lib/cli/tools.rb +46 -0
  12. data/lib/request_log_analyzer.rb +44 -0
  13. data/lib/request_log_analyzer/aggregator.rb +49 -0
  14. data/lib/request_log_analyzer/aggregator/database_inserter.rb +83 -0
  15. data/lib/request_log_analyzer/aggregator/echo.rb +29 -0
  16. data/lib/request_log_analyzer/aggregator/summarizer.rb +175 -0
  17. data/lib/request_log_analyzer/controller.rb +332 -0
  18. data/lib/request_log_analyzer/database.rb +102 -0
  19. data/lib/request_log_analyzer/database/base.rb +115 -0
  20. data/lib/request_log_analyzer/database/connection.rb +38 -0
  21. data/lib/request_log_analyzer/database/request.rb +22 -0
  22. data/lib/request_log_analyzer/database/source.rb +13 -0
  23. data/lib/request_log_analyzer/database/warning.rb +14 -0
  24. data/lib/request_log_analyzer/file_format.rb +160 -0
  25. data/lib/request_log_analyzer/file_format/amazon_s3.rb +71 -0
  26. data/lib/request_log_analyzer/file_format/apache.rb +141 -0
  27. data/lib/request_log_analyzer/file_format/merb.rb +67 -0
  28. data/lib/request_log_analyzer/file_format/rack.rb +11 -0
  29. data/lib/request_log_analyzer/file_format/rails.rb +176 -0
  30. data/lib/request_log_analyzer/file_format/rails_development.rb +12 -0
  31. data/lib/request_log_analyzer/filter.rb +30 -0
  32. data/lib/request_log_analyzer/filter/anonymize.rb +39 -0
  33. data/lib/request_log_analyzer/filter/field.rb +42 -0
  34. data/lib/request_log_analyzer/filter/timespan.rb +45 -0
  35. data/lib/request_log_analyzer/line_definition.rb +111 -0
  36. data/lib/request_log_analyzer/log_processor.rb +99 -0
  37. data/lib/request_log_analyzer/mailer.rb +62 -0
  38. data/lib/request_log_analyzer/output.rb +113 -0
  39. data/lib/request_log_analyzer/output/fixed_width.rb +220 -0
  40. data/lib/request_log_analyzer/output/html.rb +184 -0
  41. data/lib/request_log_analyzer/request.rb +175 -0
  42. data/lib/request_log_analyzer/source.rb +72 -0
  43. data/lib/request_log_analyzer/source/database_loader.rb +87 -0
  44. data/lib/request_log_analyzer/source/log_parser.rb +274 -0
  45. data/lib/request_log_analyzer/tracker.rb +206 -0
  46. data/lib/request_log_analyzer/tracker/duration.rb +104 -0
  47. data/lib/request_log_analyzer/tracker/frequency.rb +95 -0
  48. data/lib/request_log_analyzer/tracker/hourly_spread.rb +107 -0
  49. data/lib/request_log_analyzer/tracker/timespan.rb +81 -0
  50. data/lib/request_log_analyzer/tracker/traffic.rb +106 -0
  51. data/request-log-analyzer.gemspec +40 -0
  52. data/spec/database.yml +23 -0
  53. data/spec/fixtures/apache_combined.log +5 -0
  54. data/spec/fixtures/apache_common.log +10 -0
  55. data/spec/fixtures/decompression.log +12 -0
  56. data/spec/fixtures/decompression.log.bz2 +0 -0
  57. data/spec/fixtures/decompression.log.gz +0 -0
  58. data/spec/fixtures/decompression.log.zip +0 -0
  59. data/spec/fixtures/decompression.tar.gz +0 -0
  60. data/spec/fixtures/decompression.tgz +0 -0
  61. data/spec/fixtures/header_and_footer.log +6 -0
  62. data/spec/fixtures/merb.log +84 -0
  63. data/spec/fixtures/merb_prefixed.log +9 -0
  64. data/spec/fixtures/multiple_files_1.log +5 -0
  65. data/spec/fixtures/multiple_files_2.log +2 -0
  66. data/spec/fixtures/rails.db +0 -0
  67. data/spec/fixtures/rails_1x.log +59 -0
  68. data/spec/fixtures/rails_22.log +12 -0
  69. data/spec/fixtures/rails_22_cached.log +10 -0
  70. data/spec/fixtures/rails_unordered.log +24 -0
  71. data/spec/fixtures/syslog_1x.log +5 -0
  72. data/spec/fixtures/test_file_format.log +13 -0
  73. data/spec/fixtures/test_language_combined.log +14 -0
  74. data/spec/fixtures/test_order.log +16 -0
  75. data/spec/integration/command_line_usage_spec.rb +84 -0
  76. data/spec/integration/munin_plugins_rails_spec.rb +58 -0
  77. data/spec/integration/scout_spec.rb +151 -0
  78. data/spec/lib/helpers.rb +52 -0
  79. data/spec/lib/macros.rb +18 -0
  80. data/spec/lib/matchers.rb +77 -0
  81. data/spec/lib/mocks.rb +76 -0
  82. data/spec/lib/testing_format.rb +46 -0
  83. data/spec/spec_helper.rb +24 -0
  84. data/spec/unit/aggregator/database_inserter_spec.rb +93 -0
  85. data/spec/unit/aggregator/summarizer_spec.rb +26 -0
  86. data/spec/unit/controller/controller_spec.rb +41 -0
  87. data/spec/unit/controller/log_processor_spec.rb +18 -0
  88. data/spec/unit/database/base_class_spec.rb +183 -0
  89. data/spec/unit/database/connection_spec.rb +34 -0
  90. data/spec/unit/database/database_spec.rb +133 -0
  91. data/spec/unit/file_format/amazon_s3_format_spec.rb +49 -0
  92. data/spec/unit/file_format/apache_format_spec.rb +203 -0
  93. data/spec/unit/file_format/file_format_api_spec.rb +69 -0
  94. data/spec/unit/file_format/line_definition_spec.rb +75 -0
  95. data/spec/unit/file_format/merb_format_spec.rb +52 -0
  96. data/spec/unit/file_format/rails_format_spec.rb +164 -0
  97. data/spec/unit/filter/anonymize_filter_spec.rb +21 -0
  98. data/spec/unit/filter/field_filter_spec.rb +66 -0
  99. data/spec/unit/filter/filter_spec.rb +17 -0
  100. data/spec/unit/filter/timespan_filter_spec.rb +58 -0
  101. data/spec/unit/mailer_spec.rb +30 -0
  102. data/spec/unit/request_spec.rb +111 -0
  103. data/spec/unit/source/log_parser_spec.rb +119 -0
  104. data/spec/unit/tracker/duration_tracker_spec.rb +130 -0
  105. data/spec/unit/tracker/frequency_tracker_spec.rb +88 -0
  106. data/spec/unit/tracker/hourly_spread_spec.rb +79 -0
  107. data/spec/unit/tracker/timespan_tracker_spec.rb +73 -0
  108. data/spec/unit/tracker/tracker_api_spec.rb +124 -0
  109. data/spec/unit/tracker/traffic_tracker_spec.rb +107 -0
  110. data/tasks/github-gem.rake +323 -0
  111. data/tasks/request_log_analyzer.rake +26 -0
  112. metadata +220 -0
@@ -0,0 +1,79 @@
1
+ require File.dirname(__FILE__) + '/../../spec_helper'
2
+
3
+ describe RequestLogAnalyzer::Tracker::HourlySpread do
4
+
5
+ before(:each) do
6
+ @tracker = RequestLogAnalyzer::Tracker::HourlySpread.new
7
+ @tracker.prepare
8
+ end
9
+
10
+ it "should store timestamps correctly" do
11
+ @tracker.update(request(:timestamp => 20090102000000))
12
+ @tracker.update(request(:timestamp => 20090101000000))
13
+ @tracker.update(request(:timestamp => 20090103000000))
14
+
15
+ @tracker.hour_frequencies[0].should eql(3)
16
+ end
17
+
18
+ it "should count the number of timestamps correctly" do
19
+ @tracker.update(request(:timestamp => 20090102000000))
20
+ @tracker.update(request(:timestamp => 20090101000000))
21
+ @tracker.update(request(:timestamp => 20090103000000))
22
+ @tracker.update(request(:timestamp => 20090103010000))
23
+
24
+ @tracker.total_requests.should eql(4)
25
+ end
26
+
27
+ it "should set the first request timestamp correctly" do
28
+ @tracker.update(request(:timestamp => 20090102000000))
29
+ @tracker.update(request(:timestamp => 20090101000000))
30
+ @tracker.update(request(:timestamp => 20090103000000))
31
+
32
+ @tracker.first_timestamp.should == DateTime.parse('Januari 1, 2009 00:00:00')
33
+ end
34
+
35
+ it "should set the last request timestamp correctly" do
36
+ @tracker.update(request(:timestamp => 20090102000000))
37
+ @tracker.update(request(:timestamp => 20090101000000))
38
+ @tracker.update(request(:timestamp => 20090103000000))
39
+
40
+ @tracker.last_timestamp.should == DateTime.parse('Januari 3, 2009 00:00:00')
41
+ end
42
+
43
+ it "should return the correct timespan in days when multiple requests are given" do
44
+ @tracker.update(request(:timestamp => 20090102000000))
45
+ @tracker.update(request(:timestamp => 20090101000000))
46
+ @tracker.update(request(:timestamp => 20090103000000))
47
+
48
+ @tracker.timespan.should == 2
49
+ end
50
+
51
+ end
52
+
53
+ describe RequestLogAnalyzer::Tracker::HourlySpread, 'reporting' do
54
+
55
+ before(:each) do
56
+ @tracker = RequestLogAnalyzer::Tracker::HourlySpread.new
57
+ @tracker.prepare
58
+ end
59
+
60
+ it "should generate a report without errors when no request was tracked" do
61
+ lambda { @tracker.report(mock_output) }.should_not raise_error
62
+ end
63
+
64
+ it "should generate a report without errors when multiple requests were tracked" do
65
+ @tracker.update(request(:timestamp => 20090102000000))
66
+ @tracker.update(request(:timestamp => 20090101000000))
67
+ @tracker.update(request(:timestamp => 20090103000000))
68
+ @tracker.update(request(:timestamp => 20090103010000))
69
+ lambda { @tracker.report(mock_output) }.should_not raise_error
70
+ end
71
+
72
+ it "should generate a YAML output" do
73
+ @tracker.update(request(:timestamp => 20090102000000))
74
+ @tracker.update(request(:timestamp => 20090101000000))
75
+ @tracker.update(request(:timestamp => 20090103000000))
76
+ @tracker.update(request(:timestamp => 20090103010000))
77
+ @tracker.to_yaml_object.should == {"22:00 - 23:00"=>0, "9:00 - 10:00"=>0, "7:00 - 8:00"=>0, "2:00 - 3:00"=>0, "12:00 - 13:00"=>0, "11:00 - 12:00"=>0, "16:00 - 17:00"=>0, "15:00 - 16:00"=>0, "19:00 - 20:00"=>0, "3:00 - 4:00"=>0, "21:00 - 22:00"=>0, "20:00 - 21:00"=>0, "14:00 - 15:00"=>0, "13:00 - 14:00"=>0, "4:00 - 5:00"=>0, "10:00 - 11:00"=>0, "18:00 - 19:00"=>0, "17:00 - 18:00"=>0, "8:00 - 9:00"=>0, "6:00 - 7:00"=>0, "5:00 - 6:00"=>0, "1:00 - 2:00"=>1, "0:00 - 1:00"=>3, "23:00 - 24:00"=>0}
78
+ end
79
+ end
@@ -0,0 +1,73 @@
1
+ require File.dirname(__FILE__) + '/../../spec_helper'
2
+
3
+ describe RequestLogAnalyzer::Tracker::Timespan do
4
+
5
+ before(:each) do
6
+ @tracker = RequestLogAnalyzer::Tracker::Timespan.new
7
+ @tracker.prepare
8
+ end
9
+
10
+ it "should set the first request timestamp correctly" do
11
+ @tracker.update(request(:timestamp => 20090102000000))
12
+ @tracker.update(request(:timestamp => 20090101000000))
13
+ @tracker.update(request(:timestamp => 20090103000000))
14
+
15
+ @tracker.first_timestamp.should == DateTime.parse('Januari 1, 2009 00:00:00')
16
+ end
17
+
18
+ it "should set the last request timestamp correctly" do
19
+ @tracker.update(request(:timestamp => 20090102000000))
20
+ @tracker.update(request(:timestamp => 20090101000000))
21
+ @tracker.update(request(:timestamp => 20090103000000))
22
+
23
+ @tracker.last_timestamp.should == DateTime.parse('Januari 3, 2009 00:00:00')
24
+ end
25
+
26
+ it "should return the correct timespan in days when multiple requests are given" do
27
+ @tracker.update(request(:timestamp => 20090102000000))
28
+ @tracker.update(request(:timestamp => 20090101000000))
29
+ @tracker.update(request(:timestamp => 20090103000000))
30
+
31
+ @tracker.timespan.should == 2
32
+ end
33
+
34
+ it "should return a timespan of 0 days when only one timestamp is set" do
35
+ @tracker.update(request(:timestamp => 20090103000000))
36
+ @tracker.timespan.should == 0
37
+ end
38
+
39
+ it "should raise an error when no timestamp is set" do
40
+ lambda { @tracker.timespan }.should raise_error
41
+ end
42
+ end
43
+
44
+ describe RequestLogAnalyzer::Tracker::Timespan, 'reporting' do
45
+
46
+ before(:each) do
47
+ @tracker = RequestLogAnalyzer::Tracker::Timespan.new
48
+ @tracker.prepare
49
+ end
50
+
51
+ it "should have a title" do
52
+ @tracker.title.should_not eql("")
53
+ end
54
+
55
+ it "should generate a report without errors when no request was tracked" do
56
+ lambda { @tracker.report(mock_output) }.should_not raise_error
57
+ end
58
+
59
+ it "should generate a report without errors when multiple requests were tracked" do
60
+ @tracker.update(request(:category => 'a', :timestamp => 20090102000000))
61
+ @tracker.update(request(:category => 'a', :timestamp => 20090101000000))
62
+ @tracker.update(request(:category => 'a', :timestamp => 20090103000000))
63
+ lambda { @tracker.report(mock_output) }.should_not raise_error
64
+ end
65
+
66
+ it "should generate a YAML output" do
67
+ @tracker.update(request(:category => 'a', :timestamp => 20090102000000))
68
+ @tracker.update(request(:category => 'a', :timestamp => 20090101000000))
69
+ @tracker.update(request(:category => 'a', :timestamp => 20090103000000))
70
+ @tracker.to_yaml_object.should == { :first => DateTime.parse('20090101000000'), :last => DateTime.parse('20090103000000')}
71
+ end
72
+
73
+ end
@@ -0,0 +1,124 @@
1
+ require File.dirname(__FILE__) + '/../../spec_helper'
2
+
3
+ describe RequestLogAnalyzer::Tracker::Base do
4
+
5
+ describe 'API' do
6
+
7
+ before(:each) do
8
+ @tracker = Class.new(RequestLogAnalyzer::Tracker::Base).new
9
+
10
+ @summarizer = RequestLogAnalyzer::Aggregator::Summarizer.new(mock_source)
11
+ @summarizer.trackers << @tracker
12
+ end
13
+
14
+ it "should receive :prepare when the summarizer is preparing" do
15
+ @tracker.should_receive(:prepare).once
16
+ @summarizer.prepare
17
+ end
18
+
19
+ it "should receive :update for every request for which should_update? returns true" do
20
+ @tracker.should_receive(:should_update?).twice.and_return(true)
21
+ @tracker.should_receive(:update).twice
22
+
23
+ @summarizer.aggregate(testing_format.request(:field => 'value1'))
24
+ @summarizer.aggregate(testing_format.request(:field => 'value2'))
25
+ end
26
+
27
+ it "should not :update for every request for which should_update? returns false" do
28
+ @tracker.should_receive(:should_update?).twice.and_return(false)
29
+ @tracker.should_not_receive(:update)
30
+
31
+ @summarizer.aggregate(testing_format.request(:field => 'value1'))
32
+ @summarizer.aggregate(testing_format.request(:field => 'value2'))
33
+ end
34
+
35
+ it "should receive :report when the summary report is being built" do
36
+ @tracker.should_receive(:report).with(anything).once
37
+ @summarizer.report(mock_output)
38
+ end
39
+
40
+ it "should receieve :finalize when the summarizer is finalizing" do
41
+ @tracker.should_receive(:finalize).once
42
+ @summarizer.finalize
43
+ end
44
+ end
45
+
46
+ describe '#should_update?' do
47
+ before(:each) do
48
+ @tracker_class = Class.new(RequestLogAnalyzer::Tracker::Base)
49
+ end
50
+
51
+ it "should return true by default, when no checks are installed" do
52
+ tracker = @tracker_class.new
53
+ tracker.should_update?(testing_format.request).should be_true
54
+ end
55
+
56
+ it "should return false if the line type is not in the request" do
57
+ tracker = @tracker_class.new(:line_type => :not_there)
58
+ tracker.should_update?(request(:line_type => :different)).should be_false
59
+ end
60
+
61
+ it "should return true if the line type is in the request" do
62
+ tracker = @tracker_class.new(:line_type => :there)
63
+ tracker.should_update?(request(:line_type => :there)).should be_true
64
+ end
65
+
66
+ it "should return true if a field name is given to :if and it is in the request" do
67
+ tracker = @tracker_class.new(:if => :field)
68
+ tracker.should_update?(request(:field => 'anything')).should be_true
69
+ end
70
+
71
+ it "should return false if a field name is given to :if and it is not the request" do
72
+ tracker = @tracker_class.new(:if => :field)
73
+ tracker.should_update?(request(:other_field => 'anything')).should be_false
74
+ end
75
+
76
+ it "should return false if a field name is given to :unless and it is in the request" do
77
+ tracker = @tracker_class.new(:unless => :field)
78
+ tracker.should_update?(request(:field => 'anything')).should be_false
79
+ end
80
+
81
+ it "should return true if a field name is given to :unless and it is not the request" do
82
+ tracker = @tracker_class.new(:unless => :field)
83
+ tracker.should_update?(request(:other_field => 'anything')).should be_true
84
+ end
85
+
86
+ it "should return the value of the block if one is given to the :if option" do
87
+ tracker = @tracker_class.new(:if => lambda { false } )
88
+ tracker.should_update?(request(:field => 'anything')).should be_false
89
+ end
90
+
91
+ it "should return the inverse value of the block if one is given to the :if option" do
92
+ tracker = @tracker_class.new(:unless => lambda { false } )
93
+ tracker.should_update?(request(:field => 'anything')).should be_true
94
+ end
95
+
96
+ it "should return false if any of the checks fail" do
97
+ tracker = @tracker_class.new(:if => :field, :unless => lambda { false }, :line_type => :not_present )
98
+ tracker.should_update?(request(:line_type => :present, :field => 'anything')).should be_false
99
+ end
100
+
101
+ it "should return true if all of the checks succeed" do
102
+ tracker = @tracker_class.new(:if => :field, :unless => lambda { false }, :line_type => :present )
103
+ tracker.should_update?(request(:line_type => :present, :field => 'anything')).should be_true
104
+ end
105
+
106
+
107
+ end
108
+
109
+ describe '#to_yaml_object' do
110
+
111
+ before(:each) do
112
+ @tracker = Class.new(RequestLogAnalyzer::Tracker::Base).new
113
+
114
+ @summarizer = RequestLogAnalyzer::Aggregator::Summarizer.new(mock_source)
115
+ @summarizer.trackers << @tracker
116
+ end
117
+
118
+ it "should receive :to_yaml object when finalizing" do
119
+ @summarizer.options[:yaml] = temp_output_file(:yaml)
120
+ @tracker.should_receive(:to_yaml_object).once
121
+ @summarizer.to_yaml
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,107 @@
1
+ require File.dirname(__FILE__) + '/../../spec_helper'
2
+
3
+ describe RequestLogAnalyzer::Tracker::Traffic do
4
+
5
+ context 'using a field-based category' do
6
+ before(:each) do
7
+ @tracker = RequestLogAnalyzer::Tracker::Traffic.new(:traffic => :traffic, :category => :category)
8
+ @tracker.prepare
9
+
10
+ @tracker.update(request(:category => 'a', :traffic => 1))
11
+ @tracker.update(request(:category => 'b', :traffic => 2))
12
+ @tracker.update(request(:category => 'b', :traffic => 3))
13
+ end
14
+
15
+ it "should register a request in the right category using the category field" do
16
+ @tracker.categories.should include('a', 'b')
17
+ end
18
+
19
+ it "should register requests under the correct category" do
20
+ @tracker.hits('a').should == 1
21
+ @tracker.hits('b').should == 2
22
+ end
23
+ end
24
+
25
+ context 'using a dynamic category' do
26
+
27
+ before(:each) do
28
+ @categorizer = lambda { |request| request[:traffic] < 2 ? 'few' : 'lots' }
29
+ @tracker = RequestLogAnalyzer::Tracker::Traffic.new(:traffic => :traffic, :category => @categorizer)
30
+ @tracker.prepare
31
+
32
+ @tracker.update(request(:category => 'a', :traffic => 1))
33
+ @tracker.update(request(:category => 'b', :traffic => 2))
34
+ @tracker.update(request(:category => 'b', :traffic => 3))
35
+ end
36
+
37
+ it "should use the categorizer to determine the category" do
38
+ @tracker.categories.should include('few', 'lots')
39
+ end
40
+
41
+ it "should register requests under the correct category using the categorizer" do
42
+ @tracker.hits('few').should == 1
43
+ @tracker.hits('lots').should == 2
44
+ end
45
+ end
46
+
47
+ describe '#update' do
48
+
49
+ before(:each) do
50
+ @tracker = RequestLogAnalyzer::Tracker::Traffic.new(:traffic => :traffic, :category => :category)
51
+ @tracker.prepare
52
+
53
+ @tracker.update(request(:category => 'a', :traffic => 2))
54
+ @tracker.update(request(:category => 'a', :traffic => 1))
55
+ @tracker.update(request(:category => 'a', :traffic => 3))
56
+ end
57
+
58
+ it "should calculate the total traffic correctly" do
59
+ @tracker.sum('a').should == 6
60
+ end
61
+
62
+ it "should calculate the traffic variance correctly" do
63
+ @tracker.variance('a').should == 1.0
64
+ end
65
+
66
+ it "should calculate the traffic standard deviation correctly" do
67
+ @tracker.stddev('a').should == 1.0
68
+ end
69
+
70
+ it "should calculate the average traffic correctly" do
71
+ @tracker.mean('a').should == 2.0
72
+ end
73
+
74
+ it "should calculate the overall mean traffic correctly" do
75
+ @tracker.mean_overall.should == 2.0
76
+ end
77
+
78
+ it "should set min and max traffic correctly" do
79
+ @tracker.min('a').should == 1
80
+ @tracker.max('a').should == 3
81
+ end
82
+ end
83
+
84
+ describe '#report' do
85
+ before(:each) do
86
+ @tracker = RequestLogAnalyzer::Tracker::Traffic.new(:category => :category, :traffic => :traffic)
87
+ @tracker.prepare
88
+ end
89
+
90
+ it "should generate a report without errors when one category is present" do
91
+ @tracker.update(request(:category => 'a', :traffic => 2))
92
+ @tracker.report(mock_output)
93
+ lambda { @tracker.report(mock_output) }.should_not raise_error
94
+ end
95
+
96
+ it "should generate a report without errors when no category is present" do
97
+ lambda { @tracker.report(mock_output) }.should_not raise_error
98
+ end
99
+
100
+ it "should generate a report without errors when multiple categories are present" do
101
+ @tracker.update(request(:category => 'a', :traffic => 2))
102
+ @tracker.update(request(:category => 'b', :traffic => 2))
103
+ lambda { @tracker.report(mock_output) }.should_not raise_error
104
+ end
105
+
106
+ end
107
+ end
@@ -0,0 +1,323 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/tasklib'
4
+ require 'date'
5
+ require 'git'
6
+
7
+ module GithubGem
8
+
9
+ # Detects the gemspc file of this project using heuristics.
10
+ def self.detect_gemspec_file
11
+ FileList['*.gemspec'].first
12
+ end
13
+
14
+ # Detects the main include file of this project using heuristics
15
+ def self.detect_main_include
16
+ if detect_gemspec_file =~ /^(\.*)\.gemspec$/ && File.exist?("lib/#{$1}.rb")
17
+ "lib/#{$1}.rb"
18
+ elsif FileList['lib/*.rb'].length == 1
19
+ FileList['lib/*.rb'].first
20
+ else
21
+ nil
22
+ end
23
+ end
24
+
25
+ class RakeTasks
26
+
27
+ attr_reader :gemspec, :modified_files, :git
28
+ attr_accessor :gemspec_file, :task_namespace, :main_include, :root_dir, :spec_pattern, :test_pattern, :remote, :remote_branch, :local_branch
29
+
30
+ # Initializes the settings, yields itself for configuration
31
+ # and defines the rake tasks based on the gemspec file.
32
+ def initialize(task_namespace = :gem)
33
+ @gemspec_file = GithubGem.detect_gemspec_file
34
+ @task_namespace = task_namespace
35
+ @main_include = GithubGem.detect_main_include
36
+ @modified_files = []
37
+ @root_dir = Dir.pwd
38
+ @test_pattern = 'test/**/*_test.rb'
39
+ @spec_pattern = 'spec/**/*_spec.rb'
40
+ @local_branch = 'master'
41
+ @remote = 'origin'
42
+ @remote_branch = 'master'
43
+
44
+ yield(self) if block_given?
45
+
46
+ @git = Git.open(@root_dir)
47
+ load_gemspec!
48
+ define_tasks!
49
+ end
50
+
51
+ protected
52
+
53
+ # Define Unit test tasks
54
+ def define_test_tasks!
55
+ require 'rake/testtask'
56
+
57
+ namespace(:test) do
58
+ Rake::TestTask.new(:basic) do |t|
59
+ t.pattern = test_pattern
60
+ t.verbose = true
61
+ t.libs << 'test'
62
+ end
63
+ end
64
+
65
+ desc "Run all unit tests for #{gemspec.name}"
66
+ task(:test => ['test:basic'])
67
+ end
68
+
69
+ # Defines RSpec tasks
70
+ def define_rspec_tasks!
71
+ require 'spec/rake/spectask'
72
+
73
+ namespace(:spec) do
74
+ desc "Verify all RSpec examples for #{gemspec.name}"
75
+ Spec::Rake::SpecTask.new(:basic) do |t|
76
+ t.spec_files = FileList[spec_pattern]
77
+ end
78
+
79
+ desc "Verify all RSpec examples for #{gemspec.name} and output specdoc"
80
+ Spec::Rake::SpecTask.new(:specdoc) do |t|
81
+ t.spec_files = FileList[spec_pattern]
82
+ t.spec_opts << '--format' << 'specdoc' << '--color'
83
+ end
84
+
85
+ desc "Run RCov on specs for #{gemspec.name}"
86
+ Spec::Rake::SpecTask.new(:rcov) do |t|
87
+ t.spec_files = FileList[spec_pattern]
88
+ t.rcov = true
89
+ t.rcov_opts = ['--exclude', '"spec/*,gems/*"', '--rails']
90
+ end
91
+ end
92
+
93
+ desc "Verify all RSpec examples for #{gemspec.name} and output specdoc"
94
+ task(:spec => ['spec:specdoc'])
95
+ end
96
+
97
+ # Defines the rake tasks
98
+ def define_tasks!
99
+
100
+ define_test_tasks! if has_tests?
101
+ define_rspec_tasks! if has_specs?
102
+
103
+ namespace(@task_namespace) do
104
+ desc "Updates the filelist in the gemspec file"
105
+ task(:manifest) { manifest_task }
106
+
107
+ desc "Builds the .gem package"
108
+ task(:build => :manifest) { build_task }
109
+
110
+ desc "Sets the version of the gem in the gemspec"
111
+ task(:set_version => [:check_version, :check_current_branch]) { version_task }
112
+ task(:check_version => :fetch_origin) { check_version_task }
113
+
114
+ task(:fetch_origin) { fetch_origin_task }
115
+ task(:check_current_branch) { check_current_branch_task }
116
+ task(:check_clean_status) { check_clean_status_task }
117
+ task(:check_not_diverged => :fetch_origin) { check_not_diverged_task }
118
+
119
+ checks = [:check_current_branch, :check_clean_status, :check_not_diverged, :check_version]
120
+ checks.unshift('spec:basic') if has_specs?
121
+ checks.unshift('test:basic') if has_tests?
122
+ checks.push << [:check_rubyforge] if gemspec.rubyforge_project
123
+
124
+ desc "Perform all checks that would occur before a release"
125
+ task(:release_checks => checks)
126
+
127
+ release_tasks = [:release_checks, :set_version, :build, :github_release]
128
+ release_tasks << [:rubyforge_release] if gemspec.rubyforge_project
129
+
130
+ desc "Release a new verison of the gem"
131
+ task(:release => release_tasks) { release_task }
132
+
133
+ task(:check_rubyforge) { check_rubyforge_task }
134
+ task(:rubyforge_release) { rubyforge_release_task }
135
+ task(:github_release => [:commit_modified_files, :tag_version]) { github_release_task }
136
+ task(:tag_version) { tag_version_task }
137
+ task(:commit_modified_files) { commit_modified_files_task }
138
+
139
+ desc "Updates the gem release tasks with the latest version on Github"
140
+ task(:update_tasks) { update_tasks_task }
141
+ end
142
+ end
143
+
144
+ # Updates the files list and test_files list in the gemspec file using the list of files
145
+ # in the repository and the spec/test file pattern.
146
+ def manifest_task
147
+ # Load all the gem's files using "git ls-files"
148
+ repository_files = git.ls_files.keys
149
+ test_files = Dir[test_pattern] + Dir[spec_pattern]
150
+
151
+ update_gemspec(:files, repository_files)
152
+ update_gemspec(:test_files, repository_files & test_files)
153
+ end
154
+
155
+ # Builds the gem
156
+ def build_task
157
+ sh "gem build -q #{gemspec_file}"
158
+ Dir.mkdir('pkg') unless File.exist?('pkg')
159
+ sh "mv #{gemspec.name}-#{gemspec.version}.gem pkg/#{gemspec.name}-#{gemspec.version}.gem"
160
+ end
161
+
162
+ # Updates the version number in the gemspec file, the VERSION constant in the main
163
+ # include file and the contents of the VERSION file.
164
+ def version_task
165
+ update_gemspec(:version, ENV['VERSION']) if ENV['VERSION']
166
+ update_gemspec(:date, Date.today)
167
+
168
+ update_version_file(gemspec.version)
169
+ update_version_constant(gemspec.version)
170
+ end
171
+
172
+ def check_version_task
173
+ raise "#{ENV['VERSION']} is not a valid version number!" if ENV['VERSION'] && !Gem::Version.correct?(ENV['VERSION'])
174
+ proposed_version = Gem::Version.new(ENV['VERSION'] || gemspec.version)
175
+ # Loads the latest version number using the created tags
176
+ newest_version = git.tags.map { |tag| tag.name.split('-').last }.compact.map { |v| Gem::Version.new(v) }.max
177
+ raise "This version (#{proposed_version}) is not higher than the highest tagged version (#{newest_version})" if newest_version && newest_version >= proposed_version
178
+ end
179
+
180
+ # Checks whether the current branch is not diverged from the remote branch
181
+ def check_not_diverged_task
182
+ raise "The current branch is diverged from the remote branch!" if git.log.between('HEAD', git.remote(remote).branch(remote_branch).gcommit).any?
183
+ end
184
+
185
+ # Checks whether the repository status ic clean
186
+ def check_clean_status_task
187
+ raise "The current working copy contains modifications" if git.status.changed.any?
188
+ end
189
+
190
+ # Checks whether the current branch is correct
191
+ def check_current_branch_task
192
+ raise "Currently not on #{local_branch} branch!" unless git.branch.name == local_branch.to_s
193
+ end
194
+
195
+ # Fetches the latest updates from Github
196
+ def fetch_origin_task
197
+ git.fetch('origin')
198
+ end
199
+
200
+ # Commits every file that has been changed by the release task.
201
+ def commit_modified_files_task
202
+ if modified_files.any?
203
+ modified_files.each { |file| git.add(file) }
204
+ git.commit("Released #{gemspec.name} gem version #{gemspec.version}")
205
+ end
206
+ end
207
+
208
+ # Adds a tag for the released version
209
+ def tag_version_task
210
+ git.add_tag("#{gemspec.name}-#{gemspec.version}")
211
+ end
212
+
213
+ # Pushes the changes and tag to github
214
+ def github_release_task
215
+ git.push(remote, remote_branch, true)
216
+ end
217
+
218
+ # Checks whether Rubyforge is configured properly
219
+ def check_rubyforge_task
220
+ # Login no longer necessary when using rubyforge 2.0.0 gem
221
+ # raise "Could not login on rubyforge!" unless `rubyforge login 2>&1`.strip.empty?
222
+ output = `rubyforge names`.split("\n")
223
+ raise "Rubyforge group not found!" unless output.any? { |line| %r[^groups\s*\:.*\b#{Regexp.quote(gemspec.rubyforge_project)}\b.*] =~ line }
224
+ raise "Rubyforge package not found!" unless output.any? { |line| %r[^packages\s*\:.*\b#{Regexp.quote(gemspec.name)}\b.*] =~ line }
225
+ end
226
+
227
+ # Task to release the .gem file toRubyforge.
228
+ def rubyforge_release_task
229
+ sh 'rubyforge', 'add_release', gemspec.rubyforge_project, gemspec.name, gemspec.version.to_s, "pkg/#{gemspec.name}-#{gemspec.version}.gem"
230
+ end
231
+
232
+ # Gem release task.
233
+ # All work is done by the task's dependencies, so just display a release completed message.
234
+ def release_task
235
+ puts
236
+ puts '------------------------------------------------------------'
237
+ puts "Released #{gemspec.name} version #{gemspec.version}"
238
+ end
239
+
240
+ private
241
+
242
+ # Checks whether this project has any RSpec files
243
+ def has_specs?
244
+ FileList[spec_pattern].any?
245
+ end
246
+
247
+ # Checks whether this project has any unit test files
248
+ def has_tests?
249
+ FileList[test_pattern].any?
250
+ end
251
+
252
+ # Loads the gemspec file
253
+ def load_gemspec!
254
+ @gemspec = eval(File.read(@gemspec_file))
255
+ end
256
+
257
+ # Updates the VERSION file with the new version
258
+ def update_version_file(version)
259
+ if File.exists?('VERSION')
260
+ File.open('VERSION', 'w') { |f| f << version.to_s }
261
+ modified_files << 'VERSION'
262
+ end
263
+ end
264
+
265
+ # Updates the VERSION constant in the main include file if it exists
266
+ def update_version_constant(version)
267
+ if main_include && File.exist?(main_include)
268
+ file_contents = File.read(main_include)
269
+ if file_contents.sub!(/^(\s+VERSION\s*=\s*)[^\s].*$/) { $1 + version.to_s.inspect }
270
+ File.open(main_include, 'w') { |f| f << file_contents }
271
+ modified_files << main_include
272
+ end
273
+ end
274
+ end
275
+
276
+ # Updates an attribute of the gemspec file.
277
+ # This function will open the file, and search/replace the attribute using a regular expression.
278
+ def update_gemspec(attribute, new_value, literal = false)
279
+
280
+ unless literal
281
+ new_value = case new_value
282
+ when Array then "%w(#{new_value.join(' ')})"
283
+ when Hash, String then new_value.inspect
284
+ when Date then new_value.strftime('%Y-%m-%d').inspect
285
+ else raise "Cannot write value #{new_value.inspect} to gemspec file!"
286
+ end
287
+ end
288
+
289
+ spec = File.read(gemspec_file)
290
+ regexp = Regexp.new('^(\s+\w+\.' + Regexp.quote(attribute.to_s) + '\s*=\s*)[^\s].*$')
291
+ if spec.sub!(regexp) { $1 + new_value }
292
+ File.open(gemspec_file, 'w') { |f| f << spec }
293
+ modified_files << gemspec_file
294
+
295
+ # Reload the gemspec so the changes are incorporated
296
+ load_gemspec!
297
+ end
298
+ end
299
+
300
+ # Updates the tasks file using the latest file found on Github
301
+ def update_tasks_task
302
+ require 'net/http'
303
+
304
+ server = 'github.com'
305
+ path = '/wvanbergen/github-gem/raw/master/tasks/github-gem.rake'
306
+
307
+ Net::HTTP.start(server) do |http|
308
+ response = http.get(path)
309
+ open(__FILE__, "w") { |file| file.write(response.body) }
310
+ end
311
+
312
+ relative_file = File.expand_path(__FILE__).sub(%r[^#{git.dir.path}/], '')
313
+ if git.status[relative_file] && git.status[relative_file].type == 'M'
314
+ git.add(relative_file)
315
+ git.commit("Updated to latest gem release management tasks.")
316
+ puts "Updated to latest version of gem release management tasks."
317
+ else
318
+ puts "Release managament tasks already are at the latest version."
319
+ end
320
+ end
321
+
322
+ end
323
+ end