From ec4d04bb9466f16b8d4358539898a490caa252e6 Mon Sep 17 00:00:00 2001 From: Scott Poore Date: Mon, 18 Nov 2024 13:15:22 -0600 Subject: [PATCH 1/5] Add GUI call functionality to scauto CLI Add CLI GUI commands to make direct calls to the GUI model library functions necessary for some manual and remotely executed test steps. As a part of the change to run GUI functions from the CLI, we also need to move the initialization of the screen object in __init__ instead of __enter__. resolves #146 --- SCAutolib/cli_commands.py | 201 +++++++++++++++++++++++++++++++------- SCAutolib/models/gui.py | 4 +- 2 files changed, 169 insertions(+), 36 deletions(-) diff --git a/SCAutolib/cli_commands.py b/SCAutolib/cli_commands.py index 2c66a40..8ce3a34 100644 --- a/SCAutolib/cli_commands.py +++ b/SCAutolib/cli_commands.py @@ -6,15 +6,50 @@ from pathlib import Path from sys import exit +from collections import OrderedDict + from SCAutolib import logger, exceptions, schema_user from SCAutolib.controller import Controller from SCAutolib.enums import ReturnCode -@click.group() +def check_conf_path(conf): + return click.Path(exists=True, resolve_path=True)(conf) + + +class NaturalOrderGroup(click.Group): + """ + Command group trying to list subcommands in the order they were added. + Example use:: + + @click.group(cls=NaturalOrderGroup) + + If passing dict of commands from other sources, ensure they are of type + OrderedDict and properly ordered, otherwise order of them will be random + and newly added will come to the end. + """ + def __init__(self, name=None, commands=None, **attrs): + if commands is None: + commands = OrderedDict() + elif not isinstance(commands, OrderedDict): + commands = OrderedDict(commands) + click.Group.__init__(self, name=name, + commands=commands, + **attrs) + + def list_commands(self, ctx): + """ + List command names as they are in commands dict. + + If the dict is OrderedDict, it will preserve the order commands + were added. + """ + return self.commands.keys() + + +@click.group(cls=NaturalOrderGroup) @click.option("--conf", "-c", default="./conf.json", - type=click.Path(exists=True, resolve_path=True), show_default=True, help="Path to JSON configuration file.") @click.option('--force', "-f", is_flag=True, default=False, show_default=True, @@ -29,35 +64,11 @@ def cli(ctx, force, verbose, conf): logger.setLevel(verbose) ctx.ensure_object(dict) # Create a dict to store the context ctx.obj["FORCE"] = force # Store the force option in the context - ctx.obj["CONTROLLER"] = Controller(conf) + if ctx.invoked_subcommand and not gui: + ctx.obj["CONTROLLER"] = Controller(check_conf_path(conf)) -@click.command() -@click.option("--ca-type", "-t", - required=False, - default='all', - type=click.Choice(['all', 'local', 'ipa'], case_sensitive=False), - show_default=True, - help="Type of the CA to be configured. If not set, all CA's " - "from the config file would be configured") -@click.pass_context -def setup_ca(ctx, ca_type): - """ - Configure the CA's in the config file. If more than one CA is - specified, specified CA type would be configured. - """ - cnt = ctx.obj["CONTROLLER"] - if ca_type == 'all': - cnt.setup_local_ca(force=ctx.obj["FORCE"]) - cnt.setup_ipa_client(force=ctx.obj["FORCE"]) - elif ca_type == 'local': - cnt.setup_local_ca(force=ctx.obj["FORCE"]) - elif ca_type == 'ipa': - cnt.setup_ipa_client(force=ctx.obj["FORCE"]) - exit(ReturnCode.SUCCESS.value) - - -@click.command() +@cli.command() @click.option("--gdm", "-g", required=False, default=False, @@ -85,7 +96,32 @@ def prepare(ctx, gdm, install_missing, graphical): exit(ReturnCode.SUCCESS.value) -@click.command() +@cli.command() +@click.option("--ca-type", "-t", + required=False, + default='all', + type=click.Choice(['all', 'local', 'ipa'], case_sensitive=False), + show_default=True, + help="Type of the CA to be configured. If not set, all CA's " + "from the config file would be configured") +@click.pass_context +def setup_ca(ctx, ca_type): + """ + Configure the CA's in the config file. If more than one CA is + specified, specified CA type would be configured. + """ + cnt = ctx.obj["CONTROLLER"] + if ca_type == 'all': + cnt.setup_local_ca(force=ctx.obj["FORCE"]) + cnt.setup_ipa_client(force=ctx.obj["FORCE"]) + elif ca_type == 'local': + cnt.setup_local_ca(force=ctx.obj["FORCE"]) + elif ca_type == 'ipa': + cnt.setup_ipa_client(force=ctx.obj["FORCE"]) + exit(ReturnCode.SUCCESS.value) + + +@cli.command() @click.argument("name", required=True, default=None) @@ -154,7 +190,7 @@ def setup_user(ctx, name, card_dir, card_type, passwd, pin, user_type): exit(ReturnCode.SUCCESS.value) -@click.command() +@cli.command() @click.pass_context def cleanup(ctx): """ @@ -165,7 +201,102 @@ def cleanup(ctx): exit(ReturnCode.SUCCESS.value) -cli.add_command(setup_ca) -cli.add_command(prepare) -cli.add_command(setup_user) -cli.add_command(cleanup) +@cli.group(cls=NaturalOrderGroup, chain=True) +@click.pass_context +def gui(ctx): + """ Run GUI Test commands """ + pass + + +@gui.command() +def init(): + """ Initialize GUI for testing """ + return "init" + + +@gui.command() +@click.argument("name") +def assert_text(name): + """ Check if a word is found on the screen """ + return f"assert_text:{name}" + + +@gui.command() +@click.argument("name") +def assert_no_text(name): + """ Check that a word is not found on the screen """ + return f"assert_no_text:{name}" + + +@gui.command() +@click.argument("name") +def click_on(name): + """ Click on object containing word """ + return f"click_on:{name}" + + +@gui.command() +def check_home_screen(): + """ Check if screen appears to be the home screen """ + return f"check_home_screen" + + +@gui.command() +@click.argument("keys") +def kb_send(keys): + """ Send key(s) to keyboard """ + return f"kb_send:{keys}" + + +@gui.command() +@click.argument("keys") +def kb_write(keys): + """ Send string to keyboard """ + return f"kb_write:{keys}" + + +@gui.command() +def done(): + """ cleanup after testing """ + return "done" + + +@gui.result_callback() +@click.pass_context +def run_all(ctx, actions): + click.echo(actions) + + from SCAutolib.models.gui import GUI + gui = GUI() + for action in actions: + if "init" in action: + gui.__enter__() + if "assert_text" in action: + assert_text = action.split(":")[1] + click.echo(f"CLICK: assert_text:{assert_text}") + gui.assert_text(assert_text) + if "assert_no_text" in action: + assert_text = action.split(":")[1] + click.echo(f"CLICK: assert_text:{assert_text}") + gui.assert_text(assert_text) + if "click_on" in action: + click_on = action.split(":")[1] + click.echo(f"CLICK: click_on:{click_on}") + gui.click_on(click_on) + if "check_home_screen" in action: + click.echo("CLICK: check_home_screen") + gui.check_home_screen() + if "kb_send" in action: + params = action.split(":")[1].split() + click.echo(f"CLICK: kb_send:{params}") + gui.kb_send(*params) + if "kb_write" in action: + params = action.split(":")[1].split() + click.echo(f"CLICK: kb_write:{params}") + gui.kb_write(*params) + if "done" in action: + gui.__exit__(None, None, None) + + +if __name__ == "__main__": + cli() diff --git a/SCAutolib/models/gui.py b/SCAutolib/models/gui.py index 93ae660..295acc9 100644 --- a/SCAutolib/models/gui.py +++ b/SCAutolib/models/gui.py @@ -290,8 +290,10 @@ def __init__(self, wait_time: float = 5, res_dir_name: str = None): # otherwise the first character is not sent keyboard.send('enter') - def __enter__(self): + # create screen object to use from calls self.screen = Screen(self.screenshot_directory, self.html_file) + + def __enter__(self): # By restarting gdm, the system gets into defined state run(['systemctl', 'restart', 'gdm'], check=True) # Cannot screenshot before gdm starts displaying From d998bf317b9010e5daf2261c9abc434485e4aa4f Mon Sep 17 00:00:00 2001 From: Scott Poore Date: Wed, 27 Nov 2024 16:36:55 -0600 Subject: [PATCH 2/5] GUI: update kb_write to handle upper case chars kb_write sent the text directly two keyboard.write() which doesn't handle uppercase characters. A workaround is to use keyboard.sent() to send shift+ for the uppercase characters and rely on keyboard.write() to send the rest. --- SCAutolib/models/gui.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/SCAutolib/models/gui.py b/SCAutolib/models/gui.py index 295acc9..e8a2392 100644 --- a/SCAutolib/models/gui.py +++ b/SCAutolib/models/gui.py @@ -379,7 +379,18 @@ def click_on(self, key: str, timeout: float = 30): def kb_write(self, *args, **kwargs): # delay is a workaround needed for keyboard library kwargs.setdefault('delay', 0.1) - keyboard.write(*args, **kwargs) + + word = args[0] + last = "" + for char in word: + if char.isupper(): + if last != "": + keyboard.write(*[last], **kwargs) + last = "" + keyboard.send(f"shift+{char.lower()}") + else: + last = f"{last}{char}" + keyboard.write(*[last], **kwargs) @action_decorator @log_decorator From a5f47a2a50f3fa6f9513c3c3888525f40480f59b Mon Sep 17 00:00:00 2001 From: Scott Poore Date: Wed, 4 Dec 2024 15:44:32 -0600 Subject: [PATCH 3/5] Controller updates from gpantelakis Updates patch that fix Controller and cli to better handle setup. Follow up changes per PR review also added to cli_commands.py. Add note about where NaturalOrderGroup in cli_commands came from. --- SCAutolib/cli_commands.py | 78 +++++++++++++++++++++++---------------- SCAutolib/controller.py | 77 ++++++++++++++++++++++---------------- SCAutolib/models/file.py | 6 +++ 3 files changed, 99 insertions(+), 62 deletions(-) diff --git a/SCAutolib/cli_commands.py b/SCAutolib/cli_commands.py index 8ce3a34..a736fa7 100644 --- a/SCAutolib/cli_commands.py +++ b/SCAutolib/cli_commands.py @@ -17,6 +17,9 @@ def check_conf_path(conf): return click.Path(exists=True, resolve_path=True)(conf) +# In Help output, force the subcommand list to match the order +# listed in this file. Solution was found here: +# https://github.com/pallets/click/issues/513#issuecomment-301046782 class NaturalOrderGroup(click.Group): """ Command group trying to list subcommands in the order they were added. @@ -34,8 +37,8 @@ def __init__(self, name=None, commands=None, **attrs): elif not isinstance(commands, OrderedDict): commands = OrderedDict(commands) click.Group.__init__(self, name=name, - commands=commands, - **attrs) + commands=commands, + **attrs) def list_commands(self, ctx): """ @@ -64,8 +67,10 @@ def cli(ctx, force, verbose, conf): logger.setLevel(verbose) ctx.ensure_object(dict) # Create a dict to store the context ctx.obj["FORCE"] = force # Store the force option in the context - if ctx.invoked_subcommand and not gui: - ctx.obj["CONTROLLER"] = Controller(check_conf_path(conf)) + parsed_conf = None + if ctx.invoked_subcommand != "gui": + parsed_conf = check_conf_path(conf) + ctx.obj["CONTROLLER"] = Controller(parsed_conf) @cli.command() @@ -202,8 +207,13 @@ def cleanup(ctx): @cli.group(cls=NaturalOrderGroup, chain=True) +@click.option("--install-missing", "-i", + required=False, + default=False, + is_flag=True, + help="Install missing packages") @click.pass_context -def gui(ctx): +def gui(ctx, install_missing): """ Run GUI Test commands """ pass @@ -215,19 +225,19 @@ def init(): @gui.command() +@click.option("--no", + required=False, + default=False, + is_flag=True, + help="Reverse the action") @click.argument("name") -def assert_text(name): +def assert_text(name, no): """ Check if a word is found on the screen """ + if no: + return f"assert_no_text:{name}" return f"assert_text:{name}" -@gui.command() -@click.argument("name") -def assert_no_text(name): - """ Check that a word is not found on the screen """ - return f"assert_no_text:{name}" - - @gui.command() @click.argument("name") def click_on(name): @@ -236,9 +246,16 @@ def click_on(name): @gui.command() -def check_home_screen(): +@click.option("--no", + required=False, + default=False, + is_flag=True, + help="Reverse the action") +def check_home_screen(no): """ Check if screen appears to be the home screen """ - return f"check_home_screen" + if no: + return "check_no_home_screen" + return "check_home_screen" @gui.command() @@ -263,8 +280,9 @@ def done(): @gui.result_callback() @click.pass_context -def run_all(ctx, actions): - click.echo(actions) +def run_all(ctx, actions, install_missing): + """ Run all cli actions in order """ + ctx.obj["CONTROLLER"].setup_graphical(install_missing, True) from SCAutolib.models.gui import GUI gui = GUI() @@ -272,30 +290,28 @@ def run_all(ctx, actions): if "init" in action: gui.__enter__() if "assert_text" in action: - assert_text = action.split(":")[1] - click.echo(f"CLICK: assert_text:{assert_text}") + assert_text = action.split(":", 1)[1] gui.assert_text(assert_text) if "assert_no_text" in action: - assert_text = action.split(":")[1] - click.echo(f"CLICK: assert_text:{assert_text}") - gui.assert_text(assert_text) + assert_text = action.split(":", 1)[1] + gui.assert_no_text(assert_text) if "click_on" in action: - click_on = action.split(":")[1] - click.echo(f"CLICK: click_on:{click_on}") + click_on = action.split(":", 1)[1] gui.click_on(click_on) if "check_home_screen" in action: - click.echo("CLICK: check_home_screen") gui.check_home_screen() + if "check_no_home_screen" in action: + gui.check_home_screen(False) if "kb_send" in action: - params = action.split(":")[1].split() - click.echo(f"CLICK: kb_send:{params}") - gui.kb_send(*params) + params = action.split(":", 1)[1].split()[0] + gui.kb_send(params) if "kb_write" in action: - params = action.split(":")[1].split() - click.echo(f"CLICK: kb_write:{params}") - gui.kb_write(*params) + params = action.split(":", 1)[1].split()[0] + gui.kb_write(params) + gui.kb_send('enter') if "done" in action: gui.__exit__(None, None, None) + ctx.obj["CONTROLLER"].cleanup() if __name__ == "__main__": diff --git a/SCAutolib/controller.py b/SCAutolib/controller.py index 52266e6..9b8dece 100644 --- a/SCAutolib/controller.py +++ b/SCAutolib/controller.py @@ -34,7 +34,7 @@ class Controller: def conf_path(self): return self._lib_conf_path - def __init__(self, config: Union[Path, str], params: {} = None): + def __init__(self, config: Union[Path, str] = None, params: {} = None): """ Constructor will parse and check input configuration file. If some required fields in the configuration are missing, CLI parameters @@ -55,15 +55,18 @@ def __init__(self, config: Union[Path, str], params: {} = None): # Check params # Parse config file - self._lib_conf_path = config.absolute() if isinstance(config, Path) \ - else Path(config).absolute() - - with self._lib_conf_path.open("r") as f: - tmp_conf = json.load(f) - if tmp_conf is None: - raise exceptions.SCAutolibException( - "Data are not loaded correctly.") - self.lib_conf = self._validate_configuration(tmp_conf, params) + self.lib_conf = None + if config: + self._lib_conf_path = config.absolute() if isinstance(config, Path) \ + else Path(config).absolute() + + with self._lib_conf_path.open("r") as f: + tmp_conf = json.load(f) + if tmp_conf is None: + raise exceptions.SCAutolibException( + "Data are not loaded correctly.") + self.lib_conf = self._validate_configuration(tmp_conf, params) + self.users = [] for d in (LIB_DIR, LIB_BACKUP, LIB_DUMP, LIB_DUMP_USERS, LIB_DUMP_CAS, LIB_DUMP_CARDS, LIB_DUMP_CONFS): @@ -147,12 +150,6 @@ def setup_system(self, install_missing: bool, gdm: bool, graphical: bool): packages = ["opensc", "httpd", "sssd", "sssd-tools", "gnutls-utils", "openssl", "nss-tools"] - if gdm: - packages.append("gdm") - - if graphical: - # ffmpeg-free is in EPEL repo - packages += ["tesseract", "ffmpeg-free"] # Prepare for virtual cards if any(c["card_type"] == CardType.virtual @@ -181,21 +178,7 @@ def setup_system(self, install_missing: bool, gdm: bool, graphical: bool): raise exceptions.SCAutolibException(msg) if graphical: - if not isDistro('fedora'): - run(['dnf', 'groupinstall', 'Server with GUI', '-y', - '--allowerasing']) - run(['pip', 'install', 'python-uinput']) - else: - # Fedora doesn't have server with GUI group so installed gdm - # manually and also python3-uinput should be installed from RPM - run(['dnf', 'install', 'gdm', 'python3-uinput', '-y']) - # disable subscription message - run(['systemctl', '--global', 'mask', - 'org.gnome.SettingsDaemon.Subscription.target']) - # disable welcome message - self.dconf_file.create() - self.dconf_file.save() - run('dconf update') + self.setup_graphical(install_missing, gdm) if not isDistro('fedora'): run(['dnf', 'groupinstall', "Smart Card Support", '-y', @@ -216,6 +199,38 @@ def setup_system(self, install_missing: bool, gdm: bool, graphical: bool): dump_to_json(user.User(username="root", password=self.lib_conf["root_passwd"])) + def setup_graphical(self, install_missing: bool, gdm: bool): + packages = ["gcc", "tesseract", "ffmpeg-free"] + + if gdm: + packages.append("gdm") + + missing = _check_packages(packages) + if install_missing and missing: + _install_packages(missing) + elif missing: + msg = "Can't continue with graphical. Some packages are missing: " \ + f"{', '.join(missing)}" + logger.critical(msg) + raise exceptions.SCAutolibException(msg) + + if not isDistro('fedora'): + run(['dnf', 'groupinstall', 'Server with GUI', '-y', + '--allowerasing']) + run(['pip', 'install', 'python-uinput']) + else: + # Fedora doesn't have server with GUI group so installed gdm + # manually and also python3-uinput should be installed from RPM + run(['dnf', 'install', 'gdm', 'python3-uinput', '-y']) + # disable subscription message + run(['systemctl', '--global', 'mask', + 'org.gnome.SettingsDaemon.Subscription.target']) + # disable welcome message + if not self.dconf_file.exists(): + self.dconf_file.create() + self.dconf_file.save() + run('dconf update') + def setup_local_ca(self, force: bool = False): """ Setup local CA based on configuration from the configuration file. All diff --git a/SCAutolib/models/file.py b/SCAutolib/models/file.py index 91784a1..f3c170e 100644 --- a/SCAutolib/models/file.py +++ b/SCAutolib/models/file.py @@ -92,6 +92,12 @@ def remove(self): f"Removed file {self._conf_file}." ) + def exists(self): + """ + Checks if a file exists. Returns boolean. + """ + return self._conf_file.exists() + def set(self, key: str, value: Union[int, str, bool], section: str = None, separator: str = "="): """ From ba65b4019debfff1d778c12f21e03ef0244fb6d6 Mon Sep 17 00:00:00 2001 From: Scott Poore Date: Mon, 9 Dec 2024 11:50:10 -0600 Subject: [PATCH 4/5] Logging update from gpantelakis Update to fix logging to work better when multiple scauto commands may be run. --- SCAutolib/cli_commands.py | 2 +- SCAutolib/models/gui.py | 88 +++++++++++++++++++++++++++++---------- 2 files changed, 67 insertions(+), 23 deletions(-) diff --git a/SCAutolib/cli_commands.py b/SCAutolib/cli_commands.py index a736fa7..7743d87 100644 --- a/SCAutolib/cli_commands.py +++ b/SCAutolib/cli_commands.py @@ -285,7 +285,7 @@ def run_all(ctx, actions, install_missing): ctx.obj["CONTROLLER"].setup_graphical(install_missing, True) from SCAutolib.models.gui import GUI - gui = GUI() + gui = GUI(from_cli=True) for action in actions: if "init" in action: gui.__enter__() diff --git a/SCAutolib/models/gui.py b/SCAutolib/models/gui.py index e8a2392..f1abf8d 100644 --- a/SCAutolib/models/gui.py +++ b/SCAutolib/models/gui.py @@ -1,7 +1,6 @@ import inspect -import os -from os.path import join from time import sleep, time +from pathlib import Path import cv2 import keyboard @@ -24,7 +23,14 @@ def __init__(self, directory: str, html_file: str = None): """ self.directory = directory self.html_file = html_file + + taken_images = [str(image).split('/')[-1] + for image in Path(directory).iterdir()] + taken_images.sort(reverse=True) + self.screenshot_num = 1 + if len(taken_images) > 0: + self.screenshot_num = int(taken_images[0].split('.')[0]) + 1 def screenshot(self, timeout: float = 30): """Runs ffmpeg to take a screenshot. @@ -237,7 +243,8 @@ def wrapper(self, *args, **kwargs): class GUI: """Represents the GUI and allows controlling the system under test.""" - def __init__(self, wait_time: float = 5, res_dir_name: str = None): + def __init__(self, wait_time: float = 5, res_dir_name: str = None, + from_cli: bool = False): """Initializes the GUI of system under test. :param wait_time: Time to wait after each action @@ -247,30 +254,52 @@ def __init__(self, wait_time: float = 5, res_dir_name: str = None): self.wait_time = wait_time self.gdm_init_time = 10 + self.from_cli = from_cli # Create the directory for screenshots + self.html_directory = Path("/tmp/SC-tests") + if not self.html_directory.exists(): + self.html_directory.mkdir() if res_dir_name: - self.html_directory = '/tmp/SC-tests/' + res_dir_name + self.html_directory = self.html_directory.joinpath(res_dir_name) + elif from_cli: + run_dirs = [str(run_dir).split('/')[-1] + for run_dir in self.html_directory.iterdir() + if "cli_gui" in str(run_dir)] + run_dirs.sort(reverse=True) + + last_run_dir = Path(run_dirs[0]) if len(run_dirs) > 0 else None + if last_run_dir and not last_run_dir.joinpath('done').exists(): + # Use the old run directory + logger.debug("Using HTML logging file from last time.") + self.html_directory = self.html_directory.joinpath( + last_run_dir) + else: + # Create new run directory + logger.debug("Creating new HTML logging file.") + self.html_directory = self.html_directory.joinpath( + str(int(time())) + '_cli_gui') else: calling_func = inspect.stack()[1][3] - self.html_directory = '/tmp/SC-tests/' + str(int(time())) - self.html_directory += "_" + calling_func + self.html_directory = self.html_directory.joinpath( + str(int(time())) + '_' + calling_func) - self.screenshot_directory = self.html_directory + "/screenshots" + self.screenshot_directory = self.html_directory.joinpath("screenshots") # will create both dirs - os.makedirs(self.screenshot_directory, exist_ok=True) + self.screenshot_directory.mkdir(parents=True, exist_ok=True) - self.html_file = join(self.html_directory, "index.html") - with open(self.html_file, 'w') as fp: - fp.write( - "\n" - "\n" - "\n" - "\n" - "Test Results\n" - "\n" - "\n" - ) + self.html_file = self.html_directory.joinpath("index.html") + if not self.html_file.exists(): + with open(self.html_file, 'w') as fp: + fp.write( + "\n" + "\n" + "\n" + "\n" + "Test Results\n" + "\n" + "\n" + ) fmt = "" fmt += "%(asctime)s " @@ -284,6 +313,9 @@ def __init__(self, wait_time: float = 5, res_dir_name: str = None): logging.Formatter("

" + fmt + "

") ) + if self.from_cli: + logger.addHandler(self.fileHandler) + self.mouse = Mouse() # workaround for keyboard library @@ -300,11 +332,17 @@ def __enter__(self): # This would break the display sleep(self.gdm_init_time) - logger.addHandler(self.fileHandler) + if not self.from_cli: + logger.addHandler(self.fileHandler) return self def __exit__(self, type, value, traceback): + done_file = self.html_directory.joinpath('done') + print(done_file) + if done_file.exists(): + return + run(['systemctl', 'stop', 'gdm'], check=True) with open(self.html_file, 'a') as fp: @@ -313,7 +351,13 @@ def __exit__(self, type, value, traceback): "\n" ) - logger.removeHandler(self.fileHandler) + print(done_file) + with open(done_file, 'w') as fp: + fp.write("done") + + if not self.from_cli: + logger.removeHandler(self.fileHandler) + logger.info(f"HTML file with results created in {self.html_directory}.") @action_decorator From 36133fd2d4acdf13d3865f840d5381ba75e8fce0 Mon Sep 17 00:00:00 2001 From: Scott Poore Date: Fri, 20 Dec 2024 11:27:54 -0600 Subject: [PATCH 5/5] flake8: install krb5-config/gcc --- .github/workflows/flake8.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/flake8.yaml b/.github/workflows/flake8.yaml index 8111bb9..dae99be 100644 --- a/.github/workflows/flake8.yaml +++ b/.github/workflows/flake8.yaml @@ -21,6 +21,13 @@ jobs: with: python-version: 3 + - name: Install krb5-config deps + run: sudo apt-get update -y + && sudo apt-get upgrade -y + && sudo apt-get install -y + libkrb5-dev + gcc + - name: Install dependencies run: python3 -m pip install tox