def invoke_script(config, name, args, fallback): """ Invokes the script identified by it's string NAME. On default this command will attempt to invoke the most recent version of the script, which is subject to version control and came with the most recent CI build. To use the stable fallback version which comes with the ufotest software itself use the --fallback flag """ ctitle('Invoking script') cparams({ 'script name': name, 'additional args': args, 'use fallback?': fallback, }) try: result = config.sm.invoke(name, args, use_fallback=fallback) if result['exit_code'] == 0: cresult(f'script "{name}" exits with code 0') cprint('STDOUT:\n' + result['stdout']) elif result['exit_code'] != 0: cerror(f'script "{name}" exits with code 1') cerror('STDERR:\n' + result['stderr']) cprint('STDOUT:\n' + result['stdout']) sys.exit(1) except KeyError: cerror(f'A script with name "{name}" is not registered with ufotest!') sys.exit(1) sys.exit(0)
def list_scripts(config, full): """ Displays a list of all scripts that are registered with ufotest. Use the --detail flag to display more information about each script. """ ctitle('list registered scripts') for script_name, script_wrapper in config.sm.scripts.items(): csubtitle(f'{script_name} ({script_wrapper.data["class"]})') if full: details = { 'description': script_wrapper.description, 'author': script_wrapper.author, 'path': script_wrapper.path, 'has fallback?': script_name in config.sm.fallback_scripts, } else: details = { 'path': script_wrapper.path, 'has fallback?': script_name in config.sm.fallback_scripts } cparams(details) sys.exit(0)
def test(config, suite, test_id): """ Run the test "TEST_ID" TEST_ID is a string, which identifies a certain test procedure. To view all these possible identifiers consult the config file. This command executes one or multiple camera test cases. These test cases interface with the camera to perform various actions to confirm certain functionality of the camera and the fpga module. This process will most likely take a good amount of time to finish. After all test cases are done, a test report will be generated and saved in an according sub folder of the "archive" The --suite flag can be used to execute a whole test suite (consisting of multiple test cases) instead. In case this flag is passed, the TEST_ID argument will be interpreted as the string name identifier of the suite. NOTE: To quickly assess a test case without actually having to interface with the camera itself it is possible to use the --mock flag *on the ufotest base command* to use the mock camera to perform the test cases. """ ctitle(f'RUNNING TEST{" SUITE" if suite else ""}') cparams({ 'test identifier': test_id, 'is test suite': suite, 'verbose': config.verbose() }) try: with TestContext() as test_context: # 1 -- DYNAMICALLY LOADING THE TESTS test_runner = TestRunner(test_context) test_runner.load() # 2 -- EXECUTING THE TEST CASES if suite: click.secho(' Executing test suite: {}...'.format(test_id), bold=True) test_runner.run_suite(test_id) else: click.secho(' Executing test: {}...'.format(test_id), bold=True) test_runner.run_test(test_id) # 3 -- SAVING THE RESULTS AS A REPORT test_report = TestReport(test_context) test_report.save(test_context.folder_path) except Exception as e: click.secho('[!] {}'.format(e), fg='red', bold=True) sys.exit(1) if config.verbose(): click.secho(test_report.to_string()) cresult('Test report saved to: {}'.format(test_context.folder_path)) cprint( 'View the report at: http://localhost/archive/{}/report.html'.format( test_context.folder_name)) sys.exit(0)
def recompile(config): """ This command recompiles all the static HTML files of the test reports. All test reports are actually static HTML files, which are created from a template after the corresponding test run finishes. This presents an issue if any web interface related changes are made to the config or if the html templates are updated with a new release version. In such a case the test report html pages would not reflect the changes. In this case, the "recompile" command can be used to recreate the static html files using the current config / template versions. """ # THE PROBLEM # So this is the problem: Test reports are actually static html files. These html templates for each test reports # are rendered once the test run finishes and then they are just html files. In contrary to dynamically creating # the html content whenever the corresponding request is made. And I would like to keep it like this, because the # it is easier to handle in other regards, but this poses the following problem: If certain changes are made to the # server these are not reflected within the rest report html pages. The most pressing problem is if the URL changes # This will break all links on the test report pages. Another issue is when a new version of ufotest introduces # changes to the report template, which wont be reflected in older reports. # THE IDEA # The "recompile" command should fix this by recreating all the test report static html files with the current # version of the report template based on the info in the report.json file ctitle('RECOMPILE TEST REPORT HTML') cparams({'archive folder': config.get_archive_path()}) count = 0 for root, folders, files in os.walk(config.get_archive_path()): for folder in folders: folder_path = os.path.join(root, folder) report_json_path = os.path.join(folder_path, 'report.json') report_html_path = os.path.join(folder_path, 'report.html') if not os.path.exists(report_json_path): continue with open(report_json_path, mode='r+') as file: report_data = json.load(file) with open(report_html_path, mode='w+') as file: # "report_data" is only the dict representation of the test report. This is problematic because we # cannot call the "to_html" method then. But the test report class and all the test result classes # implement the HTMLTemplateMixin class, which enables to render the html template from just this dict # representation using the static method "html_from_dict". This is possible because the dict itself # saves the entire template string in one of its fields. html = HTMLTemplateMixin.html_from_dict(report_data) file.write(html) cprint(f'Recompiled test report {folder}') count += 1 break cresult(f'Recompiled {count} test reports!') sys.exit(0)
def install_all(config, path, no_dependencies, save_json, skip): """ Installing all dependencies for the ufo camera project into PATH PATH has to be the path to an existing folder. The command installs all the required repositories into this folder. Apart from the repositories, the command also installs the required system packages for the operation of the UFO camera. For this it relies on the package installation method and operation system defined within the ufotest config file. """ # The path sting which is passed into this function could also be a relative path such as ".." which stands for # "the parent folder relative to the current one from which I am executing this". All functions expect an # absolute path and "realpath" converts these relative expressions into absolute paths path = os.path.realpath(path) operating_system = CONFIG['install']['os'] ctitle('INSTALLING NECESSARY REQUIREMENTS') cparams({ 'operating system': operating_system, 'package install command': CONFIG['install'][operating_system]['package_install'], 'camera dimensions:': f'{CONFIG.get_sensor_width()} x {CONFIG.get_sensor_height()}' }) results = {} # We will only actually execute the installation procedures if the skip flag is not set if not skip: if no_dependencies: cprint('Skipping system packages...') else: install_dependencies(verbose=config.verbose()) for dependency_name, install_function in DEPENDENCY_INSTALL_FUNCTIONS.items( ): csubtitle(f'Installing "{dependency_name}"') result = install_function(path) results[dependency_name] = result else: results['mock'] = mock_install_repository(path) # Creating the JSON file if the flag was set. if save_json: try: json_path = os.path.join(os.getcwd(), 'install.json') with open(json_path, mode='w+') as file: json.dump(results, file, indent=4, sort_keys=True) except Exception as e: cerror(f'Could not save "install.json" because: {str(e)}') sys.exit(1) sys.exit(0)
def init(force, update): """ Initializes the installation folder for this application. This folder will be located at "$HOME/.ufotest". The init furthermore includes the creation of the necessary sub folder structures within the installation folder as well as the creation of the default config file and all the static assets for the web interface. The --force flag can be used to *overwrite* an existing installation. It will delete the existing installation, if one exists, and then create an entirely new installation. The --update flag can be used to perform an update rather than a complete installation. This can be invoked with an installation present and will not overwrite custom configuration files. The only thing which will be replaced during such an update will be the static files. The new static files from the current system package version of ufotest will be copied to the active installation folder. """ installation_path = get_path() ctitle('INITIALIZING UFOTEST INSTALLATION') cparams({'installation path': installation_path}) if update: if check_path(installation_path, is_dir=True): update_install(installation_path) cresult('Updated ufotest installation') sys.exit(0) else: cerror( 'Cannot perform update without already existing installation!') sys.exit(1) if check_path(installation_path, is_dir=True): if force: shutil.rmtree(get_path()) cprint('Deleted old installation folder') else: cerror('An installation folder already exists at the given path!') cerror( 'Please use the --force flag if you wish to forcefully replace the existing installation' ) sys.exit(1) else: cprint(' Installation folder does not yet exist') # This method actually executes the necessary steps. init_install() cresult( 'UfoTest successfully initialized, use the --help option for further commands' ) sys.exit(0)
def script_details(config, name): """ Shows the full details of the scripts identified by it's string NAME. This command prints the full details for the given script, including its source code. """ # ~ Checking for eventual problems with the script # First we need to check if a script with the given name even exists if name in config.sm.scripts: script = config.sm.scripts[name] elif name in config.sm.fallback_scripts: script = config.sm.fallback_scripts[name] else: cerror( f'A script identified by "{name}" is nor registered with ufotest!') sys.exit(1) # Then we need to check if the file even exists / the content can be read, since we also want to display the # content of it try: with open(script.path, mode='r+') as file: content = file.read() except: cerror( f'The file {script.path} does not exists and/or is not readable!') sys.exit(1) # ~ Displaying the results to the user in case there are no problems ctitle(name) cparams({ 'type': script.data['class'], 'path': script.path, 'author': script.author }) cprint('DESCRIPTION:\n' + script.description) print() cprint('CONTENT:\n' + content)
def build(config, suite, skip) -> None: """ Start a new CI build process using the test suite SUITE. A build process first clones the target repository, which was specified within the "ci" section of the config file. Specifically, it checks out the branch, which is also defined in the config, and uses the most recent commit to that branch. After the repository has been cloned, the bit file within is used to flash a new configuration onto the FPGA hardware. Finally, the given test suite is executed and the results are being saved to the archive. """ # ~ PRINT CONFIGURATION ctitle('BUILDING FROM REMOTE REPOSITORY') cparams({ 'repository url': CONFIG.get_ci_repository_url(), 'repository branch': CONFIG.get_ci_branch(), 'bitfile relative path': CONFIG.get_ci_bitfile_path(), 'test suite': suite }) # ~ RUNNING THE BUILD PROCESS try: with build_context_from_config(CONFIG) as build_context: # The "build_context_from_config" function builds the context object entirely on the basis of the # configuration file, which means, that it also uses the default test suite defined there. Since the build # function is meant to be able to pass the test suite as a parameter, we will have to overwrite this piece # of config manually. build_context.test_suite = suite build_runner = BuildRunner(build_context) build_runner.run(test_only=skip) build_report = BuildReport(build_context) build_report.save(build_context.folder_path) sys.exit(0) except BuildError as e: cerror( f'An error has occurred during the build process...\n {str(e)}') sys.exit(1)
def flash(config, file: str, type: str) -> None: """ Uses the given FILE to flash a new firmware configuration to the internal memory of the FPGA board, where FILE is the string absolute path of the file which contains a valid specification of the fpga configuration. The --type option can be used to select which kind of configuration file to be flashed. currently only BIT files are supported. """ # -- ECHO CONFIGURATION ctitle('FLASHING BITFILE TO FPGA') click.secho('--| bitfile path: {}\n'.format(file)) # ~ SKIP FOR MOCK # If the "--mock" option is active then we completely skip this command since presumably there is not actual # camera connected to do the flashing. # TODO: In the future we could add an action hook here to be able to inject come actual code for the case of # mock camera + flash command. if config['context']['mock']: cresult('Skip flash when due to --mock option being enabled') sys.exit(0) # ~ CHECKING IF THE GIVEN FILE EVEN EXISTS file_path = os.path.abspath(file) file_exists = check_path(file_path, is_dir=False) if not file_exists: cerror('The given path for the bitfile does not exist') sys.exit(1) # ~ CHECKING IF THE FILE EVEN IS A BIT FILE file_extension = file_path.split('.')[-1].lower() is_bit_file = file_extension == 'bit' if not is_bit_file: cerror('The given path does not refer to file of the type ".bit"') sys.exit(1) # ~ CHECKING IF VIVADO IS INSTALLED vivado_installed = check_vivado() if not vivado_installed: cerror( 'Vivado is not installed, please install Vivado to be able to flash the fpga!' ) sys.exit(1) if CONFIG.verbose(): cprint('Vivado installation found') # ~ STARTING VIVADO SETTINGS vivado_command = CONFIG['install']['vivado_settings'] run_command(vivado_command, cwd=os.getcwd()) if CONFIG.verbose(): cprint('Vivado setup completed') # ~ FLASHING THE BIT FILE flash_command = "{setup} ; {command} -nolog -nojournal -mode batch -source {tcl} -tclargs {file}".format( setup=CONFIG['install']['vivado_settings'], command=CONFIG['install']['vivado_command'], tcl=CONFIG.sm.scripts['fpga_bitprog'].path, file=file_path) exit_code, _ = run_command(flash_command) if not exit_code: cresult('Flashed FPGA with: {}'.format(file_path)) sys.exit(0) else: cerror('There was an error during the flashing of the FPGA') sys.exit(1)
def frame(config, output, display): """ Capture a single frame from the camera. If this command is invoked without any additional options, the frame will be captured from the camera and then saved to the location "/tmp/frame.png". The output location for the image file can be overwritten by using the --output option to specify another path. The --display flag can be used to additionally display the image to the user after the frame has been captured. This feature requires a graphical interface to be available to the system. The frame will be opened in a seperate matplotlib figure window. """ config.pm.do_action('pre_command_frame', config=config, namespace=globals(), output=output, display=display) ctitle('CAPTURING FRAME') cparams({ 'output path': output, 'display frame': display, 'sensor_dimensions': f'{CONFIG.get_sensor_width()} x {CONFIG.get_sensor_height()}' }) # Setup all the important environment variables and stuff setup_environment() exit_code, _ = run_command('rm /tmp/frame*') if not exit_code: cprint('Removed previous frame buffer') # ~ GET THE FRAME FROM THE CAMERA # "get_frame" handles the whole communication process with the camera, it requests the frame, saves the raw data, # decodes it into an image and then returns the string path of the final image file. try: camera_class = config.pm.apply_filter('camera_class', UfoCamera) camera = camera_class(config) frame = camera.get_frame() except PciError as error: cerror('PCI communication with the camera failed!') cerror(f'PciError: {str(error)}') sys.exit(1) except FrameDecodingError as error: cerror('Decoding of the frame failed!') cerror(f'FrameDecodingError: {str(error)}') sys.exit(1) # ~ Saving the frame as a file _, file_extension = output.split('.') if file_extension == 'raw': cerror('Currently the frame cannot be saved as .raw format!') else: from PIL import Image image = Image.fromarray(frame) image.save(output) # ~ Displaying the image if the flag is set if display: # An interesting thing is that matplotlib is imported in-time here and not at the top of the file. This actually # had to be changed due to a bug. When using ufotest in a headless environment such as a SSH terminal session # it would crash immediately, because a headless session does not work with the graphical matplotlib. Since # it really only is needed for this small section here, it makes more sense to just import it in-time. import matplotlib.pyplot as plt matplotlib.use('TkAgg') plt.imshow(frame) plt.show() sys.exit(0)