mysql_binlog 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'pp'
4
+ require 'mysql_binlog'
5
+
6
+ include MysqlBinlog
7
+
8
+ b = Binlog.new(BinlogFileReader, ARGV.first)
9
+
10
+ b.each_event do |event|
11
+ pp event
12
+ end
@@ -0,0 +1,162 @@
1
+ module MysqlBinlog
2
+ QUERY_EVENT_STATUS_TYPES = [
3
+ :flags2,
4
+ :sql_mode,
5
+ :catalog,
6
+ :auto_increment,
7
+ :charset,
8
+ :time_zone,
9
+ :catalog_nz,
10
+ :lc_time_names,
11
+ :charset_database,
12
+ :table_map_for_update,
13
+ ]
14
+
15
+ QUERY_EVENT_FLAGS2 = {
16
+ :auto_is_null => 1 << 14,
17
+ :not_autocommit => 1 << 19,
18
+ :no_foreign_key_checks => 1 << 26,
19
+ :relaxed_unique_checks => 1 << 27,
20
+ }
21
+
22
+ class BinlogEventFieldParser
23
+ attr_accessor :binlog
24
+ attr_accessor :reader
25
+ attr_accessor :parser
26
+
27
+ def initialize(binlog_instance)
28
+ @binlog = binlog_instance
29
+ @reader = binlog_instance.reader
30
+ @parser = binlog_instance.parser
31
+ @table_map = {}
32
+ end
33
+
34
+ def rotate_event(header, fields)
35
+ name_length = reader.remaining(header)
36
+ fields[:name_length] = name_length
37
+ fields[:name] = reader.read(name_length)
38
+ end
39
+
40
+ def _query_event_status(header, fields)
41
+ status = {}
42
+ end_position = reader.position + fields[:status_length]
43
+ while reader.position < end_position
44
+ status_type = QUERY_EVENT_STATUS_TYPES[parser.read_uint8]
45
+ status[status_type] = case status_type
46
+ when :flags2
47
+ flags2 = parser.read_uint32
48
+ QUERY_EVENT_FLAGS2.inject([]) do |result, (flag_name, flag_bit_value)|
49
+ if (flags2 & flag_bit_value) != 0
50
+ result << flag_name
51
+ end
52
+ result
53
+ end
54
+ when :sql_mode
55
+ parser.read_uint64
56
+ when :catalog
57
+ parser.read_lpstringz
58
+ when :auto_increment
59
+ {
60
+ :increment => parser.read_uint16,
61
+ :offset => parser.read_uint16,
62
+ }
63
+ when :charset
64
+ {
65
+ :character_set_client => parser.read_uint16,
66
+ :collation_connection => parser.read_uint16,
67
+ :collation_server => parser.read_uint16,
68
+ }
69
+ when :time_zone
70
+ parser.read_lpstring
71
+ when :catalog_nz
72
+ parser.read_lpstring
73
+ when :lc_time_names
74
+ parser.read_uint16
75
+ when :charset_database
76
+ parser.read_uint16
77
+ when :table_map_for_update
78
+ parser.read_uint64
79
+ end
80
+ end
81
+ status
82
+ end
83
+
84
+ def query_event(header, fields)
85
+ fields[:status] = _query_event_status(header, fields)
86
+ fields.delete :status_length
87
+ fields[:db] = parser.read_nstringz(fields[:db_length])
88
+ fields.delete :db_length
89
+ query_length = reader.remaining(header)
90
+ fields[:query] = reader.read([query_length, binlog.max_query_length].min)
91
+ end
92
+
93
+ def intvar_event(header, fields)
94
+ case fields[:intvar_type]
95
+ when 1
96
+ fields[:intvar_name] = :last_insert_id
97
+ when 2
98
+ fields[:intvar_name] = :insert_id
99
+ else
100
+ fields[:intvar_name] = nil
101
+ end
102
+ end
103
+
104
+ def table_map_event(header, fields)
105
+ fields[:table_id] = parser.read_uint48
106
+ fields[:flags] = parser.read_uint16
107
+ map_entry = @table_map[fields[:table_id]] = {}
108
+ map_entry[:db] = parser.read_lpstringz
109
+ map_entry[:table] = parser.read_lpstringz
110
+ columns = parser.read_varint
111
+ columns_type = parser.read_uint8_array(columns).map { |c| MYSQL_TYPES[c] }
112
+ columns_metadata = parser.read_lpstring
113
+ columns_nullable = parser.read_bit_array(columns)
114
+
115
+ map_entry[:columns] = columns.times.map do |c|
116
+ {
117
+ :type => columns_type[c],
118
+ :nullable => columns_nullable[c],
119
+ }
120
+ end
121
+
122
+ fields[:map_entry] = map_entry
123
+ end
124
+
125
+ def _generic_rows_event_row_images(header, fields)
126
+ row_images = []
127
+ end_position = reader.position + reader.remaining(header)
128
+ while reader.position < end_position
129
+ row_image = []
130
+ columns_null = parser.read_bit_array(fields[:table][:columns].size)
131
+ fields[:table][:columns].each_with_index do |column, column_index|
132
+ if !fields[:columns_used][column_index]
133
+ row_image << nil
134
+ elsif columns_null[column_index]
135
+ row_image << { column => nil }
136
+ else
137
+ row_image << { column => parser.read_mysql_type(column[:type]) }
138
+ end
139
+ end
140
+ row_images << row_image
141
+ end
142
+ row_images
143
+ end
144
+
145
+ def generic_rows_event(header, fields)
146
+ table_id = parser.read_uint48
147
+ fields[:table] = @table_map[table_id]
148
+ fields[:flags] = parser.read_uint16
149
+ columns = parser.read_varint
150
+ fields[:columns_used] = parser.read_bit_array(columns)
151
+ if EVENT_TYPES[header[:event_type]] == :update_rows_event
152
+ fields[:columns_update] = parser.read_bit_array(columns)
153
+ end
154
+ fields[:row_image] = _generic_rows_event_row_images(header, fields)
155
+ fields.delete :columns_used
156
+ end
157
+ alias :write_rows_event :generic_rows_event
158
+ alias :update_rows_event :generic_rows_event
159
+ alias :delete_rows_event :generic_rows_event
160
+
161
+ end
162
+ end
@@ -0,0 +1,179 @@
1
+ module MysqlBinlog
2
+ MYSQL_TYPES_HASH = {
3
+ :decimal => 0,
4
+ :tiny => 1,
5
+ :short => 2,
6
+ :long => 3,
7
+ :float => 4,
8
+ :double => 5,
9
+ :null => 6,
10
+ :timestamp => 7,
11
+ :longlong => 8,
12
+ :int24 => 9,
13
+ :date => 10,
14
+ :time => 11,
15
+ :datetime => 12,
16
+ :year => 13,
17
+ :newdate => 14,
18
+ :varchar => 15,
19
+ :bit => 16,
20
+ :newdecimal => 246,
21
+ :enum => 247,
22
+ :set => 248,
23
+ :tiny_blob => 249,
24
+ :medium_blob => 250,
25
+ :long_blob => 251,
26
+ :blob => 252,
27
+ :var_string => 253,
28
+ :string => 254,
29
+ :geometry => 255,
30
+ }
31
+
32
+ MYSQL_TYPES = MYSQL_TYPES_HASH.inject(Array.new(256)) do |type_array, item|
33
+ type_array[item[1]] = item[0]
34
+ type_array
35
+ end
36
+
37
+ class BinlogParser
38
+ attr_accessor :binlog
39
+ attr_accessor :reader
40
+
41
+ def initialize(binlog_instance)
42
+ @format_cache = {}
43
+ @binlog = binlog_instance
44
+ @reader = binlog_instance.reader
45
+ end
46
+
47
+ def read_uint8
48
+ reader.read(1).unpack("C").first
49
+ end
50
+
51
+ def read_uint16
52
+ reader.read(2).unpack("v").first
53
+ end
54
+
55
+ def read_uint24
56
+ a, b, c = reader.read(3).unpack("CCC")
57
+ a + (b << 8) + (c << 16)
58
+ end
59
+
60
+ def read_uint32
61
+ reader.read(4).unpack("V").first
62
+ end
63
+
64
+ def read_uint48
65
+ a, b, c = reader.read(6).unpack("vvv")
66
+ a + (b << 16) + (c << 32)
67
+ end
68
+
69
+ def read_uint64
70
+ reader.read(8).unpack("Q").first
71
+ end
72
+
73
+ def read_float
74
+ reader.read(4).unpack("g").first
75
+ end
76
+
77
+ def read_double
78
+ reader.read(8).unpack("G").first
79
+ end
80
+
81
+ def read_varint
82
+ # Cheating for now.
83
+ read_uint8
84
+ end
85
+
86
+ def read_lpstring
87
+ length = reader.read(1).unpack("C").first
88
+ reader.read(length)
89
+ end
90
+
91
+ def read_lpstringz
92
+ string = read_lpstring
93
+ reader.read(1) # null
94
+ string
95
+ end
96
+
97
+ def read_nstring(length)
98
+ string = reader.read(length)
99
+ string
100
+ end
101
+
102
+ def read_nstringz(length)
103
+ string = reader.read(length)
104
+ reader.read(1) # null
105
+ string
106
+ end
107
+
108
+ def read_uint8_array(length)
109
+ reader.read(length).bytes.to_a
110
+ end
111
+
112
+ def read_bit_array(length)
113
+ data = reader.read((length+7)/8)
114
+ data.unpack("b*").first.bytes.to_a.map { |i| (i-48) == 1 }.shift(length)
115
+ end
116
+
117
+ def read_and_unpack(format_description)
118
+ @format_cache[format_description] ||= {}
119
+ this_format = @format_cache[format_description][:format] ||=
120
+ format_description.inject("") { |o, f| o+(f[:format] || "") }
121
+ this_length = @format_cache[format_description][:length] ||=
122
+ format_description.inject(0) { |o, f| o+(f[:length] || 0) }
123
+
124
+ fields = {}
125
+
126
+ fields_array = reader.read(this_length).unpack(this_format)
127
+ format_description.each_with_index do |field, index|
128
+ fields[field[:name]] = fields_array[index]
129
+ end
130
+
131
+ fields
132
+ end
133
+
134
+ def read_mysql_type(column_type)
135
+ case column_type
136
+ #when :decimal
137
+ when :tiny
138
+ read_uint8
139
+ when :short
140
+ read_uint16
141
+ when :int24
142
+ read_uint24
143
+ when :long
144
+ read_uint32
145
+ when :longlong
146
+ read_uint64
147
+ when :string
148
+ length = read_varint
149
+ read_nstring(length)
150
+
151
+ when :float
152
+ read_float
153
+ when :double
154
+ read_double
155
+ #when :null
156
+ when :timestamp
157
+ read_uint32
158
+ #when :date
159
+ #when :time
160
+ #when :datetime
161
+ #when :year
162
+ #when :newdate
163
+ #when :varchar
164
+ #when :bit
165
+ #when :newdecimal
166
+ #when :enum
167
+ #when :set
168
+ #when :tiny_blob
169
+ #when :medium_blob
170
+ #when :long_blob
171
+ #when :blob
172
+ #when :var_string
173
+ #when :string
174
+ #when :geometry
175
+ end
176
+ end
177
+
178
+ end
179
+ end
@@ -0,0 +1,234 @@
1
+ require 'mysql_binlog/binlog_parser'
2
+
3
+ module MysqlBinlog
4
+ def puts_hex(data)
5
+ hex = data.bytes.each_slice(24).inject("") do |string, slice|
6
+ string << slice.map { |b| "%02x" % b }.join(" ") + "\n"
7
+ string
8
+ end
9
+ puts hex
10
+ end
11
+
12
+ MAGIC = [
13
+ { :name => :magic, :length => 4, :format => "V" },
14
+ ]
15
+
16
+ EVENT_HEADER = [
17
+ { :name => :timestamp, :length => 4, :format => "V" },
18
+ { :name => :event_type, :length => 1, :format => "C" },
19
+ { :name => :server_id, :length => 4, :format => "V" },
20
+ { :name => :event_length, :length => 4, :format => "V" },
21
+ { :name => :next_position, :length => 4, :format => "V" },
22
+ { :name => :flags, :length => 2, :format => "v" },
23
+ ]
24
+
25
+ EVENT_TYPES = [
26
+ :unknown_event, # 0
27
+ :start_event_v3, # 1
28
+ :query_event, # 2
29
+ :stop_event, # 3
30
+ :rotate_event, # 4
31
+ :intvar_event, # 5
32
+ :load_event, # 6
33
+ :slave_event, # 7
34
+ :create_file_event, # 8
35
+ :append_block_event, # 9
36
+ :exec_load_event, # 10
37
+ :delete_file_event, # 11
38
+ :new_load_event, # 12
39
+ :rand_event, # 13
40
+ :user_var_event, # 14
41
+ :format_description_event, # 15
42
+ :xid_event, # 16
43
+ :begin_load_query_event, # 17
44
+ :execute_load_query_event, # 18
45
+ :table_map_event, # 19
46
+ :pre_ga_write_rows_event, # 20
47
+ :pre_ga_update_rows_event, # 21
48
+ :pre_ga_delete_rows_event, # 22
49
+ :write_rows_event, # 23
50
+ :update_rows_event, # 24
51
+ :delete_rows_event, # 25
52
+ :incident_event, # 26
53
+ :heartbeat_log_event, # 27
54
+ ]
55
+
56
+ EVENT_FLAGS = {
57
+ :binlog_in_use => 0x01,
58
+ :thread_specific => 0x04,
59
+ :suppress_use => 0x08,
60
+ :artificial => 0x20,
61
+ :relay_log => 0x40,
62
+ }
63
+
64
+ EVENT_FORMATS = {
65
+ :format_description_event => [
66
+ { :name => :binlog_version, :length => 2, :format => "v" },
67
+ { :name => :server_version, :length => 50, :format => "A50" },
68
+ { :name => :create_timestamp, :length => 4, :format => "V" },
69
+ { :name => :header_length, :length => 1, :format => "C" },
70
+ ],
71
+ :rotate_event => [
72
+ { :name => :pos, :length => 8, :format => "Q" },
73
+ ],
74
+ :query_event => [
75
+ { :name => :thread_id, :length => 4, :format => "V" },
76
+ { :name => :elapsed_time, :length => 4, :format => "V" },
77
+ { :name => :db_length, :length => 1, :format => "C" },
78
+ { :name => :error_code, :length => 2, :format => "v" },
79
+ { :name => :status_length, :length => 2, :format => "v" },
80
+ ],
81
+ :intvar_event => [
82
+ { :name => :intvar_type, :length => 1, :format => "C" },
83
+ { :name => :intvar_value, :length => 8, :format => "Q" },
84
+ ],
85
+ :xid_event => [
86
+ { :name => :xid, :length => 8, :format => "Q" },
87
+ ],
88
+ :rand_event => [ # Untested
89
+ { :name => :seed1, :length => 8, :format => "Q" },
90
+ { :name => :seed2, :length => 8, :format => "Q" },
91
+ ],
92
+ }
93
+
94
+ class UnsupportedVersionException < Exception; end
95
+ class MalformedBinlogException < Exception; end
96
+ class ZeroReadException < Exception; end
97
+ class ShortReadException < Exception; end
98
+
99
+ class Binlog
100
+ attr_reader :fde
101
+ attr_accessor :reader
102
+ attr_accessor :parser
103
+ attr_accessor :event_field_parser
104
+ attr_accessor :filter_event_types
105
+ attr_accessor :filter_flags
106
+ attr_accessor :max_query_length
107
+
108
+ def initialize(reader_class, *args)
109
+ @reader = reader_class.new(*args)
110
+ @parser = BinlogParser.new(self)
111
+ @event_field_parser = BinlogEventFieldParser.new(self)
112
+ @fde = nil
113
+ @filter_event_types = nil
114
+ @filter_flags = nil
115
+ @max_query_length = 1048576
116
+ end
117
+
118
+ def rewind
119
+ reader.rewind
120
+ read_file_header
121
+ end
122
+
123
+ def read_additional_fields(event_type, header, fields)
124
+ if event_field_parser.methods.include? event_type.to_s
125
+ event_field_parser.send(event_type, header, fields)
126
+ end
127
+ end
128
+
129
+ def skip_event(header)
130
+ reader.skip(header)
131
+ end
132
+
133
+ def read_event_header
134
+ header = parser.read_and_unpack(EVENT_HEADER)
135
+ flags = EVENT_FLAGS.inject([]) do |result, (flag_name, flag_bit_value)|
136
+ if (header[:flags] & flag_bit_value) != 0
137
+ result << flag_name
138
+ end
139
+ result
140
+ end
141
+ header[:flags] = flags
142
+ header
143
+ end
144
+
145
+ def read_event_content(header)
146
+ content = nil
147
+
148
+ event_type = EVENT_TYPES[header[:event_type]]
149
+ if EVENT_FORMATS.include? event_type
150
+ content = parser.read_and_unpack(EVENT_FORMATS[event_type])
151
+ else
152
+ content = {}
153
+ end
154
+
155
+ read_additional_fields(event_type, header, content)
156
+
157
+ skip_event(header)
158
+ content
159
+ end
160
+
161
+ def read_event
162
+ while true
163
+ skip_this_event = false
164
+ return nil if reader.end?
165
+
166
+ filename = reader.filename
167
+ position = reader.position
168
+
169
+ unless header = read_event_header
170
+ return nil
171
+ end
172
+
173
+ event_type = EVENT_TYPES[header[:event_type]]
174
+
175
+ if @filter_event_types
176
+ unless @filter_event_types.include? event_type or
177
+ event_type == :format_description_event
178
+ skip_event(header)
179
+ skip_this_event = true
180
+ end
181
+ end
182
+
183
+ if @filter_flags
184
+ unless @filter_flags.include? header[:flags]
185
+ skip_event(header)
186
+ skip_this_event = true
187
+ end
188
+ end
189
+
190
+ unless [:rotate_event, :format_description_event].include? event_type
191
+ next if skip_this_event
192
+ end
193
+
194
+ content = read_event_content(header)
195
+ content
196
+
197
+ case event_type
198
+ when :rotate_event
199
+ reader.rotate(content[:name], content[:pos])
200
+ when :format_description_event
201
+ process_fde(content)
202
+ end
203
+
204
+ break
205
+ end
206
+
207
+ {
208
+ :type => event_type,
209
+ :filename => filename,
210
+ :position => position,
211
+ :header => header,
212
+ :event => content,
213
+ }
214
+ end
215
+
216
+ def process_fde(fde)
217
+ if (version = fde[:binlog_version]) != 4
218
+ raise UnsupportedVersionException.new("Binlog version #{version} is not supported")
219
+ end
220
+
221
+ @fde = {
222
+ :header_length => fde[:header_length],
223
+ :binlog_version => fde[:binlog_version],
224
+ :server_version => fde[:server_version],
225
+ }
226
+ end
227
+
228
+ def each_event
229
+ while event = read_event
230
+ yield event
231
+ end
232
+ end
233
+ end
234
+ end
@@ -0,0 +1,65 @@
1
+ module MysqlBinlog
2
+ class BinlogFileReader
3
+ def initialize(filename)
4
+ @filename = filename
5
+ @binlog = nil
6
+
7
+ open_file(filename)
8
+ end
9
+
10
+ def open_file(filename)
11
+ @filename = filename
12
+ @binlog = File.open(filename, mode="r")
13
+
14
+ if (magic = read(4).unpack("V").first) != 1852400382
15
+ raise MalformedBinlogException.new("Magic number #{magic} is incorrect")
16
+ end
17
+ end
18
+
19
+ def rotate(filename, position)
20
+ open_file(filename)
21
+ seek(position)
22
+ end
23
+
24
+ def filename
25
+ @filename
26
+ end
27
+
28
+ def position
29
+ @binlog.tell
30
+ end
31
+
32
+ def rewind
33
+ @binlog.rewind
34
+ end
35
+
36
+ def seek(pos)
37
+ @binlog.seek(pos)
38
+ end
39
+
40
+ def end?
41
+ @binlog.eof?
42
+ end
43
+
44
+ def remaining(header)
45
+ header[:next_position] - @binlog.tell
46
+ end
47
+
48
+ def skip(header)
49
+ seek(header[:next_position])
50
+ end
51
+
52
+ def read(length)
53
+ return "" if length == 0
54
+ data = @binlog.read(length)
55
+ if !data
56
+ raise MalformedBinlogException.new
57
+ elsif data.length == 0
58
+ raise ZeroReadException.new
59
+ elsif data.length < length
60
+ raise ShortReadException.new
61
+ end
62
+ data
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,64 @@
1
+ module MysqlBinlog
2
+ class BinlogStreamReader
3
+ def initialize(connection, filename, position)
4
+ require 'mysql_binlog_dump'
5
+ @filename = nil
6
+ @position = nil
7
+ @packet_data = nil
8
+ @packet_pos = nil
9
+ @connection = connection
10
+ MysqlBinlogDump.binlog_dump(connection, filename, position)
11
+ end
12
+
13
+ def rotate(filename, position)
14
+ puts "rotate called with #{filename}:#{position}"
15
+ @filename = filename
16
+ @position = position
17
+ end
18
+
19
+ def filename
20
+ @filename
21
+ end
22
+
23
+ def position
24
+ @position
25
+ end
26
+
27
+ def rewind
28
+ false
29
+ end
30
+
31
+ def tell
32
+ @packet_pos
33
+ end
34
+
35
+ def end?
36
+ false
37
+ end
38
+
39
+ def remaining(header)
40
+ @packet_data.length - @packet_pos
41
+ end
42
+
43
+ def skip(header)
44
+ @packet_data = nil
45
+ @packet_pos = nil
46
+ end
47
+
48
+ def read_packet
49
+ @packet_data = MysqlBinlogDump.next_packet(@connection)
50
+ @packet_pos = 0
51
+ end
52
+
53
+ def read(length)
54
+ unless @packet_data
55
+ read_packet
56
+ return nil unless @packet_data
57
+ end
58
+ pos = @packet_pos
59
+ @position += length if @position
60
+ @packet_pos += length
61
+ @packet_data[pos...(pos+length)]
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,4 @@
1
+ require 'mysql_binlog/mysql_binlog'
2
+ require 'mysql_binlog/binlog_event_field_parser'
3
+ require 'mysql_binlog/reader/binlog_file_reader'
4
+ require 'mysql_binlog/reader/binlog_stream_reader'
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mysql_binlog
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - Jeremy Cole
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-06-24 00:00:00 Z
19
+ dependencies: []
20
+
21
+ description: Library for parsing MySQL binary logs in Ruby
22
+ email: jeremy@jcole.us
23
+ executables:
24
+ - mysql_binlog_dump
25
+ extensions: []
26
+
27
+ extra_rdoc_files: []
28
+
29
+ files:
30
+ - lib/mysql_binlog.rb
31
+ - lib/mysql_binlog/mysql_binlog.rb
32
+ - lib/mysql_binlog/binlog_parser.rb
33
+ - lib/mysql_binlog/binlog_event_field_parser.rb
34
+ - lib/mysql_binlog/reader/binlog_file_reader.rb
35
+ - lib/mysql_binlog/reader/binlog_stream_reader.rb
36
+ - bin/mysql_binlog_dump
37
+ homepage: http://jcole.us/
38
+ licenses: []
39
+
40
+ post_install_message:
41
+ rdoc_options: []
42
+
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ none: false
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ hash: 3
51
+ segments:
52
+ - 0
53
+ version: "0"
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ hash: 3
60
+ segments:
61
+ - 0
62
+ version: "0"
63
+ requirements: []
64
+
65
+ rubyforge_project:
66
+ rubygems_version: 1.8.10
67
+ signing_key:
68
+ specification_version: 3
69
+ summary: MySQL Binary Log Parser
70
+ test_files: []
71
+