crazy_ivan 1.1.1 → 1.2.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/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>