#!/usr/bin/perl
use warnings;
use strict;
use forks;
#use threads;
 
##
# Startup definitions
##
unless ( defined( $ARGV[0] ) ) {
	print "st.pl macrofile [concurrent] [total] [sleep] [verbosity] [chartfile]\n";
	print "\n";
	print "		macrofile  - The file contain telnet commands\n";
	print "		concurrent - amount of threads to run conncurrently (default = 1)\n";
	print "		total	   - Total amount of runs to complete (default = 1)\n";
	print "		verbosity  - How much stuff to print out, 0-5 (default = 1)\n";
	print "		sleep	   - Max time to sleep before starting starting unit test\n";
	print "		chartfile  - To create a chart, specify a file name\n";
	print "\n";
	exit 1;
}
 
my $tmf = $ARGV[0];
unless ( -r $tmf) {
	print "Could not read macro file $tmf\n";
	exit 1;
}
 
###
# terminate and intterupt handler
###
sub sigterm() {
  if (threads->tid() == 0) {
    print "\nAbort request received\n";
    &summary();
  } else {
    threads->join();
  }
}
 
$SIG{TERM} = \&sigterm;
$SIG{INT} = \&sigterm;
 
my $concur = (defined($ARGV[1])) ? $ARGV[1] : 1;
my $runtotal = (defined($ARGV[2])) ? $ARGV[2] : 1;
my $debug = (defined($ARGV[4])) ? $ARGV[4] : 1;
my $swait = (defined($ARGV[3])) ? $ARGV[3] : 1;
my $chartname = (defined($ARGV[5])) ? $ARGV[5] : undef;
 
#if (defined($chartname) && ! -w $chartname) {
#  print "warning: could not write to file $chartname\n";
#  $chartname = undef;
#}
 
my $timeout = 180;
 
my $ctlchars = {
	'\^E'	=> "\x05",
	'\^X'	=> "\x18",
	'\^n'	=> "\x0a",
	'\^s'	=> "\x20",		# spacebar
	'%F4'	=> "\x1b\x4f\x53",
	'%F11%'	=> "\x1b\x5b\x32\x33\x7e"
};
 
##
# Load our macro file
##
print "$tmf $concur $runtotal\n" if ($debug>2);
my @repeats = (); 
my @cmds = ( );
{
  my $incmd = 0;
  open (FH, "<$tmf");
  while (<FH>) {
	  chop;
	  if ($_ =~ /^#/) {
		  # Comment
		  next;
	  } elsif ($_ =~ /^$/) {
		  # Blank line
		  next;
	  } elsif ($_ =~ /\.\.\./) {
		  $incmd = 1;
	  } elsif ($incmd == 1) {
		  push (@cmds, $_);
	  } elsif ($incmd == 0 && $_ =~ /^HOST.*$/) {
		  my $parms = { };
		  foreach my $pairs (split(",", $_)) {
			  my ($name, $value) = split("=", $pairs);
			  $parms->{$name} = $value;
		  } 
		  push(@repeats, $parms);
	  }
  } 
  close (FH);
}
##
# Start our scheduling
##
 
my @data = ();
my $runstart = time();
 
{
  my $replen = $#repeats;
  my $repidx = 0;
  my $seq = 0;
 
  while (1) {
	  my $donework = 0;
	  my @thrlist = threads->list(threads::running);
	  my $thrcount = $#thrlist +1;
	  # Create a new thread if the time is right
	  if ( $thrcount < $concur && $seq < $runtotal ) {
		  $seq++;
		  my $parm = $repeats[$repidx];
		  my $thr = threads->create({'exit' => 'threads_only'}, \&unittest, \@cmds, $parm, $seq, $thrcount +1 );
		  my $tid = $thr->tid();
		  print "thread: $seq $thrcount tid: $tid repidx $repidx\n" if ($debug > 1);
		  # get the next repeat entry
		  $repidx++;
		  $repidx = 0 if ($repidx > $replen);
		  $donework = 1;
		  next;
	  }
 
	  # Get data back from our threads
	  my @jlist = threads->list(threads::joinable);
	  foreach (@jlist) {
		  my $rd = $_->join();
		  if (defined($rd)) {
		    push (@data, $rd);
		    print "$rd->{'msg'}\n" if ($debug > 0);
		    $donework = 1;
		  }
	  }
 
	  # If all our threads are finished, and have been joined
	  #   and we have run all sequences, then terminate
	  @thrlist = threads->list(threads::running);
	  $thrcount = $#thrlist + 1;
	  last if ($thrcount == 0 && $seq >= $runtotal && $#jlist == -1);
 
	  # Give other procs runtime
	  #select(undef, undef, undef, 0.1); # unless ($donework==1) ;
  }
}
 
# Some time to clean up the threads
sleep 1;
&summary();
 
###
# Print out our average
###
sub summary() {
 
  if ($#data <= 0) {
    print "Not enough data collected\n";
    return;
  }
 
  my $sum = 0;
  my $count = 0;
  foreach (@data) {
    $sum += $_->{'dur'};
    $count++;
  }
  my $avg = ($sum / $count);
  print "Total Sequences: $count Average Duration: $avg\n";
 
  &chart($avg) if (defined($chartname) && $#data >= 0);
}
 
sub chart() {
  my ($avg) = @_;
 
  use Chart::Clicker;
  use Chart::Clicker::Decoration::Legend::Tabular;
  use Chart::Clicker::Renderer::Bar;
  use Chart::Clicker::Context;
  use Chart::Clicker::Data::DataSet;
  use Chart::Clicker::Data::Series;
 
  # Sort our array of hashes
  my @sorted = sort { $a->{seq} <=> $b->{seq} } @data;
 
  my $cc = Chart::Clicker->new(width => 900, height => 400, format => 'png');
 
  $cc->title->text("Stress Test Results ($concur/$runtotal,$swait)");
  $cc->title->font->size(14);
 
  # Duration (duration/sequence)
  my $ser1 = createSeries(\@sorted, 'seq', 'dur', 'Duration');
  my $ds1 = Chart::Clicker::Data::DataSet->new(series => [ $ser1 ]);
  #my $ctx1 = Chart::Clicker::Context->new(name => 'Duration');
  #$ctx1->range_axis->label('Duration');
  #$ctx1->domain_axis->label('Sequence');
  #$ds1->context('Duration');
  #$cc->add_to_contexts($ctx1)
 
  # Start Time (start/sequence)
  my $ser2 = createSeries(\@sorted, 'seq', 'delta', 'Start Time');
  my $ser3 = createSeries(\@sorted, 'seq', 'procs', 'Proc Count');
  my $ds2 = Chart::Clicker::Data::DataSet->new(series => [ $ser2, $ser3 ]);
  my $ctx2 = Chart::Clicker::Context->new(name => 'Count');
  $ctx2->range_axis->label('Count');
  $ctx2->domain_axis->label('Sequence_#');
  $ds2->context('Count');
  $cc->add_to_contexts($ctx2);
 
 
  $cc->add_to_datasets($ds1);
  $cc->add_to_datasets($ds2);
 
  my $defctx = $cc->get_context('default');
  $defctx->range_axis->label('Duration_secs)');
  $defctx->domain_axis->label('Sequence_#');
  $defctx->domain_axis->tick_label_angle(0.785398163);
  $defctx->renderer->brush->width(1);
 
 
  # Procs (procs/sequence)
 
  #my $ds3 = Chart::Clicker::Data::DataSet->new(series => [ $ser3 ]);
  #my $ctx3 = Chart::Clicker::Context->new(name => 'Procs');
  #$ctx3->range_axis->label('Procs');
  #$ctx3->domain_axis->label('Sequence');
  #$ds3->context('Procs');
  #$cc->add_to_contexts($ctx3);
  #$cc->add_to_datasets($ds3);  
 
  #foreach (@sorted) {
  #  $ds_dur->add_data( 'Duration', $_->{'dur'} ); 
  #}
 
  #use List::Util qw(max min);
  #use Statistics::Basic qw(median mean);  
  #$cc->legend(Chart::Clicker::Decoration::Legend::Tabular->new(
  #    header => [ qw(Name Min Max Median Mean) ],
  #    data => [
	#  [ min(&hashv2arr(\@data, 'dur' ) )."", max(&hashv2arr(\@data, 'dur' ) )."", median(&hashv2arr(\@data, 'dur' ) )."", mean(&hashv2arr(\@data, 'dur' ) )."" ],
  #    ]
  #));  
 
 
  $cc->write_output($chartname);
  print "Chart $chartname written\n";
}
 
 
 
##
# This runs the actual test
##
sub unittest() {
	my ($cmds, $parms, $seq, $num) = @_;
	if ($seq < $concur) {
		my $srand = int(rand($swait));
		print "sleep $seq $num $srand\n" if ($debug > 1); 
		#select(undef, undef, undef, $srand);
	}
	my $data = { };
 
	my $start = time();
	$data->{'start'} = $start;
	$data->{'delta'} = $start - $runstart;
	$data->{'seq'} = $seq;
	$data->{'num'} = $num;
	my @thrlist = threads->list(threads::running);
	$data->{'procs'} = $#thrlist +1;
	$data->{'msg'} = "$start $seq $num ";
 
	use Net::Telnet ();
	my $t = new Net::Telnet( Timeout => $timeout, cmd_remove_mode => 1,);
 
 
	my $DH = $t->dump_log('dump/' . $parms->{'DUMP'} . "_$seq.log") if (defined($parms->{'DUMP'}));
	`mkdir 'dump/'` if (defined($parms->{'DUMP'}) && ! -d 'dump/') ;
	$t->timeout($parms->{'TIMEOUT'}) if (defined($parms->{'TIMEOUT'}));
	$t->open($parms->{'HOST'});
 
	$data->{'host'} = $parms->{'HOST'};
	$data->{'dump'} = (defined($parms->{'DUMP'})) ? $parms->{'DUMP'} : "";
 
	foreach (@$cmds) {
		# Replace parms in the cmd
		foreach my $p (keys(%$parms)) {
			$_ =~ s/\%$p\%/$parms->{$p}/g;
		}
 
		if ($_ =~ /^\@.*$/) { # Wait
			$_ =~ s/^@/ /g;
			my ($tok1, $tok2) = split('~', $_);
			print "$seq $num wait: $_ ($tok2)\n" if ($debug > 2);
			$t->waitfor(Timeout => $tok2, Match => $tok1);
		} elsif ($_ =~ /^\!.*$/) { # Delay
			$_ =~ s/^!/ /g;
			print "$seq $num delay: $_\n" if ($debug > 2);
			select (undef, undef, undef, $_);
		} else { # send keystroke
			# Replace control characters
			print "$seq $num cmd: $_\n" if ($debug > 2);
			foreach my $cc (keys(%$ctlchars)) {
				$_ =~ s/$cc/$ctlchars->{$cc}/g;
			}
			$t->put($_);
		}
	}
 
	my $end = time();
	my $dur = $end - $start;
	$data->{'msg'} .= " $dur";
 
	$data->{'end'} = $end;
	$data->{'dur'} = $dur;
 
	return $data;
}
 
sub stub() {
  my ($cmds, $parms, $seq, $num) = @_;
  my $data = { };
 
  $data->{'start'} = time();
  $data->{'seq'} = $seq;
  $data->{'num'} = $num;
 
  my $sleep = int(rand(5));
  sleep $sleep;
 
  $data->{'end'} = time();
  $data->{'dur'} = $data->{'end'} - $data->{'start'};
  $data->{'sleep'}= $sleep;
  $data->{'tid'} = threads->tid();
  $data->{'msg'} = "$seq $num $sleep $data->{'start'} $data->{'end'} $data->{'dur'}";
 
  return $data;
}
 
sub hashv2arr() {
  my ($arr, $hash) = @_;
  my @new = ();
  my @meh = @$arr;
  foreach (@$arr) {
    push (@new, $_->{$hash});
  }
  return @new;
}
 
sub createSeries() {
  my ($data, $key, $value, $name) = @_;
 
  my @keys = &hashv2arr($data, $key);
  my @values = &hashv2arr($data, $value);
  my $series = Chart::Clicker::Data::Series->new(
	keys => \@keys,
	values => \@values,
	name => $name
  );
 
  return $series;
}
 
#exit 0;
 
#foreach (@repeats) {
#	&unittest(\@cmds, $_);
#}
 
#print "Repeats\n---------------------------------\n";
#my $count = 0;
#foreach my $r (@repeats) {
#	print "$count: ";
#	foreach (keys(%$r)) {
#		print "$_=$r->{$_},";
#	}
#	print "\n";
#	$count++;
#}
#
#print "\n\nMacro\n---------------------------------\n";
#foreach (@cmds) {
#	print $_."\n";
#}
HOST=server,USER=newio,PASS=apassword,DUMP=dump
...
@/login[: ]*$/i
%USER%^n
@/password[: ]*$/i
%PASS%^n
@/%USER%\@%HOST%.*$/i
sitemenu.sh^n
!0.25
1^n
!5
^n
!1
^n
!1
1
!0.25
1
!0.25
1
!0.25
^E
!0.25
^E
!0.25