diff --git a/mpsd-software-environment.py b/mpsd-software-environment.py index 7bace592891f3395dedbbcdbc998f1c1ade38db5..50eefa1bf5990c4927f85b9f7b8b8eb04a2bfcc0 100755 --- a/mpsd-software-environment.py +++ b/mpsd-software-environment.py @@ -4,6 +4,7 @@ import argparse import datetime +import logging import os import subprocess import sys @@ -11,6 +12,7 @@ import time from pathlib import Path from typing import List, Tuple + about_tool = """ Build toolchains using Spack.\n @@ -30,6 +32,47 @@ config_vars = { } +def set_up_logging(loglevel="warning", filename=None): + """Set up logging. + + This function sets up the logging configuration for the script. + It configures the log level, log format, and log handlers + for both file and console output. + + + Parameters + ---------- + loglevel : str or int + Loglevels are: + - warning (default): only print statements if something is unexpected + - info (show more detailed progress) + - debug (show very detailed output) + filename : str + - filename to save logging messages into + + If loglevel is 'debug', save line numbers in log messages. + """ + log_level_numeric = getattr(logging, loglevel.upper(), logging.WARNING) + assert log_level_numeric + if not isinstance(log_level_numeric, int): + raise ValueError("Invalid log level: %s" % loglevel) + + handlers = [] + if filename: + handlers.append(logging.FileHandler(filename)) + + handlers.append(logging.StreamHandler()) + linenumbers = " %(lineno)4d" if log_level_numeric == logging.DEBUG else "" + logging.basicConfig( + format="%(asctime)s %(levelname)7s" + linenumbers + " | %(message)s", + datefmt="[%X]", + level=log_level_numeric, + handlers=handlers, + force=True, + ) + logging.debug(f"Logging has been setup, loglevel={loglevel.upper()} {filename=}") + + # Helper class to change directory via context manager class os_chdir: """The os_chdir class is a context manager. @@ -123,16 +166,16 @@ def run(*args, counter=[0], **kwargs): options = ", ".join([f"{key}={value}" for key, value in kwargs.items()]) # provide information about upcoming subprocess.run call - print(f"{token} Starting subprocess.run({arg}) with options {options}") - print(f"{token} getcwd={os.getcwd()}") - print(f"{token} COMMAND={command}") + logging.info(f"{token} Starting subprocess.run('{command}') with options {options}") + logging.debug(f"{token} getcwd={os.getcwd()}") + logging.debug(f"{token} exact call: subprocess.run({arg})") time_start = time.time() process = subprocess.run(*args, **kwargs) execution_time = time.time() - time_start - print(f"{token} Completed in {execution_time:.4f}s.") - print(f"{token}") # near-empty line to make reading logs easier + logging.debug(f"{token} Completed in {execution_time:.4f}s.") + logging.debug(f"{token}") # near-empty line to make reading logs easier return process @@ -395,7 +438,7 @@ def install_environment( ------- None """ - print( + logging.info( f"Installing release {mpsd_release} with toolchains {toolchains} " f"to {script_dir}" ) @@ -419,7 +462,7 @@ def install_environment( elif toolchains == "NONE": # No toolchains requested, so we only create the env and print the # list of available toolchains - print( + logging.warning( "No toolchains requested. Available toolchains for release " f"{mpsd_release} are: \n {available_toolchains}" ) @@ -443,7 +486,7 @@ def install_environment( # and replace _toolchains_ with the toolchain name and # _mpsd_spack_ver_ with mpsd_release - print(f"Installing toolchain {toolchain} to {toolchain_dir}") + logging.info(f"Installing toolchain {toolchain} to {toolchain_dir}") install_log_file = ( config_vars["build_log_file"] .replace("mpsd_spack_ver_", f"{mpsd_release}_") @@ -474,20 +517,30 @@ def install_environment( def remove_environment(release, toolchains, target_dir): """Remove release from installation.""" msg = f"Removing release {release} with toolchains {toolchains} from {target_dir}" - print(msg) + logging.info(msg) raise NotImplementedError(msg) def start_new_environment(release, from_release, target_dir): """Start new MPSD software environment version.""" msg = f"Starting new release {release} from {from_release} to {target_dir}" - print(msg) + logging.info(msg) raise NotImplementedError(msg) def main(): """Execute main entry point.""" parser = argparse.ArgumentParser(description=about_tool) + parser.add_argument( + "--log", + "-l", + dest="loglevel", + choices=["warning", "info", "debug"], + required=False, + default="warning", + help="Set the log level", + ) + subparsers = parser.add_subparsers( dest="action", title="actions", description="valid actions", required=True ) @@ -556,6 +609,9 @@ def main(): # Carry out the action args = parser.parse_args() + # parse logging first + set_up_logging(args.loglevel) + # target dir is the place where this script exists. the # release `dev` in script_dir/dev-23a script_dir = Path(os.path.dirname(os.path.realpath(__file__))) diff --git a/tests.py b/tests.py index 43d08a7be8cc80320ff7006f4718eda7eb4d8268..01bd4daeecfb3d9bf582ee53bbe1e477f6d5194f 100644 --- a/tests.py +++ b/tests.py @@ -1,6 +1,7 @@ """Tests for mpsd-software-environment.py.""" import importlib +import logging import os import shutil import subprocess @@ -10,6 +11,11 @@ import pytest mod = importlib.import_module("mpsd-software-environment") +# set loglevel to debug - useful for understanding problems. +# (if the tests pass, pytest doesn't show any output) +mod.set_up_logging(loglevel="debug", filename="tests.log") +logging.debug(f"We have set up logging from {__file__}") + def create_mock_git_repository(target_directory, create_directory=True): """