diff options
-rw-r--r-- | resources/tools/presentation/generator_cpta.py | 49 | ||||
-rw-r--r-- | resources/tools/presentation/input_data_files.py | 70 | ||||
-rw-r--r-- | resources/tools/presentation/input_data_parser.py | 80 | ||||
-rw-r--r-- | resources/tools/presentation/specification_CPTA.yaml | 143 | ||||
-rw-r--r-- | resources/tools/presentation/specification_parser.py | 17 |
5 files changed, 113 insertions, 246 deletions
diff --git a/resources/tools/presentation/generator_cpta.py b/resources/tools/presentation/generator_cpta.py index 8d10e15a49..ac0a5c60e1 100644 --- a/resources/tools/presentation/generator_cpta.py +++ b/resources/tools/presentation/generator_cpta.py @@ -338,11 +338,7 @@ def _generate_all_charts(spec, input_data): :rtype: dict """ - logs = list() - - logs.append( - (u"INFO", f" Generating the chart {graph.get(u'title', u'')} ...") - ) + logging.info(f" Generating the chart {graph.get(u'title', u'')} ...") job_name = list(graph[u"data"].keys())[0] @@ -350,11 +346,9 @@ def _generate_all_charts(spec, input_data): res = dict() # Transform the data - logs.append( - (u"INFO", + logging.info( f" Creating the data set for the {graph.get(u'type', u'')} " f"{graph.get(u'title', u'')}." - ) ) if graph.get(u"include", None): @@ -410,13 +404,10 @@ def _generate_all_charts(spec, input_data): for tag in group: for tst_name, test_data in chart_data.items(): if not test_data: - logs.append( - (u"WARNING", f"No data for the test {tst_name}") - ) + logging.warning(f"No data for the test {tst_name}") continue if tag not in chart_tags[tst_name]: continue - message = f"index: {index}, test: {tst_name}" try: trace, rslt = _generate_trending_traces( test_data, @@ -426,10 +417,8 @@ def _generate_all_charts(spec, input_data): split(u'-')[2:-1]), color=COLORS[index]) except IndexError: - logs.append( - (u"ERROR", f"Out of colors: {message}") - ) - logging.error(f"Out of colors: {message}") + logging.error(f"Out of colors: index: " + f"{index}, test: {tst_name}") index += 1 continue traces.extend(trace) @@ -441,11 +430,8 @@ def _generate_all_charts(spec, input_data): else: for tst_name, test_data in chart_data.items(): if not test_data: - logs.append( - (u"WARNING", f"No data for the test {tst_name}") - ) + logging.warning(f"No data for the test {tst_name}") continue - message = f"index: {index}, test: {tst_name}" try: trace, rslt = _generate_trending_traces( test_data, @@ -455,8 +441,9 @@ def _generate_all_charts(spec, input_data): tst_name.split(u'.')[-1].split(u'-')[2:-1]), color=COLORS[index]) except IndexError: - logs.append((u"ERROR", f"Out of colors: {message}")) - logging.error(f"Out of colors: {message}") + logging.error( + f"Out of colors: index: {index}, test: {tst_name}" + ) index += 1 continue traces.extend(trace) @@ -514,25 +501,13 @@ def _generate_all_charts(spec, input_data): f"{spec.cpta[u'output-file']}/{graph[u'output-file-name']}" f"{spec.cpta[u'output-file-type']}") - logs.append((u"INFO", f" Writing the file {name_file} ...")) + logging.info(f" Writing the file {name_file} ...") plpl = plgo.Figure(data=traces, layout=layout) try: ploff.plot(plpl, show_link=False, auto_open=False, filename=name_file) except plerr.PlotlyEmptyDataError: - logs.append((u"WARNING", u"No data for the plot. Skipped.")) - - for level, line in logs: - if level == u"INFO": - logging.info(line) - elif level == u"ERROR": - logging.error(line) - elif level == u"DEBUG": - logging.debug(line) - elif level == u"CRITICAL": - logging.critical(line) - elif level == u"WARNING": - logging.warning(line) + logging.warning(u"No data for the plot. Skipped.") return {u"job_name": job_name, u"csv_table": csv_tbl, u"results": res} @@ -542,7 +517,7 @@ def _generate_all_charts(spec, input_data): builds_dict[job] = list() for build in spec.input[u"builds"][job]: status = build[u"status"] - if status not in (u"failed", u"not found", u"removed"): + if status not in (u"failed", u"not found", u"removed", None): builds_dict[job].append(str(build[u"build"])) # Create "build ID": "date" dict: diff --git a/resources/tools/presentation/input_data_files.py b/resources/tools/presentation/input_data_files.py index e1fa5b2284..8b941f2f94 100644 --- a/resources/tools/presentation/input_data_files.py +++ b/resources/tools/presentation/input_data_files.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Cisco and/or its affiliates. +# Copyright (c) 2020 Cisco and/or its affiliates. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at: @@ -42,16 +42,14 @@ SEPARATOR = u"__" REGEX_RELEASE = re.compile(r'(\D*)(\d{4}|master)(\D*)') -def _download_file(url, file_name, log, arch=False): +def _download_file(url, file_name, arch=False): """Download a file with input data. :param url: URL to the file to download. :param file_name: Name of file to download. - :param log: List of log messages. :param arch: If True, also .gz file is downloaded :type url: str :type file_name: str - :type log: list of tuples (severity, msg) :type arch: bool :returns: True if the download was successful, otherwise False. :rtype: bool @@ -90,29 +88,27 @@ def _download_file(url, file_name, log, arch=False): success = False session = None try: - log.append((u"INFO", f" Connecting to {url} ...")) + logging.info(f" Connecting to {url} ...") session = requests_retry_session() response = session.get(url, stream=True) code = response.status_code - log.append((u"INFO", f" {code}: {responses[code]}")) + logging.info(f" {code}: {responses[code]}") if code != codes[u"OK"]: if session: session.close() url = url.replace(u"_info", u"") - log.append((u"INFO", f" Connecting to {url} ...")) + logging.info(f" Connecting to {url} ...") session = requests_retry_session() response = session.get(url, stream=True) code = response.status_code - log.append((u"INFO", f" {code}: {responses[code]}")) + logging.info(f" {code}: {responses[code]}") if code != codes[u"OK"]: return False, file_name file_name = file_name.replace(u"_info", u"") dst_file_name = file_name.replace(u".gz", u"") - log.append( - (u"INFO", f" Downloading the file {url} to {dst_file_name} ...") - ) + logging.info(f" Downloading the file {url} to {dst_file_name} ...") with open(dst_file_name, u"wb") as file_handle: for chunk in response.iter_content(chunk_size=CHUNK_SIZE): if chunk: @@ -121,44 +117,37 @@ def _download_file(url, file_name, log, arch=False): if arch and u".gz" in file_name: if session: session.close() - log.append( - (u"INFO", f" Downloading the file {url} to {file_name} ...") - ) + logging.info(f" Downloading the file {url} to {file_name} ...") session = requests_retry_session() response = session.get(url, stream=True) if response.status_code == codes[u"OK"]: with open(file_name, u"wb") as file_handle: file_handle.write(response.raw.read()) else: - log.append( - (u"ERROR", f"Not possible to download the file {url} to " - f"{file_name} ...") + logging.error( + f"Not possible to download the file {url} to {file_name}" ) success = True except RequestException as err: - log.append( - (u"ERROR", f"HTTP Request exception:\n{repr(err)}") - ) + logging.error(f"HTTP Request exception:\n{repr(err)}") except (IOError, ValueError, KeyError) as err: - log.append((u"ERROR", f"Download failed.\n{repr(err)}")) + logging.error(f"Download failed.\n{repr(err)}") finally: if session: session.close() - log.append((u"INFO", u" Download finished.")) + logging.info(u" Download finished.") return success, file_name -def _unzip_file(spec, build, pid, log): +def _unzip_file(spec, build, pid): """Unzip downloaded source file. :param spec: Specification read form the specification file. :param build: Information about the build. - :param log: List of log messages. :type spec: Specification :type build: dict - :type log: list of tuples (severity, msg) :returns: True if the download was successful, otherwise False. :rtype: bool """ @@ -178,42 +167,35 @@ def _unzip_file(spec, build, pid, log): new_name = \ f"{file_name.rsplit(u'.')[-2]}{SEPARATOR}{data_file.split(u'/')[-1]}" - log.append((u"INFO", f" Unzipping: {data_file} from {file_name}.")) + logging.info(f" Unzipping: {data_file} from {file_name}.") try: with ZipFile(file_name, u'r') as zip_file: zip_file.extract(data_file, tmp_dir) - log.append( - (u"INFO", f" Renaming the file {join(tmp_dir, data_file)} to " - f"{new_name}") + logging.info( + f" Renaming the file {join(tmp_dir, data_file)} to {new_name}" ) rename(join(tmp_dir, data_file), new_name) build[u"file-name"] = new_name return True except (BadZipfile, RuntimeError) as err: - log.append( - (u"ERROR", f"Failed to unzip the file {file_name}: {repr(err)}.") - ) + logging.error(f"Failed to unzip the file {file_name}: {repr(err)}.") return False except OSError as err: - log.append( - (u"ERROR", f"Failed to rename the file {data_file}: {repr(err)}.") - ) + logging.error(f"Failed to rename the file {data_file}: {repr(err)}.") return False -def download_and_unzip_data_file(spec, job, build, pid, log): +def download_and_unzip_data_file(spec, job, build, pid): """Download and unzip a source file. :param spec: Specification read form the specification file. :param job: Name of the Jenkins job. :param build: Information about the build. :param pid: PID of the process executing this method. - :param log: List of log messages. :type spec: Specification :type job: str :type build: dict :type pid: int - :type log: list of tuples (severity, msg) :returns: True if the download was successful, otherwise False. :rtype: bool """ @@ -235,7 +217,7 @@ def download_and_unzip_data_file(spec, job, build, pid, log): logging.info(f"Trying to download {url}") arch = bool(spec.configuration.get(u"archive-inputs", True)) - success, downloaded_name = _download_file(url, new_name, log, arch=arch) + success, downloaded_name = _download_file(url, new_name, arch=arch) if not success: @@ -261,9 +243,7 @@ def download_and_unzip_data_file(spec, job, build, pid, log): spec.environment[u"paths"][u"DIR[WORKING,DATA]"], f"{job}{SEPARATOR}{build[u'build']}{SEPARATOR}{file_name[idx]}" ) - success, downloaded_name = _download_file( - url, new_name, log, arch=arch - ) + success, downloaded_name = _download_file(url, new_name, arch=arch) if success: file_name = file_name[idx] if file_name.endswith(u".gz"): @@ -296,11 +276,11 @@ def download_and_unzip_data_file(spec, job, build, pid, log): logging.info(f"Downloading {url}") - success, downloaded_name = _download_file(url, new_name, log) + success, downloaded_name = _download_file(url, new_name) if success and downloaded_name.endswith(u".zip"): if not is_zipfile(downloaded_name): - log.append((u"ERROR", f"Zip file {new_name} is corrupted.")) + logging.error(f"Zip file {new_name} is corrupted.") success = False if success: @@ -310,6 +290,6 @@ def download_and_unzip_data_file(spec, job, build, pid, log): build[u"file-name"] = downloaded_name[:-3] if downloaded_name.endswith(u".zip"): - success = _unzip_file(spec, build, pid, log) + success = _unzip_file(spec, build, pid) return success diff --git a/resources/tools/presentation/input_data_parser.py b/resources/tools/presentation/input_data_parser.py index cd9c1a248d..27db6a84d8 100644 --- a/resources/tools/presentation/input_data_parser.py +++ b/resources/tools/presentation/input_data_parser.py @@ -1,4 +1,4 @@ -# Copyright (c) 2019 Cisco and/or its affiliates. +# Copyright (c) 2020 Cisco and/or its affiliates. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at: @@ -1394,16 +1394,14 @@ class InputData: """ return self.data[job][build][u"tests"] - def _parse_tests(self, job, build, log): + def _parse_tests(self, job, build): """Process data from robot output.xml file and return JSON structured data. :param job: The name of job which build output data will be processed. :param build: The build which output data will be processed. - :param log: List of log messages. :type job: str :type build: dict - :type log: list of tuples (severity, msg) :returns: JSON data structure. :rtype: dict """ @@ -1417,9 +1415,8 @@ class InputData: try: result = ExecutionResult(data_file) except errors.DataError as err: - log.append( - (u"ERROR", f"Error occurred while parsing output.xml: " - f"{repr(err)}") + logging.error( + f"Error occurred while parsing output.xml: {repr(err)}" ) return None checker = ExecutionChecker(metadata, self._cfg.mapping, @@ -1444,40 +1441,30 @@ class InputData: :type repeat: int """ - logs = list() - - logs.append( - (u"INFO", f" Processing the job/build: {job}: {build[u'build']}") - ) + logging.info(f" Processing the job/build: {job}: {build[u'build']}") state = u"failed" success = False data = None do_repeat = repeat while do_repeat: - success = download_and_unzip_data_file(self._cfg, job, build, pid, - logs) + success = download_and_unzip_data_file(self._cfg, job, build, pid) if success: break do_repeat -= 1 if not success: - logs.append( - (u"ERROR", + logging.error( f"It is not possible to download the input data file from the " f"job {job}, build {build[u'build']}, or it is damaged. " - f"Skipped.") + f"Skipped." ) if success: - logs.append( - (u"INFO", - f" Processing data from the build {build[u'build']} ...") - ) - data = self._parse_tests(job, build, logs) + logging.info(f" Processing data from build {build[u'build']}") + data = self._parse_tests(job, build) if data is None: - logs.append( - (u"ERROR", + logging.error( f"Input data file from the job {job}, build " - f"{build[u'build']} is damaged. Skipped.") + f"{build[u'build']} is damaged. Skipped." ) else: state = u"processed" @@ -1485,13 +1472,13 @@ class InputData: try: remove(build[u"file-name"]) except OSError as err: - logs.append( - ("ERROR", f"Cannot remove the file {build[u'file-name']}: " - f"{repr(err)}") + logging.error( + f"Cannot remove the file {build[u'file-name']}: {repr(err)}" ) # If the time-period is defined in the specification file, remove all # files which are outside the time period. + is_last = False timeperiod = self._cfg.input.get(u"time-period", None) if timeperiod and data: now = dt.utcnow() @@ -1505,26 +1492,20 @@ class InputData: # Remove the data and the file: state = u"removed" data = None - logs.append( - (u"INFO", - f" The build {job}/{build[u'build']} is " - f"outdated, will be removed.") + is_last = True + logging.info( + f" The build {job}/{build[u'build']} is " + f"outdated, will be removed." ) - logs.append((u"INFO", u" Done.")) - - for level, line in logs: - if level == u"INFO": - logging.info(line) - elif level == u"ERROR": - logging.error(line) - elif level == u"DEBUG": - logging.debug(line) - elif level == u"CRITICAL": - logging.critical(line) - elif level == u"WARNING": - logging.warning(line) - - return {u"data": data, u"state": state, u"job": job, u"build": build} + logging.info(u" Done.") + + return { + u"data": data, + u"state": state, + u"job": job, + u"build": build, + u"last": is_last + } def download_and_parse_data(self, repeat=1): """Download the input data files, parse input data from input files and @@ -1541,6 +1522,8 @@ class InputData: for build in builds: result = self._download_and_parse_build(job, build, repeat) + if result[u"last"]: + break build_nr = result[u"build"][u"build"] if result[u"data"]: @@ -1908,9 +1891,10 @@ class InputData: data[job][str(build)][ test_id][param] = u"No Data" except KeyError as err: - logging.error(repr(err)) if continue_on_error: + logging.debug(repr(err)) continue + logging.error(repr(err)) return None return data diff --git a/resources/tools/presentation/specification_CPTA.yaml b/resources/tools/presentation/specification_CPTA.yaml index 72eab82b40..1e199511bd 100644 --- a/resources/tools/presentation/specification_CPTA.yaml +++ b/resources/tools/presentation/specification_CPTA.yaml @@ -198,206 +198,111 @@ csit-vpp-perf-mrr-daily-master: start: 1086 end: "lastCompletedBuild" - skip: - - 1102 + max-builds: 180 # Max nr of builds to use csit-dpdk-perf-mrr-weekly-master: start: 107 end: "lastCompletedBuild" - skip: - - 110 - - 111 - - 112 - - 114 + max-builds: 15 # Max nr of builds to use plot-performance-trending-vpp-3n-hsw: csit-vpp-perf-mrr-daily-master: start: 1086 end: "lastCompletedBuild" - skip: - - 1102 + max-builds: 180 # Max nr of builds to use plot-performance-trending-dpdk-3n-hsw: csit-dpdk-perf-mrr-weekly-master: start: 107 end: "lastCompletedBuild" - skip: - - 110 - - 111 - - 112 - - 114 + max-builds: 15 # Max nr of builds to use # 3n-skx plot-performance-trending-all-3n-skx: csit-vpp-perf-mrr-daily-master-3n-skx: start: 834 end: "lastCompletedBuild" - skip: - - 849 - - 851 - - 862 - - 864 - - 865 - - 866 + max-builds: 180 # Max nr of builds to use csit-dpdk-perf-mrr-weekly-master-3n-skx: start: 72 end: "lastCompletedBuild" - skip: - - 76 - - 77 - - 79 + max-builds: 15 # Max nr of builds to use plot-performance-trending-vpp-3n-skx: csit-vpp-perf-mrr-daily-master-3n-skx: start: 834 end: "lastCompletedBuild" - skip: - - 849 - - 851 - - 862 - - 864 - - 865 - - 866 + max-builds: 180 # Max nr of builds to use plot-performance-trending-dpdk-3n-skx: csit-dpdk-perf-mrr-weekly-master-3n-skx: start: 72 end: "lastCompletedBuild" - skip: - - 76 - - 77 - - 79 + max-builds: 15 # Max nr of builds to use # 2n-skx plot-performance-trending-all-2n-skx: csit-vpp-perf-mrr-daily-master-2n-skx: start: 858 end: "lastCompletedBuild" - skip: - - 868 - - 870 - - 874 + max-builds: 180 # Max nr of builds to use csit-dpdk-perf-mrr-weekly-master-2n-skx: start: 78 end: "lastCompletedBuild" - skip: - - 79 + max-builds: 15 # Max nr of builds to use plot-performance-trending-vpp-2n-skx: csit-vpp-perf-mrr-daily-master-2n-skx: start: 858 end: "lastCompletedBuild" - skip: - - 868 - - 870 - - 874 + max-builds: 180 # Max nr of builds to use plot-performance-trending-dpdk-2n-skx: csit-dpdk-perf-mrr-weekly-master-2n-skx: start: 78 end: "lastCompletedBuild" - skip: - - 79 + max-builds: 15 # Max nr of builds to use plot-performance-trending-vpp-nfv-2n-skx: csit-vpp-perf-mrr-weekly-master-2n-skx: start: 50 end: "lastCompletedBuild" - skip: - - 52 - - 54 - - 55 + max-builds: 15 # Max nr of builds to use plot-performance-trending-vpp-2n-clx: csit-vpp-perf-mrr-daily-master-2n-clx: start: 236 end: "lastCompletedBuild" - skip: - - 239 - - 248 - - 251 - - 252 - - 253 + max-builds: 180 # Max nr of builds to use plot-performance-trending-dpdk-2n-clx: csit-dpdk-perf-mrr-weekly-master-2n-clx: start: 23 end: "lastCompletedBuild" - skip: - - 24 + max-builds: 15 # Max nr of builds to use # 3n-tsh plot-performance-trending-all-3n-tsh: csit-vpp-perf-mrr-daily-master-3n-tsh: start: 144 end: "lastCompletedBuild" - skip: - - 149 - - 150 - - 152 - - 153 - - 154 - - 155 - - 156 - - 157 + max-builds: 180 # Max nr of builds to use plot-performance-trending-vpp-3n-tsh: csit-vpp-perf-mrr-daily-master-3n-tsh: start: 144 end: "lastCompletedBuild" - skip: - - 149 - - 150 - - 152 - - 153 - - 154 - - 155 - - 156 - - 157 + max-builds: 180 # Max nr of builds to use plot-performance-trending-vpp-3n-dnv: csit-vpp-perf-mrr-daily-master-3n-dnv: start: 329 end: "lastCompletedBuild" - skip: - - 343 - - 344 - - 345 - - 348 - - 350 - - 356 - - 357 - - 358 - - 360 - - 363 - - 364 - - 381 - - 387 - - 388 - - 390 - - 391 - - 392 + max-builds: 180 # Max nr of builds to use plot-performance-trending-vpp-2n-dnv: csit-vpp-perf-mrr-daily-master-2n-dnv: start: 335 end: "lastCompletedBuild" - skip: - - 344 - - 349 - - 350 - - 351 - - 354 - - 360 - - 364 - - 365 - - 366 - - 370 - - 378 - - 387 - - 394 - - 395 - - 396 - - 397 plot-layouts: @@ -497,11 +402,13 @@ csit-vpp-perf-mrr-daily-master: start: 1086 end: "lastCompletedBuild" + max-builds: 180 # Max nr of builds to download skip: - 1102 csit-dpdk-perf-mrr-weekly-master: start: 113 end: "lastCompletedBuild" + max-builds: 15 # Max nr of builds to download skip: - 114 @@ -509,6 +416,7 @@ csit-vpp-perf-mrr-daily-master-3n-skx: start: 834 end: "lastCompletedBuild" + max-builds: 180 # Max nr of builds to download skip: - 849 - 851 @@ -519,6 +427,7 @@ csit-dpdk-perf-mrr-weekly-master-3n-skx: start: 78 end: "lastCompletedBuild" + max-builds: 15 # Max nr of builds to download skip: - 79 @@ -526,6 +435,7 @@ csit-vpp-perf-mrr-daily-master-2n-skx: start: 858 end: "lastCompletedBuild" + max-builds: 180 # Max nr of builds to download skip: - 868 - 870 @@ -533,6 +443,7 @@ csit-vpp-perf-mrr-weekly-master-2n-skx: start: 50 end: "lastCompletedBuild" + max-builds: 15 # Max nr of builds to download skip: - 52 - 54 @@ -540,12 +451,14 @@ csit-dpdk-perf-mrr-weekly-master-2n-skx: start: 78 end: "lastCompletedBuild" + max-builds: 15 # Max nr of builds to download skip: - 79 csit-vpp-perf-mrr-daily-master-2n-clx: start: 236 end: "lastCompletedBuild" + max-builds: 180 # Max nr of builds to download skip: - 239 - 248 @@ -556,6 +469,7 @@ csit-dpdk-perf-mrr-weekly-master-2n-clx: start: 23 end: "lastCompletedBuild" + max-builds: 15 # Max nr of builds to download skip: - 24 @@ -563,6 +477,7 @@ csit-vpp-perf-mrr-daily-master-3n-tsh: start: 144 end: "lastCompletedBuild" + max-builds: 180 # Max nr of builds to download skip: - 149 - 150 @@ -577,6 +492,7 @@ csit-vpp-perf-mrr-daily-master-3n-dnv: start: 329 end: "lastCompletedBuild" + max-builds: 180 # Max nr of builds to download skip: - 343 - 344 @@ -600,6 +516,7 @@ csit-vpp-perf-mrr-daily-master-2n-dnv: start: 335 end: "lastCompletedBuild" + max-builds: 180 # Max nr of builds to download skip: - 344 - 349 diff --git a/resources/tools/presentation/specification_parser.py b/resources/tools/presentation/specification_parser.py index 61ef42ef8d..548bbff532 100644 --- a/resources/tools/presentation/specification_parser.py +++ b/resources/tools/presentation/specification_parser.py @@ -528,14 +528,15 @@ class Specification: continue if isinstance(builds, dict): build_end = builds.get(u"end", None) + max_builds = builds.get(u"max-builds", None) try: build_end = int(build_end) except ValueError: # defined as a range <start, build_type> build_end = self._get_build_number(job, build_end) - builds = [x for x in range(builds[u"start"], - build_end + 1) - if x not in builds.get(u"skip", list())] + builds = [x for x in range(builds[u"start"], build_end + 1)] + if max_builds and max_builds < len(builds): + builds = builds[:max_builds] self.configuration[u"data-sets"][set_name][job] = builds elif isinstance(builds, list): for idx, item in enumerate(builds): @@ -590,14 +591,23 @@ class Specification: if builds: if isinstance(builds, dict): build_end = builds.get(u"end", None) + max_builds = builds.get(u"max-builds", None) + reverse = bool(builds.get(u"reverse", False)) try: build_end = int(build_end) except ValueError: # defined as a range <start, build_type> + if build_end in (u"lastCompletedBuild", + u"lastSuccessfulBuild"): + reverse = True build_end = self._get_build_number(job, build_end) builds = [x for x in range(builds[u"start"], build_end + 1) if x not in builds.get(u"skip", list())] + if reverse: + builds.reverse() + if max_builds and max_builds < len(builds): + builds = builds[:max_builds] self._specification[u"input"][u"builds"][job] = list() for build in builds: self._specification[u"input"][u"builds"][job]. \ @@ -608,6 +618,7 @@ class Specification: f"No build is defined for the job {job}. Trying to " f"continue without it." ) + except KeyError: raise PresentationError(u"No data to process.") |