Added jenkins testing infrastructure.
authoralfongj <alfongj@gmail.com>
Thu, 24 Apr 2014 02:04:19 +0000 (19:04 -0700)
committerBarret Rhoden <brho@cs.berkeley.edu>
Mon, 5 May 2014 23:23:31 +0000 (16:23 -0700)
Contains:
1. launcher.sh: Bash script in charge of compilation and running
   all the testing components.
2. changes.py and compilation_components.json: Script that checks
   for changes (via a git diff) since last commit and decides
   which components of akaros to compile.
3. qemu_launcher.c and Makelocal_qemu: Wrapper for launching qemu
   for akaros in a way that is killable from jenkins without being
   root.
4. wait_until.py: Script that checks for a given line in akaros
   output in order to know once it finished testing and stuff so
   we can kill it.
5. test_reporter.py and markup.py: Scripts for parsing akaros
   output for test output and then reporting it in XUnit XML
   format.

tools/jenkins/config/Makelocal_qemu [new file with mode: 0644]
tools/jenkins/config/compilation_components.json [new file with mode: 0644]
tools/jenkins/launcher.sh [new file with mode: 0755]
tools/jenkins/utils/changes.py [new file with mode: 0755]
tools/jenkins/utils/markup.py [new file with mode: 0644]
tools/jenkins/utils/qemu_launcher.c [new file with mode: 0644]
tools/jenkins/utils/test_reporter.py [new file with mode: 0755]
tools/jenkins/utils/wait_until.py [new file with mode: 0755]

diff --git a/tools/jenkins/config/Makelocal_qemu b/tools/jenkins/config/Makelocal_qemu
new file mode 100644 (file)
index 0000000..9e19a45
--- /dev/null
@@ -0,0 +1,16 @@
+NETWORK_CARD ?= rtl8139
+CPU_MODEL ?= Nehalem
+NUM_CPUS ?= 8
+MEMORY_SIZE ?= 1024
+KERNEL_BIN ?= obj/kern/akaros-kernel
+ENABLE_KVM ?= #-enable-kvm
+
+QEMU_NETWORK = -net nic,model=$(NETWORK_CARD) -net user,hostfwd=tcp::5555-:23
+
+qemu: all qemu-run 
+
+# PLEASE NOTE: This may fail if qemu_launcher does not run with sudo permissions.
+# See http://www.tuxation.com/setuid-on-shell-scripts.html
+qemu-run:
+       qemu_launcher -s $(ENABLE_KVM) $(QEMU_NETWORK) -cpu $(CPU_MODEL) \
+       -smp $(NUM_CPUS) -m $(MEMORY_SIZE) -kernel $(KERNEL_BIN) -nographic
diff --git a/tools/jenkins/config/compilation_components.json b/tools/jenkins/config/compilation_components.json
new file mode 100644 (file)
index 0000000..49fa664
--- /dev/null
@@ -0,0 +1,39 @@
+{
+    "_comment" : "Please see utils/changes.py for explanation of this file.",
+    "compilation_components" : {
+        "cross-compiler": {
+            "PATHS": [
+                "./tools/compilers/+",
+                "./kern/include/ros/+",
+                "./kern/arch/{{arch}}/ros/+",
+                "./user/parlib/include/+",
+                "./user/pthread/+"
+            ]
+        },
+        "kernel": {
+            "PATHS": [
+                "./",
+                "./kern/arch/+",
+                "./kern/drivers/+",
+                "./kern/include/+",
+                "./kern/kfs/+",
+                "./kern/src/+",
+                "./scripts/kconfig/+"
+            ]
+        },
+        "userspace": {
+            "PATHS": [
+                "./",
+                "./kern/include/ros/+",
+                "./user/+",
+                "./tests/+",
+                "./scripts/kconfig/+"
+            ]
+        },
+        "busybox": {
+            "PATHS": [
+                "./tools/patches/busybox/+"
+            ]
+        }
+    }
+}
diff --git a/tools/jenkins/launcher.sh b/tools/jenkins/launcher.sh
new file mode 100755 (executable)
index 0000000..2fff737
--- /dev/null
@@ -0,0 +1,311 @@
+#!/bin/bash
+# This script should be called from Jenkins when a new commit has been pushed 
+# to the repo. 
+# It analyzes what parts of the codebase have been modified, compiles everything
+# that is needed, and reports on the results. 
+
+set -e
+
+readonly TMP_DIR=tmp
+readonly DIFF_FILE=$TMP_DIR/changes.txt
+readonly AKAROS_OUTPUT_FILE=$TMP_DIR/akaros_out.txt
+readonly TEST_OUTPUT_DIR=output-tests
+readonly TEST_DIR=tools/jenkins
+readonly SCR_DIR=tools/jenkins/utils
+readonly DOWNLOADS_DIR=dl
+
+# Config files
+readonly CONF_DIR=tools/jenkins/config
+readonly CONF_COMP_COMPONENTS_FILE=$CONF_DIR/compilation_components.json
+
+# Utility scripts
+readonly SCR_WAIT_UNTIL=$SCR_DIR/wait_until.py
+readonly SCR_GIT_CHANGES=$SCR_DIR/changes.py
+readonly SCR_GEN_TEST_REPORTS=$SCR_DIR/test_reporter.py
+
+# Busybox settings
+readonly BUSYBOX_VERSION=1.17.3
+readonly BUSYBOX_DL_URL=http://www.busybox.net/downloads/busybox-1.17.3.tar.bz2
+readonly BUSYBOX_CONF_FILE=tools/patches/busybox/busybox-1.17.3-config
+
+################################################################################
+###############                   INITIAL SETUP                  ###############
+################################################################################
+
+if [ "$INITIAL_SETUP" == true ]; then
+       echo -e "\n[INITIAL_SETUP]: Begin"
+       # Create directory for tests and other temp files.
+       mkdir -p $TMP_DIR
+       mkdir -p $TEST_OUTPUT_DIR
+       mkdir -p $DOWNLOADS_DIR
+
+       # Compile QEMU launcher
+       mkdir -p $WORKSPACE/install/qemu_launcher/
+       gcc $SCR_DIR/qemu_launcher.c -o install/qemu_launcher/qemu_launcher
+
+       echo "* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *"
+       echo "Set up finished succesfully."
+       echo "Please run sudo chown root:root install/qemu_launcher/qemu_launcher"
+       echo "Please run sudo chmod 4755 install/qemu_launcher/qemu_launcher"
+       echo "* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *"
+       echo ""
+       echo -e "[INITIAL_SETUP]: End\n"
+       exit 0
+fi
+
+
+
+################################################################################
+###############                 PRE BUILD SETUP                  ###############
+################################################################################
+
+function add_cross_compiler_to_path() {
+       export PATH=$WORKSPACE/install/riscv-ros-gcc/bin:$PATH
+       export PATH=$WORKSPACE/install/i686-ros-gcc/bin:$PATH
+       export PATH=$WORKSPACE/install/x86_64-ros-gcc/bin:$PATH
+}
+
+# Clean these two directories
+rm $TMP_DIR/* $TEST_OUTPUT_DIR/* -f
+add_cross_compiler_to_path
+
+
+################################################################################
+###############                    COMPILATION                   ###############
+################################################################################
+
+function build_config() {
+       echo -e "\n[SET_MAKE_CONFIG]: Begin"
+
+       # Begin with default configuration.
+       case "$COMPILATION_ARCH" in
+       RISCV)  make ARCH=riscv defconfig
+           ;;
+       I686)  make ARCH=x86 defconfig
+                  sed -i -e 's/CONFIG_64BIT=y/# CONFIG_64BIT is not set/' \
+                         -e 's/# CONFIG_X86_32 is not set/CONFIG_X86_32=y/' \
+                         -e 's/CONFIG_X86_64=y/# CONFIG_X86_64 is not set/' \
+                         .config
+           ;;
+       X86_64)  make ARCH=x86 defconfig
+           ;;
+       esac
+
+       # Enable postboot kernel tests to run.
+       # These don't take much to execute so we can run them always and just parse
+       # results if needed.
+       echo "CONFIG_POSTBOOT_KERNEL_TESTING=y" >> .config
+
+       echo -e "[SET_MAKE_CONFIG]: End\n"
+}
+
+function build_cross_compiler() {
+       declare -A ARCH_SUBDIRS=( ["RISCV"]="riscv-ros-gcc" \
+                                 ["I686"]="i686-ros-gcc" \
+                                 ["X86_64"]="x86_64-ros-gcc" )
+
+       echo -e "\n[BUILD_CROSS_COMPILER]: Begin"
+
+       cd tools/compilers/gcc-glibc
+
+       # Clean everything up
+       # TODO: Possibly down the line try to optimize this to only clean the 
+       # architecture that we need to rebuild.
+       make clean
+
+       # Define cross compiler Makelocal.
+       echo "# Number of make jobs to spawn.  
+MAKE_JOBS := 3
+RISCV_INSTDIR         := $WORKSPACE/install/${ARCH_SUBDIRS["RISCV"]}/
+I686_INSTDIR          := $WORKSPACE/install/${ARCH_SUBDIRS["I686"]}/
+X86_64_INSTDIR        := $WORKSPACE/install/${ARCH_SUBDIRS["X86_64"]}/
+" > Makelocal
+
+       # Create / clean directory where the cross compiler will be installed.
+       CROSS_COMP_DIR=$WORKSPACE/install/${ARCH_SUBDIRS["$COMPILATION_ARCH"]}/
+       mkdir -p CROSS_COMP_DIR
+       rm -rf CROSS_COMP_DIR*
+
+       # Compile cross compiler.
+       case "$COMPILATION_ARCH" in
+       RISCV)  make riscv
+           ;;
+       I686)  make i686
+           ;;
+       X86_64)  make x86_64
+           ;;
+       esac
+
+       # Go back to root directory.
+       cd ../../..
+       echo -e "[BUILD_CROSS_COMPILER]: End\n"
+}
+
+function build_kernel() {
+       echo -e "\n[BUILD_KERNEL]: Begin"
+       make clean
+       make
+       echo -e "[BUILD_KERNEL]: End\n"
+}
+
+function build_userspace() {
+       echo -e "\n[BUILD_USERSPACE]: Begin"
+       # This is needed because of a bug that won't let tests to be compiled
+       # unless the following files are present.
+       cd kern/kfs/bin
+       touch busybox
+       touch chmod
+       cd -
+
+       # Build and install user libs.
+       make userclean
+       make install-libs
+
+       # Compile tests.
+       make testclean
+       make tests
+
+       # Fill memory with tests.
+       make fill-kfs
+
+       # Recompile kernel.
+       make
+       echo -e "[BUILD_USERSPACE]: End\n"
+}
+
+function build_busybox() {
+       echo -e "\n[BUILD_BUSYBOX]: Begin"
+       
+       BUSYBOX_DIR=busybox-$BUSYBOX_VERSION
+       
+       cd $DOWNLOADS_DIR
+       
+       # Download busybox if we do not have it yet.
+       if [[ ! -d "$BUSYBOX_DIR" ]]; then
+               echo "Trying to download from $BUSYBOX_DL_URL ..."
+               
+               wget $BUSYBOX_DL_URL -O busybox-$BUSYBOX_VERSION.tar.bz2
+               tar -jxvf busybox-$BUSYBOX_VERSION.tar.bz2
+               rm busybox-$BUSYBOX_VERSION.tar.bz2
+               cp ../$BUSYBOX_CONF_FILE $BUSYBOX_DIR/.config
+       fi
+
+       # Build busybox and copy it into kfs
+       cd $BUSYBOX_DIR
+       make
+       cp busybox_unstripped ../../kern/kfs/bin/busybox
+       cd ../../
+
+       # Recompile kernel to include busybox
+       make
+
+       echo -e "[BUILD_BUSYBOX]: End\n"
+}
+
+# TODO: This won't work for RISCV, it must be changed to whatever is used.
+function run_qemu() {
+       echo -e "\n[RUN_AKAROS_IN_QEMU]: Begin"
+
+       echo "-include $CONF_DIR/Makelocal_qemu" > Makelocal
+       export PATH=$WORKSPACE/install/qemu_launcher/:$PATH
+       make qemu > $AKAROS_OUTPUT_FILE &
+       MAKE_PID=$!
+
+       # TODO: Rather than finishing after Kernel PB Tests, put a generic 
+       #       "C'est fini" statement somewhere and look for it
+       WAIT_RESULT=`$SCR_WAIT_UNTIL $AKAROS_OUTPUT_FILE END_KERNEL_POSTBOOT_TESTS \
+           ${MAX_RUN_TIME:-100}`
+
+       # Extract Qemu_launcher PID
+       QEMU_PID=`ps --ppid $MAKE_PID | grep qemu_launcher | sed -e 's/^\s*//' | \
+                 cut -d' ' -f1`
+
+       # To kill qemu we need to send a USR1 signal to Qemu_launcher.
+       kill -10 $QEMU_PID
+
+       wait $MAKE_PID
+
+       echo -e "[RUN_AKAROS_IN_QEMU]: End\n"
+
+       # If the run was terminated via a timeout, then we finish with an error.
+       if [[ "$WAIT_RESULT" == TIMEOUT ]]; then
+               echo "AKAROS was terminated after running for $MAX_RUN_TIME seconds."
+               exit 1
+       fi
+}
+
+
+
+if [ "$COMPILE_ALL" == true ]; then
+       echo "Building all AKAROS"
+       build_config
+       
+       build_cross_compiler
+       build_kernel
+       build_userspace
+       build_busybox
+
+       run_qemu
+
+       AFFECTED_COMPONENTS="cross-compiler kernel userspace busybox"
+else
+       # Save changed files between last tested commit and current one.
+       git diff --stat $GIT_PREVIOUS_COMMIT $GIT_COMMIT > $DIFF_FILE
+
+       # Extract build targets by parsing diff file.
+       AFFECTED_COMPONENTS=`$SCR_GIT_CHANGES $DIFF_FILE $CONF_COMP_COMPONENTS_FILE`
+       # Can contain {cross-compiler, kernel, userspace, busybox}
+
+       if [[ -n $AFFECTED_COMPONENTS ]]; 
+       then
+               echo "Detected changes in "$AFFECTED_COMPONENTS
+               build_config
+
+               if [[ $AFFECTED_COMPONENTS == *cross-compiler* ]]
+               then
+                       build_cross_compiler
+                       build_kernel
+                       build_userspace
+                       build_busybox
+               else 
+                       if [[ $AFFECTED_COMPONENTS == *kernel* ]]
+                       then
+                               build_kernel
+                       fi
+
+                       if [[ $AFFECTED_COMPONENTS == *userspace* ]]
+                       then
+                               build_userspace
+                       fi
+
+                       if [[ $AFFECTED_COMPONENTS == *busybox* ]]
+                       then
+                               build_busybox
+                       fi
+               fi
+       else
+               echo "Skipping build. No changes detected."
+       fi
+
+       run_qemu
+fi
+
+
+################################################################################
+###############                  TEST REPORTING                  ###############
+################################################################################
+
+echo -e "\n[TEST_REPORTING]: Begin"
+
+TESTS_TO_RUN="KERNEL_POSTBOOT" # TODO(alfongj): Remove this when not needed.
+# for COMPONENT in "${AFFECTED_COMPONENTS_ARRAY[@]}"; 
+# do
+#      # TODO(alfongj): Add to tests to run the name of the test suites to be ran.
+#      # TESTS_TO_RUN="$TESTS_TO_RUN SOMETHING"
+# done
+
+# Generate test report
+$SCR_GEN_TEST_REPORTS $AKAROS_OUTPUT_FILE $TEST_OUTPUT_DIR $TESTS_TO_RUN
+echo "Tests generated in $TEST_OUTPUT_DIR"
+
+echo -e "[TEST_REPORTING]: End\n"
diff --git a/tools/jenkins/utils/changes.py b/tools/jenkins/utils/changes.py
new file mode 100755 (executable)
index 0000000..47fc75b
--- /dev/null
@@ -0,0 +1,151 @@
+#!/usr/bin/python
+"""Parses a 'git diff --stat' file to extract what files have been changed as
+shown in the diff. Extracts the directory paths of those files, and decides
+which components of AKAROS should be compiled and tested, accordingly.
+"""
+import json
+import os
+import re
+import sys
+
+REGEX_EXTRACT_PATH_FROM_GIT_DIFF_LINE = r'^(?:\s)*([^\s]*)(?:\s)*\|(?:.*)\n?$'
+# Path to file with git diff
+DIFF_FILE = sys.argv[1]
+# Path to file with JSON definition of akaros components
+CONFIG_COMP_COMPONENTS_FILE = sys.argv[2]
+
+# Arguments to fill variable paths with. Useful, for example, to define a path
+# that will vary whether we are compiling an architecture or another.
+# TODO(alfongj): Get these from Env var or sys.argv
+
+
+"""The following is the definition of the given components of AKAROS (filled in
+from CONFIG_COMP_COMPONENTS_FILE (which should be 
+config/compilation_components.json or something).
+
+Each 'component' consists of a name (which will be a unique identifier printed
+out to console) as key, plus a list of PATHS as content. If any of these paths
+is modified, then we will consider that the full component is affected, and
+therefore may need to be compiled and tested again.
+
+Paths should be written from the root repo folder, beginning with './' and not
+'/', and ending in '/'.
+
+If a path should include any subpaths, then it must be appended with a + symbol.
+e.g. ./path/with/subpaths/+
+
+If a path has variable arguments, they should be represented like {{this}}.
+These arguments will be filled in with the contents of PATH_ARGS.
+e.g. ./path/with/{{VARIABLE}}/{{ARGUMENTS}}/. 
+"""
+akaros_components = {}
+
+affected_components = {}
+
+def get_variable_path_args() :
+       """Returns dict of arguments to use in the load_component_config function
+       to generate dynamic paths. Currently it is only being used to change a 
+       subdirectory in one of the paths depending on the architecture being 
+       tested.
+       """
+       PATH_ARGS = {
+               "I686": {
+                       "arch": "x86"
+               },
+               "X86_64": {
+                       "arch": "x86"
+               },
+               "RISCV": {
+                       "arch": "riscv"
+               }
+       }
+       compilation_arch = os.getenv('COMPILATION_ARCH', 'I686')
+       return PATH_ARGS[compilation_arch]
+
+def load_component_config() :
+       """Loads ../config/compilation_components.json object, which contains a
+       list of all the different AKAROS compilation components along with the paths
+       to look for for compiling them.
+       """
+       conf_file_contents = ""
+       # Read config file.
+       with open(CONFIG_COMP_COMPONENTS_FILE, 'r') as conf_file :
+               conf_file_contents = conf_file.read().replace('\n', '')
+
+       # Replace variable arguments.
+       var_path_args = get_variable_path_args()
+       for arg in var_path_args :
+               wrapped_arg = "{{" + arg + "}}"
+               conf_file_contents = conf_file_contents.replace(wrapped_arg, 
+                                                               var_path_args[arg])
+
+       # Parse JSON into python object.
+       global akaros_components
+       akaros_components = json.loads(conf_file_contents)['compilation_components']
+
+def extract_dir(diff_line) :
+       """Given a line from a "git diff --stat" output, it tries to extract a 
+       directory from it. 
+
+       If a blank or non-change line is passed, it ignores it and returns nothing.
+
+       If a 'change' line (e.g. ' path/to/file.ext  |  33++ ') is passed, it strips
+       the path (not including the file name) and prepends a './' to it and returns
+       it.
+       """
+       match = re.match(REGEX_EXTRACT_PATH_FROM_GIT_DIFF_LINE, diff_line) 
+       if (match) :
+               full_path = './' + match.group(1)
+               folder_list = full_path.split('/')[0:-1]
+               folder_path = '/'.join(folder_list) + '/'
+               return folder_path
+
+def includes_subpaths(path) :
+       """Checks if a given path includes subpaths or not. It includes them if it
+       ends in a '+' symbol.
+       """
+       return path[-1] == '+'
+
+def check_components_affected(path_of_changed_file) :
+       """Checks if a given directory should set the state of one of the components
+       to affected.
+       """
+       global affected_components
+       for component in akaros_components :
+               affected = component in affected_components
+               paths = akaros_components[component]['PATHS']
+               if (not affected) :
+                       for path in paths :
+                               if (includes_subpaths(path)) :
+                                       # Checks if a given string contains the given path.
+                                       # e.g., If the path is 'path/to':
+                                               # The regex will match for: 'path/to/file.txt' or 
+                                                       # 'path/to/and/subpath/to/file.txt'
+                                               # But not for: 'path/file.txt' nor for 'path/tofile.txt' or
+                                                       # 'path/tobananas/file.txt'
+                                       regex = re.compile('^\%s(?:.*/)*$' % path[:-1])
+                               else :
+                                       # Checks if a given string contains the given path with no 
+                                       # subpaths.
+                                       # e.g., If the path is 'path/to':
+                                               # The regex will match for: 'path/to/file.txt'
+                                               # But not for: 'path/file.txt' nor for 'path/tofile.txt' or
+                                                       # 'path/tobananas/file.txt' or 
+                                                       # 'path/to/and/subpath/to/file.txt'
+                                       regex = re.compile('^\%s[^/]*$' % path)
+
+                               if (re.match(regex, path_of_changed_file)) :
+                                       affected_components[component] = True
+                                       break
+
+def main() :
+       load_component_config()
+       diff_file = open(DIFF_FILE)
+       for line in diff_file :
+               cur_dir = extract_dir(line)
+               if (cur_dir) :
+                       check_components_affected(cur_dir)
+
+       print ' '.join(affected_components)
+
+main()
diff --git a/tools/jenkins/utils/markup.py b/tools/jenkins/utils/markup.py
new file mode 100644 (file)
index 0000000..d5e9a6a
--- /dev/null
@@ -0,0 +1,527 @@
+# This code is in the public domain, it comes
+# with absolutely no warranty and you can do
+# absolutely whatever you want with it.
+
+__date__ = '1 October 2012'
+__version__ = '1.9'
+__doc__= """
+This is markup.py - a Python module that attempts to
+make it easier to generate HTML/XML from a Python program
+in an intuitive, lightweight, customizable and pythonic way.
+
+The code is in the public domain.
+
+Version: %s as of %s.
+
+Documentation and further info is at http://markup.sourceforge.net/
+
+Please send bug reports, feature requests, enhancement
+ideas or questions to nogradi at gmail dot com.
+
+Installation: drop markup.py somewhere into your Python path.
+""" % ( __version__, __date__ )
+
+try:
+    basestring
+    import string
+except:
+    # python 3
+    basestring = str
+    string = str
+
+# tags which are reserved python keywords will be referred 
+# to by a leading underscore otherwise we end up with a syntax error
+import keyword
+
+class element:
+    """This class handles the addition of a new element."""
+
+    def __init__( self, tag, case='lower', parent=None ):
+        self.parent = parent
+
+        if case == 'upper':
+            self.tag = tag.upper( )
+        elif case == 'lower':
+            self.tag = tag.lower( )
+        elif case =='given':
+            self.tag = tag
+        else:
+            self.tag = tag
+    
+    def __call__( self, *args, **kwargs ):
+        if len( args ) > 1:
+            raise ArgumentError( self.tag )
+
+        # if class_ was defined in parent it should be added to every element
+        if self.parent is not None and self.parent.class_ is not None:
+            if 'class_' not in kwargs:
+                kwargs['class_'] = self.parent.class_
+            
+        if self.parent is None and len( args ) == 1:
+            x = [ self.render( self.tag, False, myarg, mydict ) for myarg, mydict in _argsdicts( args, kwargs ) ]
+            return '\n'.join( x )
+        elif self.parent is None and len( args ) == 0:
+            x = [ self.render( self.tag, True, myarg, mydict ) for myarg, mydict in _argsdicts( args, kwargs ) ]
+            return '\n'.join( x )
+            
+        if self.tag in self.parent.twotags:
+            for myarg, mydict in _argsdicts( args, kwargs ):
+                self.render( self.tag, False, myarg, mydict )
+        elif self.tag in self.parent.onetags:
+            if len( args ) == 0:
+                for myarg, mydict in _argsdicts( args, kwargs ):
+                    self.render( self.tag, True, myarg, mydict )    # here myarg is always None, because len( args ) = 0
+            else:
+                raise ClosingError( self.tag )
+        elif self.parent.mode == 'strict_html' and self.tag in self.parent.deptags:
+            raise DeprecationError( self.tag )
+        else:
+            raise InvalidElementError( self.tag, self.parent.mode )
+    
+    def render( self, tag, single, between, kwargs ):
+        """Append the actual tags to content."""
+
+        out = "<%s" % tag
+        for key, value in list( kwargs.items( ) ):
+            if value is not None:               # when value is None that means stuff like <... checked>
+                key = key.strip('_')            # strip this so class_ will mean class, etc.
+                if key == 'http_equiv':         # special cases, maybe change _ to - overall?
+                    key = 'http-equiv'
+                elif key == 'accept_charset':
+                    key = 'accept-charset'
+                out = "%s %s=\"%s\"" % ( out, key, escape( value ) )
+            else:
+                out = "%s %s" % ( out, key )
+        if between is not None:
+            out = "%s>%s</%s>" % ( out, between, tag )
+        else:
+            if single:
+                out = "%s />" % out
+            else:
+                out = "%s>" % out
+        if self.parent is not None:
+            self.parent.content.append( out )
+        else:
+            return out
+    
+    def close( self ):
+        """Append a closing tag unless element has only opening tag."""
+
+        if self.tag in self.parent.twotags:
+            self.parent.content.append( "</%s>" % self.tag )
+        elif self.tag in self.parent.onetags:
+            raise ClosingError( self.tag )
+        elif self.parent.mode == 'strict_html' and self.tag in self.parent.deptags:
+            raise DeprecationError( self.tag )
+
+    def open( self, **kwargs ):
+        """Append an opening tag."""
+
+        if self.tag in self.parent.twotags or self.tag in self.parent.onetags:
+            self.render( self.tag, False, None, kwargs )
+        elif self.mode == 'strict_html' and self.tag in self.parent.deptags:
+            raise DeprecationError( self.tag )
+
+class page:
+    """This is our main class representing a document. Elements are added
+    as attributes of an instance of this class."""
+
+    def __init__( self, mode='strict_html', case='lower', onetags=None, twotags=None, separator='\n', class_=None ):
+        """Stuff that effects the whole document.
+
+        mode -- 'strict_html'   for HTML 4.01 (default)
+                'html'          alias for 'strict_html'
+                'loose_html'    to allow some deprecated elements
+                'xml'           to allow arbitrary elements
+
+        case -- 'lower'         element names will be printed in lower case (default)
+                'upper'         they will be printed in upper case
+                'given'         element names will be printed as they are given
+
+        onetags --              list or tuple of valid elements with opening tags only
+        twotags --              list or tuple of valid elements with both opening and closing tags
+                                these two keyword arguments may be used to select
+                                the set of valid elements in 'xml' mode
+                                invalid elements will raise appropriate exceptions
+        
+        separator --            string to place between added elements, defaults to newline
+        
+        class_ --               a class that will be added to every element if defined"""
+        
+        valid_onetags = [ "AREA", "BASE", "BR", "COL", "FRAME", "HR", "IMG", "INPUT", "LINK", "META", "PARAM" ]
+        valid_twotags = [ "A", "ABBR", "ACRONYM", "ADDRESS", "B", "BDO", "BIG", "BLOCKQUOTE", "BODY", "BUTTON",
+                "CAPTION", "CITE", "CODE", "COLGROUP", "DD", "DEL", "DFN", "DIV", "DL", "DT", "EM", "FIELDSET",
+                "FORM", "FRAMESET", "H1", "H2", "H3", "H4", "H5", "H6", "HEAD", "HTML", "I", "IFRAME", "INS",
+                "KBD", "LABEL", "LEGEND", "LI", "MAP", "NOFRAMES", "NOSCRIPT", "OBJECT", "OL", "OPTGROUP",
+                "OPTION", "P", "PRE", "Q", "SAMP", "SCRIPT", "SELECT", "SMALL", "SPAN", "STRONG", "STYLE",
+                "SUB", "SUP", "TABLE", "TBODY", "TD", "TEXTAREA", "TFOOT", "TH", "THEAD", "TITLE", "TR",
+                "TT", "UL", "VAR" ]
+        deprecated_onetags = [ "BASEFONT", "ISINDEX" ]
+        deprecated_twotags = [ "APPLET", "CENTER", "DIR", "FONT", "MENU", "S", "STRIKE", "U" ]
+
+        self.header = [ ]
+        self.content = [ ]
+        self.footer = [ ]
+        self.case = case
+        self.separator = separator
+
+        # init( ) sets it to True so we know that </body></html> has to be printed at the end
+        self._full = False
+        self.class_= class_
+
+        if mode == 'strict_html' or mode == 'html':
+            self.onetags = valid_onetags
+            self.onetags += list( map( string.lower, self.onetags ) )
+            self.twotags = valid_twotags
+            self.twotags += list( map( string.lower, self.twotags ) )
+            self.deptags = deprecated_onetags + deprecated_twotags
+            self.deptags += list( map( string.lower, self.deptags ) )
+            self.mode = 'strict_html'
+        elif mode == 'loose_html':
+            self.onetags = valid_onetags + deprecated_onetags 
+            self.onetags += list( map( string.lower, self.onetags ) )
+            self.twotags = valid_twotags + deprecated_twotags
+            self.twotags += list( map( string.lower, self.twotags ) )
+            self.mode = mode
+        elif mode == 'xml':
+            if onetags and twotags:
+                self.onetags = onetags
+                self.twotags = twotags
+            elif ( onetags and not twotags ) or ( twotags and not onetags ):
+                raise CustomizationError( )
+            else:
+                self.onetags = russell( )
+                self.twotags = russell( )
+            self.mode = mode
+        else:
+            raise ModeError( mode )
+
+    def __getattr__( self, attr ):
+
+        # tags should start with double underscore
+        if attr.startswith("__") and attr.endswith("__"):
+            raise AttributeError( attr )
+        # tag with single underscore should be a reserved keyword
+        if attr.startswith( '_' ):
+            attr = attr.lstrip( '_' ) 
+            if attr not in keyword.kwlist:
+                raise AttributeError( attr )
+
+        return element( attr, case=self.case, parent=self )
+
+    def __str__( self ):
+        
+        if self._full and ( self.mode == 'strict_html' or self.mode == 'loose_html' ):
+            end = [ '</body>', '</html>' ]
+        else:
+            end = [ ]
+
+        return self.separator.join( self.header + self.content + self.footer + end )
+
+    def __call__( self, escape=False ):
+        """Return the document as a string.
+
+        escape --   False   print normally
+                    True    replace < and > by &lt; and &gt;
+                            the default escape sequences in most browsers"""
+
+        if escape:
+            return _escape( self.__str__( ) )
+        else:
+            return self.__str__( )
+
+    def add( self, text ):
+        """This is an alias to addcontent."""
+        self.addcontent( text )
+
+    def addfooter( self, text ):
+        """Add some text to the bottom of the document"""
+        self.footer.append( text )
+
+    def addheader( self, text ):
+        """Add some text to the top of the document"""
+        self.header.append( text )
+
+    def addcontent( self, text ):
+        """Add some text to the main part of the document"""
+        self.content.append( text )
+
+
+    def init( self, lang='en', css=None, metainfo=None, title=None, header=None,
+              footer=None, charset=None, encoding=None, doctype=None, bodyattrs=None, script=None, base=None ):
+        """This method is used for complete documents with appropriate
+        doctype, encoding, title, etc information. For an HTML/XML snippet
+        omit this method.
+
+        lang --     language, usually a two character string, will appear
+                    as <html lang='en'> in html mode (ignored in xml mode)
+        
+        css --      Cascading Style Sheet filename as a string or a list of
+                    strings for multiple css files (ignored in xml mode)
+
+        metainfo -- a dictionary in the form { 'name':'content' } to be inserted
+                    into meta element(s) as <meta name='name' content='content'>
+                    (ignored in xml mode)
+        
+        base     -- set the <base href="..."> tag in <head>
+        
+        bodyattrs --a dictionary in the form { 'key':'value', ... } which will be added
+                    as attributes of the <body> element as <body key='value' ... >
+                    (ignored in xml mode)
+
+        script --   dictionary containing src:type pairs, <script type='text/type' src=src></script>
+                    or a list of [ 'src1', 'src2', ... ] in which case 'javascript' is assumed for all
+
+        title --    the title of the document as a string to be inserted into
+                    a title element as <title>my title</title> (ignored in xml mode)
+
+        header --   some text to be inserted right after the <body> element
+                    (ignored in xml mode)
+
+        footer --   some text to be inserted right before the </body> element
+                    (ignored in xml mode)
+
+        charset --  a string defining the character set, will be inserted into a
+                    <meta http-equiv='Content-Type' content='text/html; charset=myset'>
+                    element (ignored in xml mode)
+
+        encoding -- a string defining the encoding, will be put into to first line of
+                    the document as <?xml version='1.0' encoding='myencoding' ?> in
+                    xml mode (ignored in html mode)
+
+        doctype --  the document type string, defaults to
+                    <!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.01 Transitional//EN'>
+                    in html mode (ignored in xml mode)"""
+
+        self._full = True
+
+        if self.mode == 'strict_html' or self.mode == 'loose_html':
+            if doctype is None:
+                doctype = "<!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.01 Transitional//EN'>"
+            self.header.append( doctype )
+            self.html( lang=lang )
+            self.head( )
+            if charset is not None:
+                self.meta( http_equiv='Content-Type', content="text/html; charset=%s" % charset )
+            if metainfo is not None:
+                self.metainfo( metainfo )
+            if css is not None:
+                self.css( css )
+            if title is not None:
+                self.title( title )
+            if script is not None:
+                self.scripts( script )
+            if base is not None:
+                self.base( href='%s' % base )
+            self.head.close()
+            if bodyattrs is not None:
+                self.body( **bodyattrs )
+            else:
+                self.body( )
+            if header is not None:
+                self.content.append( header )
+            if footer is not None:
+                self.footer.append( footer )
+
+        elif self.mode == 'xml':
+            if doctype is None:
+                if encoding is not None:
+                    doctype = "<?xml version='1.0' encoding='%s' ?>" % encoding
+                else:
+                    doctype = "<?xml version='1.0' ?>"
+            self.header.append( doctype )
+
+    def css( self, filelist ):
+        """This convenience function is only useful for html.
+        It adds css stylesheet(s) to the document via the <link> element."""
+      
+        if isinstance( filelist, basestring ):
+            self.link( href=filelist, rel='stylesheet', type='text/css', media='all' )
+        else:
+            for file in filelist:
+                self.link( href=file, rel='stylesheet', type='text/css', media='all' )
+
+    def metainfo( self, mydict ):
+        """This convenience function is only useful for html.
+        It adds meta information via the <meta> element, the argument is
+        a dictionary of the form { 'name':'content' }."""
+
+        if isinstance( mydict, dict ):
+            for name, content in list( mydict.items( ) ):
+                self.meta( name=name, content=content )
+        else:
+            raise TypeError( "Metainfo should be called with a dictionary argument of name:content pairs." )
+
+    def scripts( self, mydict ):
+        """Only useful in html, mydict is dictionary of src:type pairs or a list
+        of script sources [ 'src1', 'src2', ... ] in which case 'javascript' is assumed for type.
+        Will be rendered as <script type='text/type' src=src></script>"""
+
+        if isinstance( mydict, dict ):
+            for src, type in list( mydict.items( ) ):
+                self.script( '', src=src, type='text/%s' % type )
+        else:
+            try:
+                for src in mydict:
+                    self.script( '', src=src, type='text/javascript' )
+            except:
+                raise TypeError( "Script should be given a dictionary of src:type pairs or a list of javascript src's." )
+
+
+class _oneliner:
+    """An instance of oneliner returns a string corresponding to one element.
+    This class can be used to write 'oneliners' that return a string
+    immediately so there is no need to instantiate the page class."""
+    
+    def __init__( self, case='lower' ):
+        self.case = case
+    
+    def __getattr__( self, attr ):
+        
+        # tags should start with double underscore
+        if attr.startswith("__") and attr.endswith("__"):
+            raise AttributeError( attr )
+        # tag with single underscore should be a reserved keyword
+        if attr.startswith( '_' ):
+            attr = attr.lstrip( '_' ) 
+            if attr not in keyword.kwlist:
+                raise AttributeError( attr )
+        
+        return element( attr, case=self.case, parent=None )
+
+oneliner = _oneliner( case='lower' )
+upper_oneliner = _oneliner( case='upper' )
+given_oneliner = _oneliner( case='given' )
+
+def _argsdicts( args, mydict ):
+    """A utility generator that pads argument list and dictionary values, will only be called with len( args ) = 0, 1."""
+    
+    if len( args ) == 0:
+        args = None, 
+    elif len( args ) == 1:
+        args = _totuple( args[0] )
+    else:
+        raise Exception( "We should have never gotten here." )
+
+    mykeys = list( mydict.keys( ) )
+    myvalues = list( map( _totuple, list( mydict.values( ) ) ) )
+
+    maxlength = max( list( map( len, [ args ] + myvalues ) ) )
+
+    for i in range( maxlength ):
+        thisdict = { }
+        for key, value in zip( mykeys, myvalues ):
+            try:
+                thisdict[ key ] = value[i]
+            except IndexError:
+                thisdict[ key ] = value[-1]
+        try:
+            thisarg = args[i]
+        except IndexError:
+            thisarg = args[-1]
+
+        yield thisarg, thisdict
+
+def _totuple( x ):
+    """Utility stuff to convert string, int, long, float, None or anything to a usable tuple."""
+
+    if isinstance( x, basestring ):
+        out = x,
+    elif isinstance( x, ( int, long, float ) ):
+        out = str( x ),
+    elif x is None:
+        out = None,
+    else:
+        out = tuple( x )
+
+    return out
+
+def escape( text, newline=False ):
+    """Escape special html characters."""
+
+    if isinstance( text, basestring ):
+        if '&' in text:
+            text = text.replace( '&', '&amp;' )
+        if '>' in text:
+            text = text.replace( '>', '&gt;' )
+        if '<' in text:
+            text = text.replace( '<', '&lt;' )
+        if '\"' in text:
+            text = text.replace( '\"', '&quot;' )
+        if '\'' in text:
+            text = text.replace( '\'', '&quot;' )
+        if newline:
+            if '\n' in text:
+                text = text.replace( '\n', '<br>' )
+
+    return text
+
+_escape = escape
+
+def unescape( text ):
+    """Inverse of escape."""
+    
+    if isinstance( text, basestring ):
+        if '&amp;' in text:
+            text = text.replace( '&amp;', '&' )
+        if '&gt;' in text:
+            text = text.replace( '&gt;', '>' )
+        if '&lt;' in text:
+            text = text.replace( '&lt;', '<' )
+        if '&quot;' in text:
+            text = text.replace( '&quot;', '\"' )
+
+    return text
+
+class dummy:
+    """A dummy class for attaching attributes."""
+    pass
+
+doctype = dummy( )
+doctype.frameset = """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">"""
+doctype.strict = """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">"""
+doctype.loose = """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">"""
+
+class russell:
+    """A dummy class that contains anything."""
+
+    def __contains__( self, item ):
+        return True
+
+
+class MarkupError( Exception ):
+    """All our exceptions subclass this."""
+    def __str__( self ):
+        return self.message
+
+class ClosingError( MarkupError ):
+    def __init__( self, tag ):
+        self.message = "The element '%s' does not accept non-keyword arguments (has no closing tag)." % tag
+
+class OpeningError( MarkupError ):
+    def __init__( self, tag ):
+        self.message = "The element '%s' can not be opened." % tag
+
+class ArgumentError( MarkupError ):
+    def __init__( self, tag ):
+        self.message = "The element '%s' was called with more than one non-keyword argument." % tag
+
+class InvalidElementError( MarkupError ):
+    def __init__( self, tag, mode ):
+        self.message = "The element '%s' is not valid for your mode '%s'." % ( tag, mode )
+
+class DeprecationError( MarkupError ):
+    def __init__( self, tag ):
+        self.message = "The element '%s' is deprecated, instantiate markup.page with mode='loose_html' to allow it." % tag
+
+class ModeError( MarkupError ):
+    def __init__( self, mode ):
+        self.message = "Mode '%s' is invalid, possible values: strict_html, html (alias for strict_html), loose_html, xml." % mode
+
+class CustomizationError( MarkupError ):
+    def __init__( self ):
+        self.message = "If you customize the allowed elements, you must define both types 'onetags' and 'twotags'."
+
+if __name__ == '__main__':
+    import sys
+    sys.stdout.write( __doc__ )
diff --git a/tools/jenkins/utils/qemu_launcher.c b/tools/jenkins/utils/qemu_launcher.c
new file mode 100644 (file)
index 0000000..749b007
--- /dev/null
@@ -0,0 +1,44 @@
+#include <unistd.h>
+#include <stdio.h>
+#include <string.h>
+#include <signal.h>
+
+pid_t pid;
+
+void signal_handler(int signal) {
+       int res = kill(pid, SIGKILL);
+}
+
+int main(int argc, char *argv[])
+{
+       pid = fork();
+
+       if (pid == -1) {
+               perror("Fork failed when trying to spawn qemu process.\n");
+               return 1;
+       } else if (pid == 0) { /* Child process */
+               char* prog_name = "qemu-system-x86_64";
+               char* params[argc];
+               int i;
+
+               params[0] = prog_name;
+               for (i = 1; i < argc; ++i)
+               {
+                       params[i] = argv[i];
+               }
+               params[argc] = NULL;
+
+               execvp(prog_name, params);
+       } else { /* Parent process */
+               int status;
+
+               if (signal(SIGUSR1, signal_handler) == SIG_ERR) {
+                       fputs("An error occurred while setting a signal handler.\n", stderr);
+                       return 2;
+               }
+
+               (void) waitpid(pid, &status, 0);
+       }
+
+       return 0;
+}
diff --git a/tools/jenkins/utils/test_reporter.py b/tools/jenkins/utils/test_reporter.py
new file mode 100755 (executable)
index 0000000..2f58e55
--- /dev/null
@@ -0,0 +1,236 @@
+#!/usr/bin/python
+"""Parses AKAROS output to detect tests and report on them.
+Arguments:
+       [0]: Path to file containing AKAROS output.
+       [1]: Path to directory where to save test reports.
+       [2]: IDs of tests suites to look for.
+"""
+import markup
+import re
+import sys
+
+
+
+class TestSuite() :
+       """Represents a test suite (collection of test cases) and has the ability of
+       printing itself plus the test cases into XML markup.
+       """
+       def __init__(self, name, class_name) :
+               """The tests will be reported as belonging to 'name.class_name', so you 
+               can represent two levels of hierarchy this way.
+               """
+               self.name = name
+               self.class_name = class_name
+               self.test_cases = []
+               self.errors_num = 0
+               self.failures_num = 0
+               self.skipped_num = 0
+
+       def add_test_case(self, name, status, el_time=None, failure_msg=None) :
+               """Adds a test case to the suite.
+               """
+               test_case = TestCase(self, name, status, el_time, failure_msg)
+               self.test_cases.append(test_case)
+
+               if status == 'DISABLED' :
+                       self.skipped_num += 1
+               elif status == 'FAILED' :
+                       self.failures_num += 1
+
+       def generate_markup(self) :
+               """Generates and returns a string containing the representation of the
+               suite and the testcases in XML XUnit format.
+
+               Returns:
+                       String containing the representation.
+               """
+               report = markup.page( mode='xml' )
+               report.init( encoding='UTF-8' )
+
+               report.testsuite.open(name='akaros_tests', tests=len(self.test_cases), \
+                                     errors=self.errors_num, \
+                                     failures=self.failures_num, \
+                                     skip=self.skipped_num)
+               for test_case in self.test_cases:
+                       test_case.generate_markup(report)
+               report.testsuite.close()
+
+               return report
+
+class TestCase() :
+       """Represents a test case, and has ability to print it into a markup page.
+       """
+       def __init__(self, suite, name, status, el_time=None, failure_msg=None) :
+               self.suite = suite
+               self.name = name
+               self.status = status
+               if self.status in ['PASSED', 'FAILED'] :
+                       self.el_time = el_time
+               if self.status == 'FAILED' :
+                       self.failure_msg = failure_msg
+
+       def generate_markup(self, report) :
+               """Generates XML markup representing the test case, and in XUnit format
+               in the given markup.page file.
+               """
+               full_name = self.suite.name + '.' + self.suite.class_name
+
+               if self.status in ['PASSED', 'FAILED'] :
+                       report.testcase.open(classname=full_name, name=self.name, \
+                                            time=self.el_time)
+               else :
+                       report.testcase.open(classname=full_name, name=self.name)
+
+               if self.status == 'DISABLED' :
+                       report.skipped.open(type='DISABLED', message='Disabled')
+                       report.skipped.close()
+               elif self.status == 'FAILED' :
+                       report.failure.open(type='FAILED', message=self.failure_msg)
+                       report.failure.close()
+
+               report.testcase.close()
+
+
+
+class TestParser() :
+       """This class is a helper for parsing the output from test suite groups
+       ran inside AKAROS.
+
+       Tests must be printed on to a file (specified by test_output_path) with the
+       following format:
+       <-- BEGIN_{test_suite_name}_{test_class_name}_TESTS -->
+               (PASSED|FAILED|DISABLED) [{test_case_name}]({test_et}s)? {failure_msg}?
+               (PASSED|FAILED|DISABLED) [{test_case_name}]({test_et}s)? {failure_msg}?
+               ...
+       <-- END_{test_suite_name}_{test_class_name}_TESTS -->
+
+       For example:
+       <-- BEGIN_KERNEL_PB_TESTS -->
+               PASSED   [test_easy_to_pass](1.000s)
+               FAILED   [test_will_fail](0.01s)   This test should do X and Y.
+               DISABLED [test_useless]
+               ...
+       <-- END_KERNEL_PB_TESTS -->
+       """
+
+       def __init__(self, test_output_path, test_suite_name, test_class_name) :
+               self.test_output = open(test_output_path, 'r')
+               self.regex_test_start = \
+                   re.compile('^\s*<--\s*BEGIN_%s_%s_TESTS\s*-->\s*$' \
+                              % (test_suite_name, test_class_name))
+               self.regex_test_end = \
+                   re.compile('^\s*<--\s*END_%s_%s_TESTS\s*-->\s*$' \
+                              % (test_suite_name, test_class_name))
+               self.test_suite_name = test_suite_name
+               self.test_class_name = test_class_name
+
+               # Prepare for reading.
+               self.__advance_to_beginning_of_tests()
+
+       def __advance_to_beginning_of_tests(self) :
+               beginning_reached = False
+               while not beginning_reached :
+                       line = self.test_output.readline()
+                       if (re.match(self.regex_test_start, line)) :
+                               beginning_reached = True
+                       elif (len(line) == 0) :
+                               exc_msg = 'Could not find tests for {0}_{1}.'
+                               exc_msg = exc_msg.format(self.test_suite_name, \
+                                                        self.test_class_name)
+                               raise Exception(exc_msg)
+
+       def __extract_test_result(self, line) :
+               regex = r'^\s*([A-Z]+)\s*.*$'
+               matchRes = re.match(regex, line)
+               return matchRes.group(1)
+
+       def __extract_test_name(self, line) :
+               regex = r'^\s*(?:[A-Z]+)\s*\[([a-zA-Z_-]+)\].*$'
+               matchRes = re.match(regex, line)
+               return matchRes.group(1)
+
+       def __extract_test_elapsed_time(self, line) :
+               regex= r'^\s*(?:PASSED|FAILED)\s*\[(?:[a-zA-Z_-]+)\]\(([0-9\.]+)s\).*$'
+               matchRes = re.match(regex, line)
+               return matchRes.group(1)
+
+       def __extract_test_fail_msg(self, line) :
+               regex = r'^\s*FAILED\s*\[(?:[a-zA-Z_-]+)\](?:\(.*\))?\s+(.*)$'
+               matchRes = re.match(regex, line)
+               return matchRes.group(1)
+
+       def __next_test(self) :
+               """Parses the next test from the test output file.
+               Returns:
+                       First, True if there was a next test and we had not reached the end.
+                       Second, a String with the name of the test.
+                       Third, result of the test (PASSED, FAILED, DISABLED).
+                       Fourth, time elapsed in seconds, with 3 decimals.
+                       Fifth, message of a failed test.
+               """
+               # Look for test.
+               line = ''
+               while len(line) < 8 :
+                       line = self.test_output.readline()
+                       if (len(line) == 0) : # EOF
+                               return False, '', '', ''
+
+               if (re.match(self.regex_test_end, line)) :
+                       return False, '', '', 0, ''
+               else :
+                       name = self.__extract_test_name(line)
+                       res = self.__extract_test_result(line)
+                       time = self.__extract_test_elapsed_time(line) \
+                              if res in ['FAILED', 'PASSED'] else None
+                       msg = self.__extract_test_fail_msg(line) if res == 'FAILED' \
+                             else None
+                       
+                       return True, name, res, time, msg
+
+       def __cleanup(self) :
+               self.test_output.close()
+
+       def parse_test_suite(self) :
+               test_suite = TestSuite(self.test_suite_name, self.test_class_name)
+
+               end_not_reached = True
+               while end_not_reached :
+                       end_not_reached, test_name, test_res, test_et, fail_msg \
+                           = self.__next_test()
+                       if end_not_reached :
+                               test_suite.add_test_case(test_name, test_res, test_et, fail_msg)
+               
+               self.__cleanup()
+               
+               return test_suite
+
+
+
+KERNEL_PB_TESTS_KEY = 'KERNEL_POSTBOOT'
+KERNEL_PB_TESTS_SUITE_NAME = 'KERNEL'
+KERNEL_PB_TESTS_CLASS_NAME = 'POSTBOOT'
+
+def save_report(dir, filename, report) :
+       filepath = dir + '/' + filename + '_TESTS.xml'
+       report_file = open(filepath, 'w+')
+       report_file.write(report)
+       report_file.flush()
+       report_file.close()
+
+
+def main() :
+       akaros_output_file_path = sys.argv[1]
+       test_output_dir = sys.argv[2]
+       tests_to_run = sys.argv[3].strip().split(' ')
+
+       # Kernel Postboot Tests
+       if KERNEL_PB_TESTS_KEY in tests_to_run :
+               test_suite = TestParser(akaros_output_file_path, \
+                                       KERNEL_PB_TESTS_SUITE_NAME, \
+                                       KERNEL_PB_TESTS_CLASS_NAME).parse_test_suite()
+               test_report_str = test_suite.generate_markup().__str__()
+               kernel_pb_tests_report = save_report(test_output_dir, \
+                                                    KERNEL_PB_TESTS_KEY, \
+                                                    test_report_str)
+
+main()
diff --git a/tools/jenkins/utils/wait_until.py b/tools/jenkins/utils/wait_until.py
new file mode 100755 (executable)
index 0000000..58f791a
--- /dev/null
@@ -0,0 +1,74 @@
+#!/usr/bin/python
+"""This script keeps running until one of the two following conditions occur:
+1) A line in the file specified by argv[1] contains argv[2] => This will exit
+   with a result code of 0 and print SUCCESSS.
+2) argv[3] seconds or more occur since invoking the script => This will exit
+   with a result code of 0 (because Jenkins would autokill the build if we
+   returned an error code) and print TIMEOUT.
+
+Please note:
+    The timeout specified by argv[3] may be checked with a precision of up to 5
+    seconds, so it may be possible that the script runs for argv[3] + 5 secs (or
+    even a little bit more).
+"""
+import sys
+import time
+import re
+
+
+OUTPUT_FILE = sys.argv[1]
+REGEX_END_LINE = r'^.*' + sys.argv[2] + '.*$'
+MAX_TIME_TO_RUN = int(sys.argv[3])
+
+def is_end_line(line) :
+       """Returns true if a given file contains the 'End line' string.
+       """
+
+       if re.match(REGEX_END_LINE, line) :
+               return True
+       else :
+               return False
+
+def main() :
+       """Opens the OUTPUT_FILE and continuously reads lines from it until either
+       there are no more lines or the end line is reached. In the former case, it
+       waits an exponentially increasing time interval for more lines to be printed
+       onto the file.
+
+       If MAX_TIME_TO_RUN seconds are elapsed, then the script also terminates, 
+       with an error condition.
+       """
+       timeout_time = time.time() + MAX_TIME_TO_RUN
+
+       output_file = open(OUTPUT_FILE, 'r')
+
+       # Min and max waiting times (sec.) between reading two lines of the file.
+       MIN_READ_DELAY = 0.1
+       MAX_READ_DELAY = 5
+       READ_DELAY_INCREM_FACTOR = 1.5 # Times what the read delay is increased.
+
+       secs_before_read = 2
+       end_not_reached = True
+
+       while end_not_reached :
+               line = output_file.readline()
+               
+               if (len(line) == 0) :
+                       time.sleep(secs_before_read)
+                       # Sleep with exponential backoff.
+                       secs_before_read = MAX_READ_DELAY \
+                                          if (secs_before_read > MAX_READ_DELAY) \
+                                          else secs_before_read * READ_DELAY_INCREM_FACTOR
+               else :
+                       secs_before_read = MIN_READ_DELAY
+                       end_not_reached = not is_end_line(line)
+
+
+               if (time.time() >= timeout_time) :
+                       print "TIMEOUT"
+                       exit(0)
+
+       print "SUCCESS"
+       exit(0)
+
+main()