tallakt-plcutil 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,243 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require 'optparse'
4
+ require 'tallakt-plcutil/siemens/awlfile'
5
+ require 'tallakt-plcutil/siemens/sdffile'
6
+ require 'tallakt-plcutil/wonderware/intouchfile'
7
+
8
+ module PlcUtil
9
+ # Command line tool to read and output an awl file
10
+ class Step7ToIntouchRunner
11
+ def initialize(command_line_arguments)
12
+ # standard command line options
13
+ @awloptions = {}
14
+ @intouchoptions = {}
15
+ @output = nil
16
+ @symlistfile = nil
17
+ @no_block = false
18
+ @access_name = nil
19
+ @filter_file = nil
20
+
21
+ # Parse command line options
22
+ option_parser.parse! command_line_arguments
23
+ if command_line_arguments.empty?
24
+ show_help
25
+ exit
26
+ end
27
+
28
+ # Read Siemens S7 files
29
+ # AWL - generated by using 'generate source' and selecting a DB
30
+ # SDF - export file generated from variable list and selecting 'export'
31
+ awl_files = command_line_arguments.select {|fn| fn.match(/\.awl$/i) }
32
+ sdf_files = command_line_arguments - awl_files
33
+ @sdflist = sdf_files.map{|filename| SdfFile.new filename }
34
+ @awllist = awl_files.map{|filename| AwlFile.new filename, @awloptions}
35
+
36
+ # create a lookup table for used tags in the file to prevent duplicate ids
37
+ # TODO Move this into Intouchfile class
38
+ @used_tags = {}
39
+
40
+ @sdflist.each do |sdf|
41
+ sdf.tags.each do |item|
42
+ @used_tags[siemens_to_ww_tagname_long item[:name]] = true
43
+ end
44
+ end
45
+
46
+ @awllist.each do |awl|
47
+ awl.each_tag :no_block => @no_block do |item|
48
+ @used_tags[siemens_to_ww_tagname_long item[:name]] = true
49
+ end
50
+ end
51
+
52
+ # load filter file to enable override of functions filter_comment_format and filter_handle_tag
53
+ load @filter_file if @filter_file && File.exists?(@filter_file)
54
+
55
+ # Write to intouch file
56
+ if @output
57
+ File.open @output, 'w' do |f|
58
+ print_to_file f
59
+ end
60
+ else
61
+ print_to_file $stdout
62
+ end
63
+ end
64
+
65
+ def format_addr(addr, ww_data_type, options = {})
66
+ case addr
67
+ when String
68
+ # from symbol list file, ww accepts addres directly
69
+ if options[:is_bool]
70
+ addr.gsub /\s/, ''
71
+ else
72
+ addr.gsub /^([A-Z])[DW]\s*([\d\.]+)/, '\1' + ww_data_type + '\2'
73
+ end
74
+ else
75
+ db = addr.data_block_addr || 'DB???'
76
+ db + ',' + ww_data_type + addr.byte.to_s + (options[:is_bool] ? ('.' + addr.bit.to_s) : '')
77
+ end
78
+ end
79
+
80
+ # This function may be overriden in filter ruby file
81
+ def filter_comment_format(comment, struct_comment)
82
+ sc, cc = struct_comment, comment
83
+ comment = nil unless cc && cc.match(/./)
84
+ sc = nil unless sc && sc.match(/./)
85
+ if cc || sc
86
+ [cc, sc].uniq.compact.join(' / ').gsub(/"/, '')
87
+ else
88
+ ''
89
+ end
90
+ end
91
+
92
+ # This function may be overridden in filter ruby file
93
+ def filter_handle_tag(item)
94
+ ww_name = siemens_to_ww_tagname item[:name]
95
+ cc = filter_comment_format item[:comment], item[:struct_comment]
96
+ created_io = case item[:type]
97
+ when :bool
98
+ @intouchfile.new_io_disc(ww_name) do |io|
99
+ io.item_name = format_addr(item[:addr], 'X', :is_bool => true)
100
+ io
101
+ end
102
+ when :int
103
+ @intouchfile.new_io_int(ww_name) do |io|
104
+ io.item_name = format_addr(item[:addr], 'INT')
105
+ io
106
+ end
107
+ when :word
108
+ @intouchfile.new_io_int(ww_name) do |io|
109
+ io.item_name = format_addr(item[:addr], 'WORD')
110
+ io
111
+ end
112
+ when :real
113
+ @intouchfile.new_io_real(ww_name) do |io|
114
+ io.item_name = format_addr(item[:addr], 'REAL')
115
+ io
116
+ end
117
+ when :byte
118
+ @intouchfile.new_io_int(ww_name) do |io|
119
+ io.item_name = format_addr(item[:addr], 'BYTE')
120
+ io
121
+ end
122
+ when :char
123
+ @intouchfile.new_io_int(ww_name) do |io|
124
+ io.item_name = format_addr(item[:addr], 'CHAR')
125
+ io
126
+ end
127
+ when :date, :s5time, :time_of_day, :timer
128
+ # skip
129
+ when :dint
130
+ @intouchfile.new_io_int(ww_name) do |io|
131
+ io.item_name = format_addr(item[:addr], 'DINT')
132
+ io
133
+ end
134
+ when :dword, :time
135
+ @intouchfile.new_io_int(ww_name) do |io|
136
+ io.item_name = format_addr(item[:addr], 'DWORD')
137
+ io
138
+ end
139
+ else
140
+ raise RuntimeError.new('Unsupported type found: ' + item[:type].to_s)
141
+ end
142
+
143
+ # Common options
144
+ if created_io
145
+ created_io.item_use_tagname = 'No'
146
+ created_io.comment = cc
147
+
148
+ yield created_io if block_given?
149
+ end
150
+ end
151
+
152
+
153
+ # This function may be overridden in filter ruby file
154
+ def filter_handle_sdf_files
155
+ @sdflist.each do |sdf|
156
+ sdf.tags.each do |item|
157
+ filter_handle_tag item
158
+ end
159
+ end
160
+ end
161
+
162
+ # This function may be overridden in filter ruby file
163
+ def filter_handle_awl_files
164
+ @awllist.each do |awl|
165
+ awl.each_tag :no_block => @no_block do |item|
166
+ filter_handle_tag item
167
+ end
168
+ end
169
+ end
170
+
171
+
172
+
173
+ def print_to_file(f)
174
+ @intouchfile = IntouchFile.new nil, @intouchoptions
175
+ filter_handle_sdf_files
176
+ filter_handle_awl_files
177
+ @intouchfile.write_csv f
178
+ end
179
+
180
+ def siemens_to_ww_tagname(s)
181
+ new_unique_tag(siemens_to_ww_tagname_long s)
182
+ end
183
+
184
+ def siemens_to_ww_tagname_long(s)
185
+ s.gsub(/[\. ]/, '_').gsub(/\[(\d+)\]/) { '_' + $1 }
186
+ end
187
+
188
+ def new_unique_tag_helper(s, n)
189
+ s[0..(31 - n.to_s.size - 1)] + '%' + n.to_s
190
+ end
191
+
192
+ def new_unique_tag(s)
193
+ if s.size < 33
194
+ s
195
+ else
196
+ n = nil
197
+ new_tag = new_unique_tag_helper s, n
198
+ while @used_tags.key? new_tag
199
+ n ||= 0
200
+ n += 1
201
+ new_tag = new_unique_tag_helper s, n
202
+ end
203
+ @used_tags[new_tag] = true
204
+ new_tag
205
+ end
206
+ end
207
+
208
+ def option_parser
209
+ OptionParser.new do |opts|
210
+ opts.banner = "Usage: s7tointouch [options] <.awl or .sdf files>"
211
+ opts.on("-s", "--symlist FILE", String, "Specify SYMLIST.DBF file from S7 project ") do |symlistfile|
212
+ @awloptions[:symlist] = symlistfile
213
+ end
214
+ opts.on("-n", "--no-block", String, "Dont use the datablock as part of the tag", "name") do
215
+ @no_block = true
216
+ end
217
+ opts.on("-b", "--block NAME=ADDR", String, "Define address of datablock without", "reading symlist") do |blockdef|
218
+ name, addr = blockdef.split(/=/)
219
+ @awloptions[:blocks] ||= {}
220
+ @awloptions[:blocks][name] = addr
221
+ end
222
+ opts.on("-a", "--access ACCESSNAME", String, "Set access name for all tags") do |access_name|
223
+ @intouchoptions[:access_name] = access_name
224
+ end
225
+ opts.on("-f", "--filter FILTER_RUBY_FILE", String, "Specify ruby filter file to override", "filter functions") do |filter_file|
226
+ @filter_file = filter_file
227
+ end
228
+ opts.on("-o", "--output FILE", String, "Output to specified file instead of", "standard output") do |output|
229
+ @output = output
230
+ end
231
+ opts.on_tail("-h", "--help", "Show this message") do
232
+ puts opts
233
+ exit
234
+ end
235
+ end
236
+ end
237
+
238
+ def show_help
239
+ puts option_parser
240
+ end
241
+ end
242
+ end
243
+
@@ -0,0 +1,254 @@
1
+ require 'rubygems'
2
+ require 'optparse'
3
+ require 'tallakt-plcutil/wonderware/intouchfile'
4
+
5
+ module PlcUtil
6
+ class IntouchPrettyPrintRunner
7
+ MAX_WW_TAGLENGTH = 32
8
+
9
+ def initialize(arguments)
10
+ # Standard options
11
+ @mode = :io
12
+
13
+ # Parse command line options
14
+ option_parser.parse! arguments
15
+ if arguments.size > 1
16
+ show_help
17
+ exit
18
+ end
19
+ filename, = arguments
20
+
21
+
22
+
23
+ # Read from intouch file
24
+ @intouch_file = IntouchFile.new
25
+ if filename
26
+ File.open filename do |f|
27
+ @intouch_file.read_csv f
28
+ end
29
+ else
30
+ @intouch_file.read_csv $stdin
31
+ end
32
+
33
+ # print the tags in intouch
34
+ case @mode
35
+ when :io
36
+ print_io
37
+ when :duplicates
38
+ print_duplicates
39
+ when :missing
40
+ print_alarms_missing_text
41
+ when :alarm_groups
42
+ print_alarm_groups
43
+ when :access_names
44
+ print_access_names
45
+ when :tag
46
+ print_tag @tag
47
+ end
48
+ end
49
+
50
+ def addr_signature(tag)
51
+ get_tag_field(:access_name, tag) + get_tag_field(:item_name, tag)
52
+ end
53
+
54
+ def tag_is_io?(tag)
55
+ tag.respond_to?(:item_name) && tag.item_name &&
56
+ tag.respond_to?(:access_name) && tag.access_name
57
+ end
58
+
59
+ def print_duplicates
60
+ print_io do |tags|
61
+ addresscount = {}
62
+
63
+ tags.each do |tag|
64
+ addr = addr_signature tag
65
+
66
+ addresscount[addr] ||= 0
67
+ addresscount[addr] += 1 if tag_is_io?(tag)
68
+ end
69
+ tmp = tags.select {|tag| addresscount[addr_signature tag] > 1 }
70
+ tmp.sort {|a, b| addr_signature(a) <=> addr_signature(b) }
71
+ end
72
+ end
73
+
74
+ def print_alarms_missing_text
75
+ print_io do |tags|
76
+ tags.select {|tag| tag.alarm? && (!tag.alarm_comment || !tag.alarm_comment.match(/\S/)) }
77
+ end
78
+ end
79
+
80
+ def print_io
81
+ ss = %w( :IODisc :IOReal :IndirectAnalog :MemoryReal :IOMsg :IndirectMsg
82
+ :MemoryMsg :MemoryDisc :IndirectDisc :IOInt :MemoryInt)
83
+ tags = []
84
+ ss.each {|section| @intouch_file.each_tag(section) {|tag| tags << tag } }
85
+
86
+ if block_given?
87
+ tags = yield tags
88
+ end
89
+
90
+ columns = []
91
+ columns << {:max => 1, :min => 1, :nospace => true, :gen => Proc.new { |tag| alarm_icon(tag) } }
92
+ columns << {:max => MAX_WW_TAGLENGTH, :min => 15, :gen => lambda {|tag| tag.tag } }
93
+ columns << {:max => 20, :min => 10, :gen => Proc.new {|tag| get_tag_field(:item_name, tag) } }
94
+ columns << {:max => 20, :min => 10, :gen => Proc.new {|tag| get_tag_field(:access_name, tag) } }
95
+ columns << {:rest => true, :gen => Proc.new {|tag| comment_for_tag(tag) } }
96
+
97
+ # shrink columns
98
+ columns.each do |c|
99
+ if c[:max]
100
+ c[:adjusted] = max_str_len(tags, c[:max], c[:min]) {|tag| c[:gen].call(tag) }
101
+ end
102
+ end
103
+
104
+ # use remaining space for :rest tag
105
+ spaces = columns.map {|c| c[:nospace] ? 0 : 1 }.reduce(:+) - 1
106
+ wasted = columns.map {|c| c[:adjusted] || 0 }.reduce(:+) - spaces
107
+ rest = columns.find {|c| c[:rest] }
108
+ @column_width ||= console_width
109
+ rest[:adjusted] = @column_width - wasted if rest
110
+
111
+ first_columns = columns - [columns.last]
112
+ tags.each do |tag|
113
+ cs = first_columns.map do |c|
114
+ fix_string(c, tag).ljust(c[:adjusted]) + (c[:nospace] ? '' : ' ')
115
+ end
116
+ cs << fix_string(columns.last, tag)
117
+ str = cs.join
118
+ if tag.tag.size > MAX_WW_TAGLENGTH
119
+ str = red(str)
120
+ else
121
+ str = yellow(str) if tag.alarm?
122
+ end
123
+ puts str
124
+ end
125
+ end
126
+
127
+ def max_str_len(tags, abs_max, abs_min)
128
+ [abs_max, tags.reduce(abs_min) {|max, tag| max = [max, (yield tag).size].max }].min
129
+ end
130
+
131
+
132
+
133
+ def get_tag_field(field, tag)
134
+ if tag.respond_to? field
135
+ tag.method(field).call || ''
136
+ else
137
+ ''
138
+ end
139
+ end
140
+
141
+ def fix_string(column, tag)
142
+ str = column[:gen].call(tag)
143
+ if str.size > column[:adjusted]
144
+ str[0..(column[:adjusted] - 4)] + '...'
145
+ else
146
+ str
147
+ end
148
+ end
149
+
150
+
151
+ def print_alarm_groups
152
+ print_four_column_tags ':AlarmGroup'
153
+ end
154
+
155
+ def print_access_names
156
+ print_four_column_tags ':IOAccess'
157
+ end
158
+
159
+ def print_four_column_tags(section)
160
+ lines = []
161
+ @intouch_file.each_tag section do |tag|
162
+ lines << tag.tag
163
+ end
164
+ lines.each_slice(4) do |slice|
165
+ slice[3] ||= nil
166
+ puts ('%-20s' * 4) % slice
167
+ end
168
+ end
169
+
170
+ def print_tag(tag)
171
+ t = @intouch_file.find_tag(tag)
172
+ if t
173
+ puts 'Tag: ' + tag
174
+ t.intouch_fields.each do |field|
175
+ puts '%-30s%s' % [field.to_s, t.method(field).call]
176
+ end
177
+ else
178
+ puts 'Tag %s was not found' % tag
179
+ end
180
+ end
181
+
182
+ def option_parser
183
+ OptionParser.new do |opts|
184
+ opts.banner = "Usage: intouchreader [options] [DBFILE]"
185
+ opts.on("-c", "--access-names", "Show access name") do
186
+ @mode = :access_names
187
+ end
188
+ opts.on("-t", "--tag TAGNAME", "Show all fields for the specified tag") do |tag|
189
+ @mode = :tag
190
+ @tag = tag
191
+ end
192
+ opts.on("-w", "--wide", "Print wider than console width") do
193
+ @column_width = 9999
194
+ end
195
+ opts.on("-a", "--alarm-groups", "Show alarm groups") do
196
+ @mode = :alarm_groups
197
+ end
198
+ opts.on("-m", "--missing", "Show only alarms with missing text") do
199
+ @mode = :missing
200
+ end
201
+ opts.on("-d", "--duplicates", "Show only duplicated tags (shares address)") do
202
+ @mode = :duplicates
203
+ end
204
+ opts.on_tail("-h", "--help", "Show this message") do
205
+ puts opts
206
+ exit
207
+ end
208
+ end
209
+ end
210
+
211
+ def show_help
212
+ puts option_parser
213
+ end
214
+
215
+ private
216
+
217
+ def console_width
218
+ 80
219
+ end
220
+
221
+ def comment_for_tag(tag)
222
+ if tag.alarm?
223
+ tag.alarm_comment
224
+ else
225
+ tag.comment
226
+ end || ''
227
+ end
228
+
229
+ def alarm_icon(tag)
230
+ if tag.alarm?
231
+ '*'
232
+ else
233
+ ' '
234
+ end
235
+ end
236
+
237
+ def colorize(text, color_code)
238
+ "#{color_code}#{text}\e[0m"
239
+ end
240
+
241
+ def yellow(text)
242
+ colorize text, "\e[33m"
243
+ end
244
+
245
+ def red(text)
246
+ colorize text, "\e[31m"
247
+ end
248
+
249
+ def green(text)
250
+ colorize text, "\e[32m"
251
+ end
252
+
253
+ end
254
+ end
@@ -7,14 +7,15 @@ module PlcUtil
7
7
  class IntouchFile
8
8
  ApostropheReqiredColumns =
9
9
  %w(
10
- SymbolicName Comment OnMsg Group AccessName HiAlarmInhibitor MajDevAlarmInhibitor DSCAlarmInhibitor InitialMessage
11
- HiHiAlarmInhibitor tag Application MinDevAlarmInhibitor Topic AlarmComment EngUnits LoAlarmInhibitor
10
+ SymbolicName Comment OnMsg Group AccessName
11
+ HiAlarmInhibitor MajDevAlarmInhibitor
12
+ DSCAlarmInhibitor InitialMessage
13
+ HiHiAlarmInhibitor tag Application MinDevAlarmInhibitor
14
+ Topic AlarmComment EngUnits LoAlarmInhibitor
12
15
  OffMsg LoLoAlarmInhibitor RocAlarmInhibitor ItemName
13
16
  )
14
17
 
15
18
  def initialize(filename = nil, options = {})
16
- # Lookup table to check wether a certain column must write its values inside apostrophes
17
- @lookup_apostrophe_fields = ApostropheReqiredColumns.inject({}) {|h, v| h[v] = true; h}
18
19
 
19
20
  # load standard sections
20
21
  @sections = YAML.load_file File.join(File.dirname(__FILE__), "standard_sections.yaml")
@@ -38,10 +39,11 @@ module PlcUtil
38
39
  if section
39
40
  @sections[section][:rows].find {|row| row.tag == tag}
40
41
  else
41
- tmp = @sections.collect do |sec|
42
- sec[:rows].find {|row| row.tag == tag }
43
- end.flatten
44
- tmp && tmp.first
42
+ result = []
43
+ @sections.each do |name, sec|
44
+ result << sec[:rows].find {|row| row.tag == tag }
45
+ end
46
+ result.find {|x| x }
45
47
  end
46
48
  end
47
49
 
@@ -91,13 +93,13 @@ module PlcUtil
91
93
  else
92
94
  # new tag
93
95
  cols = l.gsub(/"/, '').split /;/
94
- new_data_instance(current[:name][1..-1], cols[0], cols[1, -1])
96
+ new_data_instance(current[:name][1..-1], cols[0], cols[1..-1], colnames)
95
97
  end
96
98
  end
97
99
  end
98
100
 
99
101
  def write_csv(io, mode = :update)
100
- throw 'Please use mode :ask/:update/:replace' unless [:ask, :replace, :update].include? mode
102
+ raise 'Please use mode :ask/:update/:replace' unless [:ask, :replace, :update].include? mode
101
103
  io.puts ':mode=' + mode.to_s
102
104
  @sections.each_value do |section|
103
105
  next if section[:rows].empty?
@@ -112,12 +114,14 @@ module PlcUtil
112
114
 
113
115
 
114
116
  def apostrophe_req?(colname)
115
- @lookup_apostrophe_fields[colname] || colname.match(/^:/)
117
+ # Lookup table to check wether a certain column must write its values inside apostrophes
118
+ @@lookup_apostrophe_fields ||= ApostropheReqiredColumns.inject({}) {|h, v| h[v] = true; h}
119
+ @@lookup_apostrophe_fields[colname] || colname.match(/^:/)
116
120
  end
117
121
 
118
122
  def to_csv_line(colnames)
119
123
  '[%s].join ";"' % colnames.map do |c|
120
- cc = c.match(/^:/) ? 'tag' : camel_conv(c)
124
+ cc = c.match(/^:/) ? 'tag' : dasherize(c)
121
125
  if apostrophe_req? c
122
126
  # When nil, dont display the "" chars, since this will overwrite with an empty string
123
127
  # The check is performed at runtime in the generated class
@@ -133,18 +137,21 @@ module PlcUtil
133
137
  # define new class for the data type
134
138
  klass = colnames[0][1..-1]
135
139
  #return if defined? klass
136
- attrs = colnames[1..-1].map {|x| ':' + camel_conv(x) }.join(', ')
137
- attr_list = colnames[1..-1].map {|x| camel_conv(x)}.join(', ')
140
+ attrs = colnames[1..-1].map {|x| ':' + dasherize(x) }.join(', ')
141
+ attr_list = colnames[1..-1].map {|x| dasherize(x)}.join(', ')
138
142
  values_format = colnames.map {|c| apostrophe_req?(c) ? '"%s"' : '%s'}.join(';')
139
143
 
140
144
  PlcUtil.module_eval <<-END
141
145
  class #{klass}
142
146
  attr_accessor #{attrs}
143
- attr_reader :tag
147
+ attr_accessor :tag
144
148
 
145
- def initialize(tag, values=nil)
149
+ def initialize(tag, values={})
146
150
  @tag = tag
147
- #{attr_list} = values if values
151
+ values.each do |key, value|
152
+ mn = key.to_s + '='
153
+ method(mn).call(value) if respond_to?(mn)
154
+ end
148
155
  end
149
156
 
150
157
  def to_csv
@@ -154,6 +161,18 @@ module PlcUtil
154
161
  def intouch_fields
155
162
  [#{attrs}]
156
163
  end
164
+
165
+ def to_h
166
+ result = {:tag => tag }
167
+ intouch_fields.each do |field|
168
+ result[field] = method(field).call
169
+ end
170
+ result
171
+ end
172
+
173
+ def alarm?
174
+ respond_to?(:alarm_state) && alarm_state && alarm_state.match(/On|Off/)
175
+ end
157
176
  end
158
177
  END
159
178
 
@@ -161,11 +180,11 @@ module PlcUtil
161
180
  IntouchFile.class_eval <<-END
162
181
  public
163
182
 
164
- def new_#{camel_conv klass}(tag, values = nil)
183
+ def new_#{dasherize klass}(tag, values = {})
165
184
  result = #{klass}.new tag, values
166
185
  @sections['#{colnames[0]}'][:rows] << result
167
- if result.respond_to? :access_name
168
- result.access_name = @options[:access_name]
186
+ if result.respond_to? :access_name
187
+ result.access_name = @options[:access_name] if @options[:access_name]
169
188
  end
170
189
  yield result if block_given?
171
190
  result
@@ -173,11 +192,12 @@ module PlcUtil
173
192
  END
174
193
  end
175
194
 
176
- def new_data_instance(klass, tag, values)
177
- method('new_' + camel_conv(klass)).call(tag, values)
195
+ def new_data_instance(klass, tag, values, colnames)
196
+ attributes = colnames[1..-1].map {|cn| dasherize(cn)}
197
+ method('new_' + dasherize(klass)).call(tag, Hash[attributes.zip(values)])
178
198
  end
179
199
 
180
- def camel_conv(s)
200
+ def dasherize(s)
181
201
  res = s.gsub(/[A-Z]{3,}/){|x| x[0..-2].capitalize + x[-1..-1]}
182
202
  res.gsub!(/[A-Z]/, '_\0')
183
203
  res.gsub! /^_/ , ''
@@ -0,0 +1 @@
1
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
data/test/helper.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
7
+ require 'tallakt-plcutil'
8
+
9
+ class Test::Unit::TestCase
10
+ end