configurationengine/doc/plugins/dev-plugin/example-plugin.rst
changeset 0 2e8eeb919028
child 3 e7e0ae78773e
equal deleted inserted replaced
-1:000000000000 0:2e8eeb919028
       
     1 .. _plugin-howto-example-plugin:
       
     2 
       
     3 Example plug-in
       
     4 ===============
       
     5 
       
     6 The example plug-in implements a simple Implementation Markup Language that can write
       
     7 text files with possibly some content coming from ConfML setting values. The plug-in
       
     8 demonstrates some recommended practices for developing ConE plug-ins:
       
     9 
       
    10 - Plug-in structure:
       
    11     - Reader class
       
    12     - Implementation class
       
    13     - Implementation model
       
    14 - Using ``cone.public.utils`` for ConfML setting reference handling
       
    15 - Unit tests:
       
    16     - Testing the reader class, the implementation class and the model classes separately
       
    17     - Output generation testing (plug-in scope integration test)
       
    18 
       
    19 The ExampleML language
       
    20 ----------------------
       
    21 
       
    22 The Implementation Markup Language in the example plug-in is ExampleML. The language
       
    23 offers a simple mechanism to write text files to the output directory. For example:
       
    24 
       
    25 .. code-block :: xml
       
    26 
       
    27     <?xml version="1.0" encoding="UTF-8"?>
       
    28     <exampleml xmlns="http://www.example.org/xml/exampleml/1">
       
    29         <output file="test1.txt" encoding="UTF-8">Test</output>
       
    30         <output file="some/dir/test2.txt" encoding="UTF-16">Test</output>
       
    31     </exampleml>
       
    32 
       
    33 To demonstrate the use of ConfML setting references, the language supports also
       
    34 those with the form ``${Feature.Setting}``. This is the usual way of using them
       
    35 in implementation languages, and it is recommended to use the same convention
       
    36 in all ImplMLs. For example:
       
    37 
       
    38 .. code-block :: xml
       
    39 
       
    40     <?xml version="1.0" encoding="UTF-8"?>
       
    41     <exampleml xmlns="http://www.example.org/xml/exampleml/1">
       
    42         <output file="${SomeFeature.OutputDir}/test2.txt"
       
    43                 encoding="${SomeFeature.OutputEncoding}">
       
    44             Value from ConfML: ${SomeFeature.OutputText}
       
    45         </output>
       
    46     </exampleml>
       
    47 
       
    48 .. _plugin-howto-example-plugin-dir-structure:
       
    49 
       
    50 Directory structure
       
    51 -------------------
       
    52 
       
    53 - ``plugins/`` - Root directory for all ConE plug-in sources
       
    54     - ``example/`` - Example plug-in package directory
       
    55         - ``ConeExamplePlugin/`` - Source for the example plug-in
       
    56             - ``examplemlplugin/`` - Module directory containing all plug-in code
       
    57                 - ``tests/`` - Unit tests and test data for the plug-in
       
    58                     - ``project/`` - Configuration project used in the tests
       
    59                     - ``gen_expected/`` - Expected output for generation test case
       
    60                     - ``__init__.py`` - Test module initialization file
       
    61                     - ``runtests.py`` - Script for running all test cases
       
    62                     - ``unittest_exampleml_impl.py`` - File containing test cases
       
    63                     - ``unittest_exampleml_reader.py`` - File containing test cases
       
    64                     - ``unittest_exampleml_generation.py`` - File containing test cases
       
    65                 - ``__init__.py`` - Plug-in module initialization file
       
    66                 - ``exampleml_impl.py`` - Plug-in source file
       
    67                 - ``exampleml_reader.py`` - Plug-in source file
       
    68             - ``setup.py`` - Setup script for packaging the plug-in into an .egg file
       
    69             - ``setup.cfg`` - Configuration file for ``setup.py``
       
    70         - ``integration-test/`` - Integration tests for the example plug-in package
       
    71             - ``testdata/`` - Test data for the integration tests
       
    72             - ``__init__.py`` - Test module initialization file
       
    73             - ``runtests.py`` - Script for running all test cases
       
    74             - ``export_standalone.py`` - Script for exporting extra data for standalone test export
       
    75             - ``unittest_generate.py`` - File containing test cases
       
    76 
       
    77 Logical structure
       
    78 -----------------
       
    79 
       
    80 Logically the plug-in is divided into three parts:
       
    81 
       
    82 - *Implementation model*, represents the logical model of the implementation specified in the XML data
       
    83 - *Implementation class*, works as the interface of the plug-in towards ConE and uses the model to do the actual work
       
    84 - *Implementation reader*, converts the XML data into the logical model and creates a new implementation class instance
       
    85 
       
    86 In this case the *model* consists just of the class Output, which corresponds to the ``<output>`` element.
       
    87 
       
    88 Plug-in code
       
    89 ------------
       
    90 
       
    91 exampleml_model.py
       
    92 ..................
       
    93 
       
    94 This file defines the ``Output`` class, which comprises the whole implementation model
       
    95 in this case. The class contains the same attributes as its XML element counterpart:
       
    96 file, encoding and text, as well as the methods for generating output from the
       
    97 ``Output`` object.
       
    98 
       
    99 .. literalinclude:: /../source/plugins/example/ConeExamplePlugin/examplemlplugin/exampleml_model.py
       
   100    :linenos:
       
   101 
       
   102 Notice the use of ``cone.public.utils`` to handle the ConfML settings references. Usage of
       
   103 setting refs is common enough to warrant a set of functions related to their handling in ``utils``.
       
   104 It is strongly recommended to use these utility functions instead of creating your own.
       
   105 
       
   106 .. note::
       
   107 
       
   108     The expanding of ConfML setting references is done here, in the ``Output`` object, instead of in the reader
       
   109     when the implementation is parsed. If it was done in the parsing phase, ConfML setting values changed
       
   110     in rules would not be expanded to their new values.
       
   111 
       
   112 Another noteworthy thing is that the ``Output`` class implements the methods ``__eq__()``,
       
   113 ``__ne__()`` and ``__repr__()``. These have no real use in the actual implementation, but they
       
   114 make unit testing easier, as will be seen later on.
       
   115 
       
   116 The logic for creating the output is here encoded directly in the model, but in cases where
       
   117 the model is more complex, it may be necessary to create a separate writer class, particularly
       
   118 if there is more than one output format to be created based on the same model.
       
   119 
       
   120 exampleml_impl.py
       
   121 .................
       
   122 
       
   123 This file defines the implementation class. As can be seen, the class is quite simple, since
       
   124 it uses the model class to do the actual work and only works as an interface towards ConE.
       
   125 
       
   126 .. literalinclude:: /../source/plugins/example/ConeExamplePlugin/examplemlplugin/exampleml_impl.py
       
   127    :linenos:
       
   128 
       
   129 exampleml_reader.py
       
   130 ...................
       
   131 
       
   132 This file defines the reader class. Note how the reading of a single element is given its
       
   133 own method. Again, this is to make unit testing easier.
       
   134 
       
   135 .. literalinclude:: /../source/plugins/example/ConeExamplePlugin/examplemlplugin/exampleml_reader.py
       
   136    :linenos:
       
   137 
       
   138 
       
   139 Unit tests
       
   140 ----------
       
   141 
       
   142 Due to the dynamic nature of Python, an extensive set of unit tests is required for every plug-in.
       
   143 The unit tests for a ConE plug-in should be in a ``tests`` module (directory) under the plug-in's
       
   144 main module directory, and contain a set ``unittests_*.py`` files. The naming here is important,
       
   145 since the ``runtests.py`` file used to run all the unit tests at once collects test cases only
       
   146 from .py files starting with ``unittest_``.
       
   147 
       
   148 Unit tests can be executed by running each individual unit test file separately using "Run as" -> "Python unit-test" or
       
   149 all of the plug-in's unit tests at once by using "Run as" -> "Python run" on ``runtests.py``.
       
   150 
       
   151 .. image:: run_unittest.png
       
   152 
       
   153 *Running a single unit test file*
       
   154 
       
   155 It is recommended (well, actually required) to make sure that all unit tests pass before committing changes made to
       
   156 the code of any plug-in. Also, it should be checked that all of the plug-in's unit tests pass when run as part
       
   157 of all plug-in unit tests. It is possible that tests that pass when running ``runtests.py`` fail when running them
       
   158 as part of the whole plug-in test set, as in that case the working directory is not the same. The entire plug-in test
       
   159 set can be run from ``source/plugins/tests/runtests.py``.
       
   160 
       
   161 
       
   162 unittest_exampleml_reader.py
       
   163 ............................
       
   164 
       
   165 This file tests that the reader class functions properly. See how there is a test case for reading
       
   166 a single ``<output>`` element that tests all the special cases there, and then another that
       
   167 tests the top-level reader method. Here it becomes obvious why it is worthwhile to implement the
       
   168 ``__eq__()`` etc. methods in model classes, as in the tests we can just give the parser an XML
       
   169 string and specify exactly what kind of a Python object is expected to be parsed from it.
       
   170 
       
   171 .. literalinclude:: /../source/plugins/example/ConeExamplePlugin/examplemlplugin/tests/unittest_exampleml_reader.py
       
   172    :linenos:
       
   173 
       
   174 unittest_exampleml_impl.py
       
   175 ..........................
       
   176 
       
   177 This file tests that the implementation class works as expected. The methods to test are ``has_ref()``
       
   178 and ``list_output_files()``, since it is vital for the plug-in's correct operation that these methods
       
   179 do what they are supposed to. Note that ``generate()`` is not tested here, as it is tested in its own
       
   180 file.
       
   181 
       
   182 .. literalinclude:: /../source/plugins/example/ConeExamplePlugin/examplemlplugin/tests/unittest_exampleml_impl.py
       
   183    :linenos:
       
   184 
       
   185 unittest_exampleml_generation.py
       
   186 ................................
       
   187 
       
   188 This file tests that the plug-in works correctly throughout its lifecycle, so it works as an integration test.
       
   189 Note that plug-in instances are not created manually, but an implementation container is created from the project.
       
   190 This means that the test also makes sure that the plug-in interoperates correctly with ConE's plug-in machinery.
       
   191 
       
   192 Also note that the output is checked against an expected set of files using the method ``assert_dir_contents_equal()``,
       
   193 which comes from a unit test base class defined in a special ``testautomation`` module. This module contains also other
       
   194 helper methods for use in unit tests, so if you need something more sophisticated than the simple methods provided
       
   195 by the ``unittest`` module, you should check the ``testautomation`` module before writing the methods yourself.
       
   196 The module can be found under ``source/``.
       
   197 
       
   198 Notice also how the generation output directory is set to be in a ``temp/`` directory in the same directory
       
   199 as the test .py file is. It is recommended to keep all temporary test data in a single place like this, so that
       
   200 they don't litter e.g. the current working directory. When using a single ``temp/`` directory, it can also be
       
   201 ignored in version control to avoid unnecessarily listing the temporary data when checking for modification in
       
   202 the workding copy.
       
   203 
       
   204 .. literalinclude:: /../source/plugins/example/ConeExamplePlugin/examplemlplugin/tests/unittest_exampleml_generation.py
       
   205    :linenos:
       
   206 
       
   207 Plug-in packaging
       
   208 -----------------
       
   209 
       
   210 The file ``setup.py`` handles the packaging of the plug-in into an egg file.
       
   211 
       
   212 The most important thing here is the plug-in's entry point info. The
       
   213 plug-in's reader classes must be specified as entry points, or they won't be
       
   214 loaded.
       
   215 
       
   216 .. literalinclude:: /../source/plugins/example/ConeExamplePlugin/setup.py
       
   217    :linenos:
       
   218 
       
   219 
       
   220 .. _plugin-howto-example-plugin-integration-tests:
       
   221 
       
   222 Integration tests
       
   223 -----------------
       
   224 
       
   225 In addition to the unit tests inside the plug-in itself there is a separate integration test set.
       
   226 The purpose of these tests is to make sure that the plug-ins in the package work properly
       
   227 together with other implementations from the CLI level. E.g. a common case that is good to
       
   228 test is to check that ConfML settings changed in rules affect the implementations using
       
   229 references to those settings work properly.
       
   230 
       
   231 These tests are also exported as part of the standalone test set used to test a pre-built
       
   232 ConE distribution (see :ref:`installation-export-tests`). This affects the way some things
       
   233 are handled in the test cases, for example the way the command to run is determined.
       
   234 
       
   235 The integration test set is plug-in package specific, not plug-in specific, so the test
       
   236 project(s) used there should contain implementations of all the implementation languages
       
   237 provided by the plug-ins in the package. Of course, in this case there are only ExampleML
       
   238 implementations, since the example plug-in is the only plug-in in the example package.
       
   239 
       
   240 runtests.py
       
   241 ...........
       
   242 
       
   243 This file simply acts as a shortcut to run all test cases easily.
       
   244 
       
   245 __init__.py
       
   246 ...........
       
   247 
       
   248 This file performs all integration test specific initialization using the plug-in utility
       
   249 functions in the root ``plugins/`` directory. Note that when the integration test set is
       
   250 exported as standalone, the contents of this file are erased (the integration test
       
   251 initialization cannot be done there, since the full ConE source is not available). Because
       
   252 of this, you should not put anything that is always needed in this file.
       
   253 
       
   254 export_standalone.py
       
   255 ....................
       
   256 
       
   257 This file contains a function for exporting any needed extra data into the standalone test
       
   258 set (e.g. something from under the plug-in sources). The file doesn't necessarily need to
       
   259 exist if there is no extra data in need of exporting, but in this example it exists to show
       
   260 what could be done in it.
       
   261 
       
   262 .. literalinclude:: /../source/plugins/example/integration-test/export_standalone.py
       
   263    :linenos:
       
   264 
       
   265 unittest_generate.py
       
   266 ....................
       
   267 
       
   268 This file contains tests for generating output using the example plug-in.
       
   269 Note the following things:
       
   270 
       
   271 - The use of the variable ``CONE_CMD`` in ``get_cmd()``. This variable is set to
       
   272   contain the actual ConE command to run if the tests are being run from the
       
   273   exported standalone test set. In practice this will be something like
       
   274   ``C:/cone_test/cone/cone.cmd``.
       
   275 - The actual generation and testing is done in a separate function, ``run_test_generate()``,
       
   276   and there are two actual test functions that call it. One runs the test directly on the
       
   277   test project on the file system, and another first zips the test project and then runs
       
   278   the test on that. It is a good idea to test that generation works the same in both cases,
       
   279   since it can be easy to forget to take into account generation from a ZIP file when creating
       
   280   a plug-in (e.g. using ``shutil`` functions to perform copy operations when the ConE API
       
   281   should be used).
       
   282 
       
   283 .. literalinclude:: /../source/plugins/example/integration-test/unittest_generate.py
       
   284    :linenos: