guard-cucumber 1.4.1 → 1.5.1
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 +3 -3
- data/lib/guard/cucumber.rb +61 -44
- data/lib/guard/cucumber.rb.orig +143 -0
- data/lib/guard/cucumber/focuser.rb +14 -13
- data/lib/guard/cucumber/focuser.rb.orig +81 -0
- data/lib/guard/cucumber/inspector.rb +19 -9
- data/lib/guard/cucumber/inspector.rb.orig +77 -0
- data/lib/guard/cucumber/notification_formatter.rb +30 -26
- data/lib/guard/cucumber/notification_formatter.rb.orig +124 -0
- data/lib/guard/cucumber/runner.rb +50 -23
- data/lib/guard/cucumber/runner.rb.orig +86 -0
- data/lib/guard/cucumber/templates/Guardfile +6 -3
- data/lib/guard/cucumber/templates/Guardfile.orig +5 -0
- data/lib/guard/cucumber/version.rb +1 -1
- data/lib/guard/cucumber/version.rb.orig +6 -0
- metadata +18 -12
- data/lib/guard/cucumber/version.rbc +0 -203
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 446cce06cfc0d77430a6b6c7e3ce31b76325ca71
|
4
|
+
data.tar.gz: 9544582765bbb7025dfadb2a88bd3b8114e6ed45
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6ab8153c6cdcd1090baca18e1d1ef3a35ee609b6459fcd7c0ada3b6828d0a44963b5af0de79268641b29e11901ea3cb3f68a2130325d6bdbe13f8123e36f3025
|
7
|
+
data.tar.gz: e7cb7a1c69c3b507eb65f068ee642a778cd6ba3c1d92c1c0f7944e0f3974c3334126ede5c758c2182d61822d1d237fbba9c66d9f37e28c0c361ab7620b1978e4
|
data/README.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
Guard::Cucumber allows you to automatically run Cucumber features when files are modified.
|
4
4
|
|
5
|
-
Tested on MRI Ruby 1.
|
5
|
+
Tested on MRI Ruby 1.9.3, 2.0.0, 2.1.0 and the latest versions of JRuby.
|
6
6
|
|
7
7
|
If you have any questions please join us on our [Google group](http://groups.google.com/group/guard-dev) or on `#guard` (irc.freenode.net).
|
8
8
|
|
@@ -131,7 +131,7 @@ Since guard-cucumber version 0.3.2, the default `:cli` options are:
|
|
131
131
|
:cli => '--no-profile --color --format progress --strict'
|
132
132
|
```
|
133
133
|
|
134
|
-
This default configuration has been chosen to avoid strange behavior when mixing configurations
|
134
|
+
This default configuration has been chosen to avoid strange behavior when mixing configurations from
|
135
135
|
the cucumber.yml default profile with the guard-cucumber `:cli` option.
|
136
136
|
|
137
137
|
You can safely remove `config/cucumber.yml`, since all configuration is done in the `Guardfile`.
|
@@ -226,7 +226,7 @@ For questions please join us in our [Google group](http://groups.google.com/grou
|
|
226
226
|
|
227
227
|
## Author
|
228
228
|
|
229
|
-
Developed by Michael Kessler, sponsored by [
|
229
|
+
Developed by Michael Kessler, sponsored by [FlinkFinger](http://www.flinkfinger.com).
|
230
230
|
|
231
231
|
If you like Guard::Cucumber, you can watch the repository at [GitHub](https://github.com/netzpirat/guard-cucumber)
|
232
232
|
and follow [@netzpirat](https://twitter.com/#!/netzpirat) on Twitter for project updates.
|
data/lib/guard/cucumber.rb
CHANGED
@@ -1,18 +1,16 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
1
|
+
require "guard"
|
2
|
+
require "guard/plugin"
|
3
|
+
require "cucumber"
|
4
|
+
require "guard/cucumber/version"
|
5
5
|
|
6
6
|
module Guard
|
7
|
-
|
8
7
|
# The Cucumber guard that gets notifications about the following
|
9
8
|
# Guard events: `start`, `stop`, `reload`, `run_all` and `run_on_change`.
|
10
9
|
#
|
11
|
-
class Cucumber <
|
12
|
-
|
13
|
-
autoload :
|
14
|
-
autoload :
|
15
|
-
autoload :Focuser, 'guard/cucumber/focuser'
|
10
|
+
class Cucumber < Plugin
|
11
|
+
autoload :Runner, "guard/cucumber/runner"
|
12
|
+
autoload :Inspector, "guard/cucumber/inspector"
|
13
|
+
autoload :Focuser, "guard/cucumber/focuser"
|
16
14
|
|
17
15
|
attr_accessor :last_failed, :failed_path
|
18
16
|
|
@@ -21,25 +19,31 @@ module Guard
|
|
21
19
|
# @param [Array<Guard::Watcher>] watchers the watchers in the Guard block
|
22
20
|
# @param [Hash] options the options for the Guard
|
23
21
|
# @option options [String] :cli any arbitrary Cucumber CLI arguments
|
24
|
-
# @option options [Array<String>] :feature_sets a list of non-standard
|
22
|
+
# @option options [Array<String>] :feature_sets a list of non-standard
|
23
|
+
# feature directory/ies
|
25
24
|
# @option options [Boolean] :bundler use bundler or not
|
26
|
-
# @option options [Array<String>] :rvm a list of rvm version to use for the
|
25
|
+
# @option options [Array<String>] :rvm a list of rvm version to use for the
|
26
|
+
# test
|
27
27
|
# @option options [Boolean] :notification show notifications
|
28
|
-
# @option options [Boolean] :all_after_pass run all features after changed
|
28
|
+
# @option options [Boolean] :all_after_pass run all features after changed
|
29
|
+
# features pass
|
29
30
|
# @option options [Boolean] :all_on_start run all the features at startup
|
30
|
-
# @option options [Boolean] :keep_failed Keep failed features until they
|
31
|
-
#
|
32
|
-
# @option options [Boolean] :
|
31
|
+
# @option options [Boolean] :keep_failed Keep failed features until they
|
32
|
+
# pass
|
33
|
+
# @option options [Boolean] :run_all run override any option when running
|
34
|
+
# all specs
|
35
|
+
# @option options [Boolean] :change_format use a different cucumber format
|
36
|
+
# when running individual features
|
33
37
|
#
|
34
|
-
def initialize(
|
35
|
-
super
|
38
|
+
def initialize(options = {})
|
39
|
+
super(options)
|
36
40
|
|
37
41
|
@options = {
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
42
|
+
all_after_pass: true,
|
43
|
+
all_on_start: true,
|
44
|
+
keep_failed: true,
|
45
|
+
cli: "--no-profile --color --format progress --strict",
|
46
|
+
feature_sets: ["features"]
|
43
47
|
}.update(options)
|
44
48
|
|
45
49
|
@last_failed = false
|
@@ -59,7 +63,9 @@ module Guard
|
|
59
63
|
# @raise [:task_has_failed] when stop has failed
|
60
64
|
#
|
61
65
|
def run_all
|
62
|
-
|
66
|
+
opts = options.merge(options[:run_all] || {})
|
67
|
+
opts.merge!(message: "Running all features")
|
68
|
+
passed = Runner.run(options[:feature_sets], opts)
|
63
69
|
|
64
70
|
if passed
|
65
71
|
@failed_paths = []
|
@@ -87,23 +93,17 @@ module Guard
|
|
87
93
|
#
|
88
94
|
def run_on_modifications(paths)
|
89
95
|
paths += @failed_paths if @options[:keep_failed]
|
90
|
-
paths
|
91
|
-
options = @options[:change_format] ? change_format(@options[:change_format]) : @options
|
92
|
-
passed = Runner.run(paths, paths.include?('features') ? options.merge({ :message => 'Running all features' }) : options)
|
96
|
+
paths = Inspector.clean(paths, options[:feature_sets])
|
93
97
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
# run all the specs if the changed specs failed, like autotest
|
98
|
-
run_all if @last_failed && @options[:all_after_pass]
|
99
|
-
else
|
100
|
-
# remember failed paths for the next change
|
101
|
-
@failed_paths += read_failed_features if @options[:keep_failed]
|
102
|
-
# track whether the changed feature failed for the next change
|
103
|
-
@last_failed = true
|
98
|
+
options = @options
|
99
|
+
if @options[:change_format]
|
100
|
+
options = change_format(@options[:change_format])
|
104
101
|
end
|
105
102
|
|
106
|
-
|
103
|
+
msg = "Running all features"
|
104
|
+
options.merge!(message: msg) if paths.include?("features")
|
105
|
+
|
106
|
+
_run(paths, options)
|
107
107
|
end
|
108
108
|
|
109
109
|
private
|
@@ -116,9 +116,9 @@ module Guard
|
|
116
116
|
def read_failed_features
|
117
117
|
failed = []
|
118
118
|
|
119
|
-
if File.exist?(
|
120
|
-
failed = File.open(
|
121
|
-
File.delete(
|
119
|
+
if File.exist?("rerun.txt")
|
120
|
+
failed = File.open("rerun.txt") { |file| file.read.split(" ") }
|
121
|
+
File.delete("rerun.txt")
|
122
122
|
end
|
123
123
|
|
124
124
|
failed
|
@@ -130,14 +130,31 @@ module Guard
|
|
130
130
|
# @return [Hash] the new options
|
131
131
|
#
|
132
132
|
def change_format(format)
|
133
|
-
cli_parts = @options[:cli].split(
|
133
|
+
cli_parts = @options[:cli].split(" ")
|
134
134
|
cli_parts.each_with_index do |part, index|
|
135
|
-
if part ==
|
135
|
+
if part == "--format" && cli_parts[index + 2] != "--out"
|
136
136
|
cli_parts[index + 1] = format
|
137
137
|
end
|
138
138
|
end
|
139
|
-
@options.merge(:
|
139
|
+
@options.merge(cli: cli_parts.join(" "))
|
140
140
|
end
|
141
141
|
|
142
|
+
private
|
143
|
+
|
144
|
+
def _run(paths, options)
|
145
|
+
if Runner.run(paths, options)
|
146
|
+
# clean failed paths memory
|
147
|
+
@failed_paths -= paths if @options[:keep_failed]
|
148
|
+
# run all the specs if the changed specs failed, like autotest
|
149
|
+
run_all if @last_failed && @options[:all_after_pass]
|
150
|
+
return
|
151
|
+
end
|
152
|
+
|
153
|
+
# remember failed paths for the next change
|
154
|
+
@failed_paths += read_failed_features if @options[:keep_failed]
|
155
|
+
# track whether the changed feature failed for the next change
|
156
|
+
@last_failed = true
|
157
|
+
throw :task_has_failed
|
158
|
+
end
|
142
159
|
end
|
143
160
|
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
require 'guard'
|
2
|
+
require 'guard/guard'
|
3
|
+
require 'cucumber'
|
4
|
+
require 'guard/cucumber/version'
|
5
|
+
|
6
|
+
module Guard
|
7
|
+
|
8
|
+
# The Cucumber guard that gets notifications about the following
|
9
|
+
# Guard events: `start`, `stop`, `reload`, `run_all` and `run_on_change`.
|
10
|
+
#
|
11
|
+
class Cucumber < Guard
|
12
|
+
|
13
|
+
autoload :Runner, 'guard/cucumber/runner'
|
14
|
+
autoload :Inspector, 'guard/cucumber/inspector'
|
15
|
+
autoload :Focuser, 'guard/cucumber/focuser'
|
16
|
+
|
17
|
+
attr_accessor :last_failed, :failed_path
|
18
|
+
|
19
|
+
# Initialize Guard::Cucumber.
|
20
|
+
#
|
21
|
+
# @param [Array<Guard::Watcher>] watchers the watchers in the Guard block
|
22
|
+
# @param [Hash] options the options for the Guard
|
23
|
+
# @option options [String] :cli any arbitrary Cucumber CLI arguments
|
24
|
+
# @option options [Array<String>] :feature_sets a list of non-standard feature directory/ies
|
25
|
+
# @option options [Boolean] :bundler use bundler or not
|
26
|
+
# @option options [Array<String>] :rvm a list of rvm version to use for the test
|
27
|
+
# @option options [Boolean] :notification show notifications
|
28
|
+
# @option options [Boolean] :all_after_pass run all features after changed features pass
|
29
|
+
# @option options [Boolean] :all_on_start run all the features at startup
|
30
|
+
# @option options [Boolean] :keep_failed Keep failed features until they pass
|
31
|
+
# @option options [Boolean] :run_all run override any option when running all specs
|
32
|
+
# @option options [Boolean] :change_format use a different cucumber format when running individual features
|
33
|
+
#
|
34
|
+
def initialize(watchers = [], options = { })
|
35
|
+
super
|
36
|
+
|
37
|
+
@options = {
|
38
|
+
:all_after_pass => true,
|
39
|
+
:all_on_start => true,
|
40
|
+
:keep_failed => true,
|
41
|
+
:cli => '--no-profile --color --format progress --strict',
|
42
|
+
:feature_sets => ['features']
|
43
|
+
}.update(options)
|
44
|
+
|
45
|
+
@last_failed = false
|
46
|
+
@failed_paths = []
|
47
|
+
end
|
48
|
+
|
49
|
+
# Gets called once when Guard starts.
|
50
|
+
#
|
51
|
+
# @raise [:task_has_failed] when stop has failed
|
52
|
+
#
|
53
|
+
def start
|
54
|
+
run_all if @options[:all_on_start]
|
55
|
+
end
|
56
|
+
|
57
|
+
# Gets called when all specs should be run.
|
58
|
+
#
|
59
|
+
# @raise [:task_has_failed] when stop has failed
|
60
|
+
#
|
61
|
+
def run_all
|
62
|
+
passed = Runner.run(options[:feature_sets], options.merge(options[:run_all] || { }).merge(:message => 'Running all features'))
|
63
|
+
|
64
|
+
if passed
|
65
|
+
@failed_paths = []
|
66
|
+
else
|
67
|
+
@failed_paths = read_failed_features if @options[:keep_failed]
|
68
|
+
end
|
69
|
+
|
70
|
+
@last_failed = !passed
|
71
|
+
|
72
|
+
throw :task_has_failed unless passed
|
73
|
+
end
|
74
|
+
|
75
|
+
# Gets called when the Guard should reload itself.
|
76
|
+
#
|
77
|
+
# @raise [:task_has_failed] when stop has failed
|
78
|
+
#
|
79
|
+
def reload
|
80
|
+
@failed_paths = []
|
81
|
+
end
|
82
|
+
|
83
|
+
# Gets called when watched paths and files have changes.
|
84
|
+
#
|
85
|
+
# @param [Array<String>] paths the changed paths and files
|
86
|
+
# @raise [:task_has_failed] when stop has failed
|
87
|
+
#
|
88
|
+
def run_on_modifications(paths)
|
89
|
+
paths += @failed_paths if @options[:keep_failed]
|
90
|
+
paths = Inspector.clean(paths, options[:feature_sets])
|
91
|
+
options = @options[:change_format] ? change_format(@options[:change_format]) : @options
|
92
|
+
passed = Runner.run(paths, paths.include?('features') ? options.merge({ :message => 'Running all features' }) : options)
|
93
|
+
|
94
|
+
if passed
|
95
|
+
# clean failed paths memory
|
96
|
+
@failed_paths -= paths if @options[:keep_failed]
|
97
|
+
# run all the specs if the changed specs failed, like autotest
|
98
|
+
run_all if @last_failed && @options[:all_after_pass]
|
99
|
+
else
|
100
|
+
# remember failed paths for the next change
|
101
|
+
@failed_paths += read_failed_features if @options[:keep_failed]
|
102
|
+
# track whether the changed feature failed for the next change
|
103
|
+
@last_failed = true
|
104
|
+
end
|
105
|
+
|
106
|
+
throw :task_has_failed unless passed
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
# Read the failed features that from `rerun.txt`
|
112
|
+
#
|
113
|
+
# @see Guard::Cucumber::NotificationFormatter#write_rerun_features
|
114
|
+
# @return [Array<String>] the list of features
|
115
|
+
#
|
116
|
+
def read_failed_features
|
117
|
+
failed = []
|
118
|
+
|
119
|
+
if File.exist?('rerun.txt')
|
120
|
+
failed = File.open('rerun.txt') { |file| file.read.split(' ') }
|
121
|
+
File.delete('rerun.txt')
|
122
|
+
end
|
123
|
+
|
124
|
+
failed
|
125
|
+
end
|
126
|
+
|
127
|
+
# Change the `--format` cli option.
|
128
|
+
#
|
129
|
+
# @param [String] format the new format
|
130
|
+
# @return [Hash] the new options
|
131
|
+
#
|
132
|
+
def change_format(format)
|
133
|
+
cli_parts = @options[:cli].split(' ')
|
134
|
+
cli_parts.each_with_index do |part, index|
|
135
|
+
if part == '--format' && cli_parts[index + 2] != '--out'
|
136
|
+
cli_parts[index + 1] = format
|
137
|
+
end
|
138
|
+
end
|
139
|
+
@options.merge(:cli => cli_parts.join(' '))
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
143
|
+
end
|
@@ -1,6 +1,5 @@
|
|
1
1
|
module Guard
|
2
2
|
class Cucumber
|
3
|
-
|
4
3
|
# The Cucumber focuser updates cucumber feature paths to
|
5
4
|
# focus on sections tagged with a provided focus_tag.
|
6
5
|
#
|
@@ -18,7 +17,6 @@ module Guard
|
|
18
17
|
#
|
19
18
|
module Focuser
|
20
19
|
class << self
|
21
|
-
|
22
20
|
# Focus the supplied paths using the provided focus tag.
|
23
21
|
#
|
24
22
|
# @param [Array<String>] paths the locations of the feature files
|
@@ -31,12 +29,12 @@ module Guard
|
|
31
29
|
paths.inject([]) do |updated_paths, path|
|
32
30
|
focused_line_numbers = scan_path_for_focus_tag(path, focus_tag)
|
33
31
|
|
34
|
-
|
32
|
+
if focused_line_numbers.empty?
|
33
|
+
updated_paths << path
|
34
|
+
else
|
35
35
|
updated_paths << append_line_numbers_to_path(
|
36
36
|
focused_line_numbers, path
|
37
37
|
)
|
38
|
-
else
|
39
|
-
updated_paths << path
|
40
38
|
end
|
41
39
|
|
42
40
|
updated_paths
|
@@ -48,14 +46,15 @@ module Guard
|
|
48
46
|
#
|
49
47
|
# @param [String] path the file path to search
|
50
48
|
# @param [String] focus_tag the focus tag to look for in each path
|
51
|
-
# @return [Array<Integer>] the line numbers that include the focus tag
|
49
|
+
# @return [Array<Integer>] the line numbers that include the focus tag
|
50
|
+
# in path
|
52
51
|
#
|
53
52
|
def scan_path_for_focus_tag(path, focus_tag)
|
54
|
-
return [] if File.directory?(path) || path.include?(
|
53
|
+
return [] if File.directory?(path) || path.include?(":")
|
55
54
|
|
56
55
|
line_numbers = []
|
57
56
|
|
58
|
-
File.open(path,
|
57
|
+
File.open(path, "r") do |file|
|
59
58
|
while (line = file.gets)
|
60
59
|
if line.include?(focus_tag)
|
61
60
|
line_numbers << file.lineno
|
@@ -68,16 +67,18 @@ module Guard
|
|
68
67
|
|
69
68
|
# Appends the line numbers to the path
|
70
69
|
#
|
71
|
-
# @param [Array<Integer>] line_numbers the line numbers to append to
|
72
|
-
#
|
73
|
-
# @
|
70
|
+
# @param [Array<Integer>] line_numbers the line numbers to append to
|
71
|
+
# the path
|
72
|
+
# @param [String] path the path that will receive the appended line
|
73
|
+
# numbers
|
74
|
+
# @return [String] the string containing the path appended with the
|
75
|
+
# line number
|
74
76
|
#
|
75
77
|
def append_line_numbers_to_path(line_numbers, path)
|
76
|
-
line_numbers.each { |num| path +=
|
78
|
+
line_numbers.each { |num| path += ":" + num.to_s }
|
77
79
|
|
78
80
|
path
|
79
81
|
end
|
80
|
-
|
81
82
|
end
|
82
83
|
end
|
83
84
|
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Guard
|
2
|
+
class Cucumber
|
3
|
+
# The Cucumber focuser updates cucumber feature paths to
|
4
|
+
# focus on sections tagged with a provided focus_tag.
|
5
|
+
#
|
6
|
+
# For example, if the `foo.feature` file has the provided focus tag
|
7
|
+
# `@bar` on line 8, then the path will be updated using the cucumber
|
8
|
+
# syntax for focusing on a section:
|
9
|
+
#
|
10
|
+
# foo.feature:8
|
11
|
+
#
|
12
|
+
# If '@bar' is found on lines 8 and 16, the path is updated as follows:
|
13
|
+
#
|
14
|
+
# foo.feature:8:16
|
15
|
+
#
|
16
|
+
# The path is not updated if it does not contain the focus tag.
|
17
|
+
#
|
18
|
+
module Focuser
|
19
|
+
class << self
|
20
|
+
# Focus the supplied paths using the provided focus tag.
|
21
|
+
#
|
22
|
+
# @param [Array<String>] paths the locations of the feature files
|
23
|
+
# @param [String] focus_tag the focus tag to look for in each path
|
24
|
+
# @return [Array<String>] the updated paths
|
25
|
+
#
|
26
|
+
def focus(paths, focus_tag)
|
27
|
+
return false if paths.empty?
|
28
|
+
|
29
|
+
paths.inject([]) do |updated_paths, path|
|
30
|
+
focused_line_numbers = scan_path_for_focus_tag(path, focus_tag)
|
31
|
+
|
32
|
+
unless focused_line_numbers.empty?
|
33
|
+
updated_paths << append_line_numbers_to_path(
|
34
|
+
focused_line_numbers, path
|
35
|
+
)
|
36
|
+
else
|
37
|
+
updated_paths << path
|
38
|
+
end
|
39
|
+
|
40
|
+
updated_paths
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Checks to see if the file at path contains the focus tag
|
45
|
+
# It will return an empty array if the path is a directory.
|
46
|
+
#
|
47
|
+
# @param [String] path the file path to search
|
48
|
+
# @param [String] focus_tag the focus tag to look for in each path
|
49
|
+
# @return [Array<Integer>] the line numbers that include the focus tag in path
|
50
|
+
#
|
51
|
+
def scan_path_for_focus_tag(path, focus_tag)
|
52
|
+
return [] if File.directory?(path) || path.include?(":")
|
53
|
+
|
54
|
+
line_numbers = []
|
55
|
+
|
56
|
+
File.open(path, "r") do |file|
|
57
|
+
while (line = file.gets)
|
58
|
+
if line.include?(focus_tag)
|
59
|
+
line_numbers << file.lineno
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
line_numbers
|
65
|
+
end
|
66
|
+
|
67
|
+
# Appends the line numbers to the path
|
68
|
+
#
|
69
|
+
# @param [Array<Integer>] line_numbers the line numbers to append to the path
|
70
|
+
# @param [String] path the path that will receive the appended line numbers
|
71
|
+
# @return [String] the string containing the path appended with the line number
|
72
|
+
#
|
73
|
+
def append_line_numbers_to_path(line_numbers, path)
|
74
|
+
line_numbers.each { |num| path += ":" + num.to_s }
|
75
|
+
|
76
|
+
path
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|