diff options
Diffstat (limited to 'ice_validator/vvp.py')
-rw-r--r-- | ice_validator/vvp.py | 393 |
1 files changed, 67 insertions, 326 deletions
diff --git a/ice_validator/vvp.py b/ice_validator/vvp.py index b8e2e84..cc2c66f 100644 --- a/ice_validator/vvp.py +++ b/ice_validator/vvp.py @@ -46,11 +46,12 @@ To make an executable for windows execute the ``make_exe.bat`` to generate the NOTE: This script does require Python 3.6+ """ -import appdirs + import os +import traceback + import pytest import version -import yaml import contextlib import multiprocessing import queue @@ -60,8 +61,6 @@ import zipfile import platform import subprocess # nosec -from collections import MutableMapping -from configparser import ConfigParser from multiprocessing import Queue from pathlib import Path from shutil import rmtree @@ -102,9 +101,9 @@ from tkinter import ( NORMAL, ) from tkinter.scrolledtext import ScrolledText -from typing import Optional, List, Dict, TextIO, Callable, Iterator +from typing import Optional, TextIO, Callable -import preload +from config import Config VERSION = version.VERSION PATH = os.path.dirname(os.path.realpath(__file__)) @@ -213,40 +212,16 @@ class HyperlinkManager: return -class QueueWriter: - """``stdout`` and ``stderr`` will be written to this queue by pytest, and - pulled into the main GUI application""" - - def __init__(self, log_queue: queue.Queue): - """Writes data to the provided queue. - - :param log_queue: the queue instance to write to. - """ - self.queue = log_queue - - def write(self, data: str): - """Writes ``data`` to the queue """ - self.queue.put(data) - - # noinspection PyMethodMayBeStatic - def isatty(self) -> bool: - """Always returns ``False``""" - return False - - def flush(self): - """No operation method to satisfy file-like behavior""" - pass - - def run_pytest( template_dir: str, log: TextIO, result_queue: Queue, categories: Optional[list], - verbosity: str, report_format: str, halt_on_failure: bool, template_source: str, + env_dir: str, + preload_format: list, ): """Runs pytest using the given ``profile`` in a background process. All ``stdout`` and ``stderr`` are redirected to ``log``. The result of the job @@ -261,9 +236,6 @@ def run_pytest( will collect and execute all tests that are decorated with any of the passed categories, as well as tests not decorated with a category. - :param verbosity: Flag to be passed to pytest to control verbosity. - Options are '' (empty string), '-v' (verbose), - '-vv' (more verbose). :param report_format: Determines the style of report written. Options are csv, html, or excel :param halt_on_failure: Determines if validation will halt when basic failures @@ -271,6 +243,9 @@ def run_pytest( prevent a large number of errors from flooding the report. :param template_source: The path or name of the template to show on the report + :param env_dir: Optional directory of env files that can be used + to generate populated preload templates + :param preload_format: Selected preload format """ out_path = "{}/{}".format(PATH, OUT_DIR) if os.path.exists(out_path): @@ -280,283 +255,23 @@ def run_pytest( args = [ "--ignore=app_tests", "--capture=sys", - verbosity, "--template-directory={}".format(template_dir), "--report-format={}".format(report_format), "--template-source={}".format(template_source), ] + if env_dir: + args.append("--env-directory={}".format(env_dir)) if categories: for category in categories: args.extend(("--category", category)) if not halt_on_failure: args.append("--continue-on-failure") + if preload_format: + args.append("--preload-format={}".format(preload_format)) pytest.main(args=args) result_queue.put((True, None)) - except Exception as e: - result_queue.put((False, e)) - - -class UserSettings(MutableMapping): - FILE_NAME = "UserSettings.ini" - - def __init__(self, namespace, owner): - user_config_dir = appdirs.AppDirs(namespace, owner).user_config_dir - if not os.path.exists(user_config_dir): - os.makedirs(user_config_dir, exist_ok=True) - self._settings_path = os.path.join(user_config_dir, self.FILE_NAME) - self._config = ConfigParser() - self._config.read(self._settings_path) - - def __getitem__(self, k): - return self._config["DEFAULT"][k] - - def __setitem__(self, k, v) -> None: - self._config["DEFAULT"][k] = v - - def __delitem__(self, v) -> None: - del self._config["DEFAULT"][v] - - def __len__(self) -> int: - return len(self._config["DEFAULT"]) - - def __iter__(self) -> Iterator: - return iter(self._config["DEFAULT"]) - - def save(self): - with open(self._settings_path, "w") as f: - self._config.write(f) - - -class Config: - """ - Configuration for the Validation GUI Application - - Attributes - ---------- - ``log_queue`` Queue for the ``stdout`` and ``stderr` of - the background job - ``log_file`` File-like object (write only!) that writes to - the ``log_queue`` - ``status_queue`` Job completion status of the background job is - posted here as a tuple of (bool, Exception). - The first parameter is True if the job completed - successfully, and False otherwise. If the job - failed, then an Exception will be provided as the - second element. - ``command_queue`` Used to send commands to the GUI. Currently only - used to send shutdown commands in tests. - """ - - DEFAULT_FILENAME = "vvp-config.yaml" - DEFAULT_POLLING_FREQUENCY = "1000" - - def __init__(self, config: dict = None): - """Creates instance of application configuration. - - :param config: override default configuration if provided.""" - if config: - self._config = config - else: - with open(self.DEFAULT_FILENAME, "r") as f: - self._config = yaml.safe_load(f) - self._user_settings = UserSettings( - self._config["namespace"], self._config["owner"] - ) - self._watched_variables = [] - self._validate() - self._manager = multiprocessing.Manager() - self.log_queue = self._manager.Queue() - self.status_queue = self._manager.Queue() - self.log_file = QueueWriter(self.log_queue) - self.command_queue = self._manager.Queue() - - def watch(self, *variables): - """Traces the variables and saves their settings for the user. The - last settings will be used where available""" - self._watched_variables = variables - for var in self._watched_variables: - var.trace_add("write", self.save_settings) - - # noinspection PyProtectedMember,PyUnusedLocal - def save_settings(self, *args): - """Save the value of all watched variables to user settings""" - for var in self._watched_variables: - self._user_settings[var._name] = str(var.get()) - self._user_settings.save() - - @property - def app_name(self) -> str: - """Name of the application (displayed in title bar)""" - app_name = self._config["ui"].get("app-name", "VNF Validation Tool") - return "{} - {}".format(app_name, VERSION) - - @property - def category_names(self) -> List[str]: - """List of validation profile names for display in the UI""" - return [category["name"] for category in self._config["categories"]] - - @property - def polling_frequency(self) -> int: - """Returns the frequency (in ms) the UI polls the queue communicating - with any background job""" - return int( - self._config["settings"].get( - "polling-frequency", self.DEFAULT_POLLING_FREQUENCY - ) - ) - - @property - def disclaimer_text(self) -> str: - return self._config["ui"].get("disclaimer-text", "") - - @property - def requirement_link_text(self) -> str: - return self._config["ui"].get("requirement-link-text", "") - - @property - def requirement_link_url(self) -> str: - path = self._config["ui"].get("requirement-link-url", "") - if not path.startswith("http"): - path = "file://{}".format(os.path.join(PATH, path)) - return path - - @property - def terms(self) -> dict: - return self._config.get("terms", {}) - - @property - def terms_link_url(self) -> Optional[str]: - return self.terms.get("path") - - @property - def terms_link_text(self): - return self.terms.get("popup-link-text") - - @property - def terms_version(self) -> Optional[str]: - return self.terms.get("version") - - @property - def terms_popup_title(self) -> Optional[str]: - return self.terms.get("popup-title") - - @property - def terms_popup_message(self) -> Optional[str]: - return self.terms.get("popup-msg-text") - - @property - def are_terms_accepted(self) -> bool: - version = "terms-{}".format(self.terms_version) - return self._user_settings.get(version, "False") == "True" - - def set_terms_accepted(self): - version = "terms-{}".format(self.terms_version) - self._user_settings[version] = "True" - self._user_settings.save() - - def default_verbosity(self, levels: Dict[str, str]) -> str: - requested_level = self._user_settings.get("verbosity") or self._config[ - "settings" - ].get("default-verbosity", "Standard") - keys = [key for key in levels] - for key in levels: - if key.lower().startswith(requested_level.lower()): - return key - raise RuntimeError( - "Invalid default-verbosity level {}. Valid " - "values are {}".format(requested_level, ", ".join(keys)) - ) - - def get_description(self, category_name: str) -> str: - """Returns the description associated with the category name""" - return self._get_category(category_name)["description"] - - def get_category(self, category_name: str) -> str: - """Returns the category associated with the category name""" - return self._get_category(category_name).get("category", "") - - def get_category_value(self, category_name: str) -> str: - """Returns the saved value for a category name""" - return self._user_settings.get(category_name, 0) - - def _get_category(self, category_name: str) -> Dict[str, str]: - """Returns the profile definition""" - for category in self._config["categories"]: - if category["name"] == category_name: - return category - raise RuntimeError( - "Unexpected error: No category found in vvp-config.yaml " - "with a name of " + category_name - ) - - @property - def default_report_format(self): - return self._user_settings.get("report_format", "HTML") - - @property - def report_formats(self): - return ["CSV", "Excel", "HTML"] - - @property - def preload_formats(self): - excluded = self._config.get("excluded-preloads", []) - formats = (cls.format_name() for cls in preload.get_generator_plugins()) - return [f for f in formats if f not in excluded] - - @property - def default_preload_format(self): - default = self._user_settings.get("preload_format") - if default and default in self.preload_formats: - return default - else: - return self.preload_formats[0] - - @staticmethod - def get_subdir_for_preload(preload_format): - for gen in preload.get_generator_plugins(): - if gen.format_name() == preload_format: - return gen.output_sub_dir() - return "" - - @property - def default_input_format(self): - requested_default = self._user_settings.get("input_format") or self._config[ - "settings" - ].get("default-input-format") - if requested_default in self.input_formats: - return requested_default - else: - return self.input_formats[0] - - @property - def input_formats(self): - return ["Directory (Uncompressed)", "ZIP File"] - - @property - def default_halt_on_failure(self): - setting = self._user_settings.get("halt_on_failure", "True") - return setting.lower() == "true" - - def _validate(self): - """Ensures the config file is properly formatted""" - categories = self._config["categories"] - - # All profiles have required keys - expected_keys = {"name", "description"} - for category in categories: - actual_keys = set(category.keys()) - missing_keys = expected_keys.difference(actual_keys) - if missing_keys: - raise RuntimeError( - "Error in vvp-config.yaml file: " - "Required field missing in category. " - "Missing: {} " - "Categories: {}".format(",".join(missing_keys), category) - ) - - -def validate(): - return True + except Exception: + result_queue.put((False, traceback.format_exc())) class Dialog(Toplevel): @@ -610,9 +325,6 @@ class Dialog(Toplevel): # noinspection PyUnusedLocal def ok(self, event=None): - if not validate(): - self.initial_focus.focus_set() # put focus back - return self.withdraw() self.update_idletasks() self.apply() @@ -656,8 +368,6 @@ class TermsAndConditionsDialog(Dialog): class ValidatorApp: - VERBOSITY_LEVELS = {"Less": "", "Standard (-v)": "-v", "More (-vv)": "-vv"} - def __init__(self, config: Config = None): """Constructs the GUI element of the Validation Tool""" self.task = None @@ -684,7 +394,7 @@ class ValidatorApp: ) actions = Frame(control_panel) control_panel.add(actions) - control_panel.paneconfigure(actions, minsize=250) + control_panel.paneconfigure(actions, minsize=350) if self.config.disclaimer_text or self.config.requirement_link_text: self.footer = self.create_footer(parent_frame) @@ -713,16 +423,6 @@ class ValidatorApp: settings_frame = LabelFrame(actions, text="Settings") settings_row = 1 settings_frame.grid(row=3, column=1, columnspan=3, pady=10, sticky="we") - verbosity_label = Label(settings_frame, text="Verbosity:") - verbosity_label.grid(row=settings_row, column=1, sticky=W) - self.verbosity = StringVar(self._root, name="verbosity") - self.verbosity.set(self.config.default_verbosity(self.VERBOSITY_LEVELS)) - verbosity_menu = OptionMenu( - settings_frame, self.verbosity, *tuple(self.VERBOSITY_LEVELS.keys()) - ) - verbosity_menu.config(width=25) - verbosity_menu.grid(row=settings_row, column=2, columnspan=3, sticky=E, pady=5) - settings_row += 1 if self.config.preload_formats: preload_format_label = Label(settings_frame, text="Preload Template:") @@ -766,12 +466,35 @@ class ValidatorApp: self.halt_on_failure = BooleanVar(self._root, name="halt_on_failure") self.halt_on_failure.set(self.config.default_halt_on_failure) - halt_on_failure_label = Label(settings_frame, text="Halt on Basic Failures:") - halt_on_failure_label.grid(row=settings_row, column=1, sticky=E, pady=5) + halt_on_failure_label = Label( + settings_frame, text="Halt on Basic Failures:", anchor=W, justify=LEFT + ) + halt_on_failure_label.grid(row=settings_row, column=1, sticky=W, pady=5) halt_checkbox = Checkbutton( settings_frame, offvalue=False, onvalue=True, variable=self.halt_on_failure ) halt_checkbox.grid(row=settings_row, column=2, columnspan=2, sticky=W, pady=5) + settings_row += 1 + + self.create_preloads = BooleanVar(self._root, name="create_preloads") + self.create_preloads.set(0) + create_preloads_label = Label( + settings_frame, + text="Create Preload from Env Files:", + anchor=W, + justify=LEFT, + ) + create_preloads_label.grid(row=settings_row, column=1, sticky=W, pady=5) + create_preloads_checkbox = Checkbutton( + settings_frame, + offvalue=False, + onvalue=True, + variable=self.create_preloads, + command=self.set_env_dir_state, + ) + create_preloads_checkbox.grid( + row=settings_row, column=2, columnspan=2, sticky=W, pady=5 + ) directory_label = Label(actions, text="Template Location:") directory_label.grid(row=4, column=1, pady=5, sticky=W) @@ -781,10 +504,20 @@ class ValidatorApp: directory_browse = Button(actions, text="...", command=self.ask_template_source) directory_browse.grid(row=4, column=3, pady=5, sticky=W) + env_dir_label = Label(actions, text="Env Files:") + env_dir_label.grid(row=5, column=1, pady=5, sticky=W) + self.env_dir = StringVar(self._root, name="env_dir") + self.env_dir_entry = Entry( + actions, width=40, textvariable=self.env_dir, state=DISABLED + ) + self.env_dir_entry.grid(row=5, column=2, pady=5, sticky=W) + env_dir_browse = Button(actions, text="...", command=self.ask_env_dir_source) + env_dir_browse.grid(row=5, column=3, pady=5, sticky=W) + validate_button = Button( - actions, text="Validate Templates", command=self.validate + actions, text="Process Templates", command=self.validate ) - validate_button.grid(row=5, column=1, columnspan=2, pady=5) + validate_button.grid(row=6, column=1, columnspan=2, pady=5) self.result_panel = Frame(actions) # We'll add these labels now, and then make them visible when the run completes @@ -796,12 +529,12 @@ class ValidatorApp: self.result_label.bind("<Button-1>", self.open_report) self.preload_label = Label( - self.result_panel, text="View Preloads", fg="blue", cursor="hand2" + self.result_panel, text="View Preload Templates", fg="blue", cursor="hand2" ) self.underline(self.preload_label) self.preload_label.bind("<Button-1>", self.open_preloads) - self.result_panel.grid(row=6, column=1, columnspan=2) + self.result_panel.grid(row=7, column=1, columnspan=2) control_panel.pack(fill=BOTH, expand=1) main_window.add(control_panel) @@ -827,7 +560,6 @@ class ValidatorApp: self.config.watch( *self.categories, - self.verbosity, self.input_format, self.report_format, self.halt_on_failure, @@ -871,6 +603,10 @@ class ValidatorApp: footer.pack(fill=BOTH, expand=True) return footer + def set_env_dir_state(self): + state = NORMAL if self.create_preloads.get() else DISABLED + self.env_dir_entry.config(state=state) + def ask_template_source(self): if self.input_format.get() == "ZIP File": template_source = filedialog.askopenfilename( @@ -881,6 +617,9 @@ class ValidatorApp: template_source = filedialog.askdirectory() self.template_source.set(template_source) + def ask_env_dir_source(self): + self.env_dir.set(filedialog.askdirectory()) + def validate(self): """Run the pytest validations in a background process""" if not self.delete_prior_report(): @@ -904,10 +643,11 @@ class ValidatorApp: self.config.log_file, self.config.status_queue, self.categories_list(), - self.VERBOSITY_LEVELS[self.verbosity.get()], self.report_format.get().lower(), self.halt_on_failure.get(), self.template_source.get(), + self.env_dir.get(), + self.preload_format.get(), ), ) self.task.daemon = True @@ -1001,7 +741,8 @@ class ValidatorApp: # noinspection PyUnusedLocal def open_report(self, event): """Open the report in the user's default browser""" - webbrowser.open_new("file://{}".format(self.report_file_path)) + path = Path(self.report_file_path).absolute().resolve().as_uri() + webbrowser.open_new(path) def open_preloads(self, event): """Open the report in the user's default browser""" |