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