duvet 0.3.2 → 0.3.3
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/lib/duvet.rb +77 -0
- data/lib/duvet/core_ext.rb +17 -0
- data/lib/duvet/cov.rb +100 -0
- data/lib/duvet/covs.rb +49 -0
- data/lib/duvet/version.rb +3 -0
- data/templates/css/styles.sass +178 -0
- data/templates/html/file.erb +71 -0
- data/templates/html/index.erb +69 -0
- data/templates/js/jquery.js +154 -0
- data/templates/js/main.js +56 -0
- data/templates/js/plugins.js +98 -0
- data/test/helper.rb +19 -0
- data/test/test_core_ext.rb +13 -0
- data/test/test_cov.rb +76 -0
- data/test/test_covs.rb +67 -0
- data/test/test_duvet.rb +17 -0
- metadata +29 -8
data/lib/duvet.rb
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
# Add this to test/helper.rb at the before __anything__ else
|
2
|
+
#
|
3
|
+
# require 'duvet'
|
4
|
+
# Duvet.start
|
5
|
+
#
|
6
|
+
|
7
|
+
require 'coverage'
|
8
|
+
require 'pathname'
|
9
|
+
require 'erubis'
|
10
|
+
require 'sass'
|
11
|
+
|
12
|
+
require 'duvet/core_ext'
|
13
|
+
require 'duvet/covs'
|
14
|
+
require 'duvet/cov'
|
15
|
+
require 'duvet/version'
|
16
|
+
|
17
|
+
module Duvet
|
18
|
+
|
19
|
+
attr_accessor :opts
|
20
|
+
|
21
|
+
DEFAULTS = {:dir => 'cov', :style => 'rcov'}
|
22
|
+
|
23
|
+
TEMPLATE_PATH = Pathname.new(__FILE__).dirname + '..' + 'templates'
|
24
|
+
|
25
|
+
TEMPLATE_HASH = {
|
26
|
+
'time' => Time.now,
|
27
|
+
'version' => VERSION,
|
28
|
+
'name' => 'duvet'
|
29
|
+
}
|
30
|
+
|
31
|
+
# Start tracking
|
32
|
+
def self.start(opts={})
|
33
|
+
@opts = DEFAULTS.merge(opts)
|
34
|
+
|
35
|
+
Coverage.start
|
36
|
+
@running = true
|
37
|
+
end
|
38
|
+
|
39
|
+
# Get result
|
40
|
+
def self.result
|
41
|
+
cov = Coverage.result if running?
|
42
|
+
if @opts[:filter]
|
43
|
+
filtered = {}
|
44
|
+
@opts[:filter] = /#{@opts[:filter]}/ unless @opts[:filter].is_a?(Regexp)
|
45
|
+
cov.each do |k, v|
|
46
|
+
if @opts[:filter] =~ k
|
47
|
+
filtered[k] = v
|
48
|
+
end
|
49
|
+
end
|
50
|
+
cov = filtered
|
51
|
+
end
|
52
|
+
@result ||= Duvet::Covs.new(cov)
|
53
|
+
ensure
|
54
|
+
@running = false
|
55
|
+
end
|
56
|
+
|
57
|
+
# Write results
|
58
|
+
def self.write
|
59
|
+
self.result.write(Pathname.new(@opts[:dir])) if running?
|
60
|
+
end
|
61
|
+
|
62
|
+
# Proc to call when exiting
|
63
|
+
# @todo Allow user to override block used
|
64
|
+
def self.at_exit
|
65
|
+
Proc.new { self.write }
|
66
|
+
end
|
67
|
+
|
68
|
+
# @return [Boolean] whether coverage is running
|
69
|
+
def self.running?
|
70
|
+
@running
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
at_exit do
|
76
|
+
Duvet.at_exit.call
|
77
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class Pathname
|
2
|
+
|
3
|
+
# Gets just the extension of the pathname
|
4
|
+
#
|
5
|
+
# @return [String] the extension of the path, without the '.'
|
6
|
+
def extension
|
7
|
+
self.extname[1..-1]
|
8
|
+
end
|
9
|
+
|
10
|
+
# Gets the name of the file without the extension
|
11
|
+
#
|
12
|
+
# @return [String] name of the file
|
13
|
+
def file_name
|
14
|
+
self.basename.to_s[0...-(self.extension.size+1)]
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
data/lib/duvet/cov.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
module Duvet
|
2
|
+
class Cov
|
3
|
+
attr_accessor :path
|
4
|
+
|
5
|
+
def initialize(path, cov)
|
6
|
+
@path = Pathname.new(path)
|
7
|
+
if @path.to_s.include?(Dir.pwd)
|
8
|
+
@path = @path.relative_path_from(Pathname.getwd)
|
9
|
+
end
|
10
|
+
|
11
|
+
@cov = cov
|
12
|
+
end
|
13
|
+
|
14
|
+
# @return [Array] lines in file
|
15
|
+
def lines
|
16
|
+
@cov
|
17
|
+
end
|
18
|
+
|
19
|
+
# @return [Array] all lines which can be executed
|
20
|
+
def code_lines
|
21
|
+
@cov.reject {|i| i.nil?}
|
22
|
+
end
|
23
|
+
|
24
|
+
# @return [Array] all lines which have been ran
|
25
|
+
def ran_lines
|
26
|
+
@cov.reject {|i| i.nil? || i.zero?}
|
27
|
+
end
|
28
|
+
|
29
|
+
# Gives a fraction from 0 to 1 of how many lines of code have
|
30
|
+
# been executed. It ignores all lines that couldn't be executed
|
31
|
+
# such as comments.
|
32
|
+
#
|
33
|
+
# @return [Float] lines of code executed as a fraction
|
34
|
+
def code_coverage
|
35
|
+
return 0.0 if code_lines.size.zero?
|
36
|
+
ran_lines.size.to_f / code_lines.size.to_f
|
37
|
+
end
|
38
|
+
|
39
|
+
# Similar to #code_coverage but counts all lines, executable
|
40
|
+
# or not.
|
41
|
+
#
|
42
|
+
# @return [Integer] lines executed as a fraction
|
43
|
+
def total_coverage
|
44
|
+
return 0.0 if lines.size.zero?
|
45
|
+
ran_lines.size.to_f / lines.size.to_f
|
46
|
+
end
|
47
|
+
|
48
|
+
# @return [String] #code_coverage as ??.??%
|
49
|
+
def code_coverage_percent
|
50
|
+
"%.2f%" % (code_coverage*100)
|
51
|
+
end
|
52
|
+
|
53
|
+
# @return [String] #total_coverage as ??.??%
|
54
|
+
def total_coverage_percent
|
55
|
+
"%.2f%" % (total_coverage*100)
|
56
|
+
end
|
57
|
+
|
58
|
+
# @return [String]
|
59
|
+
def report
|
60
|
+
str = "#{@path}\n"
|
61
|
+
str << " total: #{total_coverage_percent}\n"
|
62
|
+
str << " code: #{code_coverage_percent}\n\n"
|
63
|
+
str
|
64
|
+
end
|
65
|
+
|
66
|
+
# @return [Hash] a hash of data for templating
|
67
|
+
def data
|
68
|
+
{
|
69
|
+
"file" => {
|
70
|
+
"path" => @path.to_s,
|
71
|
+
"url" => @path.file_name + '.html',
|
72
|
+
"source" => @path.readlines,
|
73
|
+
"lines" => lines.size,
|
74
|
+
"lines_code" => code_lines.size,
|
75
|
+
"lines_ran" => ran_lines.size
|
76
|
+
},
|
77
|
+
"coverage" => {
|
78
|
+
"code" => code_coverage_percent,
|
79
|
+
"total" => total_coverage_percent,
|
80
|
+
"lines" => lines
|
81
|
+
}
|
82
|
+
}
|
83
|
+
end
|
84
|
+
|
85
|
+
# Formats the coverage for the file to be written to a html
|
86
|
+
# file, then viewed in a web browser.
|
87
|
+
#
|
88
|
+
# @return [String]
|
89
|
+
def format
|
90
|
+
template = (TEMPLATE_PATH + 'html' + 'file.erb').read
|
91
|
+
Erubis::Eruby.new(template).result(TEMPLATE_HASH.merge(self.data))
|
92
|
+
end
|
93
|
+
|
94
|
+
def write(dir)
|
95
|
+
path = (dir + @path.file_name).to_s + '.html'
|
96
|
+
File.open(path, 'w') {|f| f.write(self.format) }
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
end
|
data/lib/duvet/covs.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
module Duvet
|
2
|
+
class Covs < Array
|
3
|
+
|
4
|
+
def initialize(cov)
|
5
|
+
self.replace []
|
6
|
+
cov.each do |path, c|
|
7
|
+
self << Cov.new(path, c)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def report
|
12
|
+
map {|i| i.report }.join('')
|
13
|
+
end
|
14
|
+
|
15
|
+
def data
|
16
|
+
{ 'files' => map {|i| i.data } }
|
17
|
+
end
|
18
|
+
|
19
|
+
def format
|
20
|
+
template = (TEMPLATE_PATH + 'html' + 'index.erb').read
|
21
|
+
Erubis::Eruby.new(template).result(TEMPLATE_HASH.merge(self.data))
|
22
|
+
end
|
23
|
+
|
24
|
+
def write(dir)
|
25
|
+
if size > 0
|
26
|
+
FileUtils.mkdir_p(dir)
|
27
|
+
File.open(dir + 'index.html', 'w') {|f| f.write(format) }
|
28
|
+
|
29
|
+
each {|c| c.write(dir) }
|
30
|
+
write_resources(dir)
|
31
|
+
else
|
32
|
+
warn "No files to create coverage for."
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def write_resources(dir)
|
37
|
+
Pathname.glob(TEMPLATE_PATH + 'css' + '*').each do |i|
|
38
|
+
f = File.new(dir + 'styles.css', 'w')
|
39
|
+
f.write Sass::Engine.new(i.read).render
|
40
|
+
end
|
41
|
+
|
42
|
+
Pathname.glob(TEMPLATE_PATH + 'js' + '*').each do |i|
|
43
|
+
f = File.new(dir + i.basename, 'w')
|
44
|
+
f.write(i.read)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
$text: rgb(0, 0, 0)
|
2
|
+
|
3
|
+
$dark-grey: #333
|
4
|
+
$light-grey: #ccc
|
5
|
+
$white: #fff
|
6
|
+
|
7
|
+
$excluded: #e0dedb
|
8
|
+
$unran: #ce8b8c
|
9
|
+
$ran: #bed2be
|
10
|
+
|
11
|
+
// Main
|
12
|
+
html
|
13
|
+
margin: 0
|
14
|
+
padding: 0
|
15
|
+
width: 100%
|
16
|
+
|
17
|
+
body
|
18
|
+
background: #f4f2ed
|
19
|
+
font: 12px/1.3em Helvetica
|
20
|
+
margin: 8px
|
21
|
+
|
22
|
+
a
|
23
|
+
text-decoration: none
|
24
|
+
|
25
|
+
|
26
|
+
// Header
|
27
|
+
header
|
28
|
+
margin: 2em .5em
|
29
|
+
text-shadow: 0 1px 0 rgba(255, 255, 255, .5)
|
30
|
+
|
31
|
+
h1, h1 a
|
32
|
+
color: $dark-grey
|
33
|
+
|
34
|
+
h2
|
35
|
+
color: lighten($dark-grey, 40%)
|
36
|
+
font-size: 16px
|
37
|
+
|
38
|
+
// Filter/Search Box
|
39
|
+
#filter
|
40
|
+
position: absolute
|
41
|
+
top: 20px
|
42
|
+
right: 8px
|
43
|
+
border: 1px solid rgb(120, 120, 120)
|
44
|
+
background: rgb(240, 240, 240)
|
45
|
+
-webkit-border-radius: 3px
|
46
|
+
-webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, .1), 0 1px 0 white
|
47
|
+
-webkit-transition: all .2s
|
48
|
+
padding: 4px
|
49
|
+
width: 180px
|
50
|
+
|
51
|
+
&:focus
|
52
|
+
outline: none
|
53
|
+
background: rgb(244, 244, 244)
|
54
|
+
border: 1px solid rgb(100, 100, 100)
|
55
|
+
|
56
|
+
|
57
|
+
// Table
|
58
|
+
.table-wrapper
|
59
|
+
min-width: 700px
|
60
|
+
|
61
|
+
table
|
62
|
+
border: 1px solid grey
|
63
|
+
background: $excluded
|
64
|
+
border-collapse: collapse
|
65
|
+
width: 100%
|
66
|
+
margin-bottom: 1em
|
67
|
+
-webkit-box-shadow: 0 1px 0 white
|
68
|
+
|
69
|
+
table.source, pre, code
|
70
|
+
font: 11px/1.2em Menlo
|
71
|
+
|
72
|
+
td pre
|
73
|
+
margin: 0
|
74
|
+
|
75
|
+
|
76
|
+
// Summary
|
77
|
+
.summary
|
78
|
+
margin-bottom: 1em
|
79
|
+
background: $excluded
|
80
|
+
|
81
|
+
tbody
|
82
|
+
.name, .name a
|
83
|
+
color: $text
|
84
|
+
font-weight: bold
|
85
|
+
|
86
|
+
tbody tr:hover
|
87
|
+
background: darken($excluded, 10%)
|
88
|
+
|
89
|
+
thead
|
90
|
+
background: $dark-grey
|
91
|
+
color: $light-grey
|
92
|
+
|
93
|
+
th
|
94
|
+
cursor: pointer
|
95
|
+
|
96
|
+
tr
|
97
|
+
text-align: left
|
98
|
+
|
99
|
+
th, td
|
100
|
+
padding: .5em
|
101
|
+
|
102
|
+
.header::after
|
103
|
+
font-size: 10px
|
104
|
+
margin-left: 10px
|
105
|
+
|
106
|
+
.descending::after
|
107
|
+
content: '▼'
|
108
|
+
|
109
|
+
.ascending::after
|
110
|
+
content: '▲'
|
111
|
+
|
112
|
+
.lines, .loc, .ran, .cov, .code
|
113
|
+
width: 100px
|
114
|
+
|
115
|
+
// Totals
|
116
|
+
.totals
|
117
|
+
border-top: 1px solid grey
|
118
|
+
background: darken($excluded, 10%)
|
119
|
+
font-weight: bold
|
120
|
+
|
121
|
+
|
122
|
+
// Source
|
123
|
+
.source tr
|
124
|
+
pre
|
125
|
+
margin-left: 3px
|
126
|
+
|
127
|
+
.count
|
128
|
+
background: $white
|
129
|
+
-webkit-border-radius: 4px
|
130
|
+
-webkit-border-top-left-radius: 0px
|
131
|
+
-webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, .2)
|
132
|
+
border: 1px solid black
|
133
|
+
display: block
|
134
|
+
padding: 3px
|
135
|
+
position: absolute
|
136
|
+
margin-top: -1.3em
|
137
|
+
right: 15px
|
138
|
+
opacity: 0
|
139
|
+
|
140
|
+
.no
|
141
|
+
color: $light-grey
|
142
|
+
background: $dark-grey
|
143
|
+
padding: 0 0 0 3px
|
144
|
+
width: 25px
|
145
|
+
|
146
|
+
&:hover
|
147
|
+
.count
|
148
|
+
opacity: 1
|
149
|
+
|
150
|
+
.no
|
151
|
+
color: $white
|
152
|
+
|
153
|
+
// line colouring
|
154
|
+
&.excluded
|
155
|
+
background: $excluded
|
156
|
+
&:hover
|
157
|
+
background: darken($excluded, 10%)
|
158
|
+
|
159
|
+
&.unran
|
160
|
+
background: $unran
|
161
|
+
&:hover
|
162
|
+
background: darken($unran, 10%)
|
163
|
+
|
164
|
+
&.ran
|
165
|
+
background: $ran
|
166
|
+
&:hover
|
167
|
+
background: darken($ran, 10%)
|
168
|
+
|
169
|
+
|
170
|
+
// Footer
|
171
|
+
footer
|
172
|
+
color: rgba(0, 0, 0, .6)
|
173
|
+
text-shadow: 0 1px 0 rgba(255, 255, 255, .5)
|
174
|
+
|
175
|
+
a
|
176
|
+
color: rgb(0, 0, 0)
|
177
|
+
|
178
|
+
|
@@ -0,0 +1,71 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="utf-8" />
|
5
|
+
<title><%= file['path'] %></title>
|
6
|
+
<link rel="stylesheet" href="styles.css" type="text/css" />
|
7
|
+
<script type="text/javascript" src="jquery.js"></script>
|
8
|
+
<script type="text/javascript" src="main.js"></script>
|
9
|
+
<script type="text/javascript" src="plugins.js"></script>
|
10
|
+
</head>
|
11
|
+
|
12
|
+
<body>
|
13
|
+
<header>
|
14
|
+
<h1><a href="index.html">Coverage</a></h1>
|
15
|
+
<h2><%= file['path'] %></h2>
|
16
|
+
</header>
|
17
|
+
|
18
|
+
<div class="table-wrapper">
|
19
|
+
<table class="summary" border="none">
|
20
|
+
<thead>
|
21
|
+
<tr>
|
22
|
+
<th>Name</th>
|
23
|
+
<th>Total Lines</th>
|
24
|
+
<th>Lines of Code</th>
|
25
|
+
<th>Lines Ran</th>
|
26
|
+
<th>Total Coverage</th>
|
27
|
+
<th>Code Coverage</th>
|
28
|
+
</tr>
|
29
|
+
</thead>
|
30
|
+
<tbody>
|
31
|
+
<tr>
|
32
|
+
<td class="name"><%= file['path'] %></td>
|
33
|
+
<td><%= file['lines'] %></td>
|
34
|
+
<td><%= file['lines_code']%></td>
|
35
|
+
<td><%= file['lines_ran']%></td>
|
36
|
+
<td><%= coverage['total']%></td>
|
37
|
+
<td><%= coverage['code']%></td>
|
38
|
+
</tr>
|
39
|
+
</tbody>
|
40
|
+
</table>
|
41
|
+
</div>
|
42
|
+
|
43
|
+
<div class="table-wrapper">
|
44
|
+
<table class="source" border="none">
|
45
|
+
<% file['source'].zip(coverage['lines']).each_with_index do |(line, count), i| %>
|
46
|
+
<% if count.nil? %>
|
47
|
+
<tr class="excluded">
|
48
|
+
<td class="no"><%= i %></td>
|
49
|
+
<td><pre><code><%= line %></code></pre></td>
|
50
|
+
</tr>
|
51
|
+
|
52
|
+
<% else %>
|
53
|
+
<tr class="<%= count.zero? ? "unran" : "ran" %>">
|
54
|
+
<td class="no"><%= i %></td>
|
55
|
+
<td><pre><code><%= line %></code></pre><span class="count"><%= count %></span></td>
|
56
|
+
</tr>
|
57
|
+
<% end %>
|
58
|
+
<% end %>
|
59
|
+
</table>
|
60
|
+
</div>
|
61
|
+
|
62
|
+
|
63
|
+
<footer>
|
64
|
+
<span>
|
65
|
+
Generated at <%= time %> with
|
66
|
+
<a href="http://github.com/hawx/duvet"><%= name %> <%= version %></a>
|
67
|
+
</span>
|
68
|
+
</footer>
|
69
|
+
</body>
|
70
|
+
</html>
|
71
|
+
|