cucumber 0.3.103 → 0.3.104
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +27 -2
- data/Manifest.txt +10 -4
- data/examples/ramaze/README.textile +7 -0
- data/examples/ramaze/Rakefile +6 -0
- data/examples/ramaze/app.rb +21 -0
- data/examples/ramaze/features/add.feature +11 -0
- data/examples/ramaze/features/step_definitions/add_steps.rb +15 -0
- data/examples/ramaze/features/support/env.rb +32 -0
- data/examples/ramaze/layout/default.html.erb +8 -0
- data/examples/ramaze/view/index.html.erb +5 -0
- data/examples/sinatra/features/support/env.rb +1 -1
- data/features/cucumber_cli.feature +5 -5
- data/features/usage_and_stepdefs_formatter.feature +169 -0
- data/lib/cucumber/ast/step_invocation.rb +7 -0
- data/lib/cucumber/ast/tags.rb +6 -1
- data/lib/cucumber/ast/tree_walker.rb +5 -11
- data/lib/cucumber/cli/options.rb +20 -11
- data/lib/cucumber/formatter/html.rb +0 -2
- data/lib/cucumber/formatter/stepdefs.rb +14 -0
- data/lib/cucumber/formatter/usage.rb +106 -50
- data/lib/cucumber/language_support/language_methods.rb +6 -9
- data/lib/cucumber/rb_support/rb_language.rb +16 -3
- data/lib/cucumber/rb_support/rb_step_definition.rb +7 -1
- data/lib/cucumber/step_match.rb +4 -0
- data/lib/cucumber/step_mother.rb +8 -37
- data/lib/cucumber/version.rb +1 -1
- data/spec/cucumber/ast/background_spec.rb +0 -6
- data/spec/cucumber/ast/tree_walker_spec.rb +0 -7
- data/spec/cucumber/cli/options_spec.rb +12 -0
- data/spec/cucumber/formatter/html_spec.rb +0 -1
- data/spec/cucumber/rb_support/rb_step_definition_spec.rb +0 -9
- data/spec/cucumber/step_mother_spec.rb +13 -34
- metadata +12 -6
- data/features/steps_formatter.feature +0 -26
- data/features/usage.feature +0 -126
- data/lib/cucumber/formatter/profile.rb +0 -78
- data/lib/cucumber/formatters/unicode.rb +0 -7
@@ -10,7 +10,6 @@ module Cucumber
|
|
10
10
|
end
|
11
11
|
|
12
12
|
def visit_features(features)
|
13
|
-
warn "The listener(s) (#{deprecated_listeners.map{ |l| l.class }}) appear to support the legacy Ast::Visitor interface, which is no longer supported." if deprecated_listeners.any?
|
14
13
|
broadcast(features) do
|
15
14
|
features.accept(self)
|
16
15
|
end
|
@@ -154,14 +153,13 @@ module Cucumber
|
|
154
153
|
message = extract_method_name_from(caller)
|
155
154
|
message.gsub!('visit_', '')
|
156
155
|
|
157
|
-
|
156
|
+
if block_given?
|
157
|
+
send_to_all("before_#{message}", *args)
|
158
|
+
yield if block_given?
|
159
|
+
send_to_all("after_#{message}", *args)
|
160
|
+
else
|
158
161
|
send_to_all(message, *args)
|
159
|
-
return
|
160
162
|
end
|
161
|
-
|
162
|
-
send_to_all("before_#{message}", *args)
|
163
|
-
yield if block_given?
|
164
|
-
send_to_all("after_#{message}", *args)
|
165
163
|
end
|
166
164
|
|
167
165
|
def send_to_all(message, *args)
|
@@ -176,10 +174,6 @@ module Cucumber
|
|
176
174
|
call_stack[0].match(/in `(.*)'/).captures[0]
|
177
175
|
end
|
178
176
|
|
179
|
-
def deprecated_listeners
|
180
|
-
@listeners.select{ |l| l.respond_to?(:visit_features) }
|
181
|
-
end
|
182
|
-
|
183
177
|
end
|
184
178
|
end
|
185
179
|
end
|
data/lib/cucumber/cli/options.rb
CHANGED
@@ -9,13 +9,17 @@ module Cucumber
|
|
9
9
|
'pdf' => ['Cucumber::Formatter::Pdf', "Generates a PDF report. You need to have the\n" +
|
10
10
|
"#{' ' * 51}prawn gem installed. Will pick up logo from\n" +
|
11
11
|
"#{' ' * 51}features/support/logo.png if present."],
|
12
|
-
'profile' => ['Cucumber::Formatter::Profile', 'Prints the 10 slowest steps at the end.'],
|
13
12
|
'progress' => ['Cucumber::Formatter::Progress', 'Prints one character per scenario.'],
|
14
13
|
'rerun' => ['Cucumber::Formatter::Rerun', 'Prints failing files with line numbers.'],
|
15
|
-
'usage' => ['Cucumber::Formatter::Usage',
|
14
|
+
'usage' => ['Cucumber::Formatter::Usage', "Prints where step definitions are used.\n" +
|
15
|
+
"#{' ' * 51}The slowest step definitions (with duration) are\n" +
|
16
|
+
"#{' ' * 51}listed first. If --dry-run is used the duration\n" +
|
17
|
+
"#{' ' * 51}is not shown, and step definitions are sorted by\n" +
|
18
|
+
"#{' ' * 51}filename instead."],
|
19
|
+
'stepdefs' => ['Cucumber::Formatter::Stepdefs', "Prints All step definitions with their locations. Same as\n" +
|
20
|
+
"the usage formatter, except that steps are not printed."],
|
16
21
|
'junit' => ['Cucumber::Formatter::Junit', 'Generates a report similar to Ant+JUnit.'],
|
17
|
-
'tag_cloud' => ['Cucumber::Formatter::TagCloud', 'Prints a tag cloud of tag usage.']
|
18
|
-
'steps' => ['Cucumber::Formatter::Steps', 'Prints location of step definitions.']
|
22
|
+
'tag_cloud' => ['Cucumber::Formatter::TagCloud', 'Prints a tag cloud of tag usage.']
|
19
23
|
}
|
20
24
|
max = BUILTIN_FORMATS.keys.map{|s| s.length}.max
|
21
25
|
FORMAT_HELP = (BUILTIN_FORMATS.keys.sort.map do |key|
|
@@ -171,9 +175,9 @@ module Cucumber
|
|
171
175
|
end
|
172
176
|
opts.on("-d", "--dry-run", "Invokes formatters without executing the steps.",
|
173
177
|
"This also omits the loading of your support/env.rb file if it exists.",
|
174
|
-
"Implies --
|
178
|
+
"Implies --no-snippets.") do
|
175
179
|
@options[:dry_run] = true
|
176
|
-
@
|
180
|
+
@options[:snippets] = false
|
177
181
|
end
|
178
182
|
opts.on("-a", "--autoformat DIRECTORY",
|
179
183
|
"Reformats (pretty prints) feature files and write them to DIRECTORY.",
|
@@ -257,12 +261,16 @@ module Cucumber
|
|
257
261
|
attr_reader :options, :profiles, :expanded_args
|
258
262
|
protected :options, :profiles, :expanded_args
|
259
263
|
|
260
|
-
def non_stdout_formats
|
261
|
-
@options[:formats].select {|format, output| output != @out_stream }
|
262
|
-
end
|
263
|
-
|
264
264
|
private
|
265
265
|
|
266
|
+
def non_stdout_formats
|
267
|
+
@options[:formats].select {|format, output| output != @out_stream }
|
268
|
+
end
|
269
|
+
|
270
|
+
def stdout_formats
|
271
|
+
@options[:formats].select {|format, output| output == @out_stream }
|
272
|
+
end
|
273
|
+
|
266
274
|
def extract_environment_variables
|
267
275
|
@args.delete_if do |arg|
|
268
276
|
if arg =~ /^(\w+)=(.*)$/
|
@@ -337,7 +345,8 @@ module Cucumber
|
|
337
345
|
if @options[:formats].empty?
|
338
346
|
@options[:formats] = other_options[:formats]
|
339
347
|
else
|
340
|
-
@options[:formats] += other_options
|
348
|
+
@options[:formats] += other_options[:formats]
|
349
|
+
@options[:formats] = stdout_formats[0..0] + non_stdout_formats
|
341
350
|
end
|
342
351
|
|
343
352
|
self
|
@@ -2,82 +2,138 @@ require 'cucumber/formatter/progress'
|
|
2
2
|
|
3
3
|
module Cucumber
|
4
4
|
module Formatter
|
5
|
-
|
6
|
-
class Usage
|
5
|
+
class Usage < Progress
|
7
6
|
include Console
|
8
7
|
|
8
|
+
class StepDefKey
|
9
|
+
attr_reader :regexp_source, :file_colon_line
|
10
|
+
attr_accessor :mean_duration, :status
|
11
|
+
|
12
|
+
def initialize(regexp_source, file_colon_line)
|
13
|
+
@regexp_source, @file_colon_line = regexp_source, file_colon_line
|
14
|
+
end
|
15
|
+
|
16
|
+
def eql?(o)
|
17
|
+
regexp_source == o.regexp_source && file_colon_line == o.file_colon_line
|
18
|
+
end
|
19
|
+
|
20
|
+
def hash
|
21
|
+
regexp_source.hash + 17*file_colon_line.hash
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
9
25
|
def initialize(step_mother, io, options)
|
26
|
+
@step_mother = step_mother
|
10
27
|
@io = io
|
11
28
|
@options = options
|
12
|
-
@
|
13
|
-
@all_step_definitions = step_mother.step_definitions.dup
|
14
|
-
@locations = []
|
15
|
-
end
|
16
|
-
|
17
|
-
def after_features(features)
|
18
|
-
print_summary(features)
|
29
|
+
@stepdef_to_match = Hash.new{|h,stepdef_key| h[stepdef_key] = []}
|
19
30
|
end
|
20
31
|
|
21
32
|
def before_step(step)
|
22
33
|
@step = step
|
23
34
|
end
|
24
35
|
|
25
|
-
def
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
@
|
36
|
+
def before_step_result(keyword, step_match, multiline_arg, status, exception, source_indent, background)
|
37
|
+
@step_duration = Time.now
|
38
|
+
end
|
39
|
+
|
40
|
+
def after_step_result(keyword, step_match, multiline_arg, status, exception, source_indent, background)
|
41
|
+
duration = Time.now - @step_duration
|
42
|
+
if step_match.name.nil? # nil if it's from a scenario outline
|
43
|
+
stepdef_key = StepDefKey.new(step_match.step_definition.regexp_source, step_match.step_definition.file_colon_line)
|
44
|
+
|
45
|
+
@stepdef_to_match[stepdef_key] << {
|
46
|
+
:keyword => keyword,
|
47
|
+
:step_match => step_match,
|
48
|
+
:status => status,
|
49
|
+
:file_colon_line => @step.file_colon_line,
|
50
|
+
:duration => duration
|
51
|
+
}
|
35
52
|
end
|
53
|
+
super
|
36
54
|
end
|
37
55
|
|
38
56
|
def print_summary(features)
|
39
|
-
|
40
|
-
|
41
|
-
sorted_defs.each do |step_definition|
|
42
|
-
step_matches_and_descriptions = @step_definitions[step_definition].sort_by do |step_match_and_description|
|
43
|
-
step_match = step_match_and_description[0]
|
44
|
-
step_match.step_definition.regexp_source
|
45
|
-
end
|
57
|
+
add_unused_stepdefs
|
58
|
+
aggregate_info
|
46
59
|
|
47
|
-
|
60
|
+
if @options[:dry_run]
|
61
|
+
keys = @stepdef_to_match.keys.sort {|a,b| a.regexp_source <=> b.regexp_source}
|
62
|
+
else
|
63
|
+
keys = @stepdef_to_match.keys.sort {|a,b| a.mean_duration <=> b.mean_duration}.reverse
|
64
|
+
end
|
65
|
+
|
66
|
+
keys.each do |stepdef_key|
|
67
|
+
print_step_definition(stepdef_key)
|
48
68
|
|
49
|
-
|
50
|
-
|
69
|
+
if @stepdef_to_match[stepdef_key].any?
|
70
|
+
print_steps(stepdef_key)
|
71
|
+
else
|
72
|
+
@io.puts(" " + format_string("NOT MATCHED BY ANY STEPS", :failed))
|
51
73
|
end
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
74
|
+
end
|
75
|
+
@io.puts
|
76
|
+
super
|
77
|
+
end
|
78
|
+
|
79
|
+
def print_step_definition(stepdef_key)
|
80
|
+
@io.print format_string(sprintf("%.7f", stepdef_key.mean_duration), :skipped) + " " unless @options[:dry_run]
|
81
|
+
@io.print format_string(stepdef_key.regexp_source, stepdef_key.status)
|
82
|
+
if @options[:source]
|
83
|
+
indent = max_length - stepdef_key.regexp_source.jlength
|
84
|
+
line_comment = " # #{stepdef_key.file_colon_line}".indent(indent)
|
85
|
+
@io.print(format_string(line_comment, :comment))
|
86
|
+
end
|
87
|
+
@io.puts
|
88
|
+
end
|
89
|
+
|
90
|
+
def print_steps(stepdef_key)
|
91
|
+
@stepdef_to_match[stepdef_key].each do |step|
|
92
|
+
@io.print " "
|
93
|
+
@io.print format_string(sprintf("%.7f", step[:duration]), :skipped) + " " unless @options[:dry_run]
|
94
|
+
@io.print format_step(step[:keyword], step[:step_match], step[:status], nil)
|
95
|
+
if @options[:source]
|
96
|
+
indent = max_length - (step[:keyword].jlength + step[:step_match].format_args.jlength)
|
97
|
+
line_comment = " # #{step[:file_colon_line]}".indent(indent)
|
98
|
+
@io.print(format_string(line_comment, :comment))
|
63
99
|
end
|
64
|
-
|
100
|
+
@io.puts
|
65
101
|
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def max_length
|
105
|
+
[max_stepdef_length, max_step_length].compact.max
|
106
|
+
end
|
66
107
|
|
67
|
-
|
108
|
+
def max_stepdef_length
|
109
|
+
@stepdef_to_match.keys.flatten.map{|key| key.regexp_source.jlength}.max
|
68
110
|
end
|
69
111
|
|
70
|
-
def
|
71
|
-
|
72
|
-
|
112
|
+
def max_step_length
|
113
|
+
@stepdef_to_match.values.flatten.map do |step|
|
114
|
+
step[:keyword].jlength + step[:step_match].format_args.jlength
|
115
|
+
end.max
|
116
|
+
end
|
73
117
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
118
|
+
def aggregate_info
|
119
|
+
@stepdef_to_match.each do |key, steps|
|
120
|
+
if steps.empty?
|
121
|
+
key.status = :skipped
|
122
|
+
key.mean_duration = 0
|
123
|
+
else
|
124
|
+
key.status = Ast::StepInvocation.worst_status(steps.map{|step| step[:status]})
|
125
|
+
total_duration = steps.inject(0) {|sum, step| step[:duration] + sum}
|
126
|
+
key.mean_duration = total_duration / steps.length
|
78
127
|
end
|
79
128
|
end
|
80
129
|
end
|
130
|
+
|
131
|
+
def add_unused_stepdefs
|
132
|
+
@step_mother.unmatched_step_definitions.each do |step_definition|
|
133
|
+
stepdef_key = StepDefKey.new(step_definition.regexp_source, step_definition.file_colon_line)
|
134
|
+
@stepdef_to_match[stepdef_key] = []
|
135
|
+
end
|
136
|
+
end
|
81
137
|
end
|
82
138
|
end
|
83
|
-
end
|
139
|
+
end
|
@@ -1,6 +1,12 @@
|
|
1
|
+
require 'cucumber/step_match'
|
2
|
+
|
1
3
|
module Cucumber
|
2
4
|
module LanguageSupport
|
3
5
|
module LanguageMethods
|
6
|
+
def create_step_match(step_definition, step_name, formatted_step_name, step_arguments)
|
7
|
+
StepMatch.new(step_definition, step_name, formatted_step_name, step_arguments)
|
8
|
+
end
|
9
|
+
|
4
10
|
def before(scenario)
|
5
11
|
begin_scenario
|
6
12
|
execute_before(scenario)
|
@@ -41,15 +47,6 @@ module Cucumber
|
|
41
47
|
transform
|
42
48
|
end
|
43
49
|
|
44
|
-
def add_step_definition(step_definition)
|
45
|
-
step_definitions << step_definition
|
46
|
-
step_definition
|
47
|
-
end
|
48
|
-
|
49
|
-
def step_definitions
|
50
|
-
@step_definitions ||= []
|
51
|
-
end
|
52
|
-
|
53
50
|
def hooks_for(phase, scenario) #:nodoc:
|
54
51
|
hooks[phase.to_sym].select{|hook| scenario.accept_hook?(hook)}
|
55
52
|
end
|
@@ -34,6 +34,7 @@ module Cucumber
|
|
34
34
|
|
35
35
|
def initialize(step_mother)
|
36
36
|
@step_mother = step_mother
|
37
|
+
@step_definitions = []
|
37
38
|
RbDsl.rb_language = self
|
38
39
|
end
|
39
40
|
|
@@ -56,10 +57,20 @@ module Cucumber
|
|
56
57
|
end
|
57
58
|
end
|
58
59
|
|
60
|
+
def step_matches(step_name, formatted_step_name)
|
61
|
+
@step_definitions.map do |step_definition|
|
62
|
+
step_definition.step_match(step_name, formatted_step_name)
|
63
|
+
end.compact
|
64
|
+
end
|
65
|
+
|
59
66
|
def arguments_from(regexp, step_name)
|
60
67
|
@regexp_argument_matcher.arguments_from(regexp, step_name)
|
61
68
|
end
|
62
69
|
|
70
|
+
def unmatched_step_definitions
|
71
|
+
@step_definitions.select{|step_definition| !step_definition.matched?}
|
72
|
+
end
|
73
|
+
|
63
74
|
def snippet_text(step_keyword, step_name, multiline_arg_class = nil)
|
64
75
|
escaped = Regexp.escape(step_name).gsub('\ ', ' ').gsub('/', '\/')
|
65
76
|
escaped = escaped.gsub(PARAM_PATTERN, ESCAPED_PARAM_PATTERN)
|
@@ -94,7 +105,9 @@ module Cucumber
|
|
94
105
|
end
|
95
106
|
|
96
107
|
def register_rb_step_definition(regexp, proc)
|
97
|
-
|
108
|
+
step_definition = RbStepDefinition.new(self, regexp, proc)
|
109
|
+
@step_definitions << step_definition
|
110
|
+
step_definition
|
98
111
|
end
|
99
112
|
|
100
113
|
def build_rb_world_factory(world_modules, proc)
|
@@ -106,12 +119,12 @@ module Cucumber
|
|
106
119
|
@world_modules += world_modules
|
107
120
|
end
|
108
121
|
|
109
|
-
protected
|
110
|
-
|
111
122
|
def load_code_file(code_file)
|
112
123
|
require code_file # This will cause self.add_step_definition, self.add_hook, and self.add_transform to be called from RbDsl
|
113
124
|
end
|
114
125
|
|
126
|
+
protected
|
127
|
+
|
115
128
|
def begin_scenario
|
116
129
|
begin_rb_scenario
|
117
130
|
end
|
@@ -42,7 +42,9 @@ module Cucumber
|
|
42
42
|
end
|
43
43
|
|
44
44
|
def arguments_from(step_name)
|
45
|
-
RegexpArgumentMatcher.arguments_from(@regexp, step_name)
|
45
|
+
args = RegexpArgumentMatcher.arguments_from(@regexp, step_name)
|
46
|
+
@matched = true if args
|
47
|
+
args
|
46
48
|
end
|
47
49
|
|
48
50
|
def invoke(args)
|
@@ -56,6 +58,10 @@ module Cucumber
|
|
56
58
|
end
|
57
59
|
end
|
58
60
|
|
61
|
+
def matched?
|
62
|
+
@matched
|
63
|
+
end
|
64
|
+
|
59
65
|
def file_colon_line
|
60
66
|
@proc.file_colon_line
|
61
67
|
end
|
data/lib/cucumber/step_match.rb
CHANGED
data/lib/cucumber/step_mother.rb
CHANGED
@@ -38,16 +38,6 @@ module Cucumber
|
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
41
|
-
# Raised when 2 or more StepDefinition have the same Regexp
|
42
|
-
class Redundant < StandardError
|
43
|
-
def initialize(step_def_1, step_def_2)
|
44
|
-
message = "Multiple step definitions have the same Regexp:\n\n"
|
45
|
-
message << step_def_1.backtrace_line << "\n"
|
46
|
-
message << step_def_2.backtrace_line << "\n\n"
|
47
|
-
super(message)
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
41
|
# This is the meaty part of Cucumber that ties everything together.
|
52
42
|
class StepMother
|
53
43
|
include Constantize
|
@@ -87,17 +77,12 @@ module Cucumber
|
|
87
77
|
def load_code_file(step_def_file)
|
88
78
|
if programming_language = programming_language_for(step_def_file)
|
89
79
|
log.debug(" * #{step_def_file}\n")
|
90
|
-
|
91
|
-
register_step_definitions(step_definitions)
|
80
|
+
programming_language.load_code_file(step_def_file)
|
92
81
|
else
|
93
82
|
log.debug(" * #{step_def_file} [NOT SUPPORTED]\n")
|
94
83
|
end
|
95
84
|
end
|
96
85
|
|
97
|
-
def register_step_definitions(step_definitions)
|
98
|
-
step_definitions.each{|step_definition| register_step_definition(step_definition)}
|
99
|
-
end
|
100
|
-
|
101
86
|
# Loads and registers programming language implementation.
|
102
87
|
# Instances are cached, so calling with the same argument
|
103
88
|
# twice will return the same instance.
|
@@ -152,7 +137,9 @@ module Cucumber
|
|
152
137
|
end
|
153
138
|
|
154
139
|
def step_match(step_name, formatted_step_name=nil) #:nodoc:
|
155
|
-
matches =
|
140
|
+
matches = @programming_languages.map do |programming_language|
|
141
|
+
programming_language.step_matches(step_name, formatted_step_name)
|
142
|
+
end.flatten
|
156
143
|
raise Undefined.new(step_name) if matches.empty?
|
157
144
|
matches = best_matches(step_name, matches) if matches.size > 1 && options[:guess]
|
158
145
|
raise Ambiguous.new(step_name, matches, options[:guess]) if matches.size > 1
|
@@ -174,16 +161,11 @@ module Cucumber
|
|
174
161
|
top_groups
|
175
162
|
end
|
176
163
|
end
|
177
|
-
|
178
|
-
def clear! #:nodoc:
|
179
|
-
step_definitions.clear
|
180
|
-
hooks.clear
|
181
|
-
steps.clear
|
182
|
-
scenarios.clear
|
183
|
-
end
|
184
164
|
|
185
|
-
def
|
186
|
-
@
|
165
|
+
def unmatched_step_definitions
|
166
|
+
@programming_languages.map do |programming_language|
|
167
|
+
programming_language.unmatched_step_definitions
|
168
|
+
end.flatten
|
187
169
|
end
|
188
170
|
|
189
171
|
def snippet_text(step_keyword, step_name, multiline_arg_class) #:nodoc:
|
@@ -244,17 +226,6 @@ module Cucumber
|
|
244
226
|
|
245
227
|
private
|
246
228
|
|
247
|
-
# Registers a StepDefinition. This can be a Ruby StepDefintion,
|
248
|
-
# or any other kind of object that implements the StepDefintion
|
249
|
-
# contract (API).
|
250
|
-
def register_step_definition(step_definition)
|
251
|
-
step_definitions.each do |already|
|
252
|
-
raise Redundant.new(already, step_definition) if already == step_definition
|
253
|
-
end
|
254
|
-
step_definitions << step_definition
|
255
|
-
step_definition
|
256
|
-
end
|
257
|
-
|
258
229
|
def programming_language_for(step_def_file) #:nodoc:
|
259
230
|
if ext = File.extname(step_def_file)[1..-1]
|
260
231
|
return nil if @unsupported_programming_languages.index(ext)
|