1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
|
# FWCore/TestProcessor Documentation
## Introduction
The `TestProcessor` class is used to test individual CMSSW framework modules. The class allows one to
* Specify a string containing the python configuration of the module and
* Pass data from the test to the module through the edm::Event and/or the edm::EventSetup
The system is composed of three parts
1. A python `TestProcess` class
1. A C++ configuration class
1. The C++ `TestProcessor` class
## Python `TestProcess` class
The `TestProcess` class has all the same attributes as the standard `cms.Process` class except
* If a process name is not give, it will default to `"TEST"`
* `cms.Path` and `cms.EndPath` are ignored
* the method `moduleToTest` was added and is used to specify which module is to be called during the test. This method also accepts a `cms.Task` which is used to run any other modules that may be needed.
As stated above, additional framework modules and EventSetup modules and Services are allowed to be specified in the configuration. If no EventSetup modules are specified in the `cms.Task` passed to `moduleToTest` than all EventSetup modules loaded into `TestProcess` will be created for the test. The same holds for Services.
### Example 1: Setup only the module
```python
from FWCore.TestProcessor.TestProcess import *
process = TestProcess()
process.foo = cms.EDProducer('FooProd')
process.moduleToTest(process.foo)
```
### Example 2: Use additional modules
```python
from FWCore.TestProcessor.TestProcess import *
process = TestProcess()
process.load("somePackage.someModules_cff")
process.foo = cms.EDProducer('FooProd')
process.moduleToTest(process.foo, cms.Task(process.bar))
```
NOTE: We recommend testing modules completely in isolation, however we realize, for some cases, that is not practical.
## C++ Configuration Class
The configuration class `edm::test::TestProcessor::Config` is used to
* hold a C++ string containing the python configuration using `TestProcess`
* register Event data products that the test may be using
* register EventSetup data products that th etest may be using
* register additional process names which simulates reading data from a file
The configuration class is separate from the `edm::test::TestProcessor` class since once the latter is setup, one can not change its behavior (i.e. you can not add additional event data products to produce).
The same configuration class can be reused to setup multiple `edm::test::TestProcessor` instances.
### Example 1: Use only the python configuration
We recommend using a C++ raw string for the python configuration. That way line breaks and quotation marks are automatically handled.
```cpp
edm::test::TestProcessor::Config config{
R"_(from FWCore.TestProcessor.TestProcess import *
process = TestProcess()
process.foo = cms.EDProducer("FooProd")
process.moduleToTest(process.foo)
)_"
};
```
### Example 2: Adding Event data products
You must register which Event data products you intend to pass to the test. A `edm::EDPutTokenT<>` object is returned from the registration call. This object must be passed to the test along with an `std::unique_ptr<>` containing the object. Multiple Event data products are allowed.
```cpp
edm::test::TestProcessor::Config config{
R"_(...
)_"
};
//Uses the module label 'bar'
auto barPutToken = config.produces<std::vector<Bar>>("bar");
```
### Example 3: Adding Event data products from an earlier Process
To add an addition process to the test, one must call the `addExtraProcess` method. This method returns a `edm::test::ProcessToken`. This token is then passed to the `produces` call. If a `edm::test::ProcessToken` is not passed, the data product is set to come from the most _recent_ process.
One can specify multiple extra processes. The order of the calls determines the order of the process histories. With the first call creating the oldest process.
```cpp
auto hltProcess = config.addExtraProcess("HLT");
auto barPutToken = config.produces<std::vector<Bar>>("bar","",hltProcess);
```
###Example 4: Adding EventSetup data products
You must register which EventSetup data products you intend to pass to the test as well as the EventSetup Record from which the data product can be obtained. A `edm::test::ESPutTokenT<>` object is returned from the registration call. This object must be passed to the test along with an `std::unique_ptr<>` containing the object. Multiple EventSetup data products are allowed.
```cpp
auto esPutToken = config.esProduces<FooData,FooRecord>();
```
## `TestProcessor` class
`edm::test::TestProcessor` does the work of running the tests via its `test()` method. An instance of the class is constructed by passing it an instance of `edm::test::TestProcessor::Config`. In the constructor, the class will load all the framework modules needed by the python configuration as well as setup all internal details needed to run CMSSW framework transitions.
The first call to `test()` will cause the _beginJob_, _beginStream_, _globalBeginRun_, _streamBeginRun_, _globalBeginLuminosityBlock_ and _streamBeginLuminosityBlock_ transitions of the modules to be called in addition to the _event_ transition. Subsequent calls to `test()` will only generate new _event_ transitions.
Calling `setRunNumber()` or `setLuminosityBlockNumber()` will trigger the appropriate _begin_ transition calls and will be preceeded by the appropriate _end_ transition calls if a call to `test()` was already made.
The `test()` method accepts an unlimited number of arguments of the type `std::pair<edm::EDPutTokenT<T>,std::unique_ptr<T>>` and `std::pair<edm::test::ESPutTokenT<T>, std::unique_ptr<T>`. Each call to `test()` will also generate a new EventSetup IOV for the Records declared during the EventSetup data product registration calls to `edm::test::TestProcessor::Config`. This allows each call to `test()` to update the EventSetup data product to be used.
### `edm::test::Event`
The return value of `test()` is an `edm::test::Event`. The `Event` class gives access to any data products created by the module being tested via a call to `get<T>()`. Since the module label and the process name are already known by the test, `get<T>()` takes one optional argument which is the _productInstanceLabel_ for the data product. The call to `get<T>()` returns an `edm::test::TestHandle<T>` which acts like a smart pointer to the data product if the product was retrieved. If the data product was not retrieved, attempting to access the data will cause an exception.
The `edm::test::Event` also has the method `modulePassed()` which is only useful when testing an `EDFilter`. In that case, the method returns `true` if the module passed the Event and `false` otherwise.
### Run and LuminosityBlock product testing
It is possible to also test Run and LuminosityBlock products created by the module. This can be accomplished by calling
* `testBeginRun(edm::RunNumber_t)`
* `testEndRun()`
* `testBeginLuminosityBlock(edm::LuminosityBlockNumber_t)`
* `testEndLuminosityBlock()`
Like `test()` one can pass an unlimited number of arguments of type `std::pair<edm::test::ESPutTokenT<T>, std::unique_ptr<T>` in order to control EventSetup data passed to the module (`EDPutToken` are not supported at this time). Each of the above `test` functions also return either `edm::test::Run` or `edm::test::LuminosityBlock` which has the same interface as `edm::test::Event` except they do not have the method `modulePassed()` because filtering does not occur on those transitions.
The `test` methods all make sure all the needed transitions occur in the necessary order. For example, calling `test()` and then `testEndRun()` will cause _streamEndLuminosityBlock_ and _globalEndLuminosityBlock_ to occur.
When calling `testBeginRun(edm:RunNumber_t)` or `testBeginLuminosityBlock(edm::LuminosityBlockNumber_t)` one is required to pass in a Run or LuminosityBlock number which is different from the number previously used by any earlier calls to a `test` function.
### Full Example
```cpp
#include "FWCore/TestProcessor/interface/TestProcessor.h"
int main() {
//The python configuration
edm::test::TestProcessor::Config config{
R"_(from FWCore.TestProcessor.TestProcess import *
process = TestProcess()
process.foo = cms.EDProducer("FooProd")
process.moduleToTest(process.foo)
)_"
};
//setup data to pass
auto barPutToken = config.produces<std::vector<Bar>>("bar");
auto esPutToken = config.esProduces<FooData,FooRecord>();
edm::test::TestProcessor tester{ config };
//Run a test
auto event = tester.test(std::make_pair(barPutToken,
std::make_unique<std::vector<Bar>(...)),
std::make_pair(esPutToken,
std::make_unique<FooData>(...)));
auto nMade = event.get<std::vector<Foo>>()->size();
std::cout << nMade <<std::endl;
if( nMade != ...) {
return 1;
}
return 0;
};
```
### Example using Catch2
[Catch2](https://github.com/catchorg/Catch2/blob/master/docs/Readme.md#top) is a simple to use C++ unit testing framemwork. It can be used in conjuction with `TestProcessor` to drive a series of tests. In addition to the code, be sure to add `<use name="catch2"/>` to the `BuildFile.xml`.
```cpp
#include "FWCore/TestProcessor/interface/TestProcessor.h"
...
#include "catch.hpp"
TEST_CASE("FooProd tests", "[FooProd]") {
//The python configuration
edm::test::TestProcessor::Config config{
R"_(from FWCore.TestProcessor.TestProcess import *
process = TestProcess()
process.foo = cms.EDProducer("FooProd")
process.moduleToTest(process.foo)
)_"
};
//setup data to pass
auto barPutToken = config.produces<std::vector<Bar>>("bar");
auto esPutToken = config.esProduces<FooData,FooRecord>();
SECTION("Pass standard data") {
edm::test::TestProcessor tester{ config };
//Run a test
auto event = tester.test(std::make_pair(barPutToken,
std::make_unique<std::vector<Bar>(...)),
std::make_pair(esPutToken,
std::make_unique<FooData>(...)));
auto const& foos = event.get<std::vector<Foo>>();
REQUIRE(foos->size() == ...);
REQUIRE(foos[0] == Foo(...));
...
SECTION("Move to new IOV") {
tester.setRunNumber(2);
auto event = tester.test(std::make_pair(barPutToken,
std::make_unique<std::vector<Bar>(...)),
std::make_pair(esPutToken,
std::make_unique<FooData>(...)));
auto const& foos = event.get<std::vector<Foo>>();
REQUIRE(foos->size() == ...);
REQUIRE(foos[0] == Foo(...));
...
};
};
SECTION("Missing event data") {
edm::test::TestProcessor tester{ config };
REQUIRE_THROWS_AS(tester.test(std::make_pair(esPutToken,
std::make_unique<FooData>(...))),
cms::Exception);
};
}
```
## Autogenerating Tests
Tests for new modules are automatically created when using `mkedprod`, `mkedfltr` or `mkedanlzr`. The same commands can be used to generate tests for existing modules just by running those commands from within the `test` directory of the package containing the module. For this case, you will need to manually add the following to `test/BuildFile.xml`:
```xml
<bin file="test_catch2_*.cc" name="test<SubSystem name><Package Name>TP">
<use name="FWCore/TestProcessor"/>
<use name="catch2"/>
</bin>
```
## Tips
### Testing different module configuration parameters
It will often be the case that a group of tests only differ based on the values of parameters used to configure a module. For this case, one would like to have a _base configuration_ which is used as the starting point for each of tests. Rather than using string manipulation to create each new configuration, we can make use of the fact we will be running a python interpreter on each configuration.
The _base configuration_ is a full configuration where, instead of setting specific values for each parameter that needs to be varied, we use a python variable name, where the variable is not declared in the _base configuration_.
```cpp
const std::string baseConfig =
R"_(from FWCore.TestProcessor.TestProcess import *
process = TestProcess()
process.foo = cms.EDProducer('FooProd', value = cms.int32( fooValue ))
process.moduleToTest(process.foo)
)_";
```
To generate a specific configuration, we just prepend to the `baseConfig` a string containing an expressiong which sets the values on the variable names
```cpp
std::string fullConfig = "fooValue = 3\n"+baseConfig;
```
Alternatively, you can setup default values in the base configuration
```cpp
const std::string baseConfig =
R"_(from FWCore.TestProcessor.TestProcess import *
process = TestProcess()
process.foo = cms.EDProducer('FooProd', value = cms.int32( 1 ))
process.moduleToTest(process.foo)
)_";
```
And then append a string at the end which sets the particular value
```cpp
std::string fullConfig = baseConfig +
R"_(process.foo.value = 3
)_"
```
|