[rocprof-compute] Merge CDash Nightly and Continuous workflow files (#2279)

* merged code-coverage and continuous workflow files

* fixed runner typos and added build mode

* add actor name to Continuous build

* improve error handling and remove redundant verbose

* fixed workflow file log output

* revert logs output in run_ci.py

* ruff format
This commit is contained in:
jamessiddeley-amd
2025-12-12 17:04:56 -05:00
committed by GitHub
vanhempi 9a8ed9f45d
commit 81720183ad
4 muutettua tiedostoa jossa 54 lisäystä ja 571 poistoa
@@ -1,157 +0,0 @@
name: rocprofiler-compute Code Coverage
run-name: rocprofiler-compute-code-coverage
on:
schedule:
- cron: '0 2 * * *'
workflow_dispatch:
inputs:
runner_matrix:
description: 'Runner configuration'
required: false
type: string
env:
ROCM_PATH: "/opt/rocm"
ROCM_VERSION: "7.0.2"
jobs:
prepare_matrix:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.generate_matrix.outputs.matrix }}
steps:
- name: Generate matrix
id: generate_matrix
run: |
MATRIX='[{"os-release":"22.04","gpu":"mi355","arch":"gfx950","runner":"linux-mi355-1gpu-ossci-rocm","code-name":"jammy"},{"os-release":"24.04","gpu":"mi355","arch":"gfx950","runner":"linux-mi355-1gpu-ossci-rocm","code-name":"noble"},{"os-release":"22.04","gpu":"mi325","arch":"gfx94X","runner":"linux-mi325-1gpu-ossci-rocm","code-name":"jammy"},{"os-release":"24.04","gpu":"mi325","arch":"gfx94X","runner":"linux-mi325-1gpu-ossci-rocm","code-name":"noble"}]'
if [ -n "${{ github.event.inputs.runner_matrix }}" ]; then
MATRIX='${{ github.event.inputs.runner_matrix }}'
fi
echo "matrix=${MATRIX}" >> $GITHUB_OUTPUT
coverage:
name: Coverage • ${{ matrix.system.gpu }} • Ubuntu ${{ matrix.system.os-release }}
needs: prepare_matrix
strategy:
fail-fast: false
matrix:
system: ${{ fromJSON(needs.prepare_matrix.outputs.matrix) }}
runs-on: ${{ matrix.system.runner }}
env:
HIP_PLATFORM: "amd"
OMPI_ALLOW_RUN_AS_ROOT: 1
OMPI_ALLOW_RUN_AS_ROOT_CONFIRM: 1
permissions:
packages: read
container:
image: ghcr.io/rocm/rocprofiler-ubuntu:${{ matrix.system.os-release }}-systems-ci-${{ matrix.system.arch }}
options:
--privileged
--ipc host
--group-add video
--device /dev/kfd
--device /dev/dri
--cap-add CAP_SYS_ADMIN
steps:
- uses: actions/checkout@v5
with:
sparse-checkout: projects/rocprofiler-compute/
- name: Update system packages
run: |
apt-get update
apt-get dist-upgrade -y
echo "✅ System packages updated!"
- name: Install amdgpu and dependencies
shell: bash
run: |
ROCM_MAJOR=$(echo ${{ env.ROCM_VERSION }} | sed 's/\./ /g' | awk '{print $1}')
ROCM_MINOR=$(echo ${{ env.ROCM_VERSION }} | sed 's/\./ /g' | awk '{print $2}')
ROCM_VERSN=$(( (${ROCM_MAJOR}*10000)+(${ROCM_MINOR}*100) ))
wget -N -P /tmp/ https://repo.radeon.com/amdgpu-install/${ROCM_MAJOR}.${ROCM_MINOR}/ubuntu/${{ matrix.system.code-name }}/amdgpu-install_${ROCM_MAJOR}.${ROCM_MINOR}.${ROCM_VERSN}-1_all.deb
apt-get install -y /tmp/amdgpu-install_${ROCM_MAJOR}.${ROCM_MINOR}.${ROCM_VERSN}-1_all.deb
apt-get update
apt install -y amd-smi-lib libdw-dev
echo "✅ amdgpu and dependencies Installed!"
- name: Install MPI
run: |
apt install -y libopenmpi-dev
echo "✅ MPI Installed!"
- name: Install ROCm
run: |
apt install -y rocm-dev
echo "✅ ROCm Installation Complete!"
- name: Install Python Dependencies
working-directory: projects/rocprofiler-compute
run: |
pip install -r requirements.txt --break-system-packages --ignore-installed
pip install -r requirements-test.txt --break-system-packages --ignore-installed
echo "✅ Python dependencies installed!"
- name: Configure, Build, Test, and Upload Coverage
id: run_coverage
timeout-minutes: 120
working-directory: projects/rocprofiler-compute
continue-on-error: true
run: |
set -e
git config --global --add safe.directory ${GITHUB_WORKSPACE}
git config --global --add safe.directory ${PWD}
BUILD_NAME="ROCm/rocprofiler-compute-${{ github.ref_name }}-ubuntu-${{ matrix.system.os-release }}-${{ matrix.system.gpu }}"
SITE_NAME="${{ matrix.system.runner }}"
PATH=${{ env.ROCM_PATH }}/bin:${{ env.ROCM_PATH }}/llvm/bin:$PATH \
LD_LIBRARY_PATH=${{ env.ROCM_PATH }}/lib:${{ env.ROCM_PATH }}/llvm/lib:$LD_LIBRARY_PATH \
python3 ./tools/run-code-coverage.py \
--build-name "${BUILD_NAME}" \
--site "${SITE_NAME}" \
--mode "Nightly" \
--build-jobs 16 \
--pytest-numprocs 8 \
--install
- name: Check Coverage Configuration
if: always()
working-directory: projects/rocprofiler-compute
run: |
echo "=== Checking Build Configuration ==="
if [ -f "build/CMakeCache.txt" ]; then
echo "CMakeCache.txt contents (relevant variables):"
grep -E "ENABLE_TESTS|ENABLE_COVERAGE|INSTALL_TESTS|PYTEST_NUMPROCS" build/CMakeCache.txt || echo "⚠️ Coverage variables not found!"
echo ""
echo "=== Checking for coverage.xml ==="
if [ -f "build/coverage.xml" ]; then
echo "✅ coverage.xml found!"
ls -lh build/coverage.xml
else
echo "❌ coverage.xml NOT found!"
fi
echo ""
echo "=== Checking CTest results ==="
if [ -f "build/Testing/Temporary/LastTest.log" ]; then
echo "Last 50 lines of test log:"
tail -n 50 build/Testing/Temporary/LastTest.log
fi
else
echo "❌ build/CMakeCache.txt not found - build may have failed!"
fi
- name: Upload Logs on Failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: test-logs-${{ matrix.system.gpu }}-${{ matrix.system.os-release }}
path: |
projects/rocprofiler-compute/build/Testing/Temporary/LastTest*.log
projects/rocprofiler-compute/build/coverage.xml
retention-days: 7
@@ -5,6 +5,15 @@ on:
schedule:
- cron: '0 6 * * *'
workflow_dispatch:
inputs:
mode:
description: 'Build mode'
required: false
default: 'continuous'
type: choice
options:
- continuous
- nightly
push:
branches: [ develop ]
paths:
@@ -49,7 +58,7 @@ jobs:
id: generate_matrix
working-directory: projects/rocprofiler-compute/.github
run: |
if [ '${{ github.event_name }}' = 'schedule' ]; then
if [ '${{ github.event_name }}' = 'schedule' ] || [ '${{ inputs.mode }}' = 'nightly' ]; then
MATRIX_CONTENT=$(cat ci-matrix.yml | yq '.matrix-ubuntu-nightly' -I=0 -o=json)
else
MATRIX_CONTENT=$(cat ci-matrix.yml | yq '.matrix-ubuntu-ci' -I=0 -o=json)
@@ -88,17 +97,20 @@ jobs:
- name: Setup Environment
id: setup_env
run: |
if [ '${{ github.event_name }}' = 'schedule' ]; then
if [ '${{ github.event_name }}' = 'schedule' ] || [ '${{ inputs.mode }}' = 'nightly' ]; then
MODE=Nightly
EXCLUDED_TESTS=""
ADD_COVERAGE="--coverage"
else
MODE=Continuous
EXCLUDED_TESTS="test_profile_live_attach_detach"
ADD_COVERAGE=""
fi
echo "mode=${MODE}" >> $GITHUB_OUTPUT
echo "excluded_tests=${EXCLUDED_TESTS}" >> $GITHUB_OUTPUT
echo "add_coverage=${ADD_COVERAGE}" >> $GITHUB_OUTPUT
if [ '${{ matrix.system.os-release }}' = '24.04']; then
if [ '${{ matrix.system.os-release }}' = '24.04' ]; then
CODE_NAME=noble
else
CODE_NAME=jammy
@@ -129,13 +141,21 @@ jobs:
- name: Install Python Requirements
working-directory: projects/rocprofiler-compute
run: |
pip install -r requirements.txt --break-system-packages --ignore-installed
pip install -r requirements-test.txt --break-system-packages --ignore-installed
for i in 1 2 3; do
pip install -r requirements.txt --break-system-packages --ignore-installed --timeout 60 && break
echo "⚠️ pip install attempt $i failed, retrying..."
sleep 10
done
for i in 1 2 3; do
pip install -r requirements-test.txt --break-system-packages --ignore-installed --timeout 60 && break
echo "⚠️ pip install attempt $i failed, retrying..."
sleep 10
done
echo "✅ pip requirements installed!"
- name: Configure, Build, and Test
id: test
timeout-minutes: 90
timeout-minutes: ${{ steps.setup_env.outputs.mode == 'Nightly' && 120 || 90 }}
working-directory: projects/rocprofiler-compute
run: |
set -e
@@ -145,9 +165,11 @@ jobs:
LD_LIBRARY_PATH=${{ env.ROCM_PATH }}/lib:${{ env.ROCM_PATH }}/llvm/lib:${{ env.ROCM_PATH }}/lib/rocm_sysdeps/lib:$LD_LIBRARY_PATH \
python3 ./tools/run-ci.py \
--name "ROCm/rocprofiler-compute-${{ github.ref_name }}-ubuntu-${{ matrix.system.os-release }}-${{ matrix.system.gpu }}" \
--actor "${{ github.actor }}" \
--site ${{ matrix.system.runner }} \
--mode ${{ steps.setup_env.outputs.mode }} \
--build-jobs 16 \
${{ steps.setup_env.outputs.add_coverage }} \
-- \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH=${{ env.ROCM_PATH }} \
@@ -158,8 +180,14 @@ jobs:
-E "${{ steps.setup_env.outputs.excluded_tests }}"
- name: Output Logs
if: failure() && steps.test.outcome == 'failure'
if: failure()
working-directory: projects/rocprofiler-compute
run: |
echo "❌ Run Failed: Now outputting LastTest.log files for detailed logs..."
cat build/Testing/Temporary/LastTest*.log
echo "❌ Run Failed: Outputting available log files..."
for log in build/Testing/Temporary/LastTest*.log build/Testing/Temporary/LastConfigure*.log; do
if [ -f "$log" ]; then
echo "=== $log ==="
cat "$log"
fi
done
@@ -58,7 +58,12 @@ def generate_custom(args, cmake_args, ctest_args):
CMAKE_CMD = which("cmake", require=True)
_ = which("ctest", require=True)
NAME = re.sub(r"(.*)-([0-9]+)/merge", "PR_\\2_\\1", NAME)
# For Continuous builds transform to PR_XXXX format and include the actor
match = re.match(r"(.*)-([0-9]+)/merge(.*)", NAME)
if match and args.actor:
NAME = f"[{args.actor}] PR_{match.group(2)}_{match.group(1)}{match.group(3)}"
elif match:
NAME = f"PR_{match.group(2)}_{match.group(1)}{match.group(3)}"
return f"""
set(CTEST_PROJECT_NAME "rocprofiler-compute")
@@ -138,7 +143,10 @@ def generate_dashboard_script(args):
ctest_submit(PARTS Coverage RETURN_VALUE _submit_ret)
endif()
handle_error("Testing" _test_ret)
# Report test failures but don't fail the build, post results to CDash
if(NOT ${{_test_ret}} EQUAL 0)
message(WARNING "Some tests failed (see CDash for details)")
endif()
ctest_submit(PARTS Done RETURN_VALUE _submit_ret)
"""
@@ -167,6 +175,13 @@ def parse_cdash_args(args):
parser.add_argument(
"-n", "--name", help="Job name", default=None, type=str, required=True
)
parser.add_argument(
"-a",
"--actor",
help="GitHub actor/username (included in Continuous builds)",
default=None,
type=str,
)
parser.add_argument("-s", "--site", help="Site name", default=SITE, type=str)
parser.add_argument(
"-c", "--coverage", help="Enable code coverage", action="store_true"
@@ -1,403 +0,0 @@
#!/usr/bin/env python3
##############################################################################
# MIT License
#
# Copyright (c) 2025 Advanced Micro Devices, Inc. All Rights Reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
##############################################################################
import argparse
import os
import subprocess
import sys
import tempfile
from pathlib import Path
def detect_repo_structure():
"""Detect if in monorepo or standalone structure"""
cwd = Path.cwd()
if "projects/rocprofiler-compute" in str(cwd):
parts = cwd.parts
idx = parts.index("rocprofiler-compute")
if idx > 0 and parts[idx - 1] == "projects":
monorepo_root = Path(*parts[: idx - 1])
project_root = monorepo_root / "projects" / "rocprofiler-compute"
return True, monorepo_root, project_root
if (cwd / "CMakeLists.txt").exists():
with open(cwd / "CMakeLists.txt") as f:
content = f.read()
if (
"project(rocprofiler-compute" in content
or "project(\n rocprofiler-compute" in content
):
return False, None, cwd
return False, None, cwd
def generate_ctest_dashboard_script(args, source_dir, binary_dir):
"""
Generate a complete CTest dashboard script that handles:
- Configuration
- Build
- Test
- Coverage
- Submit to CDash
"""
cache_entries = [
"CMAKE_BUILD_TYPE:STRING=Release",
f"CMAKE_PREFIX_PATH:PATH={os.environ.get('ROCM_PATH', '/opt/rocm')}",
"ENABLE_TESTS:BOOL=ON",
"INSTALL_TESTS:BOOL=ON",
"ENABLE_COVERAGE:BOOL=ON",
f"PYTEST_NUMPROCS:STRING={args.pytest_numprocs}",
]
test_args = ""
if args.ctest_args:
test_args = " ".join(args.ctest_args)
# ruff: noqa
script_content = f"""
cmake_minimum_required(VERSION 3.19)
##############################################################################
# CTest Dashboard Script for rocprofiler-compute
# Auto-generated by run-ci.py
##############################################################################
# Dashboard configuration
set(CTEST_PROJECT_NAME "{args.project_name}")
set(CTEST_NIGHTLY_START_TIME "01:00:00 UTC")
set(CTEST_DROP_SITE_CDASH TRUE)
# CDash submission settings
if(CMAKE_VERSION VERSION_GREATER 3.14)
set(CTEST_SUBMIT_URL "{args.submit_url}")
else()
set(CTEST_DROP_METHOD "https")
set(CTEST_DROP_SITE "{args.cdash_host}")
set(CTEST_DROP_LOCATION "{args.submit_path}")
endif()
# Build identification
set(CTEST_SITE "{args.site}")
set(CTEST_BUILD_NAME "{args.build_name}")
set(CTEST_SOURCE_DIRECTORY "{source_dir}")
set(CTEST_BINARY_DIRECTORY "{binary_dir}")
# Build config
set(CTEST_CMAKE_GENERATOR "Unix Makefiles")
set(CTEST_BUILD_CONFIGURATION "Release")
# Config CMake command with all required options
set(CTEST_CONFIGURE_COMMAND "cmake -B ${{CTEST_BINARY_DIRECTORY}} ${{CTEST_SOURCE_DIRECTORY}} -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH={os.environ.get("ROCM_PATH", "/opt/rocm")} -DENABLE_TESTS=ON -DINSTALL_TESTS=ON -DENABLE_COVERAGE=ON -DPYTEST_NUMPROCS={args.pytest_numprocs}")
message(STATUS "CMake configure command: ${{CTEST_CONFIGURE_COMMAND}}")
message(STATUS "Starting {args.mode} dashboard...")
ctest_start({args.mode})
# Config step
message(STATUS "Configuring project...")
ctest_configure(
RETURN_VALUE configure_result
CAPTURE_CMAKE_ERROR configure_error
)
if(configure_result OR configure_error)
message(WARNING "Configuration had issues but continuing...")
endif()
# Build step
message(STATUS "Building project...")
set(CTEST_BUILD_FLAGS "-j{args.build_jobs}")
ctest_build(
TARGET {"install" if args.install else "all"}
RETURN_VALUE build_result
CAPTURE_CMAKE_ERROR build_error
)
if(build_result OR build_error)
message(WARNING "Build had issues but continuing...")
endif()
# Test step
message(STATUS "Running tests...")
ctest_test(
{test_args}
PARALLEL_LEVEL 1
RETURN_VALUE test_result
CAPTURE_CMAKE_ERROR test_error
)
if(test_result OR test_error)
message(WARNING "Some tests failed but continuing with coverage and upload...")
endif()
# Coverage step (if coverage file exists)
set(COVERAGE_FILE "${{CTEST_BINARY_DIRECTORY}}/coverage.xml")
if(EXISTS "${{COVERAGE_FILE}}")
message(STATUS "Processing coverage data...")
file(MAKE_DIRECTORY "${{CTEST_BINARY_DIRECTORY}}/Testing/CoverageInfo")
file(COPY "${{COVERAGE_FILE}}"
DESTINATION "${{CTEST_BINARY_DIRECTORY}}/Testing/CoverageInfo/")
ctest_coverage(
RETURN_VALUE coverage_result
CAPTURE_CMAKE_ERROR coverage_error
)
if(coverage_result OR coverage_error)
message(WARNING "Coverage processing had issues but continuing...")
endif()
else()
message(STATUS "No coverage file found at ${{COVERAGE_FILE}}")
message(STATUS "Skipping coverage step")
endif()
message(STATUS "Submitting to CDash...")
ctest_submit(
RETRY_COUNT 3
RETRY_DELAY 5
RETURN_VALUE submit_result
CAPTURE_CMAKE_ERROR submit_error
)
if(submit_result OR submit_error)
message(WARNING "CDash submission encountered issues")
message(STATUS "Results available locally in: ${{CTEST_BINARY_DIRECTORY}}/Testing/")
else()
message(STATUS "Successfully submitted to CDash!")
message(STATUS "View at: {args.submit_url.replace("/submit.php?project=", "/index.php?project=")}")
message(STATUS "Build name: {args.build_name}")
endif()
message(STATUS "")
message(STATUS "========================================")
message(STATUS "Dashboard Summary")
message(STATUS "========================================")
if(test_result)
message(STATUS "Tests: FAILED (some tests failed or timed out)")
else()
message(STATUS "Tests: PASSED")
endif()
if(submit_result OR submit_error)
message(STATUS "CDash Upload: FAILED")
else()
message(STATUS "CDash Upload: SUCCESS")
endif()
message(STATUS "========================================")
# For nightly/continuous, don't fail on test failures
# Only warn in Experimental mode
if("{args.mode}" STREQUAL "Experimental" AND test_result)
message(WARNING "Tests failed in Experimental mode")
endif()
"""
return script_content
def main():
parser = argparse.ArgumentParser(
description="CI script for rocprofiler-compute: build, test, and upload to CDash"
)
parser.add_argument(
"--build-name",
"--name",
dest="build_name",
required=True,
help="Build name for CDash",
)
parser.add_argument(
"--project-name",
default="rocprofiler-compute",
help="CDash project name",
)
parser.add_argument(
"--submit-url",
default="https://my.cdash.org/submit.php?project=rocprofiler-compute",
help="CDash submission URL",
)
parser.add_argument(
"--cdash-host",
default="my.cdash.org",
help="CDash host (for older CMake)",
)
parser.add_argument(
"--submit-path",
default="/submit.php?project=rocprofiler-compute",
help="CDash submission path (for older CMake)",
)
parser.add_argument(
"--site",
default=None,
help="Site name for CDash (auto-detected if not provided)",
)
parser.add_argument(
"--source-dir",
"-S",
default=None,
help="Source directory path (auto-detected if not provided)",
)
parser.add_argument(
"--binary-dir",
"-B",
default=None,
help="Binary directory path (defaults to source-dir/build)",
)
parser.add_argument(
"--mode",
default="Experimental",
choices=["Experimental", "Nightly", "Continuous"],
help="CTest dashboard mode",
)
parser.add_argument(
"--build-jobs",
"-j",
type=int,
default=8,
help="Number of parallel build jobs",
)
parser.add_argument(
"--install",
action="store_true",
help="Build install target instead of all target",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Generate dashboard script but don't execute",
)
parser.add_argument(
"--pytest-numprocs",
type=int,
default=4,
help="Number of parallel processes for pytest",
)
args, unknown = parser.parse_known_args()
args.cmake_args = []
args.ctest_args = []
is_monorepo, monorepo_root, project_root = detect_repo_structure()
if not args.source_dir:
args.source_dir = str(project_root)
if not args.binary_dir:
args.binary_dir = os.path.join(args.source_dir, "build")
if not args.site:
if is_monorepo:
args.site = f"Monorepo-{os.uname().nodename}"
else:
args.site = os.uname().nodename
source_dir = Path(args.source_dir).absolute()
binary_dir = Path(args.binary_dir).absolute()
print("=" * 80)
print("rocprofiler-compute CI Dashboard")
print("=" * 80)
print(f"Repository type: {'Monorepo' if is_monorepo else 'Standalone'}")
print(f"Source directory: {source_dir}")
print(f"Binary directory: {binary_dir}")
print(f"Build name: {args.build_name}")
print(f"Dashboard mode: {args.mode}")
print(f"CDash project: {args.project_name}")
print(f"Site: {args.site}")
print("=" * 80)
script_content = generate_ctest_dashboard_script(args, source_dir, binary_dir)
if args.dry_run:
print("\nGenerated CTest Dashboard Script:")
print("=" * 80)
print(script_content)
print("=" * 80)
return 0
binary_dir.mkdir(parents=True, exist_ok=True)
with tempfile.NamedTemporaryFile(
mode="w", suffix=".cmake", delete=False, dir=binary_dir
) as f:
f.write(script_content)
script_path = f.name
print(f"\nDashboard script: {script_path}")
print("=" * 80)
try:
cmd = ["ctest", "-S", script_path, "-V"]
print(f"Running: {' '.join(cmd)}")
print("=" * 80)
print()
process = subprocess.Popen(
cmd,
cwd=source_dir,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
universal_newlines=True,
)
for line in process.stdout:
print(line, end="")
returncode = process.wait()
print()
print("=" * 80)
if returncode == 0:
print("✅ Dashboard completed successfully!")
else:
print(f"⚠️ Dashboard completed with return code: {returncode}")
print("=" * 80)
return returncode
except Exception as e:
print(f"\n❌ Error executing dashboard script: {e}", file=sys.stderr)
import traceback
traceback.print_exc()
return 1
finally:
try:
os.unlink(script_path)
except Exception:
pass
if __name__ == "__main__":
sys.exit(main())