xref: /illumos-gate/usr/src/cmd/dtrace/test/cmd/scripts/dtest.pl (revision e0731422366620894c16c1ee6515551c5f00733d)
1#!/usr/perl5/bin/perl
2#
3# CDDL HEADER START
4#
5# The contents of this file are subject to the terms of the
6# Common Development and Distribution License (the "License").
7# You may not use this file except in compliance with the License.
8#
9# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
10# or http://www.opensolaris.org/os/licensing.
11# See the License for the specific language governing permissions
12# and limitations under the License.
13#
14# When distributing Covered Code, include this CDDL HEADER in each
15# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
16# If applicable, add the following below this CDDL HEADER, with the
17# fields enclosed by brackets "[]" replaced with your own identifying
18# information: Portions Copyright [yyyy] [name of copyright owner]
19#
20# CDDL HEADER END
21#
22
23#
24# Copyright 2008 Sun Microsystems, Inc.  All rights reserved.
25# Use is subject to license terms.
26#
27
28#
29# Copyright (c) 2011, Joyent, Inc. All rights reserved.
30#
31require 5.8.4;
32
33use File::Find;
34use File::Basename;
35use Getopt::Std;
36use Cwd;
37use Cwd 'abs_path';
38
39$PNAME = $0;
40$PNAME =~ s:.*/::;
41$OPTSTR = 'abd:fFghi:jlnqsx:';
42$USAGE = "Usage: $PNAME [-abfFghjlnqs] [-d dir] [-i isa] "
43    . "[-x opt[=arg]] [file | dir ...]\n";
44($MACH = `uname -p`) =~ s/\W*\n//;
45($PLATFORM = `uname -i`) =~ s/\W*\n//;
46
47@dtrace_argv = ();
48
49$ksh_path = '/usr/bin/ksh';
50
51@files = ();
52%exceptions = ();
53%results = ();
54$errs = 0;
55
56#
57# If no test files are specified on the command-line, execute a find on "."
58# and append any tst.*.d, tst.*.ksh, err.*.d or drp.*.d files found within
59# the directory tree.
60#
61sub wanted
62{
63	push(@files, $File::Find::name)
64	    if ($_ =~ /^(tst|err|drp)\..+\.(d|ksh)$/ && -f "$_");
65}
66
67sub dirname {
68	my($s) = @_;
69	my($i);
70
71	$s = substr($s, 0, $i) if (($i = rindex($s, '/')) != -1);
72	return $i == -1 ? '.' : $i == 0 ? '/' : $s;
73}
74
75sub inpath
76{
77	my ($exec) = (@_);
78	my @path = File::Spec->path();
79
80	for my $dir (@path) {
81		if (-x $dir . "/" . $exec) {
82			return 1;
83		}
84	}
85
86	return 0;
87}
88
89sub usage
90{
91	print $USAGE;
92	print "\t -a  execute test suite using anonymous enablings\n";
93	print "\t -b  execute bad ioctl test program\n";
94	print "\t -d  specify directory for test results files and cores\n";
95	print "\t -g  enable libumem debugging when running tests\n";
96	print "\t -f  force bypassed tests to run\n";
97	print "\t -F  force tests to be run, even if missing dependencies\n";
98	print "\t -h  display verbose usage message\n";
99	print "\t -i  specify ISA to test instead of isaexec(3C) default\n";
100	print "\t -j  execute test suite using jdtrace (Java API) only\n";
101	print "\t -l  save log file of results and PIDs used by tests\n";
102	print "\t -n  execute test suite using dtrace(1m) only\n";
103	print "\t -q  set quiet mode (only report errors and summary)\n";
104	print "\t -s  save results files even for tests that pass\n";
105	print "\t -x  pass corresponding -x argument to dtrace(1M)\n";
106	exit(2);
107}
108
109sub errmsg
110{
111	my($msg) = @_;
112
113	print STDERR $msg;
114	print LOG $msg if ($opt_l);
115	$errs++;
116}
117
118sub fail
119{
120	my(@parms) = @_;
121	my($msg) = $parms[0];
122	my($errfile) = $parms[1];
123	my($n) = 0;
124	my($dest) = basename($file);
125
126	while (-d "$opt_d/failure.$n") {
127		$n++;
128	}
129
130	unless (mkdir "$opt_d/failure.$n") {
131		warn "ERROR: failed to make directory $opt_d/failure.$n: $!\n";
132		exit(125);
133	}
134
135	open(README, ">$opt_d/failure.$n/README");
136	print README "ERROR: " . $file . " " . $msg;
137
138	if (scalar @parms > 1) {
139		print README "; see $errfile\n";
140	} else {
141		if (-f "$opt_d/$pid.core") {
142			print README "; see $pid.core\n";
143		} else {
144			print README "\n";
145		}
146	}
147
148	close(README);
149
150	if (-f "$opt_d/$pid.out") {
151		rename("$opt_d/$pid.out", "$opt_d/failure.$n/$pid.out");
152		link("$file.out", "$opt_d/failure.$n/$dest.out");
153	}
154
155	if (-f "$opt_d/$pid.err") {
156		rename("$opt_d/$pid.err", "$opt_d/failure.$n/$pid.err");
157		link("$file.err", "$opt_d/failure.$n/$dest.err");
158	}
159
160	if (-f "$opt_d/$pid.core") {
161		rename("$opt_d/$pid.core", "$opt_d/failure.$n/$pid.core");
162	}
163
164	link("$file", "$opt_d/failure.$n/$dest");
165
166	$msg = "ERROR: " . $dest . " " . $msg;
167
168	if (scalar @parms > 1) {
169		$msg = $msg . "; see $errfile in failure.$n\n";
170	} else {
171		$msg = $msg . "; details in failure.$n\n";
172	}
173
174	errmsg($msg);
175}
176
177sub logmsg
178{
179	my($msg) = @_;
180
181	print STDOUT $msg unless ($opt_q);
182	print LOG $msg if ($opt_l);
183}
184
185# Trim leading and trailing whitespace
186sub trim {
187	my($s) = @_;
188
189	$s =~ s/^\s*//;
190	$s =~ s/\s*$//;
191	return $s;
192}
193
194# Load exception set of skipped tests from the file at the given
195# pathname. The test names are assumed to be paths relative to $dt_tst,
196# for example: common/aggs/tst.neglquant.d, and specify tests to be
197# skipped.
198sub load_exceptions {
199	my($listfile) = @_;
200	my($line) = "";
201
202	%exceptions = ();
203	if (length($listfile) > 0) {
204		exit(123) unless open(STDIN, "<$listfile");
205		while (<STDIN>) {
206			chomp;
207			$line = $_;
208			# line is non-empty and not a comment
209			if ((length($line) > 0) && ($line =~ /^\s*[^\s#]/ )) {
210				$exceptions{trim($line)} = 1;
211			}
212		}
213	}
214}
215
216# Return 1 if the test is found in the exception set, 0 otherwise.
217sub is_exception {
218	my($file) = @_;
219	my($i) = -1;
220
221	if (scalar(keys(%exceptions)) == 0) {
222		return 0;
223	}
224
225	# hash absolute pathname after $dt_tst/
226	$file = abs_path($file);
227	$i = index($file, $dt_tst);
228	if ($i == 0) {
229		$file = substr($file, length($dt_tst) + 1);
230		return $exceptions{$file};
231	}
232	return 0;
233}
234
235#
236# Iterate over the set of test files specified on the command-line or by a find
237# on "$defdir/common", "$defdir/$MACH" and "$defdir/$PLATFORM" and execute each
238# one.  If the test file is executable, we fork and exec it. If the test is a
239# .ksh file, we run it with $ksh_path. Otherwise we run dtrace -s on it.  If
240# the file is named tst.* we assume it should return exit status 0.  If the
241# file is named err.* we assume it should return exit status 1.  If the file is
242# named err.D_[A-Z0-9]+[.*].d we use dtrace -xerrtags and examine stderr to
243# ensure that a matching error tag was produced.  If the file is named
244# drp.[A-Z0-9]+[.*].d we use dtrace -xdroptags and examine stderr to ensure
245# that a matching drop tag was produced.  If any *.out or *.err files are found
246# we perform output comparisons.
247#
248# run_tests takes two arguments: The first is the pathname of the dtrace
249# command to invoke when running the tests. The second is the pathname
250# of a file (may be the empty string) listing tests that ought to be
251# skipped (skipped tests are listed as paths relative to $dt_tst, for
252# example: common/aggs/tst.neglquant.d).
253#
254sub run_tests {
255	my($dtrace, $exceptions_path) = @_;
256	my($passed) = 0;
257	my($bypassed) = 0;
258	my($failed) = $errs;
259	my($total) = 0;
260
261	die "$PNAME: $dtrace not found; aborting\n" unless (-x "$dtrace");
262	logmsg("executing tests using $dtrace ...\n");
263
264	load_exceptions($exceptions_path);
265
266	foreach $file (sort @files) {
267		$file =~ m:.*/((.*)\.(\w+)):;
268		$name = $1;
269		$base = $2;
270		$ext = $3;
271
272		$dir = dirname($file);
273		$isksh = 0;
274		$tag = 0;
275		$droptag = 0;
276
277		if ($name =~ /^tst\./) {
278			$isksh = ($ext eq 'ksh');
279			$status = 0;
280		} elsif ($name =~ /^err\.(D_[A-Z0-9_]+)\./) {
281			$status = 1;
282			$tag = $1;
283		} elsif ($name =~ /^err\./) {
284			$status = 1;
285		} elsif ($name =~ /^drp\.([A-Z0-9_]+)\./) {
286			$status = 0;
287			$droptag = $1;
288		} else {
289			errmsg("ERROR: $file is not a valid test file name\n");
290			next;
291		}
292
293		$fullname = "$dir/$name";
294		$exe = "$dir/$base.exe";
295		$exe_pid = -1;
296
297		if ($opt_a && ($status != 0 || $tag != 0 || $droptag != 0 ||
298		    -x $exe || $isksh || -x $fullname)) {
299			$bypassed++;
300			next;
301		}
302
303		if (!$opt_f && is_exception("$dir/$name")) {
304			$bypassed++;
305			next;
306		}
307
308		if (!$isksh && -x $exe) {
309			if (($exe_pid = fork()) == -1) {
310				errmsg(
311				    "ERROR: failed to fork to run $exe: $!\n");
312				next;
313			}
314
315			if ($exe_pid == 0) {
316				open(STDIN, '</dev/null');
317
318				exec($exe);
319
320				warn "ERROR: failed to exec $exe: $!\n";
321			}
322		}
323
324		logmsg("testing $file ... ");
325
326		if (($pid = fork()) == -1) {
327			errmsg("ERROR: failed to fork to run test $file: $!\n");
328			next;
329		}
330
331		if ($pid == 0) {
332			open(STDIN, '</dev/null');
333			exit(125) unless open(STDOUT, ">$opt_d/$$.out");
334			exit(125) unless open(STDERR, ">$opt_d/$$.err");
335
336			unless (chdir($dir)) {
337				warn "ERROR: failed to chdir for $file: $!\n";
338				exit(126);
339			}
340
341			push(@dtrace_argv, '-xerrtags') if ($tag);
342			push(@dtrace_argv, '-xdroptags') if ($droptag);
343			push(@dtrace_argv, $exe_pid) if ($exe_pid != -1);
344
345			if ($isksh) {
346				exit(123) unless open(STDIN, "<$name");
347				exec("$ksh_path /dev/stdin $dtrace");
348			} elsif (-x $name) {
349				warn "ERROR: $name is executable\n";
350				exit(1);
351			} else {
352				if ($tag == 0 && $status == $0 && $opt_a) {
353					push(@dtrace_argv, '-A');
354				}
355
356				push(@dtrace_argv, '-C');
357				push(@dtrace_argv, '-s');
358				push(@dtrace_argv, $name);
359				exec($dtrace, @dtrace_argv);
360			}
361
362			warn "ERROR: failed to exec for $file: $!\n";
363			exit(127);
364		}
365
366		if (waitpid($pid, 0) == -1) {
367			errmsg("ERROR: timed out waiting for $file\n");
368			kill(9, $exe_pid) if ($exe_pid != -1);
369			kill(9, $pid);
370			next;
371		}
372
373		kill(9, $exe_pid) if ($exe_pid != -1);
374
375		if ($tag == 0 && $status == $0 && $opt_a) {
376			#
377			# We can chuck the earler output.
378			#
379			unlink($pid . '.out');
380			unlink($pid . '.err');
381
382			#
383			# This is an anonymous enabling.  We need to get
384			# the module unloaded.
385			#
386			system("dtrace -ae 1> /dev/null 2> /dev/null");
387			system("svcadm disable -s " .
388			    "svc:/network/nfs/mapid:default");
389			system("modunload -i 0 ; modunload -i 0 ; " .
390			    "modunload -i 0");
391			if (!system("modinfo | grep dtrace")) {
392				warn "ERROR: couldn't unload dtrace\n";
393				system("svcadm enable " .
394				    "-s svc:/network/nfs/mapid:default");
395				exit(124);
396			}
397
398			#
399			# DTrace is gone.  Now update_drv(1M), and rip
400			# everything out again.
401			#
402			system("update_drv dtrace");
403			system("dtrace -ae 1> /dev/null 2> /dev/null");
404			system("modunload -i 0 ; modunload -i 0 ; " .
405			    "modunload -i 0");
406			if (!system("modinfo | grep dtrace")) {
407				warn "ERROR: couldn't unload dtrace\n";
408				system("svcadm enable " .
409				    "-s svc:/network/nfs/mapid:default");
410				exit(124);
411			}
412
413			#
414			# Now bring DTrace back in.
415			#
416			system("sync ; sync");
417			system("dtrace -l -n bogusprobe 1> /dev/null " .
418			    "2> /dev/null");
419			system("svcadm enable -s " .
420			    "svc:/network/nfs/mapid:default");
421
422			#
423			# That should have caused DTrace to reload with
424			# the new configuration file.  Now we can try to
425			# snag our anonymous state.
426			#
427			if (($pid = fork()) == -1) {
428				errmsg("ERROR: failed to fork to run " .
429				    "test $file: $!\n");
430				next;
431			}
432
433			if ($pid == 0) {
434				open(STDIN, '</dev/null');
435				exit(125) unless open(STDOUT, ">$opt_d/$$.out");
436				exit(125) unless open(STDERR, ">$opt_d/$$.err");
437
438				push(@dtrace_argv, '-a');
439
440				unless (chdir($dir)) {
441					warn "ERROR: failed to chdir " .
442					    "for $file: $!\n";
443					exit(126);
444				}
445
446				exec($dtrace, @dtrace_argv);
447				warn "ERROR: failed to exec for $file: $!\n";
448				exit(127);
449			}
450
451			if (waitpid($pid, 0) == -1) {
452				errmsg("ERROR: timed out waiting for $file\n");
453				kill(9, $pid);
454				next;
455			}
456		}
457
458		logmsg("[$pid]\n");
459		$wstat = $?;
460		$wifexited = ($wstat & 0xFF) == 0;
461		$wexitstat = ($wstat >> 8) & 0xFF;
462		$wtermsig = ($wstat & 0x7F);
463
464		if (!$wifexited) {
465			fail("died from signal $wtermsig");
466			next;
467		}
468
469		if ($wexitstat == 125) {
470			die "$PNAME: failed to create output file in $opt_d " .
471			    "(cd elsewhere or use -d)\n";
472		}
473
474		if ($wexitstat != $status) {
475			fail("returned $wexitstat instead of $status");
476			next;
477		}
478
479		if (-f "$file.out" &&
480		    system("cmp -s $file.out $opt_d/$pid.out") != 0) {
481			fail("stdout mismatch", "$pid.out");
482			next;
483		}
484
485		if (-f "$file.err" &&
486		    system("cmp -s $file.err $opt_d/$pid.err") != 0) {
487			fail("stderr mismatch: see $pid.err");
488			next;
489		}
490
491		if ($tag) {
492			open(TSTERR, "<$opt_d/$pid.err");
493			$tsterr = <TSTERR>;
494			close(TSTERR);
495
496			unless ($tsterr =~ /: \[$tag\] line \d+:/) {
497				fail("errtag mismatch: see $pid.err");
498				next;
499			}
500		}
501
502		if ($droptag) {
503			$found = 0;
504			open(TSTERR, "<$opt_d/$pid.err");
505
506			while (<TSTERR>) {
507				if (/\[$droptag\] /) {
508					$found = 1;
509					last;
510				}
511			}
512
513			close (TSTERR);
514
515			unless ($found) {
516				fail("droptag mismatch: see $pid.err");
517				next;
518			}
519		}
520
521		unless ($opt_s) {
522			unlink($pid . '.out');
523			unlink($pid . '.err');
524		}
525	}
526
527	if ($opt_a) {
528		#
529		# If we're running with anonymous enablings, we need to
530		# restore the .conf file.
531		#
532		system("dtrace -A 1> /dev/null 2> /dev/null");
533		system("dtrace -ae 1> /dev/null 2> /dev/null");
534		system("modunload -i 0 ; modunload -i 0 ; modunload -i 0");
535		system("update_drv dtrace");
536	}
537
538	$total = scalar(@files);
539	$failed = $errs - $failed;
540	$passed = ($total - $failed - $bypassed);
541	$results{$dtrace} = {
542		"passed" => $passed,
543		"bypassed" => $bypassed,
544		"failed" => $failed,
545		"total" => $total
546	};
547}
548
549die $USAGE unless (getopts($OPTSTR));
550usage() if ($opt_h);
551
552foreach $arg (@ARGV) {
553	if (-f $arg) {
554		push(@files, $arg);
555	} elsif (-d $arg) {
556		find(\&wanted, $arg);
557	} else {
558		die "$PNAME: $arg is not a valid file or directory\n";
559	}
560}
561
562$dt_tst = '/opt/SUNWdtrt/tst';
563$dt_bin = '/opt/SUNWdtrt/bin';
564$defdir = -d $dt_tst ? $dt_tst : '.';
565$bindir = -d $dt_bin ? $dt_bin : '.';
566
567if (!$opt_F) {
568	my @dependencies = ("gcc", "make", "java", "perl");
569
570	for my $dep (@dependencies) {
571		if (!inpath($dep)) {
572			die "$PNAME: '$dep' not found (use -F to force run)\n";
573		}
574	}
575}
576
577find(\&wanted, "$defdir/common") if (scalar(@ARGV) == 0);
578find(\&wanted, "$defdir/$MACH") if (scalar(@ARGV) == 0);
579find(\&wanted, "$defdir/$PLATFORM") if (scalar(@ARGV) == 0);
580
581die $USAGE if (scalar(@files) == 0);
582
583$dtrace_path = '/usr/sbin/dtrace';
584$jdtrace_path = "$bindir/jdtrace";
585
586%exception_lists = ("$jdtrace_path" => "$bindir/exception.lst");
587
588if ($opt_j || $opt_n || $opt_i) {
589	@dtrace_cmds = ();
590	push(@dtrace_cmds, $dtrace_path) if ($opt_n);
591	push(@dtrace_cmds, $jdtrace_path) if ($opt_j);
592	push(@dtrace_cmds, "/usr/sbin/$opt_i/dtrace") if ($opt_i);
593} else {
594	@dtrace_cmds = ($dtrace_path);
595}
596
597if ($opt_d) {
598	die "$PNAME: -d arg must be absolute path\n" unless ($opt_d =~ /^\//);
599	die "$PNAME: -d arg $opt_d is not a directory\n" unless (-d "$opt_d");
600	system("coreadm -p $opt_d/%p.core");
601} else {
602	my $dir = getcwd;
603	system("coreadm -p $dir/%p.core");
604	$opt_d = '.';
605}
606
607if ($opt_x) {
608	push(@dtrace_argv, '-x');
609	push(@dtrace_argv, $opt_x);
610}
611
612die "$PNAME: failed to open $PNAME.$$.log: $!\n"
613    unless (!$opt_l || open(LOG, ">$PNAME.$$.log"));
614
615if ($opt_g) {
616	$ENV{'UMEM_DEBUG'} = 'default,verbose';
617	$ENV{'UMEM_LOGGING'} = 'fail,contents';
618	$ENV{'LD_PRELOAD'} = 'libumem.so';
619}
620
621if ($opt_b) {
622	logmsg("badioctl'ing ... ");
623
624	if (($badioctl = fork()) == -1) {
625		errmsg("ERROR: failed to fork to run badioctl: $!\n");
626		next;
627	}
628
629	if ($badioctl == 0) {
630		open(STDIN, '</dev/null');
631		exit(125) unless open(STDOUT, ">$opt_d/$$.out");
632		exit(125) unless open(STDERR, ">$opt_d/$$.err");
633
634		exec($bindir . "/badioctl");
635		warn "ERROR: failed to exec badioctl: $!\n";
636		exit(127);
637	}
638
639
640	logmsg("[$badioctl]\n");
641
642	#
643	# If we're going to be bad, we're just going to iterate over each
644	# test file.
645	#
646	foreach $file (sort @files) {
647		($name = $file) =~ s:.*/::;
648		$dir = dirname($file);
649
650		if (!($name =~ /^tst\./ && $name =~ /\.d$/)) {
651			next;
652		}
653
654		logmsg("baddof'ing $file ... ");
655
656		if (($pid = fork()) == -1) {
657			errmsg("ERROR: failed to fork to run baddof: $!\n");
658			next;
659		}
660
661		if ($pid == 0) {
662			open(STDIN, '</dev/null');
663			exit(125) unless open(STDOUT, ">$opt_d/$$.out");
664			exit(125) unless open(STDERR, ">$opt_d/$$.err");
665
666			unless (chdir($dir)) {
667				warn "ERROR: failed to chdir for $file: $!\n";
668				exit(126);
669			}
670
671			exec($bindir . "/baddof", $name);
672
673			warn "ERROR: failed to exec for $file: $!\n";
674			exit(127);
675		}
676
677		sleep 60;
678		kill(9, $pid);
679		waitpid($pid, 0);
680
681		logmsg("[$pid]\n");
682
683		unless ($opt_s) {
684			unlink($pid . '.out');
685			unlink($pid . '.err');
686		}
687	}
688
689	kill(9, $badioctl);
690	waitpid($badioctl, 0);
691
692	unless ($opt_s) {
693		unlink($badioctl . '.out');
694		unlink($badioctl . '.err');
695	}
696
697	exit(0);
698}
699
700#
701# Run all the tests specified on the command-line (the entire test suite
702# by default) once for each dtrace command tested, skipping any tests
703# not valid for that command.
704#
705foreach $dtrace_cmd (@dtrace_cmds) {
706	run_tests($dtrace_cmd, $exception_lists{$dtrace_cmd});
707}
708
709$opt_q = 0; # force final summary to appear regardless of -q option
710
711logmsg("\n==== TEST RESULTS ====\n");
712foreach $key (keys %results) {
713	my $passed = $results{$key}{"passed"};
714	my $bypassed = $results{$key}{"bypassed"};
715	my $failed = $results{$key}{"failed"};
716	my $total = $results{$key}{"total"};
717
718	logmsg("\n     mode: " . $key . "\n");
719	logmsg("   passed: " . $passed . "\n");
720	if ($bypassed) {
721		logmsg(" bypassed: " . $bypassed . "\n");
722	}
723	logmsg("   failed: " . $failed . "\n");
724	logmsg("    total: " . $total . "\n");
725}
726
727exit($errs != 0);
728