kwala 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. data/README +62 -0
  2. data/bin/kwala +39 -0
  3. data/lib/kwala.rb +59 -0
  4. data/lib/kwala/actions/code_change.rb +128 -0
  5. data/lib/kwala/actions/code_coverage.rb +303 -0
  6. data/lib/kwala/actions/code_duplication.rb +222 -0
  7. data/lib/kwala/actions/code_formatting.rb +358 -0
  8. data/lib/kwala/actions/cycle_detection.rb +118 -0
  9. data/lib/kwala/actions/cyclomatic_complexity.rb +159 -0
  10. data/lib/kwala/actions/dead_code.rb +68 -0
  11. data/lib/kwala/actions/executable_files_check.rb +111 -0
  12. data/lib/kwala/actions/flagged_comments.rb +128 -0
  13. data/lib/kwala/actions/gem_plugin.rb +31 -0
  14. data/lib/kwala/actions/loc_count.rb +153 -0
  15. data/lib/kwala/actions/multi_ruby_unit_test.rb +62 -0
  16. data/lib/kwala/actions/outside_links.rb +85 -0
  17. data/lib/kwala/actions/rails_migrate.rb +62 -0
  18. data/lib/kwala/actions/strange_requires.rb +106 -0
  19. data/lib/kwala/actions/syntax_check.rb +130 -0
  20. data/lib/kwala/actions/unit_test.rb +368 -0
  21. data/lib/kwala/build_action.rb +88 -0
  22. data/lib/kwala/build_context.rb +49 -0
  23. data/lib/kwala/command_line_runner.rb +184 -0
  24. data/lib/kwala/ext/demos.jar +0 -0
  25. data/lib/kwala/ext/pmd.jar +0 -0
  26. data/lib/kwala/ext/prefuse.jar +0 -0
  27. data/lib/kwala/extensions.rb +36 -0
  28. data/lib/kwala/lib/code_analyzer.rb +678 -0
  29. data/lib/kwala/lib/cycle_detector.rb +793 -0
  30. data/lib/kwala/lib/strange_requires_detector.rb +145 -0
  31. data/lib/kwala/notification.rb +45 -0
  32. data/lib/kwala/notifications/email.rb +54 -0
  33. data/lib/kwala/notifications/rss.rb +151 -0
  34. data/lib/kwala/project_builder_utils.rb +178 -0
  35. data/lib/kwala/templates/build_template.html +33 -0
  36. data/lib/kwala/templates/code_change_summary.html +27 -0
  37. data/lib/kwala/templates/code_coverage_detailed.html +25 -0
  38. data/lib/kwala/templates/code_coverage_summary.html +30 -0
  39. data/lib/kwala/templates/code_duplication_detailed.html +45 -0
  40. data/lib/kwala/templates/code_duplication_summary.html +9 -0
  41. data/lib/kwala/templates/code_formatting_detailed.html +27 -0
  42. data/lib/kwala/templates/code_formatting_summary.html +11 -0
  43. data/lib/kwala/templates/cycle_detection_detailed.html +20 -0
  44. data/lib/kwala/templates/cycle_detection_summary.html +7 -0
  45. data/lib/kwala/templates/cyclomatic_complexity_summary.html +27 -0
  46. data/lib/kwala/templates/executable_files_check_detailed.html +31 -0
  47. data/lib/kwala/templates/executable_files_check_summary.html +20 -0
  48. data/lib/kwala/templates/flagged_comments_detailed.html +22 -0
  49. data/lib/kwala/templates/flagged_comments_summary.html +10 -0
  50. data/lib/kwala/templates/loc_count_detailed.html +24 -0
  51. data/lib/kwala/templates/loc_count_summary.html +14 -0
  52. data/lib/kwala/templates/mdd/1.png +0 -0
  53. data/lib/kwala/templates/mdd/2.png +0 -0
  54. data/lib/kwala/templates/mdd/3.png +0 -0
  55. data/lib/kwala/templates/mdd/4.png +0 -0
  56. data/lib/kwala/templates/mdd/5.png +0 -0
  57. data/lib/kwala/templates/outside_links_summary.html +4 -0
  58. data/lib/kwala/templates/strange_requires_detailed.html +23 -0
  59. data/lib/kwala/templates/strange_requires_summary.html +13 -0
  60. data/lib/kwala/templates/style.css +95 -0
  61. data/lib/kwala/templates/syntax_check_detailed.html +21 -0
  62. data/lib/kwala/templates/syntax_check_summary.html +7 -0
  63. data/lib/kwala/templates/unit_test_detailed.html +66 -0
  64. data/lib/kwala/templates/unit_test_summary.html +70 -0
  65. data/test/tc_build_action.rb +32 -0
  66. data/test/tc_templates.rb +24 -0
  67. metadata +118 -0
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env ruby -w
2
+
3
+ # Copyright (c) 2006, Ubiquitous Business Technology (http://ubit.com)
4
+ # All rights reserved.
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are
8
+ # met:
9
+ #
10
+ #
11
+ # * Redistributions of source code must retain the above copyright
12
+ # notice, this list of conditions and the following disclaimer.
13
+ #
14
+ # * Redistributions in binary form must reproduce the above
15
+ # copyright notice, this list of conditions and the following
16
+ # disclaimer in the documentation and/or other materials provided
17
+ # with the distribution.
18
+ #
19
+ # * Neither the name of Ubit nor the names of its
20
+ # contributors may be used to endorse or promote products derived
21
+ # from this software without specific prior written permission.
22
+ #
23
+ #
24
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
25
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
26
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
27
+ # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
28
+ # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
29
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
30
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
31
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
32
+ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
33
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
34
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
35
+ #
36
+ # == Author
37
+ # Michael Reinsch (mr@ubit.com)
38
+
39
+ require File.dirname(__FILE__) + "/cycle_detector" # for ReqWalker
40
+
41
+ class Dir
42
+ def == (other)
43
+ case other
44
+ when String; return path == other
45
+ when Dir; return path == other.path
46
+ when FakeDir; return path == other.path
47
+ end
48
+ false
49
+ end
50
+
51
+ def is_subdir_of?(dir)
52
+ return path[0..dir.path.length-1] == dir.path
53
+ end
54
+
55
+ def cut(basedir)
56
+ path
57
+ end
58
+ end
59
+
60
+
61
+ class StrangeRequiresDetector
62
+
63
+ attr_writer :print_strategy
64
+
65
+ def initialize(printer)
66
+ @graph = Hash.new
67
+ @printer = printer
68
+ @file_filter = Proc.new do |files|
69
+ files.find_all do |f|
70
+ /tests\// !~ f && /extensions\// !~ f &&
71
+ /tools\// !~ f && /\/ms\// !~ f && /\/site\// !~f
72
+ end
73
+ end
74
+ @req_filter = Proc.new do |reqs|
75
+ reqs.find_all do |r|
76
+
77
+ end
78
+ end
79
+ end
80
+
81
+ def cut_base(filename, basename)
82
+ filename.gsub(/^#{basename}/, '')
83
+ end
84
+
85
+ def check(dir, basedir = "")
86
+ files = ReqWalker.find_ruby_files(dir)
87
+ @file_filter.call(files).each do |f|
88
+ ReqWalker.get_requires(f).each do |r|
89
+ r = ReqWalker.find_file(r)
90
+ if r && /\/lib\/ruby\// !~ r
91
+ f_base = Dir.new(File.dirname(f))
92
+ r_base = Dir.new(File.dirname(r))
93
+ # well, we assume that a directory models our "class category"...
94
+ if f_base != r_base
95
+ if r_base.is_subdir_of?(f_base)
96
+ @printer.subdir_requirement(cut_base(f,basedir), cut_base(r,basedir))
97
+ else
98
+ @printer.parent_dir_requirement(cut_base(f,basedir), cut_base(r,basedir))
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+
106
+ end
107
+
108
+
109
+ if __FILE__ == $0
110
+ class STDOUTPrintStrategy
111
+ def subdir_requirement(from, to)
112
+ puts "yellow: #{from} -> #{to}"
113
+ end
114
+ def parent_dir_requirement(from, to)
115
+ puts "red : #{from} -> #{to}"
116
+ end
117
+ end
118
+
119
+ require 'getoptlong'
120
+
121
+ srd = StrangeRequiresDetector.new(STDOUTPrintStrategy.new)
122
+ dir = "./"
123
+
124
+ begin
125
+ options = GetoptLong.new(
126
+ ["-d","--dir", GetoptLong::REQUIRED_ARGUMENT]
127
+ )
128
+
129
+ options.each do |opt,arg|
130
+ case opt
131
+ when "-d"
132
+ dir = arg
133
+ else
134
+ STDERR.puts "Unknown command #{ opt }"
135
+ exit 1
136
+ end
137
+ end
138
+ rescue => err
139
+ STDERR.puts "Error invalid command"
140
+ exit 1
141
+ end
142
+
143
+ srd.check(dir)
144
+
145
+ end
@@ -0,0 +1,45 @@
1
+
2
+ class Notification
3
+ @@notifications = []
4
+
5
+ def self.context=(context)
6
+ @context = context
7
+ end
8
+
9
+ def self.context
10
+ @context
11
+ end
12
+
13
+ def self.notify(event, result, data)
14
+ self.notifications.each { |n| n.notify(event, result, data) }
15
+ end
16
+
17
+ def self.add_notification(obj)
18
+ self.notifications << obj
19
+ end
20
+
21
+ def self.notifications
22
+ if @@notifications.empty?
23
+ Dir.glob(NOTIFICATION_DIR + "/*.rb").sort.each do |file|
24
+ begin
25
+ require file
26
+ rescue Exception => err
27
+ $stderr.puts "Notification error loading #{file}", err.class, err.message
28
+ end
29
+ end
30
+ end
31
+ @@notifications
32
+ end
33
+
34
+ def notify(event, result = nil, data = nil)
35
+ if self.respond_to?(event)
36
+ self.send(event, result, data, Notification.context)
37
+ else
38
+ undefined_event(event, result, data, Notification.context)
39
+ end
40
+ end
41
+
42
+ def undefined_event(event, result, data, context)
43
+ end
44
+
45
+ end
@@ -0,0 +1,54 @@
1
+ require 'net/smtp'
2
+
3
+ class EmailNotification < Notification
4
+
5
+ def unit_test(result, data, context)
6
+ # TODO: Probably should move this to a configuration based
7
+ from_addr = ""
8
+ to_addr = [ "" ]
9
+ smtp_server = ""
10
+ smtp_port = 25
11
+
12
+ # Get the time and set the status msg
13
+ now = Time.now
14
+ status_msg = "Unit tests run #{result.to_s} at"
15
+
16
+ # just file name for now
17
+ data ||= {}
18
+ data = Array(data[:filtered_results]).map do |d|
19
+ d[:file_name]
20
+ end
21
+
22
+ if !data.empty?
23
+ failure_msg = <<-MSG
24
+ Test files that reported failures:
25
+ #{data.join("\n")}
26
+ MSG
27
+ else
28
+ failure_msg = ''
29
+ end
30
+
31
+ # Create the SMTP envelope
32
+ msgstr = <<-END_OF_MESSAGE
33
+ From: Kwala Continuous Integration <EMAIL>
34
+ To: Edge Dev Mailing List <EMAIL>
35
+ Subject: [Kwala] edge build #{context.vcs_num} #{result}
36
+ Date: #{now.strftime("%a, %d %b %Y %X +0900")}
37
+ Message-Id: <#{now.strftime("%m%d%Y%H%M%S")}@EMAIL>
38
+
39
+ Result Summary: #{result.to_s}
40
+ See Results at https://BASEURL/kwala/
41
+
42
+ #{failure_msg}
43
+
44
+ Commit Info:
45
+ #{context.vcs_info}
46
+ END_OF_MESSAGE
47
+
48
+ # Send the message
49
+ Net::SMTP.start(smtp_server, smtp_port) do |smtp|
50
+ smtp.send_message(msgstr, from_addr, *to_addr)
51
+ end
52
+ end
53
+
54
+ end
@@ -0,0 +1,151 @@
1
+ require 'pstore'
2
+ require 'rss/2.0'
3
+ require 'rss/utils'
4
+
5
+ class RSSNotification < Notification
6
+
7
+ def unit_test(result, data, context)
8
+ @test_results = data[:test_results]
9
+
10
+ update_pstore(context)
11
+ make_rss(context)
12
+ end
13
+
14
+ protected
15
+
16
+ PSTORE_FILE_NAME = 'prev_test_results.pstore'
17
+
18
+ RSS_FILE_NAME = 'unit_test_results.rss'
19
+
20
+ class UnitTestResult
21
+ attr_reader :filename, :test_results, :errors, :warnings, :first_seen
22
+ attr_accessor :last_seen
23
+
24
+ def initialize(result_hash, timestamp)
25
+ @filename = result_hash[:file_name]
26
+ @errors = result_hash[:sys_errors]
27
+ @warnings = result_hash[:warnings]
28
+ @first_seen = @last_seen = timestamp
29
+
30
+ @test_results = Hash.new
31
+ result_hash[:version].each do |r|
32
+ if !r[:test_results].empty?
33
+ @test_results[r[:version_name]] = r[:test_results]
34
+ end
35
+ end
36
+ end
37
+
38
+ def pass?
39
+ @errors.empty? && @warnings.empty? && @test_results.empty?
40
+ end
41
+
42
+ def ==(obj)
43
+ res = obj.kind_of?(UnitTestResult)
44
+ res &= obj.filename == @filename
45
+ res &= obj.errors == @errors
46
+ res &= obj.warnings == @warnings
47
+ res &= obj.test_results == @test_results
48
+ res
49
+ end
50
+
51
+ def to_rss_item(compare_timestamp)
52
+ if @last_seen < compare_timestamp
53
+ defect_status = "Fixed"
54
+ else
55
+ defect_status = "New"
56
+ end
57
+
58
+ item = RSS::Rss::Channel::Item.new
59
+ item.title = "#{@first_seen.strftime("%b %d, %H:%M")} - #{defect_status} defects in #{@filename}"
60
+ item.pubDate = @first_seen
61
+ item.description = defect_details_html
62
+ item
63
+ end
64
+
65
+ protected
66
+
67
+ def system_messages_html(messages, type)
68
+ details = ""
69
+ if !messages.empty?
70
+ details << "<h2>System #{type}s</h2>\n<pre>\n"
71
+ details << RSS::Utils.html_escape(messages.join)
72
+ details << "\n</pre>\n"
73
+ end
74
+ details
75
+ end
76
+
77
+ def test_result_html
78
+ details = ""
79
+ @test_results.each do |ruby_ver, defects|
80
+ details << "<h2>Test Results for #{ruby_ver}</h2>\n"
81
+ defects.each do |d|
82
+ details << "<pre>\n"
83
+ details << RSS::Utils.html_escape(d[:test_info])
84
+ details << "\n</pre>\n"
85
+ end
86
+ end
87
+ details
88
+ end
89
+
90
+ def defect_details_html
91
+ details = ""
92
+ details << test_result_html
93
+ details << system_messages_html(@errors, "Error")
94
+ details << system_messages_html(@warnings, "Warning")
95
+ details
96
+ end
97
+ end
98
+
99
+ def create_pstore(context)
100
+ PStore.new(File.join(context.output_directory, PSTORE_FILE_NAME))
101
+ end
102
+
103
+ def update_pstore(context)
104
+ create_pstore(context).transaction do |store|
105
+ run_timestamp = Time.now
106
+ del_timestamp = run_timestamp - 12*60*60 # 12 hours
107
+ prev_test_results = store.fetch(:prev_test_results, Hash.new)
108
+
109
+ prev_test_results.delete_if do |key, res|
110
+ res.last_seen <= del_timestamp
111
+ end
112
+
113
+ @test_results.each do |cur_res_hash|
114
+ cur_res = UnitTestResult.new(cur_res_hash, run_timestamp)
115
+ next if cur_res.pass?
116
+
117
+ prev_res = prev_test_results[cur_res.filename]
118
+ if prev_res.nil? || cur_res != prev_res
119
+ # new defects
120
+ prev_test_results[cur_res.filename] = cur_res
121
+ else
122
+ # same'ol defect
123
+ prev_res.last_seen = run_timestamp
124
+ end
125
+ end
126
+
127
+ store[:last_run_timestamp] = run_timestamp
128
+ store[:prev_test_results] = prev_test_results
129
+ end
130
+ end
131
+
132
+ def make_rss(context)
133
+ rss = RSS::Rss.new("2.0")
134
+ chan = RSS::Rss::Channel.new
135
+ chan.title = chan.description = "Unit Test Results for #{context.project_name}"
136
+ rss.channel = chan
137
+
138
+ create_pstore(context).transaction(true) do |store|
139
+ last_run_timestamp = store.fetch(:last_run_timestamp, 0)
140
+ test_results = store.fetch(:prev_test_results, Hash.new)
141
+
142
+ test_results.each do |key, res|
143
+ chan.items << res.to_rss_item(last_run_timestamp)
144
+ end
145
+ end
146
+
147
+ File.open(File.join(context.output_directory, RSS_FILE_NAME), "w") {|f| f.puts(rss.to_s) }
148
+ end
149
+
150
+
151
+ end
@@ -0,0 +1,178 @@
1
+ module ProjectBuilderUtils
2
+
3
+ def ProjectBuilderUtils.find_ruby_files(dir)
4
+ ruby_files = Array.new
5
+
6
+ Find.find(dir) do |path|
7
+ if File.directory?(path)
8
+ # Ignore all directories starting with .
9
+ if File.basename(path)[0] == ?. || File.basename(path) =~ /vendor/
10
+ Find.prune
11
+ else
12
+ next
13
+ end
14
+ elsif File.file?(path)
15
+ case path.to_s
16
+ when /\.rb$/
17
+ ruby_files << path
18
+ when /\.rb~$/
19
+ next
20
+ when ProjectBuilderUtils.contains_ruby_execution_command?(path)
21
+ ruby_files << path
22
+ else
23
+ end
24
+ end
25
+ end
26
+
27
+ ruby_files = ruby_files.sort
28
+ end
29
+
30
+ def ProjectBuilderUtils.strip_base_dir(base_dir, file)
31
+ m = /(#{base_dir})(.*)/.match(file)
32
+ m[2][1..-1]
33
+ end
34
+
35
+ def ProjectBuilderUtils.get_ruby_and_unit_test_files(dir=Dir.pwd)
36
+ ruby_files = ProjectBuilderUtils.find_ruby_files(dir)
37
+
38
+ # remove all test case files
39
+ test_files, ruby_files = ruby_files.partition do |f|
40
+ f =~ /\/(tc|ts)_.+\.rb$|.*_test.rb$/
41
+ end
42
+
43
+ # XXX: UGLY HACK TO AVOID SIMULATION
44
+ test_files.delete_if { |f| f =~ /test\/simulation\// }
45
+
46
+ [ruby_files.uniq, test_files.sort.uniq]
47
+ end
48
+
49
+ def ProjectBuilderUtils.clean_trailing_slash(dir)
50
+ if dir[dir.size - 1] == ?/
51
+ if dir.size > 1
52
+ dir = dir[0..dir.size - 2]
53
+ else
54
+ dir = "."
55
+ end
56
+ end
57
+ dir
58
+ end
59
+
60
+
61
+ def ProjectBuilderUtils.expand_template(template, data)
62
+ str = StringIO.new
63
+ template.expand(str, data)
64
+ str.string
65
+ end
66
+
67
+ def ProjectBuilderUtils.load_actions
68
+ Dir.glob(ACTION_DIR + "/*.rb").sort.each do |action_file|
69
+ require(action_file)
70
+ end
71
+ end
72
+
73
+ def ProjectBuilderUtils.contains_ruby_execution_command?(path)
74
+ File.open(path, "r") do |f|
75
+ return (f.read(1024).to_s =~ /^#!.*ruby/)
76
+ end
77
+ end
78
+
79
+
80
+ def ProjectBuilderUtils.expand_summary_template(data, project_name, summaries)
81
+ template = TemplateFile.new("#{TEMPLATE_DIR}/build_template.html")
82
+ data[:action_summaries] = summaries
83
+ ProjectBuilderUtils.expand_template(template, data)
84
+ end
85
+
86
+ def ProjectBuilderUtils.make_summary_report(context, summaries)
87
+ if context.vcs_num
88
+
89
+ context.amrita_data[:vcs] = {
90
+ :vcs_num => context.vcs_num,
91
+ :vcs_info => context.vcs_info
92
+ }
93
+ end
94
+ context.build_complete
95
+
96
+ exp_res = ProjectBuilderUtils.expand_summary_template(context.amrita_data, context.project_name, summaries)
97
+ if context.output_directory
98
+ sum_file = "#{context.output_directory}/index.html"
99
+ File.open(sum_file, "w") do |f|
100
+ f.puts exp_res
101
+ end
102
+ else
103
+ STDOUT.puts exp_res
104
+ end
105
+ end
106
+
107
+ # Pull this into better place
108
+ def ProjectBuilderUtils.calc_score(context, action_instances)
109
+ scores = action_instances.map { |a| a.score }
110
+ scores.compact!
111
+
112
+ # If action doesn't calculate score, just default to 1 for now
113
+ if !scores.empty?
114
+ total = scores.inject(0) { |s,v| s + v }
115
+
116
+ # puts "Total : #{total} SIZE: #{scores.size}"
117
+ score = ((total / (scores.size * 10).to_f) * 10).ceil
118
+ img_num = ProjectBuilderUtils.image_for_score(score)
119
+ # puts "!!!!!!!!!!!!!!!!!! SCORE : #{score} IMG #{img_num} !!!!!!!!!!!"
120
+ else
121
+ img_num = 1
122
+ end
123
+ # Mascot Driven Development!
124
+ context.amrita_data[:mdd] = Amrita.a(:src => "mdd/#{img_num}.png")
125
+ end
126
+
127
+ def ProjectBuilderUtils.image_for_score(score)
128
+ case score
129
+ when 8..10 : 1
130
+ when 6..7 : 2
131
+ when 4..5 : 3
132
+ when 3..2 : 4
133
+ else
134
+ 5
135
+ end
136
+ end
137
+
138
+ def ProjectBuilderUtils.make_detailed_reports(context, action_instances)
139
+ if context.output_directory
140
+ action_instances.each do |action|
141
+ det_file, det_res = action.detailed_display(context)
142
+ if det_file
143
+ File.open(det_file, "w") do |f|
144
+ f.puts det_res
145
+ end
146
+ end
147
+ end
148
+ end
149
+
150
+ ProjectBuilderUtils.calc_score(context, action_instances)
151
+ end
152
+
153
+ # returns an array of the actions that were successfully built
154
+ def ProjectBuilderUtils.build_actions(action_instances, context)
155
+ success = Array.new
156
+
157
+ action_instances.each do |action|
158
+ begin
159
+ action.build_action(context)
160
+ success << action
161
+ rescue KwalaAbortBuildException => err
162
+ # We want to failed action, but stop the others
163
+ success << action
164
+ break
165
+ end
166
+ end
167
+
168
+ success
169
+ end
170
+
171
+ def ProjectBuilderUtils.build_summaries(action_instances, context)
172
+ action_instances.map do |action|
173
+ { :summary => action.summary_display(context) }
174
+ end
175
+ end
176
+
177
+
178
+ end