Changeset 3353


Ignore:
Timestamp:
Oct 15, 2018 7:39:01 PM (6 years ago)
Author:
knoop
Message:

Implemented new palmtest including an extended initial set of test cases

Location:
palm/trunk
Files:
65 added
1 edited

Legend:

Unmodified
Added
Removed
  • palm/trunk/SCRIPTS/palmtest

    r2824 r3353  
    1 #!/usr/bin/env bash
    2 
    3 #--------------------------------------------------------------------------------#
    4 # This file is part of the PALM model system.
    5 #
    6 # PALM is free software: you can redistribute it and/or modify it under the terms
    7 # of the GNU General Public License as published by the Free Software Foundation,
    8 # either version 3 of the License, or (at your option) any later version.
    9 #
    10 # PALM is distributed in the hope that it will be useful, but WITHOUT ANY
    11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
    12 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
    13 #
    14 # You should have received a copy of the GNU General Public License along with
    15 # PALM. If not, see <http://www.gnu.org/licenses/>.
    16 #
    17 # Copyright 2017-2018  Leibniz Universitaet Hannover
    18 #--------------------------------------------------------------------------------#
    19 #
    20 # Current revisions:
    21 # -----------------
    22 #
    23 #
    24 # Former revisions:
    25 # -----------------
    26 # $Id$
    27 # Corrected "Former revisions" section
    28 # svn propset keyword
    29 #
    30 #
    31 #
    32 # 2696 kanani
    33 # Change in file header (GPL part)
    34 #
    35 # 2579 knoop
    36 # palmtest now testing for multiple cpu-setups
    37 #
    38 # 2515 kanani
    39 # Generalization of the palmtest script
    40 #
    41 # 2497 knoop
    42 # Initial revision
    43 #
    44 # Description:
    45 # ------------
    46 # Testsuite execution script
    47 #------------------------------------------------------------------------------#
    48 SOURCE="${BASH_SOURCE[0]}"
    49 while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink
    50   DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
    51   SOURCE="$(readlink "$SOURCE")"
    52   [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located
    53 done
    54 SCRIPT_LOCATION="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
    55 
    56 hrule() {
    57    printf "#"
    58    printf -- '-%.0s' {1..72}
    59    printf "#\n"
    60 }
    61 
    62 get_number_of_cpu_cores() {
    63    {
    64       n=$(sysctl -n machdep.cpu.core_count 2> /dev/null)
    65    } || {
    66       n=$(grep -c ^processor /proc/cpuinfo 2> /dev/null)
    67    } || {
    68    if ! [[ $n =~ ^-?[0-9]+$ ]]; then
    69       n=1
    70    fi
    71    }
    72    echo $n
    73 }
    74 
    75 get_core_array() {
    76    for i in 1 2 4 8 16 32; do
    77       if [[ $i -le ${1} ]]; then
    78          printf "$i "
    79       fi
    80    done
    81 }
    82 
    83 configure() {
    84    hrule
    85    printf "Configuring..."
    86    if [[ -f ${existing_working_dir}/.palm.config.${configuration} ]]; then
    87       cp ${existing_working_dir}/.palm.config.${configuration} ${tester_prefix}/.palm.config.${configuration}
    88       sed -i -e "s#%base_directory .*#%base_directory      ${tester_prefix}#g" ${tester_prefix}/.palm.config.${configuration}
    89       sed -i -e "s#%base_data .*#%base_data           ${tester_prefix}/JOBS#g" ${tester_prefix}/.palm.config.${configuration}
    90       sed -i -e "s#%source_path .*#%source_path         ${tester_prefix}/trunk/SOURCE#g" ${tester_prefix}/.palm.config.${configuration}
    91       sed -i -e "s#%user_source_path .*#%user_source_path    ${tester_prefix}/JOBS/\$fname/USER_CODE#g" ${tester_prefix}/.palm.config.${configuration}
    92       sed -i -e "s#%fast_io_catalog .*#%fast_io_catalog     ${tester_prefix}/tmp#g" ${tester_prefix}/.palm.config.${configuration}
    93       printf " finished (adapted existing .palm.config.${configuration})\n"
    94    else
    95       printf " failed (missing .palm.config.${configuration})\n"
    96       hrule
    97       exit 1
    98    fi
    99 }
    100 
    101 build() {
    102    hrule
    103    rm -rf ${tester_prefix}/JOBS
    104    rm -rf ${tester_prefix}/MAKE_DEPOSITORY*
    105    bash ${trunk_dir}/SCRIPTS/palmbuild -h "${configuration}" -v
    106 }
    107 
    108 palm_installer_test() {
    109    hrule
    110    local name=${1}
    111    local cores=${2}
    112    printf "Testing with \"${name}\" on ${cores} core(s)... "
    113    local job_id=${name}_${cores}
    114    local input_dir=${tester_prefix}/JOBS/${job_id}/INPUT
    115    local monitoring_dir=${tester_prefix}/JOBS/${job_id}/MONITORING
    116    if [[ ! -f ${test_dir}/${name}_p3d ]] || [[ ! -f ${test_dir}/${name}_rc ]]; then
    117       printf " test not found\n"
    118       return 1
    119    fi
    120    rm -rf ${monitoring_dir}
    121    mkdir -p ${input_dir}
    122    mkdir -p ${monitoring_dir}
    123    cp ${test_dir}/${name}_p3d ${input_dir}/${job_id}_p3d
    124    cp ${test_dir}/${name}_rc ${monitoring_dir}/${job_id}_rc_reference
    125    [[ -f ${test_dir}/${name}_topo ]] && cp ${test_dir}/${name}_topo ${input_dir}/${job_id}_topo
    126    [[ -f ${test_dir}/${name}_static ]] && cp ${test_dir}/${name}_static ${input_dir}/${job_id}_static
    127    [[ -f ${test_dir}/${name}_dynamic ]] && cp ${test_dir}/${name}_dynamic ${input_dir}/${job_id}_dynamic
    128    bash ${trunk_dir}/SCRIPTS/palmrun -d ${job_id} -a "d3#" -h "${configuration}" -X "$cores" -T "$cores" -v -B > ${monitoring_dir}/${job_id}_stdout 2>&1
    129    grep -A 99999 "Run-control output" ${monitoring_dir}/${job_id}_rc 1> ${monitoring_dir}/RC 2> /dev/null
    130    grep -A 99999 "Run-control output" ${monitoring_dir}/${job_id}_rc_reference 1> ${monitoring_dir}/RC_REF 2> /dev/null
    131    diff_output=$(diff ${monitoring_dir}/RC_REF ${monitoring_dir}/RC)
    132    rm ${monitoring_dir}/RC ${monitoring_dir}/RC_REF
    133    if [[ "${diff_output}" == "" ]]; then
    134       printf " passed\n"
    135       return 0
    136    else
    137       printf " failed\n"
    138       test_status="failed"
    139       return 1
    140    fi
    141 }
    142 
    143 palm_installer_test_suite() {
    144    for test_path in ${fnames}; do
    145       for n_core in $(get_core_array $max_cores); do
    146          testname_p3d=$(basename $test_path)
    147          palm_installer_test "${testname_p3d%_p3d}" "${n_core}"
    148       done
    149    done
    150    hrule
    151    if [[ "${test_status}" == "failed" ]]; then
    152       echo "Some tests failed!"
    153       hrule
    154       exit 1
    155    else
    156       echo "All found tests passed. :-)"
    157       rm -rf ${tester_prefix}/tmp/*
    158       hrule
    159       exit 0
    160    fi
    161 }
    162 
    163 existing_working_dir=$(readlink -f "${SCRIPT_LOCATION}/../../")
    164 existing_trunk_dir=$(readlink -f "${SCRIPT_LOCATION}/../")
    165 
    166 max_cores=$(get_number_of_cpu_cores)
    167 test_id=$(date +%Y-%m-%d_%H%M%S)
    168 do_plots=1
    169 configuration="default"
    170 fnames="$(echo ${existing_trunk_dir}/INSTALL/*_p3d)"
    171 
    172 while  getopts  :d:h:N:pX:  option
    173 do
    174    case  $option  in
    175       (d)   fnames="$OPTARG";;
    176       (h)   configuration="$OPTARG";;
    177       (N)   test_id="$OPTARG";;
    178       (p)   do_plots=0;;
    179       (X)   max_cores=$OPTARG;;
    180       (\?)  printf "\n  +++ unknown option $OPTARG \n";
    181             exit;;
    182    esac
    183 done
    184 tester_prefix=${existing_working_dir}/tests/${test_id}
    185 trunk_dir=${tester_prefix}/trunk
    186 test_dir=${trunk_dir}/INSTALL
    187 
    188 mkdir -p ${tester_prefix}
    189 cd ${tester_prefix}
    190 
    191 ln -s ${existing_trunk_dir} trunk
    192 
    193 
    194 
    195 configure
    196 build
    197 palm_installer_test_suite
     1#!/usr/bin/env python3
     2# PYTHON_ARGCOMPLETE_OK
     3
     4import os
     5import sys
     6import shutil
     7from datetime import datetime
     8import subprocess
     9import multiprocessing
     10import socket
     11import getpass
     12import math
     13import re
     14import threading
     15import queue
     16from contextlib import ContextDecorator
     17
     18try:
     19    from argparse import ArgumentParser
     20    from argparse import RawTextHelpFormatter
     21except ImportError:
     22    sys.exit(
     23        'ERROR: You need argparse!\n' +
     24        '   install it from http://pypi.python.org/pypi/argparse\n' +
     25        '   or run \"pip install argparse\".'
     26    )
     27
     28try:
     29    import numpy as np
     30except ImportError:
     31    sys.exit(
     32        'ERROR: You need numpy!\n' +
     33        '   install it from http://pypi.python.org/pypi/numpy\n' +
     34        '   or run \"python3 -m pip install numpy\".'
     35    )
     36
     37try:
     38    import netCDF4
     39except ImportError:
     40    sys.exit(
     41        'ERROR: You need netCDF4!\n' +
     42        '   install it from http://pypi.python.org/pypi/netCDF4\n' +
     43        '   or run \"python3 -m pip install netCDF4\".'
     44    )
     45
     46try:
     47    import yaml
     48except ImportError:
     49    sys.exit(
     50        'ERROR: You need PyYAML!\n' +
     51        '   install it from http://pypi.python.org/pypi/PyYAML\n' +
     52        '   or run \"python3 -m pip install PyYAML\".'
     53    )
     54
     55try:
     56    import argcomplete
     57except ImportError:
     58    print(
     59        'INFO: To use Tab-completion you need argcomplete!\n' +
     60        '   install it from http://pypi.python.org/pypi/argcomplete\n' +
     61        '   or run \"python3 -m pip install argcomplete\".'
     62    )
     63    has_argcomplete = False
     64else:
     65    has_argcomplete = True
     66
     67try:
     68    from termcolor import colored
     69except ImportError:
     70    def colored(string, color):
     71        return string
     72
     73
     74version = '1.0.0'
     75
     76scripts_dir = os.path.dirname(os.path.realpath(__file__))
     77trunk_dir = os.path.realpath(os.path.join(scripts_dir, '..'))
     78workspace_dir = os.path.realpath(os.path.join(trunk_dir, '..'))
     79
     80trunk_tests_dir = os.path.join(trunk_dir, 'TESTS')
     81trunk_tests_cases_dir = os.path.join(trunk_tests_dir, 'cases')
     82trunk_tests_builds_dir = os.path.join(trunk_tests_dir, 'builds')
     83
     84tests_dir = os.path.join(workspace_dir, 'tests')
     85
     86available_cores = multiprocessing.cpu_count()
     87terminal_columns, terminal_lines = shutil.get_terminal_size()
     88hline = '#' * min(terminal_columns, 300) + '\n'
     89table_width_intro = 12
     90table_width_builds = len(max([s for s in next(os.walk(trunk_tests_builds_dir))[1]], key=len)) + len('_debug')
     91table_width_cases = len(max([s for s in next(os.walk(trunk_tests_cases_dir))[1]], key=len))
     92table_width_cores = 7
     93table_width_total = table_width_intro + table_width_builds + table_width_cases + table_width_cores + 3
     94
     95task_table_line_template = \
     96    '{:' + str(table_width_intro) + '} ' + \
     97    '{:' + str(table_width_cases) + '} ' + \
     98    '{:' + str(table_width_builds) + '} ' + \
     99    '{:' + str(table_width_cores) + '} '
     100
     101config_table_line_template = \
     102    '{:' + str(table_width_intro) + '} ' + \
     103    '{:' + str(max(table_width_builds, table_width_cases)) + '} ' + \
     104    '{:8} '
     105
     106file_table_line_template = \
     107    '{:' + str(table_width_intro) + '} ' + \
     108    '{:' + str(table_width_cases + 13) + '} '
     109
     110# for debugging
     111exist_ok = False
     112
     113
     114class SignificantDigitsRounder:
     115
     116    @staticmethod
     117    def _round(value, digits=10):
     118        if value == 0.0:
     119            return value
     120        negative = value < 0.0
     121        value = -value if negative else value
     122        rounded_value = round(value, -int(math.floor(math.log10(value))) + (digits - 1))
     123        rounded_value = -rounded_value if negative else rounded_value
     124        return rounded_value
     125
     126
     127    vectorized_round = np.vectorize(_round)
     128
     129    _vectorized_round = np.vectorize(round)
     130
     131
     132    @classmethod
     133    def around(cls, array, digits=10):
     134        # TODO: divide both arrays and check decimal point
     135        sign_mask = np.ma.masked_where(array >= 0.0, array).mask
     136        pos_array = np.where(sign_mask, array, -array)
     137        non_zero_maks = np.ma.masked_where(pos_array == 0.0, pos_array).mask
     138        non_zero_array = np.where(non_zero_maks, 1.0, pos_array)
     139        i1 = -np.floor(np.log10(non_zero_array)).astype(int) + (digits - 1)
     140        rounded_non_zero_array = cls._vectorized_round(non_zero_array, i1)
     141        rounded_pos_array = np.where(non_zero_maks, 0.0, rounded_non_zero_array)
     142        return np.where(sign_mask, rounded_pos_array, -rounded_pos_array)
     143
     144
     145
     146class Logger(ContextDecorator):
     147
     148    def __init__(self, logfile_dir, logfile_name='palmtest.log', logfile_mode='a', verbose=False):
     149        self.logfile_path = os.path.join(logfile_dir, logfile_name)
     150        self.logfile_mode = logfile_mode
     151        self.verbose = verbose
     152
     153    def __enter__(self):
     154        self._file = open(self.logfile_path, self.logfile_mode)
     155        return self
     156
     157    def to_file(self, message):
     158        self._file.write(message)
     159        self._file.flush()
     160
     161    def to_log(self, message):
     162        if self.verbose:
     163            sys.stdout.write(message)
     164            sys.stdout.flush()
     165        self._file.write(message)
     166        self._file.flush()
     167
     168    def to_all(self, message):
     169        sys.stdout.write(message)
     170        sys.stdout.flush()
     171        self._file.write(message)
     172        self._file.flush()
     173
     174    def __exit__(self, *exc):
     175        self._file.close()
     176        return False
     177
     178
     179class Executor:
     180
     181    @staticmethod
     182    def _enqueue_output(out, queue):
     183        for line in iter(out.readline, b''):
     184            queue.put(line)
     185        out.close()
     186
     187    @staticmethod
     188    def execute(cmd, cwd='.', verbose=True, dry_run=False):
     189        assert isinstance(cmd, list)
     190        if dry_run:
     191            cmd = ['echo'] + cmd
     192        cmd_str = ' '.join(cmd)
     193        p = subprocess.Popen(cmd_str, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, bufsize=1)
     194        q = queue.Queue()
     195        t = threading.Thread(target=Executor._enqueue_output, args=(p.stdout, q))
     196        t.daemon = True # thread dies with the program
     197        t.start()
     198
     199        with Logger(cwd, verbose=verbose) as logger:
     200            # read line without blocking
     201            logger.to_log(hline)
     202            logger.to_log('CMD: ' + cmd_str + '\n')
     203            logger.to_log(hline)
     204            while t.is_alive():
     205                try:
     206                    line = q.get_nowait()  # or q.get(timeout=.1)
     207                except queue.Empty:
     208                    pass  # print('no output yet')
     209                else:  # got line
     210                    logger.to_log(line.decode("utf-8"))
     211            line = True
     212            while line:
     213                try:
     214                    line = q.get_nowait()  # or q.get(timeout=.1)
     215                except queue.Empty:
     216                    line = False
     217                else:  # got line
     218                    logger.to_log(line.decode("utf-8"))
     219            logger.to_log(hline)
     220
     221        rc = p.poll()
     222        failed = rc != 0
     223        return failed
     224
     225
     226class NetCDFInterface:
     227
     228    def __init__(self, filename):
     229        self.filename = filename
     230
     231    def is_healthy(self):
     232        try:
     233            self.get_run_name()
     234        except:
     235            return False
     236        else:
     237            return True
     238
     239    def get_run_name(self):
     240        with netCDF4.Dataset(self.filename, mode='r') as netcdf:
     241            l = getattr(netcdf, 'title').split()
     242            i = l.index('run:')
     243            return l[i+1]
     244
     245    def get_var_list(self):
     246        with netCDF4.Dataset(self.filename, mode='r') as netcdf:
     247            var_list = getattr(netcdf, 'VAR_LIST').split(';')
     248            var_list = filter(None, var_list)
     249            return sorted(var_list)
     250
     251    def show_content(self):
     252        with netCDF4.Dataset(self.filename, mode='r') as netcdf:
     253            for name in netcdf.ncattrs():
     254                print("Global attr", name, "=", getattr(netcdf, name))
     255            print(netcdf)
     256            for v in netcdf.variables:
     257                print(v)
     258
     259    def get_times_list(self):
     260        attributes, times = self.read_var('time')
     261        times = [str(time) for time in times]
     262        times = list(filter(None, times))
     263        return times
     264
     265    def contains(self, variable):
     266        return variable in self.get_var_list()
     267
     268    def read_var(self, variable):
     269        with netCDF4.Dataset(self.filename, mode='r') as netcdf:
     270            values = netcdf.variables[variable][:]  # extract values
     271            attributes = dict(
     272                long_name=netcdf.variables[variable].name,
     273                unit=netcdf.variables[variable].units,
     274            )
     275        return attributes, values
     276
     277
     278class FileComparator:
     279
     280    @staticmethod
     281    def compare_ascii(file_path1, file_path2, start_string=None):
     282        try:
     283            with open(file_path1, 'r') as file1:
     284                content1 = file1.readlines()
     285        except OSError:
     286            return True, colored('[reference file not found]', 'red')
     287        try:
     288            with open(file_path2, 'r') as file2:
     289                content2 = file2.readlines()
     290        except OSError:
     291            return True, colored('[output file not found]', 'red')
     292        if start_string:
     293            index1 = content1.index(start_string)
     294            index2 = content2.index(start_string)
     295            comparable_content1 = content1[index1:]
     296            comparable_content2 = content2[index2:]
     297            ln = index2 + 1
     298        else:
     299            comparable_content1 = content1
     300            comparable_content2 = content2
     301            ln = 1
     302        if len(comparable_content1) != len(comparable_content2):
     303            return True, colored('[mismatch in total number of lines]', 'red')
     304        for line1, line2 in zip(comparable_content1, comparable_content2):
     305                if not line1 == line2:
     306                    return True, colored('[mismatch in content starting line ' + str(ln) + ']', 'red')
     307                ln += 1
     308        return False, colored('[file ok]', 'green')
     309
     310    @staticmethod
     311    def compare_netcdf(file_path1, file_path2, digits=None):
     312        nci1 = NetCDFInterface(file_path1)
     313        nci2 = NetCDFInterface(file_path2)
     314        if not nci1.is_healthy():
     315            return True, colored('[reference file not found]', 'red')
     316        if not nci2.is_healthy():
     317            return True, colored('[output file not found]', 'red')
     318        times_list1 = nci1.get_times_list()
     319        times_list2 = nci2.get_times_list()
     320        if not times_list1 == times_list2:
     321            return True, colored('[wrong time dimension]', 'red')
     322        else:
     323            time_list = times_list1
     324        var_list1 = nci1.get_var_list()
     325        var_list2 = nci2.get_var_list()
     326        if not var_list1 == var_list2:
     327            return True, colored('[wrong set of variables]', 'red')
     328        else:
     329            var_list = var_list1
     330        content1 = dict()
     331        content2 = dict()
     332        for var in var_list:
     333            attributes1, values1 = nci1.read_var(var)
     334            attributes2, values2 = nci2.read_var(var)
     335            if sorted(attributes1.keys()) != sorted(attributes2.keys()):
     336                return True, colored('[wrong set of attributes in variable \"'+var+'\"]', 'red')
     337            if isinstance(digits, int):
     338                values1 = SignificantDigitsRounder.around(values1, digits=digits)
     339                values2 = SignificantDigitsRounder.around(values2, digits=digits)
     340            content1[var] = values1
     341            content2[var] = values2
     342        #for decimals in
     343        for i, time in enumerate(time_list):
     344            for var in var_list:
     345                t_content1 = content1[var][i]
     346                t_content2 = content2[var][i]
     347                if not (t_content1==t_content2).all():
     348                    if isinstance(digits, int):
     349                        return True, colored('[1st mismatch within ' + str(digits) + ' digits at time index ' + str(i) + ' in variable \"' + var + '\"]', 'red')
     350                    else:
     351                        return True, colored('[1st mismatch at time index ' + str(i) + ' in variable \"' + var + '\"]', 'red')
     352        return False, colored('[file ok]', 'green')
     353
     354
     355class OutputChecker:
     356
     357    def __init__(self, test_dir, setup_name, build_name, cores, significant_digits=None, verbose=True, dry_run=False):
     358        self.test_dir = test_dir
     359        self.setup_name = setup_name
     360        self.build_name = build_name
     361        self.cores = cores
     362        self.significant_digits = significant_digits
     363        self.verbose = verbose
     364        self.dry_run = dry_run
     365        self.job_name = self.setup_name + '__' + build_name + '__' + str(self.cores)
     366        self.job_dir = os.path.join(self.test_dir, 'JOBS', self.job_name)
     367        self.ref_monitoring_dir = os.path.join(trunk_tests_cases_dir, self.setup_name, 'MONITORING')
     368        self.ref_output_dir = os.path.join(trunk_tests_cases_dir, self.setup_name, 'OUTPUT')
     369        self.res_monitoring_dir = os.path.join(self.job_dir, 'MONITORING')
     370        self.res_output_dir = os.path.join(self.job_dir, 'OUTPUT')
     371        self.failed = None
     372
     373    def get_checkable_file_dicts(self):
     374        if os.path.isdir(self.ref_monitoring_dir):
     375            file_names_monitoring = [s for s in next(os.walk(self.ref_monitoring_dir))[2]]
     376        else:
     377            file_names_monitoring = []
     378        file_paths_monitoring = []
     379        for file_name in file_names_monitoring:
     380            file_specific_ending = file_name[len(self.setup_name):]
     381            file_specific_ending_split = file_specific_ending.split('.')
     382            postfix = file_specific_ending_split[0]
     383            if len(file_specific_ending_split) > 1:
     384                extension = file_specific_ending_split[-1]
     385            else:
     386                extension = ''
     387            if len(file_specific_ending_split) > 2:
     388                cycle_info = file_specific_ending_split[1: -1]
     389            else:
     390                cycle_info = []
     391            file_paths_monitoring.append(
     392                dict(
     393                    postfix=postfix,
     394                    cycle_info=cycle_info,
     395                    extension=extension,
     396                    ref_path=self.ref_monitoring_dir,
     397                    res_path=self.res_monitoring_dir,
     398                )
     399            )
     400        if os.path.isdir(self.ref_output_dir):
     401            file_names_output = [s for s in next(os.walk(self.ref_output_dir))[2]]
     402        else:
     403            file_names_output = []
     404        file_paths_output = []
     405        for file_name in file_names_output:
     406            file_specific_ending = file_name[len(self.setup_name):]
     407            file_specific_ending_split = file_specific_ending.split('.')
     408            postfix = file_specific_ending_split[0]
     409            if len(file_specific_ending_split) > 1:
     410                extension = file_specific_ending_split[-1]
     411            else:
     412                extension = ''
     413            if len(file_specific_ending_split) > 2:
     414                cycle_info = file_specific_ending_split[1: -1]
     415            else:
     416                cycle_info = []
     417            file_paths_output.append(
     418                dict(
     419                    postfix=postfix,
     420                    cycle_info=cycle_info,
     421                    extension=extension,
     422                    ref_path=self.ref_output_dir,
     423                    res_path=self.res_output_dir,
     424                )
     425            )
     426        return file_paths_monitoring + file_paths_output
     427
     428    def check(self):
     429        with Logger(self.test_dir, verbose=self.verbose) as logger:
     430            logger.to_log('Checking output files:')
     431            logger.to_all('\n')
     432            failed = False
     433            for file_dict in self.get_checkable_file_dicts():
     434                file_failed = False
     435                ext_list = [file_dict['extension']] if file_dict['extension'] else []
     436                file_specific_ending = '.'.join([file_dict['postfix']] + file_dict['cycle_info'] + ext_list )
     437                logger.to_all(file_table_line_template.format('Checking:', self.setup_name + file_specific_ending))
     438                ref_file_path = os.path.join(file_dict['ref_path'], self.setup_name + file_specific_ending)
     439                res_file_path = os.path.join(file_dict['res_path'], self.job_name + file_specific_ending)
     440                if re.match('_rc', file_dict['postfix']) and not file_dict['extension']:
     441                    file_failed, message = FileComparator.compare_ascii(ref_file_path, res_file_path, start_string='Run-control output:\n')
     442                elif re.match('nc', file_dict['extension']):
     443                    if self.significant_digits is not None:
     444                        if re.match('_ts', file_dict['postfix']) and 'timeseries' in self.significant_digits:
     445                            file_failed, message = FileComparator.compare_netcdf(ref_file_path, res_file_path,
     446                                                                                 digits=self.significant_digits['timeseries'])
     447                        elif re.match('_pr', file_dict['postfix']) and 'profiles' in self.significant_digits:
     448                            file_failed, message = FileComparator.compare_netcdf(ref_file_path, res_file_path,
     449                                                                                 digits=self.significant_digits['profiles'])
     450                        else:
     451                            file_failed, message = FileComparator.compare_netcdf(ref_file_path, res_file_path,
     452                                                                                 digits=self.significant_digits['other'])
     453                    else:
     454                        file_failed, message = FileComparator.compare_netcdf(ref_file_path, res_file_path)
     455                else:
     456                    message = colored('[ignored]', 'blue')
     457                if file_failed:
     458                    failed = True
     459                logger.to_all(message + '\n')
     460            if self.dry_run:
     461                failed = False
     462        return failed
     463
     464
     465class PALMJob:
     466    """The PALM job class deals with the execution of a single PALM job"""
     467
     468    @staticmethod
     469    def get_job_name(setup_name, build_name, cores):
     470        return setup_name + '__' + build_name + '__' + str(cores)
     471
     472    def __init__(self, test_dir, test_case, build_name, cores, verbose=False, dry_run=False):
     473        self.test_dir = test_dir
     474        self.test_case = test_case
     475        self.build_name = build_name
     476        self.cores = cores
     477        self.verbose = verbose
     478        self.dry_run = dry_run
     479
     480        self.attempted_debug = False
     481        self.failed_debug = None
     482        self.attempted_non_debug = False
     483        self.failed_non_debug = None
     484
     485    def _link_restart_files(self, build_name):
     486        if self.dry_run:
     487            return True, colored('[restart data dry]', 'blue')
     488        name = self.get_job_name(self.test_case.name, build_name, self.cores)
     489        source_name = self.get_job_name(self.test_case.use_binary_files_from, build_name, self.cores)
     490        source_restart_dir = os.path.join(self.test_dir, 'JOBS', source_name, 'RESTART')
     491        try:
     492            source_data_dirs_grp = [d for r, d, f in os.walk(source_restart_dir)]
     493        except:
     494            source_data_dirs_grp = []
     495        if len(source_data_dirs_grp) == 0:
     496            source_data_dirs = []
     497        else:
     498            source_data_dirs = source_data_dirs_grp[0]
     499        if len(source_data_dirs) == 0 and re.match('.+_debug', build_name):
     500            source_build_name = build_name[:-len('_debug')]
     501            source_name = self.get_job_name(self.test_case.use_binary_files_from, source_build_name, self.cores)
     502            source_restart_dir = os.path.join(self.test_dir, 'JOBS', source_name, 'RESTART')
     503            try:
     504                source_data_dirs_grp = [d for r, d, f in os.walk(source_restart_dir)]
     505            except:
     506                source_data_dirs_grp = []
     507            if len(source_data_dirs_grp) == 0:
     508                source_data_dirs = []
     509            else:
     510                source_data_dirs = source_data_dirs_grp[0]
     511        if len(source_data_dirs) == 0:
     512            source_data_dir = 'no_restart_data'
     513        else:
     514            source_data_dir = sorted(source_data_dirs)[-1]
     515        source_data_dir_path = os.path.join(source_restart_dir, source_data_dir)
     516        if os.path.isdir(source_data_dir_path) and re.match('.+_d3d.*', source_data_dir):
     517            job_restart_dir = os.path.join(self.test_dir, 'JOBS', name, 'RESTART')
     518            os.makedirs(job_restart_dir, exist_ok=exist_ok)
     519            job_data_dir_path = os.path.join(job_restart_dir, name + '_d3d')
     520            os.symlink(source_data_dir_path, job_data_dir_path, target_is_directory=True)
     521            return False, colored('[linked restart data from: ' + source_data_dir_path + ']', 'green')
     522        else:
     523            return True, colored('[no restart data found]', 'red')
     524
     525    def _execute(self, name, build_name):
     526        execution_failed = Executor.execute(
     527            [
     528                os.path.join(self.test_dir, 'trunk', 'SCRIPTS', 'palmrun'),
     529                '-h', '\"' + build_name + '\"',
     530                '-d', name,
     531                '-a', '\"' + ' '.join(self.test_case.activation_strings) + '\"',
     532                '-X', str(self.cores),
     533                '-T', str(self.cores),
     534                '-B',
     535                '-v',
     536                '-z',
     537            ],
     538            cwd=self.test_dir,
     539            verbose=self.verbose,
     540            dry_run=self.dry_run,
     541        )
     542
     543        if self.dry_run:
     544            return False, colored('[execution dry]', 'blue')
     545        elif execution_failed:
     546            return True, colored('[execution failed]', 'red')
     547        else:
     548            return False, colored('[execution ok]', 'green')
     549
     550    def _check(self, build_name):
     551        checker = OutputChecker(
     552            self.test_dir,
     553            self.test_case.name,
     554            build_name,
     555            self.cores,
     556            significant_digits=self.test_case.significant_digits,
     557            verbose=self.verbose,
     558            dry_run=self.dry_run,
     559        )
     560        check_failed = checker.check()
     561
     562        if self.dry_run:
     563            return False, colored('[checks dry]', 'blue')
     564        if check_failed:
     565            return True, colored('[checks failed]', 'red')
     566        else:
     567            return False, colored('[checks ok]', 'green')
     568
     569    def execute(self, debug=False):
     570        if debug:
     571            attempted = self.attempted_debug
     572            build_name = self.build_name + '_debug'
     573            failed = self.failed_debug
     574        else:
     575            attempted = self.attempted_non_debug
     576            build_name = self.build_name
     577            failed = self.failed_non_debug
     578
     579        if not attempted:
     580            with Logger(self.test_dir, verbose=self.verbose) as logger:
     581                status_prefix = task_table_line_template.format('Testing:', self.test_case.name, build_name, self.cores)
     582                logger.to_all(status_prefix)
     583                logger.to_log('[started]' + '\n')
     584                attempted = True
     585
     586                name = self.get_job_name(self.test_case.name, build_name, self.cores)
     587
     588                input_dir = os.path.join(self.test_dir, 'JOBS', name, 'INPUT')
     589                os.makedirs(input_dir, exist_ok=exist_ok)
     590
     591                # copying needs to be done per file, because input files need to be renamed
     592                for input_file in self.test_case.input_file_names:
     593                    postfix = input_file[len(self.test_case.name):]
     594                    src = os.path.join(self.test_case.input_dir, input_file)
     595                    dst = os.path.join(input_dir, name + postfix)
     596                    shutil.copy(src, dst)
     597
     598                # copying the entire directory is ok, because source files do not need to be renamed
     599                user_code_dir = os.path.join(self.test_dir, 'JOBS', name, 'USER_CODE')
     600                if os.path.isdir(self.test_case.user_code_dir):
     601                    shutil.copytree(self.test_case.user_code_dir, user_code_dir, copy_function=shutil.copy)
     602
     603                if self.test_case.requires_binary_files:
     604                    link_restart_files_failed, message = self._link_restart_files(build_name)
     605                    logger.to_log(status_prefix)
     606                    logger.to_log(message + ' ')
     607                    logger.to_log('\n')
     608
     609                failed, message = self._execute(name, build_name)
     610                logger.to_log(status_prefix)
     611                logger.to_all(message + ' ')
     612                logger.to_log('\n')
     613
     614                failed, message = self._check(build_name)
     615                logger.to_log(status_prefix)
     616                logger.to_log(message + ' ')
     617
     618                logger.to_all('\n')
     619
     620        if debug:
     621            self.attempted_debug = attempted
     622            self.failed_debug = failed
     623        else:
     624            self.attempted_non_debug = attempted
     625            self.failed_non_debug = failed
     626
     627        return failed
     628
     629    def status(self):
     630        return dict(
     631            attempted=self.attempted_non_debug or self.attempted_debug,
     632            failed=self.failed_non_debug and self.failed_debug,
     633            debugged=self.attempted_debug,
     634            non_debug_failed=self.failed_non_debug,
     635        )
     636
     637
     638class PALMBuild:
     639    """The PALM build class deals with configuration and execution of all required PALM builds"""
     640
     641    def __init__(self, test_dir, build_name, verbose=False, dry_run=False):
     642        self.test_dir = test_dir
     643        self.build_name = build_name
     644        self.verbose = verbose
     645        self.dry_run = dry_run
     646        self.configured = False
     647        self.executed = False
     648        self.available = False
     649        self.requires_mpi = False
     650        self.requires_netcdf = False
     651        self.requires_fftw = False
     652        self.requires_rrtmg = False
     653        self.attempted_non_debug = False
     654        self.attempted_debug = False
     655        self.failed_non_debug = None
     656        self.failed_debug = None
     657
     658    def configure(self):
     659        try:
     660            with open(os.path.join(trunk_tests_builds_dir, self.build_name, 'build_config.yml'), 'r') as f:
     661                build_config = yaml.load(f)
     662        except:
     663            return True, colored('[build not found]', 'red')
     664
     665        if 'compiler' in build_config:
     666            self.compiler = build_config['compiler']
     667        else:
     668            return True, colored('[missing \"compiler\" keyword]', 'red')
     669
     670        if not isinstance(self.compiler, dict):
     671            return True, colored('[\"compiler\" keyword must be dict]', 'red')
     672
     673        if 'linker' in build_config:
     674            self.linker = build_config['linker']
     675        else:
     676            return True, colored('[missing \"linker\" keyword]', 'red')
     677
     678        if not isinstance(self.linker, dict):
     679            return True, colored('[\"linker\" keyword must be dict]', 'red')
     680
     681        if 'mpi_wrapper' in self.compiler:
     682            if 'mpi_wrapper}}' in self.compiler['mpi_wrapper']:
     683                self.requires_mpi = True
     684        else:
     685            return True, colored('[missing \"mpi_wrapper\" keyword]', 'red')
     686
     687        if 'includes' in self.compiler:
     688            for include in self.compiler['includes']:
     689                if 'include.netcdf}}' in include:
     690                    self.requires_netcdf = True
     691                if 'include.fftw}}' in include:
     692                    self.requires_fftw = True
     693                if 'include.rrtmg}}' in include:
     694                    self.requires_rrtmg = True
     695        else:
     696            return True, colored('[missing \"includes\" keyword in compiler]', 'red')
     697
     698        if 'options' in self.linker:
     699            for lib in self.linker['options']:
     700                if 'lib.netcdf}}' in lib:
     701                    self.requires_netcdf = True
     702                if 'lib.fftw}}' in lib:
     703                    self.requires_fftw = True
     704                if 'lib.rrtmg}}' in lib:
     705                    self.requires_rrtmg = True
     706        else:
     707            return True, colored('[missing \"options\" keyword in linker]', 'red')
     708
     709        library_names = []
     710        if self.requires_netcdf:
     711            library_names.append('netcdf')
     712        if self.requires_fftw:
     713            library_names.append('fftw')
     714        if self.requires_rrtmg:
     715            library_names.append('rrtmg')
     716
     717        if not 'executable' in self.compiler:
     718            return True, colored('[missing \"executable\" keyword in compiler]', 'red')
     719
     720        if not 'definitions' in self.compiler:
     721            return True, colored('[missing \"definitions\" keyword in compiler]', 'red')
     722
     723        if not 'options' in self.compiler:
     724            return True, colored('[missing \"options\" keyword in compiler]', 'red')
     725
     726        if not 'default' in self.compiler['options']:
     727            return True, colored('[missing \"default\" keyword in compiler.options]', 'red')
     728
     729        if not 'debug' in self.compiler['options']:
     730            return True, colored('[missing \"debug\" keyword in compiler.options]', 'red')
     731
     732        try:
     733            with open(os.path.join(workspace_dir, 'palmtest.yml'), 'r') as f:
     734                palmtest_config = yaml.load(f)
     735        except:
     736            return True, colored('[palmtest.yml not found]', 'red')
     737
     738        if 'palm_config_template' in palmtest_config:
     739            if isinstance(palmtest_config['palm_config_template'], str):
     740                custom_template = palmtest_config['palm_config_template']
     741        try:
     742            with open(os.path.join(custom_template), 'r') as palm_config_template_file:
     743                template = palm_config_template_file.read()
     744        except:
     745            try:
     746                with open(os.path.join(scripts_dir, '.palm.config.default.in'), 'r') as palm_config_template_file:
     747                    template = palm_config_template_file.read()
     748            except:
     749                return True, colored('[trunk/SCRIPTS/.palm.config.default.in not found]', 'red')
     750
     751        template = template.replace('@CMAKE_INSTALL_PREFIX@', self.test_dir)
     752        template = template.replace('@PALM_HOSTNAME@', socket.gethostname())
     753        template = template.replace('@CMAKE_USERNAME@', getpass.getuser())
     754        template = template.replace('@MPI_Fortran_COMPILER@', self.compiler['mpi_wrapper'])
     755        template = template.replace('@CMAKE_Fortran_COMPILER@', self.compiler['executable'])
     756        cpp_options_str = ['-D' + s for s in self.compiler['definitions']]
     757        template = template.replace('@PALM_CPP_OPTIONS_STR@', ' '.join(cpp_options_str))
     758        template = template.replace('@PALM_CORES@', str(available_cores))
     759        template = template.replace('@PALM_COMPILER_OPTIONS@', '{{palmtest.compiler.options}} ' + ' '.join(self.compiler['includes']))
     760        template = template.replace('@PALM_LINKER_OPTIONS@', ' '.join(self.linker['options']))
     761
     762        if 'environments' in palmtest_config:
     763            available_environments = palmtest_config['environments']
     764        else:
     765            return True, colored('[missing \"environments\" keyword in palmtest.yml]', 'red')
     766
     767        if 'id' in self.compiler:
     768            c_id = self.compiler['id']
     769        else:
     770            return True, colored('[missing \"id\" keyword in compiler]', 'red')
     771
     772        if c_id in available_environments:
     773            self.available = True
     774
     775            environment = available_environments[c_id]
     776            if 'executable' not in environment:
     777                return True, colored('[palmtest.yml environment \"' + c_id + '\" has no \"executable\"]', 'red')
     778            value = environment['executable']
     779            if isinstance(value, str):
     780                template = template.replace('{{' + '.'.join([c_id, 'executable']) + '}}', value)
     781            if self.requires_mpi:
     782                if 'mpi_wrapper' not in environment:
     783                    return True, colored('[palmtest.yml environment \"' + c_id + '\" has no \"mpi_wrapper\"]', 'red')
     784                value = environment['mpi_wrapper']
     785                if isinstance(value, str):
     786                    template = template.replace('{{' + '.'.join([c_id, 'mpi_wrapper']) + '}}', value)
     787            if 'include' not in environment:
     788                return True, colored('[palmtest.yml environment \"' + c_id + '\" has no \"include\"]', 'red')
     789            if 'lib' not in environment:
     790                return True, colored('[palmtest.yml environment \"' + c_id + '\" has no \"lib\"]', 'red')
     791            for lib in library_names:
     792                if lib not in environment['include']:
     793                    return True, colored('[palmtest.yml environment \"' + c_id + '\" has no \"include.'+lib+'\"]', 'red')
     794                value = environment['include'][lib]
     795                if isinstance(value, str):
     796                    template = template.replace('{{' + '.'.join([c_id, 'include', lib]) + '}}', value)
     797                if lib not in environment['lib']:
     798                    return True, colored('[palmtest.yml environment \"' + c_id + '\" has no \"lib.'+lib+'\"]', 'red')
     799                value = environment['lib'][lib]
     800                if isinstance(value, str):
     801                    template = template.replace('{{' + '.'.join([c_id, 'lib', lib]) + '}}', value)
     802
     803            with open(os.path.join(self.test_dir, '.palm.config.' + self.build_name), 'w') as palm_config_file:
     804                palm_config_file.write(
     805                    template.replace(
     806                        '{{palmtest.compiler.options}}',
     807                        ' '.join(self.compiler['options']['default']),
     808                    )
     809                )
     810            with open(os.path.join(self.test_dir, '.palm.config.' + self.build_name + '_debug'), 'w') as palm_config_file:
     811                palm_config_file.write(
     812                    template.replace(
     813                        '{{palmtest.compiler.options}}',
     814                        ' '.join(self.compiler['options']['debug']),
     815                    )
     816                )
     817            self.configured = True
     818            return False, colored('[configuration ok]', 'green')
     819
     820        else:
     821            return True, colored('[palmtest.yml environment \"' + c_id + '\" not found]', 'red')
     822
     823    def _execute(self, build_name):
     824        self.attempted = True
     825        build_failed = Executor.execute(
     826            [
     827                os.path.join(self.test_dir, 'trunk', 'SCRIPTS', 'palmbuild'),
     828                '-h', '\"' + build_name + '\"',
     829                '-v',
     830            ],
     831            cwd=self.test_dir,
     832            verbose=self.verbose,
     833            dry_run=self.dry_run,
     834        )
     835
     836        if self.dry_run:
     837            return False, colored('[build dry]', 'blue')
     838        if build_failed:
     839            return True, colored('[build failed]', 'red')
     840        else:
     841            return False, colored('[build ok]', 'green')
     842
     843    def build(self, debug=False):
     844        if debug:
     845            attempted = self.attempted_debug
     846            build_name = self.build_name + '_debug'
     847            failed = self.failed_debug
     848        else:
     849            attempted = self.attempted_non_debug
     850            build_name = self.build_name
     851            failed = self.failed_non_debug
     852
     853        if not attempted:
     854            with Logger(self.test_dir, verbose=self.verbose) as logger:
     855                status_prefix = task_table_line_template.format('Building:', '', build_name, '')
     856                logger.to_all(status_prefix)
     857                logger.to_log('[started]' + '\n')
     858                attempted = True
     859
     860                failed, message = self._execute(build_name)
     861                logger.to_log(status_prefix)
     862                logger.to_all(message + ' ')
     863                logger.to_all('\n')
     864
     865        if debug:
     866            self.attempted_debug = attempted
     867            self.failed_debug = failed
     868        else:
     869            self.attempted_non_debug = attempted
     870            self.failed_non_debug = failed
     871
     872        return failed
     873
     874    def report(self):
     875        return dict(
     876            failed_debug=self.failed_debug,
     877            failed_non_debug=self.failed_non_debug,
     878        )
     879
     880
     881class PALMTestCase:
     882    """The PALM test case class deals with the configuration and execution of all PALM test cases"""
     883
     884    def __init__(self,test_dir,  name, verbose=False, dry_run=False):
     885        self.test_dir = test_dir
     886        self.name = name
     887        self.verbose = verbose
     888        self.dry_run = dry_run
     889        self.user_code_dir = os.path.join(trunk_tests_cases_dir, self.name, 'USER_CODE')
     890        self.input_dir = os.path.join(trunk_tests_cases_dir, self.name, 'INPUT')
     891        self.number_of_cores = []
     892        self.build_names = []
     893        self.input_file_names = []
     894        self.configured = False
     895
     896    def configure(self, requested_build_names, requested_cores):
     897        f_name = os.path.join(trunk_tests_cases_dir, self.name, 'case_config.yml')
     898        try:
     899            with open(f_name, 'r') as f:
     900                config = yaml.load(f)
     901        except:
     902            return True, colored('[Case \"' + self.name + '\" could not be found.]', 'red')
     903        try:
     904            self.use_binary_files_from = config['use_binary_files_from']
     905        except:
     906            self.use_binary_files_from = None
     907        self.requires_binary_files = bool(self.use_binary_files_from)
     908
     909        if 'allowed_builds' not in config:
     910            return True, colored('[missing \"allowed_builds\" keyword]', 'red')
     911        self.allowed_build_names = config['allowed_builds']
     912
     913        if 'allowed_number_of_cores' not in config:
     914            return True, colored('[missing \"allowed_number_of_cores\" keyword]', 'red')
     915        self.allowed_number_of_cores = config['allowed_number_of_cores']
     916
     917        if 'activation_strings' not in config:
     918            return True, colored('[missing \"activation_strings\" keyword]', 'red')
     919        self.activation_strings = config['activation_strings']
     920
     921        if 'significant_digits_for_netcdf_checks' not in config:
     922            return True, colored('[missing \"significant_digits_for_netcdf_checks\" keyword]', 'red')
     923        self.significant_digits = config['significant_digits_for_netcdf_checks']
     924
     925        if 'timeseries' not in config['significant_digits_for_netcdf_checks']:
     926            return True, colored('[missing \"timeseries\" keyword in significant_digits_for_netcdf_checks]', 'red')
     927
     928        if 'profiles' not in config['significant_digits_for_netcdf_checks']:
     929            return True, colored('[missing \"profiles\" keyword in significant_digits_for_netcdf_checks]', 'red')
     930
     931        if 'other' not in config['significant_digits_for_netcdf_checks']:
     932            return True, colored('[missing \"other\" keyword in significant_digits_for_netcdf_checks]', 'red')
     933
     934        self.number_of_cores = sorted(set(requested_cores).intersection(self.allowed_number_of_cores))
     935        self.build_names = sorted(set(requested_build_names).intersection(self.allowed_build_names))
     936        self.input_file_names = [s for s in next(os.walk(self.input_dir))[2]]
     937        self.configured = True
     938        if len(self.number_of_cores) == 0 :
     939            return True, colored('[no allowed cores requested]', 'blue')
     940        if len(self.build_names) == 0:
     941            return True, colored('[no allowed builds requested]', 'blue')
     942        if len(self.input_file_names) == 0:
     943            return True, colored('[no input files found]', 'red')
     944        return False, colored('[configuration ok]', 'green')
     945
     946
     947
     948class PALMTest:
     949
     950    def __init__(self, args):
     951        self.verbose = args.verbose
     952        self.no_auto_debug = args.no_auto_debug
     953        self.force_debug = args.force_debug
     954        self.fail_on_debug = args.fail_on_debug
     955        self.dry_run = args.dry_run
     956        self.test_id = args.test_id
     957        self.test_case_names = args.cases
     958        self.requested_build_names = args.builds
     959        self.requested_cores = args.cores
     960        self.test_case_queue = []
     961        self.build_database = dict()
     962
     963    def prepare(self):
     964        self.test_dir = os.path.join(tests_dir, self.test_id)
     965        try:
     966            os.makedirs(self.test_dir, exist_ok=exist_ok)
     967        except:
     968            print('ERROR: Found existing test directory: ' + self.test_dir)
     969            exit(1)
     970        with Logger(self.test_dir, verbose=self.verbose) as logger:
     971            logger.to_all(hline)
     972            logger.to_all('This is the PALM tester  (version: ' + version + ')' + '\n')
     973            logger.to_all(hline)
     974            try:
     975                with open(os.path.join(workspace_dir, 'palmtest.yml'), 'r') as f:
     976                    pass
     977            except:
     978                logger.to_all('ERROR: No palmtest.yml file was found in your working directory!\n')
     979                logger.to_all('INFO:  A template for this file can be found at: trunk/TESTS/palmtest.yml\n')
     980                logger.to_all('       Please copy the template to your working directory and adjust it to your system!\n')
     981                exit(1)
     982
     983            self.execution_trunk_dir = os.path.join(self.test_dir, 'trunk')
     984            os.symlink(trunk_dir, self.execution_trunk_dir)
     985            self.execution_jobs_dir = os.path.join(self.test_dir, 'JOBS')
     986            os.makedirs(self.execution_jobs_dir, exist_ok=exist_ok)
     987
     988            try:
     989                with open(os.path.join(scripts_dir, '.palm.iofiles'), 'r') as iofiles_template_file:
     990                    iofiles_template = iofiles_template_file.read()
     991                with open(os.path.join(self.test_dir, '.palm.iofiles'), 'w') as iofiles_file:
     992                    iofiles_file.write(iofiles_template.replace('$fast_io_catalog', '$base_data'))
     993            except:
     994                logger.to_all('ERROR: No .palm.iofiles file was found in trunk/SCRIPTS/')
     995                exit(1)
     996
     997            final_cores_list = list(filter(lambda x: x <= available_cores, self.requested_cores))
     998
     999            logger.to_all(config_table_line_template.format('Object:', 'Name:', 'Action:') + 'Status:\n')
     1000            logger.to_all(hline)
     1001
     1002            if 'all' in self.requested_build_names:
     1003                self.requested_build_names = [name for name in next(os.walk(trunk_tests_builds_dir))[1]]
     1004            found_build_names = []
     1005            for build_name in self.requested_build_names:
     1006                build = PALMBuild(self.test_dir, build_name, verbose=self.verbose, dry_run=self.dry_run)
     1007                configuration_failed, message = build.configure()
     1008                if not configuration_failed:
     1009                    self.build_database[build_name] = build
     1010                    found_build_names.append(build_name)
     1011                    logger.to_all(config_table_line_template.format('Build', build_name, 'approved'))
     1012                    logger.to_all(message + '\n')
     1013                else:
     1014                    logger.to_all(config_table_line_template.format('Build', build_name, 'rejected'))
     1015                    logger.to_all(message + '\n')
     1016            final_build_list = found_build_names
     1017
     1018            if 'all' in self.test_case_names:
     1019                self.test_case_names = sorted([name for name in next(os.walk(trunk_tests_cases_dir))[1]])
     1020
     1021            additional_initial_runs_2 = [self.test_case_names]
     1022            while len(additional_initial_runs_2[-1]) > 0:
     1023                additional_initial_runs_1 = []
     1024                for test_case_name in additional_initial_runs_2[-1]:
     1025                    test_case = PALMTestCase(self.test_dir, test_case_name, verbose=self.verbose)
     1026                    test_case_configuration_failed, message = test_case.configure(final_build_list, final_cores_list)
     1027                    if not test_case_configuration_failed:
     1028                        if test_case.requires_binary_files:
     1029                            additional_initial_runs_1.append(test_case.use_binary_files_from)
     1030                additional_initial_runs_2.append(sorted(set(additional_initial_runs_1)))
     1031
     1032            test_case_order = []
     1033            for i in range(len(additional_initial_runs_2)-1):
     1034                # low and high refer to priority
     1035                low = additional_initial_runs_2[i]
     1036                high = additional_initial_runs_2[i+1]
     1037                for item in high:
     1038                    while item in low:
     1039                        low.remove(item)
     1040                test_case_order.append(low)
     1041
     1042            test_case_order_no_dublicates = []
     1043            for test_cases in test_case_order:
     1044                seen = set()
     1045                seen_add = seen.add
     1046                test_case_order_no_dublicates.append( [x for x in test_cases if not (x in seen or seen_add(x))] )
     1047
     1048            approved_test_case_order = [[]] + list(reversed(test_case_order_no_dublicates))
     1049            for i, test_cases in enumerate(list(approved_test_case_order)):
     1050                info = 'Case (dep)' if i < len(approved_test_case_order)-1 else 'Case'
     1051                for test_case_name in list(test_cases):
     1052                    sys.stdout.flush()
     1053                    test_case = PALMTestCase(self.test_dir, test_case_name, verbose=self.verbose)
     1054                    test_case_configuration_failed, message = test_case.configure(final_build_list, final_cores_list)
     1055                    if test_case_configuration_failed:
     1056                        # removing as configuration failed should only apply to added dependencies
     1057                        approved_test_case_order[i].remove(test_case_name)
     1058                        logger.to_all(config_table_line_template.format(info, test_case_name, 'rejected'))
     1059                        logger.to_all(message + '\n')
     1060                    elif test_case.requires_binary_files:
     1061                        if test_case.use_binary_files_from not in approved_test_case_order[i-1]:
     1062                            # removing as dependency is already removed
     1063                            approved_test_case_order[i].remove(test_case_name)
     1064                            logger.to_all(config_table_line_template.format(info, test_case_name, 'disabled'))
     1065                            logger.to_all(colored('[requires dependency \"' + test_case.use_binary_files_from + '\"]', 'red') + '\n')
     1066                        else:
     1067                            logger.to_all(config_table_line_template.format(info, test_case_name, 'approved'))
     1068                            logger.to_all(message + '\n')
     1069                    else:
     1070                        logger.to_all(config_table_line_template.format(info, test_case_name, 'approved'))
     1071                        logger.to_all(message + '\n')
     1072
     1073            final_case_list = []
     1074            for cases in approved_test_case_order:
     1075                for case in cases:
     1076                    if case not in final_case_list:
     1077                        final_case_list.append(case)
     1078
     1079            for build_name in final_build_list:
     1080                build = PALMBuild(
     1081                    self.test_dir,
     1082                    build_name,
     1083                    verbose=self.verbose,
     1084                    dry_run=self.dry_run,
     1085                )
     1086                configuration_failed, message = build.configure()
     1087                if not configuration_failed:
     1088                    self.build_database[build_name] = build
     1089                else:
     1090                    logger.to_all(message + '\n')
     1091
     1092            for case_name in final_case_list:
     1093                test_case = PALMTestCase(
     1094                    self.test_dir,
     1095                    case_name,
     1096                    verbose=self.verbose,
     1097                    dry_run=self.dry_run,
     1098                )
     1099                test_case_configuration_failed, message = test_case.configure(final_build_list, final_cores_list)
     1100                if not test_case_configuration_failed:
     1101                    self.test_case_queue.append(test_case)
     1102            logger.to_all(hline)
     1103
     1104            r = '{:' + str(table_width_intro) + '} '
     1105
     1106            logger.to_all(r.format('Test ID:') + self.test_id + '\n')
     1107            logger.to_all(r.format('Builds:') + str('\n' + r.format('')).join(sorted(self.build_database.keys())) + '\n')
     1108            logger.to_all(r.format('Cases:') + str('\n' + r.format('')).join([c.name for c in self.test_case_queue]) + '\n')
     1109            logger.to_all(r.format('Cores:') + ' '.join([str(i) for i in final_cores_list]) + '\n')
     1110
     1111    def _execute(self, test_case, build_name, cores):
     1112        job = PALMJob(
     1113            self.test_dir,
     1114            test_case,
     1115            build_name,
     1116            cores,
     1117            verbose=self.verbose,
     1118            dry_run=self.dry_run
     1119        )
     1120        if self.force_debug:
     1121            build_failed_non_debug = True
     1122            job_failed_non_debug = True
     1123            build_failed_debug = self.build_database[build_name].build(debug=True)
     1124            if build_failed_debug:
     1125                job_failed_debug = True
     1126            else:
     1127                job_failed_debug = job.execute(debug=True)
     1128        elif self.no_auto_debug:
     1129            build_failed_non_debug = self.build_database[build_name].build(debug=False)
     1130            if build_failed_non_debug:
     1131                job_failed_non_debug = True
     1132            else:
     1133                job_failed_non_debug = job.execute(debug=False)
     1134            build_failed_debug = None
     1135            job_failed_debug = None
     1136        else:
     1137            build_failed_non_debug = self.build_database[build_name].build(debug=False)
     1138            if build_failed_non_debug:
     1139                job_failed_non_debug = True
     1140                build_failed_debug = self.build_database[build_name].build(debug=True)
     1141                if build_failed_debug:
     1142                    job_failed_debug = False
     1143                else:
     1144                    job_failed_debug = job.execute(debug=True)
     1145            else:
     1146                job_failed_non_debug = job.execute(debug=False)
     1147                if job_failed_non_debug:
     1148                    build_failed_debug = self.build_database[build_name].build(debug=True)
     1149                    if build_failed_debug:
     1150                        job_failed_debug = True
     1151                    else:
     1152                        job_failed_debug = job.execute(debug=True)
     1153                else:
     1154                    build_failed_debug = None
     1155                    job_failed_debug = None
     1156        return dict(
     1157            build_failed_non_debug=build_failed_non_debug,
     1158            job_failed_non_debug=job_failed_non_debug,
     1159            build_failed_debug=build_failed_debug,
     1160            job_failed_debug=job_failed_debug,
     1161        )
     1162
     1163    def execute(self):
     1164        with Logger(self.test_dir, verbose=self.verbose) as logger:
     1165            logger.to_all(hline)
     1166            logger.to_all(task_table_line_template.format('Task:', 'Case:', 'Build:', 'Cores:') + 'Status:\n')
     1167            logger.to_all(hline)
     1168            self.test_report = dict()
     1169            for test_case in self.test_case_queue:
     1170                logger.to_log(hline)
     1171                logger.to_file(hline)
     1172                logger.to_file(hline)
     1173                status_dict = dict()
     1174                for build_name in test_case.build_names:
     1175                    status_dict[build_name] = dict()
     1176                    for cores in test_case.number_of_cores:
     1177                        status_dict[build_name][cores] = self._execute(test_case, build_name, cores)
     1178                self.test_report[test_case.name] = status_dict
     1179                logger.to_log(hline)
     1180                logger.to_file('\n' * 10)
     1181
     1182    def report(self):
     1183        with Logger(self.test_dir, verbose=self.verbose) as logger:
     1184            logger.to_all(hline)
     1185            r = '{:10}' + '    total: ' + '{:<3d}' + \
     1186                             '    ok: ' + colored('{:<3d}', 'green') + \
     1187                       '    debugged: ' + colored('{:<3d}', 'yellow') + \
     1188                         '    failed: ' + colored('{:<3d}', 'red')
     1189            n_all = 0
     1190            n_ok = 0
     1191            n_debugged = 0
     1192            n_failed = 0
     1193            for build_name, build in self.build_database.items():
     1194                status = build.report()
     1195                b = status['failed_non_debug']
     1196                bd = status['failed_debug']
     1197                n_all += 1
     1198                if not b and b is not None:
     1199                    n_ok += 1
     1200                if bd is not None:
     1201                    n_debugged += 1
     1202                if b and (bd or bd is None):
     1203                    n_failed += 1
     1204            logger.to_all(r.format('Builds:', n_all, n_ok, n_debugged, n_failed) + '\n')
     1205            total_failed = n_failed
     1206            total_debugged = n_debugged
     1207            n_all = 0
     1208            n_ok = 0
     1209            n_debugged = 0
     1210            n_failed = 0
     1211            # {'case_name': {'build_name': {4: {'build_failed_debug': None,
     1212            #                                   'build_failed_non_debug': False,
     1213            #                                   'job_failed_debug': None,
     1214            #                                   'job_failed_non_debug': False}}},
     1215            for case_name, case in self.test_report.items():
     1216                for build_name, build in case.items():
     1217                    for cores, results in build.items():
     1218                        n_all += 1
     1219                        b = results['build_failed_non_debug']
     1220                        bd = results['build_failed_debug']
     1221                        j = results['job_failed_non_debug']
     1222                        jd = results['job_failed_debug']
     1223                        if not j:
     1224                            n_ok += 1
     1225                        if jd is not None:
     1226                            n_debugged += 1
     1227                        if j and (jd or jd is None):
     1228                            n_failed += 1
     1229            logger.to_all(r.format('Tests:', n_all, n_ok, n_debugged, n_failed))
     1230            total_failed += n_failed
     1231            total_debugged += n_debugged
     1232        if self.fail_on_debug:
     1233            return (total_failed + total_debugged) > 0
     1234        else:
     1235            return total_failed > 0
     1236
     1237
     1238class CustomCompleter:
     1239
     1240    def __init__(self):
     1241        pass
     1242
     1243    def __call__(self, prefix, parsed_args, **kwargs):
     1244        return (i for i in self.get_items() if i.startswith(prefix))
     1245
     1246    def get_items(self):
     1247        return []
     1248
     1249
     1250class CaseCompleter(CustomCompleter):
     1251
     1252    def get_items(self):
     1253        case_names = [name for name in next(os.walk(trunk_tests_cases_dir))[1]]
     1254        return case_names + ['all']
     1255
     1256
     1257class BuildCompleter(CustomCompleter):
     1258
     1259    def get_items(self):
     1260        build_names = [name for name in next(os.walk(trunk_tests_builds_dir))[1]]
     1261        return build_names + ['all']
     1262
     1263
     1264class PALMTestArgumentParser(ArgumentParser):
     1265
     1266    def __init__(self):
     1267        super().__init__(
     1268            description='This is the PALM tester\n' +
     1269                        'Developer Support: knoop@muk.uni-hannover.de',
     1270            formatter_class=RawTextHelpFormatter,
     1271            add_help=True,
     1272            allow_abbrev=False,
     1273        )
     1274        self.add_argument(
     1275            '--version',
     1276            action='version',
     1277            version=version,
     1278        )
     1279        self.add_argument(
     1280            '--verbose',
     1281            action='store_true',
     1282            dest='verbose',
     1283            help='Increase verbosity of terminal output.',
     1284            required=False,
     1285        )
     1286        self.add_argument(
     1287            '--no-auto-debug',
     1288            action='store_true',
     1289            dest='no_auto_debug',
     1290            help='Disable automatic debugging in case of test failure.',
     1291            required=False,
     1292        )
     1293        self.add_argument(
     1294            '--force-debug',
     1295            action='store_true',
     1296            dest='force_debug',
     1297            help='Force debugging regardless of test failure (ignores --no-auto-debug).',
     1298            required=False,
     1299        )
     1300        self.add_argument(
     1301            '--fail-on-debug',
     1302            action='store_true',
     1303            dest='fail_on_debug',
     1304            help='Return a non-zero exit status in case debugging was required.',
     1305            required=False,
     1306        )
     1307        self.add_argument(
     1308            '--dry-run',
     1309            action='store_true',
     1310            dest='dry_run',
     1311            help='Prepare and process all requested tests without actually building or executing PALM.',
     1312            required=False,
     1313        )
     1314        self.add_argument(
     1315            '--cases',
     1316            action='store',
     1317            dest='cases',
     1318            default=['all'],
     1319            help='A list of test cases to be executed. (default: %(default)s)',
     1320            nargs='+',
     1321            required=False,
     1322            type=str,
     1323            metavar='STR',
     1324        ).completer = CaseCompleter()
     1325        self.add_argument(
     1326            '--builds',
     1327            action='store',
     1328            dest='builds',
     1329            default=['all'],
     1330            help='A list of builds to be executed. (default: %(default)s)',
     1331            nargs='+',
     1332            required=False,
     1333            type=str,
     1334            metavar='STR',
     1335        ).completer = BuildCompleter()
     1336        self.add_argument(
     1337            '--cores',
     1338            action='store',
     1339            dest='cores',
     1340            default=[i for i in range(1, available_cores+1)],
     1341            choices=[i for i in range(1, available_cores+1)],
     1342            help='The number of cores tests are supposed to be executed on. (default: %(default)s)',
     1343            nargs='+',
     1344            required=False,
     1345            type=int,
     1346            metavar='INT',
     1347        )
     1348        self.add_argument(
     1349            '--test-id',
     1350            action='store',
     1351            dest='test_id',
     1352            default=datetime.now().strftime('%Y-%m-%d_%H:%M:%S.%f'),
     1353            help='An individual test id. (default: current timestamp)',
     1354            required=False,
     1355            type=str,
     1356            metavar='STR',
     1357        )
     1358
     1359
     1360if __name__ == '__main__':
     1361    parser = PALMTestArgumentParser()
     1362    if has_argcomplete:
     1363        argcomplete.autocomplete(parser)
     1364    args = parser.parse_args()
     1365    palm_test = PALMTest(args)
     1366    palm_test.prepare()
     1367    palm_test.execute()
     1368    failed = palm_test.report()
     1369    exit(1 if failed else 0)
Note: See TracChangeset for help on using the changeset viewer.