#!/usr/local/bin/perl
#
#
# (C) 1997 Standard Performance Evaluation Corporation
# Author:  Christopher Chan-Nui
#
#    Revised 06/05/98 - Paula
#    Added support for Custom Ad Rotation model: Dynamic GET w/Cookie
#    Created 3 Dynamic URLS to support POST, GET, and GET w/cookie
#    will default to use DYNAMIC_ROOT (POST) if others not specified
#    Add percentage for GET w/cookie to loadload data.  Also fixed
#    defaults for other percentages and changed lg_proc and lg_num to
#    use actual test values to avoid configuration description errors.
#
#    Revised 09/23/98 - Paula, Keti, Yea-Cheng
#	Ketii:	   Added client logging option; use -l option to invoke.
#       Yea-Cheng: Improved post log validation to check for serialization 
#                  across all clients not just within each client (ie. check 
#                  that the record id increases after each entry).  This is 
#		   in keeping with the 2.1.2 SUT run rule where the result of 
#		   any dynamic request must be visible to any client on any
#		   subsequent dynamic request.
#	Paula:     Added urlroot to list of parameters passed on Reset command,
#		   this will be used in WEB98_INC processing.
#		   Updated parameters used for dynamic script designation - 
#		   allow use of 4 separate RC parameters to specify COMMAND's,
#		   simple dynamic GET's, dynamic GET's for Custom Ad Rotation 
#	           model and POST's
#		   Add minor bug fixes.
#
#    Revised 02/01/99 - Yea-Cheng
#    Support the new SIMULTANEOUS_CONNECTIONS in the rc file. LOAD_VALUES and 
#    PROCS are no more supported. In the rc file, a client can be configured 
#    as name(port)[speed].
#
#
#    SIMULTANEOUS_CONNECTIONS replaces LOAD_VALUES in the sense that the 
#    number of connections will be distributed among all the clients as the 
#    requested workload of a client. The speed of a client will be taken 
#    into consideration when calculating the workload distribution. On a 
#    client system, there will be as many number of processes/threads 
#    created as the requested workload. Each of these processes/threads 
#    will send requests to the SUT in a contiguous manner. 
#
#    Support the new USER_LINE_SPEED, and USER_SPEED_LIMIT in the rc file. 
#    Every Client process/thread will not start another operation until enough 
#    time has elapsed so that all the bytes received from the SUT can be 
#    transmitted over the user dial up line. The speed of the dialup line
#    (USER_LINE_SPEED) will determine that elapsed time. The connection to 
#    the SUT will remain open for that period of time.
#  
#    Based on the average size of SPECweb fileset files, at the user
#    dialup line speed of 400000bits/secs, each connection can get about
#    3 OPS/sec. SIMULTANEOUS_CONNECTIONS will therefore determine the scope 
#    of the file_set directories every client will access in a single run.
#
#    Process the new result data: rate, rate2, and aggregate_bytes.
#    Report the new result in terms of the number of simultaneous connections,
#    the aggregate bit rate, the throughput, and the statistical 
#    distributions of bit rates in raw, ascii, and screen formats. 
#    (The .ps and .html formats still need work.)
#
#    Other changes: unlink 'post.log' file before fetching it from the server.
#    
#    Revised 2/24/99 - Ruth Morgenstein
#    Changed the way run data from each point is passed around.  There
#    used to be 3 pieces of data in the 'config' structure (connections,
#    throughput, and avg. response).  There's more data than that, so
#    I replaced those 3 fields with an associative array containing
#    all the relevent data.  Having this array, makes it easy to change
#    what data gets passed around.
#
#
#    Revised 2/26/99 - Ruth Morgenstein
#    Added code to support ITERATIONS.  For each point in SIMULTANEOUS_
#    CONNECTIONS, it will be repeated ITERATIONS times.  Each group if
#    ITERATIONS is a separate test.  For each group, WARMUP_TIME done
#    before the 1st point and the new parameter RAMPUP_TIME is done
#    before iterations 2 through n.
#    
#    The reported result is the median value.
#
#    Revised 3/11/99 - Ruth Morgenstein
#    Add checking for required values to make sure the test will be valid.
#    Added -C flag, to make all required values conforming
#    Added validation code to check all paths for GETs and POSTS
#    Changed SPECweb98 to SPECweb99 in ASCII output
#    Better error reporting
#    
#
#    
#    Revised 3/28/00 and 5/11/00 - Greg Darnell
#    Version 1.02
#    Added bitrate check per client to catch problem where client 
#    overstates results due to overload condition or sleep function
#    inaccuracy.
#    Added check for single-client load over 850 sim. connections.   
#
#    Revised 5/24/00 - Paula Smith
#    Added support to allow a command to be issued to clean up
#    the work directory before starting a test.  Should help 
#    reduce problems with clients "hanging" when there are too
#    many files have accumulated.  Adds to rc file: RSH, CLEANUP_WORK,
#    CLIENT_USER, and CLIENT_DIR to allow flexibility in managing
#    this operation.
#
#    Revised 7/6/00 - Greg Darnell
#    Minor fixes - added support for NT networked file names in
#    cleanup code, changed 2 error messages.



require 5.005;
use Getopt::Std;
use IO::Select;
use Time::localtime;
use LWP::UserAgent;


use strict;
use IO::Socket;
use IO::File;
use Compress::Zlib;
use MIME::Base64;
use Cwd;
use WebClient;
use vars qw ($have_ps $have_pdf);




# Load GD.pm if we can.  If GD.pm exists we will output the graph for HTML
use vars qw($min_font_size);
$min_font_size = 6;

sub Log;
sub client_log; 


##############################################################################
# Set up global variables
##############################################################################

use vars qw(@info @config_files %formatter $default_config_file $output_dir);

$default_config_file = "rc";

use vars qw(@info @config_files %formatter $default_config_file
	    $MAX_ERROR_RATE $MIX_TOLERANCE $MIN_WARMUP_TIME $MIN_CONFORM_PCT 
	    $REQ_ITERATIONS 
	    $REQ_RUN_TIME 
	    $REQ_RAMPUP_TIME 
	    $REQ_RAMPDOWN_TIME
	    $REQ_DYNAMIC_CONTENT 
	    $REQ_DYNAMIC_POST 
	    $REQ_DYNAMIC_CAD_GET
	    $REQ_DYNAMIC_CGI_GET 
	    $REQ_REQUESTS_PER_KEEP_ALIVE
	    $REQ_PERCENT_KEEP_ALIVE_REQUESTS 
	    $REQ_ABORTIVE_CLOSE
	    $REQ_USER_LINE_SPEED 
	    $REQ_USER_SPEED_LIMIT 
	    $REQ_MAX_FILE
	    $REQ_WAIT_BETWEEN_POINTS
	    $ERROR_CHECK_RANGE 
	    $FILES_PER_CLASS
	    $MB_PER_DIRECTORY);


$MAX_ERROR_RATE    = .0104999999;  # dependant upon %.3f formatting
$MIX_TOLERANCE     = .1049999999;  # dependant upon %.3f formatting
$MIN_WARMUP_TIME   = 1200;
$MIN_CONFORM_PCT   = 95;

$REQ_ITERATIONS    = 3;
$REQ_RUN_TIME      = 1200;
$REQ_RAMPUP_TIME   = 300;
$REQ_RAMPDOWN_TIME   = 300;
$REQ_DYNAMIC_CONTENT = 0.3;
$REQ_DYNAMIC_POST = 0.16;
$REQ_DYNAMIC_CAD_GET = 0.42;
$REQ_DYNAMIC_CGI_GET = .005;
$REQ_REQUESTS_PER_KEEP_ALIVE = 10;
$REQ_PERCENT_KEEP_ALIVE_REQUESTS = 0.7;
$REQ_ABORTIVE_CLOSE = 0;
$REQ_USER_LINE_SPEED = 400000;
$REQ_USER_SPEED_LIMIT = 320000;
$REQ_MAX_FILE = 8;
$REQ_WAIT_BETWEEN_POINTS = 0;

$ERROR_CHECK_RANGE = .25;
$FILES_PER_CLASS = 9;
$MB_PER_DIRECTORY = 4.88;

use vars qw($verbose $output_types @output_types $ignore_errors $compliant
	    $rawformat $logclient $file_num $test_time $version $rcs_version
	    $max_load @configs $short_version $details);
use vars qw( @hardware_fields @availability_dates_fields @software_fields
	     @http_software_fields @test_sponser_fields @network_fields
	     @clients_fields @bm_config_fields @notes_fields );


&init_output;

@info = (
    @hardware_fields,
    @availability_dates_fields,
    @software_fields,
    @http_software_fields,
    @test_sponser_fields,
    @network_fields,
    @clients_fields,
    @bm_config_fields,
    @notes_fields,
);

$verbose = 10;
$output_types="";
$logclient = 1;	
$details = 0;

$test_time = time;
$version = "99-v1.02";
($short_version)= $version =~ m/([\d.]+-)/;
$rcs_version = q#$Id: manager,v 1.30 1999/06/29 20:30:12 ruth Exp $#;


#
# A note on the @configs data structures.
#
# @configs is an array of inputs and results from each config file.
#   A config file can either be an "rc" file that gets used to run a test,
#   or a raw file that gets read in for post-processing.
# We typically operate on 1 $config within @configs at a time
#
# Each $config a reference to a hash containing inputs and results from a
# series of points
# 
# Each $config hash has a key for each input from the rc file.  For example,
#  $config->{'WARMUP_TIME'} is used to hold the WARMUP_TIME that was input
#  from the rc file.  In addition, there are 2 substructures, used to hold
#  output data from the test:
#         'results'
#         'errors'
# 
# The 'results' key contains an array, @results, with each element of the array
# representing the results from an individual run within the series of
# tests.
# Each array element $results in the @results array contains an array, with each
# entry as the following
#    $results[0]    %run_stats - hash of statistics from the point
#    $results[1]    $run_number - (this ought match the index of @results)
#    $results[2]    $unused field
#    $results[3]    @class_results - per class rollup info
#    $results[4]    @errors - errors for this run
#
#
# The %run_stats contains the following keys:
#  connections, invalid, conforming, conform_pct, throughput,
#  ops_per_thread, response, bitrate, cum_conformance
#
# So, to roll things up, here's an example of getting to a run stat, for the
# 5th point in a test
# $config->{'results'}[4][0]{'conforming'}
#
# My advice:  use lots of temporaries    
#
#  -- Ruth                  

if (! exists $ENV{'SPEC'}) {
    die "\nERROR: environement variable \$SPEC not defined.\n" .
	"Do \". shrc\" in sh or \"shrc.bat\" from NT to setup SPEC environment\n\n";
}


if (! -e "$ENV{'SPEC'}/bin/fonts") {
    die "\nERROR: SPEC environment not properly installed.\n" .
	"Can't find \$SPEC/bin/fonts directory\n\n";
}
    

# Make sure the results directory exists
$output_dir = "results/";
mkdir $output_dir, 0755 if ! -d $output_dir;

# Initialize HTTP useragent
my $ua = new LWP::UserAgent;
$ua->agent("SPECweb/$short_version " . $ua->agent);

# Open up the log file
my $file_num = &find_next_number($output_dir);
my $log_file = "res.$file_num";
open (LOG, ">$output_dir$log_file") || 
    die "Can't open log file '$log_file': $!\n";
select((select(LOG),$|=1)[0]);  # Unbuffer LOG file


Log (10, "\nOpened logfile $output_dir$log_file\n");


eval "use PSPDF";
if ($@) {
    $have_ps  = $have_pdf = 0;
    &Log (10, "WARNING: Unable to find PSPDF.pm -- no PDF or PS output will be made\n");
} else {
    $have_ps = 1;
    $have_pdf = PSPDF::pdf_ok();
    if ($have_pdf != 1) {
	&Log(10, "WARNING: Can't find pdflib -- no PDF output will be made\n");
    }
}



#    die "
#$ENV{'SPEC'}/bin/fonts") if exists $ENV{'SPEC'};

sub usage {
    print <<EOT;
Usage: $0 [options] [option=val...] [file...] 
    -v   #       verbosity level
    -o   types   Produce output in the specified types
    -I           Do not abort on certain classes of errors
    -R           files are reformatted and not used as config files
    -l           Turn off client logging in log-client.nnn file
    -C           set all parameters need for a compliant run
    -D           turn on details in the ASCII output
EOT
    exit 1;
}

sub parse_arguments {
    my %opts;
    my $rc = getopts("Iv:c:d:ho:RlCD", \%opts);
    &usage if $opts{'h'} || !$rc;

    push (@config_files, $opts{'c'})  if defined $opts{'c'};
    $verbose           = $opts{'v'}   if defined $opts{'v'};
    $output_types      = $opts{'o'}   if defined $opts{'o'};
    $ignore_errors     = 1            if defined $opts{'I'};
    $rawformat         = 1            if defined $opts{'R'};
    $logclient         = 0            if defined $opts{'l'};
    $compliant         = 1            if defined $opts{'C'};
    $details           = 1	      if defined $opts{'D'};


    @config_files = grep(!m/=/, @ARGV);
    @ARGV = grep(m/=/, @ARGV);
}

parse_arguments();

#open log file for client summaries if user specified this option
if ($logclient) {
    my $cllog_file = "log-client.$file_num";
    open (CLLOG, ">$output_dir$cllog_file") || die "Can't open client log file '$cllog_file': $!\n";
    select((select(CLLOG),$|=1)[0]);  # Unbuffer client log file
    Log (10, "\nOpened client logfile $output_dir$cllog_file\n");
}


# These are the valid formatters, to add a new formatter just write the
# function and add it here.
$formatter{'ps'}      = \&write_ps       if $have_ps;
$formatter{'pdf'}     = \&write_pdf      if $have_pdf;
$formatter{'asc'}     = \&write_asc;
$formatter{'html'}    = \&write_html;
$formatter{'screen'}  = \&write_screen;
$formatter{'raw'}     = \&write_raw;

# If no config file was specified, use the efault
push (@config_files, $default_config_file) if (!@config_files);

# Rawformat is the process of taking a .raw file and outputting any of
# the defined output formats.  This is used so that people don't have
# to create all of the different outputs they desire at once.  They can
# produce any output from a .raw file.  Also useful for fixing up
# incorrect notes and things.

if ($rawformat) {
    # for all of the files (not actually config files) specified on the
    # command line
    for my $config_file (@config_files) {
	local(*FILE);
	open (FILE, "<$config_file") || die "Cannot open '$config_file'";
	my @foo=();
	# Grab the raw data out of the file
	while (<FILE>) {
	    push (@foo, $_) if /^(%% )?BEGIN SPECWEB99/ .. /^(%% )?END SPECWEB99/;
	}
	close(FILE);

	if ($#foo < 0) {
	    print STDERR "******\n$config_file is not a valid raw file\n";
	    next;
	}

	# strip any comment and character escaping that might have been
	# done.  We comment for Postscript and PDF and escape for HTML.
	@foo = grep(!m/(%% )?(BEGIN|END) SPECWEB99/, @foo);
	grep(s/^\s*%%\s*//, @foo);
	grep(s/\&lt;/</g, @foo);
	grep(s/\&gt;/>/g, @foo);
	grep(s/\&amp;/\&/g, @foo);

	# Read in the config file, we use a special option to mark this
	# as a read of a raw file.
	my $config = config_read(\@foo, 1);

	my ($num) = $config_file =~ m/^.*\.(\d+)\.raw$/;
	&do_outputs($config, $num, $output_dir);
    
    }
    # If rawformatting that's all we do
    exit (0);
}

# Read in and parse all the config files and find the maximum load value
$max_load = 0;
for (@config_files) {
    my ($req, $res, $url);

    #as we read each config file, we save it's info as an entry in @configs
    my $config = config_read($_);
    push (@configs, $config);

    #
    # Check for optional clean up of work directory
    #
    if ($config->{'CLEANUP_WORK'} ne "") {
        for my $data (@{$config->{'clients_parsed'}}) {
            my ($hostname, $port, $speed, $clients_server) = @$data;
            &do_system($config->{'RSH'},
                       $config->{'CLEANUP_WORK'},
                       $hostname,
                       $config->{'CLIENT_USER'},
                       $config->{'CLIENT_DIR'},
                      );
        }
    }


    # Make sure that for each config file, we can actually do all the
    # operations (Static GET, Dyn GET, Dyn CGI GET, Dyn CAD GET, Dyn POST)
    Log(5, "\nValidating all paths for $config->{'FILENAME'}\n");
    &verify_all_HTTP_operations($config);
    Log(5, "\n");
}


# Run all of the separate config files
for my $config (@configs) {

#    &validate_config($config);
    #For multiple iterations, monkey with the SIMULTANEOUS CONNECTIONS
    #parameter to make separate tests for each point
    if ($config->{'ITERATIONS'} > 1) {
	my @saved_connections_list = @{$config->{'SIMULTANEOUS_CONNECTIONS'}};
	foreach my $point (@saved_connections_list) {
	    $config->{'results'} = [];
	    $config->{'errors'} = [];
	    &validate_config($config);

	    @{$config->{'SIMULTANEOUS_CONNECTIONS'}} = ();
	    for (my $i = 0; $i < $config->{'ITERATIONS'}; $i++) {
		push (@{$config->{'SIMULTANEOUS_CONNECTIONS'}} ,$point);
	    }
	    run_all ($config);
	    &do_outputs($config, $file_num, $output_dir);
	    $file_num++;
	}
    }
        
    #If single iteration, then just run all the points -- this is really
    #just a testing mode, not something that can be done in a submittable test
    else {
	&validate_config($config);
	run_all($config);
	&do_outputs($config, $file_num, $output_dir);
	$file_num++;
    }
}

##############################################################################
# Config file routines
##############################################################################

# Initialize configuration file to have default values
sub config_init {
    Log(30, 'init_config\n');
    my $config = {};

    &set_required_parameters($config);

    $config->{'SIMULTANEOUS_CONNECTIONS'} =  10;
    $config->{'SUCCESSIVE_SPEED_LIMITS'} = 0;
    $config->{'LOAD_FILL'}           =   0;
    $config->{'CLIENTS'}             = 'client1';
    $config->{'URL_ROOT'}            = 'http://server/spec/file_set';
    $config->{'DYNAMIC_ROOT'}        = 'http://server/spec/cgi-bin/dynamic?';
    $config->{'DYN_CMD_SCRIPT'}      = '';
    $config->{'DYN_GET_SCRIPT'}      = '';
    $config->{'DYN_CAD_SCRIPT'}      = '';
    $config->{'DYN_CGI_SCRIPT'}      = '';
    $config->{'DYN_POST_SCRIPT'}     = '';
    $config->{'HTTP_PROTOCOL'}       = '';

    $config->{'RSH'}                 = 'rsh $host -l $user "$command"';
    $config->{'CLEANUP_WORK'}        = '';
    $config->{'CLIENT_USER'}         = '';
    $config->{'CLIENT_DIR'}          = '';

    $config->{'SERVER'}              = "server";
    $config->{'OUTPUT_NAME'}         = "output";
#    $config->{'LOG_SUFFIX'}          = ".err";
    $config->{'WAIT_TO_BEGIN'}       = 5;
    $config->{'WAIT_BETWEEN_POINTS'} = 0;
    $config->{'OUTPUT_TYPE'}         = "all";
    $config->{'Volume'}              = '--';
    $config->{'Issue'}               = '--';
    $config->{'Page'}                = '--';
    $config->{'munge_date'}          = 1;
#    $config->{'detail'}		     = 1;  #Turn on more data in ASCII

    for (@info) {
	$config->{$_->[1]} = '';
    }

    return $config;
}



sub set_required_parameters {
    my ($config) = @_;

    $config->{'USER_LINE_SPEED'}     =  $REQ_USER_LINE_SPEED;
    $config->{'USER_SPEED_LIMIT'}    =  $REQ_USER_SPEED_LIMIT;
    $config->{'RUN_TIME'}            = $REQ_RUN_TIME;
    $config->{'RAMPDOWN_TIME'}       =  $REQ_RAMPDOWN_TIME;
    $config->{'RAMPUP_TIME'}         =  $REQ_RAMPUP_TIME;
    $config->{'DYNAMIC_CONTENT'}     = $REQ_DYNAMIC_CONTENT;
    $config->{'DYNAMIC_POST'}        = $REQ_DYNAMIC_POST;
    $config->{'DYNAMIC_CAD_GET'}     = $REQ_DYNAMIC_CAD_GET;
    $config->{'DYNAMIC_CGI_GET'}     = $REQ_DYNAMIC_CGI_GET;
    $config->{'REQUESTS_PER_KEEP_ALIVE'} = $REQ_REQUESTS_PER_KEEP_ALIVE;  
    $config->{'PERCENT_KEEP_ALIVE_REQUESTS'} =$REQ_PERCENT_KEEP_ALIVE_REQUESTS; 
    $config->{'MAX_FILE'}            = $REQ_MAX_FILE;
    $config->{'ABORTIVE_CLOSE'}      = $REQ_ABORTIVE_CLOSE;
    $config->{'ITERATIONS'}      = $REQ_ITERATIONS;
    $config->{'WAIT_BETWEEN_POINTS'}      = $REQ_WAIT_BETWEEN_POINTS;
    

    $config->{'WARMUP_TIME'}         = $MIN_WARMUP_TIME if $config->{'WARMUP_TIME'} < $MIN_WARMUP_TIME;

}
# Read in a configuration file
#     $file may be a filename or an reference to the array containing
#     the file.
sub config_read {
    my($file, $rawread) = @_;
    my ($key, $value, $num, @lines);

    Log(30, "Reading Config file\n");

    # Initializes the default values for the config file.
    my $hash = config_init();

    # If it's an array then just use it, otherwise read in the file.
    if (ref($file) eq "ARRAY") {
	@lines = @$file;
    } else {
	$hash->{'FILENAME'} = $file;
	my $fh = new IO::File "< $file";
	die "Can't open file '$file': $!\n" if (!defined $fh);
	@lines = $fh->getlines;
	$fh->close;
    }

    # Put any option=val arguments at the bottom so they override the
    # values in the config file.
    push (@lines, @ARGV);
    my @rawlines = grep(!m/^\s*[#;]>/, @lines);


    # If we are reading a .raw file, we need to define some values so
    # that they won't be interpreted as errors
    if ($rawread) {
	for (qw(median_index median_results metric
		test_time WORK_DIR FILENAME MAX_LOAD version
		total_speed rcs_version rawconfig)) {
	    $hash->{$_} = '--';
	}
	$hash->{'results'} = [];
	$hash->{'errors'}  = [];
    } 

    # Parse the actual file
    for (@lines) {
	if (m/^\s*$/ || m/^\s*[#;]/) {
	    # Comments and blank lines are ignored
	} elsif (m/^\s*(\S+)\s*=\s*"(.*)"\s*$/ || 
		 m/^\s*(\S+)\s*=\s*(.*\S)\s*$/ ||
		 m/^\s*(\S+)\s*=\s*(.*)/){
	    # key=value, key="value" are accepted
	    ($key, $value) = ($1, $2);

	    # Check to see if the key is in the form of label### these
	    # are treated specially
	    $num = -1;
	    if ($key =~ m/(.*\D)(\d+)/) {
		($key, $num) = ($1, $2);
	    }

	    # If the key is results then we have to parse the line to
	    # fill out our data
	    if ($key eq "results") {
		# Make sure we have somewhere to put the data
		if (!defined $hash->{'results'}[$num]) {
		    $hash->{'results'}[$num] = [];
		    $hash->{'results'}[$num][0] = {};    # %run_stats
		    $hash->{'results'}[$num][1] = $num;  # $run_num
		    $hash->{'results'}[$num][2] = "unused_field";
		    $hash->{'results'}[$num][3] = [];    # @class_results
		    $hash->{'results'}[$num][4] = [];    # @errors
		}



		# For quick reference
		my $res = $hash->{'results'}[$num];
		# Find out our subkey so we can put the data in the
		# right place
		my ($subkey, $val);
		if (($subkey, $val) = $value =~ m/(\S+)\s+(.*)/) {
		    #find error strings
		    if ($subkey eq "error") {
			push (@{$res->[4]},  $val) ;
		    }
		    #find per class statistics
		    elsif ($subkey eq "class") {
			my ($class_num, $class_name, $class_data) = 
			    $val =~ m/^(\d+)\s+"(.*)"\s+(.*)/;
			$res->[3][$class_num] = [ $class_name, split(" ", $class_data) ];
		    }
		    else {
			# all the rest are other result statistics
			$res->[0]{$subkey} = $val;
		    }

		} else {
		    Log(10, "Can't parse line: $_\n");
		    next;
		}
	    } elsif (!defined $hash->{$key}) {
		# We make sure that there aren't any typos by only
		# accepting keys which we have defined for the user
		$num = '' if ($num == -1);
		Log(10, "Key '$key$num' is unknown.  Ignoring.\n");
	    } else {
		# If it was in the form label### assign the value to
		# the correct index in the entry, otherwise just assign
		# the value.
		#if ($num ne "") {
		if ($num != -1) {
		    if (ref($hash->{$key}) ne "ARRAY") {
			$hash->{$key} = [$hash->{$key}];
		    }
		    $hash->{$key}->[$num] = $value;
		} else {
		    $hash->{$key} = $value;
		}
	    }
	} else {
	    Log(10, "Can't parse $.: $_\n");
	}
    }

    $hash->{'metric'} = 'SPECweb99';

    if (!$rawread) {
	chomp(@rawlines); chomp(@rawlines);
	my $rawtxt = join("\n", @rawlines);
	my $cmptxt = Compress::Zlib::memGzip($rawtxt);
	my $md5txt = encode_base64($cmptxt);
	$hash->{'rawconfig'} = [ split("\n", $md5txt) ];
    }


    # Fix up a bunch of values
	
    &finalize_config($hash,[qw(SIMULTANEOUS_CONNECTIONS CLIENTS)]) if !$rawread;

    # If $compliant asked for, then change the parameters to valid values
    if ($compliant && !$rawread) {
	Log (10, "\nSetting all required parameters to legal values\n\n");
	&set_required_parameters($hash);
    }

    # Any descriptive information which doesn't have a label gets
    # assigned two dashes
    for (@info) {
	my $key = $_->[1];
	$hash->{$key} = "--" if ($hash->{$key} =~ /^\s*$/);
    }

    # The 'errors' field in @notes_field in @info shouldn't
    # get preloaded with '--'.  We want it to be an array
    delete $hash->{'errors'} if !$rawread;

    return $hash;
}

# Fix up the data from the config file
sub finalize_config {
    Log(30, "finalize_config\n");
    my ($hash, $multi) = @_;
    my ($key, %load, $step, $fudge_step, $fudge, $num, $bad, $i);

    # Turn multi-valued entries into an arrays so we can fetch data out easily
    for $key (@$multi) {
	if (ref($hash->{$key}) eq "") {
	    $hash->{$key} = [grep(!/^\s*$/, split(/ |,/, $hash->{$key}))];
	}
    }
    $hash->{'OUTPUT_TYPES'} = 
	join (",", grep(!/^\s*$/, split(/ |,/, $hash->{'OUTPUT_TYPES'})));

    # Make sure we have version information
    $hash->{'version'}    =$version     if !defined $hash->{'version'};
    $hash->{'rcs_version'}=$rcs_version if !defined $hash->{'rcs_version'};

    # Check to see if MSL and TIME_WAIT were RFC compliant
    ($hash->{'msl'})       = $hash->{'msl'}       =~ /(\d+)/;
    ($hash->{'time_wait'}) = $hash->{'time_wait'} =~ /(\d+)/;
    $hash->{'msl'}       .= ' (Non RFC1122)' if $hash->{'msl'} < 120;
    $hash->{'time_wait'} .= ' (Non RFC1122)' if $hash->{'time_wait'} < 240 || 
					$hash->{'time_wait'}/$hash->{'msl'} < 2;


    # Record where this took place
    $hash->{'WORK_DIR'}  =cwd if ($hash->{'WORK_DIR'} eq "");

    # Set individual DYN parameters if the 1-size-fits-all DYNAMIC_ROOT was used
    if ($hash->{'DYN_GET_SCRIPT'} eq '') {
	$hash->{'DYN_GET_SCRIPT'} = $hash->{'DYNAMIC_ROOT'} 
    }
    if ($hash->{'DYN_CAD_SCRIPT'} eq '') {
	$hash->{'DYN_CAD_SCRIPT'} = $hash->{'DYNAMIC_ROOT'} 
    }
    if ($hash->{'DYN_POST_SCRIPT'} eq '') {
	$hash->{'DYN_POST_SCRIPT'} = $hash->{'DYNAMIC_ROOT'} 
    }
    if ($hash->{'DYN_CMD_SCRIPT'} eq '') {
	$hash->{'DYN_CMD_SCRIPT'} = $hash->{'DYNAMIC_ROOT'} 
    }
    # Get rid of the '?' and anything following it
    ($hash->{'DYN_CMD_SCRIPT'} = $hash->{'DYN_CMD_SCRIPT'}) =~ s/\?.*$//;
    print "$hash->{'DYN_CMD_SCRIPT'} \n";

    # This code is from before we had reasonable RAMP_DOWN values
    # Set some really long value for WAIT_BETWEEN_POINTS.  Hopefully to
    # allow all of the sockets to shut down gracefully.
#    if ($hash->{'WAIT_BETWEEN_POINTS'} eq "" && $hash->{'time_wait'} > 0) {
#	$hash->{'WAIT_BETWEEN_POINTS'} = $hash->{'time_wait'};
#    }


    #expand any ranges in simultaneous_connections string and store list
    @{$hash->{'SIMULTANEOUS_CONNECTIONS'}} =
	&expand_ranges(@{$hash->{'SIMULTANEOUS_CONNECTIONS'}});

    $hash->{'MAX_LOAD'} = &max(@{$hash->{'SIMULTANEOUS_CONNECTIONS'}});

    # Make sure that we have 10 evenly spaced points
    %load = map {$_ => 1} @{$hash->{'SIMULTANEOUS_CONNECTIONS'}};
    my $max = $hash->{'MAX_LOAD'};


        if ($max < 10 && $hash->{'LOAD_FILL'}) {
	Log(5, "Can't make 10 evenly spaced points for a load of less than 10\n");
    }

    # Store the load in order 
    @{$hash->{'SIMULTANEOUS_CONNECTIONS'}} = sort { $a <=> $b } keys %load;
    $hash->{'test_time'} = $test_time;

    # Parse the client names.  See if they've overriden the port to
    # connect to, or if they've specified a relative speed rating
    $hash->{'clients_parsed'} = [];
    $hash->{'total_speed'} = 0;

    Log(10, "\nCLIENT LIST: \n");
    $i = 1;
    for (@{$hash->{'CLIENTS'}}) {
	my ($name, $port, $speed, $host);
	($port)  = m/\((\d*)\)/;           # ()
	($speed) = m/\[([\d.]*)\]/;        # []
	($host)  = m/\{([^<{([\])}>]*)\}/; # {}
	($name)  = m/^([^<{([\])}>]+)/;
	$speed = 1 if $speed eq "" || $speed <= 0;
        Log(10, "  Client $i: name=$name, port=$port, speed=$speed, host=$host\n");
	$hash->{'total_speed'} += $speed;
	push (@{$hash->{'clients_parsed'}}, [$name, $port, $speed, $host]);
	$i++;
    }
}

##############################################################################
# Do the actual runs
##############################################################################

sub run_all {
    Log(30, "run_all\n");
    my ($config) = @_;
    #my (@values)   = &expand_ranges(@{$config->{'SIMULTANEOUS_CONNECTIONS'}});
    my (@values)   = @{$config->{'SIMULTANEOUS_CONNECTIONS'}};
    my $num_runs  = 1;
    my $max_run   = @values;

#    my @results;

    # Do a run for each requested load value
    for (my $i = 0; $i < @values; $i++)  {
	my $load = $values[$i];
#    while (@values) {
#	my $load = shift @values;
        my $date = &date;

	#for multiple iterations, the 1st point does a long warmup,
	#and the rest of the points do a short rampup
	

	my $test_warmup = $config->{'WARMUP_TIME'};
	my ($tmp_string) = "Point $num_runs";

	if ($config->{'ITERATIONS'} > 1 ) {
	    $tmp_string = "Iteration $num_runs of $config->{'ITERATIONS'}";
	    if ( $i > 0) {
		$test_warmup = $config->{'RAMPUP_TIME'};
	    }
	}

	$tmp_string = sprintf("\n%s with load of %d connections\n%s\n",
			      $tmp_string, $load, $date);
	
	Log (10, $tmp_string);
	&client_log($tmp_string) if $logclient;

	my $num_clients = @{$config->{'CLIENTS'}};
	Log 101, <<EOT;
=============================================================================
Number of clients        = $num_clients
Simultaneous Connections = $load
Warm-up time (seconds)   = $test_warmup
Run time (seconds)       = $config->{'RUN_TIME'}
EOT

	my $rc = run_one($config, $load, $num_runs++, $test_warmup);
	if (!defined $rc) {
	    &Log(10, "Error running with load of $load\n");
	    return undef if !$ignore_errors;
	}
	if (@values) {
	    &Log(20, "Waiting for $config->{'WAIT_BETWEEN_POINTS'} seconds between points\n");
	    sleep ($config->{'WAIT_BETWEEN_POINTS'});
	}
    }

    &validate_results($config);


    #Log(10, "Finding median result\n");
    ($config->{'median_index'},$config->{'median_results'}) = 
	&find_median_result($config);

#    my(%med_run_stats)= %{$config->{'results'}[$config->{'median_index'}][0]};
    
#    $config->{'bm_requested_conn'} = $med_run_stats{'requested_conn'};
#    $config->{'bm_fileset_size'} = $MB_PER_DIRECTORY * 
#	&dirs_from_load($config, $med_run_stats{'requested_conn'});

}


# Run one data point
sub run_one {
    Log(30, "run_one\n");
    my ($config, $load, $num, $test_warmup) = @_;
    my ($i, $j, $exe, $hostname, $client, @clients);
    my (%tmp, $tmp, @errors, $class);
    my ($ts, $ts1, $ts2);

    my $num_clients = @{$config->{'CLIENTS'}};
    my $total_speed = $config->{'total_speed'};

    my $client_num = 0;
    my $expired_list = '' ;
    my @client_loads;


    #Calculate the total load with regard to the weightings
    my $total_load;
    my $wtload = 0;
    
    for my $data (@{$config->{'clients_parsed'}}) {
	my ($hostname, $port, $speed, $clients_server) = @$data;
	my $client_load = int ($load * ($speed/$total_speed));
	if ($client_load > $wtload) {
	    $wtload = $client_load;
	}
	
	$total_load += $client_load;
	push (@client_loads, $client_load);
	next if $client_load <= 0;
    }
    my $maxthread = $num_clients * $wtload;
    
    # If the total load is not evenly divisible by the clients
    # and their weightings, then distribute the extra processes
    my $delta_load = $load  - $total_load;
    for (; $delta_load > 0; $delta_load--) {
	@client_loads[$delta_load]++;
    }
    
    # Initialize the POST and Get w/Cookie data on the server if necessary
    if ($config->{'DYNAMIC_POST'} > 0 || $config->{'DYNAMIC_CAD_GET'} > 0 ) {
	my ($req, $res, $dynamic_url);

	$ts = time;
	$ts1 = $ts % 360;
	$ts2 = $ts1 + 100;
	if ($ts2 > 359) {
	    $ts = $ts2 - 360;
	    $ts2 = $ts1;
	    $ts1 = $ts;
	}
	$expired_list = $ts1 . ',' . $ts2;

	# For the command/Reset, we don't want any other argument pieces
	# so delete everything after the first '?' and append the command.
	$dynamic_url = $config->{'DYN_CMD_SCRIPT'};
        $dynamic_url  =~ s/\@\@SERVER\@\@/$config->{'SERVER'}/e;


	my $pttime = $config->{'RUN_TIME'} + $test_warmup + $config->{'RAMPDOWN_TIME'};
	my $tmp_url_root = $config->{'URL_ROOT'};
	$tmp_url_root =~ s/\@\@SERVER\@\@/$config->{'SERVER'}/e;
	$req = new HTTP::Request 'GET' => $dynamic_url . 
	   '?command/Reset' . '&maxload=' . $max_load . '&pttime=' . $pttime .  '&maxthread=' . $maxthread . '&exp=' . $expired_list . '&urlroot=' . $tmp_url_root;
	$req->header('Accept' => '*/*');
	$res = $ua->request($req);
	if (!$res->is_success) {
	    die "Can't reset POST log on server, make sure cgi script\n" .
		"DYNAMIC_ROOT (or DYN_CMD_SCRIPT) is configured correctly!\n" .
		"Error: " . $res->code . " " . $res->message, "\n";
	}
    } 

    # Open and initialize each client
    my $data;
    my $clientnum = 0;
    for my $data (@{$config->{'clients_parsed'}}) {
	my ($hostname, $port, $speed, $clients_server) = @$data;
	if ($clients_server eq '') {
	    $clients_server = $config->{'SERVER'};
	}
	$port   = 'specweb(2222)'    if $port  eq "";
        my $client_load = shift (@client_loads);

	next if $client_load <= 0;
	# added 5-11-2000 to detect excess client loading
	if ($client_load > 850) {
	   push (@errors, 
		sprintf "Invalid test: Client $client_num($hostname) load of $client_load\n", $client_load);
           push (@errors,
	        sprintf "              exceeds maximum 850 connections allowable.\n");
	    #die "Client $client_num($hostname) exceeds maximum 850 connections.\n";
	}

	Log(30, "Opening client '$hostname'\n");
	my $client = WebClient->new(PeerAddr => $hostname,
				    PeerPort => $port,
				    Proto    => 'tcp');
	die "Can't open socket to '$hostname'\n" if (!defined $client);

	# If the hostname has already been seen, then add a modifier to
	# the end in the hopes that we can make unique names.
	my $tmp = $hostname;
	if ($tmp{$tmp}++) {
	    $tmp .= "-".$tmp{$tmp}++;
	}

	# Wait until this client is ready and that the version of the
	# daemon on each client matches this manager script
	wait_until_ready([ $client ]);
	if ($client->identity() ne "SPEC-WEB-$version") {
	    die "Client $client_num($hostname) is not running version $version of the SPEC daemon!\n";
	}

	# Configure this client
	my $max_dir = dirs_from_load($config, $load);
	my $tmp_url_root        = $config->{'URL_ROOT'};
	my $tmp_dyn_post_script = $config->{'DYN_POST_SCRIPT'};
	my $tmp_dyn_get_script  = $config->{'DYN_GET_SCRIPT'};
	my $tmp_dyn_cad_script  = $config->{'DYN_CAD_SCRIPT'};
	my $tmp_dyn_cgi_script  = $config->{'DYN_CGI_SCRIPT'};
	$tmp_url_root        =~ s/\@\@SERVER\@\@/$clients_server/e;
	$tmp_dyn_post_script =~ s/\@\@SERVER\@\@/$clients_server/e;
	$tmp_dyn_get_script  =~ s/\@\@SERVER\@\@/$clients_server/e;
	$tmp_dyn_cad_script  =~ s/\@\@SERVER\@\@/$clients_server/e;
	$tmp_dyn_cgi_script  =~ s/\@\@SERVER\@\@/$clients_server/e;
	$client->workload99   ($tmp_url_root, 
			       $tmp_dyn_post_script, 
			       $tmp_dyn_get_script, 
			       $tmp_dyn_cad_script, 
			       $tmp_dyn_cgi_script, 
			       $max_dir, 
			       $config->{'MAX_FILE'}, 
			       $config->{'DYNAMIC_CONTENT'}, 
			       $config->{'DYNAMIC_POST'}, 
			       $config->{'DYNAMIC_CAD_GET'}, 
			       $config->{'DYNAMIC_CGI_GET'}, 
			       $config->{'PERCENT_KEEP_ALIVE_REQUESTS'}, 
			       $config->{'REQUESTS_PER_KEEP_ALIVE'}, 
			       $config->{'USER_LINE_SPEED'}, 
			       $config->{'USER_SPEED_LIMIT'}, 
			       $expired_list, 
			       "class0", "0.35", 
			       "class1", "0.50", 
			       "class2", "0.14", 
			       "class3", "0.01");

	$client->set_server   ($config->{'SERVER'});
	$client->set_client   ($tmp);
	$client->set_clientnum($clientnum++);
	$client->set_runtime  ($config->{'RUN_TIME'});
	$client->set_rampup   ($test_warmup);;
	$client->set_rampdown ($config->{'RAMPDOWN_TIME'}) if $config->{'RAMPDOWN_TIME'};
	$client->set_abortive_close ($config->{'ABORTIVE_CLOSE'}) if ($config->{'ABORTIVE_CLOSE'});
	$client->set_loadgen  ($client_load);
        # set the load in terms of OPS/sec:
	$client->set_load (($config->{'USER_LINE_SPEED'}/122000)*$client_load);
	my $protocol = $config->{'HTTP_PROTOCOL'};
	if ($protocol =~ s#.*/##) {
	    $protocol *= 10;
	    $client->set_http_protocol($protocol);
	} else {
	    die "Can't use HTTP_PROTOCOL '$config->{'HTTP_PROTOCOL'}'\n";
	}
	$client->srandom      (2331 + 256*$client_num++);
#       $client->workload     ('workload');
	$client->init         ();
	$client->set_auto_advance(1);
	push (@clients, $client);
    }




    wait_until_ready(\@clients);
    sleep($config->{'WAIT_TO_BEGIN'});

    # At this point in time all of the clients are ready to run.  So we
    # just send the comand to kick them off.
    for (@clients) {
	$_->dorampup();
    }
    wait_until_ready(\@clients);

    # Everybody is running now, queue up a few commands to run fetch
    # the results and then disconnect from the client.
    for (@clients) {
	$_->wait_advance('Result');
	$_->fetch_results();
	$_->quit();
    }
    wait_until_ready(\@clients);

    # OKAY -- we're all done running this point/iteration, now


    # Check to see that the post log looks okay
    my ($server_post_count_ref,$post_errors_ref);
    if ($config->{'DYNAMIC_POST'} > 0) {
	($server_post_count_ref,$post_errors_ref) = 
	    &validate_post_log($config, $num);
	push (@errors, @$post_errors_ref);
    }

    # For each of the clients, sum all of the results for each class.
    my $cl_results   = [];
    my $lg_res    = [];
    my $successive_speed_limits = [];
    my $speed_limit_string;
    my $lg_cum_res = [];
    my $total_cum = [];
    my $total_missing_ops;

    my %run_stats;

    #Initialize min_bitrate to max possible speed
    $run_stats{'min_bitrate'} = $config->{'USER_LINE_SPEED'} + 1000;

    # Get results from each client
    for (@clients) {
	&client_log( "\n***** Client:  $_->{'client'}\n") if $logclient;

        $lg_res = $_->{'lg_results'};

	my(%client_run_stats);

	# Load generator status info
	$client_run_stats{'requested_conn'} = $_->loadgen();
	$client_run_stats{'conforming'} = $lg_res->[1];
	$client_run_stats{'invalid_conn'}=$client_run_stats{'requested_conn'} - 
	    $lg_res->[0];

	$client_run_stats{'min_bitrate'} = $lg_res->[2] * 8;
	$client_run_stats{'mean_bitrate'} = $lg_res->[3] * 8;
	$client_run_stats{'max_bitrate'} = $lg_res->[4] * 8;

	$run_stats{'requested_conn'} += $client_run_stats{'requested_conn'};
	$run_stats{'conforming'} += $client_run_stats{'conforming'};
	$run_stats{'invalid_conn'} += $client_run_stats{'invalid_conn'};

	if ($client_run_stats{'min_bitrate'}  < $run_stats{'min_bitrate'}) {
	    $run_stats{'min_bitrate'} = $client_run_stats{'min_bitrate'};
	}
	if ($client_run_stats{'max_bitrate'}  > $run_stats{'max_bitrate'}) {
	    $run_stats{'max_bitrate'} = $client_run_stats{'max_bitrate'};
	}


	# Look for generators that have 0 ops (i.e. 0 good, 0 bad);
	my (%no_ops, @opkeys, @text_results);
	@text_results = split (/\n/, $_->text_results());
	for (@text_results) {
#	    Log 130, "$_\n";
	    # pattern: 602 generator class time time2 good bad ...
	    if (m/^602 (\d+) \d+ \d+ \d+ 0 0/) {
		$no_ops{$1} = 1;
	    }
	}
	@opkeys = keys (%no_ops);
	$client_run_stats{'gens_missing_class_ops'} = @opkeys;
	$run_stats{'gens_missing_class_ops'} += @opkeys;


	# Cumulative conformance data
        $lg_cum_res = $_->{'lg_cum_results'};
	for (my $i = 0; $i < @$lg_cum_res; $i++) {
	    $client_run_stats{'cum_conformance'} .= 
		sprintf("%6.2f ", 
			($lg_cum_res->[$i] / 
			 $client_run_stats{'requested_conn'})*100.0);
            $total_cum->[$i] += $lg_cum_res->[$i];
	}

	# Successive speed limit string
        $successive_speed_limits = $_->{'successive_speed_limits'};
	if (! $config->{'successive_speed_limits'}) {
	    $config->{'SUCCESSIVE_SPEED_LIMITS'}= [@{$successive_speed_limits}];
	    $speed_limit_string = sprintf "@{$successive_speed_limits}";
	}

	# Total up per class results
	my $tmp = $_->results();

	for ($class = 0; $class < @$tmp; $class++) {
	    $cl_results->[$class] = [] if !defined($cl_results->[$class]);

	    # Don't subtotal name and probability fields
	    $cl_results->[$class][0] = $tmp->[$class][0];
	    $cl_results->[$class][1] = $tmp->[$class][1];


	    # Subtotal all the other stats
	    for ($j = 2; $j < @{$tmp->[$class]}; $j++) {
		$cl_results->[$class][$j] = 0 
		    if !defined($cl_results->[$class][$j]);
		$cl_results->[$class][$j] += $tmp->[$class][$j];
	    }
	}

	#Printout class results for this client
	my (@misc_results) = 
	    &printout_class_results ($tmp, $config->{'RUN_TIME'},$num,$client_run_stats{'requested_conn'},$_->{'client'},"CLIENT");
	
	
	for (qw(response bitrate throughput)) {
	    $client_run_stats{$_} = shift @misc_results;
	}
	push (@errors, @misc_results);

	#Prinout summary infor for this client
	printout_summary( \%client_run_stats, $speed_limit_string, 
			 $config->{'USER_SPEED_LIMIT'},"CLIENT");



	&client_log("ERRORS FOUND\n" . join("\n", @misc_results) . "\n\n") 
	    if (@misc_results);
	&client_log ("=============================================================================\n");

	undef $_;
    } #end of for (@clients)

    # Printout class results for this run
    my (@misc_results) = &printout_class_results($cl_results, 
						 $config->{'RUN_TIME'}, 
						 $num, "", "", "TOTALS");
    #Save return values and errors
    for (qw(response bitrate throughput)) {
	$run_stats{$_} = shift @misc_results;
    }
    push (@errors, @misc_results);

    # Check to see if the posts per class match what's in the post.log
    if ($config->{'DYNAMIC_POST'} > 0) {
	my (@server_post_count) = @$server_post_count_ref;
	for ($class = 0; $class < @server_post_count; $class++) {
	    my $difference = $cl_results->[$class][6] - 
		$server_post_count[$class]; 
	    # The server isn't allowed to have more posts than were submitted
	    if ($difference < 0) {
		push (@errors,
		      sprintf "Iteration $num: The server post.log recorded %d too many POSTS in class %d",
		      abs($difference), $class);
	    }
	    # The server is allowed to have less, up to the MAX_ERROR_RATE percent.
	    elsif ($difference / $cl_results->[$class][6] > $MAX_ERROR_RATE) {
		push (@errors,
		      sprintf "Iteration $num:  %.1f%% of class %d POSTS missing in post.log.",
		      100 * $difference / $cl_results->[$class][6], $class);
	    }
	    
#	    if ($cl_results->[$class][6] != $server_post_count[$class]) {
#		push (@errors, 
#		      sprintf "Iteration $num: The server saw %d instead of %d class %d POST transactions",
#		      $server_post_count[$class], $cl_results->[$class][6], 
#		      $class);
#	    }
	}
    }

    # Make the conformance stirng
    for (my $i = 0; $i < @$total_cum; $i++) {
	$run_stats{'cum_conformance'}  .= 
	    sprintf("%6.2f ", 
		    ($total_cum->[$i] /
		     $run_stats{'requested_conn'})*100.0);
    }

    # Printout summary info for this run
    printout_summary( \%run_stats, $speed_limit_string, 
		         $config->{'USER_SPEED_LIMIT'}, "TOTALS");

    # A few more error checks
    if ($run_stats{'invalid_conn'} > 0) {
	push (@errors,
	      sprintf "Iteration $num: $run_stats{'invalid_conn'} invalid connections found");
	push (@errors,
	      sprintf ("             %d from load generators with 0 requests in a class or classes", 
		       $run_stats{'gens_missing_class_ops'}));
	push (@errors,
	      sprintf ("             %d from load generators with > %.1f%% errors in a class or classes", 
	$run_stats{'invalid_conn'} -  $run_stats{'gens_missing_class_ops'},
	$MAX_ERROR_RATE * 100));

    }

    if ($run_stats{'conform_pct'} < $MIN_CONFORM_PCT) {
	push (@errors,
	      sprintf "Iteration $num: %% conforming connections is %.1f%% must be >= %.1f%%",
	      $run_stats{'conform_pct'}, $MIN_CONFORM_PCT);

    }
    
    # Save the errors off so that we can peruse them later.
    $config->{'results'} = [] if !defined $config->{'results'};
    push (@{$config->{'results'}}, 
	  [ { %run_stats },		#%run_stats
	   $num - 1,			#$num is 1-based 
	   "unused field",		#place-holder, if we need it
	   $cl_results,			#class results stuff
	   [ @errors ] ]);		#@errors

    &Log (101, "ERRORS FOUND\n" . join("\n", @errors) . "\n\n") if (@errors);
    &Log (101, "=============================================================================\n");

    return 1;
}


# This is called to validate whether the inputs in the config file will
# generate a valid test
sub validate_config {
    my ($config) = @_;
    my (@errors);


    push (@errors, &validate_req('RUN_TIME', $config->{'RUN_TIME'}));
    push (@errors, &validate_req('RAMPUP_TIME', $config->{'RAMPUP_TIME'}));
    push (@errors, &validate_req('ITERATIONS', $config->{'ITERATIONS'}));
    push (@errors, &validate_req('RAMPDOWN_TIME', $config->{'RAMPDOWN_TIME'}));
    push (@errors, &validate_req('DYNAMIC_CONTENT', $config->{'DYNAMIC_CONTENT'}));
    push (@errors, &validate_req('DYNAMIC_POST', $config->{'DYNAMIC_POST'}));
    push (@errors, &validate_req('DYNAMIC_CAD_GET', $config->{'DYNAMIC_CAD_GET'}));
    push (@errors, &validate_req('DYNAMIC_CGI_GET', $config->{'DYNAMIC_CGI_GET'}));
    push (@errors, &validate_req('REQUESTS_PER_KEEP_ALIVE', $config->{'REQUESTS_PER_KEEP_ALIVE'}));
    push (@errors, &validate_req('PERCENT_KEEP_ALIVE_REQUESTS', $config->{'PERCENT_KEEP_ALIVE_REQUESTS'}));
    push (@errors, &validate_req('ABORTIVE_CLOSE', $config->{'ABORTIVE_CLOSE'}));
    push (@errors, &validate_req('USER_LINE_SPEED', $config->{'USER_LINE_SPEED'}));
    push (@errors, &validate_req('USER_SPEED_LIMIT', $config->{'USER_SPEED_LIMIT'}));
    push (@errors, &validate_req('MAX_FILE', $config->{'MAX_FILE'}));
    push (@errors, &validate_req('WAIT_BETWEEN_POINTS', $config->{'WAIT_BETWEEN_POINTS'}));
    

    # check for reportable values for warmup and runtime
    if ($config->{'WARMUP_TIME'} < $MIN_WARMUP_TIME) {
	push (@errors, 
	    sprintf "Invalid test: WARMUP_TIME is %d, must be greater than %d",
		    $config->{'WARMUP_TIME'}, $MIN_WARMUP_TIME);
    }


    if (@errors) {
	push (@{$config->{'errors'}}, @errors);
	my $errorstring = "\nWARNING: RUN WILL BE INVALID\n" . 
	    join ("\n", @errors) . "\n\n";
	&Log(10, $errorstring);
    }

    # Check the validity of USER_SPEED_LIMIT versus USER_LINE_SPEED
    if ($config->{'USER_SPEED_LIMIT'} > $config->{'USER_LINE_SPEED'} ) {
       die "Fatal error: USER_SPEED_LIMIT cannot be larger than USER_LINE_SPEED!\n";
    }

}

sub validate_req {
    my ($varname, $input_value) = @_;

    # Create the $REQ_varname variable and find out it's value
    my $REQ_varname = '$REQ_' . $varname;
    my $REQ_value =  eval $REQ_varname; 

    #Test it against the input, and return an error string if not equal
    if ($REQ_value != $input_value) {
	return "Invalid test: $varname is $input_value, must be $REQ_value";
    }
    return;
}





# This is called to validate the results of the entire run for this
# config file.  It uses the error data calculated for each data point
# in run_one
sub validate_results {
    &Log(30, "validate_results\n");
    my ($config) = @_;
    my ($num, $idxname, $peakidx, $peak, $min, $max, $tmp);
    my (@invalid_mix, @invalid_errors);
    my (@results) = @{$config->{'results'}};
    my (@test_errors) = ();
    my %conform_cxn;



    #@{$config->{'errors'}} = ();        #MC: initialize to no errors

    &Log(10, scalar(@{$config->{'results'}}) . " data points\n");

    #Look through all of the results
    my ($i);
    for ($i = 0; $i < @results; $i++) {
	#Test for errors
	my @run_errors = @{$results[$i][4]};
	if (@run_errors ) {
	    push (@test_errors, "\n");
	    push (@test_errors, @run_errors);
	}
    }

    push (@{$config->{'errors'}},  @test_errors);

    # Return a list of the errors -- why? nobody checks the return value
    #return @{$config->{'errors'}};;
}

#check the post log for any errors -- done after every point
sub validate_post_log {

    my ($config, $iteration) = @_;
    my @errors = ();

    # Fetch the POST data on the server if necessary
    my @server_post_count;

    my ($req, $res, $real_count, $reference_count, $dynamic_url);

    unlink('post.log');
    
    # For the command/Fetch, we don't want any other argument pieces
    # so delete everything after the first '?' and append the command.
    $dynamic_url = $config->{'DYN_CMD_SCRIPT'};
    $dynamic_url  =~ s/\@\@SERVER\@\@/$config->{'SERVER'}/e;
    
    $req = new HTTP::Request 'GET' => $dynamic_url . '?command/Fetch';
    $req->header('Accept' => '*/*');
    $res = $ua->request($req, 'post.log');
    if (!$res->is_success) {
	die "Can't fetch POST log from server, make sure cgi script\n" .
	    "DYNAMIC_ROOT (or DYN_CMD_SCRIPT) is configured correctly!\n" .
		"Error: " . $res->code . " " . $res->message, "\n";
    }
    if (open(FILE, "<post.log")) {
	my $lastseen = 0;
	my $errorseen = 0;
	my $seen_pre = 0;
	while (<FILE>) {
	    if (!$seen_pre) {
		$seen_pre = 1 if m/^<pre>/;
		next;
	    } elsif (m#^</pre>#) {
		     $seen_pre = 0;
		     next;
		 } elsif (m#^\s*$#) {
			  next;
		      } elsif (m#^\s*(\d+)\s*$#) {
			       $reference_count = $1;
			       next;
			   }
	    $real_count++;
	    my ($count, $ts, $pid1, $dir, $class, $num, $client, $query, $pid2, $cookie) = split;
	    warn "POST Log contains inconsistent data!\n" if ($pid1 != $pid2);
	    if (!$errorseen && $lastseen >= $count) {
		$errorseen=1;
		push (@errors, 
		      "Iteration $iteration: out of order entry in log\n");
	    } else {
		$lastseen = $count;
	    }
	    $server_post_count[$class]++;
	}
    } else {
	die "Can't open POST log we fetched from server 'post.log': $!\n";
    }
    if ($reference_count != $real_count) {
	push (@errors, 
	      "Iteration $iteration: Number of POST requests in log inconsistant\n");
    }
    return \@server_post_count, \@errors;
}


# This simply loops until all of the clients are in the 'Ready' state.
sub wait_until_ready {
    my ($clients, $state) = @_;
    $state = 'Ready' if $state eq '';
    my $ready = 0;
    while (!$ready) {
	$ready = 1;
	for (@$clients) {
	    if ($_->state() ne $state) {
		$ready = 0;
		event_loop($clients);
		last;
	    }
	}
    }
}

# This checks to see which clients are active, and selects on them.
sub event_loop {
    my ($clients) = @_;
    my $sel_read   = IO::Select->new();
    my $sel_write  = IO::Select->new();
    my %sock_to_client;
    for (@$clients) {
	my $sock = $_->sock();
	$sel_read->add($_->sock())  if $_->need_read();
	$sel_write->add($_->sock()) if $_->need_write();
	$sock_to_client{$sock} = $_;
    }
    my ($reads, $writes, $errors) = $sel_read->select($sel_write, undef);
    for (@$reads) {
	$sock_to_client{$_}->handle_read();
    }
    for (@$writes) {
	$sock_to_client{$_}->handle_write();
    }
}

sub dirs_from_load {
    my ($config, $load) = @_;

    # 122000 is the average size of reply per SPECweb99 request
    my $opsps = ($config->{'USER_LINE_SPEED'}/122000) * $load;
    return int(25+($opsps/5));
}


#the @results are passed in -- an index number for the median one is returned
sub find_median_result {

    my ($config) = @_;
    my @sorted_results = sort results_sort @{$config->{'results'}};
    my $median_index = $#sorted_results / 2;


    return 
	$sorted_results[$median_index][1], 
	$sorted_results[$median_index][0]{'conforming'};
}




##############################################################################
# Helper functions
##############################################################################

# Try one of each test operation to make sure it works -- die if any
# are broken.
# Note:  This is the ONLY place where the custom ad scan/replacement scheme
# is verified.
# Another note:  we set $config->{'HTTP_PROTOCOL'} in here with the return
#   protocol from the 1st GET (if not specified in the rc file, that is)
sub verify_all_HTTP_operations {

    my($config) = @_;
    my($url);

    my($thiserror, $errors, $res);

    my $tmp_url_root = $config->{'URL_ROOT'};
    $tmp_url_root =~ s/\@\@SERVER\@\@/$config->{'SERVER'}/e;
    $max_load =  $config->{'MAX_LOAD'} if $config->{'MAX_LOAD'} > $max_load;

    if ($config->{'DYNAMIC_POST'} > 0 || $config->{'DYNAMIC_CAD_GET'} > 0 ) {
	my $tmp_dyn_cmd_script = $config->{'DYN_CMD_SCRIPT'};
	$tmp_dyn_cmd_script =~ s/\@\@SERVER\@\@/$config->{'SERVER'}/e;
	($res, $thiserror) = &HTTP_operation 
	    ("Reset command",
	     $tmp_dyn_cmd_script . '?command/Reset' . '&maxload=' . $max_load .
	     '&pttime=' . 100 .  '&maxthread=' . 100 . '&exp=' . 
	     '1,100' . '&urlroot=' . $tmp_url_root ,
	     'dynreset.out');
	
	if ($thiserror) {
	    Log (1, $thiserror);
	    $errors++;
	}
    }


    my($filenum) = int(rand($FILES_PER_CLASS - 1)); 
    $url = sprintf ("/file_set/dir%05d/class3_$filenum",
		    &dirs_from_load($config, $config->{'MAX_LOAD'})-1);


    # Static GET
    ($res, $thiserror) = &HTTP_operation("static GET", 
				    $tmp_url_root . $url,
				    'staticget.out');
    if ($thiserror) {
	Log (1, $thiserror);
	$errors++;
    }

    #Decide whether we need HTTP 1.0 or 1.1
    if ($config->{'HTTP_PROTOCOL'} eq '') {
	$config->{'HTTP_PROTOCOL'} = $res->protocol();
	#print "HTTP_PROTOCOL = $config->{'HTTP_PROTOCOL'}\n";
    }

    # Switch to shorter files!
    $url =~ s/class3/class1/;

    # Dynamic GET
    my $tmp_dyn_get_script = $config->{'DYN_GET_SCRIPT'};
    $tmp_dyn_get_script .= '?' if !($tmp_dyn_get_script =~ m/\?/);
    $tmp_dyn_get_script =~ s/\@\@SERVER\@\@/$config->{'SERVER'}/e;
    if ($config->{'DYNAMIC_CONTENT'} > 0) {
	($res, $thiserror) = &HTTP_operation("dynamic GET", 
					       $tmp_dyn_get_script . $url,
					       'dynget.out');
	if ($thiserror) {
	    Log (1, $thiserror);
	    $errors++;
	}
    }


    # Dynamic CGI GET
    my $tmp_dyn_cgi_script = $config->{'DYN_CGI_SCRIPT'};
    $tmp_dyn_cgi_script =~ s/\@\@SERVER\@\@/$config->{'SERVER'}/e;
    $tmp_dyn_cgi_script .= '?' if !($tmp_dyn_cgi_script =~ m/\?/);
    if ($config->{'DYNAMIC_CGI_GET'} > 0) {
	($res, $thiserror) = &HTTP_operation("dynamic CGI GET", 
			     $tmp_dyn_cgi_script . $url,
			     'dyncgiget.out');
	if ($thiserror) {
	    Log (1, $thiserror);
	    $errors++;
	}
    }

    my ($filename) = 'dyncadget.out';
    # Dynamic Custom Ad GET
    my $tmp_dyn_cad_script = $config->{'DYN_CAD_SCRIPT'};
    $tmp_dyn_cad_script =~ s/\@\@SERVER\@\@/$config->{'SERVER'}/e;
    $tmp_dyn_cad_script .= '?' if !($tmp_dyn_cad_script =~ m/\?/);
    if ($config->{'DYNAMIC_CAD_GET'} > 0) {
	my ($old_errors) = $errors;
	($res, $thiserror) = &HTTP_operation("dynamic custom ad (CAD) GET", 
					     $tmp_dyn_cad_script . $url,
					     $filename);

	if ($thiserror) {
	    Log (1, $thiserror);
	    $errors++;
	}


	# From the cookie, figure out the expected n, x, and y values
	my ($adid, $en, $ex, $ey, $fn, $fx, $fy, $ct, $found, $sep);
	my $headers = $res->headers_as_string();

	if ($headers =~ m/Set-Cookie.*Ad_id=(\d+)/) {
	    $adid = $1;
	    $en = sprintf( "%05d", int($adid/36));
	    $ex = int(($adid % 36) / 9);
	    $ey = int($adid % 9);
	}
	
	if (open(FILE, "<$filename")) {
	    while (<FILE>) {
		#Note:  on NT, the headers show up in the file, not in
		#headers-as-string() method
		if (undef($adid) && m/Set-Cookie.*Ad_id=(\d+)/) {
		    $adid = $1;
		    $en = sprintf( "%05d", int($adid/36));
		    $ex = int(($adid % 36) / 9);
		    $ey = int($adid % 9);
		}

		    
		if (m#<!WEB99CAD><IMG SRC="(\S+)"><!/WEB99CAD>#) {
		    $found = $1;
		    $ct++;
		    if ($1 =~ m/(\S{1})class/) {
			$sep = $1;
		    }
		    else {
			Log (1, "  **ERROR**:  Bizarre stuff found in WEBCAD string\n\n$_\n");
			$errors++;
		    }
		    my($expected) =sprintf("%sfile_set%sdir%5s%sclass%01d_%01d",
					   $sep,$sep,$en,$sep,$ex,$ey);

		    if ($expected ne $found) {
			Log (1,  "ERROR: Substitution in Custom Ad scan is incorrect\n Expected: $expected Found: $found\n");
			$errors++
		    }
		}
	    }
	    close FILE;

	    if (undef($adid)) {
		Log (1, "  **ERROR**:  No Set-Cookie header returned -- Header string:\n\n$headers\n");
		$errors++;
	    }
		
	    elsif ($ct == 0) {
		Log(1,  "  **ERROR**:  No Custom Ad substitution was found -- see runrules for information on the WEB99CAD string substitution\n");
		$errors++
	    }
	}
	else {
	    Log(1, " **ERROR**:  Can't open $filename fetched from dynamic CAD GET of $!\n");
	    $errors++
	}
	
	if ($old_errors < $errors) {
	    Log(1, "    NOTE:  errors in Custom Ad Rotation may be caused by" .
		" problems in \n command/Reset.  Check that upfgen and cadgen" .
		" get run correctly\n");
	}
    }

    # Dynamic post
    my $tmp_dyn_post_script = $config->{'DYN_POST_SCRIPT'};
    $tmp_dyn_post_script =~ s/\@\@SERVER\@\@/$config->{'SERVER'}/e;
    $tmp_dyn_post_script .= '?' if !($tmp_dyn_post_script =~ m/\?/);
    if ($config->{'DYNAMIC_POST'} > 0) {
	($res, $thiserror) = &HTTP_operation("dynamic POST",
				    $tmp_dyn_post_script . $url,
				    'dynpost.out');

	if ($thiserror) {
	    Log (1, $thiserror);
	    $errors++;
	}

	my ($server_pcount) = 0;
	my($pcount_ref, $errors_ref) = &validate_post_log($config, "pre test");
	for (@$pcount_ref) {$server_pcount += $_};
	if ($server_pcount != 1) {
	    Log(1, "  **ERROR**:  There should only be 1 post log entry;" .
		"$server_pcount" .
		" were found\n");
	    Log(1, "    NOTE:  errors in dynamic POST may be caused by" .
		" problems in \n command/Reset.  Check that the post.log" .
		" gets zeroed correctly\n");
	    $errors++;
	}


    }

    die "\n\n****** FATAL ERRORS FOUND *******\n\n" if $errors;
}

# Code for doing HTTP operations from manager
sub HTTP_operation {
    my ($operation, $url, $output_file) = @_;
    my($req, $res, $error);

    Log(5, "\n  Checking $operation with '$url'\n");

    if ($operation =~ m/POST/i) {
	$req = new HTTP::Request 'POST' => $url . "f";
	my($urlroot) = ($url =~ m#\?(.*file_set/)#);
	$req->content("class=1&client=2&dir=00004&num=3&urlroot=$urlroot");
    }
    else {
	$req = new HTTP::Request 'GET' => $url;
    }
    
    $req->header('Accept' => '*/*');
    
    if ($operation =~ m/CAD/i || $operation =~ m/POST/i) {
	$req->header('Cookie'=> "my_cookie=user_id=10001&last_ad=20");
    }
    $res = $ua->request($req,$output_file);
    if (!$res->is_success) {
	if ($operation =~ m/static/i) {
	    $error .=  "  **ERROR**:" . 
		"Can't fetch file $url with a $operation request\n" .
		    "Did WAFGEN generate a large enough fileset?\n" .
			"Error: " . $res->code . " " . $res->message .  "\n";
	}
	else {
	    $error .=  "  **ERROR**:" .
		" Can't fetch file $url with a $operation request\n" .
		    "Is DYNAMIC_ROOT (or DYN_xxx_SCRIPT) configured " .
			" correctly?\n" .
			    "Error: " . $res->code . " " 
				. $res->message .  "\n";
	}
	return $res, $error;
    }

    if ($operation =~ m/Reset/i) {
	return $res, $error;
    }

    my($fileset_string);
    if (open(FILE, "<$output_file")) {
	while (<FILE>) {
	    if (m/\d+\s+(file_set.*class.*)/){
		$fileset_string = $1;
		last;
	    }
	}
	if  (!$fileset_string ) {
	    $error .= "  **ERROR**:  Improper file returned.  See " .
		"$output_file for returned text\n";
	}
    }
    else {
	$error .= "Can't open $output_file to check $operation: $!\n";
    }

	
    return $res, $error;
    
}


# Printout the class summary tables.  The first table has the mix,
# op count, and response time information.  The 2nd table has bitrate
# information.
# This routine works for both the per-client and per-iteration printouts
# Also, check for per-class errors:
#		1) Mix problems (only for entire iteration)
#               2) Error rate (for each client)
sub printout_class_results {
    my ($cl_res_array, $runtime, $runnum, $connections, $clientname, $output_type) = @_;

# Set this (and change the label ) to the confidence interval you
# desire:
#   my $CHI_SQR = 2.71;           # chi-sqr value for 90%   CI
#   my $CHI_SQR = 3.84;           # chi-sqr value for 95%   CI
#   my $CHI_SQR = 5.02;           # chi-sqr value for 97.5% CI
#   my $CHI_SQR = 6.63;           # chi-sqr value for 99%   CI
    my $CHI_SQR = 7.88;          # chi-sqr value for 99.5% CI
    
    my (@subtotals, $mix_str, $rate_str, $total_wtd_abr, @errors);

    $subtotals[0] = $cl_res_array->[0][0];
    $subtotals[1] = -10101;	#The probability subtotal is meaningless


    # Subtotal all the classes, then save in the xxx_T variables
    for (my $class = 0; $class < @$cl_res_array; $class++) {
	for (my $j = 2; $j < @{$cl_res_array->[$class]}; $j++) {
	    $subtotals[$j] += $cl_res_array->[$class][$j];
	}
    }
    my ($name_T, $prob_T, $time_T, $time2_T, $good_T, $bad_T, $post_T, 
	$rate_T, $rate2_T, $slow_rate_T, $aggregate_bytes_T) = @subtotals;


    # Calculate the aggregate bit-rate
    my $throughput_T = $runtime?$good_T / $runtime:0;
    my $aggregate_rate_T = $throughput_T ? 
	$aggregate_bytes_T / ($time_T/1000) : 0;
    # convert into bits/sec
    $aggregate_rate_T *= 8;


    # added 3-28-00 by Greg Darnell to check for overloaded client
    if ($output_type eq "CLIENT") {
        my ($observed_bitrate) = 8. * ($aggregate_bytes_T /( $runtime * $connections));
	if ($observed_bitrate < (0.99 * $aggregate_rate_T)) {
            push (@errors, 
		sprintf "Iteration $runnum: $clientname reported %.1f Kbps, actual bitrate %.1f Kbps",
		  $aggregate_rate_T/1000., $observed_bitrate / 1000.);
            push (@errors,
                  sprintf "             Observed bitrate must be within 1%% of client-reported rate.");
            push (@errors,
                  sprintf "             See documentation for possible causes of this error.");
	}
    }

    # Build $mix_str with basic class mix information
    $mix_str = "
-------------------------+-----------------+--------------------------+------
	     M  I  X     | O P   C O U N T | MEAN RESPONSE TIME Ms/Op | Total
Class     Target  Actual | Success   Error |     Mean   StdDev 95% CI |  Time
-------------------------+-----------------+--------------------------+------
";


    #Build $rate_str with bit-rte information
    $rate_str .= "
-------+--------------------------------+------------------+--------------------
       |Individual OP Bit Rate(bits/sec)| Aggregate Bit    | Weighted ABR
Class  |        Mean      StdDev 95% CI | Rate  (bits/sec) |      (%)
-------+--------------------------------+------------------+--------------------
";


    # Go through each class, calculating and printing stats for the 2 tables
    for (my $class = 0; $class < @$cl_res_array; $class++) {
	my ($name, $prob, $time, $time2, $good, $bad, $post, $rate, 
	    $rate2, $slow_rate, $aggregate_bytes) = @{$cl_res_array->[$class]};

	# Stats for response times
	my $stddev     = 0;
	my $confidence = 0;
	my $mean       = 0;

	# Stats for bit rates
	my $stddev_rate     = 0;
	my $confidence_rate = 0;
	my $mean_rate       = 0;
	my $abr             = 0;
	my $weighted_abr    = 0;


	my $actual_mix= $good_T ? $good/$good_T : 0 ;
	my $class_time = $time_T ? $time/$time_T : 0;

	if ($good > 0) {
	    $mean = $time / $good;

	    $mean_rate = $rate / $good;
            # convert into bits/sec
            $mean_rate *= 8;
            $abr = ($aggregate_bytes / ($time/1000)) * 8.0;
            $weighted_abr = $abr * $class_time;
            $total_wtd_abr += $weighted_abr;
	}

	if ($good > 1) {
	    $stddev     = sqrt(($time2 - ($time*$time)/$good) / ($good - 1));
	    $confidence = sqrt($CHI_SQR * $stddev / $good);

	    $stddev_rate = sqrt(($rate2 - ($rate*$rate)/$good) / ($good - 1));
            # convert into bits/sec
            $stddev_rate *= 8;
	    $confidence_rate = sqrt($CHI_SQR * $stddev_rate / $good);

	}

	# Check for per-class error rate problems
	# NOTE:  this is the only place where Class 3 error rates are checked
	# Classes 0-2 are checked in the client on a per load-generator
	# per-class basis.  If the error rate is too high, the load-gen is
	# marked as invalid.
	# Class 3 is such a small portion, a single error could invalidate
	# a load-gen, so class 3 errors rates are calculated for each client.
	if ($output_type eq "CLIENT") {
	    if ($good && $bad / ($bad+$good) > $MAX_ERROR_RATE) {
		push (@errors, 
		      sprintf "Iteration $runnum: error rate for class %s is %.1f%%, must be <= %.1f%%",
		      $name, 100 * $bad/($bad+$good),  $MAX_ERROR_RATE * 100);
	    }
	}


	# Check for errors on the whole run
	elsif ($output_type eq "TOTALS") {
	    # Mix problems for each class
	    if ($actual_mix < $prob * (1 - $MIX_TOLERANCE) ||
		$actual_mix > $prob * (1 + $MIX_TOLERANCE)) {

		push (@errors, 
		      sprintf "Iteration $runnum: mix for class %s is %.3f, must be between %.3f and %.3f",
		      $name, $actual_mix,
		      $prob * (1 - $MIX_TOLERANCE),
		      $prob * (1 + $MIX_TOLERANCE));
	    }
	}

	$mix_str.=sprintf ("%-12s %5.3f %5.3f | %7d %7d | %8.2f %8.2f %6.2f |%5.1f%%\n",
			   $name, $prob, $actual_mix, $good, $bad, $mean, 
			   $stddev, $confidence, $class_time*100.0);

	$rate_str .= sprintf ("%-6s | %11.2f %11.2f %6.2f |  %11.2f     | %11.2f(%3.2f)\n",
			      $name, $mean_rate, $stddev_rate, $confidence_rate,
			      $abr, $weighted_abr, 
			      ($aggregate_rate_T ? $weighted_abr/$aggregate_rate_T : 0)*100);
    }


    # Calculate and print the totals for the 2 tables
    my $stddev_T     = 0;
    my $confidence_T = 0;
    my $mean_T       = 0;
    my $error_pct_T  = 0;

    if ($good_T > 0) {
	$mean_T = $time_T / $good_T;
	$error_pct_T = ($bad_T / ($bad_T+$good_T)) * 100;
    }
    if ($good_T > 1) {
	$stddev_T =sqrt(($time2_T - ($time_T*$time_T)/$good_T) / ($good_T - 1));
	$confidence_T = sqrt($CHI_SQR * $stddev_T / $good_T);
    }
    

    $mix_str .= sprintf "-------------------------+-----------------+--------------------------+------
			 | %7d %7d | %8.2f %8.2f %6.2f |\n\n", 
	$good_T, $bad_T, $mean_T, $stddev_T, $confidence_T;
    
    $rate_str .= sprintf "-------+--------------------------------+------------------+--------------------
       |                                |                  | %11.2f(%3.2f) 
", 
	   $total_wtd_abr, 
	   ($aggregate_rate_T ? $total_wtd_abr/$aggregate_rate_T : 0 )*100;
    
    

    # Print out mix table and rate table

    if ($output_type eq "ASCII") { return ($mix_str . $rate_str) }

    if ($logclient && $output_type eq "CLIENT") {
	&client_log($mix_str . $rate_str);
    }
    &Log(101, $mix_str . $rate_str) if $output_type eq "TOTALS";
 
    return $mean_T, $aggregate_rate_T, $throughput_T, @errors;

    

}


#
#
# Summary information for a client or run
#
sub printout_summary {
    my ($run_stats, $speed_limit_string, $user_speed_limit, $output_type) = @_;

    $run_stats->{'conform_pct'} = 
	100 * $run_stats->{'conforming'}/$run_stats->{'requested_conn'};

    $run_stats->{'ops_per_thread'} = $run_stats->{'throughput'} /
	$run_stats->{'requested_conn'};




    my ($str2) = sprintf "
             |            | total   | ops/sec/| 
             |  SPECweb99 | ops/sec | loadgen | msec/op
-------------+------------+---------+---------+---------
RESULTS      |   %6d   |  %6d |  %4.2f   |  %5.1f 

        
             |           |           |           |  conforming*
             | requested |  valid    |  invalid  |    (conf %) 
-------------+-----------+-----------+-----------+----------------
SIMULTANEOUS | %7d   | %7d   | %7d   | %7d (%6.2f%%)
CONNECTIONS  |           |           |           |
* a conforming connection is one that operates faster than 320K bit/sec


             |  mean   |  min    |  max   
-------------+---------+---------+--------               
AGGREGATE    |         |         |
BITRATES     | %6d  | %6d  | %6d                         
(bits/sec)   |         |         |                                
                                                               

-------------------------------------------------------------------------
Percentage of simultaneous connections conforming at various speed limits
--------------------------+----------------------------------------------
Successive Speed Limits : |  %s
Cumulative Conformance %: |  %s
--------------------------+----------------------------------------------

",
    $run_stats->{'conforming'},
    $run_stats->{'throughput'},
    $run_stats->{'ops_per_thread'},
    $run_stats->{'response'},

    $run_stats->{'requested_conn'},
    $run_stats->{'requested_conn'} -     $run_stats->{'invalid_conn'},
    $run_stats->{'invalid_conn'},
    $run_stats->{'conforming'},
    $run_stats->{'conform_pct'},


    $run_stats->{'bitrate'},
    $run_stats->{'min_bitrate'},
    $run_stats->{'max_bitrate'},
    $speed_limit_string,
    $run_stats->{'cum_conformance'};

   my($str) = sprintf "

RESULTS:  %6d SPECweb99 

BITRATES(bits/sec):  avg = %6d    min =%6d     max = %6d 

THROUGHPUTS: ops/sec = %6d    ops/sec/loadgen = %4.2f    msec/op = %6.1f 

SIMULTANEOUS_CONNECTIONS: 
 %6d requested %6d invalid %6d (%.2f%) conforming at %d bits/sec

-------------------------------------------------------------------------
Percentage of simultaneous connections conforming at various speed limits
--------------------------+----------------------------------------------
Successive Speed Limits : |  %s
Cumulative Conformance %: |  %s
--------------------------+----------------------------------------------

",
    $run_stats->{'conforming'},
    $run_stats->{'bitrate'},
    $run_stats->{'min_bitrate'},
    $run_stats->{'max_bitrate'},
    $run_stats->{'throughput'},
    $run_stats->{'ops_per_thread'},
    $run_stats->{'response'},

    $run_stats->{'requested_conn'},
    $run_stats->{'invalid_conn'},
    $run_stats->{'conforming'},
    $run_stats->{'conform_pct'},
    $user_speed_limit,

    $speed_limit_string,
    $run_stats->{'cum_conformance'};

    # Print out mix table and rate table
    return $str2 if $output_type eq "ASCII";
    &client_log($str2) if $logclient && $output_type eq "CLIENT";
    &Log(101, $str2) if $output_type eq "TOTALS";

}


##############################################################################
# Utility functions
##############################################################################

sub Log {
    my ($level, @data) = @_;
    my ($type, @output);
    my $log_output    = "";
    my $screen_output = "";
    for (@data) {
	$type = ref($_);
	if ($type eq "ARRAY") {
	    if ($verbose >= $level) {
		$screen_output .= join ("", @$_);
		$log_output    .= join ("", @$_);
	    } elsif ($level >= 100) {
		$screen_output .= $_ if $verbose + 100 >= $level;
		$log_output    .= join ("", @$_);
	    }
	} elsif ($type eq "SCALAR") {
	    if ($verbose >= $level) {
		$screen_output .= $$_;
		$log_output    .= $$_;
	    } elsif ($level >= 100) {
		$screen_output .= $_ if $verbose + 100 >= $level;
		$log_output    .= $$_;
	    }
	} else {
	    if ($verbose >= $level) {
		$screen_output .= $_;
		$log_output    .= $_;
	    } elsif ($level >= 100) {
		$screen_output .= $_ if $verbose + 100 >= $level;
		$log_output    .= $_;
	    }
	}
    }
    print     $screen_output;
    print LOG $log_output;
}

sub client_log {
    my (@data) = @_;
    my ($type, @output);
    my $log_output    = "";
    for (@data) {
	$type = ref($_);
	if ($type eq "ARRAY") {
            $log_output    .= join ("", @$_);
	} elsif ($type eq "SCALAR") {
	    $log_output    .= $$_;
	} else {
	    $log_output    .= $_;
	}
    }
    print CLLOG $log_output;
}

sub max {
    my ($val) = @_;
    for (@_) {
	$val = $_ if $_ > $val;
    }
    return $val;
}

# This expands strings in the form of M or M-N or M-NxO into a complete
# list of the values they represent.  1-3 would be 1,2,3 and 1-5x2
# would be 1,3,5
sub expand_ranges {
    my (@data) = @_;
    my (@rc, $start, $stop, $step, $i);

    for (@data) {
	if (($start, $stop, $step) = m/^(\d+)-(\d+)x(\d+)$/) {
	    for ($i = $start; $i <= $stop; $i+=$step) {
		push (@rc, $i);
	    }
	} elsif (($start, $stop) = m/^(\d+)-(\d+)$/) {
	    for ($i = $start; $i <= $stop; $i++) {
		push (@rc, $i);
	    }
	} else {
	    push (@rc, $_);
	}
    }
    @rc;
}

# Return the date (Note that this doesn't have time time zone in it,
# but I'm not sure if we are that concerned about it
sub date {
    my ($result);
    chomp($result = &ctime(time));
    return $result;
}

# Given an list of choices, choose the one that matches the first few
# characters.  Ambiguous and non-matching strings return undef.
sub choose_string {
    my ($string, @choices) = @_;
    my (@matches) = grep (m/^$string/, @choices);
    return undef if @matches != 1;
    return $matches[0];
}

# resolve_choices takes a string containing a comma separated list of
# choices and returns the choices from valid_outputs that match.  The
# choices in required_outputs are added automatically.
sub resolve_choices {
    my($temp, $valid_outputs, $required_outputs) = @_;
    my @temp = split(/\s*[,\s]\s*/, $temp);
    my %temp;
    my @output_types = ();

    for (@$required_outputs) {
	push (@output_types, $_) if (!$temp{$_}++);
    }

    for (@temp) {
	$temp = &choose_string ($_, @$valid_outputs, "all");
	if ($temp eq "all") {
	    for (@$valid_outputs) {
		if (!$temp{$_}++) {
		    push (@output_types, $_);
		}
	    }
	} elsif (!defined $temp) {
	    Log(0, "Don't know how to output type '$_'\n");
	} else {
	    if (!$temp{$_}++) {
		push (@output_types, $_);
	    }
	}
    }
    push (@output_types, $valid_outputs->[0]) if (!@output_types);
    @output_types;
}

# Find the largest 3 digit numbered file in the directory and return a
# 3 digit string of the next number.  Used to output numbered log files.  
sub find_next_number {
    local (*DIR);
    my($path) = @_;
    my (@files, $max);
    opendir (DIR, $path) || die "Can't open directory '$path': $!\n";
    @files = readdir(DIR);
    closedir (DIR);
    $max = 0;
    for (@files) {
	$max = $1 if ((m/^(\d{3})\./ || m/\.(\d{3})$/ || m/\.(\d{3})\./) && $max < $1 );
    }
    if ($max < 0) {
	$max = "000";
    } else {
	$max = sprintf ("%03d", $max+1);
    }
    $max;
}

#For 2 $results, compares 'conforming' -- if equal, then compares 'throughput'
sub results_sort {
    my ($res) = $a->[0]{'conforming'} <=> $b->[0]{'conforming'};
    return $res if $res != 0;
    return $a->[0]{'throughput'} <=> $b->[0]{'throughput'};
}

##############################################################################
# Formatting functions
##############################################################################

# All formatting functions build the output file in a string which they
# return.  If a filename is specified, then they also output to a file.

# do_outputs, figures out what types are needed and calls the appropriate
#  functions

sub do_outputs {



    my ($config, $file_num, $output_dir) = @_;

    # Fill in some benchmark stats for the output
    my(%med_run_stats)= %{$config->{'results'}[$config->{'median_index'}][0]};
    $config->{'bm_requested_conn'} = $med_run_stats{'requested_conn'};
    $config->{'bm_fileset_size'} = $MB_PER_DIRECTORY * 
	&dirs_from_load($config, $med_run_stats{'requested_conn'});

    my $outsel=($output_types ne "")?$output_types:$config->{'OUTPUT_TYPE'};
    my @types=&resolve_choices($outsel, [ sort keys %formatter ], ['screen', 'raw']);

    # Output the results
    for (@types) {
	my $name   = "$output_dir$config->{'OUTPUT_NAME'}.$file_num.$_";
	my $output = &{$formatter{$_}}($config);

	next if $_ eq 'screen';

	my $fh = new IO::File ">$name";
	&Log (10, "Writing '$name'\n");
	if (!defined $fh) {
	    &Log (0, "Cannot create '$name': $!\n");
	    exit (1);
	}
	print $fh $output;
	$fh->close();
    }
}

sub init_output {
    @hardware_fields = (
		    [ 'Vendor',           'hw_vendor'      ],
		    [ 'Model',            'hw_model'       ],
		    [ 'Processor',        'hw_cpu'         ],
		    [ '# Processors',           'hw_ncpu'        ],
		    [ 'Primary Cache',    'hw_pcache'      ],
		    [ 'Secondary Cache',  'hw_scache'      ],
		    [ 'Other Cache',      'hw_ocache'      ],
		    [ 'Memory',           'hw_memory'      ],
		    [ 'Disk Subsystem',   'hw_disk'        ],
		    [ 'Disk Controllers', 'hw_controllers' ],    
		    [ 'Other Hardware',   'hw_other'       ],
    );
    @availability_dates_fields = (
		    [ 'All Hardware',        'hw_avail'       ],
		    [ 'HTTP Software',       'http_avail'     ],
		    [ 'Operating System',    'os_avail'       ],
		    [ 'Supplemental System', 'software_avail' ],
    );
    @software_fields = (
		    [ 'Operating System',        'sw_os'          ],
		    [ 'File System',             'sw_file'        ],
		    [ 'Other Software',            'sw_other'       ],
    );
    @http_software_fields = (
		    [ 'Vendor',           'sw_vendor'      ],
		    [ 'HTTP Software',    'sw_http'        ],
		    [ 'API',              'sw_api'         ],
		    [ 'Server Cache',     'http_cache'     ],
		    [ 'Log Mode',         'http_logmode'   ],
    );
    @test_sponser_fields = (
		    [ 'Test Date',        'test_date'      ],
		    [ 'Tested By',        'tester_name'    ],
		    [ 'SPEC License',     'license_num'    ],
    );
    @network_fields = (
		    [ '# of Controllers',    'net_nctrls'     ],
		    [ 'Network Controllers', 'net_controller' ],
		    [ '# of Nets',           'net_nnets'      ],
		    [ 'Type of Nets',        'net_net'        ],
		    [ 'Network Speed',       'net_speed'      ],
		    [ 'MSL (sec)',           'msl'            ],
		    [ 'Time-Wait (sec)',     'time_wait'      ],
		    [ 'MTU',                 'net_mtu'        ],
    );
    @clients_fields = (
		    [ '# of Clients',        'lg_num'         ],
		    [ 'Model',               'lg_model'       ],
		    [ 'Processor',           'lg_cpu'         ],
		    [ '# of Processors',     'lg_ncpu'        ],
		    [ 'Memory',              'lg_memory'      ],
		    [ 'Network Controller',  'lg_netctrlr'    ],
		    [ 'Operating System',    'lg_os'          ],
		    [ 'Compiler',            'lg_compiler'    ],
    );
    @bm_config_fields = (
 		    [ 'Requested Connections', 'bm_requested_conn' ],
		    [ 'Fileset Size (MB)',     'bm_fileset_size' ],
    );
    @notes_fields = (
		[ 'SUT Notes',              'notes_sut',      ],
		[ 'Operating System Notes', 'notes_os',       ],
		[ 'HTTP Software Notes',    'notes_http',     ],
		[ 'HTTP API Notes',         'notes_api',      ],
		[ 'Software Notes',         'notes_software', ],
		[ 'Hardware Notes',         'notes_hardware', ],
		[ 'Network Notes',          'notes_net',      ],
		[ 'Client Notes',           'notes_client',   ],
		[ 'Other Notes',            'notes',          ],
		[ 'Errors',                 'errors',         ],
    );
    return 0;
}


# .raw files are to look at the raw data and to re-format into the
# other formats.
sub write_raw {
    my ($config, $output_name) = @_;
    my $rc = "";

    # Header for a raw file
    $rc .= "BEGIN SPECWEB99 1.02\n";
#    for (qw( peak_index peak_load peak_throughput peak_response peak_size )) {
#       $rc .= "$_= $config->{$_}\n";
#    }

    # Print out the results
    my $num = "000";
    for (@{$config->{'results'}}) {
	my(%run_stats) = %{$_->[0]};
	my($statname);
	foreach $statname (sort keys %run_stats) {
	    $rc .= "results$num = $statname $run_stats{$statname}\n";
	}

	my $class = 0;
	for (@{$_->[3]}) {
	    my ($name, @rest) = @$_;
	    $rc .= "results$num = class      $class \"$name\" ". join(" ", @rest). "\n";
	    $class++;
	}
	for (@{$_->[4]}) {
	    $rc .= "results$num = error      $_\n";
	}
	$num++;
    }

    # Print out the entire configuration array
    for my $key (sort keys %$config) {
	if ($key eq 'results' || $key eq 'clients_parsed') {
	    # except these, results is done above, and clients_parsed would
	    # need similar logic, but really isn't worth it.  If we
	    # *really* need it we can regenerate it
	} elsif (ref($config->{$key}) eq "ARRAY") {
	    # arrays are output as numbered lines
	    my $num = "000";
	    for (@{$config->{$key}}) {
		$rc .= "$key$num = $_\n";
		$num++;
	    }
	} elsif ($config->{$key} ne "--" && $config->{$key} !~ /^\s*$/) {
	    # Don't print out lines with no values
	    $rc .= "$key = $config->{$key}\n";
	}
    }
    # Trailer to mark the end of the raw data
    $rc .= "END SPECWEB99 1.02\n";

    return $rc;
}

# write_screen  Just writes the ASCII output to the screen.  Used to
# output the results during the run
sub write_screen {
    print &write_asc($_[0]);
}

# Produces an ASCII formatted listing.
sub write_asc {
    my ($config) = @_;
    my (@results) = @{$config->{'results'}};
    my (@errors)  = @{$config->{'errors'}} if $config->{'errors'};
    my $rc = "";

    # Make sure we mark runs invalid
    my $invalid_string = (@errors)?"INVALID TEST":"";


    my $successive_speed_limits = [];
    $successive_speed_limits = $config->{'SUCCESSIVE_SPEED_LIMITS'};
    my $speed_limit_string = sprintf "@{$successive_speed_limits}";

    # Banner
    $rc .= sprintf <<EOT, firstof($config->{'hw_vendor'}).' : '.firstof($config->{'hw_model'}), $config->{'median_results'}, firstof($config->{'sw_vendor'}).' : '.firstof($config->{'sw_http'});
			       SPECweb99 Result

===============================================================================
							||
 %-45.45s          ||   %6d SPECweb99
 %-45.45s          ||        $invalid_string
							||
===============================================================================

PERFORMANCE

               |  Conforming Simultaneous
   Iteration   |       Connections
---------------+----------------------------
EOT

    for (my $i = 0; $i < @results; $i++) {
	my ($conforming) = $results[$i][0]->{'conforming'};
	$rc .= sprintf("     %2d        |      %7d\n", $i+1, $conforming);
    }
	
    $rc .= "---------------+----------------------------\n";
    $rc .= sprintf("    Median     |      %7d\n",  $config->{'median_results'});
    $rc .= "\n";
		  


    ### Use actual test values - so no mismatch occurs ###
    $config->{'lg_num'} = scalar(@{$config->{'CLIENTS'}});

    my $width = 79;
    $rc .= '=' x $width . "\n";
    for my $info (
	[ 'Availability Dates',      [ @availability_dates_fields ], ],
	[ 'Hardware',                [ @hardware_fields ],           ],
	[ 'Software',                [ @software_fields ],           ],
	[ 'HTTP Software',           [ @http_software_fields ],      ],
	[ 'Test Sponsor',            [ @test_sponser_fields ],       ],
	[ 'Network',                 [ @network_fields ],            ],
	[ 'Clients',                 [ @clients_fields ],            ],
	[ 'Benchmark Configuration', [ @bm_config_fields ],          ],) {
	my ($title, $fields) = @$info;

	$rc .= ' ' x int(($width - length($title))/2) . $title . "\n\n";
	for my $field (@$fields) {
	    my ($title, $key) = @$field;
	    my @vals;
	    if (ref($config->{$key}) eq 'ARRAY') {
		@vals = @{$config->{$key}};
	    } else {
		@vals = ($config->{$key});
	    }
	    my $val = shift(@vals);
	    $val = '--' if $val eq '';
	    $rc .= sprintf "%-30s%s\n", $title, $val;
	    for (@vals) {
		$rc .= sprintf "%-30s%s\n", '', $_;
	    }
	}
	$rc .= "\n";
    }
    $rc .= '=' x $width . "\n";
    my $title = "Notes/Tuning information";
    $rc .= ' ' x int(($width - length($title))/2) . $title . "\n";

    for my $info ( @notes_fields ) {
	my ($name, $key) = @$info;
	my $val = $config->{$key};
	my @vals = ();
	next if $val eq '' || $val eq '--' || (ref($val) eq 'ARRAY' && @$val == 0);
	$rc .= "\n$name\n";
	if (ref($val) eq 'ARRAY') {
	    for (@$val) {
		push (@vals, split("\n", $_));
	    }
	} else {
	    @vals = split("\n", $val);
	}
	for (@vals) {
	    $rc .= "    $_\n";
	}
    }

    $rc .= <<EOT; 

===============================================================================

Test Run Details
 
    Run    Conforming  Percent  |  Throughput  Response ops/sec/  Kbits/
    Num    Connections Conform  |  ops/sec     msec     loadgen   sec
EOT

    # Print out the data points we've collected
    for (my $i = 0; $i < @results; $i++) {
	my(%run_stats) = %{$results[$i][0]};
	$rc .= sprintf ("  %2s %2d   %6d     %7.1f%%   |  %7.1f     %7.1f   %5.2f     %5.1f\n", 
			
			$i == $config->{'median_index'} ? "=>" : "  ",
			$i+1,
			$run_stats{'conforming'}, 
			$run_stats{'conform_pct'}, $run_stats{'throughput'},
			$run_stats{'response'}, $run_stats{'ops_per_thread'},
			$run_stats{'bitrate'} / 1000);
    }

    if ($details) {
	$rc .= sprintf <<EOT, $speed_limit_string;


                    Cumulative Conformance % Chart

    Run  Conforming    |  Percent of connections conforming at various speeds
    Num  Connections   |  %s
-----------------------+---------------------------------------------------\n
EOT

	for (my $i = 0; $i < @results; $i++) {
	    my(%run_stats) = %{$results[$i][0]};
	    $rc .= sprintf ("  %2s %2d   %5d        |   %s\n",
			    $i == $config->{'median_index'} ? "=>" : "  ",
			    $i + 1,
			    $run_stats{'conforming'}, 
			    $run_stats{'cum_conformance'});
	}    
    $rc .= "\n\n";
    }

    if ($details) {
	#Write results for individual tests 
	for (my $i = 0; $i < @results; $i++) {
	    $rc .= sprintf("\n\nResults From Iteration %d\n", $i+1);
	    my (@class_results) = @{$results[$i][3]};
	    $rc .= &printout_class_results( \@class_results,
					   $config->{'RUN_TIME'}, $i + 1, "","",
					   "ASCII");
	    my(%run_stats) = %{$results[$i][0]};
	    $rc .= &printout_summary(\%run_stats, $speed_limit_string, 
				     $config->{'USER_SPEED_LIMIT'}, "ASCII");
	}
    }

    return $rc;
}

############################################################################
# HTML Output

sub html_escape {
    my $rc = '';
    for (@_) {
	next if (ref($_) ne '');
	s/>/\&gt;/g;
#	s/&/\&amp;/g;
	s/</\&lt;/g;
	$rc .= $_;
    }
    return $rc;
}

sub html_sub_table {
    my ($config, $title, $entries) = @_;
    my $rc = '';
    $rc .= qq#    <TABLE BORDER WIDTH="100%">\n#;
    $rc .= qq#      <TR><TH COLSPAN=2># . &html_escape($title) . qq#</TH></TR>\n#;
    for (@$entries) {
	my ($name, $key) = @$_;
	my $val = $config->{$key};
	$rc .= qq#      <TR><TD ALIGN="left"># . &html_escape($name) . qq#</TD><TD>#;
	my @vals;
	if (ref($val) eq 'ARRAY') {
	    for (@$val) {
		push (@vals, split("\n", &html_escape($_)));
	    }
	} else {
	    @vals = split("\n", &html_escape($val));
	}
	$rc .= join ("<BR>", @vals);
	$rc .= "</TD></TR>\n";
    }
    $rc .= qq#    </TABLE>\n#;
}

sub html_testrun_tables{
    my ($config) = @_;
    my $rc = '';
    $rc .= qq#    <TABLE BORDER WIDTH="100%">\n#;
    $rc .= qq#    <CAPTION><FONT SIZE+=2><B>Test results for each iteration (median is shaded)\n</B></FONT>#;
    $rc .= qq#      <TR>\n#;
    $rc .= qq#         <TH >Iteration</TH>\n#;
    $rc .= qq#         <TH >Conforming Connections</TH>\n#;
    $rc .= qq#         <TH >Percent Conform</TH>\n#;
    $rc .= qq#         <TH >Throughput ops/sec</TH>\n#;
    $rc .= qq#         <TH >Response msec</TH>\n#;
    $rc .= qq#         <TH >ops/sec/ loadgen</TH>\n#;
    $rc .= qq#         <TH >kbit/ sec</TH>\n#;
    $rc .= qq#      </TR>\n#;

    for (my $i = 0;  $i < @{$config->{'results'}}; $i++) {
	my (%run_stats) = %{$config->{'results'}[$i][0]};
#	print "I=$i and throughput = $run_stats{'throughput'}";
	if ($i == $config->{'median_index'}) {
	    $rc .= qq#      <TR BGCOLOR="cccc99">\n#;
	}
	else {
	    
	    $rc .= qq#      <TR>\n#;
	}
	$rc .= qq#         <TD ALIGN=CENTER># . ($i + 1) . qq#</TD>\n#;
	$rc .= qq#         <TD ALIGN=CENTER># 
	    . ($run_stats{'conforming'}) . qq#</TD>\n#;
	$rc .= qq#         <TD ALIGN=CENTER># 
	    . (sprintf "%.1f%%", $run_stats{'conform_pct'}) . qq#</TD>\n#;
	$rc .= qq#         <TD ALIGN=CENTER># 
	    . (sprintf "%.1f", $run_stats{'throughput'}) . qq#</TD>\n#;
	$rc .= qq#         <TD ALIGN=CENTER># 
	    . (sprintf "%.1f",$run_stats{'response'}) . qq#</TD>\n#;
	$rc .= qq#         <TD ALIGN=CENTER># 
	    . (sprintf "%.2f", $run_stats{'ops_per_thread'}) . qq#</TD>\n#;
	$rc .= qq#         <TD ALIGN=CENTER># 
	    . (sprintf "%.1f", $run_stats{'bitrate'}/1000) . qq#</TD>\n#;
	$rc .= qq#      </TR>\n#;

    }
    $rc .= qq#    </TABLE\n><P>#;

      
#    $rc .= qq#    <TABLE BORDER WIDTH="100%">\n#;
#    $rc .= qq#    <CAPTION><FONT SIZE+=2><B>Percent conforming connections at various speed (bits/sec)</B></FONT>\n#;
#    $rc .= qq#      <TR>\n#;
#    $rc .= qq#         <TH>Iteration</TH>\n#;
#    $rc .= qq#         <TH>Conforming Connections</TH>\n#;
#    for (@{$config->{'SUCCESSIVE_SPEED_LIMITS'}}) {
#	$rc .= qq#         <TH ># . $_ . qq#</TH>\n#;
#    }
#    $rc .= qq#      </TR>\n#;

#    for (my $i = 0;  $i < @{$config->{'results'}}; $i++) {
#	my (%run_stats) = %{$config->{'results'}[$i][0]};
#	$rc .= qq#      <TR>\n#;
#	$rc .= qq#         <TD ALIGN=CENTER># . ($i +1) .  qq#</TD>\n#;
#	$rc .= qq#         <TD ALIGN=CENTER># 
#	    . $run_stats{'conforming'} .  qq#</TD>\n#;
#	for (split(/\s+/, $run_stats{'cum_conformance'})) {
#	    $rc .= qq#         <TD ALIGN=CENTER># . $_ .  qq#</TD>\n#;
#	}
#	     $rc .= qq#      </TR>\n#;
#    }
#    $rc .= qq#    </TABLE\n>#;



    return $rc;
}
sub write_html {
    my ($config) = @_;
    my (@results) = sort {$a->[0] <=> $b->[0]} @{$config->{'results'}};
    my (@errors)  = @{$config->{'errors'}} if $config->{'errors'};

    # Get the raw file, and escape troublesome characters
    my $raw = &html_escape(write_raw($config));

    # Check to see if it's invalid
    my $invalid_text = "INVALID " if @errors;

    my $hw_vendor = firstof($config->{'hw_vendor'});
    my $hw_model  = firstof($config->{'hw_model'});
    my $sw_vendor = firstof($config->{'sw_vendor'});
    my $sw_http   = firstof($config->{'sw_http'});
    my $sim_con   = firstof($config->{'SIMULTANEOUS_CONNECTIONS'});

    my $rc = <<EOT;
<!DOCTYPE HTML PUBLIC "-//Netscape Comm. Corp.//DTD HTML//EN">
<HTML>
<HEAD>
   <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=iso-8859-1">
   <META NAME="GENERATOR" CONTENT="SPECweb99 1.0">
   <TITLE>SPECweb99 Result</TITLE>
<TITLE>SPECweb99 Result</TITLE>
</HEAD>
<BODY>
<!-- 
%% BEGIN SPECWEB99 1.02
$raw
%% END SPECWEB99 1.02
-->

<H1>${invalid_text}SPECweb99 Result</H1></TD>

<TABLE WIDTH="100%">
<TR>
  <TD ALIGN="LEFT">
    <TABLE>
	<TR><TH ALIGN="RIGHT">$hw_vendor:</TH><TH ALIGN="left">$hw_model</TH></TR>
	<TR><TH ALIGN="RIGHT">$sw_vendor:</TH><TH ALIGN="left">$sw_http</TH></TR>
    </TABLE>
  </TD>
  <TD ALIGN="RIGHT">
    <FONT SIZE=+2><B>${invalid_text}SPECweb99 = $config->{'median_results'}</B></FONT>
  </TD>
</TR>
</TABLE>

<P><HR><P>
<H2>Performance</H2>
<CENTER>
<TABLE BORDER=2 CELLPADDING=3 WIDTH="45%" >
<TR>
  <TH WIDTH="30%" HEIGHT=40>Iteration</TH>
  <TH WIDTH="70%" HEIGHT=40>Conforming Simultaneous Connections</TH>
</TR>
<TR>
  <TD ALIGN=CENTER COLSPAN = 2>
    <TABLE BORDER=0 WIDTH=100%>
EOT

    # Print out the data points - iteration, conforming connections
    for (my $i = 0; $i < @results; $i++) {
        my ($conform) = $results[$i][0]->{'conforming'};
        $rc .= "      <TR ALIGN=CENTER>\n";
        $rc .= "         <TD WIDTH=\"25%\">" . ($i+1) . "</TD>\n";
        $rc .= "         <TD WIDTH=\"75%\">" . $conform .  "</TD>\n";
	$rc .= "       </TR>\n";
    }
    $rc .= "    </TD>\n";
    # Print out the median value
    $rc .= <<EOT;
<TR ALIGN=CENTER>
  <TD BGCOLOR="cccc99"><FONT SIZE=+1><B>Median</FONT></B></TD>
  <TD BGCOLOR="cccc99"><FONT SIZE=+1><B>
EOT


    $rc .= $config->{'median_results'};
    $rc .= <<EOT;

</B></FONT></TD>
</TR>

</TABLE>


</TABLE>
</CENTER>

<P><HR><P>

<H2>Configuration</H2>
<TABLE WIDTH="100%">
  <TR>
    <!-- Left -->
    <TD VALIGN="top">
EOT

    $rc .= &html_sub_table($config, 'Availability Dates', 
			    [ @availability_dates_fields ]);
    $rc .= "<P>\n";
    $rc .= &html_sub_table($config, 'Hardware',      [ @hardware_fields ]);
    $rc .= "<P>\n";
    $rc .= &html_sub_table($config, 'Software',      [ @software_fields ]);
    $rc .= "<P>\n";
    $rc .= &html_sub_table($config, 'HTTP Software', [ @http_software_fields ]);
    $rc .= qq#</TD>\n<!-- Right --><TD VALIGN="top">\n#;
    $rc .= &html_sub_table($config, 'Test Sponsor',  [ @test_sponser_fields ]);
    $rc .= "<P>\n";
    $rc .= &html_sub_table($config, 'Network',       [ @network_fields ]);
    $rc .= "<P>\n";
    $rc .= &html_sub_table($config, 'Clients',       [ @clients_fields ]);
    $rc .= "<P>\n";
    $rc .= &html_sub_table($config, 'Benchmark Configuration', 
			    [ @bm_config_fields ]);
    $rc .= "</TD></TR>\n</TABLE>\n";
    $rc .= "<P><HR><P>\n<H2>Notes</H2>\n";

    # Output all of the notes into one big string
    for my $info ( @notes_fields ) {
	my ($name, $key) = @$info;
	my $val = $config->{$key};
	my @vals = ();
	next if $val eq '' || $val eq '--' || (ref($val) eq 'ARRAY' && @$val == 0);
	if (ref($val) eq 'ARRAY') {
	    for (@$val) {
		push (@vals, split("\n", &html_escape($_)));
	    }
	} else {
	    @vals = split("\n", &html_escape($val));
	}
	if (@vals) {
	    $rc .= "<H3>$name</H3>\n<UL>\n";
	    for (@vals) {
		$rc .= "  <LI>".&html_escape($_)."</LI>\n";
	    }
	    $rc .= "</UL>\n";
	}
    }
    $rc .= "<P><HR><P>\n<H2>Test Run Details</H2>\n";
    $rc .= &html_testrun_tables($config);

    # And a footer
    my $date=date();
    $rc .= <<EOT;
<P><HR><P>
$date
</BODY>
</HTML>
EOT

    return $rc;
}

############################################################################
# Postscript Output

sub write_ps {
    my ($config) = @_;

    my $output = SPEC_report($config, 'PS');

    # Make the raw file into a big comment
    my $raw = html_escape(write_raw($config));
    chomp($raw); chomp($raw);
    $raw =~ s/\n/\n%% /g;
    $raw = "%% $raw\n";

    return "$output$raw";
}

############################################################################
# PDF Output

sub write_pdf {
    my ($config) = @_;

    my $output = SPEC_report($config, 'PDF');

    # Make the raw file into a big comment
    my $raw = html_escape(write_raw($config));
    chomp($raw); chomp($raw);
    $raw =~ s/\n/\n%% /g;
    $raw = "%% $raw\n";

    return "$output$raw";
}

############################################################################
# PS/PDF Formatting routines

sub SPEC_report($$) {
    my ($config, $mode) = @_;
    my ($width, $height, $hsize);

    $mode = 'PS' if $mode eq '';
    my $pspdf = new PSPDF $mode;

    $pspdf->set_fontdir ("$ENV{'SPEC'}/bin/fonts") if exists $ENV{'SPEC'};

    $pspdf->Creator('t');
    $pspdf->Author ('Christopher Chan-Nui');
    $pspdf->Title  ('Here we go');
    $pspdf->Subject('A brief history of time');

    $pspdf->open();

    ($width, $height) = SPEC_newpage($pspdf, $config);

    use vars qw($inch);
    $inch = 72;

# Draw the Title Bar
    $hsize = 0.75 * $inch;
    SPEC_title($pspdf, 0, $height-$hsize, $width, $hsize, $config);
    $height -= $hsize;

# Draw the Info Bar
    $hsize = 0.125 * $inch;
    SPEC_infobar($pspdf, 0, $height-$hsize, $width, $hsize, $config);
    $height -= $hsize;

# Draw the Detail boxes
    $hsize = SPEC_detail($pspdf, 0, $height, $width, $height, $config);
    $height -= $hsize;

    my $border = 10;
    my $indent = 15+$border;
    $hsize = 20;
    $pspdf->set_font('Times-Bold', 16);
    $pspdf->set_text_pos($width/2, $height-$hsize);
    $pspdf->show_center('Notes/Tuning Information');
    $height -= $hsize;

    for my $notestype (@notes_fields) {
	my ($name, $key) = @$notestype;
	my $val = $config->{$key};
	next if $val eq '' || $val eq '--' || (ref($val) eq 'ARRAY' && @$val == 0);

	my $num = 1;
	$num = @$val if ref($val) eq 'ARRAY';
	if ($num*10+12+2 > $height && $height < 3*10+12+2) {
	    $pspdf->grestore();
	    $pspdf->end_page();

	    ($width, $height) = SPEC_newpage($pspdf, $config);

	    $pspdf->set_font('Times-Bold', 16);
	    $height -= 20;
	    $pspdf->set_text_pos($width/2, $height);
	    $pspdf->show_center('Notes/Tuning Information (Continued)');
	    $height -= 10;
	}

	$hsize = 12;
	$pspdf->set_font('Times-Bold', $hsize);
	$pspdf->set_text_pos($border, $height-$hsize);
	$pspdf->show($name);
	$height -= $hsize+2; # Fudge it a little
	$hsize = 10;
	$pspdf->set_font('Times-Roman', $hsize);
	if (ref($val) eq 'ARRAY') {
	    for my $subval (@$val) {
		$height -= $hsize;
		if ($height < 0) {
		    $pspdf->grestore();
		    $pspdf->end_page();

		    ($width, $height) = SPEC_newpage($pspdf, $config);

		    $pspdf->set_font('Times-Bold', 16);
		    $height -= 20;
		    $pspdf->set_text_pos($width/2, $height);
		    $pspdf->show_center('Notes/Tuning Information (Continued)');
		    $height -= 10;

		    $hsize = 12;
		    $pspdf->set_font('Times-Bold', $hsize);
		    $pspdf->set_text_pos($border, $height-$hsize);
		    $pspdf->show("$name (Continued)");
		    $height -= $hsize+2; # Fudge it a little
		    $hsize = 10;
		    $pspdf->set_font('Times-Roman', $hsize);
		    $height -= $hsize;
		}
		my $rc = $pspdf->string_fit($subval, $indent, $height, 
			$width-$indent-$border, $hsize, 
			$min_font_size, "Your '$key' is too long!\n");
		return 0 if $rc == 0;
	    }
	} else {
	    $height -= $hsize;
	    my $rc = $pspdf->string_fit($val, $indent, $height, 
		    $width-$indent-$border, $hsize, 
		    $min_font_size, "Your '$key' is too long!\n");
	    return 0 if $rc == 0;
	}
	$height -= $hsize;
    }

    $pspdf->grestore();
    $pspdf->end_page();
    $pspdf->close();

    return $pspdf->output();
}

sub SPEC_newpage($$) {
    my ($p, $config) = @_;
    my (@errors)  = @{$config->{'errors'}} if $config->{'errors'};

    $p->begin_page('letter');
    $p->set_font('Times-Roman', 18);

    my $width  = $p->{'width'};
    my $height = $p->{'height'};

    if (@errors) {
	$p->gsave();
	$p->setgray(0.80);
	$p->rotate(50);
	my $width = sqrt($width*$width+$height*$height)*0.75;
	my $rc = $p->string_fit('Invalid Run', $width*0.25, 0, $width, 144);
	$p->grestore();
    }

    $p->gsave();
# We need a margin, let's use 0.5 inch
    my $inch = 72;
    my $border = 0.5 * $inch;
    $height -= $border * 2;
    $width  -= $border * 2;
    $p->translate($border,$border);

# Draw a box for the page
    $p->rect(0,0, $width,$height);
    $p->stroke();

# Draw the advertisement at the bottom
    my $hsize = 0.5 * $inch;
    SPEC_advertisement($p, 0, 0, $width, $hsize);
    $height -= $hsize;
    $p->translate(0,$hsize);

# Draw the Banner Bar
    $hsize = 0.5 * $inch;
    SPEC_banner($p, 0, $height-$hsize, $width, $hsize, $config);
    $height -= $hsize;

    return ($width, $height);
}

sub SPEC_logo ($$$$$) {
    my ($p, $x, $y, $width, $height) = @_;
    my $fontsize=72;

    return 0; # No logo for normal users

    $p->gsave();
	$p->rect(0,0, $width,$height);
	$p->stroke();
	my $border = (($width < $height)?$width:$height) * 0.05;
	$p->translate($x + $border, $y + $border);
	$width -= $border * 2;
	$height -= $border * 2;

        # Find out what the correct scale for the text is
	my $i = 0.5;
	$p->set_font('Times-Bold', $fontsize);
	my $w = $p->stringwidth('spec');
	$fontsize *= ($width / $w);
	$fontsize = $height * 0.3 if $fontsize > $height * 0.3;

        # Print text for bottom
	$p->set_font('Times-Bold', $fontsize);
	my $XHeight = $p->string_XHeight() * 1.1;
	my $Descender = $p->string_Descender() * 1.1;
	$p->set_text_pos($width/2, $Descender /4);
	$p->show_center('spec');

        # Print Grid
	my $gridsize = $height - $fontsize;
	$gridsize = $width if $width < $gridsize;
	$p->translate(($width-$gridsize)/2, $XHeight + $Descender /2);

	for ($i = 0; $i <= 4; $i++) {
	    $p->moveto ($i/4 * $gridsize,                0);
	    $p->lineto ($i/4 * $gridsize,        $gridsize);
	    $p->moveto (               0, $i/4 * $gridsize);
	    $p->lineto (       $gridsize, $i/4 * $gridsize);
	}
	$p->stroke();
	for ($i = 11; $i <= 11; $i++)  {
	    $p->gsave();
	    my $x1 = $i/16 * $gridsize;
	    my $y1 =  4/16 * $gridsize;
	    my $x2 = 12/16 * $gridsize; 
	    my $y2 = (16-$i)/16 * $gridsize;
	    $p->grestore();
	    $p->moveto ( 3/16 * $gridsize,  3/16 * $gridsize);
	    $p->curveto( $x1,$y1, $x2,$y2,
			13/16 * $gridsize, 13/16 * $gridsize);
	    $p->stroke();
	}
    $p->grestore();
    return 1;
}

sub SPEC_banner ($$$$$$) {
    my ($p, $x, $y, $width, $height, $config) = @_;
    my $bigfontsize = $height * 0.8;
    my $fontsize = $height * 0.2;
    my $copyright_char = "\343";
    my $copyright_msg  = 
	    ' Copyright 1999, Standard Performance Evaluation Corporation';
    $p->gsave();
	$p->translate($x,$y);
	$p->rect(0,0, $width,$height);
	$p->stroke();

	SPEC_logo($p, 0, 0, $height, $height);

	$p->set_font('Times-Bold', $bigfontsize);
	$p->set_text_pos($width/2, $height-$bigfontsize*0.9);
	$p->show_center("$config->{'metric'} Result");

	$p->set_font("Symbol", $fontsize);
	my $w = $p->stringwidth($copyright_char);
	$p->set_font('Times-Roman', $fontsize);
	$w += $p->stringwidth ($copyright_msg);
	$p->set_text_pos(($width - $w)/2, $height-$bigfontsize*0.9 - $fontsize);
	$p->set_font("Symbol", $fontsize);
	$p->show($copyright_char);
	$p->set_font ('Times-Roman', $fontsize);
	$p->show($copyright_msg);
    $p->grestore();
    return 1;
}

sub SPEC_advertisement ($$$$$) {
    my ($p, $x, $y, $width, $height) = @_;
    $p->gsave();
	$p->translate($x,$y);
	$p->rect_fill(0,0, $width,$height, 0.9);

	my $font_size = $height / 4;
	$p->set_font("Times-Roman", $font_size);

	my $margin = $height/8;

	$x = $margin;
	$y = $height - $margin - $font_size;

	$p->set_text_pos($x, $y);

	$p->show('For more');
	$p->continue_text('Information');
	$p->continue_text('Contact');

	$p->set_text_pos($width/2, $y);
	$p->show_center('SPEC');
	$p->continue_text_center('10754 Ambassador Drive, Suite 201');
	$p->continue_text_center('Manassas, VA   20109-2626');

	$p->set_text_pos($width - $margin, $y);
	$p->show_right('(703) 331-0180');
	$p->continue_text_right('info@spec.org');
	$p->continue_text_right('http://www.spec.org');

    $p->grestore();
    return 1;
}

sub SPEC_infobar ($$$$$$) {
    my ($p, $x, $y, $width, $height, $config) = @_;

    $p->gsave();
	$p->translate($x,$y);
	$p->rect(0,0, $width,$height);
	$p->stroke();
	my $border = (($width<$height)?$width:$height)*0.1;
	$p->set_font("Times-Roman", $height - $border*2);
	my $descender = $p->string_Descender()*0.8;
	my $yborder = $border;
	$yborder = $descender if $descender > $yborder;
	my $xpos = 0;
	for my $info (  [ 'Hardware Avail:'     , 'hw_avail'       , 0.21 ],
			[ 'OS Avail:'           , 'os_avail'       , 0.21 ],
			[ 'HTTP Software Avail:', 'http_avail'     , 0.21 ],
			[ 'Sup. Software Avail:', 'software_avail' , 0.21 ],
			[ 'SPEC license #:'     , 'license_num'    , 0.16 ],
		   ) {
	    my ($name, $key, $relwidth) = @$info;
	    my $mywidth = $relwidth * $width;
	    $p->rect($xpos, 0, $mywidth, $height);
	    $p->stroke();
	    $p->set_text_pos($xpos+$border, $yborder);
	    $p->show($name);
	    $p->set_text_pos($xpos+$mywidth-$border, $yborder);
	    $p->show_right(firstof($config->{$key}));
	    $xpos += $mywidth;
	}
    $p->grestore();
    return 1;
}

# Given the upper left corner
sub SPEC_info_box ($$$$$$$$) {
    my ($p, $x, $y, $width, $height, $config, $list, $size) = @_;
    my $namewidth = 0;
    $p->set_font("Times-Roman", $size*1.2);

    # Do a quick run through the items to find the longest tag string
    for my $item (@$list) {
	my $reftype = ref($item);
	if ($reftype eq 'ARRAY') {
	    my ($name, $key) = @$item;
	    my $w = $p->stringwidth("$name:", $size);
	    $namewidth = $w if $w > $namewidth;
	}
    }

    my $valpos = $namewidth + $width / 10;

    my $height = 0;
    for my $item (@$list) {
	my $reftype = ref($item);
	if ($reftype eq 'ARRAY') {
	    my ($name, $key) = @$item;
	    $height += $size;
	    $p->set_font("Times-Roman", $size);
	    $p->set_text_pos($x, $y - $height);
	    $p->show("$name:");

	    my $val = $config->{$key};
	    if (ref($val) eq 'ARRAY') {
		my $mysize = $size;
		for my $subval (@$val) {
		    my $rc = $p->string_calcsize($subval, $width-$valpos, $mysize, 
			    $min_font_size, "Your '$key' is too long!\n");
		    return 0 if $rc == 0;
		    $mysize = $rc if $rc < $mysize;
		}
		$p->set_font("Times-Roman", $mysize);
		for my $subval (@$val) {
		    $p->set_text_pos($x + $valpos, $y - $height);
		    $p->show($subval);
		    $height += $mysize;
		}
		$height -= $mysize if @$val;
	    } else {
		my $rc = $p->string_fit($val, $x+$valpos, $y-$height, 
			$width-$valpos, $size, 
			$min_font_size, "Your '$key' is too long!\n");
		return 0 if $rc == 0;
	    }
	} elsif ($reftype eq '') {
	    $p->set_font("Times-Bold", $size * 1.2);
	    $height += $size * 1.2;
	    $p->set_text_pos($x + $width / 2, $y - $height);
	    $height += $size * .2; # Nudge it down a little
	    $p->show_center($item);
	} else {
	    die "SPEC_info_length: list contains bogus element ref type!\n";
	}
    }
    return $height;
}
# This is an odd one, the Y position is the TOP of the box, not the bottom
# as with all of the others, and it returns 0 on error, otherwise it 
# returns the length (height) used.
sub SPEC_detail ($$$$$$) {
    my ($p, $x, $y, $width, $height, $config) = @_;
    my ($str, $size, $len);
    my $fontsize = 10;
    $p->gsave();
	$p->set_font("Times-Roman", 10);
	my $border = 10;

        my $split = $width * .45;
	my $left_len = SPEC_info_box ($p, $x+$border, $y, 
		$split-$border*2, $height, $config, [
			'Hardware',      @hardware_fields,
		    '', 'Software',      @software_fields,
		    '', 'HTTP Software', @http_software_fields,
		], $fontsize);
	return 0 if $left_len == 0;
	my $right_len = SPEC_info_box ($p, $x+$split+$border, $y, 
		$width-$split-$border*2, $height, $config, [
			'Test Sponsor', @test_sponser_fields,
		    '', 'Network',      @network_fields,
		    '', 'Clients',      @clients_fields,
		], $fontsize);
	return 0 if $right_len == 0;
	$len = ($left_len>$right_len)?$left_len:$right_len;
	$len += $border;

	$p->rect($x, $y-$len, $width, $len);
	$p->rect($x, $y-$len, $split, $len);
	$p->stroke();

	if ($len > $height) {
	    warn "Too many lines in the detail boxes!\n";
	    return 0;
	}
    $p->grestore();
    return $len;
}

sub SPEC_title_right ($$$$$$) {
    my ($p, $x, $y, $width, $height, $config) = @_;
    my ($str, $size);
    $p->gsave();
	$p->translate($x,$y);
	$p->rect(0,0, $width,$height);
	$p->stroke();

	my $border = (($width<$height)?$width:$height)*0.1;
	$p->translate ($border, $border);
	$width  -= $border * 2;
	$height -= $border * 2;

	$p->set_font("Times-Roman", 10);

	$str = "$config->{'metric'} = ";
	$size = $p->string_fit($str, 0, $height/2+$p->string_Descender($height/2)*0.8, $width, $height/2.5 );
	return 0 if ($size <= 0);

	$str = $config->{'median_results'};
	$size = $p->string_fit_right($str, $width, $height/2+$p->string_Descender($height/2)*0.8, $width, $height/2.5, 
	    $min_font_size, "Your median_results is too long!\n");
	return 0 if ($size <= 0);

	$size = $height/4;
	$p->set_font("Times-Roman", $size);
	$p->set_text_pos(0, $height/4);
	$p->show("Test Date:");
	$p->continue_text("Tested By:");

	$str = firstof($config->{'test_date'});
	$size = $p->string_fit_right($str, $width, $height/4, $width, $height/4,
	    $min_font_size, "Your test_date is too long!\n");
	return 0 if ($size <= 0);

	$str = firstof($config->{'tester_name'});
	$size = $p->string_fit_right($str, $width, 0, $width, $height/4, 
	    $min_font_size, "Your tester_name is too long!\n");
	return 0 if ($size <= 0);
    $p->grestore();
    return 1;
}

sub firstof($) {
    my ($val) = @_;
    $val = $val->[0] if (ref($val) eq 'ARRAY');
    return $val;
}
sub SPEC_title_left ($$$$$$) {
    my ($p, $x, $y, $width, $height, $config) = @_;

    $p->gsave();
	$p->translate($x,$y);
	$p->rect(0,0, $width,$height);
	$p->stroke();

	$p->set_font("Times-Roman", 10);

	my $border = (($width<$height)?$width:$height)*0.1;
	$width  -= $border * 2;
	$height -= $border * 2;
	$p->translate ($border, $border + $p->string_Descender($height/2)*0.8);

	my $str = firstof($config->{'hw_vendor'}).': '.
		  firstof($config->{'hw_model'});
	my $size = $p->string_fit($str, 0, $height/2, $width, $height/2, 
	    $min_font_size,
	    "Your combined hw_vendor and hw_model line is too long!\n");
	return 0 if ($size <= 0);

	my $str = firstof($config->{'sw_vendor'}).': '.
		  firstof($config->{'sw_http'});
	$size = $p->string_fit($str, 0, 0, $width, $height/2, $min_font_size,
	    "Your combined sw_vendor and sw_http line is too long!\n");
	return 0 if ($size <= 0);
    $p->grestore();
    return 1;
}
sub SPEC_title ($$$$$$) {
    my ($p, $x, $y, $width, $height, $config) = @_;
    my $rc = 0;
    $rc = SPEC_title_left ($p, $x,            $y, $width*0.7, $height, $config);
    return 0 if $rc == 0;
    $rc = SPEC_title_right($p, $x+$width*0.7, $y, $width*0.3, $height, $config);
    return $rc;
}

sub do_system {
    package X;
    my ($cmd, $command, $host, $user, $dir) = @_;
    my ($pid, $process, $rc);
    my ($stdout, $stderr, $old_sigchld);

    use constant DS => '$';

    $cmd =~ s/\\/\\\\/g;
    $cmd =~ s/\@/\\@/g;
    $cmd =~ s/\%/\\%/g;
    $cmd =~ s/\$\\/\@{[DS]}\\/g;
    $cmd = eval "qq%$cmd%";

    if ($stdout ne "") {
        open (SAVEOUT, ">&STDOUT") || die "Can't redirect stdout: $!\n";
        open (STDOUT, ">>$stdout") || die "Can't redirect stdout: $!\n";
    }
    if ($stderr ne "") {
        open (SAVEERR, ">&STDERR");
        if ($stderr eq $stdout) {
            open (STDERR, ">&STDOUT") || die "Can't redirect stderr: $!\n";
        } else {
            open (STDERR, ">>$stderr") || die "Can't redirect stderr: $!\n";
        }
    }
    $old_sigchld = $SIG{'CHLD'};
    $SIG{'CHLD'} = 'DEFAULT';

    eval "system $cmd";
    $rc = $?;
    $SIG{'CHLD'} = $old_sigchld;

    if ($stdout ne "") {
        open (STDOUT, ">&SAVEOUT");
        close (SAVEOUT);
    }
    if ($stderr ne "") {
        open (STDERR, ">&SAVEERR");
        close (SAVEERR);
    }
    return $rc;
}
