simplecov 0.17.1 → 0.21.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/CHANGELOG.md +106 -432
- data/README.md +375 -93
- data/doc/alternate-formatters.md +16 -1
- data/doc/commercial-services.md +5 -0
- data/lib/minitest/simplecov_plugin.rb +15 -0
- data/lib/simplecov.rb +294 -128
- data/lib/simplecov/combine.rb +30 -0
- data/lib/simplecov/combine/branches_combiner.rb +32 -0
- data/lib/simplecov/combine/files_combiner.rb +24 -0
- data/lib/simplecov/combine/lines_combiner.rb +43 -0
- data/lib/simplecov/combine/results_combiner.rb +60 -0
- data/lib/simplecov/command_guesser.rb +6 -3
- data/lib/simplecov/configuration.rb +191 -15
- data/lib/simplecov/coverage_statistics.rb +56 -0
- data/lib/simplecov/default_formatter.rb +20 -0
- data/lib/simplecov/defaults.rb +14 -13
- data/lib/simplecov/exit_codes.rb +5 -0
- data/lib/simplecov/exit_codes/exit_code_handling.rb +29 -0
- data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +83 -0
- data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +54 -0
- data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +53 -0
- data/lib/simplecov/file_list.rb +72 -13
- data/lib/simplecov/filter.rb +9 -6
- data/lib/simplecov/formatter.rb +2 -2
- data/lib/simplecov/formatter/multi_formatter.rb +5 -7
- data/lib/simplecov/formatter/simple_formatter.rb +4 -4
- data/lib/simplecov/last_run.rb +3 -1
- data/lib/simplecov/lines_classifier.rb +5 -5
- data/lib/simplecov/no_defaults.rb +1 -1
- data/lib/simplecov/process.rb +19 -0
- data/lib/simplecov/profiles.rb +9 -7
- data/lib/simplecov/result.rb +18 -12
- data/lib/simplecov/result_adapter.rb +30 -0
- data/lib/simplecov/result_merger.rb +130 -59
- data/lib/simplecov/simulate_coverage.rb +29 -0
- data/lib/simplecov/source_file.rb +272 -126
- data/lib/simplecov/source_file/branch.rb +84 -0
- data/lib/simplecov/source_file/line.rb +72 -0
- data/lib/simplecov/useless_results_remover.rb +18 -0
- data/lib/simplecov/version.rb +1 -1
- metadata +44 -158
- data/CONTRIBUTING.md +0 -51
- data/ISSUE_TEMPLATE.md +0 -23
- data/lib/simplecov/jruby_fix.rb +0 -44
- data/lib/simplecov/railtie.rb +0 -9
- data/lib/simplecov/railties/tasks.rake +0 -13
- data/lib/simplecov/raw_coverage.rb +0 -41
data/lib/simplecov/filter.rb
CHANGED
@@ -14,11 +14,12 @@ module SimpleCov
|
|
14
14
|
#
|
15
15
|
class Filter
|
16
16
|
attr_reader :filter_argument
|
17
|
+
|
17
18
|
def initialize(filter_argument)
|
18
19
|
@filter_argument = filter_argument
|
19
20
|
end
|
20
21
|
|
21
|
-
def matches?(
|
22
|
+
def matches?(_source_file)
|
22
23
|
raise "The base filter class is not intended for direct use"
|
23
24
|
end
|
24
25
|
|
@@ -29,17 +30,19 @@ module SimpleCov
|
|
29
30
|
|
30
31
|
def self.build_filter(filter_argument)
|
31
32
|
return filter_argument if filter_argument.is_a?(SimpleCov::Filter)
|
33
|
+
|
32
34
|
class_for_argument(filter_argument).new(filter_argument)
|
33
35
|
end
|
34
36
|
|
35
37
|
def self.class_for_argument(filter_argument)
|
36
|
-
|
38
|
+
case filter_argument
|
39
|
+
when String
|
37
40
|
SimpleCov::StringFilter
|
38
|
-
|
41
|
+
when Regexp
|
39
42
|
SimpleCov::RegexFilter
|
40
|
-
|
43
|
+
when Array
|
41
44
|
SimpleCov::ArrayFilter
|
42
|
-
|
45
|
+
when Proc
|
43
46
|
SimpleCov::BlockFilter
|
44
47
|
else
|
45
48
|
raise ArgumentError, "You have provided an unrecognized filter type"
|
@@ -49,7 +52,7 @@ module SimpleCov
|
|
49
52
|
|
50
53
|
class StringFilter < SimpleCov::Filter
|
51
54
|
# Returns true when the given source file's filename matches the
|
52
|
-
# string configured when initializing this Filter with StringFilter.new('somestring)
|
55
|
+
# string configured when initializing this Filter with StringFilter.new('somestring')
|
53
56
|
def matches?(source_file)
|
54
57
|
source_file.project_filename.include?(filter_argument)
|
55
58
|
end
|
data/lib/simplecov/formatter.rb
CHANGED
@@ -6,12 +6,10 @@ module SimpleCov
|
|
6
6
|
module InstanceMethods
|
7
7
|
def format(result)
|
8
8
|
formatters.map do |formatter|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
nil
|
14
|
-
end
|
9
|
+
formatter.new.format(result)
|
10
|
+
rescue StandardError => e
|
11
|
+
warn("Formatter #{formatter} failed with #{e.class}: #{e.message} (#{e.backtrace.first})")
|
12
|
+
nil
|
15
13
|
end
|
16
14
|
end
|
17
15
|
end
|
@@ -27,7 +25,7 @@ module SimpleCov
|
|
27
25
|
|
28
26
|
def self.[](*args)
|
29
27
|
warn "#{Kernel.caller.first}: [DEPRECATION] ::[] is deprecated. Use ::new instead."
|
30
|
-
new(Array(
|
28
|
+
new(Array(args))
|
31
29
|
end
|
32
30
|
end
|
33
31
|
end
|
@@ -1,14 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
#
|
4
|
-
# A ridiculously simple formatter for SimpleCov results.
|
5
|
-
#
|
6
3
|
module SimpleCov
|
7
4
|
module Formatter
|
5
|
+
#
|
6
|
+
# A ridiculously simple formatter for SimpleCov results.
|
7
|
+
#
|
8
8
|
class SimpleFormatter
|
9
9
|
# Takes a SimpleCov::Result and generates a string out of it
|
10
10
|
def format(result)
|
11
|
-
output = ""
|
11
|
+
output = +""
|
12
12
|
result.groups.each do |name, files|
|
13
13
|
output << "Group: #{name}\n"
|
14
14
|
output << "=" * 40
|
data/lib/simplecov/last_run.rb
CHANGED
@@ -8,23 +8,23 @@ module SimpleCov
|
|
8
8
|
RELEVANT = 0
|
9
9
|
NOT_RELEVANT = nil
|
10
10
|
|
11
|
-
WHITESPACE_LINE = /^\s
|
12
|
-
COMMENT_LINE = /^\s
|
11
|
+
WHITESPACE_LINE = /^\s*$/.freeze
|
12
|
+
COMMENT_LINE = /^\s*#/.freeze
|
13
13
|
WHITESPACE_OR_COMMENT_LINE = Regexp.union(WHITESPACE_LINE, COMMENT_LINE)
|
14
14
|
|
15
15
|
def self.no_cov_line
|
16
|
-
/^(\s*)#(\s*)(
|
16
|
+
/^(\s*)#(\s*)(:#{SimpleCov.nocov_token}:)/o
|
17
17
|
end
|
18
18
|
|
19
19
|
def self.no_cov_line?(line)
|
20
|
-
line
|
20
|
+
no_cov_line.match?(line)
|
21
21
|
rescue ArgumentError
|
22
22
|
# E.g., line contains an invalid byte sequence in UTF-8
|
23
23
|
false
|
24
24
|
end
|
25
25
|
|
26
26
|
def self.whitespace_line?(line)
|
27
|
-
line
|
27
|
+
WHITESPACE_OR_COMMENT_LINE.match?(line)
|
28
28
|
rescue ArgumentError
|
29
29
|
# E.g., line contains an invalid byte sequence in UTF-8
|
30
30
|
false
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Process
|
4
|
+
class << self
|
5
|
+
def fork_with_simplecov(&block)
|
6
|
+
if defined?(SimpleCov) && SimpleCov.running
|
7
|
+
fork_without_simplecov do
|
8
|
+
SimpleCov.at_fork.call(Process.pid)
|
9
|
+
block.call if block_given?
|
10
|
+
end
|
11
|
+
else
|
12
|
+
fork_without_simplecov(&block)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
alias fork_without_simplecov fork
|
17
|
+
alias fork fork_with_simplecov
|
18
|
+
end
|
19
|
+
end
|
data/lib/simplecov/profiles.rb
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
#
|
4
|
-
# Profiles are SimpleCov configuration procs that can be easily
|
5
|
-
# loaded using SimpleCov.start :rails and defined using
|
6
|
-
# SimpleCov.profiles.define :foo do
|
7
|
-
# # SimpleCov configuration here, same as in SimpleCov.configure
|
8
|
-
# end
|
9
|
-
#
|
10
3
|
module SimpleCov
|
4
|
+
#
|
5
|
+
# Profiles are SimpleCov configuration procs that can be easily
|
6
|
+
# loaded using SimpleCov.start :rails and defined using
|
7
|
+
# SimpleCov.profiles.define :foo do
|
8
|
+
# # SimpleCov configuration here, same as in SimpleCov.configure
|
9
|
+
# end
|
10
|
+
#
|
11
11
|
class Profiles < Hash
|
12
12
|
#
|
13
13
|
# Define a SimpleCov profile:
|
@@ -18,6 +18,7 @@ module SimpleCov
|
|
18
18
|
def define(name, &blk)
|
19
19
|
name = name.to_sym
|
20
20
|
raise "SimpleCov Profile '#{name}' is already defined" unless self[name].nil?
|
21
|
+
|
21
22
|
self[name] = blk
|
22
23
|
end
|
23
24
|
|
@@ -27,6 +28,7 @@ module SimpleCov
|
|
27
28
|
def load(name)
|
28
29
|
name = name.to_sym
|
29
30
|
raise "Could not find SimpleCov Profile called '#{name}'" unless key?(name)
|
31
|
+
|
30
32
|
SimpleCov.configure(&self[name])
|
31
33
|
end
|
32
34
|
end
|
data/lib/simplecov/result.rb
CHANGED
@@ -5,7 +5,7 @@ require "forwardable"
|
|
5
5
|
|
6
6
|
module SimpleCov
|
7
7
|
#
|
8
|
-
# A simplecov code coverage result, initialized from the Hash Ruby
|
8
|
+
# A simplecov code coverage result, initialized from the Hash Ruby's built-in coverage
|
9
9
|
# library generates (Coverage.result).
|
10
10
|
#
|
11
11
|
class Result
|
@@ -20,15 +20,18 @@ module SimpleCov
|
|
20
20
|
# Explicitly set the command name that was used for this coverage result. Defaults to SimpleCov.command_name
|
21
21
|
attr_writer :command_name
|
22
22
|
|
23
|
-
def_delegators :files, :covered_percent, :covered_percentages, :least_covered_file, :covered_strength, :covered_lines, :missed_lines
|
23
|
+
def_delegators :files, :covered_percent, :covered_percentages, :least_covered_file, :covered_strength, :covered_lines, :missed_lines, :total_branches, :covered_branches, :missed_branches, :coverage_statistics, :coverage_statistics_by_file
|
24
24
|
def_delegator :files, :lines_of_code, :total_lines
|
25
25
|
|
26
26
|
# Initialize a new SimpleCov::Result from given Coverage.result (a Hash of filenames each containing an array of
|
27
27
|
# coverage data)
|
28
|
-
def initialize(original_result)
|
29
|
-
|
30
|
-
@
|
31
|
-
|
28
|
+
def initialize(original_result, command_name: nil, created_at: nil)
|
29
|
+
result = original_result
|
30
|
+
@original_result = result.freeze
|
31
|
+
@command_name = command_name
|
32
|
+
@created_at = created_at
|
33
|
+
@files = SimpleCov::FileList.new(result.map do |filename, coverage|
|
34
|
+
SimpleCov::SourceFile.new(filename, JSON.parse(JSON.dump(coverage))) if File.file?(filename)
|
32
35
|
end.compact.sort_by(&:filename))
|
33
36
|
filter!
|
34
37
|
end
|
@@ -61,16 +64,19 @@ module SimpleCov
|
|
61
64
|
|
62
65
|
# Returns a hash representation of this Result that can be used for marshalling it into JSON
|
63
66
|
def to_hash
|
64
|
-
{
|
67
|
+
{
|
68
|
+
command_name => {
|
69
|
+
"coverage" => coverage,
|
70
|
+
"timestamp" => created_at.to_i
|
71
|
+
}
|
72
|
+
}
|
65
73
|
end
|
66
74
|
|
67
75
|
# Loads a SimpleCov::Result#to_hash dump
|
68
76
|
def self.from_hash(hash)
|
69
|
-
command_name, data
|
70
|
-
|
71
|
-
|
72
|
-
result.created_at = Time.at(data["timestamp"])
|
73
|
-
result
|
77
|
+
hash.map do |command_name, data|
|
78
|
+
new(data.fetch("coverage"), command_name: command_name, created_at: Time.at(data["timestamp"]))
|
79
|
+
end
|
74
80
|
end
|
75
81
|
|
76
82
|
private
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SimpleCov
|
4
|
+
#
|
5
|
+
# Responsible for adapting the format of the coverage result whether it's default or with statistics
|
6
|
+
#
|
7
|
+
class ResultAdapter
|
8
|
+
attr_reader :result
|
9
|
+
|
10
|
+
def initialize(result)
|
11
|
+
@result = result
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.call(*args)
|
15
|
+
new(*args).adapt
|
16
|
+
end
|
17
|
+
|
18
|
+
def adapt
|
19
|
+
return unless result
|
20
|
+
|
21
|
+
result.each_with_object({}) do |(file_name, cover_statistic), adapted_result|
|
22
|
+
if cover_statistic.is_a?(Array)
|
23
|
+
adapted_result.merge!(file_name => {"lines" => cover_statistic})
|
24
|
+
else
|
25
|
+
adapted_result.merge!(file_name => cover_statistic)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -2,12 +2,12 @@
|
|
2
2
|
|
3
3
|
require "json"
|
4
4
|
|
5
|
-
#
|
6
|
-
# Singleton that is responsible for caching, loading and merging
|
7
|
-
# SimpleCov::Results into a single result for coverage analysis based
|
8
|
-
# upon multiple test suites.
|
9
|
-
#
|
10
5
|
module SimpleCov
|
6
|
+
#
|
7
|
+
# Singleton that is responsible for caching, loading and merging
|
8
|
+
# SimpleCov::Results into a single result for coverage analysis based
|
9
|
+
# upon multiple test suites.
|
10
|
+
#
|
11
11
|
module ResultMerger
|
12
12
|
class << self
|
13
13
|
# The path to the .resultset.json cache file
|
@@ -19,79 +19,130 @@ module SimpleCov
|
|
19
19
|
File.join(SimpleCov.coverage_path, ".resultset.json.lock")
|
20
20
|
end
|
21
21
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
data = stored_data
|
27
|
-
if data
|
28
|
-
begin
|
29
|
-
JSON.parse(data) || {}
|
30
|
-
rescue
|
31
|
-
{}
|
32
|
-
end
|
33
|
-
else
|
34
|
-
{}
|
35
|
-
end
|
36
|
-
end
|
22
|
+
def merge_and_store(*file_paths, ignore_timeout: false)
|
23
|
+
result = merge_results(*file_paths, ignore_timeout: ignore_timeout)
|
24
|
+
store_result(result) if result
|
25
|
+
result
|
37
26
|
end
|
38
27
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
28
|
+
def merge_results(*file_paths, ignore_timeout: false)
|
29
|
+
# It is intentional here that files are only read in and parsed one at a time.
|
30
|
+
#
|
31
|
+
# In big CI setups you might deal with 100s of CI jobs and each one producing Megabytes
|
32
|
+
# of data. Reading them all in easily produces Gigabytes of memory consumption which
|
33
|
+
# we want to avoid.
|
34
|
+
#
|
35
|
+
# For similar reasons a SimpleCov::Result is only created in the end as that'd create
|
36
|
+
# even more data especially when it also reads in all source files.
|
37
|
+
initial_memo = valid_results(file_paths.shift, ignore_timeout: ignore_timeout)
|
38
|
+
|
39
|
+
command_names, coverage = file_paths.reduce(initial_memo) do |memo, file_path|
|
40
|
+
merge_coverage(memo, valid_results(file_path, ignore_timeout: ignore_timeout))
|
46
41
|
end
|
42
|
+
|
43
|
+
create_result(command_names, coverage)
|
47
44
|
end
|
48
45
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
46
|
+
def valid_results(file_path, ignore_timeout: false)
|
47
|
+
results = parse_file(file_path)
|
48
|
+
merge_valid_results(results, ignore_timeout: ignore_timeout)
|
49
|
+
end
|
50
|
+
|
51
|
+
def parse_file(path)
|
52
|
+
data = read_file(path)
|
53
|
+
parse_json(data)
|
54
|
+
end
|
55
|
+
|
56
|
+
def read_file(path)
|
57
|
+
return unless File.exist?(path)
|
58
|
+
|
59
|
+
data = File.read(path)
|
60
|
+
return if data.nil? || data.length < 2
|
61
|
+
|
62
|
+
data
|
63
|
+
end
|
64
|
+
|
65
|
+
def parse_json(content)
|
66
|
+
return {} unless content
|
67
|
+
|
68
|
+
JSON.parse(content) || {}
|
69
|
+
rescue StandardError
|
70
|
+
warn "[SimpleCov]: Warning! Parsing JSON content of resultset file failed"
|
71
|
+
{}
|
72
|
+
end
|
73
|
+
|
74
|
+
def merge_valid_results(results, ignore_timeout: false)
|
75
|
+
results = results.select { |_command_name, data| within_merge_timeout?(data) } unless ignore_timeout
|
76
|
+
|
77
|
+
command_plus_coverage = results.map do |command_name, data|
|
78
|
+
[[command_name], adapt_result(data.fetch("coverage"))]
|
61
79
|
end
|
62
|
-
|
80
|
+
|
81
|
+
# one file itself _might_ include multiple test runs
|
82
|
+
merge_coverage(*command_plus_coverage)
|
63
83
|
end
|
64
84
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
85
|
+
def within_merge_timeout?(data)
|
86
|
+
time_since_result_creation(data) < SimpleCov.merge_timeout
|
87
|
+
end
|
88
|
+
|
89
|
+
def time_since_result_creation(data)
|
90
|
+
Time.now - Time.at(data.fetch("timestamp"))
|
91
|
+
end
|
92
|
+
|
93
|
+
def create_result(command_names, coverage)
|
94
|
+
return nil unless coverage
|
95
|
+
|
96
|
+
command_name = command_names.reject(&:empty?).sort.join(", ")
|
97
|
+
SimpleCov::Result.new(coverage, command_name: command_name)
|
98
|
+
end
|
99
|
+
|
100
|
+
def merge_coverage(*results)
|
101
|
+
return [[""], nil] if results.empty?
|
102
|
+
return results.first if results.size == 1
|
103
|
+
|
104
|
+
results.reduce do |(memo_command, memo_coverage), (command, coverage)|
|
105
|
+
# timestamp is dropped here, which is intentional (we merge it, it gets a new time stamp as of now)
|
106
|
+
merged_coverage = Combine.combine(Combine::ResultsCombiner, memo_coverage, coverage)
|
107
|
+
merged_command = memo_command + command
|
108
|
+
|
109
|
+
[merged_command, merged_coverage]
|
110
|
+
end
|
74
111
|
end
|
75
112
|
|
76
113
|
#
|
77
|
-
# Gets all SimpleCov::Results
|
114
|
+
# Gets all SimpleCov::Results stored in resultset, merges them and produces a new
|
78
115
|
# SimpleCov::Result with merged coverage data and the command_name
|
79
116
|
# for the result consisting of a join on all source result's names
|
80
|
-
#
|
81
117
|
def merged_result
|
82
|
-
merge_results(
|
118
|
+
# conceptually this is just doing `merge_results(resultset_path)`
|
119
|
+
# it's more involved to make syre `synchronize_resultset` is only used around reading
|
120
|
+
resultset_hash = read_resultset
|
121
|
+
command_names, coverage = merge_valid_results(resultset_hash)
|
122
|
+
|
123
|
+
create_result(command_names, coverage)
|
124
|
+
end
|
125
|
+
|
126
|
+
def read_resultset
|
127
|
+
resultset_content =
|
128
|
+
synchronize_resultset do
|
129
|
+
read_file(resultset_path)
|
130
|
+
end
|
131
|
+
|
132
|
+
parse_json(resultset_content)
|
83
133
|
end
|
84
134
|
|
85
135
|
# Saves the given SimpleCov::Result in the resultset cache
|
86
136
|
def store_result(result)
|
87
137
|
synchronize_resultset do
|
88
138
|
# Ensure we have the latest, in case it was already cached
|
89
|
-
|
90
|
-
|
139
|
+
new_resultset = read_resultset
|
140
|
+
|
141
|
+
# A single result only ever has one command_name, see `SimpleCov::Result#to_hash`
|
91
142
|
command_name, data = result.to_hash.first
|
92
|
-
|
143
|
+
new_resultset[command_name] = data
|
93
144
|
File.open(resultset_path, "w+") do |f_|
|
94
|
-
f_.puts JSON.pretty_generate(
|
145
|
+
f_.puts JSON.pretty_generate(new_resultset)
|
95
146
|
end
|
96
147
|
end
|
97
148
|
true
|
@@ -114,9 +165,29 @@ module SimpleCov
|
|
114
165
|
end
|
115
166
|
end
|
116
167
|
|
117
|
-
#
|
118
|
-
|
119
|
-
|
168
|
+
# We changed the format of the raw result data in simplecov, as people are likely
|
169
|
+
# to have "old" resultsets lying around (but not too old so that they're still
|
170
|
+
# considered we can adapt them).
|
171
|
+
# See https://github.com/simplecov-ruby/simplecov/pull/824#issuecomment-576049747
|
172
|
+
def adapt_result(result)
|
173
|
+
if pre_simplecov_0_18_result?(result)
|
174
|
+
adapt_pre_simplecov_0_18_result(result)
|
175
|
+
else
|
176
|
+
result
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# pre 0.18 coverage data pointed from file directly to an array of line coverage
|
181
|
+
def pre_simplecov_0_18_result?(result)
|
182
|
+
_key, data = result.first
|
183
|
+
|
184
|
+
data.is_a?(Array)
|
185
|
+
end
|
186
|
+
|
187
|
+
def adapt_pre_simplecov_0_18_result(result)
|
188
|
+
result.transform_values do |line_coverage_data|
|
189
|
+
{"lines" => line_coverage_data}
|
190
|
+
end
|
120
191
|
end
|
121
192
|
end
|
122
193
|
end
|