X-Git-Url: https://gerrit.onap.org/r/gitweb?a=blobdiff_plain;f=ice_validator%2Fvvp.py;h=a998fd18789d8f22c096efd2c6f7d5d51827b244;hb=0c4e64d87728b89aa9cd4d41d738f5bfe64ceee3;hp=547a3b4d008d8c75d7a305700f7306502d61acc3;hpb=b08a07233b27948f823dc004096102328f8ce725;p=vvp%2Fvalidation-scripts.git diff --git a/ice_validator/vvp.py b/ice_validator/vvp.py index 547a3b4..a998fd1 100644 --- a/ice_validator/vvp.py +++ b/ice_validator/vvp.py @@ -46,21 +46,21 @@ 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 sys import version -import yaml import contextlib import multiprocessing import queue import tempfile import webbrowser 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 @@ -101,7 +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 + +from config import Config VERSION = version.VERSION PATH = os.path.dirname(os.path.realpath(__file__)) @@ -210,52 +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 get_plugins() -> Optional[List]: - """When running in a frozen bundle, plugins to be registered - explicitly. This method will return the required plugins to register - based on the run mode""" - if hasattr(sys, "frozen"): - import pytest_tap.plugin - - return [pytest_tap.plugin] - else: - return None - - 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 @@ -270,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 @@ -280,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): @@ -289,260 +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") - pytest.main(args=args, plugins=get_plugins()) + 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.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", "") - return "file://{}".format(os.path.join(PATH, 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 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): @@ -596,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() @@ -642,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 @@ -670,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) @@ -697,45 +421,80 @@ class ValidatorApp: category_checkbox.grid(row=x + 1, column=1, columnspan=2, sticky="w") 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=1, 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=1, column=2, columnspan=3, sticky=E, pady=5) + + if self.config.preload_formats: + preload_format_label = Label(settings_frame, text="Preload Template:") + preload_format_label.grid(row=settings_row, column=1, sticky=W) + self.preload_format = StringVar(self._root, name="preload_format") + self.preload_format.set(self.config.default_preload_format) + preload_format_menu = OptionMenu( + settings_frame, self.preload_format, *self.config.preload_formats + ) + preload_format_menu.config(width=25) + preload_format_menu.grid( + row=settings_row, column=2, columnspan=3, sticky=E, pady=5 + ) + settings_row += 1 report_format_label = Label(settings_frame, text="Report Format:") - report_format_label.grid(row=2, column=1, sticky=W) + report_format_label.grid(row=settings_row, column=1, sticky=W) self.report_format = StringVar(self._root, name="report_format") self.report_format.set(self.config.default_report_format) report_format_menu = OptionMenu( settings_frame, self.report_format, *self.config.report_formats ) report_format_menu.config(width=25) - report_format_menu.grid(row=2, column=2, columnspan=3, sticky=E, pady=5) + report_format_menu.grid( + row=settings_row, column=2, columnspan=3, sticky=E, pady=5 + ) + settings_row += 1 input_format_label = Label(settings_frame, text="Input Format:") - input_format_label.grid(row=3, column=1, sticky=W) + input_format_label.grid(row=settings_row, column=1, sticky=W) self.input_format = StringVar(self._root, name="input_format") self.input_format.set(self.config.default_input_format) input_format_menu = OptionMenu( settings_frame, self.input_format, *self.config.input_formats ) input_format_menu.config(width=25) - input_format_menu.grid(row=3, column=2, columnspan=3, sticky=E, pady=5) + input_format_menu.grid( + row=settings_row, column=2, columnspan=3, sticky=E, pady=5 + ) + settings_row += 1 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=4, 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=4, column=2, columnspan=2, sticky=W, pady=5) + 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(self.config.default_create_preloads) + 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) @@ -745,10 +504,21 @@ 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") + env_dir_state = NORMAL if self.create_preloads.get() else DISABLED + self.env_dir_entry = Entry( + actions, width=40, textvariable=self.env_dir, state=env_dir_state + ) + 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 @@ -758,7 +528,14 @@ class ValidatorApp: ) self.underline(self.result_label) self.result_label.bind("", self.open_report) - self.result_panel.grid(row=6, column=1, columnspan=2) + + self.preload_label = Label( + self.result_panel, text="View Preload Templates", fg="blue", cursor="hand2" + ) + self.underline(self.preload_label) + self.preload_label.bind("", self.open_preloads) + + self.result_panel.grid(row=7, column=1, columnspan=2) control_panel.pack(fill=BOTH, expand=1) main_window.add(control_panel) @@ -773,19 +550,22 @@ class ValidatorApp: # room for them self.completion_label.pack() self.result_label.pack() # Show report link + self.preload_label.pack() # Show preload link self._root.after_idle( lambda: ( self.completion_label.pack_forget(), self.result_label.pack_forget(), + self.preload_label.pack_forget(), ) ) self.config.watch( *self.categories, - self.verbosity, self.input_format, self.report_format, self.halt_on_failure, + self.preload_format, + self.create_preloads, ) self.schedule(self.execute_pollers) if self.config.terms_link_text and not self.config.are_terms_accepted: @@ -795,9 +575,7 @@ class ValidatorApp: def create_footer(self, parent_frame): footer = Frame(parent_frame) - disclaimer = Message( - footer, text=self.config.disclaimer_text, anchor=CENTER - ) + disclaimer = Message(footer, text=self.config.disclaimer_text, anchor=CENTER) disclaimer.grid(row=0, pady=2) parent_frame.bind( "", lambda e: disclaimer.configure(width=e.width - 20) @@ -826,6 +604,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( @@ -836,6 +618,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(): @@ -851,6 +636,7 @@ class ValidatorApp: self.clear_log() self.completion_label.pack_forget() self.result_label.pack_forget() + self.preload_label.pack_forget() self.task = multiprocessing.Process( target=run_pytest, args=( @@ -858,10 +644,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 @@ -907,6 +694,8 @@ class ValidatorApp: if is_success: self.completion_label.pack() self.result_label.pack() # Show report link + if hasattr(self, "preload_format"): + self.preload_label.pack() # Show preload link else: self.log_panel.insert(END, str(e)) @@ -953,7 +742,23 @@ 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""" + path = os.path.join( + PATH, + OUT_DIR, + "preloads", + self.config.get_subdir_for_preload(self.preload_format.get()), + ) + if platform.system() == "Windows": + os.startfile(path) # nosec + elif platform.system() == "Darwin": + subprocess.Popen(["open", path]) # nosec + else: + subprocess.Popen(["xdg-open", path]) # nosec def open_requirements(self): """Open the report in the user's default browser"""