# ATE.org Plugins This document describes the Plugin Api (or "hookspec" in pluggy terms) of an ATE.org plugin. Each ATE.org Plugin provides means to access its contents in different forms. Most notably: - Identification of contents - Instances of contents ## Design Rationale The API Design in this document tries to take into account the way the IDE is structured as well as the way Pluggy works. We focus on the following points: * Ease of integration * Flexibility * Extensibility ## Identification We use the "identification" functions to discover the functionality provided by a given plugin. The plugin shall provide a number of functions, that allow us to query that functionality. ### Plugin ``` get_plugin_identification() -> plugin_ident ``` This hook shall return a dictionary with the following layout: ``` name: "Plugin Name" version: "Plugin Version" ``` ### Plugin Functionality A given plugin publishes its functionality by means of Importers/Exporters/Actuators/Instruments/Equipments/Testers. Each of these can be queried by means of a "get_x_names" function where x is the name of the respective functionality. The returnvalue of the get_x_names functions is always a list of dictionaries, where each dictionary has the following layout: ``` display_name: "Display Name" version: "Object Version" name: "Object Name" ``` Where: * display_name denotes the string, that is used to display the the object in the ATE.org UI. * Version denotes the Version of the code, that is used for the respective object * Name: Denotes the actual internal name by which the object is identified within the plugin. The plugin must be able to return an instance of the object when the get_x hook is called with the string name as x_name parameter. Note that the object name must be unqique across all plugins. Therefore it is advisable to prefix the objectname with the pluginname. E.g: * Plugin: Semi-ATE * Object: CSVFileImporter * Name: Semi-ATE.CSVFileImporter #### Importers An importer is an object that is able to import a wafermap from a given (importer-specific) datasource and in a given format. E.g. there might be an importer that imports data as .CSV value from a database. ```Python get_importer_names() -> [] ``` #### Exporters An exporter is an object that is able to export a wafermap from to a given (exporter-specific) datasink and in a given format. E.g. ther might be an exporter that export to .CSV. ``` get_exporter_names() -> [] ``` #### Equipments An equipment object is used to access the specific functionality of prober or handler. ``` get_equipment_names() -> [] ``` #### Devicepin Importer A devicepin importer is used to import the pinset, that is accessible for a given device. ``` get_devicepin_importer_names() -> [] ``` #### Instruments An instrument is a piece of hardware, that is used to measure a given aspect of the DUT. It is usually used in a lab context. ``` get_instrument_names() -> [] ``` #### General Purpose Functions General Purpose Functions are plugin functionality that needs to be available to tests, but that is not tied to a specific usage scenario. ``` get_general_purpose_function_names() -> [] ``` The complete layout of the results of this function is: ``` { display_name: "Display Name" version: "Object Version" name: "Object Name" } ``` ### Tester The following function returns all installed testers use to be run beyond the Single Tester, which will be used by the testprogram to synchronize the test execution ``` get_tester_names() -> [] ``` ## Instances All hooks that deal with the instanciation of objects are provided the name of the plugin that should actually create the instance. The reason is, that pluggy will call each hook for all registered plugins, so a call to "get_importer" will be executed for each plugin, using the same parameter. Since we want to avoid nameclashes of objects we must uniquely identify the actual type of object we want created. See the section on object names for this. A note on dependencies: Importers and exporters are allowed to depend on PyQt to provide their own UI, if necessary. Actuators, Instruments and Equiments should not depend on PyQt - these objects are created by testcases and usually used in a headless environment (i.e. the tester itself!) ### Importers ``` get_importer(importer_name) -> Importer ``` This hook shall return an instance of an importer with the given name. An importer is expected to have the following interface: ``` do_import() -> data get_abort_reason() -> string ``` The importer shall show it's own import dialog (which may well be just a file chooser!), perform the import and return the imported data in an - as of now - not yet specified format. If the import fails/is canceled do_import() shall not propagate any exception to the application, but instead return "None". The plugin shall expose the reason, why no data was returned when get_abort_reason is called. ### Exporters ``` get_exporter(exporter_name) -> Exporter ``` This hook shall return an instance of an exporter with the given name. An importer is expected to have the following interface: ``` do_export(data) -> bool get_abort_reason() -> string ``` The exporter shall show it's own export dialog (which may well be just a file chooser!) and perform the export. If the export fails/is canceled do_export() shall not propagate any exception to the application, but instead return ```False```. The plugin shall expose the reason, why no data was exported when get_abort_reason is called. ### Equipments ``` get_equipment(equipment_name) -> EquipmentInstance ``` This hook shall return an instance of a given equipment. The returned instance has no specified interface. This hook is intended for use in tests only. If the plugin is unable to resolve the equipment name it shall *immediatly* throw an exception containing the missing equipment's name. Do not return None in this case as it makes diagnostics harder than a well thought out error string. ### Devicepins ``` get_devicepin_importer(importer_name) -> Importer ``` This hook shall return an instance of a given importer. The returned instance has no specified interface. This hook is intended for use in tests only. If the plugin is unable to resolve the importer name it shall *immediatly* throw an exception containing the missing importer's name. Do not return None in this case as it makes diagnostics harder than a well thought out error string. An importer is expected to have the following interface: ``` do_import() -> data get_abort_reason() -> string ``` The importer shall show it's own import dialog (which may well be just a file chooser!), perform the import and return the imported data in an - as of now - not yet specified format. If the import fails/is canceled do_import() shall not propagate any exception to the application, but instead return "None". The plugin shall expose the reason, why no data was returned when get_abort_reason is called. ### Instruments ``` get_instrument(instrument_name) -> InstrumentInstance get_instrument_proxy(instrument_name) -> InstrumentInstance ``` This hook shall return an instance of a given instrument. The returned instance has no specified interface. This hook is intended for use in tests only. If the plugin is unable to resolve the instrument name it shall *immediatly* throw an exception containing the missing instrument's name. Do not return None in this case as it makes diagnostics harder than a well thought out error string. ### General Purpose Functions General Purpose Functions are plugin functionality that needs to be available to tests, but that is not tied to a specific usage scenario. ``` get_general_purpose_function(func_name) -> FunctionInstance ``` Note that a "Function" in this context denotes an object that can have an arbitrary interface. The runtime environment will make an instance of the object available for each test program and test ### Tester ``` get_tester(tester_name, logger: Logger) -> TesterInstance ``` This hook shall return an instance of a tester with the given name. Tester will also be provided with a logger A tester is expected to have the following interface: ``` get_sites_count() -> int def do_request(self, site_id: int, timeout: int) -> bool def test_in_progress(self, site_id: int) def test_done(self, site_id: int, timeout: int) def setup(self) def teardown(self) ``` __note__: `get_tester` will also be used in the hwsetup in semi-ate-plugin to determine the number of sites supported with which the parallelism is configured. Therefore, prevent any interaction with the tester hardware while instantiating the tester (e.g in `__init__` function) and use the `setup()` and `teardown()` functions for that. __note__: `get_sites_count` shall not be implemented by the tester itself as it's already implemented in the base class (`TesterInterface`). Though, it's required that tester implementation declare the `SITE_COUNT` static class variable. (see example below) ```python class TesterInterface: SITE_COUNT = -1 def get_site_count(self) -> int: ... return self.SITE_COUNT class TestImpl(TesterInterface): SITE_COUNT = 1 # rest ... ``` __note__: if the declaration of `SITE_COUNT` is missing an exception will be raised --- get_tester_type(tester_name) This hook shall return an instance type of the concrete tester The type will be used to generate the code containing the tester type information which is necessary to provide the IDE with the type information needed to activate the autocompletion support __note__: both `get_tester` and `get_tester_type` functions have almost the same implementation but not the same output type, they return `the tester instance` and `the tester class` respectively. * from `the tester instance` we could operate on the tester (e.g setup the tester, etc...) * from `the tester class` we extract/collect some metadata too facilitate the development process (see above) ``` get_tester_master(tester_name) -> TesterInstance ``` This hook shall return an instance of a tester master type with the given name. A tester master is expected to have the following interface: ``` get_sites_count() -> int def get_site_states(self) -> list def release_test_execution(self, sites: list) def get_strategy_type(self) ``` ## Configuration Pluginobjects may provide configuration data in the form of key-value pairs. Each plugin shall provide a list of configuration options for a given object when the function: ``` get_configuration_options(object_name) -> List[String] ``` is called. It is up to the runtime to use the data (e.g. for creating a configuartion dialog). All objects shall support the function: ``` set_configuration_values(Dict[String, String]) ``` to apply a given set of configuration options. Each object shall be implemented such, that it is able to use the provided values on the fly. Note: The configuration values are plain strings and have no notion of types. ## Complete Hookspec ``` get_plugin_identification() -> plugin_ident get_importer_names() -> [] get_exporter_names() -> [] get_equipment_names() -> [] get_devicepin_importer_names() -> [] get_instrument_names() -> [] get_general_purpose_function_names() -> [] get_tester_names() -> [] get_importer(importer_name) -> Importer get_exporter(exporter_name) -> Exporter get_equipment(equipment_name) -> EquipmentInstance get_devicepin_importer(importer_name) -> Importer get_instrument(instrument_name, logger: Logger) -> InstrumentInstance get_instrument_proxy(required_capability) -> InstrumentProxy get_tester(tester_name) -> TesterInstance get_tester_type(tester_name) -> object get_tester_master(tester_name) -> TesterInstance get_general_purpose_function(func_name) -> FunctionInstance get_configuration_options(object_name) -> List[String]] ``` The hookspecmarker is "ate.org" ### As basic example plugin The example in this chapter containes the most basic plugin possible. __File__: PluginSrc\PluginB\\_\_init__.py ``` import pluggy from ATE.semiateplugins.hookspec import hookimpl class ThePlugin(object): @hookimpl def get_plugin_identification() -> dict: return {} @hookimpl def get_importer_names() -> []: return [] @hookimpl def get_exporter_names() -> []: return [] @hookimpl def get_equipment_names() -> []: return [] @hookimpl def get_devicepin_importer_names() -> []: return [] @hookimpl def get_instrument_names() -> []: return [] @hookimpl def get_tester_names() -> []: return [] @hookimpl def get_importer(importer_name) -> Importer: raise NotImplementedError @hookspec.ate.hookimpl def get_exporter(exporter_name) -> Exporter: raise NotImplementedError @hookimpl def get_equipment(equipment_name) -> EquipmentInstance: raise NotImplementedError @hookimpl def get_devicepin_importer(importer_name) -> Importer: raise NotImplementedError @hookimpl def get_instrument(instrument_name) -> InstrumentInstance: raise NotImplementedError @hookimpl def get_instrument_proxy(instrument_name) -> InstrumentInstance: raise NotImplementedError @hookimpl def get_tester(tester_name) -> TesterInstance: raise NotImplementedError @hookimpl def get_tester_type(tester_name) -> object raise NotImplementedError @hookimpl def get_tester_master(tester_name) -> TesterInstance: raise NotImplementedError @hookimpl def get_general_purpose_function(function_name) -> FunctionInstance: raise NotImplementedError @hookimpl def get_configuration_options(object_name) -> []: raise NotImplementedError ``` ### Plugin Installation Plugins are installed by means of setuptools (Pluggy supports plugin discovery through setuptools entrypoints). For this to work we need a setup.py for each plugin: __File__: PluginSrc\setup.py ``` from setuptools import setup setup( name="SomeAtePlugin", install_requires="ate.org", entry_points={"ate.org": ["plug = PluginB:ThePlugin"]}, py_modules=["PluginB"], ) ```