crazy_ivan 1.1.1 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -52,11 +52,10 @@ Crazy Ivan (CI) is simplest possible continuous integration tool.
52
52
  to execute at the /:
53
53
 
54
54
  /shopify
55
- /.ci
56
- update
57
- version
58
- test
59
- conclusion
55
+ /.ci/update
56
+ version
57
+ test
58
+ conclusion
60
59
 
61
60
  * crazy_ivan first executes `update` and captures the output:
62
61
 
data/bin/crazy_ivan CHANGED
@@ -16,9 +16,10 @@ require "logger"
16
16
  Syslog.open('crazy_ivan', Syslog::LOG_PID | Syslog::LOG_CONS)
17
17
 
18
18
  Signal.trap("INT") do
19
- Syslog.debug("Interrupted - Now dropping a note in the test output and exiting.")
19
+ # Syslog.debug("Interrupted - Now dropping a note in the test output and exiting.")
20
+ Syslog.debug("Interrupted - exiting.")
20
21
  CrazyIvan.interrupt_test
21
- CrazyIvan::ProcessManager.unlock
22
+ ProcessManager.unlock
22
23
  puts
23
24
  exit
24
25
  end
@@ -52,35 +53,6 @@ end
52
53
 
53
54
  options = {}
54
55
 
55
-
56
- class CrazyIvan::ProcessManager
57
- PidFile = '/tmp/crazy_ivan.pid'
58
-
59
- def self.acquire_lock
60
- lock_exclusively!
61
- yield
62
- end
63
-
64
- def self.unlock
65
- File.new(PidFile).flock(File::LOCK_UN)
66
- end
67
-
68
- def self.ci_already_running?
69
- File.exists?(PidFile) && !File.new(PidFile).flock(File::LOCK_EX | File::LOCK_NB)
70
- end
71
-
72
- def self.lock_exclusively!
73
- pid = Integer(File.read(PidFile)) if File.exists?(PidFile)
74
- File.open('/tmp/crazy_ivan.pid', "w+") { |fp| fp << Process.pid }
75
-
76
- if ci_already_running?
77
- Process.kill("INT", pid)
78
- Syslog.debug("Detected another running CI process #{pid}; interrupting it and starting myself")
79
- File.new(PidFile).flock(File::LOCK_EX)
80
- end
81
- end
82
- end
83
-
84
56
  ARGV.options do |opts|
85
57
  opts.banner = "Usage: #{File.basename($PROGRAM_NAME)} test_reports_path"
86
58
 
@@ -102,7 +74,8 @@ ARGV.options do |opts|
102
74
  when /setup/
103
75
  CrazyIvan::setup
104
76
  when /\w+/ # a directory for test results
105
- CrazyIvan::ProcessManager.acquire_lock do
77
+ ProcessManager.acquire_lock do
78
+ Syslog.debug "Generating reports in #{ARGV[0]}"
106
79
  CrazyIvan::generate_test_reports_in(ARGV[0])
107
80
  end
108
81
  else
@@ -93,4 +93,9 @@ Campfire.basic_auth ARGV[1], 'x'
93
93
 
94
94
  campfire_room_id = campfire_url.path[/\d+/]
95
95
  campfire_room = Campfire.room(campfire_room_id)
96
- campfire_room.message "#{report['project_name']} broke. Please take a look at #{ARGV[2]}"
96
+
97
+ if report['test']['exit_status'].nil?
98
+ campfire_room.message "#{report['project_name']} #{report['version']['output']} built happily: #{ARGV[2]}"
99
+ else
100
+ campfire_room.message "#{report['project_name']} #{report['version']['output']} broke. Please take a look at #{ARGV[2]}"
101
+ end
@@ -0,0 +1,48 @@
1
+ class ProcessManager
2
+ @@pidfile = '/tmp/crazy_ivan.pid'
3
+
4
+ def self.pidfile=(file)
5
+ @@pidfile = file
6
+ end
7
+
8
+ def self.acquire_lock
9
+ lock_exclusively!
10
+ yield
11
+ unlock
12
+ end
13
+
14
+ def self.unlock
15
+ File.new(@@pidfile).flock(File::LOCK_UN)
16
+ end
17
+
18
+ def self.ci_already_running?
19
+ File.exists?(@@pidfile) && !File.new(@@pidfile).flock(File::LOCK_EX | File::LOCK_NB)
20
+ end
21
+
22
+ def self.lock_exclusively!(options = {})
23
+ pid = Integer(File.read(@@pidfile)) if File.exists?(@@pidfile)
24
+
25
+ Syslog.debug "Acquiring lock"
26
+
27
+ if options[:interrupt_existing_process]
28
+ File.open(@@pidfile, "w+") { |fp| fp << Process.pid }
29
+
30
+ if ci_already_running?
31
+ Process.kill("INT", pid)
32
+ Syslog.debug("Detected another running CI process #{pid}; interrupting it and starting myself")
33
+ File.new(@@pidfile).flock(File::LOCK_EX)
34
+ end
35
+ else
36
+ if ci_already_running?
37
+ msg = "Detected another running CI process #{pid} - terminating myself"
38
+ Syslog.warning msg
39
+ puts msg
40
+ Process.kill("INT", 0)
41
+ else
42
+ Syslog.debug("Locked CI process pid file")
43
+ Syslog.debug("Writing to pid file with #{Process.pid}")
44
+ File.open(@@pidfile, "w+") { |fp| fp << Process.pid }
45
+ end
46
+ end
47
+ end
48
+ end
@@ -7,14 +7,23 @@ class ReportAssembler
7
7
  def initialize(projects_directory, output_directory)
8
8
  @runners = []
9
9
  @projects_directory = projects_directory
10
- @output_directory = output_directory
10
+ @output_directory = File.expand_path(output_directory, projects_directory)
11
+ end
12
+
13
+ def different_than_last_version?(runner)
14
+ project_path = File.join(@output_directory, runner.project_name)
15
+
16
+ Dir.chdir(project_path) do
17
+ version = runner.results[:version][:output]
18
+ Dir["#{version}.json"].size == 0
19
+ end
11
20
  end
12
21
 
13
22
  def generate
14
23
  Dir.chdir(@projects_directory) do
15
24
  Dir['*'].each do |dir|
16
25
  if File.directory?(dir)
17
- runners << TestRunner.new(File.join(@projects_directory, dir))
26
+ runners << TestRunner.new(File.join(@projects_directory, dir), self)
18
27
  end
19
28
  end
20
29
  end
@@ -37,10 +46,15 @@ class ReportAssembler
37
46
  # Update the report in currently_building.json with the version output and error
38
47
  runner.version!
39
48
  update_project(runner)
40
-
41
- # Empty the currently_building.json and add to recents.json this new report with the test output and error
42
- runner.test!
43
- update_project(runner)
49
+
50
+ if different_than_last_version?(runner)
51
+ # Empty the currently_building.json and add to recents.json this new report with the test output and error
52
+ runner.test! # update_project will be called from within the runner to stream the test output
53
+ update_project(runner)
54
+ else
55
+ flush_build_progress(runner)
56
+ Syslog.debug("Already tested #{runner.project_name} version #{runner.results[:version][:output]} - skipping test")
57
+ end
44
58
  end
45
59
  end
46
60
  end
@@ -48,10 +62,6 @@ class ReportAssembler
48
62
  def filename_from_version(string)
49
63
  s = string[0..240]
50
64
 
51
- if Dir["#{s}*.json"].size > 0
52
- s += "-#{Dir["#{s}*.json"].size}"
53
- end
54
-
55
65
  return s
56
66
  end
57
67
 
@@ -65,16 +75,21 @@ class ReportAssembler
65
75
  return filtered_results
66
76
  end
67
77
 
68
- def flush_build_progress
69
- File.open("currently_building.json", 'w+') do |f|
70
- f.puts({}.to_json)
78
+ def flush_build_progress(runner)
79
+ project_results_path = File.join(@output_directory, runner.project_name)
80
+
81
+ Dir.chdir(project_results_path) do
82
+ File.open("currently_building.json", 'w+') do |f|
83
+ f.puts({}.to_json)
84
+ end
71
85
  end
72
86
  end
73
87
 
74
88
  def update_project(runner)
75
- FileUtils.mkdir_p(runner.project_name)
76
- Dir.chdir(runner.project_name) do
77
-
89
+ project_path = File.expand_path(runner.project_name, @output_directory)
90
+ FileUtils.mkdir_p(project_path)
91
+
92
+ Dir.chdir(project_path) do
78
93
  filename = ''
79
94
 
80
95
  if runner.still_building?
@@ -93,7 +108,7 @@ class ReportAssembler
93
108
 
94
109
  if runner.finished?
95
110
  Syslog.debug "Runner is FINISHED"
96
- flush_build_progress
111
+ flush_build_progress(runner)
97
112
  update_recent(runner.results, filename)
98
113
  end
99
114
  end
@@ -125,10 +140,8 @@ class ReportAssembler
125
140
  end
126
141
 
127
142
  def update_index
128
- index_template = HtmlAssetCrush.crush(File.join(TEMPLATES_PATH, "index.html"))
129
-
130
- File.open('index.html', 'w+') do |f|
131
- f.print index_template
132
- end
143
+ FileUtils.cp(File.expand_path("index.html", TEMPLATES_PATH), 'index.html')
144
+ FileUtils.mkdir_p('javascript')
145
+ FileUtils.cp(File.expand_path("date.js", File.join(TEMPLATES_PATH, 'javascript')), 'javascript/date.js')
133
146
  end
134
147
  end
@@ -1,183 +1,229 @@
1
- <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
2
- "http://www.w3.org/TR/html4/loose.dtd">
1
+ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
3
2
  <html>
4
3
  <head>
5
4
  <meta http-equiv="Content-type" content="text/html; charset=utf-8">
6
5
  <title>Crazy Ivan: CI straight up.</title>
7
6
 
8
- <link rel="stylesheet" href="css/ci.css" type="text/css" charset="utf-8">
9
- <script type="text/javascript" src="javascript/prototype.js"></script>
10
- <script type="text/javascript" src="javascript/scriptaculous.js"></script>
11
- <script type="text/javascript" src="javascript/date.js"></script>
12
- <script type="text/javascript" src="javascript/json-template.js"></script>
7
+ <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.1/jquery.min.js"></script>
8
+ <script type="text/javascript" charset="utf-8" src="javascript/date.js"></script>
13
9
  <script type="text/javascript">
14
- var init = function() {
15
- render('index.jsont', projects());
10
+ // Simple JavaScript Templating
11
+ // John Resig - http://ejohn.org/ - MIT Licensed
12
+ (function(){
13
+ var cache = {};
14
+
15
+ this.tmpl = function tmpl(str, data){
16
+ // Figure out if we're getting a template, or if we need to
17
+ // load the template - and be sure to cache the result.
18
+ var fn = !/\W/.test(str) ?
19
+ cache[str] = cache[str] ||
20
+ tmpl(document.getElementById(str).innerHTML) :
21
+
22
+ // Generate a reusable function that will serve as a template
23
+ // generator (and which will be cached).
24
+ new Function("obj",
25
+ "var p=[],print=function(){p.push.apply(p,arguments);};" +
26
+
27
+ // Introduce the data as local variables using with(){}
28
+ "with(obj){p.push('" +
29
+
30
+ // Convert the template into pure JavaScript
31
+ str
32
+ .replace(/[\r\t\n]/g, " ")
33
+ .split("<%").join("\t")
34
+ .replace(/((^|%>)[^\t]*)'/g, "$1\r")
35
+ .replace(/\t=(.*?)%>/g, "',$1,'")
36
+ .split("\t").join("');")
37
+ .split("%>").join("p.push('")
38
+ .split("\r").join("\\'")
39
+ + "');}return p.join('');");
40
+
41
+ // Provide some basic currying to the user
42
+ return data ? fn( data ) : fn;
43
+ };
44
+ })();
45
+ </script>
46
+ <style type="text/css" media="screen">
47
+ body {
48
+ margin: 2.5em 3em;
49
+ padding: 0;
50
+ background: #fff;
51
+ color: #333;
52
+ font: 100%/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif;
16
53
  }
17
-
18
- var projects = function() {
19
- var projects = [];
20
- var project_names = [];
54
+
55
+ h1 { margin: 0;}
56
+
57
+ pre { margin: 0;}
58
+
59
+ .error {
60
+ color: red;
61
+ }
62
+
63
+ .project h2 { margin: 12px 0 0 0;}
64
+
65
+ .tests { margin: 0 0 0 18px;}
66
+ .tests .test { font-size: 80%; margin-right: 8px}
67
+ .tests a.test:hover { text-decoration: underline;}
68
+ .tests .test:first-child { font-size: 100%;}
69
+ .tests .test.active { font-weight: bold;}
70
+
71
+ .result .timestamp { margin-right: 12px;}
72
+ .result .version { margin: 6px 0 6px 12px }
73
+ .result .output { padding: 5px; color: silver; background: black; margin: 12px 18px 8px 18px; overflow: auto }
74
+ .result .output .update { margin: 6px 0 6px 12px }
75
+ .result .output .test { margin: 6px 0 6px 12px}
76
+
77
+ .footer {
78
+ margin: 24px 0 0 0;
79
+ font-size: 60%;
80
+ width: 100%;
81
+ text-align: center;
82
+ }
83
+ </style>
84
+ </head>
85
+ <body>
86
+ <h1>Projects</h1>
87
+ <div id="projects"></div>
88
+
89
+ <div class="footer">
90
+ <a href="http://github.com/edward/crazy_ivan">Crazy Ivan on Github</a>
91
+ </div>
92
+
93
+ <!-- templates -->
94
+
95
+ <!-- project template -->
96
+ <script type="text/html" id="projectTemplate">
97
+ <div id="<%= projectId %>" class="project">
98
+ <h2><%= projectName %></h2>
21
99
 
22
- new Ajax.Request('projects.json', {
23
- asynchronous: false,
24
- onSuccess: function(transport) {
25
- project_names = transport.responseText.evalJSON().projects;
26
- }
27
- });
100
+ <div class="tests"></div>
101
+ <div class="results"></div>
102
+ <div>
103
+ </script>
104
+
105
+ <!-- test link template -->
106
+ <script type="text/html" id="resultLinkTemplate">
107
+ <a id="<%= projectDomId %>-<%= version.output %>" class="test"><%= shortTimeStamp %></a>
108
+ </script>
109
+
110
+ <!-- build result holder -->
111
+ <script type="text/html" id="resultTemplate">
112
+ <div class="result <%= projectDomId %>-<%= version.output %>" style="display: none">
113
+ <div>
114
+ <span class="timestamp"><%= timestamp.finish %></span>
115
+ <span class="version"><%= version.output %></span>
116
+ </div>
28
117
 
29
- project_names.each(function(project_name) {
30
- var reports = [];
118
+ <div class="output">
119
+ <div class="version" style="display: none"><pre class="error"><%= version.error %></pre></div>
31
120
 
32
- reports = recent_versions(project_name).map(function(version) {
33
- return version_test_report(project_name, version);
34
- })
121
+ <div class="update" style="display: none">
122
+ <pre><%= update.output %></pre>
123
+ <pre class="error"><%= update.error %></pre>
124
+ </div>
35
125
 
36
- project = {name: project_name, reports: reports.reverse(), build_in_progress: build_in_progress(project_name)};
37
- projects.push(project);
38
- })
39
-
40
- return {"projects": projects};
41
- }
126
+ <div class="test">
127
+ <pre><%= test.output.replace(/\</g, "&lt;").replace(/\>/g, "&gt;") %></pre>
128
+ <pre class="error"><%= test.error %></pre>
129
+ </div>
130
+ </div>
131
+ </div>
132
+ </script>
133
+
134
+ <script type="text/javascript" charset="utf-8">
135
+ var json = {projects: []};
42
136
 
43
- var recent_versions = function(project) {
44
- var recent_versions = [];
45
- new Ajax.Request(project + '/recent.json', {
46
- asynchronous: false,
47
- onSuccess: function(transport) {
48
- recent_versions = transport.responseText.evalJSON().recent_versions;
49
- }
137
+ jQuery(document).ready(function($) {
138
+ $.getJSON("projects.json", function(data) {
139
+ jQuery.each(data.projects, function(i, projectName) {
140
+ addProjectToJson(projectName);
141
+ });
50
142
  });
51
- return recent_versions;
52
- }
143
+ });
53
144
 
54
- var version_test_report = function(project, version) {
55
- var report = {};
56
- url = project + '/' + version + '.json';
145
+ function addProjectToJson(name) {
146
+ var project = {'name': name, reports: []};
147
+ var recentVersionsJsonPath = name + "/recent.json";
148
+
149
+ jQuery.getJSON(recentVersionsJsonPath, function(data) {
150
+ jQuery.each(data.recent_versions, function(i, version) {
151
+ addReportToProject(project, version);
152
+ });
153
+ });
57
154
 
58
- new Ajax.Request(url, {
59
- asynchronous: false,
60
- onSuccess: function(transport) {
61
- report = transport.responseText.evalJSON();
62
- }
155
+ json.projects.push(project);
156
+ }
157
+
158
+ function addReportToProject(project, version) {
159
+ var name = project.name;
160
+ var resultJsonPath = name + "/" + version + ".json";
161
+ jQuery.getJSON(resultJsonPath, function(data) {
162
+ project.reports.push(data);
163
+ trigger_render();
63
164
  });
64
- return report;
65
165
  }
66
166
 
67
- var build_in_progress = function(project) {
68
- var build_in_progress = {};
69
-
70
- new Ajax.Request(project + '/currently_building.json', {
71
- asynchronous: false,
72
- onSuccess: function(transport) {
73
- build_in_progress = transport.responseText.evalJSON();
74
- }
167
+ function sortReports(reports) {
168
+ return reports.sort(function(report_a, report_b) {
169
+ // Not sure why providing a 3-letter day trips up Date.js sometimes
170
+ a = Date.parse(report_a.timestamp.finish.substring(4));
171
+ b = Date.parse(report_b.timestamp.finish.substring(4));
172
+
173
+ return Date.compare(a, b);
75
174
  });
76
- return build_in_progress;
77
175
  }
78
176
 
79
- var expand = function(element) {
80
- element.siblings().invoke('show');
81
- element.remove();
177
+ var timeout = null;
178
+ function trigger_render() {
179
+ if (timeout) { clearTimeout(timeout) }
180
+ timeout = setTimeout(render, 50);
82
181
  }
83
182
 
84
- var render = function(template_name, json) {
85
- var template = jsontemplate.Template(" \
86
- <h1>Projects</h1> \
87
- <div class='projects'> \
88
- {.section projects} \
89
- {.repeated section @} \
90
- <div class='project'> \
91
- <h2>{name}</h2> \
92
- {.section build_in_progress} \
93
- {.section timestamp} \
94
- <div>[build in progress &ndash; started at {start}]</div> \
95
- {.end} \
96
- {.end} \
97
- <div class='tests'> \
98
- {.section reports} \
99
- {.repeated section @} \
100
- <a class='test {.section test} {.section exit_status} error {.end} {.end} {.section update} {.section exit_status} error {.end} {.end}' href='#'>{timestamp.finish}</a> \
101
- {.end} \
102
- {.end} \
103
- </div> \
104
- <div class='results'> \
105
- {.section reports} \
106
- {.repeated section @} \
107
- <div class='result'> \
108
- <div> \
109
- <span class='timestamp'>{timestamp.finish}</span> \
110
- <span class='version'>{version.output}</span> \
111
- </div> \
112
- \
113
- <div class='output'> \
114
- {.section version} \
115
- {.section exit_status} \
116
- <div class='version'> \
117
- <pre class='error'>{error}</pre> \
118
- </div> \
119
- {.end} \
120
- {.end} \
121
- \
122
- <div class='update'> \
123
- <pre>{update.output}</pre> \
124
- {.section update} \
125
- {.section exit_status} \
126
- <pre class='error'>{update.error}</pre> \
127
- {.end} \
128
- {.end} \
129
- </div> \
130
- \
131
- <div class='test'> \
132
- <pre>{test.output}</pre> \
133
- {.section test} \
134
- {.section exit_status} \
135
- <pre class='error'>{test.error}</pre> \
136
- {.end} \
137
- {.end} \
138
- </div> \
139
- </div> \
140
- </div> \
141
- {.end} \
142
- {.or} \
143
- <p>No test reports found. Please run crazy_ivan.</p> \
144
- {.end} \
145
- </div> \
146
- </div> \
147
- {.end} \
148
- {.or} \
149
- <p>No projects found.</p> \
150
- {.end} \
151
- </div> \
152
- ");
183
+ var render = function() {
184
+ $('#projects').empty();
153
185
 
154
- var html = template.expand(json);
155
- $("replace").update(html);
156
-
157
-
158
- $$('.project .tests').each(function(t) {
159
- t.down('.test').addClassName('latest')
160
- });
161
-
162
- // Reformat the test timestamps to be less enormous
163
- $$('.project .tests .test').each(function(t) {
164
- t.update(Date.parse(t.innerHTML).toString("HH:mm"));
165
- t.observe('click', function(e) {
166
- console.debug("hallo");
167
- $$('.results').invoke('show')
168
- return false;
186
+ jQuery.each(json.projects, function(i, project) {
187
+ var name = project.name;
188
+ var domId = name.replace(/\./g, ""); // remove . from id name
189
+
190
+ // create project holder div
191
+ $('#projects').append(tmpl("projectTemplate", {projectName: name, projectId: domId}));
192
+
193
+ project.reports = sortReports(project.reports);
194
+
195
+ jQuery.each(project.reports, function(i, report) {
196
+ var version = report.version.output;
197
+ var resultJsonPath = name + "/" + version + ".json";
198
+ var domId = name.replace(/\./g, ""); // remove . from id name
199
+
200
+ // Not sure why providing a 3-letter day trips up Date.js sometimes
201
+ report["shortTimeStamp"] = Date.parse(report.timestamp.finish.substring(4)).toString("HH:mm");
202
+ report["projectDomId"] = domId;
203
+
204
+ $("#" + domId + " .results").append(tmpl("resultTemplate", report));
205
+ $("#" + domId + " .tests").prepend(tmpl("resultLinkTemplate", report));
206
+
207
+ // add failed/success indication to link - inlining in the template screws up
208
+ if (report.test.exit_status) {
209
+ $("#" + domId + '-' + version).addClass('error');
210
+ }
169
211
  });
170
212
  });
171
-
172
- $$('.results').invoke('hide')
173
213
  }
174
- </script>
175
- </head>
176
- <body onload="init();">
177
- <div id="replace"></div>
178
-
179
- <div class="footer">
180
- <a href="http://github.com/edward/crazy_ivan">Crazy Ivan on Github</a>
181
- </div>
214
+
215
+ // listen to clicking of test result links
216
+ $('#projects .project .tests a.test').live('click', function(e) {
217
+ $('.result:visible').hide();
218
+
219
+ if($(e.target).hasClass('active')) {
220
+ $('.test').removeClass('active');
221
+ } else {
222
+ $('.test').removeClass('active');
223
+ $('.result.' + e.target.id).show();
224
+ $(e.target).addClass('active');
225
+ }
226
+ });
227
+ </script>
182
228
  </body>
183
229
  </html>