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/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 new file mode 100755 index 000000000000..e7953776764a --- /dev/null +++ b/Scripts/developer_scripts/cgal_testsuite_report.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 +import os +import json +from typing import Dict, List +from dataclasses import dataclass +from datetime import datetime +import subprocess +import re +import requests + +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 + + +@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 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) + match = re.match(r'CGAL-([^.]+\.[^-]+-[^-]+-\d+)', tarball_name) + if not match: + 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 = [] + 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('third_party_libs', []) + ] + platform_info = PlatformInfo( + name=platform.get('platform_name', 'Unknown Platform'), + debug=platform.get('debug', '-'), + os=platform.get('operating_system', '-'), + tester=platform.get('tester_name', '-'), + compiler=platform.get('compiler', '-'), + tpl_info=tpl_list + ) + platforms_info.append(platform_info) + return platforms_info + + +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: + script_dir = os.path.dirname(os.path.abspath(__file__)) + result = subprocess.run( + [os.path.join(script_dir, '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:") or line.startswith("docker.io/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") + for machine, images in machines_info.items(): + report.append(f"\n### Machine: {machine} ({len(images)} images)") + report.append("\n#### Tested Images\n") + for image in images: + report.append(f"- {image}") + + +def generate_markdown_report(platforms_info: List[PlatformInfo], version: str) -> str: + """Generate a markdown report from the platforms information.""" + 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})") + 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} | {platform.debug} | {platform.os} | " + f"{platform.tester} | {platform.compiler} |" + ) + report.append("\n## Detailed Third-party Libraries") + for platform in platforms_info: + report.append(f"\n### Platform: {platform.name}\n") + tpl_list = sorted(platform.tpl_info, key=lambda x: x.name) + 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.version == "not found" else "✅" + report.append(f"| {tpl.name} | {version_str} | {status_str} |") + 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}") + return "\n".join(report) + + +def main(): + """Main function to generate the testsuite report.""" + 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:**\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:**\n\n```\n{str(e)}\n```\n") + raise + + +if __name__ == "__main__": + main() diff --git a/Scripts/developer_scripts/list_test_runner_machines b/Scripts/developer_scripts/list_test_runner_machines new file mode 100755 index 000000000000..78977ca81e30 --- /dev/null +++ b/Scripts/developer_scripts/list_test_runner_machines @@ -0,0 +1,320 @@ +#!/bin/bash + +# 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" +} + +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' + printf '%s\n' "${MACHINE_CONTAINER_IMAGES["$1"]}" + echo '```' +} + +docker_is_active_cmd() { + systemctl is-active -q docker + return $? +} +declare -xf docker_is_active_cmd + +docker_cmd() { + if docker_is_active_cmd; then + docker "$@" + else + podman --url unix:/var/run/podman/podman.sock "$@" + fi +} +declare -xf docker_cmd + +list_of_containers_cmd() { + docker_cmd ps -a --format '{{.Names}}' --filter name="CGAL-" +} +declare -xf list_of_containers_cmd + +container_status_cmd() { + 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}}' +} +declare -xf container_human_readable_status_cmd + +simplify_date_cmd() { + date=$1 + pattern=' \+[0-9]{4} [+]?[A-Z0-9]{3,}$' + if [[ $date =~ $pattern ]]; then + date=${date% *} + fi + echo "$date" +} +declare -xf simplify_date_cmd + +container_start_time_cmd() { + 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")" +} +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") + if [ "$status" = "running" ]; then + end_time=$(date -u '+%Y-%m-%dT%H:%M:%S.%NZ') + fi + secs=$(($(date -d "$end_time" +%s) - $(date -d "$start_time" +%s))) + printf '%02dh:%02dm:%02ds\n' $((secs / 3600)) $((secs % 3600 / 60)) $((secs % 60)) +} +declare -xf container_running_time_cmd + +display_one_container_line_cmd() { + printf '%s\t%s\t%s\t%s\t%s\n' "$1" "$2" "$3" "$4" "$5" +} +declare -xf display_one_container_line_cmd + +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")" + 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() { + functions=$(declare -F | awk '/ -fx .*_cmd$/ {print $3}') + for func in $functions; do + declare -f "$func" + done +} + +machine_list_cgal_test_container() { + printf '\n```tsv\n' + remote_script=$( + display_all_exported_cmd_functions + printf "export PS4='+ %s >> %s'\n" "$1" "$PS4" + echo list_cgal_test_container_cmd + ) + ssh "$1" bash -$- -s <<<"$remote_script" + printf '```\n' +} + +help() { + cat <&2 + echo "ERROR: $1" + echo + help + exit 1 +} + +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 +if [[ $1 == --column ]] && ! command -v column >/dev/null; then + error_out 'column is required for the option --column' +fi +if [[ $1 == --bat ]] && ! command -v bat >/dev/null; then + error_out 'bat is required for the option --bat' +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_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 + } +} + +set_pretty_csv_to_cat() { + pretty_csv() { + cat + } +} + +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 +fi + +ERROR_MACHINES="" +for machine in "${TEST_MACHINES[@]}"; do + USER=${machine%@*} + HOST=${machine#*@} + # 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#*@} + # 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 +cat <