uicov 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +6 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +20 -0
- data/LICENSE +22 -0
- data/README.md +42 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/bin/uicov +6 -0
- data/lib/uicov.rb +33 -0
- data/lib/uicov/commands/command.rb +23 -0
- data/lib/uicov/commands/gather.rb +171 -0
- data/lib/uicov/commands/gentpl.rb +129 -0
- data/lib/uicov/commands/merge.rb +95 -0
- data/lib/uicov/commands/report.rb +119 -0
- data/lib/uicov/consts.rb +78 -0
- data/lib/uicov/coverage/action_data.rb +8 -0
- data/lib/uicov/coverage/check_data.rb +8 -0
- data/lib/uicov/coverage/data.rb +45 -0
- data/lib/uicov/coverage/element_data.rb +8 -0
- data/lib/uicov/coverage/member_data.rb +18 -0
- data/lib/uicov/coverage/screen_data.rb +109 -0
- data/lib/uicov/coverage/transition_data.rb +18 -0
- data/lib/uicov/coverage/types.rb +13 -0
- data/lib/uicov/coverage_data.rb +72 -0
- data/lib/uicov/coverage_info.rb +27 -0
- data/lib/uicov/main.rb +53 -0
- data/lib/uicov/opts.rb +28 -0
- data/lib/uicov/ruby_patches.rb +36 -0
- data/lib/uicov/screen_info.rb +7 -0
- data/lib/uicov/transition_info.rb +11 -0
- data/lib/uicov/ui_coverage.rb +91 -0
- data/lib/uicov/version.rb +7 -0
- data/rakefile +7 -0
- data/uicov.gemspec +32 -0
- metadata +110 -0
@@ -0,0 +1,95 @@
|
|
1
|
+
#=======
|
2
|
+
# Author: Alexey Lyanguzov (budabum@gmail.com)
|
3
|
+
#=======
|
4
|
+
|
5
|
+
module UICov
|
6
|
+
class Merge < Command
|
7
|
+
DEFAULT_FILENAME = 'merged.uic'
|
8
|
+
OPTIONS = {
|
9
|
+
'--merged-file=FILE' => "File to store merged coverage [default is '#{DEFAULT_FILENAME}']",
|
10
|
+
# '--no-transitions' => 'Do not merge transitions coverage',
|
11
|
+
# '--no-actions ' => 'Do not merge actions coverage',
|
12
|
+
# '--no-checks ' => 'Do not merge checks coverage',
|
13
|
+
# '--no-elements ' => 'Do not merge elements coverage'
|
14
|
+
}
|
15
|
+
USAGE_INFO = %Q^[options] template.uic file1.uic [file2.uic ... fileN.uic]
|
16
|
+
\n\rWhere options are:
|
17
|
+
#{OPTIONS.inject([]){|a, e| a << "\r\t#{e[0]}\t- #{e[1]}"; a}.join("\n")}
|
18
|
+
^
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
@merged_file = DEFAULT_FILENAME
|
22
|
+
end
|
23
|
+
|
24
|
+
def do_job(args)
|
25
|
+
usage 'Missed coverage file', USAGE_INFO if args.empty?
|
26
|
+
cov_files = process_args args
|
27
|
+
merge(cov_files)
|
28
|
+
@merged.save(@merged_file)
|
29
|
+
end
|
30
|
+
|
31
|
+
def merge(cov_files)
|
32
|
+
Log.warn 'Only one file is given. Nothing to merge.' if cov_files.size == 1
|
33
|
+
@merged = CovData.load cov_files[0]
|
34
|
+
cov_files[1..-1].each do |cov_file|
|
35
|
+
@cd = CovData.load cov_file
|
36
|
+
@cd.screens.each do |name, screen_data|
|
37
|
+
msd = @merged.screens[name]
|
38
|
+
if msd.nil?
|
39
|
+
@merged.screens[name] = screen_data.dup
|
40
|
+
else
|
41
|
+
merge_screen_data msd, screen_data
|
42
|
+
end
|
43
|
+
end
|
44
|
+
@merged.input_files.merge! @cd.input_files
|
45
|
+
end
|
46
|
+
return @merged
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
def process_args(args)
|
51
|
+
merged_file_option = args.grep(/--merged-file=.*/)[0]
|
52
|
+
if merged_file_option
|
53
|
+
@merged_file = File.expand_path merged_file_option.gsub(/.*=(.+)/, '\1')
|
54
|
+
args.delete_if { |e| e == merged_file_option }
|
55
|
+
end
|
56
|
+
return args
|
57
|
+
end
|
58
|
+
|
59
|
+
def merge_screen_data(msd, sd)
|
60
|
+
sd.elements.each do |name, sde|
|
61
|
+
me = msd.elements[name]
|
62
|
+
if me.nil?
|
63
|
+
msd.elements[name] = sde.dup
|
64
|
+
else
|
65
|
+
me.hit(sde.hits)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
sd.transitions.each do |name, sde|
|
69
|
+
me = msd.transitions[name]
|
70
|
+
if me.nil?
|
71
|
+
msd.transitions[name] = sde.dup
|
72
|
+
else
|
73
|
+
me.hit(sde.hits)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
sd.actions.each do |name, sde|
|
77
|
+
me = msd.actions[name]
|
78
|
+
if me.nil?
|
79
|
+
msd.actions[name] = sde.dup
|
80
|
+
else
|
81
|
+
me.hit(sde.hits)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
sd.checks.each do |name, sde|
|
85
|
+
me = msd.checks[name]
|
86
|
+
if me.nil?
|
87
|
+
msd.checks[name] = sde.dup
|
88
|
+
else
|
89
|
+
me.hit(sde.hits)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
@@ -0,0 +1,119 @@
|
|
1
|
+
#=======
|
2
|
+
# Author: Alexey Lyanguzov (budabum@gmail.com)
|
3
|
+
#=======
|
4
|
+
|
5
|
+
module UICov
|
6
|
+
class Report < Command
|
7
|
+
DEFAULT_FILENAME = 'uicov.report.html'
|
8
|
+
OPTIONS = {
|
9
|
+
'--report-file=FILE' => "File to store report [default is '#{DEFAULT_FILENAME}']",
|
10
|
+
# '--format=FORMAT ' => 'Report format. One of: html, puml [default is "html"]',
|
11
|
+
# '--no-transitions ' => 'Do not report transitions coverage',
|
12
|
+
# '--no-actions ' => 'Do not report actions coverage',
|
13
|
+
# '--no-checks ' => 'Do not report checks coverage',
|
14
|
+
# '--no-elements ' => 'Do not report elements coverage'
|
15
|
+
}
|
16
|
+
USAGE_INFO = %Q^[options] file1.uic [file2.uic ... fileN.uic]
|
17
|
+
\n\rWhere options are:
|
18
|
+
#{OPTIONS.inject([]){|a, e| a << "\r\t#{e[0]}\t- #{e[1]}"; a}.join("\n")}
|
19
|
+
^
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
@report_file = DEFAULT_FILENAME
|
23
|
+
end
|
24
|
+
|
25
|
+
def do_job(args)
|
26
|
+
usage 'Missed coverage file', USAGE_INFO if args.empty?
|
27
|
+
cov_files = process_args args
|
28
|
+
@cd = merged_file(cov_files)
|
29
|
+
@html = ''
|
30
|
+
@html << add_header
|
31
|
+
@html << create_summary_report
|
32
|
+
@html << create_screens_summary_report
|
33
|
+
@html << create_per_screen_report
|
34
|
+
save @report_file
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
def process_args(args)
|
39
|
+
report_file_option = args.grep(/--report-file=.*/)[0]
|
40
|
+
if report_file_option
|
41
|
+
@report_file = File.expand_path report_file_option.gsub(/.*=(.+)/, '\1')
|
42
|
+
args.delete_if { |e| e == report_file_option }
|
43
|
+
end
|
44
|
+
return args
|
45
|
+
end
|
46
|
+
|
47
|
+
def merged_file(cov_files)
|
48
|
+
cov_files.size > 1 ? Merge.new.merge(cov_files) : CovData.load(cov_files.first)
|
49
|
+
end
|
50
|
+
|
51
|
+
def add_header
|
52
|
+
%Q^
|
53
|
+
<style>
|
54
|
+
.covtable{
|
55
|
+
border: thin solid black;
|
56
|
+
text-align: left;
|
57
|
+
}
|
58
|
+
BODY,TABLE {font-size: small}
|
59
|
+
H2{text-align:center;}
|
60
|
+
TH{border:thin solid black;text-align:center; background-color: #CCCCCC}
|
61
|
+
TD{border:thin solid black;text-align:right}
|
62
|
+
TD.namecol{border:thin solid black;text-align:left}
|
63
|
+
CAPTION{text-align: left; font-weight: bold}
|
64
|
+
</style>
|
65
|
+
^
|
66
|
+
end
|
67
|
+
|
68
|
+
def create_summary_report
|
69
|
+
%Q^
|
70
|
+
<h1>Summary Report</h1>
|
71
|
+
^
|
72
|
+
end
|
73
|
+
|
74
|
+
def create_screens_summary_report
|
75
|
+
# tr_line1 = "<tr><td><b>%s</b></td><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>"
|
76
|
+
tr_line2 = "<tr>
|
77
|
+
<td class='namecol'><b>%s</b></td>
|
78
|
+
<td>%s</td><td>%s</td><td>%s</td>
|
79
|
+
<td>%s</td><td>%s</td><td>%s</td>
|
80
|
+
<td>%s</td><td>%s</td><td>%s</td>
|
81
|
+
<td>%s</td><td>%s</td><td>%s</td>
|
82
|
+
</tr>"
|
83
|
+
str_res = %Q^
|
84
|
+
<h1>Screens Summary Report</h1>
|
85
|
+
<table width='80%'>
|
86
|
+
<tr><th rowspan='2'>Screen</th><th colspan='3'>Elements</th><th colspan='3'>Transitions</th><th colspan='3'>Actions</th><th colspan='3'>Checks</th></tr>
|
87
|
+
<tr><th>Hit</th><th>All</th><th>%</th><th>Hit</th><th>All</th><th>%</th><th>Hit</th><th>All</th><th>%</th><th>Hit</th><th>All</th><th>%</th></tr>
|
88
|
+
^
|
89
|
+
total = @cd.screens.inject([0,0,0, 0,0,0, 0,0,0, 0,0,0]) do |arr, pair|
|
90
|
+
name, screen = pair
|
91
|
+
ec = [screen.get_count(:elements, true), screen.get_count(:elements), screen.get_coverage(:elements)]
|
92
|
+
tc = [screen.get_count(:transitions, true), screen.get_count(:transitions), screen.get_coverage(:transitions)]
|
93
|
+
ac = [screen.get_count(:actions, true), screen.get_count(:actions), screen.get_coverage(:actions)]
|
94
|
+
cc = [screen.get_count(:checks, true), screen.get_count(:checks), screen.get_coverage(:checks)]
|
95
|
+
str_res << tr_line2 % [name, *ec, *tc, *ac, *cc]
|
96
|
+
arr
|
97
|
+
end
|
98
|
+
str_res << %Q^
|
99
|
+
#{tr_line2 % ['Total', *total]}
|
100
|
+
</table>
|
101
|
+
^
|
102
|
+
end
|
103
|
+
#{@cd.screens.keys.map{|k| "#{tr_line % [k,'','','','']}".join("\n")}
|
104
|
+
|
105
|
+
def create_per_screen_report
|
106
|
+
%Q^
|
107
|
+
<h1>Detailed Report</h1>
|
108
|
+
#{@cd.screens.values.map{ |s| s.report }.join("\n")}
|
109
|
+
^
|
110
|
+
end
|
111
|
+
|
112
|
+
def save(filename)
|
113
|
+
report_file = File.expand_path filename
|
114
|
+
File.open(report_file, 'w'){|f| f.write(@html)}
|
115
|
+
Log.info "Result saved into file #{report_file}"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
data/lib/uicov/consts.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
#=======
|
2
|
+
# Author: Alexey Lyanguzov (budabum@gmail.com)
|
3
|
+
#=======
|
4
|
+
|
5
|
+
module UICov
|
6
|
+
GEM_TESTS_DIR = "#{GEM_HOME}/test"
|
7
|
+
GEM_TESTS_DATA_DIR = "#{GEM_TESTS_DIR}/data"
|
8
|
+
GEM_LIB_DIR = "#{GEM_HOME}/lib/uicov"
|
9
|
+
|
10
|
+
require "#{GEM_LIB_DIR}/ruby_patches"
|
11
|
+
require "#{GEM_LIB_DIR}/version"
|
12
|
+
|
13
|
+
require "#{GEM_LIB_DIR}/commands/command"
|
14
|
+
require "#{GEM_LIB_DIR}/main"
|
15
|
+
|
16
|
+
require "#{GEM_LIB_DIR}/coverage/types"
|
17
|
+
require "#{GEM_LIB_DIR}/coverage/data"
|
18
|
+
require "#{GEM_LIB_DIR}/coverage/member_data"
|
19
|
+
require "#{GEM_LIB_DIR}/coverage/transition_data"
|
20
|
+
require "#{GEM_LIB_DIR}/coverage/action_data"
|
21
|
+
require "#{GEM_LIB_DIR}/coverage/check_data"
|
22
|
+
require "#{GEM_LIB_DIR}/coverage/element_data"
|
23
|
+
require "#{GEM_LIB_DIR}/coverage/screen_data"
|
24
|
+
|
25
|
+
require "#{GEM_LIB_DIR}/opts"
|
26
|
+
require "#{GEM_LIB_DIR}/coverage_info"
|
27
|
+
require "#{GEM_LIB_DIR}/screen_info"
|
28
|
+
require "#{GEM_LIB_DIR}/transition_info"
|
29
|
+
require "#{GEM_LIB_DIR}/coverage_data"
|
30
|
+
require "#{GEM_LIB_DIR}/ui_coverage"
|
31
|
+
|
32
|
+
Log = Logger.new STDOUT
|
33
|
+
Log.level = Logger::DEBUG
|
34
|
+
|
35
|
+
SKIN_PARAMS=%q^
|
36
|
+
skinparam state {
|
37
|
+
FontSize 10
|
38
|
+
AttributeFontSize 10
|
39
|
+
|
40
|
+
BackgroundColor #FCFCFC
|
41
|
+
BorderColor #C0C0C0
|
42
|
+
FontColor #808080
|
43
|
+
ArrowColor #C0C0C0
|
44
|
+
AttributeFontColor #808080
|
45
|
+
ArrowFontColor #808080
|
46
|
+
|
47
|
+
BackgroundColor<<Covered>> #CCFFCC
|
48
|
+
BorderColor<<Covered>> #008800
|
49
|
+
FontColor<<Covered>> #004400
|
50
|
+
AttributeFontColor<<Covered>> #004400
|
51
|
+
|
52
|
+
BackgroundColor<<Missed>> #FFEE88
|
53
|
+
BorderColor<<Missed>> #FF8800
|
54
|
+
FontColor<<Missed>> #886622
|
55
|
+
AttributeFontColor<<Missed>> #886622
|
56
|
+
}
|
57
|
+
^
|
58
|
+
|
59
|
+
LEGEND=%q^
|
60
|
+
state Legend {
|
61
|
+
state "Uncovered Screen from Model" as UncoveredScreen
|
62
|
+
UncoveredScreen -> CoveredScreen : uncovered_transition {0, 0}
|
63
|
+
state "Covered screen from Model" as CoveredScreen<<Covered>>
|
64
|
+
CoveredScreen -[#orange]-> MissedScreen : <font color=orange><b>UNKNOWN</b></font> {1, 1} - covered transition missed in Model
|
65
|
+
CoveredScreen : <b>covered_action_with_3_calls_from_2_tests</b> {3, 2}
|
66
|
+
CoveredScreen : <font color=orange><b>covered_action_missed_in_model</b></font> {1, 1}
|
67
|
+
state "Screen missed in Model" as MissedScreen<<Missed>>
|
68
|
+
CoveredScreen -[#green]-> CoveredScreen : <font color=green><b>covered_transition_to_self</b></font> {1, 1}
|
69
|
+
MissedScreen : uncovered_action {0, 0}
|
70
|
+
MissedScreen : <b>covered_action_missed_in_model</b> {1, 1}
|
71
|
+
state "Another Covered Screen from Model" as AnotherCoveredScreen<<Covered>>
|
72
|
+
MissedScreen -left[#blue]-> AnotherCoveredScreen : <font color=blue><b>DEEP_LINK</b></font> {1, 1}
|
73
|
+
AnotherCoveredScreen -[#green]-> CoveredScreen : <font color=green><b>covered_transition</b></font> {1, 1}
|
74
|
+
CoveredScreen --> AnotherCoveredScreen : <font color=red>NOT_SET</font> {0, 0} - missed transition name in Model
|
75
|
+
AnotherCoveredScreen : uncovered_action {0 ,0}
|
76
|
+
}
|
77
|
+
^
|
78
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
#=======
|
2
|
+
# Author: Alexey Lyanguzov (budabum@gmail.com)
|
3
|
+
#=======
|
4
|
+
|
5
|
+
module UICov
|
6
|
+
class CovData
|
7
|
+
attr_reader :screens, :input_files
|
8
|
+
attr_accessor :type
|
9
|
+
|
10
|
+
def self.load(filename)
|
11
|
+
YAML.load_file(filename)
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(cov_file=nil)
|
15
|
+
@type = CoverageDataType::UNKNOWN
|
16
|
+
@input_files = {}
|
17
|
+
@screens = {}
|
18
|
+
load(cov_file) unless cov_file.nil?
|
19
|
+
end
|
20
|
+
|
21
|
+
def set_processing_date(date=Time.now)
|
22
|
+
@data_gathered_at = date.strftime('%F %R:%S.%3N')
|
23
|
+
end
|
24
|
+
|
25
|
+
def add_screen(name)
|
26
|
+
@screens[name] ||= ScreenData.new name
|
27
|
+
end
|
28
|
+
|
29
|
+
def add_covered_screen(name)
|
30
|
+
scd = add_screen name
|
31
|
+
scd.hit
|
32
|
+
return scd
|
33
|
+
end
|
34
|
+
|
35
|
+
def add_input_file(filename, filedate)
|
36
|
+
@input_files[filename] = filedate
|
37
|
+
end
|
38
|
+
|
39
|
+
def save(filename)
|
40
|
+
File.open(filename, 'w') { |f| f.write YAML.dump(self) }
|
41
|
+
Log.info "Result saved to '#{File.expand_path(filename)}'"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
@@ -0,0 +1,18 @@
|
|
1
|
+
#=======
|
2
|
+
# Author: Alexey Lyanguzov (budabum@gmail.com)
|
3
|
+
#=======
|
4
|
+
|
5
|
+
module UICov
|
6
|
+
class MemberData
|
7
|
+
attr_reader :display_name, :hits
|
8
|
+
def initialize(name)
|
9
|
+
@display_name = name
|
10
|
+
@hits = 0
|
11
|
+
end
|
12
|
+
|
13
|
+
def hit(increment=1)
|
14
|
+
@hits += increment
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
@@ -0,0 +1,109 @@
|
|
1
|
+
#=======
|
2
|
+
# Author: Alexey Lyanguzov (budabum@gmail.com)
|
3
|
+
#=======
|
4
|
+
|
5
|
+
module UICov
|
6
|
+
class ScreenData
|
7
|
+
attr_reader :elements, :transitions, :actions, :checks
|
8
|
+
def initialize(name)
|
9
|
+
@name = name
|
10
|
+
@hits = 0
|
11
|
+
@elements = {}
|
12
|
+
@transitions = {}
|
13
|
+
@actions = {}
|
14
|
+
@checks = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def hit
|
18
|
+
@hits += 1
|
19
|
+
end
|
20
|
+
|
21
|
+
def add_transition(name, to)
|
22
|
+
tr_key = TransitionData.get_key(name, to)
|
23
|
+
transitions[tr_key] ||= TransitionData.new name, to
|
24
|
+
end
|
25
|
+
|
26
|
+
def add_covered_transition(name, to)
|
27
|
+
add_transition(name, to).hit
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_action(name)
|
31
|
+
actions[name] ||= ActionData.new name
|
32
|
+
end
|
33
|
+
|
34
|
+
def add_covered_action(name)
|
35
|
+
add_action(name).hit
|
36
|
+
end
|
37
|
+
|
38
|
+
def add_check(name)
|
39
|
+
checks[name] ||= CheckData.new name
|
40
|
+
end
|
41
|
+
|
42
|
+
def add_covered_check(name)
|
43
|
+
add_check(name).hit
|
44
|
+
end
|
45
|
+
|
46
|
+
def add_element(name)
|
47
|
+
elements[name] ||= ElementData.new name
|
48
|
+
end
|
49
|
+
|
50
|
+
def add_covered_element(name)
|
51
|
+
add_element(name).hit
|
52
|
+
end
|
53
|
+
|
54
|
+
def report
|
55
|
+
%Q^
|
56
|
+
<h2>Screen: <span>#{@name}</span></h2>
|
57
|
+
<h3>Coverage summary</h3>
|
58
|
+
#{report_members_summary 'elements' unless elements.empty?}
|
59
|
+
#{report_members_summary 'transitions' unless transitions.empty?}
|
60
|
+
#{report_members_summary 'actions' unless actions.empty?}
|
61
|
+
#{report_members_summary 'checks' unless checks.empty?}
|
62
|
+
<h3>Coverage details</h3>
|
63
|
+
#{report_members 'elements' unless elements.empty?}
|
64
|
+
#{report_members 'transitions' unless transitions.empty?}
|
65
|
+
#{report_members 'actions' unless actions.empty?}
|
66
|
+
#{report_members 'checks' unless checks.empty?}
|
67
|
+
<hr/>
|
68
|
+
^
|
69
|
+
end
|
70
|
+
|
71
|
+
def get_coverage(members_name)
|
72
|
+
members = instance_variable_get("@#{members_name}")
|
73
|
+
uncovered = members.values.select{|e| e.hits == 0}
|
74
|
+
cov = ((members.size.to_f - uncovered.size) / members.size) * 100
|
75
|
+
return cov.nan? ? 100.0 : cov.round(2)
|
76
|
+
end
|
77
|
+
|
78
|
+
def get_count(members_name, hitted=false)
|
79
|
+
members = instance_variable_get("@#{members_name}")
|
80
|
+
if hitted
|
81
|
+
members.values.keep_if{ |e| 0 < e.hits}.size
|
82
|
+
else
|
83
|
+
members.size
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
def report_members(members_name)
|
89
|
+
members = instance_variable_get("@#{members_name}")
|
90
|
+
%Q^
|
91
|
+
<table class='covtable' width='25%'>
|
92
|
+
<thead><caption>#{members_name.capitalize}:</caption></thead>
|
93
|
+
<tbody>
|
94
|
+
<tr><th>Name</th><th>Hits</th></tr>
|
95
|
+
#{members.values.map{|e| "<tr><td class='namecol'>#{e.display_name}</td><td>#{e.hits}</td></tr>"}.join("\n") }
|
96
|
+
</tbody>
|
97
|
+
</table>
|
98
|
+
<br/>
|
99
|
+
^
|
100
|
+
end
|
101
|
+
|
102
|
+
def report_members_summary(members_name)
|
103
|
+
coverage = get_coverage members_name
|
104
|
+
%Q^
|
105
|
+
<div>#{members_name.capitalize}: #{coverage}%</div>
|
106
|
+
^
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|