ciquantum 0.0.10 → 0.0.11

Sign up to get free protection for your applications and to get access to all the features.
data/ciquantum.gemspec CHANGED
@@ -25,7 +25,7 @@ Gem::Specification.new do |gem|
25
25
  gem.add_runtime_dependency 'sinatra'
26
26
  gem.add_runtime_dependency 'choice'
27
27
  gem.add_runtime_dependency 'pony'
28
- gem.add_runtime_dependency 'sq_auth', '>= 0.0.24'
28
+ gem.add_runtime_dependency 'sq_auth', '>= 0.0.25'
29
29
  gem.add_development_dependency 'rack-test'
30
30
  gem.add_development_dependency 'mocha'
31
31
 
@@ -29,6 +29,10 @@ module CIQuantum
29
29
  finished_at - started_at
30
30
  end
31
31
 
32
+ def id
33
+ "#{sha}-#{finished_at.to_i}"
34
+ end
35
+
32
36
  def short_sha
33
37
  if sha
34
38
  sha[0,7]
@@ -42,7 +46,7 @@ module CIQuantum
42
46
  end
43
47
 
44
48
  def env_output
45
- out = clean_output
49
+ out = output # clean_output
46
50
  out.size > 100_000 ? out[-100_000,100_000] : out
47
51
  end
48
52
 
@@ -51,6 +55,18 @@ module CIQuantum
51
55
  @commit ||= Commit.new(sha, user, project, project_path)
52
56
  end
53
57
 
58
+ def test_results
59
+ stat = { success: 0, failures:0, pending:0 }
60
+ clean_output.gsub(/(\d+) examples, (\d+) failures(?:, (\d+) pending)?/) do
61
+ stat[:success] += $1.to_i
62
+ stat[:failures] += $2.to_i
63
+ stat[:pending] += $3.to_i
64
+ end
65
+ res = "#{stat[:success]} examples, #{stat[:failures]} failures"
66
+ res += ", #{stat[:pending]} pending" if stat[:pending] > 0
67
+ res
68
+ end
69
+
54
70
  def dump file
55
71
  config = [user, project, started_at, finished_at, sha, status, output, pid, branch]
56
72
  data = YAML.dump(config)
@@ -1,3 +1,5 @@
1
+ require 'fileutils'
2
+
1
3
  module CIQuantum
2
4
  class Core
3
5
  attr_reader :user
@@ -5,9 +7,9 @@ module CIQuantum
5
7
  attr_reader :url
6
8
  attr_reader :current_build
7
9
  attr_reader :last_build
8
- attr_reader :campfire
9
10
  attr_reader :projectname
10
11
  attr_reader :coverage_path
12
+ attr_accessor :shared_path
11
13
 
12
14
  HistoryLimit = 10
13
15
 
@@ -63,6 +65,7 @@ module CIQuantum
63
65
  write_current_build
64
66
  write_last_build
65
67
  run_hook "postbuild"
68
+ save_coverage_for_build @last_build
66
69
 
67
70
  build(@queue.next_branch_to_build) if @queue.waiting?
68
71
  end
@@ -100,8 +103,8 @@ module CIQuantum
100
103
  def build!(branch=nil)
101
104
  @git_branch = branch
102
105
  build = @current_build
103
- run_hook "prebuild"
104
106
  git_update
107
+ run_hook "prebuild"
105
108
  output = ''
106
109
  build.sha = git_sha
107
110
  build.branch = git_branch
@@ -129,7 +132,7 @@ module CIQuantum
129
132
  # shellin' out
130
133
  def runner_command
131
134
  runner = repo_config.runner.to_s
132
- runner == '' ? "rake -s test:units" : runner
135
+ runner == '' ? "rake -s --color test:units" : runner
133
136
  end
134
137
 
135
138
  def git_update
@@ -148,7 +151,8 @@ module CIQuantum
148
151
 
149
152
  # massage our repo
150
153
  def run_hook(hook)
151
- if File.exists?(file=path_in_project(".git/hooks/#{hook}")) && File.executable?(file)
154
+ file = path_in_project(".git/hooks/#{hook}")
155
+ if File.exists?(file) && File.executable?(file)
152
156
  data =
153
157
  if @last_build && @last_build.commit
154
158
  {
@@ -184,10 +188,26 @@ module CIQuantum
184
188
  @current_build = nil
185
189
  end
186
190
 
187
- def path_in_project(path)
191
+ def path_in_project path
188
192
  File.join(@project_path, path)
189
193
  end
190
194
 
195
+ def coverage_for_build build
196
+ File.join build.id, "coverage"
197
+ end
198
+
199
+ def coverage_path_for_build build
200
+ File.join @shared_path, coverage_for_build(build)
201
+ end
202
+
203
+ def save_coverage_for_build build
204
+ return unless @coverage_path
205
+ build_coverage_path = coverage_path_for_build(build)
206
+ puts "Coverage path: #{build_coverage_path}"
207
+ FileUtils.mkdir_p build_coverage_path
208
+ FileUtils.cp_r "#{path_in_project(@coverage_path)}/.", build_coverage_path
209
+ end
210
+
191
211
  def repo_config
192
212
  Config.ciquantum(@project_path)
193
213
  end
@@ -207,6 +227,10 @@ module CIQuantum
207
227
  @git_branch = (branch == '' ? "master" : branch)
208
228
  end
209
229
 
230
+ def is_current_git_branch? branch
231
+ return branch.eql? git_branch
232
+ end
233
+
210
234
  def git_sha
211
235
  `cd #{@project_path} && git rev-parse origin/#{git_branch}`.chomp
212
236
  end
@@ -224,7 +248,20 @@ module CIQuantum
224
248
  end
225
249
 
226
250
  def read_build_by_index index
227
- read_build old_builds[index] if old_builds[index]
251
+ old_builds[index] ? read_build(old_builds[index]) : nil
252
+ end
253
+
254
+ def active_builds
255
+ active_builds = []
256
+ old_builds.each do |build_file|
257
+ build = read_build(build_file)
258
+ active_builds << build
259
+ if build.status == :worked
260
+ remove_builds_from build
261
+ break
262
+ end
263
+ end
264
+ active_builds
228
265
  end
229
266
 
230
267
  def old_builds
@@ -254,12 +291,12 @@ module CIQuantum
254
291
  remove_unused_build
255
292
  end
256
293
 
257
- def remove_unused_build
258
- builds = old_builds
259
- if builds.size > HistoryLimit
260
- builds[HistoryLimit..builds.size].each do |file|
261
- file = path_in_project(".git/builds/#{file}")
262
- File.delete file if File.exists? file
294
+ def remove_builds_from limit_build
295
+ old_builds.each do |build_file|
296
+ build = read_build(build_file)
297
+ if build.finished_at < limit_build.finished_at
298
+ FileUtils.rm_r build_file
299
+ FileUtils.rm_r coverage_path_for_build build_file
263
300
  end
264
301
  end
265
302
  end
@@ -0,0 +1,6 @@
1
+ function toggleVisibility(id) {
2
+ var e = document.getElementById(id);
3
+ if(!e) return true;
4
+ e.style.display = (e.style.display == "none" ? "block" : "none")
5
+ return true;
6
+ }
@@ -35,18 +35,6 @@ h1 a {
35
35
  color: #000;
36
36
  }
37
37
 
38
- .failed, .color31 {
39
- color: red !important;
40
- }
41
-
42
- .worked, .color32 {
43
- color: green !important;
44
- }
45
-
46
- .errored, .color33 {
47
- color: yellow !important;
48
- }
49
-
50
38
  p {
51
39
  margin: 1em 0;
52
40
  }
@@ -79,16 +67,16 @@ ul.posts {
79
67
  margin-bottom: 2em;
80
68
  }
81
69
 
82
- ul.posts li {
83
- line-height: 1.75em;
84
- }
70
+ ul.posts li {
71
+ line-height: 1.75em;
72
+ }
85
73
 
86
- ul.posts .date,
87
- ul.posts .duration {
88
- color: #aaa;
89
- font-family: Monaco, "Courier New", monospace;
90
- font-size: 80%;
91
- }
74
+ ul.posts .date,
75
+ ul.posts .duration {
76
+ color: #aaa;
77
+ font-family: Monaco, "Courier New", monospace;
78
+ font-size: 80%;
79
+ }
92
80
 
93
81
  /*****************************************************************************/
94
82
  /*
@@ -110,97 +98,117 @@ ul.posts {
110
98
  margin-bottom: 2em;
111
99
  }
112
100
 
113
- .site .title a {
114
- color: #a00;
115
- text-decoration: none;
116
- }
101
+ .site .title a {
102
+ color: #a00;
103
+ text-decoration: none;
104
+ }
117
105
 
118
- .site .title a:hover {
119
- color: black;
120
- }
106
+ .site .title a:hover {
107
+ color: black;
108
+ }
121
109
 
122
- .site .title .extra {
123
- color: #aaa;
124
- text-decoration: none;
125
- margin-left: 1em;
126
- font-size: 0.9em;
127
- }
110
+ .site .title .extra {
111
+ color: #aaa;
112
+ text-decoration: none;
113
+ margin-left: 1em;
114
+ font-size: 0.9em;
115
+ }
128
116
 
129
- .site .title a.extra:hover {
130
- color: black;
131
- }
117
+ .site .title a.extra:hover {
118
+ color: black;
119
+ }
132
120
 
133
- .site .meta {
134
- color: #aaa;
135
- }
121
+ .site .meta {
122
+ color: #aaa;
123
+ }
136
124
 
137
- .site .footer {
138
- font-size: 80%;
139
- color: #666;
140
- border-top: 4px solid #eee;
141
- margin-top: 2em;
142
- overflow: hidden;
143
- }
125
+ .site .footer {
126
+ font-size: 80%;
127
+ color: #666;
128
+ border-top: 4px solid #eee;
129
+ margin-top: 2em;
130
+ overflow: hidden;
131
+ }
144
132
 
145
- .site .footer .contact {
146
- float: left;
147
- margin-right: 3em;
148
- }
133
+ .site .footer .contact {
134
+ float: left;
135
+ margin-right: 3em;
136
+ }
149
137
 
150
- .site .footer .contact a {
151
- color: #8085C1;
152
- }
138
+ .site .footer .contact a {
139
+ color: #8085C1;
140
+ }
153
141
 
154
- .site .footer .rss {
155
- margin-top: 1.1em;
156
- margin-right: -.2em;
157
- float: right;
158
- }
142
+ .site .footer .rss {
143
+ margin-top: 1.1em;
144
+ margin-right: -.2em;
145
+ float: right;
146
+ }
159
147
 
160
- .site .footer .rss img {
161
- border: 0;
162
- }
148
+ .site .footer .rss img {
149
+ border: 0;
150
+ }
163
151
 
164
152
  /*****************************************************************************/
165
153
  /*
166
- /* Posts
154
+ /* Builds
167
155
  /*
168
156
  /*****************************************************************************/
169
157
 
170
- #post {
158
+ span.failed, .color36 {
159
+ color: red !important;
160
+ }
171
161
 
162
+ span.worked, .color32 {
163
+ color: green !important;
172
164
  }
173
165
 
174
- /* standard */
166
+ span.errored, .color33 {
167
+ color: yellow !important;
168
+ }
175
169
 
176
- #post pre {
177
- border: 1px solid #ddd;
178
- background-color: #eef;
179
- padding: 0 .4em;
170
+ div.build, div.commit, div.result {
171
+ border: 1px solid black;
172
+ background-color: #E0E0E0;
180
173
  }
181
174
 
182
- #post ul,
183
- #post ol {
184
- margin-left: 1.25em;
175
+ div.commit {
176
+ font-family: 'Bitstream Vera Sans Mono', 'Courier', monospace;
185
177
  }
186
178
 
187
- #post code {
188
- border: 1px solid #ddd;
189
- background-color: #eef;
190
- font-size: 95%;
191
- padding: 0 .2em;
179
+ div.build div {
180
+ padding: 5px;
192
181
  }
193
182
 
194
- #post pre code {
195
- border: none;
196
- }
183
+ div.build {
184
+ padding: 0px;
185
+ margin: 30px 0px;
186
+ }
187
+
188
+ div.failed {
189
+ background-color: #CC3333;
190
+ }
191
+
192
+ div.worked {
193
+ background-color: #33CC33;
194
+ }
195
+
196
+ div.errored {
197
+ background-color: #CCCC33;
198
+ }
199
+
200
+ pre {
201
+ overflow: auto;
202
+ word-wrap: break-word;
203
+ }
197
204
 
198
205
  /* terminal */
199
206
 
200
207
  pre.terminal {
201
208
  border: 1px solid black;
202
- background-color: #333;
209
+ background-color: #E0E0E0;
203
210
  color: white;
211
+ margin: 0px;
204
212
  padding: 5px;
205
213
  overflow: auto;
206
214
  word-wrap: break-word;
@@ -1,5 +1,6 @@
1
1
  require 'erb'
2
2
  require 'json'
3
+ require 'fileutils'
3
4
 
4
5
  require 'sinatra'
5
6
  require 'sq_auth'
@@ -33,10 +34,15 @@ module CIQuantum
33
34
  quantum.last_build.sha
34
35
  end
35
36
 
36
- get ["ci::view", "ci::change_branch"], '/?' do
37
+ get ["ci::view"], '/?' do
37
38
  erb(:template, {}, :quantum => quantum, :can_change_branch => accessed_by?("ci::change_branch"))
38
39
  end
39
40
 
41
+ get ["ci::view"], "/*/coverage/?" do |build_id|
42
+ puts "Public folder: #{settings.public_folder}"
43
+ redirect "/#{build_id}/coverage/index.html"
44
+ end
45
+
40
46
  sq_auth_access do
41
47
  access_action "/build" do
42
48
  execute_for "ci::view"
@@ -47,7 +53,7 @@ module CIQuantum
47
53
  end
48
54
 
49
55
  post '/build' do
50
- quantum.build quantum.repo_config.branch
56
+ quantum.build quantum.git_branch
51
57
  redirect '/'
52
58
  end
53
59
 
@@ -80,8 +86,7 @@ module CIQuantum
80
86
  end
81
87
 
82
88
  get ["ci::view"], '/coverage?' do
83
- link_coverage_path
84
- redirect "coverage/index.html"
89
+ redirect "/#{quantum.last_build.sha}/coverage/index.html"
85
90
  end
86
91
 
87
92
  helpers do
@@ -110,10 +115,14 @@ module CIQuantum
110
115
  super
111
116
  check_project
112
117
  @quantum = CIQuantum.new(settings.project_path)
118
+ @quantum.shared_path = settings.public_folder
113
119
  end
114
120
 
115
121
  def self.start(host, port, project_path)
122
+ git_path = File.join(project_path, ".git")
123
+ FileUtils.cp_r settings.public_folder, git_path
116
124
  set :project_path, project_path
125
+ set :public_folder, "#{git_path}/public"
117
126
  CIQuantum::Server.run! :host => host, :port => port
118
127
  end
119
128
 
@@ -122,16 +131,6 @@ module CIQuantum
122
131
  self.new
123
132
  end
124
133
 
125
- def link_coverage_path
126
- public_coverage_path = File.join settings.public_folder, "coverage"
127
- project_coverage_path = File.join settings.project_path, quantum.coverage_path
128
-
129
- File.unlink public_coverage_path if File.exists? public_coverage_path
130
- if !quantum.coverage_path.empty? and File.exists? project_coverage_path
131
- File.symlink project_coverage_path, public_coverage_path
132
- end
133
- end
134
-
135
134
  def self.project_path=(project_path)
136
135
  set :project_path, Proc.new{project_path}, true
137
136
  end
@@ -1,3 +1,3 @@
1
1
  module CIQuantum
2
- Version = VERSION = "0.0.10"
2
+ Version = VERSION = "0.0.11"
3
3
  end
@@ -0,0 +1,22 @@
1
+
2
+ <div class="build" >
3
+ <div class="<%= build.status %>">
4
+ <b><%= build.branch %></b> : <%= build.status %> at <b><%= pretty_time(build.finished_at) %></b>. Build duration: <b><%= build.duration %></b> seconds.
5
+ </div>
6
+ <% if build.sha %>
7
+ <div class="commit">
8
+ Commit : <a href="<%= build.commit.url %>"><%= build.commit.sha %></a><br/>
9
+ Author : <%= build.commit.author %><br/>
10
+ Message: <pre><%= build.commit.message %></pre>
11
+ </div>
12
+ <% end %>
13
+ <div class="results">
14
+ <h4 class="result">>Test results: <%= build.test_results %></h4>
15
+ <a href="<%= quantum.coverage_for_build(build) %>">Show coverage</a> |
16
+ <a href="#" onclick="toggleVisibility('<%= build.id %>')">Show output</a>
17
+ <div id="<%= build.id %>" style="display:none">
18
+ <pre class="terminal"><code><%= ansi_color_codes h(build.output) %></code></pre>
19
+ </div>
20
+
21
+ </div>
22
+ </div>
@@ -1,16 +1,19 @@
1
- { "jobs": [
2
- <% if quantum.last_build %>
3
- {"name":"<%= quantum.projectname || quantum.project %>",
4
- "url":"<%= quantum.url %>",
5
- "color":"<%= quantum.last_build.status.to_s == "failed" ? 'red' : 'blue' %>",
6
- "status":"<%= quantum.last_build.status %>",
7
- "started_at":"<%= pretty_time(quantum.last_build.started_at) %>",
8
- "finished_at":"<%= pretty_time(quantum.last_build.finished_at) %>",
9
- "duration":"<%= quantum.last_build.duration if quantum.last_build.duration %>",
10
- "sha":"<%= quantum.last_build.sha %>",
11
- "short_sha":"<%= quantum.last_build.short_sha %>",
12
- "commit_url":"<%= quantum.last_build.commit.url if quantum.last_build.commit %>",
13
- "branch":"<%= quantum.last_build.branch %>"
14
- }
15
- <% end %>
16
- ]}
1
+ {
2
+ "jobs": [
3
+ <% if quantum.last_build %>
4
+ {
5
+ "name":"<%= quantum.projectname || quantum.project %>",
6
+ "url":"<%= quantum.url %>",
7
+ "color":"<%= quantum.last_build.status.to_s == "failed" ? 'red' : 'blue' %>",
8
+ "status":"<%= quantum.last_build.status %>",
9
+ "started_at":"<%= pretty_time(quantum.last_build.started_at) %>",
10
+ "finished_at":"<%= pretty_time(quantum.last_build.finished_at) %>",
11
+ "duration":"<%= quantum.last_build.duration if quantum.last_build.duration %>",
12
+ "sha":"<%= quantum.last_build.sha %>",
13
+ "short_sha":"<%= quantum.last_build.short_sha %>",
14
+ "commit_url":"<%= quantum.last_build.commit.url if quantum.last_build.commit %>",
15
+ "branch":"<%= quantum.last_build.branch %>"
16
+ }
17
+ <% end %>
18
+ ]
19
+ }
@@ -2,6 +2,7 @@
2
2
  <html>
3
3
  <head>
4
4
  <link href="<%= ciquantum_root %>/screen.css" media="screen" rel="stylesheet" type="text/css" />
5
+ <script type="text/JavaScript" src="<%= ciquantum_root %>/javascript.js"></script>
5
6
  <title><%= h(quantum.projectname) %>: CI Quantum</title>
6
7
  </head>
7
8
  <body>
@@ -35,7 +36,7 @@
35
36
  <form method="POST" action="/switch_branch">
36
37
  <select name="branch">
37
38
  <% for @branch in quantum.git_branches %>
38
- <option value="<%= @branch %>" <%= 'selected="selected"' if (@branch == quantum.git_branch) %>>"<%= @branch %>"</option>
39
+ <option value="<%= @branch %>" <%= 'selected="selected"' if quantum.is_current_git_branch?(@branch) %>>"<%= @branch %>"</option>
39
40
  <% end %>
40
41
  </select>
41
42
  <input type="submit" name="switch" value="Switch branch and build"/>
@@ -43,25 +44,10 @@
43
44
  <% end %>
44
45
  </li>
45
46
  <% end %>
46
-
47
- <% if quantum.last_build %>
48
- <li>
49
- <span class="date"><%= pretty_time(quantum.last_build.finished_at) %></span> &raquo;
50
- <% if quantum.last_build.sha %>
51
- Built <%= quantum.last_build.branch %> at <a href="<%= quantum.last_build.commit.url %>"><%= quantum.last_build.short_sha %></a>
52
- <% end %>
53
- <span class="<%= quantum.last_build.status %>">(<%= quantum.last_build.status %>)</span>
54
-
55
- <% if quantum.last_build.duration %>
56
- in <span class="duration"><%= quantum.last_build.duration %></span> seconds.
57
- <% end %>
58
47
 
59
- <% if !quantum.coverage_path.empty? || true %>
60
- <span class="coverage"><a href="coverage">Code coverage</a>
61
- <% end %>
62
- </li>
63
- <% if quantum.last_build.failed? %>
64
- <li><pre class="terminal"><code><%=ansi_color_codes h(quantum.last_build.output) %></code></pre></li>
48
+ <% if ! quantum.active_builds.empty?%>
49
+ <% for @build in quantum.active_builds %>
50
+ <ul><%= erb(:build, {}, :quantum => quantum, :build => @build) %></ul>
65
51
  <% end %>
66
52
  <% end %>
67
53
  </ul>
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ciquantum
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.10
4
+ version: 0.0.11
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2012-05-22 00:00:00.000000000 Z
14
+ date: 2012-05-31 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: bundler
@@ -132,7 +132,7 @@ dependencies:
132
132
  requirements:
133
133
  - - ! '>='
134
134
  - !ruby/object:Gem::Version
135
- version: 0.0.24
135
+ version: 0.0.25
136
136
  type: :runtime
137
137
  prerelease: false
138
138
  version_requirements: !ruby/object:Gem::Requirement
@@ -140,7 +140,7 @@ dependencies:
140
140
  requirements:
141
141
  - - ! '>='
142
142
  - !ruby/object:Gem::Version
143
- version: 0.0.24
143
+ version: 0.0.25
144
144
  - !ruby/object:Gem::Dependency
145
145
  name: rack-test
146
146
  requirement: !ruby/object:Gem::Requirement
@@ -197,6 +197,7 @@ files:
197
197
  - lib/ciquantum/commit.rb
198
198
  - lib/ciquantum/config.rb
199
199
  - lib/ciquantum/core.rb
200
+ - lib/ciquantum/public/javascript.js
200
201
  - lib/ciquantum/public/screen.css
201
202
  - lib/ciquantum/queue.rb
202
203
  - lib/ciquantum/server.rb
@@ -205,8 +206,8 @@ files:
205
206
  - lib/ciquantum/utils/coverage_merger.rb
206
207
  - lib/ciquantum/utils/mailer.rb
207
208
  - lib/ciquantum/version.rb
209
+ - lib/ciquantum/views/build.erb
208
210
  - lib/ciquantum/views/json.erb
209
- - lib/ciquantum/views/mail.erb
210
211
  - lib/ciquantum/views/mailer/html.erb
211
212
  - lib/ciquantum/views/mailer/text.erb
212
213
  - lib/ciquantum/views/template.erb
@@ -1,48 +0,0 @@
1
- <head>
2
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
3
- <title>[<%= @project %>] CIServer: state on branch <%= @build.branch %></title>
4
- <style type="text/css">
5
-
6
- .title {
7
- font-size: 120%;
8
- font-family: Verdana, Arial, Helvetica, sans-serif;
9
- border: 1px solid black;
10
- margin: 10px 20px;
11
- padding: 20px 10px;
12
- }
13
-
14
- .failed {
15
- background-color: #CC0000;
16
- }
17
-
18
- .worked {
19
- background-color: #00CC00;
20
- }
21
-
22
- pre {
23
- border: 1px solid black;
24
- background-color: #E0E0E0;
25
- }
26
-
27
- </style>
28
- </head>
29
- <body>
30
- <div class="title <%= @build.status.to_s %>"><%= @caption %></div><br>
31
- <div class="text">
32
- Last build <%= @project %> on branch <%= @build.branch %> <%= @build.status.to_s %>."<br><br>
33
-
34
- Details: <%= @build.commit.url %><br>
35
- Author: <%= @build.commit.author %><br>
36
- Message: <%= @build.commit.message %><br>
37
- </div>
38
- <% if @build.status == :failed %>
39
- <br>
40
- <code>
41
- <pre>
42
- Output:<br>
43
- <%= @build.clean_output %>
44
- </pre>
45
- </code>
46
- <% end %>
47
- <br><br><br>
48
- </body>