~percona-toolkit-dev/percona-toolkit/pt-agent

« back to all changes in this revision

Viewing changes to t/lib/QueryAdvisorRules.t

  • Committer: Daniel Nichter
  • Date: 2011-06-24 17:22:06 UTC
  • Revision ID: daniel@percona.com-20110624172206-c7q4s4ad6r260zz6
Add lib/, t/lib/, and sandbox/.  All modules are updated and passing on MySQL 5.1.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/perl
 
2
 
 
3
BEGIN {
 
4
   die "The PERCONA_TOOLKIT_BRANCH environment variable is not set.\n"
 
5
      unless $ENV{PERCONA_TOOLKIT_BRANCH} && -d $ENV{PERCONA_TOOLKIT_BRANCH};
 
6
   unshift @INC, "$ENV{PERCONA_TOOLKIT_BRANCH}/lib";
 
7
};
 
8
 
 
9
use strict;
 
10
use warnings FATAL => 'all';
 
11
use English qw(-no_match_vars);
 
12
use Test::More tests => 87;
 
13
 
 
14
use MaatkitTest;
 
15
use PodParser;
 
16
use AdvisorRules;
 
17
use QueryAdvisorRules;
 
18
use Advisor;
 
19
use SQLParser;
 
20
 
 
21
# This test should just test that the QueryAdvisor module conforms to the
 
22
# expected interface:
 
23
#   - It has a get_rules() method that returns a list of hashrefs:
 
24
#     ({ID => 'ID', code => $code}, {ID => ..... }, .... )
 
25
#   - It has a load_rule_info() method that accepts a list of hashrefs, which
 
26
#     we'll use to load rule info from POD.  Our built-in rule module won't
 
27
#     store its own rule info.  But plugins supplied by users should.
 
28
#   - It has a get_rule_info() method that accepts an ID and returns a hashref:
 
29
#     {ID => 'ID', Severity => 'NOTE|WARN|CRIT', Description => '......'}
 
30
my $p   = new PodParser();
 
31
my $qar = new QueryAdvisorRules(PodParser => $p);
 
32
 
 
33
my @rules = $qar->get_rules();
 
34
ok(
 
35
   scalar @rules,
 
36
   'Returns array of rules'
 
37
);
 
38
 
 
39
my $rules_ok = 1;
 
40
foreach my $rule ( @rules ) {
 
41
   if (    !$rule->{id}
 
42
        || !$rule->{code}
 
43
        || (ref $rule->{code} ne 'CODE') )
 
44
   {
 
45
      $rules_ok = 0;
 
46
      last;
 
47
   }
 
48
}
 
49
ok(
 
50
   $rules_ok,
 
51
   'All rules are proper'
 
52
);
 
53
 
 
54
# QueryAdvisorRules.pm has more rules than mqa-rule-LIT.001.pod so to avoid
 
55
# "There is no info" errors we remove all but LIT.001.
 
56
@rules = grep { $_->{id} eq 'LIT.001' } @rules;
 
57
 
 
58
# Test that we can load rule info from POD.  Make a sample POD file that has a
 
59
# single sample rule definition for LIT.001 or something.
 
60
$qar->load_rule_info(
 
61
   rules    => \@rules,
 
62
   file     => "$trunk/t/lib/samples/pod/mqa-rule-LIT.001.pod",
 
63
   section  => 'RULES',
 
64
);
 
65
 
 
66
# We shouldn't be able to load the same rule info twice.
 
67
throws_ok(
 
68
   sub {
 
69
      $qar->load_rule_info(
 
70
         rules    => \@rules,
 
71
         file     => "$trunk/t/lib/samples/pod/mqa-rule-LIT.001.pod",
 
72
         section  => 'RULES',
 
73
      );
 
74
   },
 
75
   qr/Rule \S+ is already defined/,
 
76
   'Duplicate rule info is caught'
 
77
);
 
78
 
 
79
# Test that we can now get a hashref as described above.
 
80
is_deeply(
 
81
   $qar->get_rule_info('LIT.001'),
 
82
   {  id          => 'LIT.001',
 
83
      severity    => 'note',
 
84
      description => "IP address used as string.  The string literal looks like an IP address but is not used inside INET_ATON().  WHERE ip='127.0.0.1' is better as ip=INET_ATON('127.0.0.1') if the column is numeric.",
 
85
   },
 
86
   'get_rule_info(LIT.001) works',
 
87
);
 
88
 
 
89
# Test getting a nonexistent rule.
 
90
is(
 
91
   $qar->get_rule_info('BAR.002'),
 
92
   undef,
 
93
   "get_rule_info() nonexistent rule"
 
94
);
 
95
 
 
96
is(
 
97
   $qar->get_rule_info(),
 
98
   undef,
 
99
   "get_rule_info(undef)"
 
100
);
 
101
 
 
102
# Add a rule for which there is no POD info and test that it's not allowed.
 
103
push @rules, {
 
104
   id   => 'FOO.001',
 
105
   code => sub { return },
 
106
};
 
107
$qar->_reset_rule_info();  # else we'll get "cannot redefine rule" error
 
108
throws_ok (
 
109
   sub {
 
110
      $qar->load_rule_info(
 
111
         rules    => \@rules,
 
112
         file     => "$trunk/t/lib/samples/pod/mqa-rule-LIT.001.pod",
 
113
         section  => 'RULES',
 
114
      );
 
115
   },
 
116
   qr/There is no info for rule FOO.001/,
 
117
   "Doesn't allow rules without info",
 
118
);
 
119
 
 
120
# ###########################################################################
 
121
# Test cases for the rules themselves.
 
122
# ###########################################################################
 
123
my @cases = (
 
124
   {  name   => 'IP address not inside INET_ATON, plus SELECT * is used',
 
125
      query  => 'SELECT * FROM tbl WHERE ip="127.0.0.1"',
 
126
      advice => [qw(COL.001 LIT.001)],
 
127
      pos    => [0, 37],
 
128
   },
 
129
   {  name   => 'Date literal not quoted',
 
130
      query  => 'SELECT col FROM tbl WHERE col < 2001-01-01',
 
131
      advice => [qw(LIT.002)],
 
132
   },
 
133
   {  name   => 'Aliases without AS keyword',
 
134
      query  => 'SELECT a b FROM tbl',
 
135
      advice => [qw(ALI.001 CLA.001)],
 
136
   },
 
137
   {  name   => 'tbl.* alias',
 
138
      query  => 'SELECT tbl.* foo FROM bar WHERE id=1',
 
139
      advice => [qw(ALI.001 ALI.002 COL.001)],
 
140
   },
 
141
   {  name   => 'tbl as tbl',
 
142
      query  => 'SELECT col FROM tbl AS tbl WHERE id=1',
 
143
      advice => [qw(ALI.003)],
 
144
   },
 
145
   {  name   => 'col as col',
 
146
      query  => 'SELECT col AS col FROM tbl AS `my tbl` WHERE id=1',
 
147
      advice => [qw(ALI.003)],
 
148
   },
 
149
   {  name   => 'Blind INSERT',
 
150
      query  => 'INSERT INTO tbl VALUES(1),(2)',
 
151
      advice => [qw(COL.002)],
 
152
   },
 
153
   {  name   => 'Blind INSERT',
 
154
      query  => 'INSERT tbl VALUE (1)',
 
155
      advice => [qw(COL.002)],
 
156
   },
 
157
   {  name   => 'SQL_CALC_FOUND_ROWS',
 
158
      query  => 'SELECT SQL_CALC_FOUND_ROWS col FROM tbl AS alias WHERE id=1',
 
159
      advice => [qw(KWR.001)],
 
160
   },
 
161
   {  name   => 'All comma joins ok',
 
162
      query  => 'SELECT col FROM tbl1, tbl2 WHERE tbl1.id=tbl2.id',
 
163
      advice => [],
 
164
   },
 
165
   {  name   => 'All ANSI joins ok',
 
166
      query  => 'SELECT col FROM tbl1 JOIN tbl2 USING(id) WHERE tbl1.id>10',
 
167
      advice => [],
 
168
   },
 
169
   {  name   => 'Mix comman/ANSI joins',
 
170
      query  => 'SELECT col FROM tbl, tbl1 JOIN tbl2 USING(id) WHERE tbl.d>10',
 
171
      advice => [qw(JOI.001)],
 
172
   },
 
173
   {  name   => 'Non-deterministic GROUP BY',
 
174
      query  => 'select a, b, c from tbl where foo="bar" group by a',
 
175
      advice => [qw(RES.001)],
 
176
   },
 
177
   {  name   => 'Non-deterministic LIMIT w/o ORDER BY',
 
178
      query  => 'select a, b from tbl where foo="bar" limit 10 group by a, b',
 
179
      advice => [qw(RES.002)],
 
180
   },
 
181
   {  name   => 'ORDER BY RAND()',
 
182
      query  => 'select a from t where id=1 order by rand()',
 
183
      advice => [qw(CLA.002)],
 
184
   },
 
185
   {  name   => 'ORDER BY RAND(N)',
 
186
      query  => 'select a from t where id=1 order by rand(123)',
 
187
      advice => [qw(CLA.002)],
 
188
   },
 
189
   {  name   => 'LIMIT w/ OFFSET does not scale',
 
190
      query  => 'select a from t where i=1 limit 10, 10 order by a',
 
191
      advice => [qw(CLA.003)],
 
192
   },
 
193
   {  name   => 'LIMIT w/ OFFSET does not scale',
 
194
      query  => 'select a from t where i=1 limit 10 OFFSET 10 order by a',
 
195
      advice => [qw(CLA.003)],
 
196
   },
 
197
   {  name   => 'Leading %wildcard',
 
198
      query  => 'select a from t where i like "%hm"',
 
199
      advice => [qw(ARG.001)],
 
200
   },
 
201
   {  name   => 'Leading _wildcard',
 
202
      query  => 'select a from t where i LIKE "_hm"',
 
203
      advice => [qw(ARG.001)],
 
204
   },
 
205
   {  name   => 'Leading "% wildcard"',
 
206
      query  => 'select a from t where i like "% eh "',
 
207
      advice => [qw(ARG.001)],
 
208
   },
 
209
   {  name   => 'Leading "_ wildcard"',
 
210
      query  => 'select a from t where i LIKE "_ eh "',
 
211
      advice => [qw(ARG.001)],
 
212
   },
 
213
   {  name   => 'GROUP BY number',
 
214
      query  => 'select a from t where i <> 4 group by 1',
 
215
      advice => [qw(CLA.004)],
 
216
   },
 
217
   {  name   => '!= instead of <>',
 
218
      query  => 'select a from t where i != 2',
 
219
      advice => [qw(STA.001)],
 
220
   },
 
221
   {  name   => "LIT.002 doesn't match",
 
222
      query  => "update foo.bar set biz = '91848182522'",
 
223
      advice => [],
 
224
   },
 
225
   {  name   => "LIT.002 doesn't match",
 
226
      query  => "update db2.tuningdetail_21_265507 inner join db1.gonzo using(g) set n.c1 = a.c1, n.w3 = a.w3",
 
227
      advice => [],
 
228
   },
 
229
   {  name   => "LIT.002 doesn't match",
 
230
      query  => "UPDATE db4.vab3concept1upload
 
231
                 SET    vab3concept1id = '91848182522'
 
232
                 WHERE  vab3concept1upload='6994465'",
 
233
      advice => [],
 
234
   },
 
235
   {  name   => "LIT.002 at end of query",
 
236
      query  => "select c from t where d=2006-10-10",
 
237
      advice => [qw(LIT.002)],
 
238
   },
 
239
   {  name   => "LIT.002 5 digits doesn't match",
 
240
      query  => "select c from t where d=12345",
 
241
      advice => [],
 
242
   },
 
243
   {  name   => "LIT.002 7 digits doesn't match",
 
244
      query  => "select c from t where d=1234567",
 
245
      advice => [],
 
246
   },
 
247
   {  name   => "SELECT var LIMIT",
 
248
      query  => "select \@\@version_comment limit 1 ",
 
249
      advice => [],
 
250
   },
 
251
   {  name   => "Date with time",
 
252
      query  => "select c from t where d > 2010-03-15 09:09:09",
 
253
      advice => [qw(LIT.002)],
 
254
   },
 
255
   {  name   => "Date with time and subseconds",
 
256
      query  => "select c from t where d > 2010-03-15 09:09:09.123456",
 
257
      advice => [qw(LIT.002)],
 
258
   },
 
259
   {  name   => "Date with time doesn't match",
 
260
      query  => "select c from t where d > '2010-03-15 09:09:09'",
 
261
      advice => [qw()],
 
262
   },
 
263
   {  name   => "Date with time and subseconds doesn't match",
 
264
      query  => "select c from t where d > '2010-03-15 09:09:09.123456'",
 
265
      advice => [qw()],
 
266
   },
 
267
   {  name   => "Short date",
 
268
      query  => "select c from t where d=73-03-15",
 
269
      advice => [qw(LIT.002)],
 
270
   },
 
271
   {  name   => "Short date with time",
 
272
      query  => "select c from t where d > 73-03-15 09:09:09",
 
273
      advice => [qw(LIT.002)],
 
274
      pos    => [34],
 
275
   },
 
276
   {  name   => "Short date with time and subseconds",
 
277
      query  => "select c from t where d > 73-03-15 09:09:09.123456",
 
278
      advice => [qw(LIT.002)],
 
279
   },
 
280
   {  name   => "Short date with time doesn't match",
 
281
      query  => "select c from t where d > '73-03-15 09:09:09'",
 
282
      advice => [qw()],
 
283
   },
 
284
   {  name   => "Short date with time and subseconds doesn't match",
 
285
      query  => "select c from t where d > '73-03-15 09:09:09.123456'",
 
286
      advice => [qw()],
 
287
   },
 
288
   {  name   => "LIKE without wildcard",
 
289
      query  => "select c from t where i like 'lamp'",
 
290
      advice => [qw(ARG.002)],
 
291
   },
 
292
   {  name   => "LIKE without wildcard, 2nd arg",
 
293
      query  => "select c from t where i like 'lamp%' or i like 'foo'",
 
294
      advice => [qw(ARG.002)],
 
295
   },
 
296
   {  name   => "LIKE with wildcard %",
 
297
      query  => "select c from t where i like 'lamp%'",
 
298
      advice => [qw()],
 
299
   },
 
300
   {  name   => "LIKE with wildcard _",
 
301
      query  => "select c from t where i like 'lamp_'",
 
302
      advice => [qw()],
 
303
   },
 
304
   {  name   => "Issue 946: LIT.002 false-positive",
 
305
      query  => "delete from t where d in('MD6500-26', 'MD6500-21-22', 'MD6214')",
 
306
      advice => [qw()],
 
307
   },
 
308
   {  name   => "Issue 946: LIT.002 false-positive",
 
309
      query  => "delete from t where d in('FS-8320-0-2', 'FS-800-6')",
 
310
      advice => [qw()],
 
311
   },
 
312
# This matches LIT.002 but unless the regex gets really complex or
 
313
# we do this rule another way, this will have to remain an exception.
 
314
#   {  name   => "Issue 946: LIT.002 false-positive",
 
315
#      query  => "select c from t where c='foo 2010-03-17 bar'",
 
316
#      advice => [qw()],
 
317
#   },
 
318
 
 
319
   {  name   => "IN(subquer)",
 
320
      query  => "select c from t where i in(select d from z where 1)",
 
321
      advice => [qw(SUB.001)],
 
322
      pos    => [33],
 
323
   },
 
324
   {  name   => "JOI.002",
 
325
      query  => "select c from `w_chapter` INNER JOIN `w_series` AS `w_chapter__series` ON `w_chapter`.`series_id` = `w_chapter__series`.`id`, `w_series`, `auth_user` where id=1",
 
326
      advice => [qw(JOI.001 JOI.002)],
 
327
   },
 
328
   {  name   => "JOI.002 ansi self-join ok",
 
329
      query  => "select c from employees as e join employees as s on e.supervisor = s.id where foo='bar'",
 
330
      advice => [],
 
331
   },
 
332
   {  name   => "JOI.002 ansi self-join with other joins ok",
 
333
      query  => "select c from employees as e join employees as s on e.supervisor = s.id join employees as r on s.id = r.foo where foo='bar'",
 
334
      advice => [],
 
335
   },
 
336
   {  name   => "JOI.002 comma self-join ok",
 
337
      query  => "select c from employees as e, employees as s where e.supervisor = s.id",
 
338
      advice => [],
 
339
   },
 
340
   {  name   => "CLA.005 ORDER BY col=<constant>",
 
341
      query  => "select col1, col2 from tbl where col3=5 order by col3, col4",
 
342
      advice => [qw(CLA.005)],
 
343
   },
 
344
   # Now col3 is not a constant, it's the string '5'.
 
345
   {  name   => "CLA.005 not tricked by '5'",
 
346
      query  => "select col1, col2 from tbl where col3='5' order by col3, col4",
 
347
      advice => [],
 
348
   },
 
349
   {  name   => "JOI.003",
 
350
      query  => "select c from L left join R using(c) where L.a=5 and R.b=10",
 
351
      advice => [qw(JOI.003)],
 
352
   },
 
353
   {  name   => "JOI.003 ok with IS NULL",
 
354
      query  => "select c from L left join R using(c) where L.a=5 and R.c is null",
 
355
      advice => [],
 
356
   },
 
357
   {  name   => "JOI.003 ok without outer table column",
 
358
      query  => "select c from L left join R using(c) where L.a=5",
 
359
      advice => [],
 
360
   },
 
361
   {  name   => "JOI.003 RIGHT",
 
362
      query  => "select c from L right join R using(c) where R.a=5 and L.b=10",
 
363
      advice => [qw(JOI.003)],
 
364
   },
 
365
   {  name   => "JOI.003 RIGHT ok with IS NULL",
 
366
      query  => "select c from L right join R using(c) where R.a=5 and L.c is null",
 
367
      advice => [],
 
368
   },
 
369
   {  name   => "JOI.003 RIGHT ok without outer table column",
 
370
      query  => "select c from L right join R using(c) where R.a=5",
 
371
      advice => [],
 
372
   },
 
373
   {  name   => "JOI.003 ok with INNER JOIN",
 
374
      query  => "select c from L inner join R using(c) where R.a=5 and L.b=10",
 
375
      advice => [],
 
376
   },
 
377
   {  name   => "JOI.003 ok with JOIN",
 
378
      query  => "select c from L join R using(c) where R.a=5 and L.b=10",
 
379
      advice => [],
 
380
   },
 
381
   {  name   => "JOI.004",
 
382
      query  => "select c from L left join R on a=b where L.a=5 and R.c is null",
 
383
      tbl_structs => {
 
384
         db => {
 
385
            L => { name => 'L', is_col => { a => 1         } },
 
386
            R => { name => 'R', is_col => { b => 1, c => 1 } },
 
387
         },
 
388
      },
 
389
      advice => [qw(JOI.004)],
 
390
   },
 
391
   {  name   => "JOI.004 USING (b)",
 
392
      query  => "select c from L left join R using(b) where L.a=5 and R.c is null",
 
393
      advice => [qw(JOI.004)],
 
394
   },
 
395
   {  name   => "JOI.004 without table info",
 
396
      query  => "select c from L left join R on a=b where L.a=5 and R.c is null",
 
397
      advice => [qw(JOI.004)],
 
398
   },
 
399
   {  name   => "JOI.004 good exclusion join",
 
400
      query  => "select c from L left join R on a=b where L.a=5 and R.b is null",
 
401
      tbl_structs => {
 
402
         db => {
 
403
            L => { name => 'L', is_col => { a => 1         } },
 
404
            R => { name => 'R', is_col => { b => 1, c => 1 } },
 
405
         },
 
406
      },
 
407
      advice => [],
 
408
   },
 
409
   {  name   => "JOI.004 RIGHT",
 
410
      query  => "select c from L right join R on a=b where R.a=5 and L.c is null",
 
411
      tbl_structs => {
 
412
         db => {
 
413
            L => { name => 'L', is_col => { a => 1, c => 1 } },
 
414
            R => { name => 'R', is_col => { b => 1,        } },
 
415
         },
 
416
      },
 
417
      advice => [qw(JOI.004)],
 
418
   },
 
419
   {  name   => "JOI.004 can table-qualify cols from WHERE",
 
420
      query  => "select c from L left join R on a=b where a=5 and c is null",
 
421
      tbl_structs => {
 
422
         db => {
 
423
            L => { name => 'L', is_col => { a => 1         } },
 
424
            R => { name => 'R', is_col => { b => 1, c => 1 } },
 
425
         },
 
426
      },
 
427
      advice => [qw(JOI.004)],
 
428
   },
 
429
   {  name   => "CLA.006 GROUP BY different tables",
 
430
      query  => "select id from tbl1 join tbl2 using(a) where 1 group by tbl1.id, tbl2.id",
 
431
      advice => [qw(CLA.006)],
 
432
   },
 
433
   {  name   => "CLA.006 ORDER BY different tables",
 
434
      query  => "select id from tbl1 join tbl2 using(a) where 1 order by tbl1.id, tbl2.id",
 
435
      advice => [qw(CLA.006)],
 
436
   },
 
437
   {  name   => "CLA.006 GROUP BY tbl_a ORDER BY tbl_b",
 
438
      query  => "select id from tbl1 join tbl2 using(a) where 1 group by tbl1.id order by tbl2.id",
 
439
      advice => [qw(CLA.006)],
 
440
   },
 
441
   {  name   => "CLA.006 GROUP BY tbl_a ORDER BY tbl_b (2)",
 
442
      query  => "select id, foo from tbl1 join tbl2 using(a) where 1 group by tbl1.id order by tbl2.id, tbl1.foo",
 
443
      advice => [qw(CLA.006 RES.001)],
 
444
   },
 
445
   {  name   => "CLA.006 GROUP BY tbl_a ORDER BY tbl_b (3)",
 
446
      query  => "select id,foo from tbl1 join tbl2 using(a) where 1 group by tbl1.id, tbl2.foo order by tbl2.id",
 
447
      advice => [qw(CLA.006)],
 
448
   },
 
449
   # CLA.006 cannot be detected without table qualifications for every column
 
450
   {  name   => "CLA.006 without full table qualifications",
 
451
      query  => "select id from tbl1 join tbl2 using(a) where 1 group by id order by tbl1.id",
 
452
      advice => [],
 
453
   },
 
454
   {
 
455
      name   => 'Issue 1163, ARG.001 false-positive',
 
456
      query  => "SELECT COUNT(*) FROM foo WHERE meta_key = '_edit_lock' AND post_id = 488",
 
457
      advice => [qw()],
 
458
   },
 
459
   {
 
460
      name   => 'Issue 1163, RES.001 false-positive',
 
461
      query  => "SELECT YEAR(post_date) AS `year`, MONTH(post_date) AS `month`, count(ID) as posts FROM foo_posts  WHERE post_type = 'post' AND post_status = 'publish' GROUP BY YEAR(post_date), MONTH(post_date) ORDER BY post_date DESC",
 
462
      advice => [qw()],
 
463
   },
 
464
   {
 
465
      name   => 'CLA.007 ORDER BY ASC and DESC',
 
466
      query  => "select col1, col2 from tbl where i=1 order by col1, col2 desc",
 
467
      advice => [qw(CLA.007)],
 
468
   },
 
469
);
 
470
 
 
471
# Run the test cases.
 
472
$qar = new QueryAdvisorRules(PodParser => $p);
 
473
$qar->load_rule_info(
 
474
   rules   => [ $qar->get_rules() ],
 
475
   file    => "$trunk/bin/pt-query-advisor",
 
476
   section => 'RULES',
 
477
);
 
478
 
 
479
my $adv = new Advisor(match_type => "pos");
 
480
$adv->load_rules($qar);
 
481
$adv->load_rule_info($qar);
 
482
 
 
483
my $sp = new SQLParser();
 
484
 
 
485
foreach my $test ( @cases ) {
 
486
   my $query_struct = $sp->parse($test->{query});
 
487
   my $event = {
 
488
      arg          => $test->{query},
 
489
      query_struct => $query_struct,
 
490
      tbl_structs  => $test->{tbl_structs},
 
491
   };
 
492
   my ($ids, $pos) = $adv->run_rules(
 
493
      event       => $event,
 
494
   );
 
495
   is_deeply(
 
496
      $ids,
 
497
      $test->{advice},
 
498
      $test->{name},
 
499
   );
 
500
 
 
501
   if ( $test->{pos} ) {
 
502
      is_deeply(
 
503
         $pos,
 
504
         $test->{pos},
 
505
         "$test->{name} matched near pos"
 
506
      );
 
507
   }
 
508
 
 
509
   # To help me debug.
 
510
   die if $test->{stop};
 
511
}
 
512
 
 
513
# #############################################################################
 
514
# Done.
 
515
# #############################################################################
 
516
my $output = '';
 
517
{
 
518
   local *STDERR;
 
519
   open STDERR, '>', \$output;
 
520
   $p->_d('Complete test coverage');
 
521
}
 
522
like(
 
523
   $output,
 
524
   qr/Complete test coverage/,
 
525
   '_d() works'
 
526
);
 
527
exit;