blob: fb1e457e8e99e4b05268b41a39f9a9dabc6732e7 [file] [log] [blame]
#!/usr/bin/env perl
# Copyright (C) 2018 Bocoup LLC. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above
# copyright notice, this list of conditions and the following
# disclaimer.
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials
# provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER "AS IS" AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
# OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
use strict;
use warnings;
use 5.8.8;
package Test262::Runner;
use File::Find;
use File::Temp qw(tempfile tempdir);
use File::Spec::Functions qw(abs2rel);
use File::Basename qw(dirname);
use File::Path qw(mkpath);
use Cwd qw(abs_path);
use FindBin;
use Env qw(DYLD_FRAMEWORK_PATH);
use Config;
use Time::HiRes qw(time);
use IO::Handle;
use IO::Select;
my $Bin;
BEGIN {
$ENV{DBIC_OVERWRITE_HELPER_METHODS_OK} = 1;
$Bin = $ENV{T262_EXEC_BIN} || $FindBin::Bin;
unshift @INC, "$Bin";
unshift @INC, "$Bin/lib";
unshift @INC, "$Bin/local/lib/perl5";
unshift @INC, "$Bin/local/lib/perl5/$Config{archname}";
unshift @INC, "$Bin/..";
$ENV{LOAD_ROUTES} = 1;
}
use YAML qw(Load Dump Bless);
use Parallel::ForkManager;
use Getopt::Long qw(GetOptions);
# Tweaked versions of YAML::DumpFile and YAML::LoadFile.
# The library versions unconditionally and erroneously assume that a UTF-8 encoding needs to be performed,
# meaning that the output file ends up double-encoded.
sub DumpFile {
my $OUT;
my $filename = shift;
if (Scalar::Util::openhandle $filename) {
$OUT = $filename;
}
else {
my $mode = '>';
if ($filename =~ /^\s*(>{1,2})\s*(.*)$/) {
($mode, $filename) = ($1, $2);
}
open $OUT, $mode, $filename
or YAML::Mo::Object->die('YAML_DUMP_ERR_FILE_OUTPUT', $filename, "$!");
}
local $/ = "\n"; # reset special to "sane"
print $OUT Dump(@_);
unless (ref $filename eq 'GLOB') {
close $OUT
or do {
my $errsav = $!;
YAML::Mo::Object->die('YAML_DUMP_ERR_FILE_OUTPUT_CLOSE', $filename, $errsav);
}
}
}
sub LoadFile {
my $IN;
my $filename = shift;
if (Scalar::Util::openhandle $filename) {
$IN = $filename;
}
else {
open $IN, '<', $filename
or YAML::Mo::Object->die('YAML_LOAD_ERR_FILE_INPUT', $filename, "$!");
}
return Load(do { local $/; <$IN> });
}
my $webkitdirIsAvailable;
if (eval {require webkitdirs; 1;}) {
webkitdirs->import(qw(executableProductDir setConfiguration));
$webkitdirIsAvailable = 1;
}
my $podIsAvailable;
if (eval {require Pod::Usage; 1;}) {
Pod::Usage->import();
$podIsAvailable = 1;
}
# Commandline settings
my $maxProcesses;
my @cliTestDirs;
my $verbose;
my $JSC;
my $harnessDir;
my %filterFeatures;
my $ignoreConfig;
my $config;
my %configSkipHash;
my $expect;
my $saveExpectations;
my $failingOnly;
my $latestImport;
my $runningAllTests;
my $timeout;
my $skippedOnly;
my $test262Dir;
my $webkitTest262Dir = abs_path("$Bin/../../../JSTests/test262");
my $expectationsFile = abs_path("$webkitTest262Dir/expectations.yaml");
my $configFile = abs_path("$webkitTest262Dir/config.yaml");
my $resultsDir = $ENV{PWD} . "/test262-results";
my $resultsFile;
my $summaryTxtFile;
my $summaryFile;
my @results;
my @files;
my $tempdir = tempdir();
my ($deffh, $deffile) = getTempFile();
my $startTime = time();
main();
sub processCLI {
my $help = 0;
my $release;
my $ignoreExpectations;
my @features;
my $stats;
my $specifiedResultsFile;
my $specifiedExpectationsFile;
# If adding a new commandline argument, you must update the POD
# documentation at the end of the file.
GetOptions(
'j|jsc=s' => \$JSC,
't|t262=s' => \$test262Dir,
'o|test-only=s@' => \@cliTestDirs,
'p|child-processes=i' => \$maxProcesses,
'h|help' => \$help,
'release' => \$release,
'debug' => sub { $release = 0; },
'v|verbose' => \$verbose,
'f|features=s@' => \@features,
'c|config=s' => \$configFile,
'i|ignore-config' => \$ignoreConfig,
's|save' => \$saveExpectations,
'e|expectations=s' => \$specifiedExpectationsFile,
'x|ignore-expectations' => \$ignoreExpectations,
'F|failing-files' => \$failingOnly,
'l|latest-import' => \$latestImport,
'stats' => \$stats,
'r|results=s' => \$specifiedResultsFile,
'timeout=i' => \$timeout,
'S|skipped-files' => \$skippedOnly,
);
if ($help) {
if ($podIsAvailable) {
pod2usage(-exitstatus => 0, -verbose => 2, -input => __FILE__);
} else {
print "Pod::Usage is not available to print the help options\n";
exit;
}
}
if ($specifiedResultsFile) {
if (!$stats && !$failingOnly) {
print "Waring: supplied results file not used for this command.\n";
}
elsif (-e $specifiedResultsFile) {
$resultsFile = $specifiedResultsFile;
}
else {
die "Error: results file $specifiedResultsFile does not exist.";
}
}
if ($stats || $failingOnly) {
# If not supplied, try to find the results file in expected directory
$resultsFile ||= abs_path("$resultsDir/results.yaml");
if ($failingOnly && ! -e $resultsFile) {
die "Error: cannot find results file to run failing tests," .
"please specify with --results.";
}
if ($stats) {
if (! -e $resultsFile) {
die "Error: cannot find results file to summarize," .
" please specify with --results.";
}
summarizeResults();
exit;
}
}
if ($JSC) {
$JSC = abs_path($JSC);
# Make sure the path and file jsc exist
if (! ($JSC && -e $JSC)) {
die "Error: --jsc path does not exist.";
}
if (not defined $DYLD_FRAMEWORK_PATH) {
$DYLD_FRAMEWORK_PATH = dirname($JSC);
}
} else {
$JSC = getBuildPath($release);
}
if ($latestImport) {
# Does not allow custom $test262Dir
$test262Dir = '';
}
if (! $test262Dir) {
$test262Dir = $webkitTest262Dir;
} else {
$test262Dir = abs_path($test262Dir);
}
$harnessDir = "$test262Dir/harness";
if (! -e $harnessDir) {
# if the harness directory does not exist in the custom test262 path,
# then use the webkits harness directory.
$harnessDir = "$webkitTest262Dir/harness";
}
if (! $ignoreConfig) {
if ($configFile && ! -e $configFile) {
die "Error: Config file $configFile does not exist!\n" .
"Run without config file with -i or supply with --config.\n";
}
$config = LoadFile($configFile) or die $!;
if ($config->{skip} && $config->{skip}->{files}) {
%configSkipHash = map { $_ => 1 } @{$config->{skip}->{files}};
}
}
if ($specifiedExpectationsFile) {
$expectationsFile = abs_path($specifiedExpectationsFile);
if (! -e $expectationsFile && ! $ignoreExpectations) {
print("Warning: Supplied expectations file $expectationsFile does"
. " not exist. Running tests without expectation file.\n");
}
}
# If the expectation file doesn't exist and is not specified, run all tests.
# If we are running only skipped files, then ignore the expectation file.
if (! $ignoreExpectations && -e $expectationsFile && !$skippedOnly) {
$expect = LoadFile($expectationsFile) or die $!;
}
# If running only the skipped files from the config list
if ($skippedOnly) {
if (! -e $configFile) {
die "Error: Config file $configFile does not exist!\n" .
"Cannot run skipped tests from config. Supply file with --config.\n";
}
}
if (@features) {
%filterFeatures = map { $_ => 1 } @features;
}
$maxProcesses ||= getProcesses();
print "\nSettings:\n"
. "Test262 Dir: " . abs2rel($test262Dir) . "\n"
. "JSC: " . abs2rel($JSC) . "\n"
. "Child Processes: $maxProcesses\n";
print "Test timeout: $timeout\n" if $timeout;
print "DYLD_FRAMEWORK_PATH: $DYLD_FRAMEWORK_PATH\n" if $DYLD_FRAMEWORK_PATH;
print "Features to include: " . join(', ', @features) . "\n" if @features;
print "Paths: " . join(', ', @cliTestDirs) . "\n" if @cliTestDirs;
print "Config file: " . abs2rel($configFile) . "\n" if $config;
print "Expectations file: " . abs2rel($expectationsFile) . "\n" if $expect;
print "Results file: ". abs2rel($resultsFile) . "\n" if $stats || $failingOnly;
print "Running only the failing files in expectations.yaml\n" if $failingOnly;
print "Running only the latest imported files\n" if $latestImport;
print "Running only the skipped files in config.yaml\n" if $skippedOnly;
print "Verbose mode\n" if $verbose;
print "---\n\n";
}
sub main {
processCLI();
my @defaultHarnessFiles = (
"$harnessDir/sta.js",
"$harnessDir/assert.js",
"$harnessDir/doneprintHandle.js",
);
print $deffh getHarness(\@defaultHarnessFiles);
if (!@cliTestDirs) {
# If not commandline test path supplied, use the root directory of all tests.
push(@cliTestDirs, 'test');
$runningAllTests = 1 if !$latestImport;
}
if ($latestImport) {
@files = loadImportFile();
} elsif ($failingOnly) {
# If we only want to re-run failure, only run tests in results file
findAllFailing();
} else {
# Otherwise, get all files from directory
foreach my $testsDir (@cliTestDirs) {
find(
{ wanted => \&wanted, bydepth => 1 },
qq($test262Dir/$testsDir)
);
sub wanted {
/(?<!_FIXTURE)\.[jJ][sS]$/s && push(@files, $File::Find::name);
}
}
}
my $pm = Parallel::ForkManager->new($maxProcesses);
my $select = IO::Select->new();
my @children;
my @parents;
my $activeChildren = 0;
my @resultsfhs;
# Open each process
PROCESSES:
foreach (0..$maxProcesses-1) {
my $i = $_;
# Make temporary files to record results
my ($fh, $filename) = getTempFile();
$resultsfhs[$i] = $fh;
socketpair($children[$i], $parents[$i], 1, 1, 0);
my $child = $children[$i];
my $parent = $parents[$i];
$child->autoflush(1);
$parent->autoflush(1);
# seeds each child with a file;
my $pid = $pm->start;
if ($pid) { # parent
$select->add($child);
# each child starts with a file;
my $file = shift @files;
if ($file) {
chomp $file;
print $child "$file\n";
$activeChildren++;
}
next PROCESSES;
}
# children will start here
srand(time ^ $$); # Creates a new seed for each fork
CHILD:
while (<$parent>) {
my $file = $_;
chomp $file;
if ($file eq 'END') {
last;
}
processFile($file, $resultsfhs[$i]);
print $parent "signal\n";
}
$child->close();
$pm->finish;
}
my @ready;
FILES:
while ($activeChildren and @ready = $select->can_read($timeout)) {
foreach (@ready) {
my $readyChild = $_;
my $childMsg = <$readyChild>;
chomp $childMsg;
$activeChildren--;
my $file = shift @files;
if ($file) {
chomp $file;
print $readyChild "$file\n";
$activeChildren++;
} elsif (!$activeChildren) {
last FILES;
}
}
}
foreach (@children) {
print $_ "END\n";
}
foreach (@parents) {
$_->close();
}
$pm->wait_all_children;
# Read results from file into @results and close
foreach (0..$maxProcesses-1) {
my $i = $_;
seek($resultsfhs[$i], 0, 0);
push @results, LoadFile($resultsfhs[$i]);
close $resultsfhs[$i];
}
close $deffh;
@results = sort { "$a->{path} . $a->{mode}" cmp "$b->{path} . $b->{mode}" } @results;
my %failed;
my $failcount = 0;
my $newfailcount = 0;
my $newpasscount = 0;
my $skipfilecount = 0;
my $newfailurereport = '';
my $newpassreport = '';
# Create expectation file and calculate results
foreach my $test (@results) {
my $expectedFailure = 0;
if ($expect && $expect->{$test->{path}}) {
$expectedFailure = $expect->{$test->{path}}->{$test->{mode}}
}
if ($test->{result} eq 'FAIL') {
$failcount++;
# Record this round of failures
SetFailureForTest(\%failed, $test);
# If an unexpected failure
if (!$expectedFailure || ($expectedFailure ne $test->{error})) {
$newfailcount++;
if ($verbose) {
my $path = $test->{path};
my $mode = $test->{mode};
# Print full output from JSC
my $err = $test->{output};
$newfailurereport .= "FAIL $path ($mode)\n"
. "Full Output:\n"
. "$err\n\n";
}
}
}
elsif ($test->{result} eq 'PASS') {
# If this is an newly passing test
if ($expectedFailure || $skippedOnly) {
$newpasscount++;
if ($verbose || $skippedOnly) {
my $path = $test->{path};
my $mode = $test->{mode};
$newpassreport .= "PASS $path ($mode)\n";
}
}
}
elsif ($test->{result} eq 'SKIP') {
$skipfilecount++;
}
}
# In verbose mode, the result of every test is printed, so summarize useful results
if ($verbose && $expect && ($newfailurereport || $newpassreport)) {
print "\n";
if ($newfailurereport) {
print "---------------NEW FAILING TESTS SUMMARY---------------\n\n";
print "$newfailurereport";
}
if ($newpassreport) {
print "---------------NEW PASSING TESTS SUMMARY---------------\n\n";
print "$newpassreport\n";
}
print "---------------------------------------------------------\n\n";
}
# If we are running only skipped tests, report all the new passing tests
if ($skippedOnly && $newpassreport) {
print "---------------NEW PASSING TESTS SUMMARY---------------\n";
print "\n$newpassreport\n";
print "---------------------------------------------------------\n";
}
my $totalRun = scalar @results - $skipfilecount;
print "\n$totalRun tests run\n";
print "$skipfilecount test files skipped\n";
if (!$expect) {
print "$failcount tests failed\n";
print "$newpasscount tests newly pass\n" if $skippedOnly;
} else {
print "$failcount tests failed in total\n";
print "$newfailcount tests newly fail\n";
print "$newpasscount tests newly pass\n";
}
if ($saveExpectations) {
if (!$runningAllTests) {
UpdateResults($expect, \@results, \%failed);
}
DumpFile($expectationsFile, \%failed);
print "Saved expectation file in: $expectationsFile\n";
}
if (! -e $resultsDir) {
mkpath($resultsDir);
}
$resultsFile = abs_path("$resultsDir/results.yaml");
DumpFile($resultsFile, \@results);
print "Saved all the results in $resultsFile\n";
my $styleCss = abs_path("$Bin/report.css");
qx/cp $styleCss $resultsDir/;
summarizeResults();
printHTMLResults(\%failed, $totalRun, $failcount, $newfailcount, $skipfilecount);
print "See the summaries and results in the $resultsDir.\n\n";
printf("Done in %.2f seconds!\n", time() - $startTime);
my $totalfailures = $expect ? $newfailcount : $failcount;
exit ($totalfailures ? 1 : 0);
}
sub loadImportFile {
my $importFile = abs_path("$Bin/../../../JSTests/test262/latest-changes-summary.txt");
die "Error: Import file not found at $importFile.\n" if ! -e $importFile;
open(my $fh, "<", $importFile) or die $!;
my @files = grep { $_ =~ /^[AM]\s*test\// } <$fh>;
return map { $_ =~ s/^\w\s(\w*)/$test262Dir\/$1/; chomp $_; $_ } @files;
}
sub getProcesses {
return $ENV{NUMBER_OF_PROCESSORS} if (defined($ENV{NUMBER_OF_PROCESSORS}));
my $cores;
my $uname = qx(which uname >> /dev/null && uname);
chomp $uname;
if ($uname eq 'Darwin') {
# sysctl should be available
$cores = qx/sysctl -n hw.ncpu/;
} elsif ($uname eq 'Linux') {
$cores = qx(which getconf >> /dev/null && getconf _NPROCESSORS_ONLN);
if (!$cores) {
$cores = qx(which lscpu >> /dev/null && lscpu -p | egrep -v '^#' | wc -l);
}
}
chomp $cores;
if (!$cores) {
$cores = 1;
}
return $cores;
}
sub parseError {
my $error = shift;
if ($error =~ /^Exception: ([\w\d]+: .*)/m) {
return $1;
} else {
# Unusual error format. Save the first line instead.
my @errors = split("\n", $error);
return $errors[0];
}
}
sub getBuildPath {
my ($release) = @_;
my $jsc;
if ($webkitdirIsAvailable) {
my $webkit_config = $release ? 'Release' : 'Debug';
setConfiguration($webkit_config);
my $jscDir = executableProductDir();
$jsc = $jscDir . '/jsc';
$jsc = $jscDir . '/JavaScriptCore.framework/Helpers/jsc' if (! -e $jsc);
$jsc = $jscDir . '/bin/jsc' if (! -e $jsc);
# Sets the Env DYLD_FRAMEWORK_PATH, abs_path will remove any extra '/' character
$DYLD_FRAMEWORK_PATH = abs_path(dirname($jsc)) if (-e $jsc);
}
if (! $jsc || ! -e $jsc) {
# If we cannot find jsc using webkitdirs, look in path
$jsc = qx(which jsc);
chomp $jsc;
if (! $jsc ) {
die("Cannot find jsc, try with --release or specify with --jsc <path>.\n\n");
}
}
return $jsc;
}
sub processFile {
my ($filename, $resultsfh) = @_;
my $contents = getContents($filename);
my $data = parseData($contents, $filename);
my $resultsdata;
# Check test against filters in config file
my $file = abs2rel( $filename, $test262Dir );
my $skipTest = shouldSkip($file, $data);
# If we only want to run skipped tests, invert filter
$skipTest = !$skipTest if $skippedOnly;
if ($skipTest) {
if (! $skippedOnly) {
$resultsdata = processResult($filename, $data, "skip");
DumpFile($resultsfh, $resultsdata);
}
return;
}
else {
my @scenarios = getScenarios(@{ $data->{flags} });
my $includes = $data->{includes};
my ($includesfh, $includesfile);
($includesfh, $includesfile) = compileTest($includes) if defined $includes;
my $args = getFeatureFlags($data);
foreach my $scenario (@scenarios) {
my ($exitCode, $result, $execTime) = runTest($includesfile, $filename, $scenario, $data, $args);
$resultsdata = processResult($filename, $data, $scenario, $exitCode, $result, $execTime);
DumpFile($resultsfh, $resultsdata);
}
close $includesfh if defined $includesfh;
}
}
sub getFeatureFlags {
my ($data) = @_;
my $featureFlags = '';
if (exists $config->{flags} and $data->{features}) {
foreach my $feature (@{ $data->{features} }) {
$featureFlags .= ' --' . $config->{flags}->{$feature} . '=1' if $config->{flags}->{$feature};
}
}
if (grep $_ eq 'CanBlockIsFalse', @{$data->{flags}}) {
$featureFlags .= ' --can-block-is-false';
}
return $featureFlags;
}
sub shouldSkip {
my ($filename, $data) = @_;
if (exists $config->{skip}) {
# Filter by file
if( $configSkipHash{$filename} ) {
return 1;
}
# Filter by paths
my @skipPaths;
@skipPaths = @{ $config->{skip}->{paths} } if defined $config->{skip}->{paths};
return 1 if (grep {$filename =~ $_} @skipPaths);
my @skipFeatures;
@skipFeatures = map {
# Remove inline comments from the yaml parsed config
$_ =~ /(\S*)/;
} @{ $config->{skip}->{features} } if defined $config->{skip}->{features};
my $skip = 0;
my $keep = 0;
my @features = @{ $data->{features} } if $data->{features};
# Filter by features, loop over file features to for less iterations
foreach my $feature (@features) {
$skip = (grep {$_ eq $feature} @skipFeatures) ? 1 : 0;
# keep the test if the config skips the feature but it was also request
# through the CLI --features
return 1 if $skip && !$filterFeatures{$feature};
$keep = 1 if $filterFeatures{$feature};
}
# filter tests that do not contain the --features features
return 1 if (%filterFeatures and not $keep);
}
return 0;
}
sub getScenarios {
my @flags = @_;
my @scenarios;
my $nonStrict = 'default';
my $strictMode = 'strict mode';
if (grep $_ eq 'raw', @flags) {
push @scenarios, 'raw';
} elsif (grep $_ eq 'noStrict', @flags) {
push @scenarios, $nonStrict;
} elsif (grep $_ eq 'onlyStrict', @flags) {
push @scenarios, $strictMode;
} elsif (grep $_ eq 'module', @flags) {
push @scenarios, 'module';
} else {
# Add 2 default scenarios
push @scenarios, $strictMode;
push @scenarios, $nonStrict;
};
return @scenarios;
}
sub compileTest {
my $includes = shift;
my ($tfh, $tfname) = getTempFile();
my @includes = map { "$harnessDir/$_" } @{ $includes };
my $includesContent = getHarness(\@includes);
print $tfh $includesContent;
return ($tfh, $tfname);
}
sub runTest {
my ($includesfile, $filename, $scenario, $data, $args) = @_;
$includesfile ||= '';
if ($timeout) {
$args .= " --watchdog=$timeout ";
}
if (exists $data->{negative}) {
my $type = $data->{negative}->{type};
$args .= " --exception=$type ";
}
if (exists $data->{flags}) {
if (grep $_ eq 'async', @{ $data->{flags} }) {
$args .= ' --test262-async ';
}
}
my $prefixFile = '';
if ($scenario eq 'module') {
$prefixFile='--module-file=';
} elsif ($scenario eq 'strict mode') {
$prefixFile='--strict-file=';
}
# Raw tests should not include the default harness
my $defaultHarness = '';
$defaultHarness = $deffile if $scenario ne 'raw';
my $prefix = $DYLD_FRAMEWORK_PATH ? qq(DYLD_FRAMEWORK_PATH=$DYLD_FRAMEWORK_PATH) : "";
my $execTimeStart = time();
my $result = qx($prefix $JSC $args $defaultHarness $includesfile '$prefixFile$filename');
my $execTime = time() - $execTimeStart;
chomp $result;
if ($?) {
return ($?, $result, $execTime);
} else {
return ($?, 0, $execTime);
}
}
sub processResult {
my ($path, $data, $scenario, $exitCode, $result, $execTime) = @_;
# Report a relative path
my $file = abs2rel( $path, $test262Dir );
my %resultdata;
$resultdata{path} = $file;
$resultdata{mode} = $scenario;
$resultdata{time} = $execTime;
my $currentfailure = parseError($result) if $result;
my $expectedfailure = $expect
&& $expect->{$file}
&& $expect->{$file}->{$scenario};
my $exitSignalNumber = $exitCode & 0x7f if $scenario ne 'skip';
if ($scenario ne 'skip' && ($currentfailure || $exitSignalNumber)) {
# We have a new failure if the process crashed OR we haven't loaded
# an expectation file (all fails are new) OR we have loaded an
# expectation fail and (there is no expected failure OR the failure
# has changed).
my $isnewfailure = $exitSignalNumber || !$expect
|| !$expectedfailure || $expectedfailure ne $currentfailure;
# Print the failure if we haven't loaded an expectation file
# or the failure is new.
my $printFailure = (!$expect || $isnewfailure) && !$skippedOnly;
my $newFail = '';
$newFail = '! NEW ' if $isnewfailure;
$newFail = "$newFail (Exit code: $exitCode) " if $exitSignalNumber;
my $failMsg = '';
$failMsg = "FAIL $file ($scenario)\n";
my $featuresList = '';
if ($verbose && $data->{features}) {
$featuresList = 'Features: ' . join(', ', @{ $data->{features} }) . "\n";
}
print "$newFail$failMsg$featuresList$result\n\n" if ($printFailure || $verbose);
$resultdata{result} = 'FAIL';
$resultdata{error} = $currentfailure;
$resultdata{error} = "Bad exit code: $exitCode" if $exitSignalNumber;
$resultdata{output} = $result;
} elsif ($scenario ne 'skip' && !$currentfailure) {
if ($expectedfailure) {
print "NEW PASS $file ($scenario)\n";
} elsif ($verbose) {
print "PASS $file ($scenario)\n";
}
$resultdata{result} = 'PASS';
} else {
if ($verbose) {
print "SKIP $file\n";
}
$resultdata{result} = 'SKIP';
}
$resultdata{features} = $data->{features} if $data->{features};
return \%resultdata;
}
sub getTempFile {
my ($tfh, $tfname) = tempfile(DIR => $tempdir);
return ($tfh, $tfname);
}
sub getContents {
my $filename = shift;
open(my $fh, '<', $filename) or die $!;
my $contents = join('', <$fh>);
close $fh;
return $contents;
}
sub parseData {
my ($contents, $filename) = @_;
my $parsed;
my $found = '';
if ($contents =~ /\/\*(---[\r\n]+[\S\s]*)[\r\n]+\s*---\*\//m) {
$found = $1;
};
eval {
$parsed = Load($found);
};
if ($@) {
print "\nError parsing YAML data on file $filename.\n";
print "$@\n";
};
return $parsed;
}
sub getHarness {
my ($filesref) = @_;
my $content;
for (@{$filesref}) {
my $file = $_;
open(my $harness_file, '<', $file)
or die "$!, '$file'";
$content .= join('', <$harness_file>);
close $harness_file;
};
return $content || '';
}
sub SetFailureForTest {
my ($failed, $test) = @_;
if ($failed->{$test->{path}}) {
$failed->{$test->{path}}->{$test->{mode}} = $test->{error};
}
else {
$failed->{$test->{path}} = {
$test->{mode} => $test->{error}
};
}
}
sub UpdateResults {
print "Updating results... \n";
my ($expect, $results, $failed) = @_;
foreach my $test (@{$results}) {
delete $expect->{$test->{path}};
}
foreach my $path (keys(%{$expect})) {
foreach my $mode (keys(%{$expect->{$path}})) {
my $test = {
path => $path,
mode => $mode,
error => $expect->{$path}->{$mode},
result => 'FAIL'
};
SetFailureForTest($failed, $test);
}
}
}
sub summarizeResults {
print "Summarizing results...\n";
if (not @results) {
my @rawresults = LoadFile($resultsFile) or die $!;
@results = @{$rawresults[0]};
}
# Create test262-results folder if it does not exits
if (! -e $resultsDir) {
mkpath($resultsDir);
}
$summaryTxtFile = abs_path("$resultsDir/summary.txt");
$summaryFile = abs_path("$resultsDir/summary.yaml");
my $summaryHTMLFile = abs_path("$resultsDir/summary.html");
my %byfeature;
my %bypath;
foreach my $test (@results) {
my $result = $test->{result};
if ($test->{features}) {
foreach my $feature (@{$test->{features}}) {
if (not exists $byfeature{$feature}) {
$byfeature{$feature} = [0, 0, 0, 0];
}
if ($result eq 'PASS') {
$byfeature{$feature}->[0]++;
}
if ($result eq 'FAIL') {
$byfeature{$feature}->[1]++;
}
if ($result eq 'SKIP') {
$byfeature{$feature}->[2]++;
}
if ($test->{time}) {
$byfeature{$feature}->[3] += $test->{time};
}
}
}
my @paths = split('/', $test->{path});
@paths = @paths[ 1 ... scalar @paths-2 ];
foreach my $i (0..scalar @paths-1) {
my $partialpath = join("/", @paths[0...$i]);
if (not exists $bypath{$partialpath}) {
$bypath{$partialpath} = [0, 0, 0, 0];
}
if ($result eq 'PASS') {
$bypath{$partialpath}->[0]++;
}
if ($result eq 'FAIL') {
$bypath{$partialpath}->[1]++;
}
if ($result eq 'SKIP') {
$bypath{$partialpath}->[2]++;
}
if ($test->{time}) {
$bypath{$partialpath}->[3] += $test->{time};
}
}
}
open(my $sfh, '>', $summaryTxtFile) or die $!;
open(my $htmlfh, '>', $summaryHTMLFile) or die $!;
print $htmlfh qq{<html><head>
<title>Test262 Summaries</title>
<link rel="stylesheet" href="report.css">
</head>
<body>
<h1>Test262 Summaries</h1>
<div class="visit">Visit <a href="index.html">the index</a> for a report of failures.</div>
<h2>By Features</h2>
<table class="summary-table">
<thead>
<th>Feature</th>
<th>%</th>
<th>Total</th>
<th>Run</th>
<th>Passed</th>
<th>Failed</th>
<th>Skipped</th>
<th>Exec. time</th>
<th>Avg. time</th>
</thead>
<tbody>};
print $sfh sprintf("%-6s %-6s %-6s %-6s %-6s %-6s %-7s %-6s %s\n", 'TOTAL', 'RUN', 'PASS-%', 'PASS', 'FAIL', 'SKIP', 'TIME', 'AVG', 'FEATURE');
foreach my $key (sort keys %byfeature) {
my $totalFilesRun = $byfeature{$key}->[0] + $byfeature{$key}->[1];
my $totalFiles = $totalFilesRun + $byfeature{$key}->[2];
my $iper = ($byfeature{$key}->[0] / $totalFiles) * 100;
my $per = sprintf("%.0f", $iper) . "%";
my $time = sprintf("%.1f", $byfeature{$key}->[3]) . "s";
my $avgTime;
if ($totalFilesRun) {
$avgTime = sprintf("%.2f", $byfeature{$key}->[3] / $totalFilesRun) . "s";
} else {
$avgTime = "0s";
}
print $sfh sprintf("%-6s %-6s %-6s %-6d %-6d %-6d %-7s %-6s %s\n",
$totalFiles,
$totalFilesRun,
$per,
$byfeature{$key}->[0],
$byfeature{$key}->[1],
$byfeature{$key}->[2],
$time,
$avgTime,
$key);
print $htmlfh qq{
<tr class="per-$iper">
<td>$key</td>
<td>$per</td>
<td>$totalFiles</td>
<td>$totalFilesRun</td>
<td>$byfeature{$key}->[0]</td>
<td>$byfeature{$key}->[1]</td>
<td>$byfeature{$key}->[2]</td>
<td>$time</td>
<td>$avgTime</td>
</tr>};
}
print $htmlfh qq{</tbody></table>
<h2>By Path</h2>
<table class="summary-table">
<thead>
<th>Folder</th>
<th>%</th>
<th>Total</th>
<th>Run</th>
<th>Passed</th>
<th>Failed</th>
<th>Skipped</th>
<th>Exec. time</th>
<th>Avg. time</th>
</thead>
<tbody>};
print $sfh sprintf("\n\n%-6s %-6s %-6s %-6s %-6s %-6s %-7s %-6s %s\n", 'TOTAL', 'RUN', 'PASS-%', 'PASS', 'FAIL', 'SKIP', 'TIME', 'AVG', 'FOLDER');
foreach my $key (sort keys %bypath) {
my $totalFilesRun = $bypath{$key}->[0] + $bypath{$key}->[1];
my $totalFiles = $totalFilesRun + $bypath{$key}->[2];
my $iper = ($bypath{$key}->[0] / $totalFiles) * 100;
my $per = sprintf("%.0f", $iper) . "%";
my $time = sprintf("%.1f", $bypath{$key}->[3]) . "s";
my $avgTime;
if ($totalFilesRun) {
$avgTime = sprintf("%.2f", $bypath{$key}->[3] / $totalFilesRun) . "s";
} else {
$avgTime = "0s";
}
print $sfh sprintf("%-6s %-6s %-6s %-6d %-6d %-6d %-7s %-6s %s\n",
$totalFiles,
$totalFilesRun,
$per,
$bypath{$key}->[0],
$bypath{$key}->[1],
$bypath{$key}->[2],
$time,
$avgTime,
$key);
print $htmlfh qq{
<tr class="per-$iper">
<td>$key</td>
<td>$per</td>
<td>$totalFiles</td>
<td>$totalFilesRun</td>
<td>$bypath{$key}->[0]</td>
<td>$bypath{$key}->[1]</td>
<td>$bypath{$key}->[2]</td>
<td>$time</td>
<td>$avgTime</td>
</tr>};
}
print $htmlfh qq{</tbody></table>
<div class="visit">Visit <a href="index.html">the index</a> for a report of failures.</div>
</body></html>};
close($sfh);
close($htmlfh);
my %resultsyaml = (
byFolder => \%bypath,
byFeature => \%byfeature,
);
DumpFile($summaryFile, \%resultsyaml);
}
sub findAllFailing {
my @allresults = LoadFile($resultsFile) or die $!;
@allresults = @{$allresults[0]};
my %filedictionary;
foreach my $test (@allresults) {
if ($test->{result} eq 'FAIL') {
$filedictionary{$test->{path}} = 1;
}
}
@files = map { qq($test262Dir/$_) } keys %filedictionary;
}
sub printHTMLResults {
my %failed = %{shift()};
my ($total, $failcount, $newfailcount, $skipcount) = @_;
# Create test262-results folder if it does not exits
if (! -e $resultsDir) {
mkpath($resultsDir);
}
my $indexHTML = abs_path("$resultsDir/index.html");
open(my $htmlfh, '>', $indexHTML) or die $!;
print $htmlfh qq{<html><head>
<title>Test262 Results</title>
<link rel="stylesheet" href="report.css">
</head>
<body>
<h1>Test262 Results</h1>
<div class="visit">Visit <a href="summary.html">the summary</a> for statistics.</div>};
print $htmlfh qq{<h2>Stats</h2><ul>};
{
my $failedFiles = scalar (keys %failed);
my $totalPlus = $total + $skipcount;
print $htmlfh qq{
<li>$total test files run from $totalPlus files, $skipcount skipped test files</li>
<li>$failcount failures from $failedFiles distinct files, $newfailcount new failures</li>
};
}
print $htmlfh qq{</ul><h2>Failures</h2><ul>};
foreach my $path (sort keys %failed) {
my $scenarios = $failed{$path};
print $htmlfh qq{<li class="list-item">
<label for="$path" class="expander-control">$path</label>
<input type="checkbox" id="$path" class="expander">
<ul class="expand">};
while (my ($scenario, $value) = each %{$scenarios}) {
print $htmlfh qq{<li>$scenario: $value</li>};
}
print $htmlfh qq{</ul></li>}
}
print $htmlfh qq{</ul>
<div class="visit">Visit <a href="summary.html">the summary</a> for statistics.</div>
</body></html>};
close $htmlfh;
}
__END__
=head1 DESCRIPTION
This program will run all Test262 tests. If you edit, make sure your changes are Perl 5.8.8 compatible.
=head1 SYNOPSIS
Run using native Perl:
=over 8
test262-runner -j $jsc-dir
=back
Run using carton (recommended for testing on Perl 5.8.8):
=over 8
carton exec 'test262-runner -j $jsc-dir'
=back
=head1 OPTIONS
=over 8
=item B<--help, -h>
Print a brief help message and exits.
=item B<--child-processes, -p>
Specify the number of child processes.
=item B<--t262, -t>
Specify root test262 directory.
=item B<--jsc, -j>
Specify JSC location. If not provided, script will attempt to look up JSC.
=item B<--release>
Use the Release build of JSC. Can only use if --jsc <path> is not provided. The Debug build of JSC is used by default.
=item B<--debug>
Use the Debug build of JSC. Can only use if --jsc <path> is not provided. Negates the --release option.
=item B<--verbose, -v>
Verbose output for test results. Includes error message for test.
=item B<--config, -c>
Specify a config file. If not provided, script will load local JSTests/test262/config.yaml
=item B<--ignore-config, -i>
Ignores config file if supplied or findable in directory. Will still filter based on commandline arguments.
=item B<--features, -f>
Filter test on list of features (only runs tests in feature list).
=item B<--test-only, -o>
Specify one or more specific test262 directory of test to run, relative to the root test262 directory. For example, --test-only 'test/built-ins/Number/prototype'
=item B<--save, -s>
Overwrites the test262-expectations.yaml file with the current list of test262 files and test results.
=item B<--expectations, -e>
Specify a expectations file for loading and saving. If not provided, script will load and save to JSTests/test262/expectations.yaml.
=item B<--ignore-expectations, -x>
Ignores the test262-expectations.yaml file and outputs all failures, instead of only unexpected failures.
=item B<--failing-files, -F>
Runs all test files that failed in a given results file (specifc with --results). This option will run the rests in both strict and non-strict modes, even if the test only fails in one of the two modes.
=item B<--latest-import, -l>
Runs the test files listed in the last import (./JSTests/test262/latest-changes-summary.txt).
=item B<--skipped-files, -S>
Runs all test files that are skipped according to the config.yaml file.
=item B<--stats>
Calculate conformance statistics from results/results.yaml file or a supplied results file (--results). Saves results in results/summary.txt and results/summary.yaml.
=item B<--results, -r>
Specifies a results file for the --stats or --failing-files options.
=item B<--timeout>
Specifies a timeout execution in ms for each test. Defers the value to the jsc --watchdog argument. Disabled by default.
=back
=cut