coverband 4.1.1 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/README.md +50 -43
  4. data/changes.md +31 -4
  5. data/lib/coverband/adapters/base.rb +24 -1
  6. data/lib/coverband/adapters/redis_store.rb +28 -7
  7. data/lib/coverband/at_exit.rb +19 -2
  8. data/lib/coverband/collectors/coverage.rb +35 -79
  9. data/lib/coverband/collectors/delta.rb +67 -0
  10. data/lib/coverband/integrations/background.rb +4 -3
  11. data/lib/coverband/integrations/rack_server_check.rb +4 -1
  12. data/lib/coverband/integrations/resque.rb +4 -0
  13. data/lib/coverband/reporters/base.rb +21 -14
  14. data/lib/coverband/reporters/html_report.rb +40 -15
  15. data/lib/coverband/reporters/web.rb +29 -6
  16. data/lib/coverband/utils/html_formatter.rb +20 -4
  17. data/lib/coverband/utils/railtie.rb +8 -0
  18. data/lib/coverband/utils/result.rb +3 -2
  19. data/lib/coverband/utils/results.rb +63 -0
  20. data/lib/coverband/utils/source_file.rb +5 -0
  21. data/lib/coverband/version.rb +1 -1
  22. data/lib/coverband.rb +23 -3
  23. data/public/application.css +5 -0
  24. data/public/application.js +108 -1644
  25. data/public/dependencies.js +1581 -0
  26. data/test/coverband/adapters/redis_store_test.rb +26 -9
  27. data/test/coverband/collectors/coverage_test.rb +15 -9
  28. data/test/coverband/collectors/delta_test.rb +52 -0
  29. data/test/coverband/coverband_test.rb +7 -0
  30. data/test/coverband/integrations/background_test.rb +14 -11
  31. data/test/coverband/integrations/middleware_test.rb +1 -0
  32. data/test/coverband/integrations/resque_worker_test.rb +7 -1
  33. data/test/coverband/reporters/base_test.rb +4 -4
  34. data/test/coverband/reporters/console_test.rb +1 -1
  35. data/test/coverband/reporters/html_test.rb +6 -7
  36. data/test/integration/full_stack_test.rb +10 -8
  37. data/test/integration/rails_full_stack_test.rb +23 -4
  38. data/test/rails4_dummy/config/application.rb +1 -1
  39. data/test/rails4_dummy/config/coverband.rb +4 -1
  40. data/test/rails5_dummy/config/application.rb +1 -1
  41. data/test/rails5_dummy/config/coverband.rb +3 -1
  42. data/test/rails_test_helper.rb +13 -8
  43. data/test/test_helper.rb +8 -1
  44. data/views/file_list.erb +9 -0
  45. data/views/gem_list.erb +1 -1
  46. data/views/layout.erb +4 -3
  47. data/views/source_file.erb +16 -4
  48. data/views/source_file_loader.erb +7 -0
  49. metadata +8 -4
  50. data/test/integration/rails_gems_full_stack_test.rb +0 -36
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+ module Coverband
3
+ module Collectors
4
+ class Delta
5
+ @semaphore = Mutex.new
6
+ @@previous_coverage = nil
7
+ attr_reader :current_coverage
8
+
9
+ def initialize(current_coverage)
10
+ @current_coverage = current_coverage
11
+ end
12
+
13
+ class RubyCoverage
14
+ def self.results
15
+ ::Coverage.peek_result.dup
16
+ end
17
+ end
18
+
19
+ def self.results(process_coverage = RubyCoverage)
20
+ @semaphore.synchronize do
21
+ set_default_results
22
+ new(process_coverage.results).results
23
+ end
24
+ end
25
+
26
+ def self.previous_results
27
+ @@previous_coverage
28
+ end
29
+
30
+ def self.set_default_results
31
+ @@previous_coverage ||= {}
32
+ end
33
+
34
+ def results
35
+ new_results = generate
36
+ @@previous_coverage = current_coverage
37
+ new_results
38
+ end
39
+
40
+ def self.reset
41
+ @@previous_coverage = nil
42
+ end
43
+
44
+ private
45
+
46
+ def generate
47
+ current_coverage.each_with_object({}) do |(file, line_counts), new_results|
48
+ if @@previous_coverage[file]
49
+ new_results[file] = array_diff(line_counts, @@previous_coverage[file])
50
+ else
51
+ new_results[file] = line_counts
52
+ end
53
+ end
54
+ end
55
+
56
+ def array_diff(latest, original)
57
+ latest.map.with_index do |v, i|
58
+ if (v && original[i])
59
+ [0, v - original[i]].max
60
+ else
61
+ nil
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -3,6 +3,7 @@
3
3
  module Coverband
4
4
  class Background
5
5
  @semaphore = Mutex.new
6
+ @thread = nil
6
7
 
7
8
  def self.stop
8
9
  return unless @thread
@@ -15,15 +16,15 @@ module Coverband
15
16
  end
16
17
 
17
18
  def self.running?
18
- !!@thread
19
+ @thread && @thread.alive?
19
20
  end
20
21
 
21
22
  def self.start
22
- return if @thread
23
+ return if running?
23
24
 
24
25
  logger = Coverband.configuration.logger
25
26
  @semaphore.synchronize do
26
- return if @thread
27
+ return if running?
27
28
  logger&.debug('Coverband: Starting background reporting')
28
29
  sleep_seconds = Coverband.configuration.background_reporting_sleep_seconds
29
30
  @thread = Thread.new do
@@ -20,7 +20,10 @@ module Coverband
20
20
 
21
21
  def rails_server?
22
22
  @stack.any? do |location|
23
- location.path.include?('rails/commands/commands_tasks.rb') && location.label == 'server'
23
+ (
24
+ location.path.include?('rails/commands/commands_tasks.rb') && location.label == 'server' ||
25
+ location.path.include?('rails/commands/server/server_command.rb') && location.label == 'perform'
26
+ )
24
27
  end
25
28
  end
26
29
  end
@@ -2,9 +2,13 @@
2
2
 
3
3
  Resque.after_fork do |job|
4
4
  Coverband.start
5
+ Coverband.runtime_coverage!
6
+ # no reason to miss coverage on a first resque job
7
+ Coverband::Collectors::Delta.set_default_results
5
8
  end
6
9
 
7
10
  Resque.before_first_fork do
11
+ Coverband.eager_loading_coverage!
8
12
  Coverband.configuration.background_reporting_enabled = false
9
13
  Coverband::Background.stop
10
14
  Coverband::Collectors::Coverage.instance.report_coverage(true)
@@ -15,7 +15,7 @@ module Coverband
15
15
 
16
16
  if Coverband.configuration.verbose
17
17
  msg = "report:\n #{scov_style_report.inspect}"
18
- Coverband.configuration.logger.debug msg
18
+ # Coverband.configuration.logger.debug msg
19
19
  end
20
20
  scov_style_report
21
21
  end
@@ -28,15 +28,18 @@ module Coverband
28
28
  end
29
29
 
30
30
  # normalize names across servers
31
- report_hash.each_with_object({}) do |(key, vals), fixed_report|
32
- filename = relative_path_to_full(key, roots)
33
- fixed_report[filename] = if fixed_report.key?(filename) && fixed_report[filename]['data'] && vals['data']
34
- merged_data = merge_arrays(fixed_report[filename]['data'], vals['data'])
35
- vals['data'] = merged_data
36
- vals
37
- else
38
- vals
39
- end
31
+ report_hash.each_with_object({}) do |(name, report), fixed_report|
32
+ fixed_report[name] = {}
33
+ report.each_pair do |key, vals|
34
+ filename = relative_path_to_full(key, roots)
35
+ fixed_report[name][filename] = if fixed_report[name].key?(filename) && fixed_report[name][filename]['data'] && vals['data']
36
+ merged_data = merge_arrays(fixed_report[name][filename]['data'], vals['data'])
37
+ vals['data'] = merged_data
38
+ vals
39
+ else
40
+ vals
41
+ end
42
+ end
40
43
  end
41
44
  end
42
45
 
@@ -64,10 +67,14 @@ module Coverband
64
67
  ###
65
68
  def get_current_scov_data_imp(store, roots)
66
69
  scov_style_report = {}
67
- store.coverage.each_pair do |key, line_data|
68
- next if Coverband.configuration.ignore.any? { |i| key.match(i) }
69
- next unless line_data
70
- scov_style_report[key] = line_data
70
+ store.get_coverage_report.each_pair do |name, data|
71
+ data.each_pair do |key, line_data|
72
+ next if Coverband.configuration.ignore.any? { |i| key.match(i) }
73
+ next unless line_data
74
+
75
+ scov_style_report[name] = {} unless scov_style_report.key?(name)
76
+ scov_style_report[name][key] = line_data
77
+ end
71
78
  end
72
79
 
73
80
  scov_style_report = fix_file_names(scov_style_report, roots)
@@ -3,23 +3,28 @@
3
3
  module Coverband
4
4
  module Reporters
5
5
  class HTMLReport < Base
6
- def self.report(store, options = {})
7
- scov_style_report = super(store, options)
8
- open_report = options.fetch(:open_report) { true }
9
- html = options.fetch(:html) { false }
10
- notice = options.fetch(:notice) { nil }
11
- base_path = options.fetch(:base_path) { nil }
6
+ attr_accessor :filtered_report_files, :open_report, :html, :notice,
7
+ :base_path, :filename
12
8
 
13
- # list all files, even if not tracked by Coverband (0% coverage)
14
- tracked_glob = "#{Coverband.configuration.current_root}/{app,lib,config}/**/*.{rb}"
15
- report_files = Coverband::Utils::Result.add_not_loaded_files(scov_style_report, tracked_glob)
16
- # apply coverband filters
17
- filtered_report_files = {}
18
- report_files.each_pair do |file, data|
19
- next if Coverband.configuration.ignore.any? { |i| file.match(i) }
20
- filtered_report_files[file] = data
21
- end
9
+ def initialize(store, options = {})
10
+ coverband_reports = Coverband::Reporters::Base.report(store, options)
11
+ self.open_report = options.fetch(:open_report) { true }
12
+ self.html = options.fetch(:html) { false }
13
+ # TODO: refactor notice out to top level of web only
14
+ self.notice = options.fetch(:notice) { nil }
15
+ self.base_path = options.fetch(:base_path) { nil }
16
+ self.filename = options.fetch(:filename) { nil }
17
+
18
+ self.filtered_report_files = self.class.fix_reports(coverband_reports)
19
+ end
20
+
21
+ def file_details
22
+ Coverband::Utils::HTMLFormatter.new(filtered_report_files,
23
+ base_path: base_path,
24
+ notice: notice).format_source_file!(filename)
25
+ end
22
26
 
27
+ def report
23
28
  if html
24
29
  Coverband::Utils::HTMLFormatter.new(filtered_report_files,
25
30
  base_path: base_path,
@@ -35,6 +40,26 @@ module Coverband
35
40
  Coverband::Utils::S3Report.instance.persist! if Coverband.configuration.s3_bucket
36
41
  end
37
42
  end
43
+
44
+ private
45
+
46
+ def self.fix_reports(reports)
47
+ # list all files, even if not tracked by Coverband (0% coverage)
48
+ tracked_glob = "#{Coverband.configuration.current_root}/{app,lib,config}/**/*.{rb}"
49
+ filtered_report_files = {}
50
+
51
+ reports.each_pair do |report_name, report_data|
52
+ filtered_report_files[report_name] = {}
53
+ report_files = Coverband::Utils::Result.add_not_loaded_files(report_data, tracked_glob)
54
+
55
+ # apply coverband filters
56
+ report_files.each_pair do |file, data|
57
+ next if Coverband.configuration.ignore.any? { |i| file.match(i) }
58
+
59
+ filtered_report_files[report_name][file] = data
60
+ end
61
+ end
62
+ end
38
63
  end
39
64
  end
40
65
  end
@@ -22,6 +22,8 @@ module Coverband
22
22
 
23
23
  if request.post?
24
24
  case request.path_info
25
+ when %r{\/clear_file}
26
+ clear_file
25
27
  when %r{\/clear}
26
28
  clear
27
29
  when %r{\/collect_coverage}
@@ -39,6 +41,8 @@ module Coverband
39
41
  [200, { 'Content-Type' => 'text/html' }, [settings]]
40
42
  when %r{\/debug_data}
41
43
  [200, { 'Content-Type' => 'text/json' }, [debug_data]]
44
+ when %r{\/load_file_details}
45
+ [200, { 'Content-Type' => 'text/json' }, [load_file_details]]
42
46
  when %r{\/$}
43
47
  [200, { 'Content-Type' => 'text/html' }, [index]]
44
48
  else
@@ -50,11 +54,11 @@ module Coverband
50
54
  def index
51
55
  notice = "<strong>Notice:</strong> #{Rack::Utils.escape_html(request.params['notice'])}<br/>"
52
56
  notice = request.params['notice'] ? notice : ''
53
- Coverband::Reporters::HTMLReport.report(Coverband.configuration.store,
54
- html: true,
55
- base_path: base_path,
56
- notice: notice,
57
- open_report: false)
57
+ Coverband::Reporters::HTMLReport.new(Coverband.configuration.store,
58
+ html: true,
59
+ base_path: base_path,
60
+ notice: notice,
61
+ open_report: false).report
58
62
  end
59
63
 
60
64
  def settings
@@ -62,7 +66,15 @@ module Coverband
62
66
  end
63
67
 
64
68
  def debug_data
65
- Coverband.configuration.store.coverage.to_json
69
+ Coverband.configuration.store.get_coverage_report.to_json
70
+ end
71
+
72
+ def load_file_details
73
+ filename = request.params['filename']
74
+ Coverband::Reporters::HTMLReport.new(Coverband.configuration.store,
75
+ filename: filename,
76
+ base_path: base_path,
77
+ open_report: false).file_details
66
78
  end
67
79
 
68
80
  def collect_coverage
@@ -81,6 +93,17 @@ module Coverband
81
93
  [301, { 'Location' => "#{base_path}?notice=#{notice}" }, []]
82
94
  end
83
95
 
96
+ def clear_file
97
+ if Coverband.configuration.web_enable_clear
98
+ filename = request.params['filename']
99
+ Coverband.configuration.store.clear_file!(filename)
100
+ notice = "coverage for file #{filename} cleared"
101
+ else
102
+ notice = 'web_enable_clear isnt enabled in your configuration'
103
+ end
104
+ [301, { 'Location' => "#{base_path}?notice=#{notice}" }, []]
105
+ end
106
+
84
107
  def reload_files
85
108
  Coverband.configuration&.safe_reload_files&.each do |safe_file|
86
109
  load safe_file
@@ -18,7 +18,7 @@ module Coverband
18
18
  def initialize(report, options = {})
19
19
  @notice = options.fetch(:notice) { nil }
20
20
  @base_path = options.fetch(:base_path) { nil }
21
- @coverage_result = Coverband::Utils::Result.new(report) if report
21
+ @coverage_result = Coverband::Utils::Results.new(report) if report
22
22
  end
23
23
 
24
24
  def format!
@@ -33,6 +33,12 @@ module Coverband
33
33
  format_settings
34
34
  end
35
35
 
36
+ def format_source_file!(filename)
37
+ source_file = @coverage_result.file_from_path_with_type(filename)
38
+
39
+ formatted_source_file(@coverage_result, source_file)
40
+ end
41
+
36
42
  private
37
43
 
38
44
  def format_settings
@@ -86,20 +92,28 @@ module Coverband
86
92
  end
87
93
 
88
94
  # Returns the html for the given source_file
89
- def formatted_source_file(source_file)
95
+ def formatted_source_file(result, source_file)
90
96
  template('source_file').result(binding)
91
97
  rescue Encoding::CompatibilityError => e
92
98
  puts "Encoding error file:#{source_file.filename} Coverband/ERB error #{e.message}."
93
99
  end
94
100
 
101
+ # Returns the html to ajax load a given source_file
102
+ def formatted_source_file_loader(result, source_file)
103
+ template('source_file_loader').result(binding)
104
+ rescue Encoding::CompatibilityError => e
105
+ puts "Encoding error file:#{source_file.filename} Coverband/ERB error #{e.message}."
106
+ end
107
+
95
108
  # Returns a table containing the given source files
96
- def formatted_file_list(title, source_files, options = {})
109
+ def formatted_file_list(title, result, source_files, options = {})
97
110
  title_id = title.gsub(/^[^a-zA-Z]+/, '').gsub(/[^a-zA-Z0-9\-\_]/, '')
98
111
  # Silence a warning by using the following variable to assign to itself:
99
112
  # "warning: possibly useless use of a variable in void context"
100
113
  # The variable is used by ERB via binding.
101
114
  title_id = title_id
102
115
  options = options
116
+
103
117
  if title == 'Gems'
104
118
  template('gem_list').result(binding)
105
119
  else
@@ -116,7 +130,9 @@ module Coverband
116
130
  end
117
131
 
118
132
  def coverage_css_class(covered_percent)
119
- if covered_percent > 90
133
+ if covered_percent.nil?
134
+ ''
135
+ elsif covered_percent > 90
120
136
  'green'
121
137
  elsif covered_percent > 80
122
138
  'yellow'
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ Coverband.eager_loading_coverage!
3
4
  module Coverband
4
5
  class Railtie < Rails::Railtie
5
6
  initializer 'coverband.configure' do |app|
@@ -8,6 +9,13 @@ module Coverband
8
9
 
9
10
  config.after_initialize do
10
11
  Coverband.report_coverage(true)
12
+ Coverband.configuration.logger&.debug('Coverband: reported after_initialize')
13
+ Coverband.runtime_coverage!
14
+ end
15
+
16
+ config.before_initialize do
17
+ Coverband.configuration.logger&.debug('Coverband: set to eager_loading')
18
+ Coverband.eager_loading_coverage!
11
19
  end
12
20
 
13
21
  rake_tasks do
@@ -32,8 +32,9 @@ module Coverband
32
32
  # Initialize a new Coverband::Result from given Coverage.result (a Hash of filenames each containing an array of
33
33
  # coverage data)
34
34
  def initialize(original_result)
35
- @original_result = original_result.freeze
36
- @files = Coverband::Utils::FileList.new(original_result.map do |filename, coverage|
35
+ @original_result = (original_result || {}).freeze
36
+
37
+ @files = Coverband::Utils::FileList.new(@original_result.map do |filename, coverage|
37
38
  Coverband::Utils::SourceFile.new(filename, coverage) if File.file?(filename)
38
39
  end.compact.sort_by(&:short_name))
39
40
  filter!
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ ####
4
+ # A way to access the various coverage data breakdowns
5
+ ####
6
+ module Coverband
7
+ module Utils
8
+ class Results
9
+ attr_accessor :type, :report
10
+
11
+ def initialize(report)
12
+ self.report = report
13
+ self.type = Coverband::MERGED_TYPE
14
+ @results = {}
15
+ end
16
+
17
+ def file_with_type(source_file, results_type)
18
+ return unless get_results(results_type)
19
+
20
+ get_results(results_type).source_files.find { |file| file.filename == source_file.filename }
21
+ end
22
+
23
+ def file_from_path_with_type(full_path, results_type = :merged)
24
+ return unless get_results(results_type)
25
+
26
+ get_results(results_type).source_files.find { |file| file.filename == full_path }
27
+ end
28
+
29
+ def method_missing(method, *args)
30
+ if get_results(type).respond_to?(method)
31
+ get_results(type).send(method, *args)
32
+ else
33
+ super
34
+ end
35
+ end
36
+
37
+ def respond_to_missing?(method)
38
+ if get_results(type).respond_to?(method)
39
+ true
40
+ else
41
+ false
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ ###
48
+ # This is a first version of lazy loading the results
49
+ # for the full advantage we need to push lazy loading to the file level
50
+ # inside Coverband::Utils::Result
51
+ ###
52
+ def get_results(type)
53
+ return nil unless Coverband::ALL_TYPES.include?(type)
54
+
55
+ if @results.key?(type)
56
+ @results[type]
57
+ else
58
+ @results[type] = Coverband::Utils::Result.new(report[type])
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -10,6 +10,7 @@
10
10
  module Coverband
11
11
  module Utils
12
12
  class SourceFile
13
+ include Coverband::Utils::FilePathHelper
13
14
  # Representation of a single line in a source file including
14
15
  # this specific line's source code, line_number and code coverage,
15
16
  # with the coverage being either nil (coverage not applicable, e.g. comment
@@ -219,6 +220,10 @@ module Coverband
219
220
  .gsub(%r{^\.\/}, '')
220
221
  end
221
222
 
223
+ def relative_path
224
+ full_path_to_relative(filename)
225
+ end
226
+
222
227
  def gem?
223
228
  filename =~ %r{^.*\/gems\/}
224
229
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Coverband
4
- VERSION = '4.1.1'
4
+ VERSION = '4.2.0'
5
5
  end
data/lib/coverband.rb CHANGED
@@ -3,7 +3,6 @@
3
3
  require 'logger'
4
4
  require 'json'
5
5
  require 'redis'
6
-
7
6
  require 'coverband/version'
8
7
  require 'coverband/at_exit'
9
8
  require 'coverband/configuration'
@@ -19,7 +18,7 @@ require 'coverband/utils/gem_list'
19
18
  require 'coverband/utils/source_file'
20
19
  require 'coverband/utils/file_groups'
21
20
  require 'coverband/utils/lines_classifier'
22
- require 'coverband/utils/railtie' if defined? ::Rails::Railtie
21
+ require 'coverband/utils/results'
23
22
  require 'coverband/collectors/coverage'
24
23
  require 'coverband/reporters/base'
25
24
  require 'coverband/reporters/html_report'
@@ -29,10 +28,14 @@ require 'coverband/integrations/rack_server_check'
29
28
  require 'coverband/reporters/web'
30
29
  require 'coverband/integrations/middleware'
31
30
  require 'coverband/integrations/background'
32
- require 'coverband/integrations/resque' if defined? Resque
33
31
 
34
32
  module Coverband
35
33
  CONFIG_FILE = './config/coverband.rb'
34
+ RUNTIME_TYPE = nil
35
+ EAGER_TYPE = :eager_loading
36
+ MERGED_TYPE = :merged
37
+ TYPES = [RUNTIME_TYPE, EAGER_TYPE]
38
+ ALL_TYPES = TYPES + [:merged]
36
39
 
37
40
  class << self
38
41
  attr_accessor :configuration_data
@@ -48,6 +51,7 @@ module Coverband
48
51
  else
49
52
  configuration.logger&.debug('using default configuration')
50
53
  end
54
+ coverage.reset_instance
51
55
  end
52
56
 
53
57
  def self.report_coverage(force_report = false)
@@ -60,14 +64,30 @@ module Coverband
60
64
 
61
65
  def self.start
62
66
  Coverband::Collectors::Coverage.instance
67
+ # TODO Railtie sets up at_exit after forks, via middleware, perhaps this hsould be
68
+ # added if not rails or if rails but not rackserverrunning
63
69
  AtExit.register
64
70
  Background.start if configuration.background_reporting_enabled && !RackServerCheck.running?
65
71
  end
66
72
 
73
+ def self.eager_loading_coverage!
74
+ coverage.eager_loading!
75
+ end
76
+
77
+ def self.runtime_coverage!
78
+ coverage.runtime!
79
+ end
80
+
81
+ def self.coverage
82
+ Coverband::Collectors::Coverage.instance
83
+ end
84
+
67
85
  unless ENV['COVERBAND_DISABLE_AUTO_START']
68
86
  # Coverband should be setup as early as possible
69
87
  # to capture usage of things loaded by initializers or other Rails engines
70
88
  configure
71
89
  start
90
+ require 'coverband/utils/railtie' if defined? ::Rails::Railtie
91
+ require 'coverband/integrations/resque' if defined? Resque
72
92
  end
73
93
  end
@@ -748,6 +748,11 @@ td {
748
748
  text-align: center;
749
749
  border-radius: 6px; }
750
750
 
751
+ .loader {
752
+ text-align: center;
753
+ }
754
+
755
+
751
756
  #header {
752
757
  color: #FFF;
753
758
  font-size: 12px;