From 24710afe3056b7dfc7a3b031debd831e78773c41 Mon Sep 17 00:00:00 2001 From: Nicolas Saillant Date: Thu, 21 Nov 2024 15:52:34 +0100 Subject: [PATCH 01/12] Add initial version of CGAL testsuite report script --- .../cgal_testsuite_report.py | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 Scripts/developer_scripts/cgal_testsuite_report.py diff --git a/Scripts/developer_scripts/cgal_testsuite_report.py b/Scripts/developer_scripts/cgal_testsuite_report.py new file mode 100644 index 000000000000..77e0d62b551e --- /dev/null +++ b/Scripts/developer_scripts/cgal_testsuite_report.py @@ -0,0 +1,110 @@ +import json +import sys +from typing import Dict, List +from dataclasses import dataclass +from datetime import datetime +import re +import requests + +@dataclass +class TPLInfo: + name: str + version: str + status: str + +@dataclass +class PlatformInfo: + name: str + debug: str + os: str + tester: str + compiler: str + tpl_info: List[TPLInfo] + +def get_latest_version() -> str: + url = "https://cgal.geometryfactory.com/CGAL/Releases/LATEST" + response = requests.get(url) + response.raise_for_status() + version_text = response.text.strip() + match = re.match(r'CGAL-([^.]+\.[^-]+-[^-]+-\d+)', version_text) + if not match: + raise ValueError(f"Unexpected version format: {version_text}") + return match.group(1) + +def fetch_json_data(version: str) -> Dict: + url = f"https://cgal.geometryfactory.com/CGAL/testsuite/CGAL-{version}/search_index.json" + response = requests.get(url) + response.raise_for_status() + return response.json() + +def analyze_tpl_data(json_data: Dict) -> List[PlatformInfo]: + platforms_info = [] + for platform in json_data.get('platforms', []): + tpl_list = [ + TPLInfo( + name=item.get('name', 'Unknown'), + version=item.get('version', 'N/A'), + status=item.get('status', 'unknown') + ) + for item in platform.get('tpl', []) + ] + platform_info = PlatformInfo( + name=platform.get('name', 'Unknown Platform'), + debug=platform.get('debug', '-'), + os=platform.get('os', '-'), + tester=platform.get('tester', '-'), + compiler=platform.get('compiler', '-'), + tpl_info=tpl_list + ) + platforms_info.append(platform_info) + return platforms_info + +def generate_markdown_report(platforms_info: List[PlatformInfo], version: str) -> str: + report = [] + report.append("# TestSuite Report") + report.append(f"Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") + report.append(f"CGAL Version: {version}\n") + report.append("## Platforms Summary") + report.append("\n| Platform | Debug | OS | Tester | Compiler |") + report.append("|----------|--------|----|---------| ---------|") + for platform in platforms_info: + report.append( + f"| [{platform.name}](#platform-{platform.name.lower().replace(' ', '-')}) | {platform.debug} | {platform.os} | " + f"{platform.tester} | {platform.compiler} |" + ) + report.append("\n") + report.append("## Detailed TPL\n") + for platform in platforms_info: + report.append(f"### Platform: {platform.name} ") + tpl_list = sorted(platform.tpl_info, key=lambda x: x.name) + report.append("\n| TPL Name | Version | Status |") + report.append("|----------|----------|---------|") + for tpl in tpl_list: + version_str = str(tpl.version) if tpl.version else "N/A" + status_str = "✅" if tpl.status == "found" else "❌" + report.append(f"| {tpl.name} | {version_str} | {status_str} |") + report.append("\n") + found_tpls = sum(1 for tpl in tpl_list if tpl.status == "found") + total_tpls = len(tpl_list) + report.append(f"**Summary**: {found_tpls}/{total_tpls} TPLs found") + return "\n".join(report) + +def main(): + try: + version = get_latest_version() + json_data = fetch_json_data(version) + platforms_info = analyze_tpl_data(json_data) + markdown_report = generate_markdown_report(platforms_info, version) + print(markdown_report) + except requests.RequestException as e: + print(f"Error fetching data: {str(e)}", file=sys.stderr) + sys.exit(1) + except json.JSONDecodeError: + print("Error: Invalid JSON data", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"Error processing data: {str(e)}", file=sys.stderr) + sys.exit(1) + +if __name__ == "__main__": + main() From 7821b40967b1252a88bc0812c7f914efbb3c8c9e Mon Sep 17 00:00:00 2001 From: Nicolas Saillant Date: Thu, 21 Nov 2024 16:09:25 +0100 Subject: [PATCH 02/12] Enhance JSON data with platform information --- .../test_handling/create_testresult_page | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Maintenance/test_handling/create_testresult_page b/Maintenance/test_handling/create_testresult_page index f26fd7c30de4..988f40d5a1a1 100755 --- a/Maintenance/test_handling/create_testresult_page +++ b/Maintenance/test_handling/create_testresult_page @@ -769,7 +769,21 @@ sub create_summary_page { foreach $platform (@platforms_to_do) { my $third_party_libraries = ""; my @tpl_list = (); + my ($operating_system, $compiler, $tester_name, $tester_address, $build_type); if (open(PLATFORM_INFO, "results_${platform}.info")) { + $_ = ; + $_ = ; + chomp; + $compiler = $_; + $_ = ; + chomp; + $operating_system = $_; + $_ = ; + chomp; + $tester_name = $_; + $_ = ; + chomp; + $tester_address = $_; my $line = ""; while () { $line = $_; @@ -798,9 +812,14 @@ sub create_summary_page { } } close PLATFORM_INFO; + $build_type = $platform_is_optimized{$platform} ? " - " : "YES"; } my $platform_info = { name => $platform, + os => $operating_system, + compiler => $compiler, + tester => $tester_name, + debug => $build_type, tpl => \@tpl_list, test_directories => [], }; From 09ea26bcec89422fc5d1c4b62b905a3838c76444 Mon Sep 17 00:00:00 2001 From: Laurent Rineau Date: Thu, 21 Nov 2024 18:35:24 +0100 Subject: [PATCH 03/12] my review --- .../cgal_testsuite_report.py | 75 +++++++++++-------- 1 file changed, 45 insertions(+), 30 deletions(-) diff --git a/Scripts/developer_scripts/cgal_testsuite_report.py b/Scripts/developer_scripts/cgal_testsuite_report.py index 77e0d62b551e..2b4f5b97d4c1 100644 --- a/Scripts/developer_scripts/cgal_testsuite_report.py +++ b/Scripts/developer_scripts/cgal_testsuite_report.py @@ -1,11 +1,15 @@ import json -import sys from typing import Dict, List from dataclasses import dataclass from datetime import datetime import re import requests +LATEST_VERSION_URL = "https://cgal.geometryfactory.com/CGAL/Releases/LATEST" +JSON_DATA_URL_TEMPLATE = "https://cgal.geometryfactory.com/CGAL/testsuite/CGAL-{version}/search_index.json" +TESTSUITE_URL_TEMPLATE = "https://cgal.geometryfactory.com/CGAL/testsuite/results-{version}.shtml" +TIMEOUT_DURATION = 10 + @dataclass class TPLInfo: name: str @@ -21,23 +25,28 @@ class PlatformInfo: compiler: str tpl_info: List[TPLInfo] -def get_latest_version() -> str: - url = "https://cgal.geometryfactory.com/CGAL/Releases/LATEST" - response = requests.get(url) +def fetch_data_from_url(url: str) -> str: + """Fetch data from a given URL.""" + response = requests.get(url, timeout=TIMEOUT_DURATION) response.raise_for_status() - version_text = response.text.strip() - match = re.match(r'CGAL-([^.]+\.[^-]+-[^-]+-\d+)', version_text) + return response.text.strip() + +def get_latest_version() -> str: + """Return latest CGAL version from LATEST (CGAL-.tar.gz)""" + tarball_name = fetch_data_from_url(LATEST_VERSION_URL) + match = re.match(r'CGAL-([^.]+\.[^-]+-[^-]+-\d+)', tarball_name) if not match: - raise ValueError(f"Unexpected version format: {version_text}") + raise ValueError(f"Unexpected tarball name format: {tarball_name}") return match.group(1) def fetch_json_data(version: str) -> Dict: - url = f"https://cgal.geometryfactory.com/CGAL/testsuite/CGAL-{version}/search_index.json" - response = requests.get(url) - response.raise_for_status() - return response.json() + """Fetch JSON data for the given CGAL testsuite.""" + url = JSON_DATA_URL_TEMPLATE.format(version=version) + json_data = fetch_data_from_url(url) + return json.loads(json_data) def analyze_tpl_data(json_data: Dict) -> List[PlatformInfo]: + """Analyze TPL data from JSON and return a list of PlatformInfo.""" platforms_info = [] for platform in json_data.get('platforms', []): tpl_list = [ @@ -59,37 +68,42 @@ def analyze_tpl_data(json_data: Dict) -> List[PlatformInfo]: platforms_info.append(platform_info) return platforms_info +def fragment_name(platform: PlatformInfo) -> str: + """Return a fragment name from a given platform.""" + return f"platform-{platform.name.lower().replace(' ', '-')}" + def generate_markdown_report(platforms_info: List[PlatformInfo], version: str) -> str: + """Generate a markdown report from the platforms information.""" report = [] report.append("# TestSuite Report") - report.append(f"Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") - report.append(f"CGAL Version: {version}\n") - report.append("## Platforms Summary") - report.append("\n| Platform | Debug | OS | Tester | Compiler |") - report.append("|----------|--------|----|---------| ---------|") + report.append(f"\nGenerated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + url = TESTSUITE_URL_TEMPLATE.format(version=version) + report.append(f"\nCGAL Version: [{version}]({url})\n") + report.append("## Platforms Summary\n") + report.append("| Platform | Debug | OS | Tester | Compiler |") + report.append("|----------|-------|----|--------|----------|") for platform in platforms_info: report.append( - f"| [{platform.name}](#platform-{platform.name.lower().replace(' ', '-')}) | {platform.debug} | {platform.os} | " + f"| [{platform.name}](#{fragment_name(platform)}) | {platform.debug} | {platform.os} | " f"{platform.tester} | {platform.compiler} |" ) - report.append("\n") - report.append("## Detailed TPL\n") + report.append("\n## Detailed Third-party Libraries\n") for platform in platforms_info: - report.append(f"### Platform: {platform.name} ") + report.append(f"### Platform: {platform.name}") tpl_list = sorted(platform.tpl_info, key=lambda x: x.name) - report.append("\n| TPL Name | Version | Status |") + report.append("\n| Library Name | Version | Status |") report.append("|----------|----------|---------|") for tpl in tpl_list: version_str = str(tpl.version) if tpl.version else "N/A" status_str = "✅" if tpl.status == "found" else "❌" report.append(f"| {tpl.name} | {version_str} | {status_str} |") - report.append("\n") found_tpls = sum(1 for tpl in tpl_list if tpl.status == "found") total_tpls = len(tpl_list) - report.append(f"**Summary**: {found_tpls}/{total_tpls} TPLs found") + report.append(f"**Summary**: found {found_tpls} third-party libraries out of {total_tpls}") return "\n".join(report) def main(): + """Main function to generate the testsuite report.""" try: version = get_latest_version() json_data = fetch_json_data(version) @@ -97,14 +111,15 @@ def main(): markdown_report = generate_markdown_report(platforms_info, version) print(markdown_report) except requests.RequestException as e: - print(f"Error fetching data: {str(e)}", file=sys.stderr) - sys.exit(1) - except json.JSONDecodeError: - print("Error: Invalid JSON data", file=sys.stderr) - sys.exit(1) + print(f"**Error fetching data:**\n\n```\n{str(e)}\n```\n") + raise + except json.JSONDecodeError as e: + print(f"**Error: Invalid JSON data**\n\n```\n{str(e)}\n```") + print(f"\nFile:\n\n```json\n{e.doc}\n```") + raise except Exception as e: - print(f"Error processing data: {str(e)}", file=sys.stderr) - sys.exit(1) + print(f"**Error processing data:**\n\n```\n{str(e)}\n```\n") + raise if __name__ == "__main__": main() From 7b5d5e4b1c131fdfb97783a03a1336839392ffc0 Mon Sep 17 00:00:00 2001 From: Laurent Rineau Date: Thu, 21 Nov 2024 18:44:23 +0100 Subject: [PATCH 04/12] use the formatter and fix a few lint warning --- .../cgal_testsuite_report.py | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/Scripts/developer_scripts/cgal_testsuite_report.py b/Scripts/developer_scripts/cgal_testsuite_report.py index 2b4f5b97d4c1..1ca33b8d9b58 100644 --- a/Scripts/developer_scripts/cgal_testsuite_report.py +++ b/Scripts/developer_scripts/cgal_testsuite_report.py @@ -10,12 +10,14 @@ TESTSUITE_URL_TEMPLATE = "https://cgal.geometryfactory.com/CGAL/testsuite/results-{version}.shtml" TIMEOUT_DURATION = 10 + @dataclass class TPLInfo: name: str version: str status: str + @dataclass class PlatformInfo: name: str @@ -25,12 +27,14 @@ class PlatformInfo: compiler: str tpl_info: List[TPLInfo] + def fetch_data_from_url(url: str) -> str: """Fetch data from a given URL.""" response = requests.get(url, timeout=TIMEOUT_DURATION) response.raise_for_status() return response.text.strip() + def get_latest_version() -> str: """Return latest CGAL version from LATEST (CGAL-.tar.gz)""" tarball_name = fetch_data_from_url(LATEST_VERSION_URL) @@ -39,12 +43,14 @@ def get_latest_version() -> str: raise ValueError(f"Unexpected tarball name format: {tarball_name}") return match.group(1) + def fetch_json_data(version: str) -> Dict: """Fetch JSON data for the given CGAL testsuite.""" url = JSON_DATA_URL_TEMPLATE.format(version=version) json_data = fetch_data_from_url(url) return json.loads(json_data) + def analyze_tpl_data(json_data: Dict) -> List[PlatformInfo]: """Analyze TPL data from JSON and return a list of PlatformInfo.""" platforms_info = [] @@ -68,15 +74,18 @@ def analyze_tpl_data(json_data: Dict) -> List[PlatformInfo]: platforms_info.append(platform_info) return platforms_info + def fragment_name(platform: PlatformInfo) -> str: """Return a fragment name from a given platform.""" - return f"platform-{platform.name.lower().replace(' ', '-')}" + return f"platform-{platform.name.lower().replace(' ', '-').replace('.', '')}" + def generate_markdown_report(platforms_info: List[PlatformInfo], version: str) -> str: """Generate a markdown report from the platforms information.""" report = [] report.append("# TestSuite Report") - report.append(f"\nGenerated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + report.append(f"\nGenerated on: { + datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") url = TESTSUITE_URL_TEMPLATE.format(version=version) report.append(f"\nCGAL Version: [{version}]({url})\n") report.append("## Platforms Summary\n") @@ -84,24 +93,27 @@ def generate_markdown_report(platforms_info: List[PlatformInfo], version: str) - report.append("|----------|-------|----|--------|----------|") for platform in platforms_info: report.append( - f"| [{platform.name}](#{fragment_name(platform)}) | {platform.debug} | {platform.os} | " + f"| [{platform.name}](#{fragment_name(platform)}) | { + platform.debug} | {platform.os} | " f"{platform.tester} | {platform.compiler} |" ) - report.append("\n## Detailed Third-party Libraries\n") + report.append("\n## Detailed Third-party Libraries") for platform in platforms_info: - report.append(f"### Platform: {platform.name}") + report.append(f"\n### Platform: {platform.name}\n") tpl_list = sorted(platform.tpl_info, key=lambda x: x.name) - report.append("\n| Library Name | Version | Status |") - report.append("|----------|----------|---------|") + report.append("| Library Name | Version | Status |") + report.append("|--------------|---------|--------|") for tpl in tpl_list: version_str = str(tpl.version) if tpl.version else "N/A" status_str = "✅" if tpl.status == "found" else "❌" report.append(f"| {tpl.name} | {version_str} | {status_str} |") found_tpls = sum(1 for tpl in tpl_list if tpl.status == "found") total_tpls = len(tpl_list) - report.append(f"**Summary**: found {found_tpls} third-party libraries out of {total_tpls}") + report.append( + f"\n**Summary**: found {found_tpls} third-party libraries out of {total_tpls}") return "\n".join(report) + def main(): """Main function to generate the testsuite report.""" try: @@ -121,5 +133,6 @@ def main(): print(f"**Error processing data:**\n\n```\n{str(e)}\n```\n") raise + if __name__ == "__main__": main() From 1105ed409d3e95fb608d9ce539983957f28092cc Mon Sep 17 00:00:00 2001 From: Nicolas Saillant Date: Fri, 29 Nov 2024 15:43:57 +0100 Subject: [PATCH 05/12] Add Docker information retrieval and reporting to testsuite report script --- .../cgal_testsuite_report.py | 45 ++++ .../list_test_runner_machines | 199 ++++++++++++++++++ 2 files changed, 244 insertions(+) create mode 100755 Scripts/developer_scripts/list_test_runner_machines diff --git a/Scripts/developer_scripts/cgal_testsuite_report.py b/Scripts/developer_scripts/cgal_testsuite_report.py index 1ca33b8d9b58..039a1dfc2445 100644 --- a/Scripts/developer_scripts/cgal_testsuite_report.py +++ b/Scripts/developer_scripts/cgal_testsuite_report.py @@ -2,6 +2,8 @@ from typing import Dict, List from dataclasses import dataclass from datetime import datetime +from collections import defaultdict +import subprocess import re import requests @@ -82,12 +84,15 @@ def fragment_name(platform: PlatformInfo) -> str: def generate_markdown_report(platforms_info: List[PlatformInfo], version: str) -> str: """Generate a markdown report from the platforms information.""" + machines_info = get_docker_info() + update_machines_platforms(machines_info, platforms_info) report = [] report.append("# TestSuite Report") report.append(f"\nGenerated on: { datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") url = TESTSUITE_URL_TEMPLATE.format(version=version) report.append(f"\nCGAL Version: [{version}]({url})\n") + add_machines_summary(report, machines_info) report.append("## Platforms Summary\n") report.append("| Platform | Debug | OS | Tester | Compiler |") report.append("|----------|-------|----|--------|----------|") @@ -113,6 +118,46 @@ def generate_markdown_report(platforms_info: List[PlatformInfo], version: str) - f"\n**Summary**: found {found_tpls} third-party libraries out of {total_tpls}") return "\n".join(report) +def get_docker_info() -> Dict[str, Dict[str, List[str]]]: + """Get Docker container information from test machines.""" + result = subprocess.run(['./list_test_runner_machines', '--table'], + capture_output=True, text=True, check=True) + machines_info = defaultdict(lambda: {'containers': [], 'platforms': set()}) + current_machine = "" + for line in result.stdout.split('\n'): + if line.startswith('## '): + current_machine = line.lstrip('# ').strip() + elif line.startswith('| CGAL-') and current_machine: + container = line.split('|')[1].strip() + machines_info[current_machine]['containers'].append(container) + return dict(machines_info) + +def update_machines_platforms(machines_info: Dict[str, Dict[str, List[str]]], platforms_info: List[PlatformInfo]): + """Update machines info with platform names.""" + machine_mapping = { + 'Friedrich': 'cgaltest@friedrich', + 'friedrich': 'cgaltest@friedrich', + 'cgal': 'lrineau@cgal', + 'cgal (GF)': 'lrineau@cgal', + 'Rubens': 'lrineau@rubens', + 'rubens': 'lrineau@rubens', + 'bonnard': 'lrineau@bonnard' + } + for platform in platforms_info: + if platform.tester in machine_mapping: + machine = machine_mapping[platform.tester] + if machine in machines_info: + machines_info[machine]['platforms'].add(platform.name) + +def add_machines_summary(report: List[str], machines_info: Dict[str, Dict[str, List[str]]]): + """Add machines summary to the report.""" + report.append("\n## Test Machines Summary\n") + report.append("| Machine | Containers Count | Platforms |") + report.append("|---------|-----------------|-----------|") + for machine, info in machines_info.items(): + containers_count = len(info['containers']) + platforms = ', '.join(sorted(info['platforms'])) or '-' + report.append(f"| {machine} | {containers_count} | {platforms} |") def main(): """Main function to generate the testsuite report.""" diff --git a/Scripts/developer_scripts/list_test_runner_machines b/Scripts/developer_scripts/list_test_runner_machines new file mode 100755 index 000000000000..0f7b13d251c5 --- /dev/null +++ b/Scripts/developer_scripts/list_test_runner_machines @@ -0,0 +1,199 @@ +#!/bin/bash + +TEST_MACHINES=$( + cat <<'HEREDOC' +lrineau@bonnard +lrineau@cgal +cgaltest@friedrich +lrineau@rubens +HEREDOC +) + +cat </dev/null || { + echo 'sed is required' + exit 1 +} + +if [[ $1 == --table ]] && ! command -v pandoc >/dev/null; then + echo 'pandoc is required for the option --table' + exit 1 +fi +if [[ $1 == --column ]] && ! command -v column >/dev/null; then + echo 'column is required for the option --column' + exit 1 +fi +if [[ $1 == --bat ]] && ! command -v bat >/dev/null; then + echo 'bat is required for the option --bat' + exit 1 +fi + +set_pretty_csv_to_md_table() { + pretty_csv() ( + echo + sed '/```/ d; /^$/ d' | pandoc -f tsv -t gfm + ) +} + +set_pretty_csv_to_column() { + pretty_csv() { + echo + column -t -s $'\t' -o $'\t' | sed 's/^\(```[^ ]*\) *\t.*/\1/' + } +} + +set_pretty_csv_to_bat() { + pretty_csv() { + bat --tabs=50 --paging=never --plain -l csv + } +} + +set_pretty_csv_to_cat() { + pretty_csv() { + cat + } +} + +case "$1" in +--table) set_pretty_csv_to_md_table ;; +--column) set_pretty_csv_to_column ;; +--bat) set_pretty_csv_to_bat ;; +--plain) set_pretty_csv_to_cat ;; +'') + if command -v bat >/dev/null; then + set_pretty_csv_to_bat + elif command -v column >/dev/null; then + set_pretty_csv_to_column + else + set_pretty_csv_to_cat + fi + ;; +*) + echo "Unknown option $1" + exit 1 + ;; +esac + +for machine in $TEST_MACHINES; do + USER=${machine%@*} + HOST=${machine#*@} + machine_title $machine + printf '\nusing `%s`\n' "$(ssh $HOST docker --version)" + printf '\nTested images:\n' + machine_tested_images $HOST $USER + printf '\nCGAL test containers:\n' + machine_list_cgal_test_container $HOST $USER | pretty_csv +done From 20424d4f20f0e470efb0c5451da3cfa413b2c850 Mon Sep 17 00:00:00 2001 From: Nicolas Saillant Date: Tue, 3 Dec 2024 15:27:10 +0100 Subject: [PATCH 06/12] Add Docker image retrieval and summary to testsuite report --- .../cgal_testsuite_report.py | 101 +++++++++--------- 1 file changed, 51 insertions(+), 50 deletions(-) diff --git a/Scripts/developer_scripts/cgal_testsuite_report.py b/Scripts/developer_scripts/cgal_testsuite_report.py index 039a1dfc2445..2e79e0df25e3 100644 --- a/Scripts/developer_scripts/cgal_testsuite_report.py +++ b/Scripts/developer_scripts/cgal_testsuite_report.py @@ -2,7 +2,6 @@ from typing import Dict, List from dataclasses import dataclass from datetime import datetime -from collections import defaultdict import subprocess import re import requests @@ -77,29 +76,71 @@ def analyze_tpl_data(json_data: Dict) -> List[PlatformInfo]: return platforms_info -def fragment_name(platform: PlatformInfo) -> str: - """Return a fragment name from a given platform.""" - return f"platform-{platform.name.lower().replace(' ', '-').replace('.', '')}" +def get_docker_images() -> Dict[str, List[str]]: + """ + Get Docker image information by calling `list_test_runner_machines`. + Returns a dictionary with machine names as keys and lists of images as values. + """ + try: + result = subprocess.run( + ['./list_test_runner_machines', '--plain'], + capture_output=True, + text=True, + check=True + ) + output = result.stdout.strip() + + machines_info = {} + current_machine = None + parsing_images = False + + for line in output.splitlines(): + if line.startswith("## "): + current_machine = line.strip("# ").strip() + machines_info[current_machine] = [] + parsing_images = False + + elif line.startswith("Tested images:"): + parsing_images = True + + elif parsing_images and line.startswith("cgal/testsuite-docker:"): + machines_info[current_machine].append(line.strip()) + + return machines_info + + except subprocess.CalledProcessError as e: + raise RuntimeError(f"Error running `list_test_runner_machines`: {e}") from e + except Exception as e: + raise RuntimeError(f"Error parsing Docker information: {e}") from e + + +def add_docker_summary(report: List[str], machines_info: Dict[str, List[str]]): + """Add a summary of Docker images used on each machine to the report.""" + report.append("\n## Docker Test Summary\n") + for machine, images in machines_info.items(): + report.append(f"\n### Machine: {machine} ({len(images)} images)") + report.append("\n#### Tested Images:") + for image in images: + report.append(f"- {image}") + report.append("") def generate_markdown_report(platforms_info: List[PlatformInfo], version: str) -> str: """Generate a markdown report from the platforms information.""" - machines_info = get_docker_info() - update_machines_platforms(machines_info, platforms_info) + machines_info = get_docker_images() report = [] report.append("# TestSuite Report") report.append(f"\nGenerated on: { datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") url = TESTSUITE_URL_TEMPLATE.format(version=version) report.append(f"\nCGAL Version: [{version}]({url})\n") - add_machines_summary(report, machines_info) - report.append("## Platforms Summary\n") + add_docker_summary(report, machines_info) + report.append("\n## Platforms Summary\n") report.append("| Platform | Debug | OS | Tester | Compiler |") report.append("|----------|-------|----|--------|----------|") for platform in platforms_info: report.append( - f"| [{platform.name}](#{fragment_name(platform)}) | { - platform.debug} | {platform.os} | " + f"| {platform.name} | {platform.debug} | {platform.os} | " f"{platform.tester} | {platform.compiler} |" ) report.append("\n## Detailed Third-party Libraries") @@ -118,46 +159,6 @@ def generate_markdown_report(platforms_info: List[PlatformInfo], version: str) - f"\n**Summary**: found {found_tpls} third-party libraries out of {total_tpls}") return "\n".join(report) -def get_docker_info() -> Dict[str, Dict[str, List[str]]]: - """Get Docker container information from test machines.""" - result = subprocess.run(['./list_test_runner_machines', '--table'], - capture_output=True, text=True, check=True) - machines_info = defaultdict(lambda: {'containers': [], 'platforms': set()}) - current_machine = "" - for line in result.stdout.split('\n'): - if line.startswith('## '): - current_machine = line.lstrip('# ').strip() - elif line.startswith('| CGAL-') and current_machine: - container = line.split('|')[1].strip() - machines_info[current_machine]['containers'].append(container) - return dict(machines_info) - -def update_machines_platforms(machines_info: Dict[str, Dict[str, List[str]]], platforms_info: List[PlatformInfo]): - """Update machines info with platform names.""" - machine_mapping = { - 'Friedrich': 'cgaltest@friedrich', - 'friedrich': 'cgaltest@friedrich', - 'cgal': 'lrineau@cgal', - 'cgal (GF)': 'lrineau@cgal', - 'Rubens': 'lrineau@rubens', - 'rubens': 'lrineau@rubens', - 'bonnard': 'lrineau@bonnard' - } - for platform in platforms_info: - if platform.tester in machine_mapping: - machine = machine_mapping[platform.tester] - if machine in machines_info: - machines_info[machine]['platforms'].add(platform.name) - -def add_machines_summary(report: List[str], machines_info: Dict[str, Dict[str, List[str]]]): - """Add machines summary to the report.""" - report.append("\n## Test Machines Summary\n") - report.append("| Machine | Containers Count | Platforms |") - report.append("|---------|-----------------|-----------|") - for machine, info in machines_info.items(): - containers_count = len(info['containers']) - platforms = ', '.join(sorted(info['platforms'])) or '-' - report.append(f"| {machine} | {containers_count} | {platforms} |") def main(): """Main function to generate the testsuite report.""" From d602987879d14aa9ecd916eb97de8c09fa5b1eca Mon Sep 17 00:00:00 2001 From: Laurent Rineau Date: Thu, 12 Dec 2024 18:11:36 +0100 Subject: [PATCH 07/12] update the script --- .../list_test_runner_machines | 80 ++++++++++++++----- 1 file changed, 61 insertions(+), 19 deletions(-) diff --git a/Scripts/developer_scripts/list_test_runner_machines b/Scripts/developer_scripts/list_test_runner_machines index 0f7b13d251c5..a644a58aa77b 100755 --- a/Scripts/developer_scripts/list_test_runner_machines +++ b/Scripts/developer_scripts/list_test_runner_machines @@ -9,12 +9,6 @@ lrineau@rubens HEREDOC ) -cat </dev/null || { - echo 'sed is required' +help() { + cat </dev/null || { + error_out 'sed is required' +} + + if [[ $1 == --table ]] && ! command -v pandoc >/dev/null; then - echo 'pandoc is required for the option --table' - exit 1 + error_out 'pandoc is required for the option --table' fi if [[ $1 == --column ]] && ! command -v column >/dev/null; then - echo 'column is required for the option --column' - exit 1 + error_out 'column is required for the option --column' fi if [[ $1 == --bat ]] && ! command -v bat >/dev/null; then - echo 'bat is required for the option --bat' - exit 1 + error_out 'bat is required for the option --bat' fi set_pretty_csv_to_md_table() { @@ -177,21 +193,47 @@ case "$1" in set_pretty_csv_to_bat elif command -v column >/dev/null; then set_pretty_csv_to_column + elif command -v pandoc >/dev/null; then + set_pretty_csv_to_md_table else set_pretty_csv_to_cat fi ;; *) - echo "Unknown option $1" - exit 1 + error_out "Unknown option $1" ;; esac +ERROR_MACHINES="" +for machine in $TEST_MACHINES; do + USER=${machine%@*} + HOST=${machine#*@} + ssh "$HOST" test -f /home/$USER/.config/CGAL/test_cgal_docker_images || { + ERROR_MACHINES="$ERROR_MACHINES $machine" + } +done +if [ -n "$ERROR_MACHINES" ]; then + for machine in $ERROR_MACHINES; do + USER=${machine%@*} + HOST=${machine#*@} + printf 'ERROR: cannot read file `/home/%s/.config/CGAL/test_cgal_docker_images` on ssh host `%s`\n' $USER $HOST + done + exit 1 +fi +cat < Date: Thu, 12 Dec 2024 18:12:05 +0100 Subject: [PATCH 08/12] Add shebang and fix list_test_runner_machines path in cgal_testsuite_report.py --- Scripts/developer_scripts/cgal_testsuite_report.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) mode change 100644 => 100755 Scripts/developer_scripts/cgal_testsuite_report.py diff --git a/Scripts/developer_scripts/cgal_testsuite_report.py b/Scripts/developer_scripts/cgal_testsuite_report.py old mode 100644 new mode 100755 index 2e79e0df25e3..ff37fccfa0cd --- a/Scripts/developer_scripts/cgal_testsuite_report.py +++ b/Scripts/developer_scripts/cgal_testsuite_report.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python3 +import os import json from typing import Dict, List from dataclasses import dataclass @@ -82,8 +84,9 @@ def get_docker_images() -> Dict[str, List[str]]: Returns a dictionary with machine names as keys and lists of images as values. """ try: + script_dir = os.path.dirname(os.path.abspath(__file__)) result = subprocess.run( - ['./list_test_runner_machines', '--plain'], + [os.path.join(script_dir, 'list_test_runner_machines'), '--plain'], capture_output=True, text=True, check=True From 0c1ccd6ed1ba32dd2ea6721858cd1c7a80ec186a Mon Sep 17 00:00:00 2001 From: Laurent Rineau Date: Thu, 12 Dec 2024 18:20:20 +0100 Subject: [PATCH 09/12] lint and format the Python script --- Scripts/developer_scripts/cgal_testsuite_report.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Scripts/developer_scripts/cgal_testsuite_report.py b/Scripts/developer_scripts/cgal_testsuite_report.py index ff37fccfa0cd..58df7f834d22 100755 --- a/Scripts/developer_scripts/cgal_testsuite_report.py +++ b/Scripts/developer_scripts/cgal_testsuite_report.py @@ -8,9 +8,12 @@ import re import requests -LATEST_VERSION_URL = "https://cgal.geometryfactory.com/CGAL/Releases/LATEST" -JSON_DATA_URL_TEMPLATE = "https://cgal.geometryfactory.com/CGAL/testsuite/CGAL-{version}/search_index.json" -TESTSUITE_URL_TEMPLATE = "https://cgal.geometryfactory.com/CGAL/testsuite/results-{version}.shtml" +CGAL_SERVER_URL = "https://cgal.geometryfactory.com/CGAL" +LATEST_VERSION_URL = f"{CGAL_SERVER_URL}/Releases/LATEST" +JSON_DATA_URL_TEMPLATE = f"{ + CGAL_SERVER_URL}/testsuite/CGAL-{{version}}/search_index.json" +TESTSUITE_URL_TEMPLATE = f"{ + CGAL_SERVER_URL}/testsuite/results-{{version}}.shtml" TIMEOUT_DURATION = 10 @@ -112,7 +115,8 @@ def get_docker_images() -> Dict[str, List[str]]: return machines_info except subprocess.CalledProcessError as e: - raise RuntimeError(f"Error running `list_test_runner_machines`: {e}") from e + raise RuntimeError( + f"Error running `list_test_runner_machines`: {e}") from e except Exception as e: raise RuntimeError(f"Error parsing Docker information: {e}") from e From 70353fb54785469e94af07d7d4710d1bf69835af Mon Sep 17 00:00:00 2001 From: Nicolas Saillant Date: Fri, 13 Dec 2024 14:35:05 +0100 Subject: [PATCH 10/12] Refactor cgal_testsuite_report.py to improve variable naming and enhance third-party library handling --- Maintenance/test_handling/create_testresult_page | 4 +++- Scripts/developer_scripts/cgal_testsuite_report.py | 14 +++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Maintenance/test_handling/create_testresult_page b/Maintenance/test_handling/create_testresult_page index b89f1382ed9b..1e64ecb66bdc 100755 --- a/Maintenance/test_handling/create_testresult_page +++ b/Maintenance/test_handling/create_testresult_page @@ -750,6 +750,8 @@ sub create_summary_page { my ($platform_num, $platform) = (0, ""); foreach $platform (@platforms_to_do) { my $platform_info = $platforms_info{$platform}; + my $build_type = $platform_is_optimized{$platform} ? " - " : "YES"; + $platform_info->{debug} = $build_type; foreach my $test_directory (sort keys %test_directories) { my $result_letter = $testresults[$platform_num]->{$test_directory}; if (defined($result_letter) && grep { $_ eq $result_letter } @letters) { @@ -769,7 +771,7 @@ sub create_summary_page { release => $release_name, platforms => \@platforms_data, }; - my $json = JSON->new->allow_nonref; + my $json = JSON->new->allow_nonref->pretty; my $json_text = $json->encode($final_data); my $fh = new IO::Compress::Gzip "$testresult_dir/$release_name/search_index.json.gz" or die "IO::Compress::Gzip failed: $GzipError\n"; diff --git a/Scripts/developer_scripts/cgal_testsuite_report.py b/Scripts/developer_scripts/cgal_testsuite_report.py index 58df7f834d22..4c820a71057b 100755 --- a/Scripts/developer_scripts/cgal_testsuite_report.py +++ b/Scripts/developer_scripts/cgal_testsuite_report.py @@ -67,13 +67,13 @@ def analyze_tpl_data(json_data: Dict) -> List[PlatformInfo]: version=item.get('version', 'N/A'), status=item.get('status', 'unknown') ) - for item in platform.get('tpl', []) + for item in platform.get('third_party_libs', []) ] platform_info = PlatformInfo( - name=platform.get('name', 'Unknown Platform'), + name=platform.get('platform_name', 'Unknown Platform'), debug=platform.get('debug', '-'), - os=platform.get('os', '-'), - tester=platform.get('tester', '-'), + os=platform.get('operating_system', '-'), + tester=platform.get('tester_name', '-'), compiler=platform.get('compiler', '-'), tpl_info=tpl_list ) @@ -109,7 +109,7 @@ def get_docker_images() -> Dict[str, List[str]]: elif line.startswith("Tested images:"): parsing_images = True - elif parsing_images and line.startswith("cgal/testsuite-docker:"): + elif parsing_images and (line.startswith("cgal/testsuite-docker:") or line.startswith("docker.io/cgal/testsuite-docker:")): machines_info[current_machine].append(line.strip()) return machines_info @@ -158,9 +158,9 @@ def generate_markdown_report(platforms_info: List[PlatformInfo], version: str) - report.append("|--------------|---------|--------|") for tpl in tpl_list: version_str = str(tpl.version) if tpl.version else "N/A" - status_str = "✅" if tpl.status == "found" else "❌" + status_str = "❌" if tpl.version == "not found" else "✅" report.append(f"| {tpl.name} | {version_str} | {status_str} |") - found_tpls = sum(1 for tpl in tpl_list if tpl.status == "found") + found_tpls = sum(1 for tpl in tpl_list if tpl.version != "not found") total_tpls = len(tpl_list) report.append( f"\n**Summary**: found {found_tpls} third-party libraries out of {total_tpls}") From 1cf07aaa07e16163d41390e68432d03c605ccb2a Mon Sep 17 00:00:00 2001 From: Laurent Rineau Date: Fri, 13 Dec 2024 16:22:51 +0100 Subject: [PATCH 11/12] fix the linter warnings --- .markdownlint.json | 7 +++++++ Scripts/developer_scripts/cgal_testsuite_report.py | 7 +++---- 2 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 .markdownlint.json diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 000000000000..70106c515cea --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,7 @@ +{ + "default": true, + "line-length": false, + "no-duplicate-heading": { + "siblings_only": true + } +} diff --git a/Scripts/developer_scripts/cgal_testsuite_report.py b/Scripts/developer_scripts/cgal_testsuite_report.py index 4c820a71057b..e7953776764a 100755 --- a/Scripts/developer_scripts/cgal_testsuite_report.py +++ b/Scripts/developer_scripts/cgal_testsuite_report.py @@ -123,13 +123,12 @@ def get_docker_images() -> Dict[str, List[str]]: def add_docker_summary(report: List[str], machines_info: Dict[str, List[str]]): """Add a summary of Docker images used on each machine to the report.""" - report.append("\n## Docker Test Summary\n") + report.append("\n## Docker Test Summary") for machine, images in machines_info.items(): report.append(f"\n### Machine: {machine} ({len(images)} images)") - report.append("\n#### Tested Images:") + report.append("\n#### Tested Images\n") for image in images: report.append(f"- {image}") - report.append("") def generate_markdown_report(platforms_info: List[PlatformInfo], version: str) -> str: @@ -140,7 +139,7 @@ def generate_markdown_report(platforms_info: List[PlatformInfo], version: str) - report.append(f"\nGenerated on: { datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") url = TESTSUITE_URL_TEMPLATE.format(version=version) - report.append(f"\nCGAL Version: [{version}]({url})\n") + report.append(f"\nCGAL Version: [{version}]({url})") add_docker_summary(report, machines_info) report.append("\n## Platforms Summary\n") report.append("| Platform | Debug | OS | Tester | Compiler |") From b3a59ac00af0e6de8a02a852a9ff187434767059 Mon Sep 17 00:00:00 2001 From: Laurent Rineau Date: Fri, 13 Dec 2024 16:23:15 +0100 Subject: [PATCH 12/12] save my last version - linter warnings, - better use of `column|bat` --- .../list_test_runner_machines | 201 ++++++++++++------ 1 file changed, 140 insertions(+), 61 deletions(-) diff --git a/Scripts/developer_scripts/list_test_runner_machines b/Scripts/developer_scripts/list_test_runner_machines index a644a58aa77b..78977ca81e30 100755 --- a/Scripts/developer_scripts/list_test_runner_machines +++ b/Scripts/developer_scripts/list_test_runner_machines @@ -1,22 +1,34 @@ #!/bin/bash -TEST_MACHINES=$( - cat <<'HEREDOC' -lrineau@bonnard -lrineau@cgal -cgaltest@friedrich -lrineau@rubens -HEREDOC +# Declare an associative array to store the images used on each machine +declare -A MACHINE_CONTAINER_IMAGES + +TEST_MACHINES=( + "lrineau@bonnard" + "lrineau@cgal" + "cgaltest@friedrich" + "lrineau@rubens" ) machine_title() { - printf '\n## %s ##\n' $1 + printf '\n## %s ##\n' "$1" +} + +machine_info() { + HOST=$1 + remote_script=$(printf "export PS4='+ %s >> %s'\n" "$HOST" "$PS4")$'\n'$( + cat <<'EOF' +source /etc/os-release +printf '\n- OS: `%s`\n- container implementation: `%s`\n' "$PRETTY_NAME" "$(docker --version)" +EOF + ) + ssh "$HOST" bash -$- -s <<<"$remote_script" } machine_tested_images() { echo echo '```plain' - ssh $1 cat /home/$2/.config/CGAL/test_cgal_docker_images + printf '%s\n' "${MACHINE_CONTAINER_IMAGES["$1"]}" echo '```' } @@ -28,9 +40,9 @@ declare -xf docker_is_active_cmd docker_cmd() { if docker_is_active_cmd; then - docker $@ + docker "$@" else - podman --url unix:/var/run/podman/podman.sock $@ + podman --url unix:/var/run/podman/podman.sock "$@" fi } declare -xf docker_cmd @@ -41,12 +53,12 @@ list_of_containers_cmd() { declare -xf list_of_containers_cmd container_status_cmd() { - docker_cmd inspect --format '{{.State.Status}}' $1 + docker_cmd inspect --format '{{.State.Status}}' "$1" } declare -xf container_status_cmd container_human_readable_status_cmd() { - docker_cmd ps --all --filter name=$1 --format '{{.Status}}' + docker_cmd ps --all --filter name="$1" --format '{{.Status}}' } declare -xf container_human_readable_status_cmd @@ -61,19 +73,19 @@ simplify_date_cmd() { declare -xf simplify_date_cmd container_start_time_cmd() { - simplify_date_cmd "$(docker_cmd inspect --format '{{.State.StartedAt}}' $1)" + simplify_date_cmd "$(docker_cmd inspect --format '{{.State.StartedAt}}' "$1")" } declare -xf container_start_time_cmd container_end_time_cmd() { - simplify_date_cmd "$(docker_cmd inspect --format '{{.State.FinishedAt}}' $1)" + simplify_date_cmd "$(docker_cmd inspect --format '{{.State.FinishedAt}}' "$1")" } declare -xf container_end_time_cmd container_running_time_cmd() { - start_time=$(container_start_time_cmd $1) - end_time=$(container_end_time_cmd $1) - status=$(container_status_cmd $1) + start_time=$(container_start_time_cmd "$1") + end_time=$(container_end_time_cmd "$1") + status=$(container_status_cmd "$1") if [ "$status" = "running" ]; then end_time=$(date -u '+%Y-%m-%dT%H:%M:%S.%NZ') fi @@ -91,19 +103,19 @@ list_cgal_test_container_cmd() { # docker_cmd ps -a --filter name=CGAL- display_one_container_line_cmd "CONTAINER" "START TIME" "END TIME" "RUNNING TIME" "STATUS" for container in $(list_of_containers_cmd); do - start_time="$(container_start_time_cmd $container)" - end_time="$(container_end_time_cmd $container)" - dur=$(container_running_time_cmd $container) - status="$(container_status_cmd $container) - $(container_human_readable_status_cmd $container)" + start_time="$(container_start_time_cmd "$container")" + end_time="$(container_end_time_cmd "$container")" + dur=$(container_running_time_cmd "$container") + status="$(container_status_cmd "$container") - $(container_human_readable_status_cmd "$container")" display_one_container_line_cmd "$container" "$start_time" "$end_time" "$dur" "$status" done } declare -xf list_cgal_test_container_cmd display_all_exported_cmd_functions() { - funcs=$(declare -F | awk '/ -fx .*_cmd$/ {print $3}') - for func in $funcs; do - declare -f $func + functions=$(declare -F | awk '/ -fx .*_cmd$/ {print $3}') + for func in $functions; do + declare -f "$func" done } @@ -114,7 +126,7 @@ machine_list_cgal_test_container() { printf "export PS4='+ %s >> %s'\n" "$1" "$PS4" echo list_cgal_test_container_cmd ) - ssh $1 bash -$- -s <<<"$remote_script" + ssh "$1" bash -$- -s <<<"$remote_script" printf '```\n' } @@ -124,18 +136,26 @@ Usage: $0 [OPTION] List the test runner machines and the containers running on them. -Options: - --table output in markdown table format - --column output in column format - --bat output with bat - --plain output in plain text +Output Format Options: + --table output in markdown table format + --column output in column format + --bat output with bat + --plain output in plain text If no option is given, the script will try to use bat, then column, and finally plain text. + +Information Options: + --images list the images used on each machine + --containers list the containers running on each machine + --info list the OS and the container implementation on each machine + +If no information option is given, the script will list all the information. HEREDOC } error_out() { + exec >&2 echo "ERROR: $1" echo help @@ -146,7 +166,6 @@ command -v sed >/dev/null || { error_out 'sed is required' } - if [[ $1 == --table ]] && ! command -v pandoc >/dev/null; then error_out 'pandoc is required for the option --table' fi @@ -171,6 +190,13 @@ set_pretty_csv_to_column() { } } +set_pretty_csv_to_column_and_bat() { + pretty_csv() { + echo + column -t -s $'\t' -o $'\t' | sed 's/^\(```[^ ]*\) *\t.*/\1/' | bat --paging=never --plain -l csv + } +} + set_pretty_csv_to_bat() { pretty_csv() { bat --tabs=50 --paging=never --plain -l csv @@ -183,40 +209,90 @@ set_pretty_csv_to_cat() { } } -case "$1" in ---table) set_pretty_csv_to_md_table ;; ---column) set_pretty_csv_to_column ;; ---bat) set_pretty_csv_to_bat ;; ---plain) set_pretty_csv_to_cat ;; -'') - if command -v bat >/dev/null; then - set_pretty_csv_to_bat - elif command -v column >/dev/null; then - set_pretty_csv_to_column - elif command -v pandoc >/dev/null; then +WHAT=() + +add_to_what() { + for i in "$@"; do + WHAT+=("$i") + done +} + +what_contains() { + local item=$1 + for i in "${WHAT[@]}"; do + if [[ "$i" == "$item" ]]; then + return 0 + fi + done + return 1 +} + +for arg in "$@"; do + case "$arg" in + --table) set_pretty_csv_to_md_table ;; + --column) set_pretty_csv_to_column ;; + --bat) set_pretty_csv_to_bat ;; + --plain) set_pretty_csv_to_cat ;; + --images) add_to_what images ;; + --containers) add_to_what containers ;; + --info) add_to_what info ;; + -h | --help) + help >&2 + exit 0 + ;; + *) + error_out "Unknown option $arg" + ;; + esac +done + +if [ ${#WHAT[@]} -eq 0 ]; then + add_to_what info images containers +fi + +STDOUT_IS_A_TTY= +if <&1 tty -s; then + STDOUT_IS_A_TTY=1 +fi + +BAT= +[ -n "$STDOUT_IS_A_TTY" ] && BAT=$(command -v bat) + +COLUMN=$(command -v column) + +PANDOC=$(command -v pandoc) +if ! declare -f pretty_csv >/dev/null; then + if [ -n "$BAT" ]; then + if [ -n "$COLUMN" ]; then + set_pretty_csv_to_column_and_bat + else + set_pretty_csv_to_bat + fi + elif [ -n "$PANDOC" ]; then set_pretty_csv_to_md_table + elif [ -n "$COLUMN" ]; then + set_pretty_csv_to_column else set_pretty_csv_to_cat fi - ;; -*) - error_out "Unknown option $1" - ;; -esac +fi ERROR_MACHINES="" -for machine in $TEST_MACHINES; do +for machine in "${TEST_MACHINES[@]}"; do USER=${machine%@*} HOST=${machine#*@} - ssh "$HOST" test -f /home/$USER/.config/CGAL/test_cgal_docker_images || { + # shellcheck disable=SC2029 + MACHINE_CONTAINER_IMAGES[$machine]=$(ssh "$HOST" cat "/home/$USER/.config/CGAL/test_cgal_docker_images") || { ERROR_MACHINES="$ERROR_MACHINES $machine" } done if [ -n "$ERROR_MACHINES" ]; then + printf '\n> %s\n> %s\n' '[!CAUTION]' 'ERROR:' for machine in $ERROR_MACHINES; do USER=${machine%@*} HOST=${machine#*@} - printf 'ERROR: cannot read file `/home/%s/.config/CGAL/test_cgal_docker_images` on ssh host `%s`\n' $USER $HOST + # shellcheck disable=SC2016 + printf '> - ERROR: cannot read file `/home/%s/.config/CGAL/test_cgal_docker_images` on ssh host `%s`\n' "$USER" "$HOST" done exit 1 fi @@ -226,16 +302,19 @@ cat <