marmara 1.0.1 → 1.0.2
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.
- checksums.yaml +4 -4
- data/README.md +55 -1
- data/lib/marmara.rb +187 -231
- data/lib/marmara/config.rb +62 -0
- data/lib/marmara/exceptions.rb +38 -0
- data/lib/marmara/style-sheet.css +78 -0
- data/lib/marmara/style-sheet.html +1 -68
- metadata +7 -32
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0e9743576c518a82530bc19f2142fb6b1b797c3b
|
4
|
+
data.tar.gz: f89b1541bfa6c5bdd765f17636937666f1e79312
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 99a0c5b4f8a41f8f6f9361ac1fbd5bf96738ba6fc525b0d8762cf0c0957017c1bd65ac053d493c82574efa7cc73b1c6c3d895a2274f610f298dd08e87735d8a8
|
7
|
+
data.tar.gz: 14d861ac82ea43640662e78dd6aa1b1f3efb1e3495cb10fbc92d20743a1ea8723a77c8700d4701856abab27df8b995cfa52f9f022be5623f0cd7e6dd27b467a1
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Marmara
|
1
|
+
# Marmara [](https://travis-ci.org/lingua_franca/marmara) [](https://badge.fury.io/rb/marmara)
|
2
2
|
Marmara is a Ruby Gem that analyses your css during UI testing and generates a code coverage report.
|
3
3
|
|
4
4
|

|
@@ -136,4 +136,58 @@ Marmara.output_directory = '../build/logs'
|
|
136
136
|
Marmara.start_recording
|
137
137
|
```
|
138
138
|
|
139
|
+
You can also pass the output directory as an value to the options hash:
|
140
|
+
|
141
|
+
```Ruby
|
142
|
+
Marmara.options = {
|
143
|
+
output_directory: '../build/logs'
|
144
|
+
}
|
145
|
+
```
|
146
|
+
|
139
147
|
Set the `output_directory` before you start recording and you should find your HTML reports located in the directory you provided. *Note that this directory will removed and re-created each time the tests are run.*
|
148
|
+
|
149
|
+
### Ignoring Files
|
150
|
+
You can ignore files by passing a string, regular expression, or array or strings or regular expressions using the `:ignore` option:
|
151
|
+
|
152
|
+
```Ruby
|
153
|
+
# Ignore all files coming from http://fonts.googleapis.com/
|
154
|
+
Marmara.options = {
|
155
|
+
ignore: 'http://fonts.googleapis.com/'
|
156
|
+
}
|
157
|
+
|
158
|
+
# Ignore all files containing 'google'
|
159
|
+
Marmara.options = {
|
160
|
+
ignore: /google/
|
161
|
+
}
|
162
|
+
|
163
|
+
# Ignore all files containing google or adobe
|
164
|
+
Marmara.options = {
|
165
|
+
ignore: [/google/, /adobe/]
|
166
|
+
}
|
167
|
+
|
168
|
+
# Ignore a specific file
|
169
|
+
Marmara.options = {
|
170
|
+
ignore: /font\-awesome\.css$/
|
171
|
+
}
|
172
|
+
```
|
173
|
+
|
174
|
+
### Setting minimum coverage
|
175
|
+
By default Marmara will not cause your tests to fail even if you have 0% coverage. To enable this, set the `:minimum` option:
|
176
|
+
|
177
|
+
```Ruby
|
178
|
+
Marmara.options = {
|
179
|
+
minimum: {
|
180
|
+
rules: 80,
|
181
|
+
selectors: 90,
|
182
|
+
declarations: 90
|
183
|
+
}
|
184
|
+
}
|
185
|
+
```
|
186
|
+
|
187
|
+
The values represent persentages and each value is optional, if a value is not present the resepctive assertion will not be made.
|
188
|
+
|
189
|
+
If the respective overall coverage percentage doest not meet your minimum, your tests should fail and you should see a message that looks like:
|
190
|
+
|
191
|
+
```bash
|
192
|
+
Failed to meet minimum CSS rule coverage of 80%
|
193
|
+
```
|
data/lib/marmara.rb
CHANGED
@@ -1,24 +1,24 @@
|
|
1
1
|
require 'open-uri'
|
2
2
|
require 'cgi'
|
3
3
|
require 'marmara/parser'
|
4
|
+
require 'marmara/config'
|
5
|
+
require 'marmara/exceptions'
|
4
6
|
|
5
7
|
module Marmara
|
6
8
|
|
7
9
|
PSEUDO_CLASSES = /^((first|last|nth|nth\-last)\-(child|of\-type)|not|empty)/
|
8
10
|
|
9
11
|
class << self
|
10
|
-
|
11
|
-
def output_directory
|
12
|
-
@output_directory || 'log/css'
|
13
|
-
end
|
14
|
-
|
15
|
-
def output_directory=(dir)
|
16
|
-
@output_directory = dir
|
17
|
-
end
|
12
|
+
include Config
|
18
13
|
|
19
14
|
def start_recording
|
20
15
|
FileUtils.rm_rf(output_directory)
|
21
16
|
ENV['_marmara_record'] = '1'
|
17
|
+
|
18
|
+
@last_html = nil
|
19
|
+
@style_sheets = {}
|
20
|
+
@style_sheet_rules = {}
|
21
|
+
@last_driver = nil
|
22
22
|
end
|
23
23
|
|
24
24
|
def stop_recording
|
@@ -53,113 +53,115 @@ module Marmara
|
|
53
53
|
|
54
54
|
# now parse each style sheet
|
55
55
|
sheets.each do |sheet|
|
56
|
-
unless
|
57
|
-
@
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
unless parser.nil?
|
72
|
-
# go over each rule in the sheet
|
73
|
-
parser.each_rule_set do |rule, media_types|
|
74
|
-
selectors = []
|
75
|
-
rule.each_selector do |sel, dec, spec|
|
76
|
-
if sel.length > 0
|
77
|
-
# we need to look for @keyframes and @font-face coverage differently
|
78
|
-
if sel[0] == '@'
|
79
|
-
rule_type = sel[1..-1]
|
80
|
-
at_rule = {
|
81
|
-
rule: rule,
|
82
|
-
type: :at_rule,
|
83
|
-
at_rule_type: rule_type
|
84
|
-
}
|
85
|
-
case rule_type
|
86
|
-
when 'font-face'
|
87
|
-
at_rule[:property] = 'font-family'
|
88
|
-
at_rule[:value] = rule.get_value('font-family').gsub(/^\s*"(.*?)"\s*;?\s*$/, '\1')
|
89
|
-
when /^(\-\w+\-)?keyframes\s+(.*?)\s*$/
|
90
|
-
at_rule[:property] = ["#{$1}animation-name", "#{$1}animation"]
|
91
|
-
at_rule[:value] = $2
|
92
|
-
at_rule[:valueRegex] = [/(?:^|,)\s*(?:#{Regexp.escape(at_rule[:value])})\s*(?:,|;?$)/, /(?:^|\s)(?:#{Regexp.escape(at_rule[:value])})(?:\s|;?$)/]
|
93
|
-
when /^(\-moz\-document|supports)/
|
94
|
-
# ignore these types
|
95
|
-
at_rule[:used] = true
|
96
|
-
end
|
56
|
+
unless ignore?(sheet)
|
57
|
+
unless @style_sheets[sheet] && @style_sheet_rules[sheet]
|
58
|
+
@style_sheet_rules[sheet] = []
|
59
|
+
all_selectors = {}
|
60
|
+
all_at_rules = []
|
61
|
+
|
62
|
+
parser = nil
|
63
|
+
begin
|
64
|
+
parser = CssParser::MarmaraParser.new
|
65
|
+
parser.load_uri!(sheet, capture_offsets: true)
|
66
|
+
rescue Exception => e
|
67
|
+
puts e.to_s
|
68
|
+
puts "\t" + e.backtrace.join("\n\t")
|
69
|
+
log "Error reading #{sheet}"
|
70
|
+
end
|
97
71
|
|
98
|
-
|
99
|
-
|
72
|
+
unless parser.nil?
|
73
|
+
# go over each rule in the sheet
|
74
|
+
parser.each_rule_set do |rule, media_types|
|
75
|
+
selectors = []
|
76
|
+
rule.each_selector do |sel, dec, spec|
|
77
|
+
if sel.length > 0
|
78
|
+
# we need to look for @keyframes and @font-face coverage differently
|
79
|
+
if sel[0] == '@'
|
80
|
+
rule_type = sel[1..-1]
|
81
|
+
at_rule = {
|
82
|
+
rule: rule,
|
83
|
+
type: :at_rule,
|
84
|
+
at_rule_type: rule_type
|
85
|
+
}
|
86
|
+
case rule_type
|
87
|
+
when 'font-face'
|
88
|
+
at_rule[:property] = 'font-family'
|
89
|
+
at_rule[:value] = rule.get_value('font-family').gsub(/^\s*"(.*?)"\s*;?\s*$/, '\1')
|
90
|
+
when /^(\-\w+\-)?keyframes\s+(.*?)\s*$/
|
91
|
+
at_rule[:property] = ["#{$1}animation-name", "#{$1}animation"]
|
92
|
+
at_rule[:value] = $2
|
93
|
+
at_rule[:valueRegex] = [/(?:^|,)\s*(?:#{Regexp.escape(at_rule[:value])})\s*(?:,|;?$)/, /(?:^|\s)(?:#{Regexp.escape(at_rule[:value])})(?:\s|;?$)/]
|
94
|
+
when /^(\-moz\-document|supports)/
|
95
|
+
# ignore these types
|
96
|
+
at_rule[:used] = true
|
97
|
+
end
|
98
|
+
|
99
|
+
if at_rule[:value]
|
100
|
+
at_rule[:valueRegex] ||= /(?:^|,)\s*(?:#{Regexp.escape(at_rule[:value])}|\"#{Regexp.escape(at_rule[:value])}\")\s*(?:,|;?$)/
|
101
|
+
|
102
|
+
# store all the info that we collected about the rule
|
103
|
+
@style_sheet_rules[sheet] << at_rule
|
104
|
+
end
|
105
|
+
else
|
106
|
+
# just a regular selector, collect it
|
107
|
+
selectors << {
|
108
|
+
original: sel,
|
109
|
+
queryable: get_safe_selector(sel)
|
110
|
+
}
|
111
|
+
all_selectors[get_safe_selector(sel)] ||= false
|
100
112
|
|
101
113
|
# store all the info that we collected about the rule
|
102
|
-
@style_sheet_rules[sheet] <<
|
114
|
+
@style_sheet_rules[sheet] << {
|
115
|
+
rule: rule,
|
116
|
+
type: :rule,
|
117
|
+
selectors: selectors,
|
118
|
+
used_selectors: [false] * selectors.count
|
119
|
+
}
|
103
120
|
end
|
104
121
|
else
|
105
|
-
# just a regular selector, collect it
|
106
|
-
selectors << {
|
107
|
-
original: sel,
|
108
|
-
queryable: get_safe_selector(sel)
|
109
|
-
}
|
110
|
-
all_selectors[get_safe_selector(sel)] ||= false
|
111
|
-
|
112
122
|
# store all the info that we collected about the rule
|
113
123
|
@style_sheet_rules[sheet] << {
|
114
124
|
rule: rule,
|
115
|
-
type: :
|
116
|
-
selectors: selectors,
|
117
|
-
used_selectors: [false] * selectors.count
|
125
|
+
type: :unknown
|
118
126
|
}
|
119
127
|
end
|
120
|
-
else
|
121
|
-
# store all the info that we collected about the rule
|
122
|
-
@style_sheet_rules[sheet] << {
|
123
|
-
rule: rule,
|
124
|
-
type: :unknown
|
125
|
-
}
|
126
128
|
end
|
127
129
|
end
|
128
|
-
end
|
129
130
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
131
|
+
# store info about the stylesheet
|
132
|
+
@style_sheets[sheet] = {
|
133
|
+
css: parser.last_file_contents,
|
134
|
+
all_selectors: all_selectors,
|
135
|
+
all_at_rules: all_at_rules,
|
136
|
+
included_with: Set.new
|
137
|
+
}
|
138
|
+
end
|
139
|
+
@style_sheets[sheet][:included_with] += sheets
|
137
140
|
end
|
138
|
-
@style_sheets[sheet][:included_with] += sheets
|
139
|
-
end
|
140
141
|
|
141
|
-
|
142
|
-
|
142
|
+
# gather together only the selectors that haven't been spotted yet
|
143
|
+
selectors_to_find = @style_sheets[sheet][:all_selectors].select{|k,v|!v}.keys
|
143
144
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
145
|
+
# don't do anything unless we have to
|
146
|
+
if selectors_to_find.length > 0
|
147
|
+
# and search for them in this document
|
148
|
+
found_selectors = evaluate_script("(function(selectors) {
|
149
|
+
var results = {};
|
150
|
+
for (var i = 0; i < selectors.length; i++) {
|
151
|
+
results[selectors[i]] = !!document.querySelector(selectors[i]);
|
152
|
+
}
|
153
|
+
return results;
|
154
|
+
})(#{selectors_to_find.to_json})", driver)
|
154
155
|
|
155
|
-
|
156
|
-
|
156
|
+
# now merge the results back in
|
157
|
+
found_selectors.each { |k,v| @style_sheets[sheet][:all_selectors][k] ||= v }
|
157
158
|
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
159
|
+
# and mark each as used if found
|
160
|
+
@style_sheet_rules[sheet].each_with_index do |rule, rule_index|
|
161
|
+
if rule[:type] == :rule
|
162
|
+
rule[:selectors].each_with_index do |sel, sel_index|
|
163
|
+
@style_sheet_rules[sheet][rule_index][:used_selectors][sel_index] ||= @style_sheets[sheet][:all_selectors][sel[:queryable]]
|
164
|
+
end
|
163
165
|
end
|
164
166
|
end
|
165
167
|
end
|
@@ -176,7 +178,11 @@ module Marmara
|
|
176
178
|
end
|
177
179
|
|
178
180
|
def get_style_sheet_html
|
179
|
-
@style_sheet_html ||= File.read(File.join(File.dirname(__FILE__), 'marmara', 'style-sheet.html'))
|
181
|
+
@style_sheet_html ||= File.read((options || {})[:html_file] || File.join(File.dirname(__FILE__), 'marmara', 'style-sheet.html'))
|
182
|
+
end
|
183
|
+
|
184
|
+
def get_style_sheet_css
|
185
|
+
@style_sheet_css ||= File.read((options || {})[:css_file] || File.join(File.dirname(__FILE__), 'marmara', 'style-sheet.css'))
|
180
186
|
end
|
181
187
|
|
182
188
|
def evaluate_script(script, driver = @last_driver)
|
@@ -184,13 +190,17 @@ module Marmara
|
|
184
190
|
@last_driver.evaluate_script(script)
|
185
191
|
end
|
186
192
|
|
193
|
+
def stat_types
|
194
|
+
@stat_types ||= ['Rule', 'Selector', 'Declaration']
|
195
|
+
end
|
196
|
+
|
187
197
|
def analyze
|
188
198
|
# start compiling the overall stats
|
189
|
-
overall_stats = {
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
199
|
+
overall_stats = {}
|
200
|
+
|
201
|
+
stat_types.each do |type|
|
202
|
+
overall_stats["#{type}s"] = { match_count: 0, total: 0 }
|
203
|
+
end
|
194
204
|
|
195
205
|
# go through all of the style sheets found
|
196
206
|
#get_latest_results.each do |uri, rules|
|
@@ -204,29 +214,20 @@ module Marmara
|
|
204
214
|
# and generate the report
|
205
215
|
html = generate_html_report(original_sheet, coverage[:covered_rules])
|
206
216
|
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
match_count: coverage[
|
211
|
-
total: coverage[
|
212
|
-
},
|
213
|
-
'Selectors' => {
|
214
|
-
match_count: coverage[:matched_selectors],
|
215
|
-
total: coverage[:total_selectors]
|
216
|
-
},
|
217
|
-
'Declarations' => {
|
218
|
-
match_count: coverage[:matched_declarations],
|
219
|
-
total: coverage[:total_declarations]
|
217
|
+
stats_to_log = {}
|
218
|
+
stat_types.each do |type|
|
219
|
+
stats_to_log["#{type}s"] = {
|
220
|
+
match_count: coverage["matched_#{type.downcase}s".to_sym],
|
221
|
+
total: coverage["total_#{type.downcase}s".to_sym]
|
220
222
|
}
|
221
|
-
})
|
222
223
|
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
224
|
+
# add to the overall stats
|
225
|
+
overall_stats["#{type}s"][:match_count] += coverage["matched_#{type.downcase}s".to_sym]
|
226
|
+
overall_stats["#{type}s"][:total] += coverage["total_#{type.downcase}s".to_sym]
|
227
|
+
end
|
228
|
+
|
229
|
+
# output stats for this file
|
230
|
+
log_stats(get_report_filename(uri), stats_to_log)
|
230
231
|
|
231
232
|
# save the report
|
232
233
|
save_report(uri, html)
|
@@ -235,65 +236,28 @@ module Marmara
|
|
235
236
|
|
236
237
|
log_stats('Overall', overall_stats)
|
237
238
|
log "\n"
|
238
|
-
end
|
239
239
|
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
if uri.scheme == 'https'
|
248
|
-
uri.port = 443 unless uri.port
|
249
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
250
|
-
http.use_ssl = true
|
251
|
-
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
252
|
-
else
|
253
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
240
|
+
# check for minimum coverage
|
241
|
+
if options && options[:minimum]
|
242
|
+
stat_types.each do |type|
|
243
|
+
Marmara.const_get("Minimum#{type}CoverageNotMet").assert(
|
244
|
+
options[:minimum]["#{type.downcase}s".to_sym],
|
245
|
+
((overall_stats["#{type}s"][:match_count] * 100.0) / overall_stats["#{type}s"][:total]).round(2)
|
246
|
+
)
|
254
247
|
end
|
255
|
-
|
256
|
-
res = http.get(uri.request_uri, {'Accept-Encoding' => 'gzip'})
|
257
|
-
src = res.body.force_encoding("UTF-8")
|
258
|
-
|
259
|
-
case res['content-encoding']
|
260
|
-
when 'gzip'
|
261
|
-
io = Zlib::GzipReader.new(StringIO.new(res.body))
|
262
|
-
src = io.read
|
263
|
-
when 'deflate'
|
264
|
-
io = Zlib::Inflate.new
|
265
|
-
src = io.inflate(res.body)
|
266
|
-
end
|
267
|
-
|
268
|
-
if String.method_defined?(:encode)
|
269
|
-
src.encode!('UTF-8', 'utf-8')
|
270
|
-
else
|
271
|
-
ic = Iconv.new('UTF-8//IGNORE', 'utf-8')
|
272
|
-
src = ic.iconv(src)
|
273
|
-
end
|
274
|
-
|
275
|
-
return src
|
276
|
-
rescue Exception => e
|
277
|
-
sleep(1)
|
278
|
-
retry if open_attempts < 4
|
279
|
-
log "\tFailed to open #{uri}"
|
280
|
-
log e.to_s
|
281
248
|
end
|
282
|
-
return nil
|
283
249
|
end
|
284
250
|
|
285
251
|
def save_report(uri, html)
|
286
|
-
|
252
|
+
path = get_report_path(uri)
|
253
|
+
FileUtils.mkdir_p(File.dirname(path))
|
254
|
+
File.open(path, 'wb:UTF-8') { |f| f.write(html) }
|
287
255
|
end
|
288
256
|
|
289
257
|
def get_report_path(uri)
|
290
258
|
File.join(output_directory, get_report_filename(uri) + '.html')
|
291
259
|
end
|
292
260
|
|
293
|
-
def get_report_filename(uri)
|
294
|
-
File.basename(uri).gsub(/^(.*?)\?.*$/, '\1')
|
295
|
-
end
|
296
|
-
|
297
261
|
def is_property_covered(sheets, property, valueRegex)
|
298
262
|
# iterate over each sheet
|
299
263
|
sheets.each do |uri|
|
@@ -320,7 +284,7 @@ module Marmara
|
|
320
284
|
total_selectors = 0
|
321
285
|
covered_selectors = 0
|
322
286
|
|
323
|
-
total_rules =
|
287
|
+
total_rules = 0
|
324
288
|
covered_rules = 0
|
325
289
|
|
326
290
|
total_declarations = 0
|
@@ -351,6 +315,7 @@ module Marmara
|
|
351
315
|
coverage[:state] = :not_covered
|
352
316
|
end
|
353
317
|
elsif rule[:type] == :rule
|
318
|
+
total_rules += 1
|
354
319
|
some_covered = rule[:used_selectors].reduce(&:|)
|
355
320
|
total_selectors += rule[:used_selectors].count
|
356
321
|
|
@@ -409,57 +374,6 @@ module Marmara
|
|
409
374
|
}
|
410
375
|
end
|
411
376
|
|
412
|
-
def organize_rules(rules)
|
413
|
-
# first sort the rules by the starting index
|
414
|
-
rules.sort_by! { |r| r[:offset].first }
|
415
|
-
|
416
|
-
# then remove unnecessary regions
|
417
|
-
i = 0
|
418
|
-
rules_removed = false
|
419
|
-
while i < rules.length - 1
|
420
|
-
# look for empty regions
|
421
|
-
if rules[i][:offset][1] <= rules[i][:offset][0]
|
422
|
-
# so that we don't lose our place, set the value to nil, then we'll strip the array of nils
|
423
|
-
rules[i] = nil
|
424
|
-
rules_removed = true
|
425
|
-
# look for regions that should be connected
|
426
|
-
elsif (next_rule = rules[i + 1]) && rules[i][:offset][1] == next_rule[:offset][0] && rules[i][:state] == next_rule[:state]
|
427
|
-
# back up the next rule to start where ours does
|
428
|
-
rules[i + 1][:offset][0] = rules[i][:offset][0]
|
429
|
-
# and get rid of ourselves
|
430
|
-
rules[i] = nil
|
431
|
-
rules_removed = true
|
432
|
-
end
|
433
|
-
i += 1
|
434
|
-
end
|
435
|
-
|
436
|
-
# strip the array of nil values we may have set in the previous step
|
437
|
-
rules.compact! if rules_removed
|
438
|
-
|
439
|
-
# look for overlapping rules
|
440
|
-
i = 0
|
441
|
-
while i < rules.length
|
442
|
-
next_rule = rules[i + 1]
|
443
|
-
if next_rule && rules[i][:offset][1] > next_rule[:offset][0]
|
444
|
-
# we found an overlapping rule
|
445
|
-
# slice up this rule and add the remaining to the end of the array
|
446
|
-
rules << {
|
447
|
-
offset: [next_rule[:offset][1], rules[i][:offset][1]],
|
448
|
-
state: rules[i][:state]
|
449
|
-
}
|
450
|
-
# and shorten the length of this rule
|
451
|
-
rules[i][:offset][1] = next_rule[:offset][0]
|
452
|
-
|
453
|
-
# start again
|
454
|
-
return organize_rules(rules)
|
455
|
-
end
|
456
|
-
i += 1
|
457
|
-
end
|
458
|
-
|
459
|
-
# we're done!
|
460
|
-
return rules
|
461
|
-
end
|
462
|
-
|
463
377
|
def generate_html_report(original_sheet, coverage)
|
464
378
|
sheet_html = ''
|
465
379
|
last_index = 0
|
@@ -476,14 +390,15 @@ module Marmara
|
|
476
390
|
sheet_html += wrap_code(original_sheet[last_index...original_sheet.length], :ignored)
|
477
391
|
end
|
478
392
|
|
479
|
-
# replace line returns with HTML line breaks
|
480
|
-
sheet_html.gsub!(/\r?\n/, '<br>')
|
481
|
-
|
482
393
|
# build the lines section
|
483
|
-
lines = (
|
394
|
+
lines = (0..original_sheet.count("\n")).to_a.map do |_line|
|
395
|
+
line = _line + 1
|
484
396
|
"<a href=\"#L#{line}\" id=\"L#{line}\">#{line}</a>"
|
485
397
|
end
|
486
|
-
|
398
|
+
|
399
|
+
get_style_sheet_html.gsub('%{style}', get_style_sheet_css)
|
400
|
+
.gsub('%{lines}', lines.join(''))
|
401
|
+
.gsub('%{style_sheet}', sheet_html)
|
487
402
|
end
|
488
403
|
|
489
404
|
def wrap_code(str, state)
|
@@ -494,7 +409,8 @@ module Marmara
|
|
494
409
|
ignored: 'class="ignored"',
|
495
410
|
not_covered: 'class="not-covered"'
|
496
411
|
}
|
497
|
-
|
412
|
+
str = CGI.escapeHTML(str).gsub(/\r?\n/, '<br>')
|
413
|
+
"<pre #{@state_attr[state]}><span>#{str}</span></pre>"
|
498
414
|
end
|
499
415
|
|
500
416
|
def rules_equal?(rule_a, rule_b)
|
@@ -522,8 +438,48 @@ module Marmara
|
|
522
438
|
end
|
523
439
|
end
|
524
440
|
|
525
|
-
def
|
526
|
-
|
441
|
+
def organize_rules(rules)
|
442
|
+
# first sort the rules by the starting index
|
443
|
+
rules.sort_by! { |r| r[:offset].first }
|
444
|
+
|
445
|
+
# then remove unnecessary regions
|
446
|
+
i = 0
|
447
|
+
rules_removed = false
|
448
|
+
while i < rules.length - 1
|
449
|
+
# look for empty regions
|
450
|
+
if rules[i][:offset][1] <= rules[i][:offset][0]
|
451
|
+
# so that we don't lose our place, set the value to nil, then we'll strip the array of nils
|
452
|
+
rules[i] = nil
|
453
|
+
rules_removed = true
|
454
|
+
end
|
455
|
+
i += 1
|
456
|
+
end
|
457
|
+
|
458
|
+
# strip the array of nil values we may have set in the previous step
|
459
|
+
rules.compact! if rules_removed
|
460
|
+
|
461
|
+
# look for overlapping rules
|
462
|
+
i = 0
|
463
|
+
while i < rules.length
|
464
|
+
next_rule = rules[i + 1]
|
465
|
+
if next_rule && rules[i][:offset][1] > next_rule[:offset][0]
|
466
|
+
# we found an overlapping rule
|
467
|
+
# slice up this rule and add the remaining to the end of the array
|
468
|
+
rules << {
|
469
|
+
offset: [next_rule[:offset][1], rules[i][:offset][1]],
|
470
|
+
state: rules[i][:state]
|
471
|
+
}
|
472
|
+
# and shorten the length of this rule
|
473
|
+
rules[i][:offset][1] = next_rule[:offset][0]
|
474
|
+
|
475
|
+
# start again
|
476
|
+
return organize_rules(rules)
|
477
|
+
end
|
478
|
+
i += 1
|
479
|
+
end
|
480
|
+
|
481
|
+
# we're done!
|
482
|
+
return rules
|
527
483
|
end
|
528
484
|
end
|
529
485
|
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Marmara
|
2
|
+
module Config
|
3
|
+
attr_reader :options
|
4
|
+
|
5
|
+
def options=(opts)
|
6
|
+
if @options
|
7
|
+
if @options[:output_directory]
|
8
|
+
opts[:output_directory] ||= @options[:output_directory]
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
@options = opts
|
13
|
+
end
|
14
|
+
|
15
|
+
def output_directory
|
16
|
+
(options || {})[:output_directory] || 'log/css'
|
17
|
+
end
|
18
|
+
|
19
|
+
def output_directory=(dir)
|
20
|
+
@options ||= {}
|
21
|
+
@options[:output_directory] = dir
|
22
|
+
end
|
23
|
+
|
24
|
+
def logger=(logger)
|
25
|
+
@logger = logger
|
26
|
+
end
|
27
|
+
|
28
|
+
def log(str, method = :info)
|
29
|
+
if @logger
|
30
|
+
@logger.send(method, str)
|
31
|
+
elsif @logger.nil?
|
32
|
+
puts str
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def ignore?(file)
|
37
|
+
return false unless options
|
38
|
+
|
39
|
+
[*options[:ignore]].each do |matcher|
|
40
|
+
if matcher.is_a?(Regexp)
|
41
|
+
return true if file =~ matcher
|
42
|
+
else
|
43
|
+
return true if file.start_with?(matcher)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
return false
|
48
|
+
end
|
49
|
+
|
50
|
+
def get_report_filename(uri)
|
51
|
+
if options && options[:rewrite]
|
52
|
+
rewrite_rules = options[:rewrite]
|
53
|
+
rewrite_rules = [rewrite_rules] unless rewrite_rules.is_a?(Array)
|
54
|
+
rewrite_rules.each do |rule|
|
55
|
+
return uri.gsub(rule[:from], rule[:to]) if uri =~ rule[:from]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
return File.basename(uri).gsub(/^(.*?)\?.*$/, '\1')
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Marmara
|
2
|
+
class MinimumCoverageNotMetBase < Exception
|
3
|
+
attr_reader :expected
|
4
|
+
attr_reader :actual
|
5
|
+
|
6
|
+
def initialize(expected, actual)
|
7
|
+
@expected = expected
|
8
|
+
@actual = actual
|
9
|
+
super("Failed to meet minimum CSS #{type} coverage of #{expected}%")
|
10
|
+
end
|
11
|
+
|
12
|
+
def type
|
13
|
+
raise "This exception class is abstract"
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.assert(expected, actual)
|
17
|
+
raise Object.const_get(self.name).new(expected, actual) if expected && expected > actual
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class MinimumRuleCoverageNotMet < MinimumCoverageNotMetBase
|
22
|
+
def type
|
23
|
+
'rule'
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class MinimumSelectorCoverageNotMet < MinimumCoverageNotMetBase
|
28
|
+
def type
|
29
|
+
'selector'
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class MinimumDeclarationCoverageNotMet < MinimumCoverageNotMetBase
|
34
|
+
def type
|
35
|
+
'declaration'
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
body {
|
2
|
+
margin: 0;
|
3
|
+
font-size: 18px;
|
4
|
+
background-color: #FBF3E9;
|
5
|
+
}
|
6
|
+
|
7
|
+
#code {
|
8
|
+
white-space: nowrap;
|
9
|
+
}
|
10
|
+
|
11
|
+
#lines {
|
12
|
+
position: relative;
|
13
|
+
float: left;
|
14
|
+
padding: 0 0.75em 0 0.5em;
|
15
|
+
text-align: right;
|
16
|
+
font-weight: bold;
|
17
|
+
color: rgba(0, 0, 0, 0.125);
|
18
|
+
letter-spacing: -0.125em;
|
19
|
+
transition: color 150ms ease-in-out;
|
20
|
+
cursor: default;
|
21
|
+
}
|
22
|
+
|
23
|
+
#lines:hover, #lines a:target {
|
24
|
+
color: rgba(0, 0, 0, 0.75);
|
25
|
+
}
|
26
|
+
|
27
|
+
#lines a {
|
28
|
+
color: inherit;
|
29
|
+
text-decoration: none;
|
30
|
+
display: block;
|
31
|
+
}
|
32
|
+
|
33
|
+
#lines a:hover::after, #lines a:target::after {
|
34
|
+
content: '';
|
35
|
+
position: absolute;
|
36
|
+
left: 0;
|
37
|
+
width: 100vw;
|
38
|
+
background-color: #00BFFF;
|
39
|
+
height: 1.6em;
|
40
|
+
opacity: 0.25;
|
41
|
+
}
|
42
|
+
|
43
|
+
#lines a:hover::after {
|
44
|
+
background-color: #BDB76B;
|
45
|
+
}
|
46
|
+
|
47
|
+
pre {
|
48
|
+
font-family: 'Cutive Mono', monospace;
|
49
|
+
display: inline;
|
50
|
+
margin: 0;
|
51
|
+
padding: 0;
|
52
|
+
line-height: 1.65em;
|
53
|
+
}
|
54
|
+
|
55
|
+
pre > span {
|
56
|
+
position: relative;
|
57
|
+
z-index: 1;
|
58
|
+
color: #333;
|
59
|
+
}
|
60
|
+
|
61
|
+
.covered {
|
62
|
+
color: #AEE6B0;
|
63
|
+
}
|
64
|
+
|
65
|
+
.not-covered {
|
66
|
+
color: #F79B95;
|
67
|
+
}
|
68
|
+
|
69
|
+
.covered, .not-covered {
|
70
|
+
background-color: currentColor;
|
71
|
+
text-shadow: 0 0 0.25em #FFF;
|
72
|
+
font-weight: bold;
|
73
|
+
padding: 0.25em 0;
|
74
|
+
}
|
75
|
+
|
76
|
+
.ignored {
|
77
|
+
color: #888;
|
78
|
+
}
|
@@ -3,74 +3,7 @@
|
|
3
3
|
<head>
|
4
4
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
5
5
|
<link href="https://fonts.googleapis.com/css?family=Cutive+Mono" rel="stylesheet">
|
6
|
-
<style type="text/css">
|
7
|
-
body {
|
8
|
-
margin: 0;
|
9
|
-
font-size: 18px;
|
10
|
-
background-color: #FBF3E9;
|
11
|
-
}
|
12
|
-
#code {
|
13
|
-
white-space: nowrap;
|
14
|
-
}
|
15
|
-
#lines {
|
16
|
-
position: relative;
|
17
|
-
float: left;
|
18
|
-
padding: 0 0.75em 0 0.5em;
|
19
|
-
text-align: right;
|
20
|
-
font-weight: bold;
|
21
|
-
color: rgba(0, 0, 0, 0.125);
|
22
|
-
letter-spacing: -0.125em;
|
23
|
-
transition: color 150ms ease-in-out;
|
24
|
-
cursor: default;
|
25
|
-
}
|
26
|
-
#lines:hover, #lines a:target {
|
27
|
-
color: rgba(0, 0, 0, 0.75);
|
28
|
-
}
|
29
|
-
#lines a {
|
30
|
-
color: inherit;
|
31
|
-
text-decoration: none;
|
32
|
-
display: block;
|
33
|
-
}
|
34
|
-
#lines a:hover::after, #lines a:target::after {
|
35
|
-
content: '';
|
36
|
-
position: absolute;
|
37
|
-
left: 0;
|
38
|
-
width: 100vw;
|
39
|
-
background-color: #00BFFF;
|
40
|
-
height: 1.6em;
|
41
|
-
opacity: 0.25;
|
42
|
-
}
|
43
|
-
#lines a:hover::after {
|
44
|
-
background-color: #BDB76B;
|
45
|
-
}
|
46
|
-
pre {
|
47
|
-
font-family: 'Cutive Mono', monospace;
|
48
|
-
display: inline;
|
49
|
-
margin: 0;
|
50
|
-
padding: 0;
|
51
|
-
line-height: 1.65em;
|
52
|
-
}
|
53
|
-
pre > span {
|
54
|
-
position: relative;
|
55
|
-
z-index: 1;
|
56
|
-
color: #333;
|
57
|
-
}
|
58
|
-
.covered {
|
59
|
-
color: #AEE6B0;
|
60
|
-
}
|
61
|
-
.not-covered {
|
62
|
-
color: #F79B95;
|
63
|
-
}
|
64
|
-
.covered, .not-covered {
|
65
|
-
background-color: currentColor;
|
66
|
-
text-shadow: 0 0 0.25em #FFF;
|
67
|
-
font-weight: bold;
|
68
|
-
padding: 0.25em 0;
|
69
|
-
}
|
70
|
-
.ignored {
|
71
|
-
color: #888;
|
72
|
-
}
|
73
|
-
</style>
|
6
|
+
<style type="text/css">%{style}</style>
|
74
7
|
</head>
|
75
8
|
<body>
|
76
9
|
<div id="lines">
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: marmara
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Godwin
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-03-
|
11
|
+
date: 2017-03-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: css_parser
|
@@ -80,35 +80,7 @@ dependencies:
|
|
80
80
|
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
|
-
|
84
|
-
name: capybara
|
85
|
-
requirement: !ruby/object:Gem::Requirement
|
86
|
-
requirements:
|
87
|
-
- - ">="
|
88
|
-
- !ruby/object:Gem::Version
|
89
|
-
version: '0'
|
90
|
-
type: :development
|
91
|
-
prerelease: false
|
92
|
-
version_requirements: !ruby/object:Gem::Requirement
|
93
|
-
requirements:
|
94
|
-
- - ">="
|
95
|
-
- !ruby/object:Gem::Version
|
96
|
-
version: '0'
|
97
|
-
- !ruby/object:Gem::Dependency
|
98
|
-
name: poltergeist
|
99
|
-
requirement: !ruby/object:Gem::Requirement
|
100
|
-
requirements:
|
101
|
-
- - ">="
|
102
|
-
- !ruby/object:Gem::Version
|
103
|
-
version: '0'
|
104
|
-
type: :development
|
105
|
-
prerelease: false
|
106
|
-
version_requirements: !ruby/object:Gem::Requirement
|
107
|
-
requirements:
|
108
|
-
- - ">="
|
109
|
-
- !ruby/object:Gem::Version
|
110
|
-
version: '0'
|
111
|
-
description: Generates a CSS coverage report
|
83
|
+
description: Generates a CSS coverage report and tests for minimum coverage
|
112
84
|
email:
|
113
85
|
- goodgodwin@hotmail.com
|
114
86
|
executables: []
|
@@ -119,7 +91,10 @@ files:
|
|
119
91
|
- README.md
|
120
92
|
- Rakefile
|
121
93
|
- lib/marmara.rb
|
94
|
+
- lib/marmara/config.rb
|
95
|
+
- lib/marmara/exceptions.rb
|
122
96
|
- lib/marmara/parser.rb
|
97
|
+
- lib/marmara/style-sheet.css
|
123
98
|
- lib/marmara/style-sheet.html
|
124
99
|
homepage: http://bikecollectives.org
|
125
100
|
licenses:
|
@@ -133,7 +108,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
133
108
|
requirements:
|
134
109
|
- - ">="
|
135
110
|
- !ruby/object:Gem::Version
|
136
|
-
version: '0'
|
111
|
+
version: '2.0'
|
137
112
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
138
113
|
requirements:
|
139
114
|
- - ">="
|