|
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: |