Logo Search packages:      
Sourcecode: faumachine version File versions  Download package

faum-gen-results.c

/* $Id: faum-gen-results.c,v 1.2 2009-02-16 10:20:43 sand Exp $ 
 * execute tests.
 *
 * Copyright (C) 2008-2009 FAUmachine Team <info@faumachine.org>.
 * This program is free software. You can redistribute it and/or modify it
 * under the terms of the GNU General Public License, either version 2 of
 * the License, or (at your option) any later version. See COPYING.
 */

#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <dirent.h>
#include <errno.h>
#include <stdbool.h>
#include <assert.h>
#include <limits.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <spawn.h>
#include <regex.h>
#include <stdarg.h>
#include <fcntl.h>

#define ARRAY_SIZE(t)   (sizeof t / sizeof(t[0]))
#define MIN(x, y) (x < y ? x : y)

/* temporary files will be called like this: */
#define TMP_FILE_NAME   "/tmp/faum-gen-results-XXXXXX"
/* maximum number of different experiments */
#define MAX_EXPERIMENTS 256
/* maximum number of different test runs */
#define MAX_TESTRUNS    1024
/* maximum number of screenshots to pick up */
#define MAX_SCREENSHOTS 20
/* maximum number of lines to pick up for messages */
#define MAX_MATCH_LINES 3

/* template definitions for html */
#include "templates/history_header.def"
#include "templates/history_entry.def"
#include "templates/history_footer.def"
#include "templates/summary_header.def"
#include "templates/summary_entry.def"
#include "templates/summary_footer.def"

/** type: result of one test run */
00051 struct exp_result_t {
      /** timestamp (YYYYMMDD-HHmm). also the directory name of the test 
       *  run. */
00054       char timestamp[sizeof("YYYYMMDD-HHmm")];
      /** run time of the test run in seconds) */
00056       int runtime;
      /** did it succeed? */
00058       bool success;
      /** name of screenshots (or empty string if none) */
00060       char screenshot[MAX_SCREENSHOTS][NAME_MAX];
      /** is the screenshot a png file (corresponds to above array )*/
00062       bool is_png[MAX_SCREENSHOTS];
      /** name of log files */
00064       char logs[5][NAME_MAX];
      /** important messages */
00066       char messages[4096];
};

/** history information about one experiment */
00070 struct exp_history_t {
      /** total number of test runs */
00072       unsigned int num_tests;
      /** total number of successful runs */
00074       unsigned int num_success;
      /** name of experiment */
00076       char exp_name[NAME_MAX];
      /* the following are only initialized, if num_tests > 0 */
      /** result of the current test run. */
00079       struct exp_result_t current_result;
      /** result of the last test run. */
00081       struct exp_result_t last_result;
      /** did the screenshots change? */
00083       bool shot_changed;
};

/** generic state */
struct cpssp {
      /** pid of the gnuplot process (0 for no subprocess active). */
00089       pid_t gnuplot_pid;
      /** file to send commands to gnuplot (write only) */
00091       FILE *gnuplot_out;
      /** temporary files, which need to be removed at the end */
00093       char tmp_files[MAX_EXPERIMENTS][sizeof(TMP_FILE_NAME)];
      /** number of tmp_files */
00095       unsigned int n_tmp_files;
      /** does the tmp file contain a dataset at index 0 matching a 
       *  successful test run? */
00098       bool is_tmp_f_succ[MAX_EXPERIMENTS];
};

/* generic info */
static struct cpssp _cpssp = {
      .gnuplot_pid = 0,
      .gnuplot_out = NULL,
      .n_tmp_files = 0,
};


/** read file contents from file in path to a newly allocated buffer and
 *  set len appropriately. If the function returns false, *buf and len
 *  are undefined.
 *
 *  @param path full path to filename
 *  @param buf *buf will be allocated and points to the contents.
 *  @param len will be set to the length of the file.
 *  @return false on error, otherwise true. 
 */
static bool
read_file_to_mem(const char *path, void **_buf, size_t *len)
{
      int fd;
      off_t fs;
      int ret;
      size_t cnt;
      size_t nr;
      char *buf;

      assert(_buf != NULL);
      assert(len != NULL);
      assert(path != NULL);
      
      fd = open(path, O_RDONLY);
      if (fd < 0) {
            fprintf(stderr, "%s: failed to open %s: %s\n", __FUNCTION__,
                  path, strerror(errno));
            return false;
      }

      fs = lseek(fd, 0, SEEK_END);
      if (fs < 0) {
            fprintf(stderr, "%s: seeking to EOF of %s failed: %s\n",
                  __FUNCTION__, path, strerror(errno));
            ret = close(fd);
            assert(ret == 0);
            return false;
      }

      *len = fs;
      fs = lseek(fd, 0, SEEK_SET);
      assert(fs >= 0);

      *_buf = malloc(*len);
      if (*_buf == NULL) {
            ret = close(fd);
            assert(ret == 0);
            return false;
      }
      buf = (char *)(*_buf);

      for (cnt = 0; cnt < *len; /* nothing */) {
            nr = read(fd, buf + cnt, *len - cnt);
            if (nr < 0) {
                  switch (errno) {
                  case EINTR:
                        continue;

                  case EAGAIN:
                  case EBADF:
                  case EISDIR:
                        assert(false);

                  default:
                        fprintf(stderr, "Error from reading %s: %s\n",
                              path, strerror(errno));
                        ret = close(fd);
                        assert(ret == 0);
                        free(buf);

                        return false;
                  }
            }
            cnt += nr;
      }

      ret = close(fd);
      assert(ret == 0);

      return true;
}

/** perform a binary diff on two files.
 *  @param p1 path to first file.
 *  @param p2 path to second file.
 *  @return true if the files differ, false if they don't or an error occured.
 */
static bool
binary_diff(const char *p1, const char *p2)
{
      void *c1;
      void *c2;
      size_t len1;
      size_t len2;
      bool ret;
      int r;

      ret = read_file_to_mem(p1, &c1, &len1);
      if (! ret) {
            return false;
      }

      ret = read_file_to_mem(p2, &c2, &len2);
      if (! ret) {
            free(c1);
            return false;

      }

      if (len1 != len2) {
            free(c1);
            free(c2);
            return true;
      }

      r = memcmp(c1, c2, len1);
      free(c1);
      free(c2);

      return r == 0;
}


/** launch gnuplot and create an output pipe to it.
 *  @param pid will get set to the gnuplot's pid on successful invocation.
 *  @return FILE pointer to the output channel, or NULL if launching gnuploat
 *          failed.
 */
static FILE *
launch_gnuplot(pid_t *pid)
{
      int ret;
      int fd[2];
      FILE *f;

      ret = pipe(fd);
      if (ret < 0) {
            fprintf(stderr, "%s: failed to create a pipe: %s\n",
                  __FUNCTION__, strerror(errno));
            return NULL;
      }

      *pid = fork();
      switch (*pid) {
      case 0: 
            /* child process */
            ret = close(0);
            assert(ret == 0);
            ret = dup(fd[0]);
            assert(ret == 0);

            ret = close(1);
            assert(ret == 0);
            ret = open("/dev/null", O_WRONLY);
            assert(ret == 1);

            ret = close(2);
            assert(ret == 0);
            ret = open("/dev/null", O_WRONLY);
            assert(ret == 2);

            ret = close(fd[1]);
            assert(ret == 0);

            execlp("gnuplot", "gnuplot", NULL);
            /* only reached on error */
            exit(EXIT_FAILURE);
            break;

      case -1:
            /* error */
            ret = close(fd[0]);
            assert(ret == 0);
            ret = close(fd[1]);
            assert(ret == 0);
            return NULL;
      
      default:
            /* parent */
            ret = close(fd[0]);
            assert(ret == 0);

            f = fdopen(fd[1], "w");
            if (f == NULL) {
                  fprintf(stderr, "%s: failed to fdopen: %s\n",
                        __FUNCTION__, strerror(errno));
                  ret = close(fd[1]);
                  assert(ret == 0);
            }

            return f;
      }
}

/** stop gnuplot (if launched) and close the associated pipe.
 *  @param pid pid of gnuplot (or 0 if not launched).
 *  @param f associated pipe.
 */
static void
stop_gnuplot(pid_t pid, FILE *f)
{
      int ret;
      pid_t p1;
      int status;

      if (f != NULL) {
            ret = fprintf(f, "quit;\n");
            /* don't care if writing works or not. */

            ret = fclose(f);
            assert(ret == 0);
      }

      if (pid > 0) {
            do {
                  p1 = waitpid(pid, &status, 0);
            } while (! WIFEXITED(status));
      }
}

/** start a subprocess performing the actual conversion.
 *  @param path path in which the screenshots reside.
 *  @param src existing source image (filename w.o. directory).
 *  @param target desired target image (filename w.o. directory).
 */
static void
launch_convert(const char *path, const char *src, const char *target)
{
      char *argv[4];
      char buf[3][PATH_MAX];
      int ret;
      pid_t child;
      pid_t pid;
      
      strncpy(buf[0], "/usr/bin/convert", sizeof(buf[0]));
      ret = snprintf(buf[1], sizeof(buf[1]), "%s/%s", path, src);
      assert(ret < sizeof(buf[1]));
      ret = snprintf(buf[2], sizeof(buf[2]), "%s/%s", path, target);
      assert(ret < sizeof(buf[2]));

      argv[0] = buf[0];
      argv[1] = buf[1];
      argv[2] = buf[2];
      argv[3] = NULL;

      ret = posix_spawn(&child, "/usr/bin/convert", NULL, NULL, argv,
                    NULL);
      if (ret < 0) {
            fprintf(stderr, "Could not spawn /usr/bin/convert: %s",
                  strerror(errno));
            return;
      }

      do {
            pid = waitpid(child, &ret, 0);
      } while (! WIFEXITED(ret));
}

/** convert a screenshot from ppm to png using the same basename.
 *  @param path relative base path to screenshot.
 *  @param shot name of the screenshot (w.o. directory part).
 *  @return name of resulting screenshot, static return.
 */
static const char *
convert_screenshot(const char *path, const char *shot)
{
      static char result[NAME_MAX];
      char *dot;
      int sz;

      dot = rindex(shot, '.');

      assert(dot != NULL);
      assert(dot - shot + 1 < sizeof(result));

      sz = snprintf(result, dot - shot + 1, "%s", shot);
      strncat(result, ".png", sizeof(result));

      launch_convert(path, shot, result);

      return result;
}

/** return a result representation of a history entry.
 *  @param entry history entry.
 *  @return string representation for html.
 */
static const char *
str_make_result(const struct exp_history_t *entry)
{
      if (entry->current_result.success) {
            return "<td><font color=\"lime\">success</font></td>";
      }

      /* not successful */
      if (entry->num_success == 0) {
            return "<td><font color=\"red\">failed</font></td>";
      }

      /* earlier, this test succeeded at least once -> warning */
      return "<td bgcolor=\"orange\">warning</td>";
}

/** return a color code string for a history entry.
 *  @param entry history entry.
 *  @return string representation for html.
 */
static const char *
str_make_color_code(const struct exp_history_t *entry)
{
      int delta;
      int rel_delta;

      if (entry->num_tests < 2) {
            /* new test, no color */
            return "<td></td>";
      }

      /* highest priority: success -> failed and failed -> success. */
      if (entry->current_result.success != entry->last_result.success) {
            if (entry->current_result.success) {
                  /* failed -> success */
                  return "<td bgcolor=\"lime\"></td>";
            }
            /* success -> failed */
            return "<td bgcolor=\"red\"></td>";
      }

      /* check if screenshots changed. */
      if (entry->shot_changed) {
            return "<td bgcolor=\"aqua\"></td>";
      }

      /* can we do a check on runtime changes in the first place? */
      if (   (entry->current_result.runtime <= 0) 
          || (entry->last_result.runtime <= 0)) {

          return "<td></td>";
      }

      /* FIXME eventually: the old version seems to only care for 
       *       successful experiments. enable the next bit to make
       *       this behave the same way.
       */
#if 0 /* see above */
      if (! (entry->current_result.success && entry->last_result.success)) {
            return "<td></td>";
      }
#endif
      /* check, if runtime changed? */
      delta = entry->current_result.runtime - entry->last_result.runtime;
      rel_delta = (delta * 100) / entry->current_result.runtime;

      if (rel_delta >= 10) {
            /* at least 10% worse than last run */
            return "<td bgcolor=\"maroon\"></td>";
      }

      if (rel_delta <= -10) {
            /* at least 10% faster than last run */
            return "<td bgcolor=\"green\"></td>";
      }

      return "<td></td>";
}

/** format a YYYYMMDD-HHMM to a human readable date.
 *  @param timeformat time in aforementioned format.
 *  @return human readable format.
 */
static const char *
str_make_datetime(const char *timeformat)
{
      static char buf[5][sizeof("dd.mm.yyyy, hh:mm")];
      static unsigned int bi = 0;

      int ret;
      int year;
      int month;
      int day;
      int hour;
      int minute;

      if (bi >= ARRAY_SIZE(buf)) {
            bi = 0;
      }

      assert(strlen(timeformat) == strlen("yyyymmdd-hhmm"));
      ret = sscanf(timeformat, "%04d%02d%02d-%02d%02d", &year, &month, &day, 
            &hour, &minute);
      assert(ret == 5);
      ret = snprintf(buf[bi], sizeof(buf[bi]), "%02d.%02d.%04d, %02d:%02d",
            day, month, year, hour, minute);
      assert(ret < sizeof(buf[bi]));

      bi++;
      return buf[bi - 1];
}

/** return a string representation of the runtime.
 *  @param runtime runtime in seconds.
 *  @return string representation.
 */
static const char *
str_runtime(int runtime)
{
      static char ret[20];

      if (runtime < 0) {
            return "n.a.";
      }

      snprintf(ret, sizeof(ret), "%dh %dm %ds", runtime / 3600, 
            (runtime / 60) % 60, runtime % 60);

      return ret;
}

/** make a link from a path with target as link name.
 *  Must not be called more than 5 times concurrently, otherwise
 *  a previous result may get overwritten.
 *  @param link_name name of the link, or '\0' to return the empty string.
 *  @param fmt format sting of the link, followed by needed format params.
 *         fmt should result in the path component.
 *  @return html link to file, static return.
 */
static const char *
str_make_link(const char *link_name, const char *fmt, ...)
{
      static char ret[5][PATH_MAX + 1024];
      static char buf[PATH_MAX];
      static unsigned int dim = 0;
      va_list args;
      int sz;

      if (dim > 4) {
            dim = 0;
      }

      if (*link_name == '\0') {
            ret[dim][0] = '\0';
            dim++;
            return ret[dim - 1];
      }

      va_start(args, fmt);
      sz = vsnprintf(buf, sizeof(buf), fmt, args);
      va_end(args);
      assert(sz < sizeof(buf));

      sz = snprintf(ret[dim], sizeof(ret[dim]), 
            "<a href=\"%s\">%s</a>", buf, link_name);
      assert(sz < sizeof(ret[dim]));
      dim++;
      return ret[dim - 1];
}


/** make links for an array of strings.
 *  @param prefix relative path prefix to prepend to each string.
 *  @param t_arr array of strings, which represent both the file target
 *         and the name of the link. empty strings will be skipped.
 *  @param t_arr_sz array size of t_arr.
 *  @param t_arr_elem_sz element size of array element.
 *  @param dest destination buffer.
 *  @param sz size of destination buffer.
 *  @return destination buffer dest.
 */
static const char *
str_make_file_links(
      const char *prefix,
      const char *t_arr,
      size_t t_arr_sz,
      size_t t_arr_elem_sz,
      char *dest,
      int sz
)
{
      char *d = dest;
      bool have_entry = false;
      int r;
      int i;

      for (i = 0; i < t_arr_sz; i++, t_arr += t_arr_elem_sz) {
            if (*t_arr == '\0') {
                  continue;
            }

            if (have_entry) {
                  r = snprintf(d, sz, "\n<br>\n");
                  sz -= r;
                  d += r;
                  assert(sz > 0);
            }

            have_entry = true;
            r = snprintf(d, sz, "<a href=\"%s/%s\">%s</a>", prefix,
                  t_arr, t_arr);
            sz -= r;
            d += r;
            assert(sz > 0);
      }

      if (! have_entry) {
            dest[0] = '\0';
      }

      return dest;
}

/** return a string representation of success/failed value.
 *  @param success did the test run succeed?
 *  @return string representation.
 */
static const char *
str_success(bool success)
{
      if (success) {
            return "<font color=\"lime\">success</font>";
      }

      return "<font color=\"red\">failed</font>";
}

/** generate html file for history.
 *  @param path path to html output file.
 *  @param experiment name of executed experiment.
 *  @param num_results number of results.
 *  @param results array containing the results.
 */
static void
html_gen_history(
      const char *path,
      const char *experiment,
      unsigned int num_results, 
      const struct exp_result_t *results
)
{
      FILE *f;
      int ret;
      unsigned int i;
      char buf1[PATH_MAX + 1024];
      char buf2[PATH_MAX + 1024];

      f = fopen(path, "w");
      if (f == NULL) {
            fprintf(stderr, "Could not open %s for writing: %s\n",
                  path, strerror(errno));
            return;
      }

      ret = fprintf(f, FORMAT_history_header, experiment, experiment);
      if (ret < 0) {
            fprintf(stderr, "Could not write to %s: %s\n", path,
                  strerror(errno));
            ret = fclose(f);
            assert(ret == 0);
            return;
      }

      for (i = 0; i < num_results; i++, results++) {
            ret = fprintf(f, FORMAT_history_entry, 
                  str_make_datetime(results->timestamp),
                  str_runtime(results->runtime),
                  str_success(results->success),
                  results->messages,
                  str_make_file_links(
                        results->timestamp, 
                        results->logs[0],
                        ARRAY_SIZE(results->logs),
                        sizeof(results->logs[0]),
                        buf1, 
                        sizeof(buf1)),
                  str_make_file_links(
                        results->timestamp,
                        results->screenshot[0],
                        ARRAY_SIZE(results->screenshot),
                        sizeof(results->screenshot[0]),
                        buf2,
                        sizeof(buf2))
                  );
            if (ret < 0) {
                  fprintf(stderr, "Could not write to %s: %s\n",
                        path, strerror(errno));
                  ret = fclose(f);
                  assert(ret == 0);
                  return;
            }
      }

      ret = fprintf(f, FORMAT_history_footer);
      if (ret < 0) {
            fprintf(stderr, "Could not write to %s: %s\n",
                  path, strerror(errno));
      }

      ret = fclose(f);
      assert(ret == 0);
}


/** generate one entry of the summary html file.
 *  @param f write to this file.
 *  @param entry generate html for this entry.
 *  @return result of fprintf call, < 0 on error.
 */
static int
html_gen_summary_entry(
      FILE *f, 
      const struct exp_history_t *entry
)
{
      int ret;
      const char *messages;
      const char *timestamp;
      int runtime = -1;
      char prefix[PATH_MAX];
      char buf1[PATH_MAX + 1024];
      char buf2[PATH_MAX + 1024];

      assert(entry->num_tests > 0);
      runtime = entry->current_result.runtime;
      messages = entry->current_result.messages;
      timestamp = entry->current_result.timestamp;

      ret = snprintf(prefix, sizeof(prefix), "%s/%s", entry->exp_name,
                  entry->current_result.timestamp);
      assert(ret < sizeof(prefix));

      ret = fprintf(f, FORMAT_summary_entry,
            str_make_link(entry->exp_name, "%s/history.html",
                        entry->exp_name),
            str_make_color_code(entry),
            str_make_datetime(timestamp),
            str_runtime(runtime),
            str_make_result(entry), 
            messages,
            str_make_file_links(
                  prefix,
                  entry->current_result.logs[0],
                  ARRAY_SIZE(entry->current_result.logs),
                  sizeof(entry->current_result.logs[0]),
                  buf1,
                  sizeof(buf1)),
            str_make_file_links(
                  prefix,
                  entry->current_result.screenshot[0],
                  ARRAY_SIZE(entry->current_result.screenshot),
                  sizeof(entry->current_result.screenshot[0]),
                  buf2,
                  sizeof(buf2))
            );

      return ret;
}

/** generate html file for summary.
 *  @param path path to html output file.
 *  @param num_hist number of history entries.
 *  @param history array of history entries.
 */
static void
html_gen_summary(
      const char *path,
      unsigned int num_hist,
      const struct exp_history_t *history
)
{
      FILE *f;
      int ret;
      unsigned int i;

      f = fopen(path, "w");
      if (f == NULL) {
            fprintf(stderr, "Could not open %s for writing: %s\n",
                  path, strerror(errno));
            return;
      }

      ret = fprintf(f, FORMAT_summary_header);
      if (ret < 0) {
            fprintf(stderr, "Could not write to %s: %s\n", path,
                  strerror(errno));
            ret = fclose(f);
            assert(ret == 0);
            return;
      }

      for (i = 0; i < num_hist; i++, history++) {
            ret = html_gen_summary_entry(f, history);
            if (ret < 0) {
                  fprintf(stderr, "Could not write to %s: %s\n",
                        path, strerror(errno));
                  ret = fclose(f);
                  assert(ret == 0);
                  return;
            }
      }

      ret = fprintf(f, FORMAT_summary_footer);
      if (ret < 0) {
            fprintf(stderr, "Could not write to %s: %s\n",
                  path, strerror(errno));
      }

      ret = fclose(f);
      assert(ret == 0);

}

/** write all tests to the FILE output filtering on successful/unsuccessful
 *  tests.
 *  @param num_results total number of results.
 *  @param results array of results.
 *  @param output write to this file.
 *  @param success filter on success/non-success.
 *  @return number of entries written.
 */
static unsigned int
plot_write_tests(
      unsigned int num_results, 
      const struct exp_result_t *results,
      FILE *output,
      bool success
)
{
      unsigned int ret = 0;
      unsigned int i;

      for (i = 0; i < num_results; i++) {
            if (results[i].success != success) {
                  continue;
            }
      
            /* skip tests with less than 5 seconds of runtime. */
            if (results[i].runtime >= 5) {
                  fprintf(output, "%s %f\n", results[i].timestamp,
                        results[i].runtime / 60.0);
                  ret++;
            }
      }

      return ret;
}

/** actually invoke gnuplot.
 *  @param out file name of destination file.
 *  @param title title of plot
 *  @param data full path to file containing plot data.
 *  @param have_data1 have a first data set (draw color: green) in data.
 *  @param have_data2 have a second data set (draw color: red) in data.
 */
static void
plot_invoke_gnuplot(
      const char *out,
      const char *title,
      const char *data,
      bool have_data1,
      bool have_data2
)
{
      FILE *gnu_plot;
      int ret;

      if ((! have_data1) && (! have_data2)) {
            /* nothing to plot. */
            return;
      }

      gnu_plot = _cpssp.gnuplot_out;

      /* generic info: title, date format, etc. */
      ret = fprintf(gnu_plot, "set title \"Experiment %s\";\n", title);
      ret = fprintf(gnu_plot, "set xlabel \"date\";\n");
      ret = fprintf(gnu_plot, "set ylabel \"runtime (minutes)\";\n");
      ret = fprintf(gnu_plot, "set xdata time;\n");
      ret = fprintf(gnu_plot, "set timefmt \"%%Y%%m%%d-%%H%%M\";\n");
      ret = fprintf(gnu_plot, "set terminal png;\n");
      ret = fprintf(gnu_plot, "set output \"%s\";\n", out);

      /* issue the actual plot command */
      if (have_data1 && have_data2) {
            /* both datasets available */
            ret = fprintf(gnu_plot, "plot \"%s\" index 0 using 1:2 with "
                  "lines ti \"success\" lt 2, \"%s\" index 1 using 1:2 "
                  "with lines ti \"failed\" lt 1;\n", data, data);
      } else if (have_data1) {
            ret = fprintf(gnu_plot, "plot \"%s\" index 0 using 1:2 with "
                  "lines ti \"success\" lt 2;\n", data);
      } else {
            assert(have_data2);
            /* only dataset 2 at index 0 */
            ret = fprintf(gnu_plot, "plot \"%s\" index 0 using 1:2 with "
                  "lines ti \"failed\" lt 1;\n", data);
      }
      ret = fflush(gnu_plot);
      assert(ret == 0);
}

/** add a temporary file that was created to the cleanup list.
 *  @param cpssp cpssp instance.
 *  @param tmp full path to temp file.
 *  @param is_succ does the tmp file match a successful test run?
 */
static void
add_tmp_file(struct cpssp *cpssp, const char *tmp, bool is_succ)
{
      assert(strlen(tmp) < sizeof(cpssp->tmp_files[0]));
      assert(cpssp->n_tmp_files < ARRAY_SIZE(cpssp->tmp_files));

      strcpy(cpssp->tmp_files[cpssp->n_tmp_files], tmp);
      cpssp->is_tmp_f_succ[cpssp->n_tmp_files] = is_succ;
      cpssp->n_tmp_files++;
}

/** plot a history file.
 *  @param path path to plot output file.
 *  @param experiment name of executed experiment.
 *  @param num_results number of results.
 *  @param results array containing the results.
 */
static void
plot_history(
      const char *path,
      const char *experiment,
      unsigned int num_results,
      const struct exp_result_t *results
)
{
      char template_name[] = TMP_FILE_NAME;
      int fd;
      FILE *tmp;
      int ret;
      unsigned int num;
      bool have_successful = false;
      bool have_failed = false;

      fd = mkstemp(template_name);
      if (fd < 0) {
            fprintf(stderr, "Could not create temporary file %s: %s\n",
                  template_name, strerror(errno));
            return;
      }

      tmp = fdopen(fd, "w");
      if (tmp == NULL) {
            fprintf(stderr, "fdopen failed: %s\n",
                  strerror(errno));
            return;
      }

      /* write all successful tests */
      num = plot_write_tests(num_results, results, tmp, true);
      if (num > 0) {
            have_successful = true;
            fprintf(tmp, "\n\n");
      }

      num = plot_write_tests(num_results, results, tmp, false);
      if (num > 0) {
            have_failed = true;
      }

      if (ferror(tmp)) {
            fprintf(stderr, "Error writing to %s: %s\n", template_name,
                  strerror(errno));
            clearerr(tmp);
            fclose(tmp);
            return;
      }

      ret = fclose(tmp);
      if (ret != 0) {
            fprintf(stderr, "Close of file %s failed: %s\n",
                  template_name, strerror(errno));
            return;
      }

      plot_invoke_gnuplot(path, experiment, template_name, have_successful,
                  have_failed);

      add_tmp_file(&_cpssp, template_name, have_successful);
}

/** determine messages from log.faum-node-pc 
 *  @param path path to log.faum-node-pc
 *  @param dest store messages there.
 *  @param sz size of dest buffer.
 */
static void
determine_messages(const char *path, char *dest, size_t sz)
{
      FILE *f;
      int ret;
      unsigned int lines = 0;
      regex_t preg;
      char buf[2048];
      char match_buf[MAX_MATCH_LINES][2048];
      unsigned int i;
      unsigned int rel_index;
      char *s;
      const char *regex = 
            "(WARNING:)|(FATAL:)|(\\.c:)|(Exception)";

      *dest = '\0';

      f = fopen(path, "r");
      if (f == NULL) {
            if (errno == ENOENT) {
                  /* file does not exist, skip */
                  *dest = '\0';
                  return;
            }

            fprintf(stderr, "Could not open file %s: %s\n", path,
                  strerror(errno));
            return;
      }

      /* set up regular expressions */
      ret = regcomp(&preg, regex, REG_EXTENDED | REG_NOSUB);
      assert(ret == 0);

      /* advance to last 16K as heuristic */
      ret = fseek(f, -16384, SEEK_END);
      /* This will either work, or not, and leave us then at pos 0. (which
       *  is equally correct). */
      
      while (! feof(f)) {
            s = fgets(buf, sizeof(buf), f);
            if (s == NULL) {
                  break;
            }
            
            ret = regexec(&preg, buf, 0, NULL, 0);
            if (ret == REG_NOMATCH) {
                  continue;
            }

            /* match... copy to match_buf ringbuffer */
            strncpy(match_buf[lines % MAX_MATCH_LINES], buf, 
                  sizeof(match_buf[0]));
            lines++;
      }


      regfree(&preg);
      ret = fclose(f);
      assert(ret >= 0);

      /* copy to dest from ringbuffer */
      for (i = 0; i < MIN(MAX_MATCH_LINES, lines); i++) {
            if (lines <= MAX_MATCH_LINES) {
                  rel_index = i;
            } else {
                  rel_index = (lines + i) % MAX_MATCH_LINES;
            }

            strncpy(dest, match_buf[rel_index], sz);
            dest[sz - 1] = '\0';
            sz -= strlen(dest);
            dest += strlen(dest);

            if (sz < 6) {
                  return;
            }

            if (i < MAX_MATCH_LINES - 1) {
                  ret = snprintf(dest, sz, "<br>");
                  assert(ret < sz);
                  dest += ret;
                  sz -= ret;
            }
      }
}

/** callback for qsort, to sort on the timestamp of a result type.
 *  @param _s1 first result entry (exp_result_t)
 *  @param _s2 second result entry (exp_result_t)
 *  @return numeric comparison result.
 */
static int
compare_timestamp(const void *_r1, const void *_r2)
{
      const struct exp_result_t *r1 = (const struct exp_result_t*)_r1;
      const struct exp_result_t *r2 = (const struct exp_result_t*)_r2;

      return strncmp(r2->timestamp, r1->timestamp, sizeof(r2->timestamp));
}

/** callback for qsort, to sort the history by experiment name.
 *  @param _h1 first history entry (exp_history_t)
 *  @parma _h2 second history entry (exp_history_t)
 *  @return numeric comparison result.
 */
static int
compare_experiment_name(const void *_h1, const void *_h2)
{
      const struct exp_history_t *h1 = (const struct exp_history_t*)_h1;
      const struct exp_history_t *h2 = (const struct exp_history_t*)_h2;

      return strncmp(h1->exp_name, h2->exp_name, sizeof(h1->exp_name));
}

/* TODO: after all setups have been converted, much logic can be removed
 *       from this function. The only part that shall remain is to walk
 *       through the directory, and pick up the last png file.
 */
/** check if filename refers to a screenshot, and if so set the 
 *  corresponding members of dest.
 *  @param prefix path prefix to the top directory of the test results.
 *  @param experiment directory of the experiment.
 *  @param test_run directory of the test run.
 *  @param filename filename in the test run directory.
 *  @param dest members in this structure will get updated if appropriate.
 */
static void
set_screenshot(
      const char *prefix,
      const char *experiment,
      const char *test_run,
      const char *filename,
      struct exp_result_t *dest
)
{
      int i;
      char num[4];
      const char *extension;
      bool ext_valid = false;

      if (strncmp(filename, "screenshot-", strlen("screenshot-")) != 0) {
            return;
      }

      if (strlen(filename) != strlen("screenshot-###.png")) {
            return;
      }

      extension = filename + strlen("screenshot-###.");
      if (strcasecmp(extension, "png") == 0) {
            ext_valid = true;
      }
      if (strcasecmp(extension, "ppm") == 0) {
            ext_valid = true;
      }     
      if (! ext_valid) {
            return;
      }

      strncpy(num, filename + strlen("screenshot-"), sizeof(num));

      i = atoi(num);
      assert(i < ARRAY_SIZE(dest->screenshot));
      assert(i < ARRAY_SIZE(dest->is_png));

      if (dest->is_png[i]) {
            /* png file already registered, do nothing. */
            return;
      }

      strncpy(dest->screenshot[i], filename, sizeof(dest->screenshot[i]));

      if (strncmp(filename + strlen("screenshot-###."), "png", 
            strlen("png")) == 0) {

            dest->is_png[i] = true;
      } else {
            dest->is_png[i] = false;
      }
}


/** check, if filename refers to a log file, and update dest->logs if
 *  appropriate.
 *  @param prefix path prefix to the top directory of the test results.
 *  @param experiment directory of the experiment.
 *  @param test_run directory of the test run.
 *  @param filename filename in the test run directory.
 *  @param dest members in this structure will get updated if appropriate.
 */
static void
set_log(
      const char *prefix,
      const char *experiment,
      const char *test_run,
      const char *filename,
      struct exp_result_t *dest
)
{
      size_t i;
      bool found = false;

      if (strlen(filename) < strlen("log.")) {
            return;
      }

      if (strncmp(filename, "log.", strlen("log.")) != 0) {
            return;
      }

      for (i = 0; i < ARRAY_SIZE(dest->logs); i++) {
            if (dest->logs[i][0] == '\0') {
                  found = true;
                  break;
            }
      }

      if (! found) {
            fprintf(stderr, "%s: WARNING discarding log %s/%s/%s/%s "
                  "(no space in array left).\n", __FUNCTION__,
                  prefix, experiment, test_run, filename);
            return;
      }

      strncpy(dest->logs[i], filename, sizeof(dest->logs[i]));
}

/** check, if filename is a test success stamp and set dest->success.
 *  @param prefix path prefix to the top directory of the test results.
 *  @param experiment directory of the experiment.
 *  @param test_run directory of the test run.
 *  @param filename filename in the test run directory.
 *  @param dest members in this structure will get updated if appropriate.
 */
static void
set_success(
      const char *prefix,
      const char *experiment,
      const char *test_run,
      const char *filename,
      struct exp_result_t *dest
)
{
      if (strcmp(filename, "test.success") == 0) {
            dest->success = true;
      }
}

/** check, if filename refers to a runtime entry and set runtime.
 *  @param prefix path prefix to the top directory of the test results.
 *  @param experiment directory of the experiment.
 *  @param test_run directory of the test run.
 *  @param filename filename in the test run directory.
 *  @param dest members in this structure will get updated if appropriate.
 */
static void
set_runtime(
      const char *prefix,
      const char *experiment,
      const char *test_run,
      const char *filename,
      struct exp_result_t *dest
)
{
      char s[PATH_MAX];
      char buffer[15];
      int sz;
      FILE *f;
      int ret;

      if (strcmp(filename, "runtime") != 0) {
            return;
      }


      /* check for existance of "%path%/runtime" */
      sz = snprintf(s, sizeof(s), "%s/%s/%s/runtime", prefix, 
                  experiment, test_run);
      assert(sz < sizeof(s));

      f = fopen(s, "r");
      if (f == NULL) {
            fprintf(stderr, "%s: WARNING, cannot read runtime for %s.\n",
                  __FUNCTION__, s);
            return;
      }

      fgets(buffer, sizeof(buffer), f);
      ret = fclose(f);
      assert(ret == 0);

      dest->runtime = atoi(buffer);
}

/** check, if filename refers to log.faum-node-pc and if so extract 
 *  messages and set dest->messages to them.
 *  @param prefix path prefix to the top directory of the test results.
 *  @param experiment directory of the experiment.
 *  @param test_run directory of the test run.
 *  @param filename filename in the test run directory.
 *  @param dest members in this structure will get updated if appropriate.
 */
static void
set_messages(
      const char *prefix,
      const char *experiment,
      const char *test_run,
      const char *filename,
      struct exp_result_t *dest
)
{
      char path[PATH_MAX];
      int sz;

      if (strcmp(filename, "log.faum-node-pc") != 0) {
            return;
      }

      sz = snprintf(path, sizeof(path), "%s/%s/%s/%s", prefix, experiment,
                  test_run, filename);
      assert(sz < sizeof(path));

      determine_messages(path, dest->messages, sizeof(dest->messages));
}


/** convert any .ppm screenshots to .png updating dest->screenshot.
 *  @param prefix path prefix to the top directory of the test results.
 *  @param experiment directory of the experiment.
 *  @param test_run directory of the test run.
 *  @param dest members in this structure will get updated if appropriate.
 */
static void
fixup_screenshots(
      const char *prefix,
      const char *experiment,
      const char *test_run,
      struct exp_result_t *dest
)
{
      int i;
      int sz;
      char buf[PATH_MAX];
      const char *s;

      assert(ARRAY_SIZE(dest->is_png) == ARRAY_SIZE(dest->screenshot));

      for (i = 0; i < ARRAY_SIZE(dest->is_png); i++) {
            if (dest->is_png[i]) {
                  continue;
            }

            if (dest->screenshot[i][0] == '\0') {
                  continue;
            }

            sz = snprintf(buf, sizeof(buf), "%s/%s/%s", prefix, 
                  experiment, test_run);
            assert(sz < sizeof(buf));

            s = convert_screenshot(buf, dest->screenshot[i]);
            assert(s != NULL);
            strncpy(dest->screenshot[i], s, sizeof(dest->screenshot[i]));
      }
}

/** initialize a result structure with sane values.
 *  @param r structure to initialize.
 */
static void
exp_result_t_init(struct exp_result_t *r)
{
      size_t i;
      r->timestamp[0] = '\0';
      r->runtime = -1;
      r->success = false;
      r->messages[0] = '\0';

      for (i = 0; i < ARRAY_SIZE(r->screenshot); i++) {
            r->screenshot[i][0] = '\0';
      }

      for (i = 0; i < ARRAY_SIZE(r->is_png); i++) {
            r->is_png[i] = false;
      }

      for (i = 0; i < ARRAY_SIZE(r->logs); i++) {
            r->logs[i][0] = '\0';
      }
}

/** evaluate one test run.
 *  @param prefix relative path prefix so that "prefix/experiment/timestamp/"
 *         is the directory of the test run to evaluate.
 *  @param experiment directory name of the experiment.
 *  @param timestamp timestamp/directory name of test run.
 *  @param ret fill in this entry.
 *  @return filled ret or NULL if invalid.
 */
static struct exp_result_t *
eval_testrun(
      const char *prefix,
      const char *experiment,
      const char *timestamp,
      struct exp_result_t *ret
)
{
      size_t i;
      int sz;
      char c;
      DIR *d;
      struct dirent *entry;
      char path[PATH_MAX];

      /* should have a timestamp component */
      sz = strlen(timestamp);
      assert(sz == sizeof(ret->timestamp) - 1);

      for (i = 0; i < strlen(timestamp); i++) {
            c = timestamp[i];
            /* make sure it is a timestamp */
            if (! (((c >= '0') && (c <= '9')) || c == '-')) {
                  return NULL;
            }
      }

      exp_result_t_init(ret);
      strncpy(ret->timestamp, timestamp, sizeof(ret->timestamp));

      /* iterate over directory entries, setting appropriate members */
      sz = snprintf(path, sizeof(path), "%s/%s/%s", prefix, experiment, 
            timestamp);
      assert(sz < sizeof(path));

      d = opendir(path);
      if (d == NULL) {
            fprintf(stderr, "%s: Could not open directory %s: %s\n",
                  __FUNCTION__, path, strerror(errno));
            fprintf(stderr, "prefix=%s, experiment=%s, timestamp=%s\n",
                  prefix, experiment, timestamp);
            return NULL;
      }

      for (entry = readdir(d); entry != NULL; entry = readdir(d)) {
            if (entry->d_type != DT_REG) {
                  continue;
            }

            set_screenshot(prefix, experiment, timestamp, entry->d_name, 
                        ret);
            set_log(prefix, experiment, timestamp, entry->d_name, ret);
            set_success(prefix, experiment, timestamp, entry->d_name, ret);
            set_runtime(prefix, experiment, timestamp, entry->d_name, ret);
            set_messages(prefix, experiment, timestamp, entry->d_name, 
                  ret);
      };

      sz = closedir(d);
      assert(sz >= 0);

      /* finally fix up screenshots */
      fixup_screenshots(prefix, experiment, timestamp, ret);

      return ret;
}

/** heuristic to check, if the dirent entry is a test run directory.
 *  @param entry dirent entry to check.
 *  @return true, if entry is a test run directory, false otherwise.
 */
static bool
is_test_run_dir(const struct dirent *entry)
{
      size_t len;
      int i;
      const char *c;

      if ((entry->d_type & DT_DIR) == 0) {
            /* not a directory */
            return false;
      }

      /* check if name matches "yyyymmdd-hhmm" */
      len = strlen(entry->d_name);
      if (len != strlen("yyyymmdd-hhmm")) {
            return false;
      }

      c = entry->d_name;
      for (i = 0; i < strlen("yyyymmdd"); i++, c++) {
            if ((*c < '0') || (*c > '9')) {
                  /* invalid character */
                  return false;
            }
      }

      if (*c != '-') {
            /* missing hyphen */
            return false;
      }
      c++;

      for (i = 0; i < strlen("hhmm"); i++, c++) {
            if ((*c < '0') || (*c > '9')) {
                  /* invalid character */
                  return false;
            }
      }

      return true;
}

/** check if the screenshots have changed an update entry->shot_changed.
 *  @param prefix prefix directory of experiments.
 *  @param exp_name directory name of experiment.
 *  @param entry update this entry. current_result and last_result members
 *         must be valid.
 */
static void
check_shot_changed(
      const char *prefix, 
      const char *exp_name, 
      struct exp_history_t *entry
)
{
      unsigned int i;
      char p1[PATH_MAX];
      char p2[PATH_MAX];
      int ret;

      for (i = 0; i < ARRAY_SIZE(entry->current_result.screenshot); i++) {
            if (   (entry->current_result.screenshot[i][0] == '\0') 
                && (entry->last_result.screenshot[i][0] == '\0')) {
                  continue;
            }

            if (entry->current_result.screenshot[i][0] == '\0') {
                  entry->shot_changed = true;
                  return;
            }

            if (entry->last_result.screenshot[i][0] == '\0') {
                  entry->shot_changed = true;
                  return;
            }


            ret = snprintf(p1, sizeof(p1), "%s/%s/%s/%s", prefix,
                        exp_name, entry->current_result.timestamp,
                        entry->current_result.screenshot[i]);
            assert(ret < sizeof(p1));

            ret = snprintf(p2, sizeof(p2), "%s/%s/%s/%s", prefix,
                        exp_name, entry->last_result.timestamp,
                        entry->last_result.screenshot[i]);
            assert(ret < sizeof(p2));

            /* both screenshots present, compare these */
            entry->shot_changed = binary_diff(p1, p2);
            if (entry->shot_changed) {
                  /* there was a change. bail out */
                  return;                 
            }
      }
}

/** generate history information and html for one experiment
 *  @param path path to directory containing experiment results.
 *  @param exp_name name of experiment.
 *  @param result store information in this entry.
 *  @return history entry of experiments (history param) or NULL on error.
 */
static struct exp_history_t *
gen_history(
      const char *prefix, 
      const char *exp_name,
      struct exp_history_t *result
)
{
      DIR *d;
      struct dirent *entry;
      char run_dir[PATH_MAX];
      int sz;
      static struct exp_result_t history[MAX_TESTRUNS];
      struct exp_result_t *det;

      result->num_tests = 0;
      result->num_success = 0;
      result->shot_changed = false;

      sz = snprintf(run_dir, sizeof(run_dir), "%s/%s", prefix, exp_name);
      assert(sz < sizeof(run_dir));

      d = opendir(run_dir);
      if (d == NULL) {
            fprintf(stderr, "%s: Could not open directory %s: %s\n",
                  __FUNCTION__, run_dir, strerror(errno));
                  return NULL;
      }

      /* evaluate all test runs, and store these in history */
      for (entry = readdir(d); entry != NULL; entry = readdir(d)) {
            if (! is_test_run_dir(entry)) {
                  continue;
            }

            strncpy(run_dir, entry->d_name, sizeof(run_dir));

            det = eval_testrun(prefix, exp_name, run_dir, 
                        &history[result->num_tests]);

            if (det == NULL) {
                  continue;
            }

            if (history[result->num_tests].success) {
                  result->num_success++;
            }
            
            result->num_tests++;
            assert(result->num_tests < ARRAY_SIZE(history));
      };
      sz = closedir(d);
      assert(sz >= 0);


      strncpy(result->exp_name, exp_name, sizeof(result->exp_name));
      
      if (result->num_tests == 0) {
            /* no test, do not list */
            return NULL;
      }

      /* sort history by timestamp */
      qsort(history, result->num_tests, sizeof(history[0]), 
            compare_timestamp);

      result->current_result = history[0];
      if (result->num_tests > 1) {
            result->last_result = history[1];
      }

      /* generate html history */
      sz = snprintf(run_dir, sizeof(run_dir), "%s/%s/history.html", 
                  prefix, exp_name);
      assert(sz < sizeof(run_dir));

      html_gen_history(run_dir, exp_name, result->num_tests, history);

      /* plot a history graph */
      if (result->num_tests > 1) {
            sz = snprintf(run_dir, sizeof(run_dir), "%s/%s/history.png",
                        prefix, exp_name);
            assert(sz < sizeof(run_dir));
            plot_history(run_dir, exp_name, result->num_tests, history);
      }

      /* check if screenshots changed */
      check_shot_changed(prefix, exp_name, result);

      return result;
}

/** is this the directory of an experiment?
 *  @param entry dirent entry to check.
 *  @return true, if it corresponds to an experiment result directory,
 *          false otherwise.
 */
static bool
is_experiment_dir(const struct dirent *entry)
{
      const char *at_char;

      if ((entry->d_type & DT_DIR) == 0) {
            return false;
      }

      /* heuristic: experiment directories always contain one '@' */
      at_char = index(entry->d_name, '@');
      if (at_char == NULL) {
            return false;
      }
      
      return true;
}

/** generate summary
 *  @param results path name of results directory
 */
static void
generate_summary(const char *results)
{
      DIR *d;
      struct dirent *entry;
      char exp_dir[PATH_MAX];
      int ret;
      static struct exp_history_t summary[MAX_EXPERIMENTS];
      unsigned int num = 0;
      struct exp_history_t *res;

      d = opendir(results);
      if (d == NULL) {
            fprintf(stderr, "Could not open results directory %s: %s\n",
                  results, strerror(errno));
            return;     
      }

      for (entry = readdir(d); entry != NULL; entry = readdir(d)) {
            if (! is_experiment_dir(entry)) {
                  continue;
            }

            ret = snprintf(exp_dir, sizeof(exp_dir), "%s", entry->d_name);
            assert(ret < sizeof(exp_dir));
            res = gen_history(results, exp_dir, &summary[num]);

            if (res) {
                  num++;
                  assert(num < ARRAY_SIZE(summary));
            }
      }

      ret = closedir(d);
      assert(ret >= 0);
      if (num == 0) {
            /* no results */
            fprintf(stderr, "Warning: no results present!\n");
            return;
      }

      qsort(summary, num, sizeof(summary[0]), compare_experiment_name);
      ret = snprintf(exp_dir, sizeof(exp_dir), "%s/summary.html", 
                  results);
      assert(ret < sizeof(exp_dir));
      html_gen_summary(exp_dir, num, summary);
}

/** remove all created temporary files again */
static void
cleanup_tmp_files(struct cpssp *cpssp)
{
      unsigned int i;
      int ret;

      for (i = 0; i < cpssp->n_tmp_files; i++) {
            ret = unlink(cpssp->tmp_files[i]);
            assert(ret == 0);
      }

      cpssp->n_tmp_files = 0;
}

/** dirty hack to pick up all temporary .dat files and generate a 
 *  summary plot from index 0.
 *  @param prefix directory prefix to filename
 *  @param filename name of output file.
 *  @param cpssp cpssp instance with temporary files in it.
 */
static void
plot_all_succ(
      const char *prefix, 
      const char *filename, 
      const struct cpssp *cpssp
)
{
      int ret;
      int i;
      FILE *gnu_plot = cpssp->gnuplot_out;
      bool first = true;
      bool do_print = false;

      /* check if there's anything to plot at all. */
      for (i = 0; i < cpssp->n_tmp_files; i++) {
            if (cpssp->is_tmp_f_succ) {
                  do_print = true;
                  break;
            }
      }

      if (! do_print) {
            return;
      }

      /* generic info: title, date format, etc. */
      ret = fprintf(gnu_plot, "set title \"All successful tests\";\n");
      ret = fprintf(gnu_plot, "set xlabel \"date\";\n");
      ret = fprintf(gnu_plot, "set ylabel \"runtime (minutes)\";\n");
      ret = fprintf(gnu_plot, "set xdata time;\n");
      ret = fprintf(gnu_plot, "set timefmt \"%%Y%%m%%d-%%H%%M\";\n");
      ret = fprintf(gnu_plot, "set terminal png;\n");
      ret = fprintf(gnu_plot, "set output \"%s/%s\";\n", prefix, filename);

      for (i = 0; i < cpssp->n_tmp_files; i++) {
            if (! cpssp->is_tmp_f_succ[i]) {
                  continue;
            }

            if (first) {
                  ret = fprintf(gnu_plot, "plot \"%s\" index 0 using "
                              "1:2 with lines ti \"\"", 
                              cpssp->tmp_files[i]);
                  first = false;
                  continue;
            } 

            ret = fprintf(gnu_plot, ", \"%s\" index 0 using "
                              "1:2 with lines ti \"\"", 
                              cpssp->tmp_files[i]);
      }
      ret = fprintf(gnu_plot, ";\n");
}

int
main(int argc, char **argv)
{
      const char *sdir;
      const char *prog_name = *argv;

      if (argc >= 2) {
            argv++;
            sdir = *argv;
      } else {
            fprintf(stderr, "Usage: %s <result directory>.\n", prog_name);
            exit(EXIT_FAILURE);
      }
      /* ignore param 2: dir to test run and
       *        param 3: name of experiment
       */

      _cpssp.gnuplot_out = launch_gnuplot(&_cpssp.gnuplot_pid);
      generate_summary(sdir);
      plot_all_succ(sdir, "summary_all.png", &_cpssp);
      stop_gnuplot(_cpssp.gnuplot_pid, _cpssp.gnuplot_out);
      cleanup_tmp_files(&_cpssp);
      
      return 0;
}

Generated by  Doxygen 1.6.0   Back to index