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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a30c4afdfc40d807cee2c3e6dd4f2e02bd895530
4
- data.tar.gz: 675fc30f309c95239b6853e7435fadec407773c4
3
+ metadata.gz: 0e9743576c518a82530bc19f2142fb6b1b797c3b
4
+ data.tar.gz: f89b1541bfa6c5bdd765f17636937666f1e79312
5
5
  SHA512:
6
- metadata.gz: 7c635c1f1e7ad3984ef846f31ac06c5ea469040d99d6f7ef9ffe708cbe8966b9f9da8ab9ccd765bc4e9a12cc3e9a03ebdfcefacc5527f6b845eddd2c127d895f
7
- data.tar.gz: 7bff9f22acbd90c6c3df8acb5a7712b943449da663b73ff17c1b0f0ab6d9599e0355ab41643156e35c432ac21d1f017ec57e65c7f6db9efba9ecee70d2962e49
6
+ metadata.gz: 99a0c5b4f8a41f8f6f9361ac1fbd5bf96738ba6fc525b0d8762cf0c0957017c1bd65ac053d493c82574efa7cc73b1c6c3d895a2274f610f298dd08e87735d8a8
7
+ data.tar.gz: 14d861ac82ea43640662e78dd6aa1b1f3efb1e3495cb10fbc92d20743a1ea8723a77c8700d4701856abab27df8b995cfa52f9f022be5623f0cd7e6dd27b467a1
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Marmara
1
+ # Marmara [![Build Status](https://travis-ci.org/lingua_franca/marmara.png?branch=master)](https://travis-ci.org/lingua_franca/marmara) [![Gem Version](https://badge.fury.io/rb/marmara.svg)](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
  ![Example screenshot of Marmara output](https://i.imgur.com/N7J6wjD.png)
@@ -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 @style_sheets[sheet] && @style_sheet_rules[sheet]
57
- @style_sheet_rules[sheet] = []
58
- all_selectors = {}
59
- all_at_rules = []
60
-
61
- parser = nil
62
- begin
63
- parser = CssParser::MarmaraParser.new
64
- parser.load_uri!(sheet, capture_offsets: true)
65
- rescue Exception => e
66
- puts e.to_s
67
- puts "\t" + e.backtrace.join("\n\t")
68
- log "Error reading #{sheet}"
69
- end
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
- if at_rule[:value]
99
- at_rule[:valueRegex] ||= /(?:^|,)\s*(?:#{Regexp.escape(at_rule[:value])}|\"#{Regexp.escape(at_rule[:value])}\")\s*(?:,|;?$)/
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] << at_rule
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: :rule,
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
- # store info about the stylesheet
131
- @style_sheets[sheet] = {
132
- css: parser.last_file_contents,
133
- all_selectors: all_selectors,
134
- all_at_rules: all_at_rules,
135
- included_with: Set.new
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
- # gather together only the selectors that haven't been spotted yet
142
- selectors_to_find = @style_sheets[sheet][:all_selectors].select{|k,v|!v}.keys
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
- # don't do anything unless we have to
145
- if selectors_to_find.length > 0
146
- # and search for them in this document
147
- found_selectors = evaluate_script("(function(selectors) {
148
- var results = {};
149
- for (var i = 0; i < selectors.length; i++) {
150
- results[selectors[i]] = !!document.querySelector(selectors[i]);
151
- }
152
- return results;
153
- })(#{selectors_to_find.to_json})", driver)
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
- # now merge the results back in
156
- found_selectors.each { |k,v| @style_sheets[sheet][:all_selectors][k] ||= v }
156
+ # now merge the results back in
157
+ found_selectors.each { |k,v| @style_sheets[sheet][:all_selectors][k] ||= v }
157
158
 
158
- # and mark each as used if found
159
- @style_sheet_rules[sheet].each_with_index do |rule, rule_index|
160
- if rule[:type] == :rule
161
- rule[:selectors].each_with_index do |sel, sel_index|
162
- @style_sheet_rules[sheet][rule_index][:used_selectors][sel_index] ||= @style_sheets[sheet][:all_selectors][sel[:queryable]]
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
- 'Rules' => { match_count: 0, total: 0 },
191
- 'Selectors' => { match_count: 0, total: 0 },
192
- 'Declarations' => { match_count: 0, total: 0 }
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
- # output stats for this file
208
- log_stats(get_report_filename(uri), {
209
- 'Rules' => {
210
- match_count: coverage[:matched_rules],
211
- total: coverage[:total_rules]
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
- # add to the overall stats
224
- overall_stats['Rules'][:match_count] += coverage[:matched_rules]
225
- overall_stats['Rules'][:total] += coverage[:total_rules]
226
- overall_stats['Selectors'][:match_count] += coverage[:matched_selectors]
227
- overall_stats['Selectors'][:total] += coverage[:total_selectors]
228
- overall_stats['Declarations'][:match_count] += coverage[:matched_declarations]
229
- overall_stats['Declarations'][:total] += coverage[:total_declarations]
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
- def download_style_sheet(uri)
241
- open_attempts = 0
242
- begin
243
- open_attempts += 1
244
- uri = Addressable::URI.parse(uri.to_s)
245
-
246
- # remote file
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
- File.open(get_report_path(uri), 'wb:UTF-8') { |f| f.write(html) }
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 = @style_sheet_rules[uri].count
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 = (1..original_sheet.lines.count + 1).to_a.map do |line|
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
- get_style_sheet_html.gsub('%{lines}', lines.join('')).gsub('%{style_sheet}', sheet_html)
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
- "<pre #{@state_attr[state]}><span>#{CGI.escapeHTML(str)}</span></pre>"
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 log(str)
526
- puts str
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.1
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-15 00:00:00.000000000 Z
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
- - !ruby/object:Gem::Dependency
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
  - - ">="