PageRenderTime 54ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 1ms

/mts

https://github.com/dsw/Multi-TeSter
Perl | 1177 lines | 966 code | 76 blank | 135 comment | 142 complexity | 3c3bc12328a3590af9d942ac694e6e38 MD5 | raw file
  1. #!/usr/bin/perl -w
  2. # -*- cperl -*-
  3. # Multi-TeSter, mts, implements a domain-specific language, MTS, for
  4. # generating and running multiple modes of similar tests from a single
  5. # source. 1) For each mode MTS runs a command line and then checks
  6. # that the expected exit code, stdout, and stderr result, failing if
  7. # they do not. 2) An MTS input file can have multiple modes and all
  8. # of the above aspects of running the program being tested can depend
  9. # on the current mode; this feature allows multiple very similar tests
  10. # to be expressed in one file, re-using their commonality.
  11. #
  12. # You can read the documentation and the license under which MTS is
  13. # released by typing:
  14. # mts --help
  15. # You can also find the documentation, copyright, and license embedded
  16. # in this script at the end after __DATA__.
  17. #
  18. # Daniel S. Wilkerson
  19. # Bugs; search for FIX below:
  20. #
  21. # For getting the results of a system call, "perldoc system" says to
  22. # be portable to use the "W*() calls of the POSIX extension; see
  23. # perlport for more information."
  24. use strict;
  25. use warnings FATAL => 'all';
  26. # the version of MTS; can be asserted by the user
  27. my $version = "MTS-2008-01-11";
  28. # these variable are substituted just before running the test
  29. my @lateSubBuiltinVars = qw(FILES FIRST_FILE OTHER_FILES MODE);
  30. my @builtinVars = ("THIS", @lateSubBuiltinVars);
  31. my %builtinVars;
  32. for my $builtin (@builtinVars) {
  33. ++$builtinVars{$builtin};
  34. }
  35. # command-line state; set once by readCommandLine and subsequent
  36. # scanning of the first file in the input files
  37. my $progName = $0; # record the program name
  38. my $verbose; # comment on what we are doing
  39. my $quiet; # don't print modes as they are tested
  40. my $recordInputFile; # record input files that pass here
  41. my $print_cmdline_state; # print the command line state and exit
  42. my $print_xform_state; # print the xform state and exit
  43. my $print_commands = 1; # print commands as they are run
  44. my $keep_temp_files = 0; # keep the temporary files
  45. my $pre = "MTS"; # the prefix to use
  46. my $diff = "diff"; # the diff program to use
  47. my @cfgFilesRead; # sequence of configuration files read
  48. my %cfgFilesRead; # set of configuration files read
  49. my %interpVars; # interpolation variables
  50. my @inputModes; # modes to use
  51. my %inputModes; # prevent duplicates
  52. my $inputDir = ""; # the input directory
  53. my $inputFile; # the input file
  54. # regexes that match mode and file names
  55. my $base_mode_re = "[_0-9a-zA-Z]+";
  56. my $base_file_re = "[-_.0-9a-zA-Z]+\.mts";
  57. my $base_var_re = "[_a-zA-Z][_a-zA-Z0-9]*";
  58. # xformFile state; set by xformFile each time it runs
  59. my @modes; # modes found in MTS_modes() lines
  60. my %modes; # prevent dupliates
  61. my %allModes; # modes union omitted modes
  62. my %mode2other; # map a mode to its MTS_other lists
  63. my %mode2prog; # map a mode to its MTS_prog line
  64. my %mode2progLoc; # map a mode to the loc of its MTS_prog line
  65. my %mode2exit; # map a mode to its MTS_exit line
  66. my %mode2out; # map a mode to its MTS_expout lines
  67. my %mode2err; # map a mode to its MTS_experr lines
  68. # read one flag; return true iff it was a flag
  69. sub readFlag {
  70. my ($arg, $loc) = @_;
  71. die unless defined $arg;
  72. die unless defined $loc;
  73. if (0) { # orthogonality
  74. } elsif ($arg =~ /^-(-?)help$/) {
  75. printDoc();
  76. exit(0);
  77. } elsif ($arg =~ /^--verbose$/) {
  78. ++$verbose;
  79. } elsif ($arg =~ /^--quiet$/) {
  80. ++$quiet;
  81. } elsif ($arg =~ /^--record-input-file=(.+)$/) {
  82. $recordInputFile = $1;
  83. } elsif ($arg =~ /^--print-cmdline-state$/) {
  84. ++$print_cmdline_state;
  85. } elsif ($arg =~ /^--print-xform-state$/) {
  86. ++$print_xform_state;
  87. } elsif ($arg =~ /^--print-commands=(.*)$/) {
  88. $print_commands = $1;
  89. } elsif ($arg =~ /^--keep-temp-files=(.*)$/) {
  90. $keep_temp_files = $1;
  91. } elsif ($arg =~ /^--pre=([a-zA-Z]+)$/) {
  92. $pre = $1;
  93. } elsif ($arg =~ /^--diff=(.+)$/) {
  94. $diff = $1;
  95. } elsif ($arg =~ /^--cfg=(.+)$/) {
  96. readCfgFile($1);
  97. } elsif ($arg =~ /^--set:(${base_var_re})=(.*)$/o) {
  98. $interpVars{$1} = $2;
  99. chomp $interpVars{$1};
  100. } elsif ($arg =~ /^--mode=(${base_mode_re})$/o) {
  101. checkMode($1, $loc);
  102. push @inputModes, $1 unless $inputModes{$1}++;
  103. } elsif ($arg =~ /^--/) {
  104. die "$loc: Unrecognized or malformed flag: '$arg';\n".
  105. "type $progName --help for help\n";
  106. } else {
  107. return 0;
  108. }
  109. return 1;
  110. }
  111. # read a configuration file
  112. sub readCfgFile {
  113. print "readCfgFile\n" if $verbose;
  114. my ($cfgFile) = @_;
  115. # check for configuration file cycles
  116. push @cfgFilesRead, $cfgFile;
  117. if ($cfgFilesRead{$cfgFile}++) {
  118. print "Cycle in configuration files:\n";
  119. for my $file (@cfgFilesRead) {
  120. print " $file";
  121. }
  122. die "";
  123. }
  124. # read the file
  125. my $lineno = 0;
  126. open CFG, $cfgFile or die "$!: $cfgFile\n";
  127. while(<CFG>) {
  128. ++$lineno;
  129. s/\#.*$//; # delete hash-to-EOL comments
  130. s/^\s*//; # trim leading whitespace
  131. next if /^\s*$/; # skip blank lines
  132. my $loc = "$cfgFile:$lineno";
  133. my $wasAFlag = readFlag($_, $loc);
  134. die "$loc: Configuration file must contain a legal flag" unless $wasAFlag;
  135. }
  136. close CFG or die "$!: $cfgFile\n";
  137. }
  138. # parse the command line
  139. sub readCommandLine {
  140. print "readCommandLine\n" if $verbose;
  141. unless (@ARGV) {
  142. printDoc();
  143. exit(0);
  144. }
  145. while(1) {
  146. my $arg = shift @ARGV;
  147. last unless defined $arg;
  148. chomp $arg;
  149. # read the flag if we have one
  150. my $res = readFlag($arg, "command-line");
  151. next if $res;
  152. # not a flag: must be an input filename
  153. die "May not specify two input files.\n" if defined $inputFile;
  154. if ($arg =~ m|^(.*/)([^/]+)$|) {
  155. my ($dir, $file) = ($1, $2);
  156. die unless defined $dir;
  157. die unless defined $file;
  158. $inputDir = $dir;
  159. $inputFile = $file;
  160. } else {
  161. # $inputDir is left unchanged
  162. $inputFile = $arg;
  163. }
  164. }
  165. # print the state if wanted
  166. if ($print_cmdline_state) {
  167. printCmdLineState();
  168. exit(0);
  169. }
  170. # check integrity
  171. checkTheInterpVars();
  172. die "No input file given; type $progName --help for help.\n"
  173. unless $inputFile;
  174. checkFile($inputFile, "command-line");
  175. }
  176. # check that the interpolation variables do not contain the prefix as
  177. # this is very likely a mistake and can always be avoided by changing
  178. # the variable name
  179. sub checkTheInterpVars {
  180. while (my ($var, $val) = each (%interpVars)) {
  181. if ($builtinVars{$var}) {
  182. die "Variable '$var' has the same name as a built-in variable.\n";
  183. }
  184. if ($var =~ /$pre/) {
  185. die <<"INTERP_ERR"
  186. Interpolation variable '$var' contains the prefix '$pre'.
  187. Pick a different name for your variable.
  188. INTERP_ERR
  189. ;
  190. }
  191. }
  192. }
  193. # interpolate the interpolation variables; if $delete is set, just
  194. # delete the values instead; Note that we substitute going forward and
  195. # never re-substitute something already substituted; this prevents the
  196. # pathological case where substituting two consecutive variables makes
  197. # the name of a third and gets us into an infinite loop
  198. sub interpTheInterpVars {
  199. my ($loc, $str, $delete) = @_;
  200. die unless defined $loc;
  201. while ($str =~ m/$pre/g) {
  202. pos($str) -= length($pre); # back up to the start of the match
  203. # find the longest variable name that matches at this position
  204. my $matchVar;
  205. while (my ($var, $val) = each (%interpVars)) {
  206. my $preVar = "${pre}_${var}"; # my editor makes me do this
  207. if ($str =~ /\G$preVar/gc) {
  208. pos($str) -= length($preVar); # back up to the start of the match
  209. if (defined $matchVar) {
  210. my $diff = length($var) - length($matchVar);
  211. die if $diff == 0;
  212. if ($diff > 0) {$matchVar = $var;}
  213. # otherwise, leave matchVar as the winner
  214. } else {
  215. $matchVar = $var;
  216. }
  217. }
  218. }
  219. # there should be exactly one
  220. die "$loc: Found the prefix '$pre' but none of the variables match.\n"
  221. unless defined $matchVar;
  222. # substitute the variable that matches
  223. my $val;
  224. if ($delete) {$val = "";}
  225. else {$val = $interpVars{$matchVar};}
  226. die "$loc: Variable '$matchVar' not defined.\n" unless defined $val;
  227. my $preVar = "${pre}_${matchVar}"; # my editor makes me do this
  228. my $savePos = pos($str);
  229. die "$loc: Variable '$matchVar' matched and now it doesn't?\n"
  230. unless $str =~ s/\G$preVar/$val/;
  231. pos($str) = $savePos + length($val); # go forward past the substitution
  232. }
  233. return $str;
  234. }
  235. # delete the interpolation variables
  236. sub deleteTheInterpVars {
  237. my ($loc, $str) = @_;
  238. return interpTheInterpVars($loc, $str, 1);
  239. }
  240. # print the documentation and usage
  241. sub printDoc {
  242. while(<DATA>) {print;}
  243. }
  244. # print the program state
  245. sub printCmdLineState {
  246. print "Command-line state:\n";
  247. print "pre: '$pre'\n" if defined $pre;
  248. print "diff: '$diff'\n" if defined $diff;
  249. print "modes:\n";
  250. for my $mode (@inputModes) {
  251. print " '$mode'\n";
  252. }
  253. print "inputDir: '$inputDir'\n";
  254. print "inputFile: '$inputFile'\n";
  255. }
  256. sub printXformFileState {
  257. print "xForm state:\n";
  258. print "modes:\n";
  259. for my $mode (@modes) {
  260. print " '$mode'\n";
  261. }
  262. print "mode2other:\n";
  263. while(my ($mode, $other) = each(%mode2other)) {
  264. print " '$mode'->";
  265. print(join(" ", @{$mode2other{$mode}}));
  266. print "\n";
  267. }
  268. print "mode2prog:\n";
  269. while(my ($mode, $prog) = each(%mode2prog)) {
  270. print " '$mode'-> $prog\n";
  271. }
  272. print "mode2progLoc:\n";
  273. while(my ($mode, $progLoc) = each(%mode2progLoc)) {
  274. print " '$mode'-> $progLoc\n";
  275. }
  276. print "mode2exit:\n";
  277. while(my ($mode, $exit) = each(%mode2exit)) {
  278. print " '$mode'-> $exit\n";
  279. }
  280. print "mode2out:\n";
  281. while(my ($mode, $out) = each(%mode2out)) {
  282. print " '$mode'->\n----\n${out}----\n";
  283. }
  284. print "mode2err:\n";
  285. while(my ($mode, $err) = each(%mode2err)) {
  286. print " '$mode'->\n----\n${err}----\n";
  287. }
  288. }
  289. sub clearXformFileState {
  290. print "clearXformFileState\n" if $verbose;
  291. undef @modes;
  292. undef %modes;
  293. undef %allModes;
  294. undef %mode2other;
  295. undef %mode2prog;
  296. undef %mode2progLoc;
  297. undef %mode2exit;
  298. undef %mode2out;
  299. undef %mode2err;
  300. }
  301. # check a modename
  302. sub checkMode {
  303. my ($mode, $loc) = @_;
  304. die unless defined $mode;
  305. die unless defined $loc;
  306. die "$loc: Bad mode name '$mode'; mode name must consist of:\n".
  307. "alphas, nums, and underscores.\n"
  308. unless $mode =~ /^${base_mode_re}$/o;
  309. }
  310. # check a filename
  311. sub checkFile {
  312. my ($file, $loc) = @_;
  313. die unless defined $file;
  314. die unless defined $loc;
  315. die "$loc: Bad file name '$file'; file name must consist of:\n".
  316. "alphas, nums, underscore, dashes, and dots and must end in '.mts'.\n"
  317. unless $file =~ /^${base_file_re}$/o;
  318. }
  319. # make transformed filename
  320. sub xformFileName {
  321. my ($file, $mode, $kind) = @_;
  322. die unless defined $file;
  323. die unless defined $mode;
  324. die unless defined $kind;
  325. checkMode($mode, "should not happen");
  326. chomp $file; # should not be necessary
  327. my ($fileBase, $fileEnd) = ($file =~ /^([^\.]+)(\..+)?\.mts$/);
  328. die "Unable to parse filename: '$file'\n" unless defined $fileBase;
  329. $fileEnd = "" unless defined $fileEnd;
  330. my $ret = "${inputDir}${pre}_${fileBase}_${mode}${fileEnd}${kind}";
  331. return $ret;
  332. }
  333. # read and transform a file; accumulate the result in the above state;
  334. # only transform the file if $mode is defined; otherwise we just
  335. # accumulate the state defined by the commands
  336. sub xformFile {
  337. print "xformFile\n" if $verbose;
  338. my ($file, $xformMode) = @_;
  339. die unless defined $file;
  340. # lexer regular expressions; NOTE: these interpolate $pre so they
  341. # can't go inot some global scope unless that is carefully done; it
  342. # is much safer to just put them here
  343. my $comment_re = "^\\s*${pre}:.*\$";
  344. my $version_re = "^\\s*${pre}_version\\(([^)]*)\\)\\s*\$";
  345. my $modes_re = "^\\s*${pre}_modes\\(([^)]*)\\)\\s*\$";
  346. my $omitmodes_re = "^\\s*${pre}_omitmodes\\(([^)]*)\\)\\s*\$";
  347. my $other_re = "^\\s*${pre}_other\\(([^)]*)\\)\\s*:\\s*(.*)\$";
  348. my $prog_re = "^\\s*${pre}_prog\\(([^)]*)\\)\\s*:\\s*(.*)\$";
  349. my $exit_re = "^\\s*${pre}_exit\\(([^)]*)\\)\\s*:\\s*(.*)\$";
  350. my $out_re = "^\\s*${pre}_exp(out\|err)\\(([^)]*)\\)\\s*(:|;)(.*)\$";
  351. my $outblk_re = "^\\s*${pre}_exp(out\|err)\\(([^)]*)\\)\\s*(\\{|\\})\\s*\$";
  352. my $add_re = "${pre}_add\\(([^)]*)\\)";
  353. my $sub_re = "${pre}_sub\\(([^)]*)\\)";
  354. # clear our state
  355. clearXformFileState();
  356. for my $var (@builtinVars) {
  357. die if defined $interpVars{$var};
  358. }
  359. # open input file
  360. die unless defined $file;
  361. open FILE, "$inputDir$file" or die "$!: $inputDir$file\n";
  362. # open the input file
  363. my $inFile;
  364. if (defined $xformMode) {
  365. # FILE.END -> FILE_$xformMode.END
  366. $inFile = xformFileName($file, $xformMode, "");
  367. $interpVars{"THIS"} = $inFile;
  368. unlink $inFile;
  369. open INFILE, ">$inFile" or die "$!: $inFile\n";
  370. }
  371. # iterate over file
  372. my $lineno = 0;
  373. my $outRecMode; # out mode being recorded or undef if not recording
  374. my $errRecMode; # err mode being recorded or undef if not recording
  375. my $outRecMode_startLoc; # starting block of out mode being recorded
  376. my $errRecMode_startLoc; # starting block of err mode being recorded
  377. while(<FILE>) {
  378. ++$lineno;
  379. my $loc = "$inputDir$file:$lineno";
  380. # MTS: This is a comment.
  381. if (/$comment_re/o) {
  382. # discard this line
  383. }
  384. # MTS_version(version_string)
  385. elsif (/$version_re/o) {
  386. die "$loc: Version mismatch.\n" unless $version eq $1;
  387. }
  388. # MTS_modes(MODE_LIST)
  389. elsif (/$modes_re/o) {
  390. die "$loc: Command not allowed within an expout block.\n" if $outRecMode;
  391. die "$loc: Command not allowed within an experr block.\n" if $errRecMode;
  392. for my $mode (split /\s*,\s*/, $1) {
  393. checkMode($mode, $loc);
  394. # NOTE: there is NO interpolation of modes
  395. push @modes, $mode unless $modes{$mode}++;
  396. ++$allModes{$mode};
  397. }
  398. }
  399. # MTS_omitmodes(MODE_LIST)
  400. elsif (/$omitmodes_re/o) {
  401. die "$loc: Command not allowed within an expout block.\n" if $outRecMode;
  402. die "$loc: Command not allowed within an experr block.\n" if $errRecMode;
  403. for my $mode (split /\s*,\s*/, $1) {
  404. checkMode($mode, $loc);
  405. # NOTE: there is NO interpolation of modes
  406. ++$allModes{$mode};
  407. }
  408. }
  409. # MTS_other(MODE_LIST):LIST_OF_OTHER_FILES
  410. elsif (/$other_re/o) {
  411. die "$loc: Command not allowed within an expout block.\n" if $outRecMode;
  412. die "$loc: Command not allowed within an experr block.\n" if $errRecMode;
  413. my $modes = $1;
  414. my $otherFilesLine = $2;
  415. for my $mode (split /\s*,\s*/, $modes) {
  416. checkMode($mode, $loc);
  417. die "$loc: Unknown mode: '$mode'\n" unless defined $allModes{$mode};
  418. next unless defined $modes{$mode};
  419. my @otherFiles = split " ", $otherFilesLine;
  420. for my $file (@otherFiles) {
  421. checkFile($file, $loc);
  422. }
  423. # NOTE: there is NO interpolation of other filenames
  424. push @{$mode2other{$mode}}, @otherFiles;
  425. }
  426. }
  427. # MTS_prog(MODE_LIST):PROG
  428. elsif (/$prog_re/o) {
  429. die "$loc: Command not allowed within an expout block.\n" if $outRecMode;
  430. die "$loc: Command not allowed within an experr block.\n" if $errRecMode;
  431. my $prog = $2;
  432. chomp $prog;
  433. # this is done later so that special variables which are unknown
  434. # now, such as the list of files, can be interpolated
  435. # $prog = interpTheInterpVars($loc, $prog) if defined $xformMode;
  436. for my $mode (split /\s*,\s*/, $1) {
  437. checkMode($mode, $loc);
  438. die "$loc: Unknown mode: '$mode'\n" unless defined $allModes{$mode};
  439. next unless defined $modes{$mode};
  440. # allow concatenation for now; not sure it is a good idea
  441. # if (defined $mode2prog{$mode}) {
  442. # die "$loc: Duplicate prog line for mode $mode.\n";
  443. # }
  444. # add the space so tokens do not accidentally concatenate
  445. # across lines
  446. $mode2prog{$mode} .= "$prog ";
  447. # Note that if the prog line is spread across multiple prog
  448. # commands and there is later an error in the prog line, say
  449. # during interpolation, the error location will be reported as
  450. # having occurred on the first line; it is otherwise too
  451. # difficult to track the source location information.
  452. $mode2progLoc{$mode} = $loc unless $mode2progLoc{$mode};
  453. }
  454. }
  455. # MTS_exit(MODE_LIST):EXIT_VALUE
  456. elsif (/$exit_re/o) {
  457. die "$loc: Command not allowed within an expout block.\n" if $outRecMode;
  458. die "$loc: Command not allowed within an experr block.\n" if $errRecMode;
  459. my $exit = $2;
  460. chomp $exit;
  461. $exit = interpTheInterpVars($loc, $exit) if defined $xformMode;
  462. for my $mode (split /\s*,\s*/, $1) {
  463. checkMode($mode, $loc);
  464. die "$loc: Unknown mode: '$mode'\n" unless defined $allModes{$mode};
  465. next unless defined $modes{$mode};
  466. if (defined $mode2exit{$mode}) {
  467. die "$loc: Duplicate exit value line for mode $mode\n";
  468. }
  469. $mode2exit{$mode} = $exit;
  470. }
  471. }
  472. # MTS_expout(MODE_LIST):LINE_OF_OUTPUT
  473. # MTS_experr(MODE_LIST):LINE_OF_OUTPUT
  474. # MTS_expout(MODE_LIST);LINE_OF_OUTPUT
  475. # MTS_experr(MODE_LIST);LINE_OF_OUTPUT
  476. elsif (/$out_re/o) {
  477. die "$loc: Command not allowed within an expout block.\n" if $outRecMode;
  478. die "$loc: Command not allowed within an experr block.\n" if $errRecMode;
  479. my $stream = $1;
  480. my $modes = $2;
  481. my $newlineFlag = $3;
  482. my $line = $4;
  483. chomp $line;
  484. if ($newlineFlag =~ /:/) { # put newline back
  485. $line .= "\n";
  486. } elsif ($newlineFlag =~ /;/) { # leave without a newline
  487. } else {
  488. die "$loc: Expected colon or semicolon after mode list\n";
  489. }
  490. $line = interpTheInterpVars($loc, $line) if defined $xformMode;
  491. for my $mode (split /\s*,\s*/, $modes) {
  492. checkMode($mode, $loc);
  493. die "$loc: Unknown mode: '$mode'\n" unless defined $allModes{$mode};
  494. next unless defined $modes{$mode};
  495. if ($stream eq "out") {$mode2out{$mode} .= $line;}
  496. elsif ($stream eq "err") {$mode2err{$mode} .= $line;}
  497. else {
  498. die "$loc: Line has prefix '$pre' but no well-formed".
  499. " embedded command.\n";
  500. }
  501. }
  502. }
  503. # MTS_expout(MODE){
  504. # MTS_experr(MODE){
  505. # MTS_expout(MODE)}
  506. # MTS_experr(MODE)}
  507. elsif (/$outblk_re/o) {
  508. my $stream = $1;
  509. my $mode = $2; # NOTE: one mode, not a list
  510. my $startStop = $3;
  511. my $startStopVal;
  512. if ($startStop eq "{") {$startStopVal = 1;}
  513. elsif ($startStop eq "}") {$startStopVal = 0;}
  514. else {
  515. die "$loc: Line has prefix '$pre' but no well-formed\n".
  516. " embedded command.\n";
  517. }
  518. # for my $mode (split /\s*,\s*/, $modes) {
  519. # extra error reporting just to be extra clear
  520. die "$loc: Only ONE mode, not a list, allowed for exp(out,err).\n"
  521. if $mode =~ /,/;
  522. checkMode($mode, $loc);
  523. die "$loc: Unknown mode: '$mode'\n" unless defined $allModes{$mode};
  524. if (defined $modes{$mode}) {
  525. if ($stream eq "out") {
  526. die "$loc: May not open nor close an expout block ".
  527. "when in an experr block\n"
  528. if $errRecMode;
  529. if ($startStopVal) {
  530. die "$loc: Attempt to open an expout block when already in one.\n"
  531. if $outRecMode;
  532. $outRecMode = $mode;
  533. $outRecMode_startLoc = $loc;
  534. } else {
  535. die "$loc: Attempt to close an expout block when not in one.\n"
  536. unless $outRecMode;
  537. die "$loc: Attempt to close an expout block for a mode "
  538. . "other than the one that is open.\n"
  539. unless $outRecMode eq $mode;
  540. undef $outRecMode;
  541. }
  542. } elsif ($stream eq "err") {
  543. die "$loc: May not open nor close an experr block ".
  544. "when in an expout block\n"
  545. if $outRecMode;
  546. if ($startStopVal) {
  547. die "$loc: Attempt to open an experr block when already in one.\n"
  548. if $errRecMode;
  549. $errRecMode = $mode;
  550. $errRecMode_startLoc = $loc;
  551. } else {
  552. die "$loc: Attempt to close an experr block when not in one.\n"
  553. unless $errRecMode;
  554. die "$loc: Attempt to close an experr block for a mode "
  555. . "other than the one that is open.\n"
  556. unless $errRecMode eq $mode;
  557. undef $errRecMode;
  558. }
  559. } else {
  560. die "$loc: Line has prefix '$pre' but no well-formed".
  561. " embedded command.\n";
  562. }
  563. }
  564. # }
  565. }
  566. # MTS_add(MODE_LIST)
  567. elsif (s/$add_re//o) {
  568. die "$loc: Command not allowed within an expout block.\n" if $outRecMode;
  569. die "$loc: Command not allowed within an experr block.\n" if $errRecMode;
  570. if (defined $xformMode) {
  571. for my $mode (split /\s*,\s*/, $1) {
  572. checkMode($mode, $loc);
  573. die "$loc: Unknown mode: '$mode'\n" unless defined $allModes{$mode};
  574. next unless defined $modes{$mode};
  575. if ($xformMode eq $mode) {
  576. $_ = interpTheInterpVars($loc, $_) if defined $xformMode;
  577. print INFILE $_ if defined $xformMode;
  578. }
  579. }
  580. }
  581. }
  582. # MTS_sub(MODE_LIST)
  583. elsif (s/$sub_re//o) {
  584. die "$loc: Command not allowed within an expout block.\n" if $outRecMode;
  585. die "$loc: Command not allowed within an experr block.\n" if $errRecMode;
  586. if (defined $xformMode) {
  587. for my $mode (split /\s*,\s*/, $1) {
  588. checkMode($mode, $loc);
  589. die "$loc: Unknown mode: '$mode'\n" unless defined $allModes{$mode};
  590. next unless defined $modes{$mode};
  591. if (! ($xformMode eq $mode)) {
  592. $_ = interpTheInterpVars($loc, $_) if defined $xformMode;
  593. print INFILE $_ if defined $xformMode;
  594. }
  595. }
  596. }
  597. }
  598. # unadorned line
  599. elsif (defined $xformMode) {
  600. $_ = interpTheInterpVars($loc, $_) if defined $xformMode;
  601. if ($outRecMode) {$mode2out{$outRecMode} .= $_;}
  602. elsif ($errRecMode) {$mode2err{$errRecMode} .= $_;}
  603. else {print INFILE $_;}
  604. }
  605. }
  606. die "$outRecMode_startLoc: Unterminated expout block.\n"
  607. if $outRecMode;
  608. die "$errRecMode_startLoc: Unterminated experr block.\n"
  609. if $errRecMode;
  610. # clean up
  611. for my $var (@builtinVars) {
  612. undef $interpVars{$var};
  613. }
  614. if (defined $xformMode) {
  615. close INFILE or die "$!: $inFile\n";
  616. chmod 0440, $inFile;
  617. }
  618. close FILE or die "$!: $inputDir$file\n";
  619. }
  620. # run the given command and return its exit value unless it dies in
  621. # some exotic way
  622. sub runCommand {
  623. # FIX: "perldoc system" says to be portable to use the "W*() calls
  624. # of the POSIX extension; see perlport for more information."
  625. my ($cmd) = @_;
  626. die unless $cmd;
  627. print "$cmd\n" if $print_commands && ! $quiet;
  628. my $res = system($cmd);
  629. if ($res == -1) {
  630. die "Failed to execute: $cmd\n";
  631. } elsif ($res & 127) {
  632. my $msg = sprintf("child died with signal %d.\n", $res & 127);
  633. die $msg;
  634. }
  635. my $exitValue = $res >> 8;
  636. return $exitValue;
  637. }
  638. # delete temporary files
  639. sub deleteFile {
  640. my ($filename) = @_;
  641. print "deleteFile: $filename\n" if $verbose;
  642. my $numDeleted = unlink $filename;
  643. die "Error deleting file: $filename\n" unless $numDeleted == 1;
  644. }
  645. # run one test in a given mode
  646. sub runTest {
  647. my ($mode) = @_;
  648. print "runTest: $mode\n" if $verbose;
  649. checkMode($mode, "should not happen");
  650. unless ($quiet) {
  651. if ($print_commands) {
  652. print "\n* ";
  653. } else {
  654. print " ";
  655. }
  656. print "$inputFile, $mode\n";
  657. }
  658. # transform the first file and get the input file state
  659. xformFile($inputFile, $mode);
  660. die "$inputFile mode: $mode; transformed a file in a mode".
  661. " for which there is no 'modes' line.\n"
  662. unless $modes{$mode};
  663. # get the prog
  664. my $prog = $mode2prog{$mode};
  665. die "$inputFile mode $mode; transformed a file in a mode".
  666. " for which there is no 'prog' line.\n"
  667. unless $prog;
  668. # compute the files variable for the prog line
  669. # FILE.END -> FILE_$mode.END
  670. # files first
  671. my $filesFirst = xformFileName($inputFile, $mode, "");
  672. # files other
  673. my $filesOther;
  674. my %filesOtherSet;
  675. ++$filesOtherSet{$filesFirst};
  676. for my $otherFile (@{$mode2other{$mode}}) {
  677. my $otherXformedFile = xformFileName($otherFile, $mode, "");
  678. die "File '$otherFile' listed twice for mode '$mode'.\n"
  679. if $filesOtherSet{$otherXformedFile}++;
  680. $filesOther .= " $otherXformedFile";
  681. }
  682. # files list
  683. my $filesList = $filesFirst;
  684. $filesList .= " $filesOther" if defined $filesOther;
  685. # interpolate the prog line
  686. for my $var (@lateSubBuiltinVars) {
  687. die if defined $interpVars{$var};
  688. }
  689. %interpVars = (%interpVars,
  690. FILES => $filesList,
  691. FIRST_FILE => $filesFirst,
  692. OTHER_FILES => $filesOther,
  693. MODE => $mode,
  694. );
  695. $prog = interpTheInterpVars($mode2progLoc{$mode}, $prog);
  696. for my $var (@lateSubBuiltinVars) {
  697. undef $interpVars{$var};
  698. }
  699. # get the exit value
  700. my $exit = $mode2exit{$mode};
  701. $exit = 0 unless defined $exit; # default to 0 == Un*x success
  702. # print the expected stdout: FILE.END -> FILE_$mode.END.expout
  703. my $expectOut = xformFileName($inputFile, $mode, ".expout");
  704. unlink $expectOut;
  705. open EXPECT_OUT, ">$expectOut" or die "$!: $expectOut\n";
  706. print EXPECT_OUT $mode2out{$mode} if defined $mode2out{$mode};
  707. close EXPECT_OUT or die "$!: $expectOut\n";
  708. chmod 0440, $expectOut;
  709. # print the expected stderr: FILE.END -> FILE_$mode.END.experr
  710. my $expectErr = xformFileName($inputFile, $mode, ".experr");
  711. unlink $expectErr;
  712. open EXPECT_ERR, ">$expectErr" or die "$!: $expectErr\n";
  713. print EXPECT_ERR $mode2err{$mode} if defined $mode2err{$mode};
  714. close EXPECT_ERR or die "$!: $expectErr\n";
  715. chmod 0440, $expectErr;
  716. # IMPORTANT: past this point the xformFile state is going to be
  717. # wiped out by further calls to xformFile for the other files, so do
  718. # not rely upon it.
  719. my @otherFiles = @{$mode2other{$mode}};
  720. clearXformFileState();
  721. # transform the other file's names
  722. for my $file (@otherFiles) {
  723. checkMode($mode, "should not happen");
  724. xformFile($file, $mode);
  725. }
  726. # run the program
  727. # FILE.END -> FILE_$mode.END.stdout
  728. my $out = xformFileName($inputFile, $mode, ".stdout");
  729. # FILE.END -> FILE_$mode.END.stderr
  730. my $err = xformFileName($inputFile, $mode, ".stderr");
  731. my $resExit = runCommand("$prog > $out 2> $err");
  732. if (!($resExit == $exit)) {
  733. die
  734. "$inputFile: mode $mode; expected and actual exit values differ;\n".
  735. "expected: $exit; actual: $resExit;\nprog: $prog\n";
  736. }
  737. # check the stdout diff
  738. my $diffOutExit = runCommand("$diff $expectOut $out");
  739. if (!($diffOutExit == 0)) {
  740. die "$inputFile: mode $mode; expected and actual stdout differ;".
  741. "\nprog: $prog\n";
  742. }
  743. # check the stderr diff
  744. my $diffErrExit = runCommand("$diff $expectErr $err");
  745. if (!($diffErrExit == 0)) {
  746. die "$inputFile: mode $mode; expected and actual stderr differ;".
  747. "\nprog: $prog\n";
  748. }
  749. # test passes
  750. unless ($keep_temp_files) {
  751. # delete the temporary files
  752. deleteFile(xformFileName($inputFile, $mode, ""));
  753. for my $file (@otherFiles) {
  754. checkMode($mode, "should not happen");
  755. deleteFile(xformFileName($file, $mode, ""));
  756. }
  757. deleteFile($expectOut);
  758. deleteFile($expectErr);
  759. deleteFile($out);
  760. deleteFile($err);
  761. }
  762. }
  763. # **** main
  764. eval {
  765. # get the command line state
  766. readCommandLine();
  767. # get the input modes
  768. if (!@inputModes) {
  769. print "get the input modes since none specified\n" if $verbose;
  770. # the second argument is the mode; passing undef means don't output
  771. # anything
  772. xformFile($inputFile, undef);
  773. # print xformState
  774. if ($print_xform_state) {
  775. printXformFileState();
  776. exit(0);
  777. }
  778. # save the input modes
  779. @inputModes = @modes;
  780. }
  781. # run tests
  782. die "Something is wrong as there are no modes at all.\n" unless @inputModes;
  783. print "for each mode in inputModes: " . join(" ", @inputModes) if $verbose;
  784. for my $mode (@inputModes) {
  785. runTest($mode);
  786. }
  787. # record that this test was run and passed if that was requested
  788. if ($recordInputFile) {
  789. open REC, ">>$recordInputFile" or die "$!: $recordInputFile\n";
  790. print REC "$inputFile\n";
  791. close REC or die "$!: $recordInputFile\n";
  792. }
  793. };
  794. if ($@) {
  795. warn "$@";
  796. exit(255);
  797. }
  798. # **** documentation
  799. __DATA__
  800. Documentation for mts: Multi-TeSter
  801. Multi-TeSter, mts, implements a domain-specific language, MTS, for
  802. generating and running multiple modes of similar tests from a single
  803. source. 1) For each mode MTS runs a command line and then checks that
  804. the expected exit code, stdout, and stderr result, failing if they do
  805. not. 2) An MTS input file can have multiple modes and all of the
  806. above aspects of running the program being tested can depend on the
  807. current mode; this feature allows multiple very similar tests to be
  808. expressed in one file, re-using their commonality.
  809. Run MTS as follows:
  810. mts [FLAGS] FILE.mts
  811. This documentation is embedded in MTS at the end; to get MTS to print
  812. this documentation, type:
  813. mts --help
  814. **** command line arguments
  815. The command line arguments are as follows:
  816. FLAGS: all of these are optional.
  817. --help
  818. Print this documentation and stop.
  819. --verbose
  820. MTS comments on what its internals are doing; mostly for debugging
  821. MTS itself.
  822. --quiet
  823. Don't even print modes as they are tested. Overrides
  824. --print-commands.
  825. --record-input-file=REC_FILE
  826. Record the input file name by appending it to REC_FILE after all
  827. of its mode tests have passed.
  828. --print-cmdline-state
  829. Print out the command line state and stop.
  830. --print-xform-state
  831. Print out the state read from the input file configuration
  832. commands after the first pass over the input file and stop.
  833. --print-commands=PRINT_COMMANDS
  834. Print out commands as they are run iff PRINT_COMMANDS evaluates to
  835. perl-true; on by default.
  836. --keep-temp-files=KEEP_TEMP_FILES
  837. Keep the temporary files iff KEEP_TEMP_FILES evaluates to
  838. perl-true; off by default.
  839. --prefix=PRE
  840. Use PRE as the prefix instead of the default "MTS".
  841. --diff=DIFF
  842. Use DIFF as the diff program instead of the default "diff".
  843. --cfg=CONFIG_FILE
  844. Immediately read in each line of CONFIG_FILE as if it were a
  845. command-line argument. The suggested suffix for config files is
  846. ".mts.cfg".
  847. --set:VAR=VALUE
  848. Supply a name-value pair which will be interpolated for the
  849. argument to prog, exit, expout, experr, add, sub, and in the file
  850. body. When multiple variables match at a location (one being a
  851. prefix of the other) the longest one is substituted.
  852. --mode=MODE
  853. Add this mode to the list of modes to run. It must occur in the
  854. list of MTS_prog(MODE_LIST) of each file. If no modes are given,
  855. those used in the union of the MTS_prog(MODE_LIST) commands of the
  856. first file are used.
  857. FILE.mts: This is the 'first file' below; it contains commands for
  858. including other files and for how to run the test and check if it
  859. succeeded or not. The name must end in '.mts'; now however that the
  860. ending is stripped from the name for each version made for each mode;
  861. therefore if you are testing a program that requires that the input
  862. file end in '.foo' your MTS testing files should end in '.foo.mts'.
  863. **** embedded command language
  864. The source files have an embedded command language as follows. MTS is
  865. a prefix string parameter; it can be reset using --pre=PRE. MODE_LIST
  866. is a list of string mode names.
  867. ** These commands must occur on a line by themselves and the whole
  868. line is not copied to the output. They can occur in any file.
  869. MTS_modes(MODE_LIST)
  870. MTS_omitmodes(MODE_LIST)
  871. Only the modes will be run. Only the modes union the omitmodes are
  872. allowed. The omitmodes command gives you a way to turn off a mode
  873. without having to remove everything about it from the file. The fact
  874. that only the modes union the omitmodes are allowed prevents subtle
  875. errors if a mode name is mis-typed. If no --mode=MODE arguments are
  876. given on the command line then the modes listed in the first file are
  877. the ones that are run. If there is more than one such modes or
  878. omitmodes command in the file, their lists respectively concatenate.
  879. Listing a mode in either one is idempotent.
  880. MTS_version(version_string)
  881. Each version of MTS has an internal version string. This command
  882. asserts that the internal vesion string is 'version_string'.
  883. MTS: This is a comment.
  884. A comment; the entire line is deleted and ignored.
  885. ** These commands refer to the whole line they are on implicitly as
  886. their argument; the command is deleted from the line as the line is
  887. copied. They can occur in any of the files.
  888. MTS_add(MODE_LIST)
  889. Add this line to the file when in one of the modes in MODE_LIST.
  890. MTS_sub(MODE_LIST)
  891. Subtract this line from the file when in one of the modes in
  892. MODE_LIST.
  893. ** These commands must occur on a line by themselves and the whole
  894. line is not copied to the output. They can occur only in the first
  895. file in the files list: the one given on the command line.
  896. MTS_other(MODE_LIST):LIST_OF_OTHER_FILES
  897. Include other files in the files list: interpolate them as MTS_OTHER
  898. and include them in MTS_FILES. LIST_OF_OTHER_FILES is a
  899. space-separated list of filenames. Repeating a file for a given mode
  900. will result in an error.
  901. MTS_prog(MODE_LIST):PROG
  902. Run the program when in a mode in the mode list. There must be at
  903. least one such line per mode and multiple lines per mode concatenate.
  904. PROG interpolates MTS_MODE as the mode string and MTS_FILES,
  905. MTS_FIRST_FILE, and MTS_OTHER_FILES as the list of files being
  906. processed, the first file, and the others (not first) files,
  907. respectively. Note that if the prog line is spread across multiple
  908. prog commands and there is later an error in the prog line, say during
  909. interpolation, the error location will be reported as having occurred
  910. on the first line; it is otherwise too difficult to track the source
  911. location information.
  912. MTS_exit(MODE_LIST):EXIT_VALUE
  913. Expect the exit value. Repeating an exit specification for a given
  914. mode will result in an error.
  915. MTS_expout(MODE_LIST):LINE_OF_OUTPUT
  916. MTS_experr(MODE_LIST):LINE_OF_OUTPUT
  917. Append LINE_OF_OUTPUT to the expected standard out or err
  918. respectively; replace the colon with a semi-colon to elide the
  919. newline. The string MTS_THIS is interpolated as the input filename to
  920. each test for each mode.
  921. MTS_expout(MODE) {
  922. BLOCK_OF_OUTPUT
  923. ...
  924. MTS_expout(MODE) }
  925. MTS_experr(MODE) {
  926. BLOCK_OF_OUTPUT
  927. ...
  928. MTS_experr(MODE) }
  929. As above but appends an entire block into the expected out or err.
  930. Note that only ONE mode is allowed, not a list of modes. No commands
  931. other than comments are allowed within an expout or experr block.
  932. **** overview of operation
  933. The modes are determined by accumulating the command-line --mode=MODE
  934. flags, or if there are none, but accumulating the MTS_modes(MODE_LIST)
  935. commands in the first file. Each mode is a test run separately as
  936. follows.
  937. Given a mode, the file list is traversed and for each file of the form
  938. FILE.END (where the '.' is the first in the filename) the file
  939. FILE_MODE.END is made, following the command language above. From the
  940. MTS_expout(..., MODE, ...)
  941. MTS_expout(MODE) {
  942. MTS_expout(MODE) }
  943. and
  944. MTS_experr(..., MODE, ...)
  945. MTS_experr(MODE) {
  946. MTS_experr(MODE) }
  947. lines and the the expected standard out and err are assembled,
  948. MTS_THIS is interpolated to FILE_MODE.END, and the expected contents
  949. written to FILE_MODE.expout and FILE_MODE.experr, respectively. These
  950. three generated files are made read-only to prevent inadvertent
  951. editing in the event of an error reported within them; of course it is
  952. the input to MTS that should be edited.
  953. In the first file there must be exactly one line of the form
  954. MTS_prog(..., MODE, ...):PROG
  955. PROG is run with MTS_FILES, MTS_FIRST_FILE, and MTS_OTHER_FILES
  956. interpolated as the list of files being processed, the first file, and
  957. the others (not first) files, respectively, and with MTS_MODE
  958. interpolated as the current mode. Output is redirected to
  959. FILE_MODE.stdout and FILE_MODE.stderr respectively.
  960. There must be one line of the form
  961. MTS_exit(.., MODE, ...):EXIT_VALUE
  962. The exit value is checked to be EXIT_VALUE; if it is not, the script
  963. dies with "expected and actual exit values differ". If that passes
  964. then the following command is run:
  965. DIFF FILE_MODE.stdout FILE_MODE.expout
  966. If diff returns non-zero the diff is displayed and the script dies
  967. similarly as before. If that passes then the standard err
  968. expectations are checked similarly.
  969. **** usage suggestion: do controlled experiments
  970. If you have a set of conditions that produces a result, it is a bit
  971. hard to know what was essential to those conditions for the result.
  972. However, if you have two very similar sets of conditions one of which
  973. produces the result and the other of which does not, then you know the
  974. *difference* of these two sets contains something essential. Often it
  975. is easy to make this difference small. Scientists call this a
  976. "controlled experiment".
  977. The multiple-modes aspect of MTS exists to enable controlled
  978. experiments as follows. To test a single feature of a piece of
  979. software, it is often necessary to provide a lot of set-up just to get
  980. to the situation where the feature is even relevant. The best way to
  981. test the feature is to write two input files, one that provides all of
  982. this set-up and then just stops [call this the "control"] and another
  983. that then adds the little bit more needed to invoke the feature [maybe
  984. the "out-of-control"?? :-)]. These two tests will be able to share
  985. most of their set-up, and yet should also produce different results.
  986. To do this with MTS, just write one input file and use, say, the
  987. MTS_add feature to implement the difference between the two modes.
  988. An example of this idiom occurs in the Oink project which computes
  989. whole-program static-time dataflow for C++ programs. For example, to
  990. test that the dataflow for a pointer-to-member is computed correctly,
  991. I didn't just write a C++ program that used pointers-to-members and
  992. then check that the dataflow graph connected from the source to the
  993. sink. Instead I run two tests, almost the same except that one has
  994. the pointer-to-member de-reference commented out. The dataflow graph
  995. should connect for one and not for the other. A more primitive
  996. version of the script is used for this.
  997. **** usage suggestion: embed a query language
  998. It is simple and templating to have a program generate large outputs
  999. and then record that as the expout in an MTS file and have MTS diff
  1000. the actual output with the expected as test. However, this is really
  1001. not the best design: 1) You have entangled the output routines and
  1002. whatever other aspect of your program you are testing. 2) Output
  1003. routines can loose information, resulting in loss of fidelity. 3) The
  1004. print routine is likely to print information on multiple independent
  1005. aspects. 4) Loss of abstraction: internal data structures could
  1006. change without actually invalidating the test.
  1007. Again, with the Oink dataflow tests, we solve this by returning a very
  1008. small output: the return value of the oink/qual program indicates
  1009. whether or not the graph connected. Scott McPeak solves this problem
  1010. in Elsa, a C++ front-end (the one used by Oink) by simply extending
  1011. C++ to contain a query/assertion language. He can now think of his
  1012. tests using a database metaphor, each test having two parts: data and
  1013. query. That is, most of the input is a C++ fragment ("data") that is
  1014. parsed and type-checked by Elsa. When the query language extensions
  1015. to C++ are encountered, the Elsa engine runs internal queries and
  1016. possibly asserts their results. Now a test with a large input has a
  1017. very small output, namely the query results, or even just the fact
  1018. that none of the assertions failed. The problems in the first
  1019. paragraph go away. One way to look at MTS is as a way of providing
  1020. this single-source data/query idiom for programs in general.
  1021. **** bugs
  1022. Note that the entire input file is re-processed for each mode and
  1023. therefore the performance of MTS is quadratic in the number of modes:
  1024. each pass has to read and ignore the output for all passes.
  1025. Non-trivial performance loss can be significant when the expected
  1026. output from each mode is non-trivial. Therefore, use the multiple
  1027. modes feature when the modes tend to share input but when they are
  1028. independent, split them into two different files.
  1029. **** license
  1030. Copyright (c) 2007, 2008
  1031. Daniel S. Wilkerson.
  1032. All rights reserved.
  1033. Redistribution and use in source and binary forms, with or without
  1034. modification, are permitted provided that the following conditions are
  1035. met:
  1036. Redistributions of source code must retain the above copyright
  1037. notice, this list of conditions and the following disclaimer.
  1038. Redistributions in binary form must reproduce the above copyright
  1039. notice, this list of conditions and the following disclaimer in the
  1040. documentation and/or other materials provided with the
  1041. distribution.
  1042. Neither the name of the author, Daniel S. Wilkerson, nor the names
  1043. of other contributors may be used to endorse or promote products
  1044. derived from this software without specific prior written
  1045. permission.
  1046. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  1047. "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  1048. LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  1049. A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  1050. OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  1051. SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  1052. LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  1053. DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  1054. THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  1055. (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  1056. OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.