source: palm/trunk/SCRIPTS/palmtest @ 3570

Last change on this file since 3570 was 3561, checked in by knoop, 6 years ago

palmtest: enabled content-check of all found netcdf variables

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