diff --git a/src/molecule/command/base.py b/src/molecule/command/base.py index f4ddad1b0e..4c3b164e55 100644 --- a/src/molecule/command/base.py +++ b/src/molecule/command/base.py @@ -31,6 +31,7 @@ import molecule.scenarios from molecule import config, logger, util +from molecule.console import get_sectioner LOG = logger.get_logger(__name__) MOLECULE_GLOB = os.environ.get("MOLECULE_GLOB", "molecule/*/molecule.yml") @@ -39,12 +40,24 @@ def section_logger(func: Callable) -> Callable: """Wrap effective execution of a method.""" + sectioner = get_sectioner() def wrapper(*args, **kwargs): + scenario = args[1].scenario.name + subcommand = args[2] + section_id = f"{scenario}.{subcommand}" + + sectioner.print_header( + section_id, + title=f"Molecule {scenario} > {subcommand}", + title_with_markers=f"[section_title]Molecule[/] [scenario]{scenario}[/] > [action]{subcommand}[/]", + ) args[0].print_info() - rt = func(*args, **kwargs) - # section close code goes here - return rt + try: + return func(*args, **kwargs) + finally: + # uses finally to ensure this prints even on error. + sectioner.print_footer(section_id) return wrapper diff --git a/src/molecule/console.py b/src/molecule/console.py index 5d0ae57b10..b586c15c13 100644 --- a/src/molecule/console.py +++ b/src/molecule/console.py @@ -1,6 +1,8 @@ """Console and terminal utilities.""" import os import sys +import time +from abc import ABC, abstractmethod from typing import Any from rich.style import Style @@ -14,6 +16,7 @@ "danger": "bold red", "scenario": "green", "action": "green", + "section_title": "bold cyan", "logging.level.notset": Style(dim=True), "logging.level.debug": Style(color="white", dim=True), "logging.level.info": Style(color="blue"), @@ -63,3 +66,148 @@ def should_do_markup() -> bool: console = ConsoleEx( force_terminal=should_do_markup(), theme=theme, record=True, redirect=True ) + + +class OutputSectioner(ABC): + @classmethod + @abstractmethod + def can_handle(cls): + """"Investigate environment to see if section markers are supported.""" + + @abstractmethod + def print_header( + self, section_id: str, title: str = None, title_with_markers: str = None + ): + """"Print the pre-section header, optionally with a (marked up) title.""" + + @abstractmethod + def print_footer(self, section_id: str): + """"Print the post-section footer.""" + + +class NullSectioner(OutputSectioner): + @classmethod + def can_handle(cls): + return True + + def print_header( + self, section_id: str, title: str = None, title_with_markers: str = None + ): + pass + + def print_footer(self, section_id: str): + pass + + +class GithubActionsGroups(OutputSectioner): + @classmethod + def can_handle(cls): + return os.getenv("CI") and os.getenv("GITHUB_ACTIONS") + + def print_header( + self, section_id: str, title: str = None, title_with_markers: str = None + ): + if title is None and title_with_markers is None: + section_title = "" + else: + section_title = title_with_markers or f"[section_title]{title}[/]" + console.print( + "::group::", + section_title, + sep="", + markup=True, + emoji=False, + highlight=False, + ) + + def print_footer(self, section_id: str): + console.print("::endgroup::", markup=True, emoji=False, highlight=False) + + +class GitlabPipelinesSections(OutputSectioner): + # GitLab requres: + # - \r (carriage return) + # - \e[0K (clear line ANSI escape code. We use \033 for the \e escape char) + clear_line = "\r\033[0K" + + @classmethod + def can_handle(cls): + return os.getenv("CI") and os.getenv("GITLAB_CI") + + def print_header( + self, section_id: str, title: str = None, title_with_markers: str = None + ): + # must be one color for the whole line or gitlab sets odd widths to each word. + # so, ignore title_with_markers + section_title = f"[section_title]{title}[/]" + console.print( + f"section_start:{int(time.time())}:{section_id}", + end=self.clear_line, + markup=False, + emoji=False, + highlight=False, + ) + console.print( + section_title, + end="\n", + markup=True, + emoji=False, + highlight=False, + ) + + def print_footer(self, section_id: str): + console.print( + f"section_end:{int(time.time())}:{section_id}", + end=f"{self.clear_line}\n", + markup=False, + emoji=False, + highlight=False, + ) + + +class TravisCIsections(OutputSectioner): + @classmethod + def can_handle(): + return os.getenv("CI") and os.getenv("TRAVIS") + + def print_header( + section_id: str, title: str = None, title_with_markers: str = None + ): + if title is None and title_with_markers is None: + section_title = "" + else: + section_title = title_with_markers or f"[section_title]{title}[/]" + console.print( + f"travis_section:start:{section_id}", + section_title, + sep="", + markup=True, + emoji=False, + highlight=False, + ) + + def print_footer(section_id: str): + console.print( + f"travis_section:end:{section_id}", + markup=False, + emoji=False, + highlight=False, + ) + + +sectioners = [ + GithubActionsGroups, + GitlabPipelinesSections, + TravisCIsections, + NullSectioner, # must be last. +] +_sectioner = None + + +def get_sectioner(): + if _sectioner is not None: + return _sectioner + for sectioner in secitoners: + if sectioner.can_handle(): + _sectioner = sectioner() + break