1. Wez Furlong
  2. lcov

Commits

oberpapr  committed f0d2582

Check-in of updated LCOV version (to be released as 1.1). Includes fixes
and modifications by Mike Kobler, Paul Larson and myself.

A quote from the CHANGS file:
- Added CHANGES file
- Added Makefile implementing the following targets:
* install : install LCOV scripts and man pages
* uninstall : revert previous installation
* dist : create lcov.tar.gz file and lcov.rpm file
* clean : clean up example directory, remove .tar and .rpm files
- Added man pages for all scripts
- Added example program to demonstrate the use of LCOV with a userspace
application
- Implemented RPM build process
- New directory structure:
* bin : contains all executables
* example : contains a userspace example for LCOV
* man : contains man pages
* rpm : contains files required for the RPM build process
- LCOV-scripts are now in bin/
- Removed .pl-extension from LCOV-script files
- Renamed readme.txt to README

README:
- Adjusted mailing list address to ltp-coverage@lists.sourceforge.net
- Fixed incorrect parameter '--output-filename' in example LCOV call
- Removed tool descriptions and turned them into man pages
- Installation instructions now refer to RPM and tarball

descriptions.tests:
- Fixed some spelling errors

genhtml:
- Fixed bug which resulted in an error when trying to combine .info files
containing data without a test name
- Fixed bug which would not correctly handle data files in directories
with names containing some special characters ('+', etc.)
- Added check for empty tracefiles to prevent division-by-zeros
- Implemented new command line option --num-spaces / the number of spaces
which replace a tab in source code view is now user defined
- Fixed tab expansion so that in source code view, a tab doesn't produce a
fixed number of spaces, but as many spaces as are needed to advance to the
next tab position
- Output directory is now created if it doesn't exist
- Renamed "overview page" to "directory view page"
- HTML output pages are now titled "LCOV" instead of "GCOV"

geninfo:
- Fixed bug which would not allow .info files to be generated in directories
with names containing some special characters

lcov:
- Fixed bug which would cause lcov to fail when the tool is installed in
a path with a name containing some special characters
- Implemented new command line option '--add-tracefile' which allows the
combination of data from several tracefiles
- Implemented new command line option '--list' which lists the contents
of a tracefile
- Implemented new command line option '--extract' which allows extracting
data for a particular set of files from a tracefile
- Fixed name of gcov kernel module (new package contains gcov-prof.c)
- Changed name of gcov kernel directory from /proc/gcov to a global constant
so that it may be changed easily when required in future versions

  • Participants
  • Parent commits 771aff3
  • Branches default

Comments (0)

Files changed (31)

File CHANGES

View file
+Version 1.1:
+============
+
+- Added CHANGES file
+- Added Makefile implementing the following targets:
+  * install    : install LCOV scripts and man pages
+  * uninstall  : revert previous installation
+  * dist       : create lcov.tar.gz file and lcov.rpm file
+  * clean      : clean up example directory, remove .tar and .rpm files
+- Added man pages for all scripts
+- Added example program to demonstrate the use of LCOV with a userspace
+  application
+- Implemented RPM build process
+- New directory structure:
+  * bin        : contains all executables
+  * example    : contains a userspace example for LCOV
+  * man        : contains man pages
+  * rpm        : contains files required for the RPM build process
+- LCOV-scripts are now in bin/
+- Removed .pl-extension from LCOV-script files
+- Renamed readme.txt to README
+
+README:
+- Adjusted mailing list address to ltp-coverage@lists.sourceforge.net
+- Fixed incorrect parameter '--output-filename' in example LCOV call
+- Removed tool descriptions and turned them into man pages
+- Installation instructions now refer to RPM and tarball
+
+descriptions.tests:
+- Fixed some spelling errors
+
+genhtml:
+- Fixed bug which resulted in an error when trying to combine .info files
+  containing data without a test name
+- Fixed bug which would not correctly handle data files in directories
+  with names containing some special characters ('+', etc.)
+- Added check for empty tracefiles to prevent division-by-zeros
+- Implemented new command line option --num-spaces / the number of spaces
+  which replace a tab in source code view is now user defined
+- Fixed tab expansion so that in source code view, a tab doesn't produce a
+  fixed number of spaces, but as many spaces as are needed to advance to the
+  next tab position
+- Output directory is now created if it doesn't exist
+- Renamed "overview page" to "directory view page"
+- HTML output pages are now titled "LCOV" instead of "GCOV"
+
+geninfo:
+- Fixed bug which would not allow .info files to be generated in directories
+  with names containing some special characters
+
+lcov:
+- Fixed bug which would cause lcov to fail when the tool is installed in
+  a path with a name containing some special characters
+- Implemented new command line option '--add-tracefile' which allows the
+  combination of data from several tracefiles
+- Implemented new command line option '--list' which lists the contents
+  of a tracefile
+- Implemented new command line option '--extract' which allows extracting
+  data for a particular set of files from a tracefile
+- Fixed name of gcov kernel module (new package contains gcov-prof.c)
+- Changed name of gcov kernel directory from /proc/gcov to a global constant
+  so that it may be changed easily when required in future versions
+
+
+Version 1.0 (2002-09-05):
+=========================
+
+- Initial version
+

File Makefile

View file
+#
+# Makefile for the LTP GCOV extension (LCOV)
+#
+# Make targets:
+#   - install:   install LCOV tools and man pages on the system
+#   - uninstall: remove tools and man pages from the system
+#   - dist:      create files required for distribution, i.e. the lcov.tar.gz
+#                and the lcov.rpm file. Just make sure to adjust the VERSION
+#                and RELEASE variables below - both version and date strings
+#                will be updated in all necessary files.
+#   - clean:     remove all generated files
+#
+
+VERSION := 1.1
+RELEASE := 1
+DATE    := $(shell date +%Y-%m-%d)
+
+BIN_DIR := $(PREFIX)/usr/local/bin
+MAN_DIR := $(PREFIX)/usr/share/man/man1
+TMP_DIR := /tmp/lcov-tmp.$(shell echo $$$$)
+FILES   := $(wildcard bin/*) $(wildcard man/*) README CHANGES Makefile \
+	   $(wildcard rpm/*)
+
+.PHONY: all info clean install uninstall
+
+all: info
+
+info:
+	@echo try "'make install'", "'make uninstall'" or "'make dist'"
+
+clean:
+	rm -f lcov-*.tar.gz
+	rm -f lcov-*.noarch.rpm
+	make -C example clean
+
+install:
+	bin/install.sh bin/lcov $(BIN_DIR)/lcov
+	bin/install.sh bin/genhtml $(BIN_DIR)/genhtml
+	bin/install.sh bin/geninfo $(BIN_DIR)/geninfo
+	bin/install.sh bin/genpng $(BIN_DIR)/genpng
+	bin/install.sh bin/gendesc $(BIN_DIR)/gendesc
+	bin/install.sh man/lcov.1 $(MAN_DIR)/lcov.1
+	bin/install.sh man/genhtml.1 $(MAN_DIR)/genhtml.1
+	bin/install.sh man/geninfo.1 $(MAN_DIR)/geninfo.1
+	bin/install.sh man/genpng.1 $(MAN_DIR)/genpng.1
+	bin/install.sh man/gendesc.1 $(MAN_DIR)/gendesc.1
+
+uninstall:
+	bin/install.sh --uninstall bin/lcov $(BIN_DIR)/lcov
+	bin/install.sh --uninstall bin/genhtml $(BIN_DIR)/genhtml
+	bin/install.sh --uninstall bin/geninfo $(BIN_DIR)/geninfo
+	bin/install.sh --uninstall bin/genpng $(BIN_DIR)/genpng
+	bin/install.sh --uninstall bin/gendesc $(BIN_DIR)/gendesc
+	bin/install.sh --uninstall man/lcov.1 $(MAN_DIR)/lcov.1
+	bin/install.sh --uninstall man/genhtml.1 $(MAN_DIR)/genhtml.1
+	bin/install.sh --uninstall man/geninfo.1 $(MAN_DIR)/geninfo.1
+	bin/install.sh --uninstall man/genpng.1 $(MAN_DIR)/genpng.1
+	bin/install.sh --uninstall man/gendesc.1 $(MAN_DIR)/gendesc.1
+
+dist: lcov-$(VERSION).tar.gz lcov-$(VERSION)-$(RELEASE).noarch.rpm
+
+lcov-$(VERSION).tar.gz: $(FILES)
+	mkdir $(TMP_DIR)
+	mkdir $(TMP_DIR)/lcov-$(VERSION)
+	cp -r * $(TMP_DIR)/lcov-$(VERSION)
+	make -C $(TMP_DIR)/lcov-$(VERSION) clean
+	bin/updateversion.pl $(TMP_DIR)/lcov-$(VERSION) $(VERSION) $(DATE)
+	cd $(TMP_DIR) ; \
+	tar cfz $(TMP_DIR)/lcov-$(VERSION).tar.gz lcov-$(VERSION)
+	mv $(TMP_DIR)/lcov-$(VERSION).tar.gz .
+	rm -rf $(TMP_DIR)
+
+lcov-$(VERSION)-$(RELEASE).noarch.rpm: lcov-$(VERSION).tar.gz
+	mkdir $(TMP_DIR)
+	mkdir $(TMP_DIR)/BUILD
+	mkdir $(TMP_DIR)/RPMS
+	mkdir $(TMP_DIR)/SOURCES
+	cp lcov-$(VERSION).tar.gz $(TMP_DIR)/SOURCES
+	rpmbuild --define '_topdir $(TMP_DIR)' \
+		 --define 'LCOV_VERSION $(VERSION)' \
+		 --define 'LCOV_RELEASE $(RELEASE)' -bb rpm/lcov.spec
+	mv $(TMP_DIR)/RPMS/noarch/lcov-$(VERSION)-$(RELEASE).noarch.rpm .
+	rm -rf $(TMP_DIR)

File README

View file
+-------------------------------------------------
+- README file for the LTP GCOV extension (LCOV) -
+- Last changes: 2003-04-14                      -
+-------------------------------------------------
+
+Description
+-----------
+  LCOV is an extension of GCOV, a GNU tool which provides information about
+  what parts of a program are actually executed (i.e. "covered") while running
+  a particular test case. The extension consists of a set of PERL scripts
+  which build on the textual GCOV output to implement the following enhanced
+  functionality:
+
+    * HTML based output: coverage rates are additionally indicated using bar
+      graphs and specific colors.
+
+    * Support for large projects: overview pages allow quick browsing of
+      coverage data by providing three levels of detail: directory view,
+      file view and source code view.
+
+  LCOV was initially designed to support Linux kernel coverage measurements,
+  but works as well for coverage measurements on standard user space
+  applications.
+
+
+Further README contents
+-----------------------
+  1. Included files
+  2. Installing LCOV
+  3. An example of how to access kernel coverage data
+  4. An example of how to access coverage data for a user space program
+  5. Questions and Comments
+
+
+
+1. Important files
+------------------
+  README             - This README file
+  CHANGES            - List of changes between releases
+  bin/lcov           - Tool for capturing LCOV coverage data
+  bin/genhtml        - Tool for creating HTML output from LCOV data
+  bin/gendesc        - Tool for creating description files as used by genhtml
+  bin/geninfo        - Internal tool (creates LCOV data files)
+  bin/genpng         - Internal tool (creates png overviews of source files)
+  bin/install.sh     - Internal tool (takes care of un-/installing)
+  descriptions.tests - Test descriptions for the LTP suite, use with gendesc
+  man                - Directory containing man pages for included tools
+  example            - Directory containing example to demonstrate LCOV
+  Makefile           - Makefile providing 'install' and 'uninstall' targets
+
+
+2. Installing LCOV
+------------------
+The LCOV package is available as either RPM or tarball from:
+     
+  http://ltp.sourceforge.net/lcov.php
+
+To install the tarball, unpack it to a directory and run:
+
+  make install
+
+Use anonymous CVS for the most recent (but possibly unstable) version:
+
+  cvs -d:pserver:anonymous@cvs.LTP.sourceforge.net:/cvsroot/ltp login
+
+(simply press the ENTER key when asked for a password)
+
+  cvs -z3 -d:pserver:anonymous@cvs.LTP.sourceforge.net:/cvsroot/ltp export -D now utils
+
+Change to the utils/analysis/lcov directory and type:
+
+  make install
+
+
+3. An example of how to access kernel coverage data
+---------------------------------------------------
+Requirements: get and install the gcov-kernel package from
+
+  http://sourceforge.net/projects/lse
+
+Copy the resulting gcov kernel module file to either the system wide modules
+directory or the same directory as the PERL scripts. As root, do the following:
+
+  a) Resetting counters
+
+     lcov --reset
+
+  b) Capturing the current coverage state to a file
+
+     lcov --capture --output-file kernel.info
+
+  c) Getting HTML output
+
+     genhtml kernel.info
+
+Point the web browser of your choice to the resulting index.html file.
+
+
+4. An example of how to access coverage data for a user space program
+---------------------------------------------------------------------
+Requirements: compile the program in question using GCC with the options
+-fprofile-arcs and -ftest-coverage. Assuming the compile directory is called
+"appdir", do the following:
+
+  a) Resetting counters
+
+     lcov --directory appdir --reset
+
+  b) Capturing the current coverage state to a file (works only after the
+     application has been started and stopped at least once)
+
+     lcov --directory appdir --capture --output-file app.info
+
+  c) Getting HTML output
+
+     genhtml app.info
+
+Point the web browser of your choice to the resulting index.html file.
+
+
+5. Questions and comments
+-------------------------
+See the included man pages for more information on how to use the LCOV tools.
+
+Please email further questions or comments regarding this tool to the
+LTP Mailing list at ltp-coverage@lists.sourceforge.net  
+

File bin/gendesc

View file
+#!/usr/bin/perl -w
+#
+#   Copyright (c) International Business Machines  Corp., 2002
+#
+#   This program is free software;  you can redistribute it and/or modify
+#   it under the terms of the GNU General Public License as published by
+#   the Free Software Foundation; either version 2 of the License, or (at
+#   your option) any later version.
+#
+#   This program is distributed in the hope that it will be useful, but
+#   WITHOUT ANY WARRANTY;  without even the implied warranty of
+#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#   General Public License for more details.                 
+#
+#   You should have received a copy of the GNU General Public License
+#   along with this program;  if not, write to the Free Software
+#   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+#
+# gendesc
+#
+#   This script creates a description file as understood by genhtml.
+#   Input file format:
+#
+#   For each test case:
+#     <test name><optional whitespace>
+#     <at least one whitespace character (blank/tab)><test description>
+#   
+#   Actual description may consist of several lines. By default, output is
+#   written to stdout. Test names consist of alphanumeric characters
+#   including _ and -.
+#
+#
+# History:
+#   2002-09-02: created by Peter Oberparleiter <Peter.Oberparleiter@de.ibm.com>
+#
+
+use strict;
+use File::Basename; 
+use Getopt::Long;
+
+
+# Constants
+our $lcov_version	= "LTP GCOV extension version 1.1";
+our $lcov_url		= "http://ltp.sourceforge.net/lcov.php";
+
+
+# Prototypes
+sub print_usage(*);
+sub gen_desc();
+
+
+# Global variables
+our $help;
+our $version;
+our $output_filename;
+our $input_filename;
+
+
+#
+# Code entry point
+#
+
+# Parse command line options
+if (!GetOptions("output-filename=s" => \$output_filename,
+		"version" =>\$version,
+		"help" => \$help
+		))
+{
+	print_usage(*STDERR);
+	exit(1);
+}
+
+$input_filename = $ARGV[0];
+
+# Check for help option
+if ($help)
+{
+	print_usage(*STDOUT);
+	exit(0);
+}
+
+# Check for version option
+if ($version)
+{
+	print($lcov_version."\n");
+	exit(0);
+}
+
+
+# Check for input filename
+if (!$input_filename)
+{
+	print(STDERR "No input filename specified\n");
+	print_usage(*STDERR);
+	exit(1);
+}
+
+# Do something
+gen_desc();
+
+
+#
+# print_usage(handle)
+#
+# Write out command line usage information to given filehandle.
+#
+
+sub print_usage(*)
+{
+	local *HANDLE = $_[0];
+	my $tool_name = basename($0);
+
+	print(HANDLE <<END_OF_USAGE)
+Usage: $tool_name [OPTIONS] INPUTFILE
+
+Convert a test case description file into a format as understood by genhtml.
+
+  -h, --help                        Print this help, then exit
+  -v, --version                     Print version number, then exit
+  -o, --output-filename FILENAME    Write description to FILENAME
+
+See $lcov_url for more information about this tool.
+END_OF_USAGE
+	;
+}
+
+
+#
+# gen_desc()
+#
+# Read text file INPUT_FILENAME and convert the contained description to a
+# format as understood by genhtml, i.e.
+#
+#    TN:<test name>
+#    TD:<test description>
+#
+# If defined, write output to OUTPUT_FILENAME, otherwise to stdout.
+#
+# Die on error.
+#
+
+sub gen_desc()
+{
+	local *INPUT_HANDLE;
+	local *OUTPUT_HANDLE;
+	my $empty_line = "ignore";
+
+	open(INPUT_HANDLE, $input_filename)
+		or die("ERROR: cannot open $input_filename!\n");
+
+	# Open output file for writing
+	if ($output_filename)
+	{
+		open(OUTPUT_HANDLE, ">$output_filename")
+			or die("ERROR: cannot create $output_filename!\n");
+	}
+	else
+	{
+		*OUTPUT_HANDLE = *STDOUT;
+	}
+
+	# Process all lines in input file
+	while (<INPUT_HANDLE>)
+	{
+		chomp($_);
+
+		if (/^\s*(\w[\w-]*)(\s*)$/)
+		{
+			# Matched test name
+			# Name starts with alphanum or _, continues with
+			# alphanum, _ or -
+			print(OUTPUT_HANDLE "TN: $1\n");
+			$empty_line = "ignore";
+		}
+		elsif (/^(\s+)(\S.*?)\s*$/)
+		{
+			# Matched test description
+			if ($empty_line eq "insert")
+			{
+				# Write preserved empty line
+				print(OUTPUT_HANDLE "TD: \n");
+			}
+			print(OUTPUT_HANDLE "TD: $2\n");
+			$empty_line = "observe";
+		}
+		elsif (/^\s*$/)
+		{
+			# Matched empty line to preserve paragraph separation
+			# inside description text
+			if ($empty_line eq "observe")
+			{
+				$empty_line = "insert";
+			}
+		}
+	}
+
+	# Close output file if defined
+	if ($output_filename)
+	{
+		close(OUTPUT_HANDLE);
+	}
+
+	close(INPUT_HANDLE);
+}

File bin/genhtml

View file
+#!/usr/bin/perl -w
+#
+#   Copyright (c) International Business Machines  Corp., 2002
+#
+#   This program is free software;  you can redistribute it and/or modify
+#   it under the terms of the GNU General Public License as published by
+#   the Free Software Foundation; either version 2 of the License, or (at
+#   your option) any later version.
+#
+#   This program is distributed in the hope that it will be useful, but
+#   WITHOUT ANY WARRANTY;  without even the implied warranty of
+#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#   General Public License for more details.                 
+#
+#   You should have received a copy of the GNU General Public License
+#   along with this program;  if not, write to the Free Software
+#   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+#
+# genhtml
+#
+#   This script generates HTML output from .info files as created by the
+#   geninfo script. Call it with --help and refer to the genhtml man page
+#   to get information on usage and available options.
+#
+#
+# History:
+#   2002-08-23 created by Peter Oberparleiter <Peter.Oberparleiter@de.ibm.com>
+#                         IBM Lab Boeblingen
+#        based on code by Manoj Iyer <manjo@mail.utexas.edu> and
+#                         Megan Bock <mbock@us.ibm.com>
+#                         IBM Austin
+#   2002-08-27 / Peter Oberparleiter: implemented frame view
+#   2002-08-29 / Peter Oberparleiter: implemented test description filtering
+#                so that by default only descriptions for test cases which
+#                actually hit some source lines are kept
+#   2002-09-05 / Peter Oberparleiter: implemented --no-sourceview
+#   2002-09-05 / Mike Kobler: One of my source file paths includes a "+" in
+#                the directory name.  I found that genhtml.pl died when it
+#                encountered it. I was able to fix the problem by modifying
+#                the string with the escape character before parsing it.
+#   2002-10-26 / Peter Oberparleiter: implemented --num-spaces
+#   2003-04-07 / Peter Oberparleiter: fixed bug which resulted in an error
+#                when trying to combine .info files containing data without
+#                a test name
+#   2003-04-10 / Peter Oberparleiter: extended fix by Mike to also cover
+#                other special characters
+#
+
+use strict;
+use File::Basename; 
+use Getopt::Long;
+
+
+# Global constants
+our $title		= "LTP GCOV extension - code coverage report";
+our $lcov_version	= "LTP GCOV extension version 1.1";
+our $lcov_url		= "http://ltp.sourceforge.net/lcov.php";
+
+# Specify coverage rate limits (in %) for classifying file entries
+# HI:   $hi_limit <= rate <= 100          graph color: green
+# MED: $med_limit <= rate <  $hi_limit    graph color: orange
+# LO:          0  <= rate <  $med_limit   graph color: red
+our $hi_limit	= 50;
+our $med_limit	= 15;
+
+# Width of overview image
+our $overview_width = 80;
+
+# Resolution of overview navigation: this number specifies the maximum
+# difference in lines between the position a user selected from the overview
+# and the position the source code window is scrolled to.
+our $nav_resolution = 4;
+
+# Clicking a line in the overview image should show the source code view at
+# a position a bit further up so that the requested line is not the first
+# line in the window. This number specifies that offset in lines.
+our $nav_offset = 10;
+
+our $overview_title = "directory";
+
+# Data related prototypes
+sub print_usage(*);
+sub gen_html();
+sub process_dir($);
+sub process_file($$$);
+sub info(@);
+sub read_info_file($);
+sub get_info_entry($);
+sub set_info_entry($$$$;$$);
+sub get_prefix(@);
+sub shorten_prefix($);
+sub get_dir_list(@);
+sub get_relative_base_path($);
+sub read_testfile($);
+sub get_date_string();
+sub split_filename($);
+sub create_sub_dir($);
+sub subtract_counts($$);
+sub add_counts($$);
+sub apply_baseline($$);
+sub remove_unused_descriptions();
+sub get_found_and_hit($);
+sub get_affecting_tests($);
+sub combine_info_files($$);
+sub combine_info_entries($$);
+sub apply_prefix($$);
+sub escape_regexp($);
+
+
+# HTML related prototypes
+sub escape_html($);
+sub get_bar_graph_code($$$);
+
+sub write_png_files();
+sub write_css_file();
+sub write_description_file($$$);
+
+sub write_html(*$);
+sub write_html_prolog(*$$);
+sub write_html_epilog(*$;$);
+
+sub write_header(*$$$$$);
+sub write_header_prolog(*$);
+sub write_header_line(*$$;$$);
+sub write_header_epilog(*$);
+
+sub write_file_table(*$$$$);
+sub write_file_table_prolog(*$$);
+sub write_file_table_entry(*$$$$);
+sub write_file_table_detail_heading(*$$);
+sub write_file_table_detail_entry(*$$$);
+sub write_file_table_epilog(*);
+
+sub write_test_table_prolog(*$);
+sub write_test_table_entry(*$$);
+sub write_test_table_epilog(*);
+
+sub write_source($$$);
+sub write_source_prolog(*);
+sub write_source_line(*$$$);
+sub write_source_epilog(*);
+
+sub write_frameset(*$$$);
+sub write_overview_line(*$$$);
+sub write_overview(*$$$$);
+
+# External prototype (defined in genpng)
+sub gen_png($$$@);
+
+
+# Global variables & initialization
+our %info_data;		# Hash containing all data from .info file
+our $dir_prefix;	# Prefix to remove from all sub directories
+our %test_description;	# Hash containing test descriptions if available
+our $date = get_date_string();
+
+our @info_filenames;	# List of .info files to use as data source
+our $test_title;	# Title for output as written to each page header
+our $output_directory;	# Name of directory in which to store output
+our $base_filename;	# Optional name of file containing baseline data
+our $desc_filename;	# Name of file containing test descriptions
+our $css_filename;	# Optional name of external stylesheet file to use
+our $quiet;		# If set, suppress information messages
+our $help;		# Help option flag
+our $version;		# Version option flag
+our $show_details;	# If set, generate detailed directory view
+our $no_prefix;		# If set, do not remove filename prefix
+our $frames;		# If set, use frames for source code view
+our $keep_descriptions;	# If set, do not remove unused test case descriptions
+our $no_sourceview;	# If set, do not create a source code view for each file
+our $tab_size = 8;	# Number of spaces to use in place of tab
+
+our $cwd = `pwd`;	# Current working directory
+chomp($cwd);
+our $tool_dir = dirname($0);	# Directory where genhtml tool is installed
+
+
+#
+# Code entry point
+#
+
+# Add current working directory if $tool_dir is not already an absolute path
+if (! ($tool_dir =~ /^\/(.*)$/))
+{
+	$tool_dir = "$cwd/$tool_dir";
+}
+
+# Parse command line options
+if (!GetOptions("output-directory=s"	=> \$output_directory,
+		"title=s"		=> \$test_title,
+		"description-file=s"	=> \$desc_filename,
+		"keep-descriptions"	=> \$keep_descriptions,
+		"css-file=s"		=> \$css_filename,
+		"baseline-file=s"	=> \$base_filename,
+		"prefix=s"		=> \$dir_prefix,
+		"num-spaces=i"		=> \$tab_size,
+		"no-prefix"		=> \$no_prefix,
+		"no-sourceview"		=> \$no_sourceview,
+		"show-details"		=> \$show_details,
+		"frames"		=> \$frames,
+		"quiet"			=> \$quiet,
+		"help"			=> \$help,
+		"version"		=> \$version
+		))
+{
+	print_usage(*STDERR);
+	exit(1);
+}
+
+@info_filenames = @ARGV;
+
+# Check for help option
+if ($help)
+{
+	print_usage(*STDOUT);
+	exit(0);
+}
+
+# Check for version option
+if ($version)
+{
+	print($lcov_version."\n");
+	exit(0);
+}
+
+# Check for info filename
+if (!@info_filenames)
+{
+	print(STDERR "No filename specified\n");
+	print_usage(*STDERR);
+	exit(1);
+}
+
+# Generate a title if none is specified
+if (!$test_title)
+{
+	if (scalar(@info_filenames) == 1)
+	{
+		# Only one filename specified, use it as title
+		$test_title = basename($info_filenames[0]);
+	}
+	else
+	{
+		# More than one filename specified, used default title
+		$test_title = "unnamed";
+	}
+}
+
+# Make sure css_filename is an absolute path (in case we're changing
+# directories)
+if ($css_filename)
+{
+	if (!($css_filename =~ /^\/(.*)$/))
+	{
+		$css_filename = $cwd."/".$css_filename;
+	}
+}
+
+# Make sure tab_size is within valid range
+if ($tab_size < 1)
+{
+	print(STDERR "ERROR: invalid number of spaces specified: ".
+		     "$tab_size!\n");
+	exit(1);
+}
+
+# Issue a warning if --no-sourceview is enabled together with --frames
+if ($no_sourceview && $frames)
+{
+	warn("WARNING: option --frames disabled because --no-sourceview ".
+	     "was specified!\n");
+	$frames = undef;
+}
+
+if ($frames)
+{
+	# Include genpng code needed for overview image generation
+	do("$tool_dir/genpng");
+}
+
+# Make sure output_directory exists, create it if necessary
+if ($output_directory)
+{
+	stat($output_directory);
+
+	if (! -e _)
+	{
+		system("mkdir -p $output_directory")
+			and die("ERROR: cannot create directory $_!\n");
+	}
+}
+
+
+# Do something
+gen_html();
+
+exit(0);
+
+
+
+#
+# print_usage(handle)
+#
+# Print usage information.
+#
+
+sub print_usage(*)
+{
+	local *HANDLE = $_[0];
+	my $executable_name = basename($0);
+
+	print(HANDLE <<END_OF_USAGE);
+Usage: $executable_name [OPTIONS] INFOFILE(S)
+
+Create HTML output for coverage data found in INFOFILE. Note that INFOFILE
+may also be a list of filenames.
+
+  -h, --help                        Print this help, then exit
+  -v, --version                     Print version number, then exit
+  -q, --quiet                       Do not print progress messages
+  -s, --show-details                Generate detailed directory view
+  -f, --frames                      Use HTML frames for source code view
+  -b, --baseline-file BASEFILE      Use BASEFILE as baseline file
+  -o, --output-directory OUTDIR     Write HTML output to OUTDIR
+  -t, --title TITLE                 Display TITLE in header of all pages
+  -d, --description-file DESCFILE   Read test case descriptions from DESCFILE
+  -k, --keep-descriptions           Do not removed unused test descriptions
+  -c, --css-file CSSFILE            Use external style sheet file CSSFILE
+  -p, --prefix PREFIX               Remove PREFIX from all directory names
+      --no-prefix                   Do not remove prefix from directory names
+      --no-source                   Do not create source code view
+      --num-spaces NUM              Replace tabs with NUM spaces in source view
+
+See $lcov_url for more information about this tool.
+END_OF_USAGE
+	;
+}
+
+
+#
+# gen_html()
+#
+# Generate a set of HTML pages from contents of .info file INFO_FILENAME.
+# Files will be written to the current directory. If provided, test case
+# descriptions will be read from .tests file TEST_FILENAME and included
+# in ouput.
+#
+# Die on error.
+#
+
+sub gen_html()
+{
+	local *HTML_HANDLE;
+	my %overview;
+	my %base_data;
+	my $lines_found;
+	my $lines_hit;
+	my $overall_found = 0;
+	my $overall_hit = 0;
+	my $dir_name;
+	my $link_name;
+	my @dir_list;
+	my %new_info;
+
+	# Read in all specified .info files
+	foreach (@info_filenames)
+	{
+		info("Reading data file $_\n");
+		%new_info = %{read_info_file($_)};
+
+		# Combine %new_info with %info_data
+		%info_data = %{combine_info_files(\%info_data, \%new_info)};
+	}
+
+	info("Found %d entries.\n", scalar(keys(%info_data)));
+
+	# Read and apply baseline data if specified
+	if ($base_filename)
+	{
+		# Read baseline file
+		info("Reading baseline file $base_filename\n");
+		%base_data = %{read_info_file($base_filename)};
+		info("Found %d entries.\n", scalar(keys(%base_data)));
+
+		# Apply baseline
+		info("Subtracting baseline data.\n");
+		%info_data = %{apply_baseline(\%info_data, \%base_data)};
+	}
+
+	@dir_list = get_dir_list(keys(%info_data));
+
+	if ($no_prefix)
+	{
+		# User requested that we leave filenames alone
+		info("User asked not to remove filename prefix\n");
+	}
+	elsif (!defined($dir_prefix))
+	{
+		# Get prefix common to most directories in list
+		$dir_prefix = get_prefix(@dir_list);
+
+		if ($dir_prefix)
+		{
+			info("Found common filename prefix \"$dir_prefix\"\n");
+		}
+		else
+		{
+			info("No common filename prefix found!\n");
+			$no_prefix=1;
+		}
+	}
+	else
+	{
+		info("Using user-specified filename prefix \"".
+		     "$dir_prefix\"\n");
+	}
+
+	# Read in test description file if specified
+	if ($desc_filename)
+	{
+		info("Reading test description file $desc_filename\n");
+		%test_description = %{read_testfile($desc_filename)};
+
+		# Remove test descriptions which are not referenced
+		# from %info_data if user didn't tell us otherwise
+		if (!$keep_descriptions)
+		{
+			remove_unused_descriptions();
+		}
+	}
+
+	# Change to output directory if specified
+	if ($output_directory)
+	{
+		chdir($output_directory)
+			or die("ERROR: cannot change to directory ".
+			"$output_directory!\n");
+	}
+
+	info("Writing .css and .png files.\n");
+	write_css_file();
+	write_png_files();
+
+	info("Generating output.\n");
+
+	# Process each subdirectory and collect overview information
+	foreach $dir_name (@dir_list)
+	{
+		($lines_found, $lines_hit) = process_dir($dir_name);
+
+		# Remove prefix if applicable
+		if (!$no_prefix && $dir_prefix)
+		{
+			# Match directory names beginning with $dir_prefix
+			$dir_name = apply_prefix($dir_name, $dir_prefix);
+		}
+
+		# Generate name for directory overview HTML page
+		if ($dir_name =~ /^\/(.*)$/)
+		{
+			$link_name = substr($dir_name, 1)."/index.html";
+		}
+		else
+		{
+			$link_name = $dir_name."/index.html";
+		}
+
+		$overview{$dir_name} = "$lines_found,$lines_hit,$link_name";
+		$overall_found	+= $lines_found;
+		$overall_hit	+= $lines_hit;
+	}
+
+	# Generate overview page
+	info("Writing directory view page.\n");
+	open(*HTML_HANDLE, ">index.html")
+		or die("ERROR: cannot open index.html for writing!\n");
+	write_html_prolog(*HTML_HANDLE, "", "LCOV - $test_title");
+	write_header(*HTML_HANDLE, 0, "", "", $overall_found, $overall_hit);
+	write_file_table(*HTML_HANDLE, "", \%overview, {}, 0);
+	write_html_epilog(*HTML_HANDLE, "");
+	close(*HTML_HANDLE);
+
+	# Check if there are any test case descriptions to write out
+	if (%test_description)
+	{
+		info("Writing test case description file.\n");
+		write_description_file( \%test_description,
+					$overall_found, $overall_hit);
+	}
+
+	if ($overall_found == 0)
+	{
+		info("Warning: No lines found!\n");
+	}
+	else
+	{
+		info("Overall coverage rate: %d of %d lines (%.1f%%)\n",
+		     $overall_hit, $overall_found,
+		     $overall_hit*100/$overall_found);
+	}
+
+	chdir($cwd);
+}
+
+
+#
+# process_dir(dir_name)
+#
+
+sub process_dir($)
+{
+	my $abs_dir = $_[0];
+	my $trunc_dir;
+	my $rel_dir = $abs_dir;
+	my $base_dir;
+	my $filename;
+	my %overview;
+	my $lines_found;
+	my $lines_hit;
+	my $overall_found=0;
+	my $overall_hit=0;
+	my $base_name;
+	my $extension;
+	my $testdata;
+	my %testhash;
+	local *HTML_HANDLE;
+
+	# Remove prefix if applicable
+	if (!$no_prefix)
+	{
+		# Match directory name beginning with $dir_prefix
+	        $rel_dir = apply_prefix($rel_dir, $dir_prefix);
+	}
+
+	$trunc_dir = $rel_dir;
+
+	# Remove leading /
+	if ($rel_dir =~ /^\/(.*)$/)
+	{
+		$rel_dir = substr($rel_dir, 1);
+	}
+
+	$base_dir = get_relative_base_path($rel_dir);
+
+	create_sub_dir($rel_dir);
+	$abs_dir = escape_regexp($abs_dir);
+
+	# Match filenames which specify files in this directory, not including
+	# sub-directories
+	foreach $filename (grep(/^$abs_dir\/[^\/]*$/,keys(%info_data)))
+	{
+		($lines_found, $lines_hit, $testdata) =
+			process_file($trunc_dir, $rel_dir, $filename);
+
+		$base_name = basename($filename);
+
+		if ($no_sourceview)
+		{
+			# User asked as not to create source code view, do not
+			# provide a page link
+			$overview{$base_name} =
+				"$lines_found,$lines_hit";
+		}
+		elsif ($frames)
+		{
+			# Link to frameset page
+			$overview{$base_name} =
+				"$lines_found,$lines_hit,".
+				"$base_name.gcov.frameset.html";
+		}
+		else
+		{
+			# Link directory to source code view page
+			$overview{$base_name} =
+				"$lines_found,$lines_hit,".
+				"$base_name.gcov.html";
+		}
+
+		$testhash{$base_name} = $testdata;
+
+		$overall_found	+= $lines_found;
+		$overall_hit	+= $lines_hit;
+	}
+
+	# Generate directory overview page (without details)
+	open(*HTML_HANDLE, ">$rel_dir/index.html")
+		or die("ERROR: cannot open $rel_dir/index.html ".
+		       "for writing!\n");
+	write_html_prolog(*HTML_HANDLE, $base_dir,
+			  "LCOV - $test_title - $trunc_dir");
+	write_header(*HTML_HANDLE, 1, $trunc_dir, $rel_dir, $overall_found,
+		     $overall_hit);
+	write_file_table(*HTML_HANDLE, $base_dir, \%overview, {}, 1);
+	write_html_epilog(*HTML_HANDLE, $base_dir);
+	close(*HTML_HANDLE);
+
+	if ($show_details)
+	{
+		# Generate directory overview page including details
+		open(*HTML_HANDLE, ">$rel_dir/index-detail.html")
+			or die("ERROR: cannot open $rel_dir/".
+			       "index-detail.html for writing!\n");
+		write_html_prolog(*HTML_HANDLE, $base_dir,
+				  "LCOV - $test_title - $trunc_dir");
+		write_header(*HTML_HANDLE, 1, $trunc_dir, $rel_dir, $overall_found,
+			     $overall_hit);
+		write_file_table(*HTML_HANDLE, $base_dir, \%overview, \%testhash, 1);
+		write_html_epilog(*HTML_HANDLE, $base_dir);
+		close(*HTML_HANDLE);
+	}
+
+	# Calculate resulting line counts
+	return ($overall_found, $overall_hit);
+}
+
+
+#
+# process_file(trunc_dir, rel_dir, filename)
+#
+
+sub process_file($$$)
+{
+	info("Processing file ".apply_prefix($_[2], $dir_prefix)."\n");
+
+	my $trunc_dir = $_[0];
+	my $rel_dir = $_[1];
+	my $filename = $_[2];
+	my $base_name = basename($filename);
+	my $base_dir = get_relative_base_path($rel_dir);
+	my $testdata;
+	my $testcount;
+	my $sumcount;
+	my $funcdata;
+	my $lines_found;
+	my $lines_hit;
+	my @source;
+	my $pagetitle;
+	local *HTML_HANDLE;
+
+	($testdata, $sumcount, $funcdata, $lines_found, $lines_hit) =
+		get_info_entry($info_data{$filename});
+
+	# Return after this point in case user asked us not to generate
+	# source code view
+	if ($no_sourceview)
+	{
+		return ($lines_found, $lines_hit, $testdata);
+	}
+
+	# Generate source code view for this file
+	open(*HTML_HANDLE, ">$rel_dir/$base_name.gcov.html")
+		or die("ERROR: cannot open $rel_dir/$base_name.gcov.html ".
+		       "for writing!\n");
+	$pagetitle = "LCOV - $test_title - $trunc_dir/$base_name";
+	write_html_prolog(*HTML_HANDLE, $base_dir, $pagetitle);
+	write_header(*HTML_HANDLE, 2, "$trunc_dir/$base_name",
+		     "$rel_dir/$base_name", $lines_found, $lines_hit);
+	@source = write_source(*HTML_HANDLE, $filename, $sumcount);
+
+	write_html_epilog(*HTML_HANDLE, $base_dir, 1);
+	close(*HTML_HANDLE);
+
+	# Additional files are needed in case of frame output
+	if (!$frames)
+	{
+		return ($lines_found, $lines_hit, $testdata);
+	}
+
+	# Create overview png file
+	gen_png("$rel_dir/$base_name.gcov.png", $overview_width, " "x$tab_size,
+		@source);
+
+	# Create frameset page
+	open(*HTML_HANDLE, ">$rel_dir/$base_name.gcov.frameset.html")
+		or die("ERROR: cannot open ".
+		       "$rel_dir/$base_name.gcov.frameset.html".
+		       " for writing!\n");
+	write_frameset(*HTML_HANDLE, $base_dir, $base_name, $pagetitle);
+	close(*HTML_HANDLE);
+
+	# Write overview frame
+	open(*HTML_HANDLE, ">$rel_dir/$base_name.gcov.overview.html")
+		or die("ERROR: cannot open ".
+		       "$rel_dir/$base_name.gcov.overview.html".
+		       " for writing!\n");
+	write_overview(*HTML_HANDLE, $base_dir, $base_name, $pagetitle,
+		       scalar(@source));
+	close(*HTML_HANDLE);
+
+	return ($lines_found, $lines_hit, $testdata);
+}
+
+
+#
+# read_info_file(info_filename)
+#
+# Read in the contents of the .info file specified by INFO_FILENAME. Data will
+# be returned as a reference to a hash containing the following mappings:
+#
+# %result: for each filename found in file -> \%data
+#
+# %data: "test"  -> \%testdata
+#        "sum"   -> \%sumcount
+#        "func"  -> \%funcdata
+#        "found" -> $lines_found (number of instrumented lines found in file)
+#	 "hit"   -> $lines_hit (number of executed lines in file)
+#
+# %testdata: name of test affecting this file -> \%testcount
+#
+# %testcount: line number -> execution count for a single test
+# %sumcount : line number -> execution count for all tests
+# %funcdata : line number -> name of function beginning at that line
+# 
+# Note that .info file sections referring to the same file and test name
+# will automatically be combined by adding all execution counts.
+#
+# Note that if INFO_FILENAME ends with ".gz", it is assumed that the file
+# is compressed using GZIP. If available, GUNZIP will be used to decompress
+# this file.
+#
+# Die on error
+#
+
+sub read_info_file($)
+{
+	my $tracefile = $_[0];		# Name of tracefile
+	my %result;			# Resulting hash: file -> data
+	my $data;			# Data handle for current entry
+	my $testdata;			#       "             "
+	my $testcount;			#       "             "
+	my $sumcount;			#       "             "
+	my $funcdata;			#       "             "
+	my $line;			# Current line read from .info file
+	my $testname;			# Current test name
+	my $filename;			# Current filename
+	my $hitcount;			# Count for lines hit
+	local *INFO_HANDLE;		# Filehandle for .info file
+
+	# Check if file exists and is readable
+	stat($_[0]);
+	if (!(-r _))
+	{
+		die("ERROR: cannot read file $_[0]!\n");
+	}
+
+	# Check if this is really a plain file
+	if (!(-f _))
+	{
+		die("ERROR: not a plain file: $_[0]!\n");
+	}
+
+	# Check for .gz extension
+	if ($_[0] =~ /^(.*)\.gz$/)
+	{
+		# Check for availability of GZIP tool
+		system("gunzip -h >/dev/null 2>/dev/null")
+			and die("ERROR: gunzip command not available!\n");
+
+		# Check integrity of compressed file
+		system("gunzip -t $_[0] >/dev/null 2>/dev/null")
+			and die("ERROR: integrity check failed for ".
+				"compressed file $_[0]!\n");
+
+		# Open compressed file
+		open(INFO_HANDLE, "gunzip -c $_[0]|")
+			or die("ERROR: cannot start gunzip to uncompress ".
+			       "file $_[0]!\n");
+	}
+	else
+	{
+		# Open uncompressed file
+		open(INFO_HANDLE, $_[0])
+			or die("ERROR: cannot read file $_[0]!\n");
+	}
+
+	$testname = "";
+	while (<INFO_HANDLE>)
+	{
+		chomp($_);
+		$line = $_;
+
+		# Switch statement
+		foreach ($line)
+		{
+			/^TN:(\w+)/ && do
+			{
+				# Test name information found
+				$testname = $1;
+				last;
+			};
+
+			/^[SK]F:(.*)/ && do
+			{
+				# Filename information found
+				# Retrieve data for new entry
+				$filename = $1;
+
+				$data = $result{$filename};
+				($testdata, $sumcount, $funcdata) =
+					get_info_entry($data);
+
+				if (defined($testname))
+				{
+					$testcount = $testdata->{$testname};
+				}
+				else
+				{
+					my %new_hash;
+					$testcount = \%new_hash;
+				}
+				last;
+			};
+
+			/^DA:(\d+),(\d+)/ && do
+			{
+				# Execution count found, add to structure
+				# Add summary counts
+				$sumcount->{$1} += $2;
+
+				# Add test-specific counts
+				if (defined($testname))
+				{
+					$testcount->{$1} += $2;
+				}
+				last;
+			};
+
+			/^FN:(\d+),([^,]+)/ && do
+			{
+				# Function data found, add to structure
+				$funcdata->{$1} = $2;
+				last;
+			};
+
+			/^end_of_record/ && do
+			{
+				# Found end of section marker
+				if ($filename)
+				{
+					# Store current section data
+					if (defined($testname))
+					{
+						$testdata->{$testname} =
+							$testcount;
+					}
+					set_info_entry($data, $testdata,
+						       $sumcount, $funcdata);
+					$result{$filename} = $data;
+				}
+
+			};
+
+			# default
+			last;
+		}
+	}
+	close(INFO_HANDLE);
+
+	# Calculate lines_found and lines_hit for each file
+	foreach $filename (keys(%result))
+	{
+		$data = $result{$filename};
+
+		($testdata, $sumcount, $funcdata) = get_info_entry($data);
+
+		$data->{"found"} = scalar(keys(%{$sumcount}));
+		$hitcount = 0;
+
+		foreach (keys(%{$sumcount}))
+		{
+			if ($sumcount->{$_} >0) { $hitcount++; }
+		}
+
+		$data->{"hit"} = $hitcount;
+
+		$result{$filename} = $data;
+	}
+
+	if (scalar(keys(%result)) == 0)
+	{
+		die("ERROR: No valid records found in tracefile $tracefile\n");
+	}
+
+	return(\%result);
+}
+
+
+#
+# get_info_entry(hash_ref)
+#
+# Retrieve data from an entry of the structure generated by read_info_file().
+# Return a list of references to hashes:
+# (test data hash ref, sum count hash ref, funcdata hash ref, lines found,
+#  lines hit)
+#
+
+sub get_info_entry($)
+{
+	my $testdata_ref = $_[0]->{"test"};
+	my $sumcount_ref = $_[0]->{"sum"};
+	my $funcdata_ref = $_[0]->{"func"};
+	my $lines_found  = $_[0]->{"found"};
+	my $lines_hit    = $_[0]->{"hit"};
+
+	return ($testdata_ref, $sumcount_ref, $funcdata_ref, $lines_found,
+	        $lines_hit);
+}
+
+
+#
+# set_info_entry(hash_ref, testdata_ref, sumcount_ref, funcdata_ref[,
+#                lines_found, lines_hit])
+#
+# Update the hash referenced by HASH_REF with the provided data references.
+#
+
+sub set_info_entry($$$$;$$)
+{
+	my $data_ref = $_[0];
+
+	$data_ref->{"test"} = $_[1];
+	$data_ref->{"sum"} = $_[2];
+	$data_ref->{"func"} = $_[3];
+
+	if (defined($_[4])) { $data_ref->{"found"} = $_[4]; }
+	if (defined($_[5])) { $data_ref->{"hit"} = $_[5]; }
+}
+
+
+#
+# get_prefix(filename_list)
+#
+# Search FILENAME_LIST for a directory prefix which is common to as many
+# list entries as possible, so that removing this prefix will minimize the
+# sum of the lengths of all resulting shortened filenames.
+#
+
+sub get_prefix(@)
+{
+	my @filename_list = @_;		# provided list of filenames
+	my %prefix;			# mapping: prefix -> sum of lengths
+	my $current;			# Temporary iteration variable
+
+	# Find list of prefixes
+	foreach (@filename_list)
+	{
+		# Need explicit assignment to get a copy of $_ so that
+		# shortening the contained prefix does not affect the list
+		$current = shorten_prefix($_);
+		while ($current = shorten_prefix($current))
+		{
+			# Skip rest if the remaining prefix has already been
+			# added to hash
+			if ($prefix{$current}) { last; }
+
+			# Initialize with 0
+			$prefix{$current}="0";
+		}
+
+	}
+
+	# Calculate sum of lengths for all prefixes
+	foreach $current (keys(%prefix))
+	{
+		foreach (@filename_list)
+		{
+			# Add original length
+			$prefix{$current} += length($_);
+
+			# Check whether prefix matches
+			if (substr($_, 0, length($current)) eq $current)
+			{
+				# Subtract prefix length for this filename
+				$prefix{$current} -= length($current);
+			}
+		}
+	}
+
+	# Find and return prefix with minimal sum
+	$current = (keys(%prefix))[0];
+
+	foreach (keys(%prefix))
+	{
+		if ($prefix{$_} < $prefix{$current})
+		{
+			$current = $_;
+		}
+	}
+
+	return($current);
+}
+
+
+#
+# shorten_prefix(prefix)
+#
+# Return PREFIX shortened by last directory component.
+#
+
+sub shorten_prefix($)
+{
+	my @list = split("/", $_[0]);
+
+	pop(@list);
+	return join("/", @list);
+}
+
+
+
+#
+# get_dir_list(filename_list)
+#
+# Return sorted list of directories for each entry in given FILENAME_LIST.
+#
+
+sub get_dir_list(@)
+{
+	my %result;
+
+	foreach (@_)
+	{
+		$result{shorten_prefix($_)} = "";
+	}
+
+	return(sort(keys(%result)));
+}
+
+
+#
+# get_relative_base_path(subdirectory)
+#
+# Return a relative path string which references the base path when applied
+# in SUBDIRECTORY.
+#
+# Example: get_relative_base_path("fs/mm") -> "../../"
+#
+
+sub get_relative_base_path($)
+{
+	my $result = "";
+	my $index;
+
+	# Make an empty directory path a special case
+	if (!$_[0]) { return(""); }
+
+	# Count number of /s in path
+	$index = ($_[0] =~ s/\//\//g);
+
+	# Add a ../ to $result for each / in the directory path + 1
+	for (; $index>=0; $index--)
+	{
+		$result .= "../";
+	}
+
+	return $result;
+}
+
+
+#
+# read_testfile(test_filename)
+#
+# Read in file TEST_FILENAME which contains test descriptions in the format:
+#
+#   TN:<whitespace><test name>
+#   TD:<whitespace><test description>
+#
+# for each test case. Return a reference to a hash containing a mapping
+#
+#   test name -> test description.
+#
+# Die on error.
+#
+
+sub read_testfile($)
+{
+	my %result;
+	my $test_name;
+	local *TEST_HANDLE;
+
+	open(TEST_HANDLE, "<".$_[0])
+		or die("ERROR: cannot open $_[0]!\n");
+
+	while (<TEST_HANDLE>)
+	{
+		chomp($_);
+
+		# Match lines beginning with TN:<whitespace(s)>
+		if (/^TN:\s+(.*?)\s*$/)
+		{
+			# Store name for later use
+			$test_name = $1;
+		}
+
+		# Match lines beginning with TD:<whitespace(s)>
+		if (/^TD:\s+(.*?)\s*$/)
+		{
+			# Check for empty line
+			if ($1)
+			{
+				# Add description to hash
+				$result{$test_name} .= " $1";
+			}
+			else
+			{
+				# Add empty line
+				$result{$test_name} .= "\n\n";
+			}
+		}
+	}
+
+	close(TEST_HANDLE);
+
+	return \%result;
+}
+
+
+#
+# escape_html(STRING)
+#
+# Return a copy of STRING in which all occurrences of HTML special characters
+# are escaped.
+#
+
+sub escape_html($)
+{
+	my $string = $_[0];
+
+	if (!$string) { return ""; }
+
+	$string =~ s/&/&amp;/g;		# & -> &amp;
+	$string =~ s/</&lt;/g;		# < -> &lt;
+	$string =~ s/>/&gt;/g;		# > -> &gt;
+	$string =~ s/\"/&quot;/g;	# " -> &quot;
+
+	while ($string =~ /^([^\t]*)(\t)/)
+	{
+		my $replacement = " "x($tab_size - (length($1) % $tab_size));
+		$string =~ s/^([^\t]*)(\t)/$1$replacement/;
+	}
+
+	$string =~ s/\n/<br>/g;		# \n -> <br>
+
+	return $string;
+}
+
+
+#
+# get_date_string()
+#
+# Return the current date in the form: yyyy-mm-dd
+#
+
+sub get_date_string()
+{
+	my $year;
+	my $month;
+	my $day;
+
+	($year, $month, $day) = (localtime())[5, 4, 3];
+
+	return sprintf("%d-%02d-%02d", $year+1900, $month+1, $day);
+}
+
+
+#
+# create_sub_dir(dir_name)
+#
+# Create subdirectory DIR_NAME if it does not already exist, including all its
+# parent directories.
+#
+# Die on error.
+#
+
+sub create_sub_dir($)
+{
+	system("mkdir -p $_[0]")
+		and die("ERROR: cannot create directory $_!\n");
+}
+
+
+#
+# write_description_file(descriptions, overall_found, overall_hit)
+#
+# Write HTML file containing all test case descriptions. DESCRIPTIONS is a
+# reference to a hash containing a mapping
+#
+#   test case name -> test case description
+#
+# Die on error.
+#
+
+sub write_description_file($$$)
+{
+	my %description = %{$_[0]};
+	my $found = $_[1];
+	my $hit = $_[2];
+	my $test_name;
+	local *HTML_HANDLE;
+
+	open(HTML_HANDLE, ">descriptions.html")
+		or die("ERROR: cannot open descriptions.html for writing!\n");
+
+	write_html_prolog(*HTML_HANDLE, "", "LCOV - test case descriptions");
+	write_header(*HTML_HANDLE, 3, "", "", $found, $hit);
+
+	write_test_table_prolog(*HTML_HANDLE,
+			 "Test case descriptions - alphabetical list");
+
+	foreach $test_name (sort(keys(%description)))
+	{
+		write_test_table_entry(*HTML_HANDLE, $test_name,
+				       escape_html($description{$test_name}));
+	}
+
+	write_test_table_epilog(*HTML_HANDLE);
+	write_html_epilog(*HTML_HANDLE, "");
+
+	close(HTML_HANDLE);
+}
+
+
+
+#
+# write_png_files()
+#
+# Create all necessary .png files for the HTML-output in the current
+# directory. .png-files are used as bar graphs.
+#
+# Die on error.
+#
+
+sub write_png_files()
+{
+	my %data;
+	local *PNG_HANDLE;
+
+	$data{"ruby.png"} =
+		[0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 
+		 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 
+		 0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x00, 0x00, 0x00, 0x25, 
+		 0xdb, 0x56, 0xca, 0x00, 0x00, 0x00, 0x07, 0x74, 0x49, 0x4d, 
+		 0x45, 0x07, 0xd2, 0x07, 0x11, 0x0f, 0x18, 0x10, 0x5d, 0x57, 
+		 0x34, 0x6e, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 
+		 0x00, 0x00, 0x0b, 0x12, 0x00, 0x00, 0x0b, 0x12, 0x01, 0xd2, 
+		 0xdd, 0x7e, 0xfc, 0x00, 0x00, 0x00, 0x04, 0x67, 0x41, 0x4d, 
+		 0x41, 0x00, 0x00, 0xb1, 0x8f, 0x0b, 0xfc, 0x61, 0x05, 0x00, 
+		 0x00, 0x00, 0x06, 0x50, 0x4c, 0x54, 0x45, 0xff, 0x35, 0x2f, 
+		 0x00, 0x00, 0x00, 0xd0, 0x33, 0x9a, 0x9d, 0x00, 0x00, 0x00, 
+		 0x0a, 0x49, 0x44, 0x41, 0x54, 0x78, 0xda, 0x63, 0x60, 0x00, 
+		 0x00, 0x00, 0x02, 0x00, 0x01, 0xe5, 0x27, 0xde, 0xfc, 0x00, 
+		 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 
+		 0x82];
+	$data{"amber.png"} =
+		[0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 
+		 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 
+		 0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x00, 0x00, 0x00, 0x25, 
+		 0xdb, 0x56, 0xca, 0x00, 0x00, 0x00, 0x07, 0x74, 0x49, 0x4d, 
+		 0x45, 0x07, 0xd2, 0x07, 0x11, 0x0f, 0x28, 0x04, 0x98, 0xcb, 
+		 0xd6, 0xe0, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 
+		 0x00, 0x00, 0x0b, 0x12, 0x00, 0x00, 0x0b, 0x12, 0x01, 0xd2, 
+		 0xdd, 0x7e, 0xfc, 0x00, 0x00, 0x00, 0x04, 0x67, 0x41, 0x4d, 
+		 0x41, 0x00, 0x00, 0xb1, 0x8f, 0x0b, 0xfc, 0x61, 0x05, 0x00, 
+		 0x00, 0x00, 0x06, 0x50, 0x4c, 0x54, 0x45, 0xff, 0xe0, 0x50, 
+		 0x00, 0x00, 0x00, 0xa2, 0x7a, 0xda, 0x7e, 0x00, 0x00, 0x00, 
+		 0x0a, 0x49, 0x44, 0x41, 0x54, 0x78, 0xda, 0x63, 0x60, 0x00, 
+	  	 0x00, 0x00, 0x02, 0x00, 0x01, 0xe5, 0x27, 0xde, 0xfc, 0x00, 
+  		 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 
+  		 0x82];
+	$data{"emerald.png"} =
+		[0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 
+		 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 
+		 0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x00, 0x00, 0x00, 0x25, 
+		 0xdb, 0x56, 0xca, 0x00, 0x00, 0x00, 0x07, 0x74, 0x49, 0x4d, 
+		 0x45, 0x07, 0xd2, 0x07, 0x11, 0x0f, 0x22, 0x2b, 0xc9, 0xf5, 
+		 0x03, 0x33, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 
+		 0x00, 0x00, 0x0b, 0x12, 0x00, 0x00, 0x0b, 0x12, 0x01, 0xd2, 
+		 0xdd, 0x7e, 0xfc, 0x00, 0x00, 0x00, 0x04, 0x67, 0x41, 0x4d, 
+		 0x41, 0x00, 0x00, 0xb1, 0x8f, 0x0b, 0xfc, 0x61, 0x05, 0x00, 
+		 0x00, 0x00, 0x06, 0x50, 0x4c, 0x54, 0x45, 0x1b, 0xea, 0x59, 
+		 0x0a, 0x0a, 0x0a, 0x0f, 0xba, 0x50, 0x83, 0x00, 0x00, 0x00, 
+		 0x0a, 0x49, 0x44, 0x41, 0x54, 0x78, 0xda, 0x63, 0x60, 0x00, 
+		 0x00, 0x00, 0x02, 0x00, 0x01, 0xe5, 0x27, 0xde, 0xfc, 0x00, 
+		 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 
+		 0x82];
+	$data{"snow.png"} =
+		[0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 
+		 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 
+		 0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x00, 0x00, 0x00, 0x25, 
+		 0xdb, 0x56, 0xca, 0x00, 0x00, 0x00, 0x07, 0x74, 0x49, 0x4d, 
+		 0x45, 0x07, 0xd2, 0x07, 0x11, 0x0f, 0x1e, 0x1d, 0x75, 0xbc, 
+		 0xef, 0x55, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 
+		 0x00, 0x00, 0x0b, 0x12, 0x00, 0x00, 0x0b, 0x12, 0x01, 0xd2, 
+		 0xdd, 0x7e, 0xfc, 0x00, 0x00, 0x00, 0x04, 0x67, 0x41, 0x4d, 
+		 0x41, 0x00, 0x00, 0xb1, 0x8f, 0x0b, 0xfc, 0x61, 0x05, 0x00, 
+		 0x00, 0x00, 0x06, 0x50, 0x4c, 0x54, 0x45, 0xff, 0xff, 0xff, 
+		 0x00, 0x00, 0x00, 0x55, 0xc2, 0xd3, 0x7e, 0x00, 0x00, 0x00, 
+		 0x0a, 0x49, 0x44, 0x41, 0x54, 0x78, 0xda, 0x63, 0x60, 0x00, 
+		 0x00, 0x00, 0x02, 0x00, 0x01, 0xe5, 0x27, 0xde, 0xfc, 0x00, 
+		 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 
+		 0x82];
+	$data{"glass.png"} =
+		[0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 
+		 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 
+		 0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x00, 0x00, 0x00, 0x25, 
+		 0xdb, 0x56, 0xca, 0x00, 0x00, 0x00, 0x04, 0x67, 0x41, 0x4d, 
+		 0x41, 0x00, 0x00, 0xb1, 0x8f, 0x0b, 0xfc, 0x61, 0x05, 0x00, 
+		 0x00, 0x00, 0x06, 0x50, 0x4c, 0x54, 0x45, 0xff, 0xff, 0xff, 
+		 0x00, 0x00, 0x00, 0x55, 0xc2, 0xd3, 0x7e, 0x00, 0x00, 0x00, 
+		 0x01, 0x74, 0x52, 0x4e, 0x53, 0x00, 0x40, 0xe6, 0xd8, 0x66, 
+		 0x00, 0x00, 0x00, 0x01, 0x62, 0x4b, 0x47, 0x44, 0x00, 0x88, 
+		 0x05, 0x1d, 0x48, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 
+		 0x73, 0x00, 0x00, 0x0b, 0x12, 0x00, 0x00, 0x0b, 0x12, 0x01, 
+		 0xd2, 0xdd, 0x7e, 0xfc, 0x00, 0x00, 0x00, 0x07, 0x74, 0x49, 
+		 0x4d, 0x45, 0x07, 0xd2, 0x07, 0x13, 0x0f, 0x08, 0x19, 0xc4, 
+		 0x40, 0x56, 0x10, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41, 
+		 0x54, 0x78, 0x9c, 0x63, 0x60, 0x00, 0x00, 0x00, 0x02, 0x00, 
+		 0x01, 0x48, 0xaf, 0xa4, 0x71, 0x00, 0x00, 0x00, 0x00, 0x49, 
+		 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82];
+
+	foreach (keys(%data))
+	{
+		open(PNG_HANDLE, ">".$_)
+			or die("ERROR: cannot create $_!\n");
+		binmode(PNG_HANDLE);
+		print(PNG_HANDLE map(chr,@{$data{$_}}));
+		close(PNG_HANDLE);
+	}
+}
+
+
+#
+# write_css_file()
+#
+# Write the cascading style sheet file gcov.css to the current directory.
+# This file defines basic layout attributes of all generated HTML pages.
+#
+
+sub write_css_file()
+{
+	local *CSS_HANDLE;
+
+	# Check for a specified external style sheet file
+	if ($css_filename)
+	{
+		# Simply copy that file
+		system("cp $css_filename gcov.css 2>/dev/null")
+			and die("ERROR: Cannot copy file $css_filename!\n");
+		return;
+	}
+
+	open(CSS_HANDLE, ">gcov.css")
+		or die ("ERROR: cannot open gcov.css for writing!\n");
+
+
+	# *************************************************************
+
+	my $css_data = ($_=<<"END_OF_CSS")
+	/* All views: initial background and text color */
+	body
+	{
+	  color:            #000000;
+	  background-color: #FFFFFF;
+	}
+
+
+	/* All views: standard link format*/
+	a:link
+	{
+	  color:           #284FA8;
+	  text-decoration: underline;
+	}
+
+
+	/* All views: standard link - visited format */
+	a:visited
+	{
+	  color:           #00CB40;
+	  text-decoration: underline;
+	}
+
+
+	/* All views: standard link - activated format */
+	a:active
+	{
+	  color:           #FF0040;
+	  text-decoration: underline;
+	}
+
+
+	/* All views: main title format */
+	td.title
+	{
+	  text-align:     center;
+	  padding-bottom: 10px;
+	  font-family:    sans-serif;
+	  font-size:      20pt;
+	  font-style:     italic;
+	  font-weight:    bold;
+	}
+
+
+	/* All views: header item format */
+	td.headerItem
+	{
+	  text-align:    right;
+	  padding-right: 6px;
+	  font-family:   sans-serif;
+	  font-weight:   bold;
+	}
+
+
+	/* All views: header item value format */
+	td.headerValue
+	{
+	  text-align:  left;
+	  color:       #284FA8;
+	  font-family: sans-serif;
+	  font-weight: bold;
+	}
+
+
+	/* All views: color of horizontal ruler */
+	td.ruler
+	{
+	  background-color: #6688D4;
+	}
+
+
+	/* All views: version string format */
+	td.versionInfo
+	{
+	  text-align:   center;
+	  padding-top:  2px;
+	  font-family:  sans-serif;
+	  font-style:   italic;
+	}
+
+
+	/* Directory view/File view (all)/Test case descriptions:
+	   table headline format */
+	td.tableHead
+	{
+	  text-align:       center;
+	  color:            #FFFFFF;
+	  background-color: #6688D4;
+	  font-family:      sans-serif;
+	  font-size:        120%;
+	  font-weight:      bold;
+	}
+
+
+	/* Directory view/File view (all): filename entry format */
+	td.coverFile
+	{
+	  text-align:       left;
+	  padding-left:     10px;
+	  padding-right:    20px; 
+	  color:            #284FA8;
+	  background-color: #DAE7FE;
+	  font-family:      monospace;
+	}
+
+
+	/* Directory view/File view (all): bar-graph entry format*/
+	td.coverBar
+	{
+	  padding-left:     10px;
+	  padding-right:    10px;
+	  background-color: #DAE7FE;
+	}
+
+
+	/* Directory view/File view (all): bar-graph outline color */
+	td.coverBarOutline
+	{
+	  background-color: #000000;
+	}
+
+
+	/* Directory view/File view (all): percentage entry for files with
+	   high coverage rate */
+	td.coverPerHi
+	{
+	  text-align:       right;
+	  padding-left:     10px;
+	  padding-right:    10px;
+	  background-color: #DAE7FE;
+	  font-weight:      bold;
+	}
+
+
+	/* Directory view/File view (all): line count entry for files with
+	   high coverage rate */
+	td.coverNumHi
+	{
+	  text-align:       right;
+	  padding-left:     10px;
+	  padding-right:    10px;
+	  background-color: #DAE7FE;
+	}
+
+
+	/* Directory view/File view (all): percentage entry for files with
+	   medium coverage rate */
+	td.coverPerMed
+	{
+	  text-align:       right;
+	  padding-left:     10px;
+	  padding-right:    10px;
+	  background-color: #FFEA20;
+	  font-weight:      bold;
+	}
+
+
+	/* Directory view/File view (all): line count entry for files with
+	   medium coverage rate */
+	td.coverNumMed
+	{
+	  text-align:       right;
+	  padding-left:     10px;
+	  padding-right:    10px;
+	  background-color: #FFEA20;
+	}
+
+
+	/* Directory view/File view (all): percentage entry for files with
+	   low coverage rate */
+	td.coverPerLo
+	{
+	  text-align:       right;
+	  padding-left:     10px;
+	  padding-right:    10px;
+	  background-color: #FF0000;
+	  font-weight:      bold;
+	}
+
+
+	/* Directory view/File view (all): line count entry for files with
+	   low coverage rate */
+	td.coverNumLo
+	{
+	  text-align:       right;
+	  padding-left:     10px;
+	  padding-right:    10px;
+	  background-color: #FF0000;
+	}
+
+
+	/* File view (all): "show/hide details" link format */
+	a.detail:link
+	{
+	  color: #B8D0FF;
+	}
+
+
+	/* File view (all): "show/hide details" link - visited format */
+	a.detail:visited
+	{
+	  color: #B8D0FF;
+	}
+
+
+	/* File view (all): "show/hide details" link - activated format */
+	a.detail:active
+	{
+	  color: #FFFFFF;
+	}
+
+
+	/* File view (detail): test name table headline format */
+	td.testNameHead
+	{
+	  text-align:       right;
+	  padding-right:    10px;
+	  background-color: #DAE7FE;
+	  font-family:      sans-serif;
+	  font-weight:      bold;
+	}
+
+
+	/* File view (detail): test lines table headline format */
+	td.testLinesHead
+	{
+	  text-align:       center;
+	  background-color: #DAE7FE;
+	  font-family:      sans-serif;
+	  font-weight:      bold;
+	}
+
+
+	/* File view (detail): test name entry */
+	td.testName
+	{
+	  text-align:       right;
+	  padding-right:    10px;
+	  background-color: #DAE7FE;
+	}
+
+
+	/* File view (detail): test percentage entry */
+	td.testPer
+	{
+	  text-align:       right;
+	  padding-left:     10px;
+	  padding-right:    10px; 
+	  background-color: #DAE7FE;
+	}
+
+
+	/* File view (detail): test lines count entry */
+	td.testNum
+	{
+	  text-align:       right;
+	  padding-left:     10px;
+	  padding-right:    10px; 
+	  background-color: #DAE7FE;
+	}
+
+
+	/* Test case descriptions: test name format*/
+	dt
+	{
+	  font-family: sans-serif;
+	  font-weight: bold;
+	}
+
+
+	/* Test case descriptions: description table body */
+	td.testDescription
+	{
+	  padding-top:      10px;
+	  padding-left:     30px;
+	  padding-bottom:   10px;
+	  padding-right:    30px;
+	  background-color: #DAE7FE;
+	}
+
+
+	/* Source code view: source code format */
+	pre.source
+	{
+	  font-family: monospace;
+	  white-space: pre;
+	}
+
+	/* Source code view: line number format */
+	span.lineNum
+	{
+	  background-color: #EFE383;
+	}
+
+
+	/* Source code view: format for lines which were executed */
+	span.lineCov
+	{
+	  background-color: #CAD7FE;
+	}
+
+
+	/* Source code view: format for lines which were not executed */
+	span.lineNoCov
+	{
+	  background-color: #FF6230;
+	}
+END_OF_CSS
+	;
+
+	# *************************************************************
+
+
+	# Remove leading tab from all lines
+	$css_data =~ s/^\t//gm;
+
+	print(CSS_HANDLE $css_data);
+
+	close(CSS_HANDLE);
+}
+
+
+#
+# get_bar_graph_code(base_dir, cover_found, cover_hit)
+#
+# Return a string containing HTML code which implements a bar graph display
+# for a coverage rate of cover_hit * 100 / cover_found.
+#
+
+sub get_bar_graph_code($$$)
+{
+	my $rate;
+	my $alt;
+	my $width;
+	my $remainder;
+	my $png_name;
+	my $graph_code;
+
+	# Check number of instrumented lines
+	if ($_[1] == 0) { return ""; }
+
+	$rate		= $_[2] * 100 / $_[1];
+	$alt		= sprintf("%.1f", $rate)."%";
+	$width		= sprintf("%.0f", $rate);
+	$remainder	= sprintf("%d", 100-$width);
+
+	# Decide which .png file to use
+	if ($rate < $med_limit)		{ $png_name = "ruby.png"; }
+	elsif ($rate < $hi_limit)	{ $png_name = "amber.png"; }
+	else				{ $png_name = "emerald.png"; }
+
+	if ($width == 0)
+	{
+		# Zero coverage
+		$graph_code = (<<END_OF_HTML)
+	        <table border=0 cellspacing=0 cellpadding=1><tr><td class="coverBarOutline"><img src="$_[0]snow.png" width=100 height=10 alt="$alt"></td></tr></table>
+END_OF_HTML
+		;
+	}
+	elsif ($width == 100)
+	{
+		# Full coverage
+		$graph_code = (<<END_OF_HTML)
+		<table border=0 cellspacing=0 cellpadding=1><tr><td class="coverBarOutline"><img src="$_[0]$png_name" width=100 height=10 alt="$alt"></td></tr></table>
+END_OF_HTML
+		;
+	}
+	else
+	{
+		# Positive coverage
+		$graph_code = (<<END_OF_HTML)
+		<table border=0 cellspacing=0 cellpadding=1><tr><td class="coverBarOutline"><img src="$_[0]$png_name" width=$width height=10 alt="$alt"><img src="$_[0]snow.png" width=$remainder height=10 alt="$alt"></td></tr></table>
+END_OF_HTML
+		;
+	}
+
+	# Remove leading tabs from all lines
+	$graph_code =~ s/^\t+//gm;
+	chomp($graph_code);
+
+	return($graph_code);
+}
+
+
+#
+# write_html(filehandle, html_code)
+#
+# Write out HTML_CODE to FILEHANDLE while removing a leading tabulator mark
+# in each line of HTML_CODE.
+#
+
+sub write_html(*$)
+{
+	local *HTML_HANDLE = $_[0];
+	my $html_code = $_[1];
+
+	# Remove leading tab from all lines
+	$html_code =~ s/^\t//gm;
+
+	print(HTML_HANDLE $html_code)
+		or die("ERROR: cannot write HTML data ($!)\n");
+}
+
+
+#
+# write_html_prolog(filehandle, base_dir, pagetitle)
+#
+# Write an HTML prolog common to all HTML files to FILEHANDLE. PAGETITLE will
+# be used as HTML page title. BASE_DIR contains a relative path which points
+# to the base directory.
+#
+