LCOV - code coverage report
Current view: top level - model - Transformation.cpp (source / functions) Coverage Total Hit
Test: lcov.info Lines: 93.8 % 434 407
Test Date: 2026-03-07 03:59:50 Functions: 100.0 % 3 3

            Line data    Source code
       1              : /*
       2              :  ___________________________________________
       3              : |    _     ___                        _     |
       4              : |   | |   |__ \                      | |    |
       5              : |   | |__    ) |__ _  __ _  ___ _ __ | |_   |
       6              : |   | '_ \  / // _` |/ _` |/ _ \ '_ \| __|  |  HTTP/2 AGENT FOR MOCK TESTING
       7              : |   | | | |/ /| (_| | (_| |  __/ | | | |_   |  Version 0.0.z
       8              : |   |_| |_|____\__,_|\__, |\___|_| |_|\__|  |  https://github.com/testillano/h2agent
       9              : |                     __/ |                 |
      10              : |                    |___/                  |
      11              : |___________________________________________|
      12              : 
      13              : Licensed under the MIT License <http://opensource.org/licenses/MIT>.
      14              : SPDX-License-Identifier: MIT
      15              : Copyright (c) 2021 Eduardo Ramos
      16              : 
      17              : Permission is hereby  granted, free of charge, to any  person obtaining a copy
      18              : of this software and associated  documentation files (the "Software"), to deal
      19              : in the Software  without restriction, including without  limitation the rights
      20              : to  use, copy,  modify, merge,  publish, distribute,  sublicense, and/or  sell
      21              : copies  of  the Software,  and  to  permit persons  to  whom  the Software  is
      22              : furnished to do so, subject to the following conditions:
      23              : 
      24              : The above copyright notice and this permission notice shall be included in all
      25              : copies or substantial portions of the Software.
      26              : 
      27              : THE SOFTWARE  IS PROVIDED "AS  IS", WITHOUT WARRANTY  OF ANY KIND,  EXPRESS OR
      28              : IMPLIED,  INCLUDING BUT  NOT  LIMITED TO  THE  WARRANTIES OF  MERCHANTABILITY,
      29              : FITNESS FOR  A PARTICULAR PURPOSE AND  NONINFRINGEMENT. IN NO EVENT  SHALL THE
      30              : AUTHORS  OR COPYRIGHT  HOLDERS  BE  LIABLE FOR  ANY  CLAIM,  DAMAGES OR  OTHER
      31              : LIABILITY, WHETHER IN AN ACTION OF  CONTRACT, TORT OR OTHERWISE, ARISING FROM,
      32              : OUT OF OR IN CONNECTION WITH THE SOFTWARE  OR THE USE OR OTHER DEALINGS IN THE
      33              : SOFTWARE.
      34              : */
      35              : 
      36              : 
      37              : #include <ert/tracing/Logger.hpp>
      38              : 
      39              : #include <Transformation.hpp>
      40              : #include <functions.hpp>
      41              : 
      42              : #include <sstream>
      43              : #include <algorithm>
      44              : 
      45              : namespace h2agent
      46              : {
      47              : namespace model
      48              : {
      49              : 
      50          595 : void Transformation::collectVariablePatterns(const std::string &str, std::map<std::string, std::string> &patterns) {
      51              : 
      52          595 :     static std::regex re("@\\{[^\\{\\}]*\\}", std::regex::optimize); // @{[^{}]*} with curly braces escaped
      53              :     // or: R"(@\{[^\{\}]*\})"
      54              : 
      55          595 :     std::string::const_iterator it(str.cbegin());
      56          595 :     std::smatch matches;
      57          595 :     std::string pattern;
      58          595 :     patterns.clear();
      59          608 :     while (std::regex_search(it, str.cend(), matches, re)) {
      60           13 :         it = matches.suffix().first;
      61           13 :         pattern = matches[0];
      62           13 :         patterns[pattern] = pattern.substr(2, pattern.size()-3); // @{foo} -> foo
      63              :     }
      64          595 : }
      65              : 
      66          197 : bool Transformation::load(const nlohmann::json &j) {
      67              : 
      68          197 :     bool collectFilterPatterns = false;
      69              : 
      70              :     // Mandatory
      71          197 :     auto source_it = j.find("source");
      72          197 :     std::string sourceSpec = *source_it;
      73              : 
      74          197 :     auto target_it = j.find("target");
      75          197 :     std::string targetSpec = *target_it;
      76              : 
      77              :     // Optional
      78          197 :     auto it = j.find("filter");
      79          197 :     filter_ = "";
      80          197 :     has_filter_ = false;
      81          197 :     if (it != j.end()) {
      82           35 :         has_filter_ = true;
      83              : 
      84              :         // [filter_type_]
      85              :         //   RegexCapture        regular expression literal -> [filter_rgx_]
      86              :         //   RegexReplace        regular expression literal (rgx) -> [filter_rgx_]  /  replace format (fmt) -> [filter_]
      87              :         //   Append              suffix value -> [filter_]
      88              :         //   Prepend             prefix value -> [filter_]
      89              :         //   Sum                 amount -> [filter_i_/filter_u_/filter_f_/filter_number_type_]
      90              :         //   Multiply            amount -> [filter_i_/filter_u_/filter_f_/filter_number_type_]
      91              :         //   ConditionVar        variable name -> [filter_]
      92              :         //   EqualTo             value -> [filter_]
      93              :         //   DifferentFrom       value -> [filter_]
      94              :         //   JsonConstraint      value -> [filter_object_]
      95              :         //   SchemaId            value -> [filter_]
      96              : 
      97           35 :         auto f_it = it->find("RegexCapture");
      98              : 
      99              :         try {
     100           35 :             if (f_it != it->end()) {
     101            6 :                 filter_ = *f_it;
     102            6 :                 filter_rgx_.assign(filter_, std::regex::optimize);
     103            4 :                 filter_type_ = FilterType::RegexCapture;
     104              :             }
     105           29 :             else if ((f_it = it->find("RegexReplace")) != it->end()) {
     106            2 :                 filter_rgx_.assign(std::string(*(f_it->find("rgx"))), std::regex::optimize);
     107            2 :                 filter_ = *(f_it->find("fmt"));
     108            2 :                 filter_type_ = FilterType::RegexReplace;
     109              :             }
     110           27 :             else if ((f_it = it->find("Append")) != it->end()) {
     111            2 :                 filter_ = *f_it;
     112            2 :                 filter_type_ = FilterType::Append;
     113            2 :                 collectFilterPatterns = true;
     114              :             }
     115           25 :             else if ((f_it = it->find("Prepend")) != it->end()) {
     116            2 :                 filter_ = *f_it;
     117            2 :                 filter_type_ = FilterType::Prepend;
     118            2 :                 collectFilterPatterns = true;
     119              :             }
     120           23 :             else if ((f_it = it->find("Sum")) != it->end()) {
     121            5 :                 if (f_it->is_number_unsigned()) { // first unsigned, because positive would be integer
     122            3 :                     filter_u_ = *f_it;
     123            3 :                     filter_number_type_ = 1 ;
     124              :                 }
     125            2 :                 else if (f_it->is_number_integer()) {
     126            1 :                     filter_i_ = *f_it;
     127            1 :                     filter_number_type_ = 0 ;
     128              :                 }
     129            1 :                 else if (f_it->is_number_float()) {
     130            1 :                     filter_f_ = *f_it;
     131            1 :                     filter_number_type_ = 2 ;
     132              :                 }
     133            5 :                 filter_type_ = FilterType::Sum;
     134              :             }
     135           18 :             else if ((f_it = it->find("Multiply")) != it->end()) {
     136            3 :                 if (f_it->is_number_unsigned()) { // first unsigned, because positive would be integer
     137            1 :                     filter_u_ = *f_it;
     138            1 :                     filter_number_type_ = 1 ;
     139              :                 }
     140            2 :                 else if (f_it->is_number_integer()) {
     141            1 :                     filter_i_ = *f_it;
     142            1 :                     filter_number_type_ = 0 ;
     143              :                 }
     144            1 :                 else if (f_it->is_number_float()) {
     145            1 :                     filter_f_ = *f_it;
     146            1 :                     filter_number_type_ = 2 ;
     147              :                 }
     148            3 :                 filter_type_ = FilterType::Multiply;
     149              :             }
     150           15 :             else if ((f_it = it->find("ConditionVar")) != it->end()) {
     151            4 :                 filter_ = *f_it;
     152            4 :                 filter_type_ = FilterType::ConditionVar;
     153              :             }
     154           11 :             else if ((f_it = it->find("EqualTo")) != it->end()) {
     155            3 :                 filter_ = *f_it;
     156            3 :                 filter_type_ = FilterType::EqualTo;
     157            3 :                 collectFilterPatterns = true;
     158              :             }
     159            8 :             else if ((f_it = it->find("DifferentFrom")) != it->end()) {
     160            3 :                 filter_ = *f_it;
     161            3 :                 filter_type_ = FilterType::DifferentFrom;
     162            3 :                 collectFilterPatterns = true;
     163              :             }
     164            5 :             else if ((f_it = it->find("JsonConstraint")) != it->end()) {
     165            3 :                 filter_object_ = *f_it;
     166            3 :                 filter_type_ = FilterType::JsonConstraint;
     167              :             }
     168            2 :             else if ((f_it = it->find("SchemaId")) != it->end()) {
     169            2 :                 filter_ = *f_it;
     170            2 :                 filter_type_ = FilterType::SchemaId;
     171              :             }
     172              :         }
     173            2 :         catch (std::regex_error &e) {
     174            2 :             ert::tracing::Logger::error(e.what(), ERT_FILE_LOCATION);
     175            2 :             return false;
     176            2 :         }
     177              :     }
     178              : 
     179              :     // Interpret source/target:
     180              : 
     181              :     // SOURCE (enum SourceType { RequestUri = 0, RequestUriPath, RequestUriParam, RequestBody, ResponseBody, RequestHeader, Eraser,
     182              :     //                           Math, Random, RandomSet, Timestamp, Strftime, Recvseq, SVar, SGVar, Value, ServerEvent, InState };)
     183          195 :     source_ = ""; // empty by default (-), as many cases are only work modes and no parameters(+) are included in their transformation configuration
     184              : 
     185              :     // Source specifications:
     186              :     // - request.uri: whole `url-decoded` request *URI* (path together with possible query parameters). This is the unmodified original *URI*, not necessarily the same as the classification *URI*.
     187              :     // - request.uri.path: `url-decoded` request *URI* path part.
     188              :     // + request.uri.param.<name>: request URI specific parameter `<name>`.
     189              :     // - request.body: request body document.
     190              :     // + request.body./<node1>/../<nodeN>: request body node path.
     191              :     // + request.header.<hname>: request header component (i.e. *content-type*).
     192              :     // - eraser: this is used to indicate that the *target* specified (next section) must be removed or reset.
     193              :     // + math.`<expression>`: this source is based in Arash Partow's exprtk math library compilation.
     194              :     // + random.<min>.<max>: integer number in range `[min, max]`. Negatives allowed, i.e.: `"-3.+4"`.
     195              :     // + timestamp.<unit>: UNIX epoch time in `s` (seconds), `ms` (milliseconds), `us` (microseconds) or `ns` (nanoseconds).
     196              :     // + strftime.<format>: current date/time formatted by [strftime](https://www.cplusplus.com/reference/ctime/strftime/).
     197              :     // - recvseq: sequence id increased for every mock reception (starts on *1* when the *h2agent* is started).
     198              :     // + var.<id>: general purpose variable.
     199              :     // + globalVar.<id>: general purpose global variable.
     200              :     // - value.<value>: free string value. Even convertible types are allowed, for example: integer string, unsigned integer string, float number string, boolean string (true if non-empty string), will be converted to the target type.
     201              :     // - inState: current processing state.
     202              :     // + serverEvent.`<server event address in query parameters format>`: access server context indexed by request *method* (`requestMethod`), *URI* (`requestUri`), events *number* (`eventNumber`) and events number *path* (`eventPath`).
     203              :     // + txtFile.`<path>`: reads text content from file with the path provided.
     204              :     // + binFile.`<path>`: reads binary content from file with the path provided.
     205              :     // + command.`<command>`: executes command on process shell and captures the standard output.
     206              : 
     207              :     // Regex needed:
     208          195 :     static std::regex requestUriParam("^request.uri.param.(.*)", std::regex::optimize); // no need to escape dots as this is validated in schema
     209          195 :     static std::regex requestBodyNode("^request.body.(.*)", std::regex::optimize);
     210          195 :     static std::regex responseBodyNode("^response.body.(.*)", std::regex::optimize);
     211          195 :     static std::regex requestHeader("^request.header.(.*)", std::regex::optimize);
     212          195 :     static std::regex math("^math.(.*)", std::regex::optimize);
     213          195 :     static std::regex random("^random\\.([-+]{0,1}[0-9]+)\\.([-+]{0,1}[0-9]+)$", std::regex::optimize); // no need to validate min/max as it was done at schema
     214          195 :     static std::regex randomSet("^randomset.(.*)", std::regex::optimize);
     215          195 :     static std::regex timestamp("^timestamp.(.*)", std::regex::optimize); // no need to validate s/ms/us/ns as it was done at schema
     216          195 :     static std::regex strftime("^strftime.(.*)", std::regex::optimize); // free format, errors captured
     217          195 :     static std::regex varId("^var.(.*)", std::regex::optimize);
     218          195 :     static std::regex gvarId("^globalVar.(.*)", std::regex::optimize);
     219          195 :     static std::regex value("^value.([.\\s\\S]*)", std::regex::optimize); // added support for special characters: \n \t \r
     220          195 :     static std::regex serverEvent("^serverEvent.(.*)", std::regex::optimize);
     221          195 :     static std::regex clientEvent("^clientEvent.(.*)", std::regex::optimize);
     222          195 :     static std::regex responseHeader_source("^response.header.(.*)", std::regex::optimize);
     223          195 :     static std::regex txtFile("^txtFile.(.*)", std::regex::optimize);
     224          195 :     static std::regex binFile("^binFile.(.*)", std::regex::optimize);
     225          195 :     static std::regex command("^command.(.*)", std::regex::optimize);
     226              : 
     227          195 :     std::smatch matches; // to capture regex group(s)
     228              :     // BE CAREFUL!: https://stackoverflow.com/a/51709911/2576671
     229              :     // In this case, it is not a problem, as we store the match from sourceSpec or targetSpec before changing them.
     230              : 
     231              :     // no need to try (controlled regex)
     232              :     //try {
     233          195 :     if (sourceSpec == "request.uri") {
     234            5 :         source_type_ = SourceType::RequestUri;
     235              :     }
     236          190 :     else if (sourceSpec == "request.uri.path") {
     237            3 :         source_type_ = SourceType::RequestUriPath;
     238              :     }
     239          187 :     else if (std::regex_match(sourceSpec, matches, requestUriParam)) { // parameter name
     240            2 :         source_ = matches.str(1);
     241            2 :         source_type_ = SourceType::RequestUriParam;
     242              :     }
     243          185 :     else if (sourceSpec == "request.body") { // whole document
     244           13 :         source_type_ = SourceType::RequestBody;
     245              :     }
     246          172 :     else if (std::regex_match(sourceSpec, matches, requestBodyNode)) { // nlohmann::json_pointer path
     247            4 :         source_ = matches.str(1);
     248            4 :         source_type_ = SourceType::RequestBody;
     249              :     }
     250          168 :     else if (sourceSpec == "response.body") { // whole document
     251            2 :         source_type_ = SourceType::ResponseBody;
     252              :     }
     253          166 :     else if (std::regex_match(sourceSpec, matches, responseBodyNode)) { // nlohmann::json_pointer path
     254            2 :         source_ = matches.str(1);
     255            2 :         source_type_ = SourceType::ResponseBody;
     256              :     }
     257          164 :     else if (std::regex_match(sourceSpec, matches, requestHeader)) { // header name
     258            2 :         source_ = matches.str(1);
     259            2 :         source_type_ = SourceType::RequestHeader;
     260              :     }
     261          162 :     else if (sourceSpec == "eraser") {
     262           13 :         source_type_ = SourceType::Eraser;
     263              :     }
     264          149 :     else if (std::regex_match(sourceSpec, matches, math)) { // math expression, i.e. "2*sqrt(2)"
     265            4 :         source_ = matches.str(1);
     266            4 :         source_type_ = SourceType::Math;
     267              :     }
     268          145 :     else if (std::regex_match(sourceSpec, matches, random)) { // range "<min>.<max>", i.e.: "-3.8", "0.100", "-15.+2", etc. These go to -> [source_i1_] and [source_i2_]
     269            8 :         source_i1_ = stoi(matches.str(1));
     270            8 :         source_i2_ = stoi(matches.str(2));
     271            8 :         source_type_ = SourceType::Random;
     272              :     }
     273          137 :     else if (std::regex_match(sourceSpec, matches, randomSet)) { // random set given by tokenized pipe-separated list of values
     274            1 :         source_ = matches.str(1) + "|"; // add pipe, just to allow getting empty part after trailing pipe ("foo|" => 'foo' ''), because the algorithm below ignores latest pipe and its trailing token.
     275            1 :         static std::regex pipedRgx(R"(\|)", std::regex::optimize);
     276            2 :         source_tokenized_ = std::vector<std::string>(
     277            4 :                                 std::sregex_token_iterator{begin(source_), end(source_), pipedRgx, -1},
     278            2 :                                 std::sregex_token_iterator{}
     279            1 :                             );
     280            1 :         source_type_ = SourceType::RandomSet;
     281            1 :         source_.pop_back(); // remove added pipe
     282              :     }
     283          136 :     else if (std::regex_match(sourceSpec, matches, timestamp)) { // unit (s: seconds, ms: milliseconds, us: microseconds, ns: nanoseconds)
     284            5 :         source_ = matches.str(1);
     285            5 :         source_type_ = SourceType::Timestamp;
     286              :     }
     287          131 :     else if (std::regex_match(sourceSpec, matches, strftime)) { // current date/time formatted by as described in https://www.cplusplus.com/reference/ctime/strftime/
     288            1 :         source_ = matches.str(1);
     289            1 :         source_type_ = SourceType::Strftime;
     290              :     }
     291          130 :     else if (sourceSpec == "recvseq") {
     292            2 :         source_type_ = SourceType::Recvseq;
     293              :     }
     294          128 :     else if (sourceSpec == "sendseq") {
     295            0 :         source_type_ = SourceType::Sendseq;
     296              :     }
     297          128 :     else if (sourceSpec == "seq") {
     298            0 :         source_type_ = SourceType::Seq;
     299              :     }
     300          128 :     else if (std::regex_match(sourceSpec, matches, varId)) { // variable id
     301           19 :         source_ = matches.str(1);
     302           19 :         source_type_ = SourceType::SVar;
     303              :     }
     304          109 :     else if (std::regex_match(sourceSpec, matches, gvarId)) { // global variable id
     305           12 :         source_ = matches.str(1);
     306           12 :         source_type_ = SourceType::SGVar;
     307              :     }
     308           97 :     else if (std::regex_match(sourceSpec, matches, value)) { // value content
     309           73 :         source_ = matches.str(1);
     310           73 :         source_type_ = SourceType::Value;
     311              :     }
     312           24 :     else if (std::regex_match(sourceSpec, matches, serverEvent)) { // value content
     313            5 :         source_ = matches.str(1); // i.e. requestMethod=GET&requestUri=/app/v1/foo/bar%3Fid%3D5%26name%3Dtest&eventNumber=3&eventPath=/requestBody
     314            5 :         source_type_ = SourceType::ServerEvent;
     315            5 :         std::map<std::string, std::string> qmap = h2agent::model::extractQueryParameters(source_);
     316            5 :         std::map<std::string, std::string>::const_iterator it;
     317           25 :         for (auto const & qp: {
     318              :         "requestMethod", "requestUri", "eventNumber", "eventPath"
     319           30 :     }) { // tokenized vector order
     320           20 :             it = qmap.find(qp);
     321           34 :             source_tokenized_.push_back((it != qmap.end()) ? it->second:"");
     322              :         }
     323            5 :     }
     324           19 :     else if (std::regex_match(sourceSpec, matches, clientEvent)) { // client event data
     325            4 :         source_ = matches.str(1); // i.e. clientEndpointId=myEndpoint&requestMethod=GET&requestUri=/app/v1/foo/bar&eventNumber=3&eventPath=/responseBody
     326            4 :         source_type_ = SourceType::ClientEvent;
     327            4 :         std::map<std::string, std::string> qmap = h2agent::model::extractQueryParameters(source_);
     328            4 :         std::map<std::string, std::string>::const_iterator it;
     329           24 :         for (auto const & qp: {
     330              :         "clientEndpointId", "requestMethod", "requestUri", "eventNumber", "eventPath"
     331           28 :     }) { // tokenized vector order
     332           20 :             it = qmap.find(qp);
     333           28 :             source_tokenized_.push_back((it != qmap.end()) ? it->second:"");
     334              :         }
     335            4 :     }
     336           15 :     else if (sourceSpec == "response.statusCode") {
     337            2 :         source_type_ = SourceType::ResponseStatusCode;
     338              :     }
     339           13 :     else if (std::regex_match(sourceSpec, matches, responseHeader_source)) { // response header name
     340            1 :         source_ = matches.str(1);
     341            1 :         source_type_ = SourceType::ResponseHeader;
     342              :     }
     343           12 :     else if (sourceSpec == "inState") {
     344            4 :         source_type_ = SourceType::InState;
     345              :     }
     346            8 :     else if (std::regex_match(sourceSpec, matches, txtFile)) { // path file
     347            3 :         source_ = matches.str(1);
     348            3 :         source_type_ = SourceType::STxtFile;
     349              :     }
     350            5 :     else if (std::regex_match(sourceSpec, matches, binFile)) { // path file
     351            2 :         source_ = matches.str(1);
     352            2 :         source_type_ = SourceType::SBinFile;
     353              :     }
     354            3 :     else if (std::regex_match(sourceSpec, matches, command)) { // command string
     355            3 :         source_ = matches.str(1);
     356            3 :         source_type_ = SourceType::Command;
     357              :     }
     358              :     // PROTECTED BY SCHEMA:
     359              :     //else { // some things could reach this (strange characters within value.* for example):
     360              :     //    ert::tracing::Logger::error(ert::tracing::Logger::asString("Cannot identify source type for: %s", sourceSpec.c_str()), ERT_FILE_LOCATION);
     361              :     //    return false;
     362              :     //}
     363              :     //}
     364              :     //catch (std::regex_error &e) {
     365              :     //    ert::tracing::Logger::error(e.what(), ERT_FILE_LOCATION);
     366              :     //    return false;
     367              :     //}
     368              : 
     369              :     // TARGET (enum TargetType { ResponseBodyString = 0, ..., ResponseHeader_t, ResponseStatusCode_t, ..., RequestHeader, RequestDelayMs, RequestTimeoutMs, ClientEventToPurge };)
     370          195 :     target_ = ""; // empty by default (-), as many cases are only work modes and no parameters(+) are included in their transformation configuration
     371          195 :     target2_ = ""; // same
     372              : 
     373              :     // Target specifications:
     374              :     // SERVER MODE
     375              :     // - response.body.string *[string]*: response body storing expected string processed.
     376              :     // - response.body.hexstring *[string]*: response body storing expected string processed from hexadecimal representation, for example `0x8001` (prefix `0x` is optional).
     377              :     // - response.body.json.string *[string]*: response body document storing expected string.
     378              :     // - response.body.json.integer *[number]*: response body document storing expected integer.
     379              :     // - response.body.json.unsigned *[unsigned number]*: response body document storing expected unsigned integer.
     380              :     // - response.body.json.float *[float number]*: response body document storing expected float number.
     381              :     // - response.body.json.boolean *[boolean]*: response body document storing expected booolean.
     382              :     // - response.body.json.object *[json object]*: response body document storing expected object.
     383              :     // - response.body.json.jsonstring *[json string]*: response body document storing expected object, extracted from json-parsed string, as root node.
     384              :     // + response.body.json.string./<n1>/../<nN> *[string]*: response body node path storing expected string.
     385              :     // + response.body.json.integer./<n1>/../<nN> *[number]*: response body node path storing expected integer.
     386              :     // + response.body.json.unsigned./<n1>/../<nN> *[unsigned number]*: response body node path storing expected unsigned integer.
     387              :     // + response.body.json.float./<n1>/../<nN> *[float number]*: response body node path storing expected float number.
     388              :     // + response.body.json.boolean./<n1>/../<nN> *[boolean]*: response body node path storing expected booblean.
     389              :     // + response.body.json.object./<n1>/../<nN> *[json object]*: response body node path storing expected object.
     390              :     // + response.body.json.jsonstring./<n1>/../<nN> *[json string]*: response body node path storing expected object, extracted from json-parsed string, under provided path.
     391              :     // + response.header.<hname> *[string (or number as string)]*: response header component (i.e. *location*).
     392              :     // - response.statusCode *[unsigned integer]*: response status code.
     393              :     // - response.delayMs *[unsigned integer]*: simulated delay to respond.
     394              :     // + var.<id> *[string (or number as string)]*: general purpose variable.
     395              :     // + globalVar.<id> *[string (or number as string)]*: general purpose global variable.
     396              :     // - outState *[string (or number as string)]*: next processing state. This overrides the default provisioned one.
     397              :     // + outState.`[POST|GET|PUT|DELETE|HEAD][.<uri>]` *[string (or number as string)]*: next processing state for specific method (virtual server data will be created if needed: this way we could modify the flow for other methods different than the one which is managing the current provision). This target **admits variables substitution** in the `uri` part.
     398              :     // + txtFile.`<path>` *[string]*: dumps source (as string) over text file with the path provided.
     399              :     // + binFile.`<path>` *[string]*: dumps source (as string) over binary file with the path provided.
     400              :     // + udpSocket.`<path>[|<milliseconds delay>]` *[string]*: sends source (as string) towards the UDP unix socket with the path provided.
     401              :     // + serverEvent.`<server event address in query parameters format>`: this target is always used in conjunction with `eraser`.
     402              :     // - break *[string]*: when non-empty string is transferred, the transformations list is interrupted. Empty string (or undefined source) ignores the action.
     403              :     //
     404              :     // CLIENT MODE ADDITIONAL TARGET TYPES:
     405              :     // - request.body.string *[string]*: request body storing expected string processed.
     406              :     // - request.body.hexstring *[string]*: request body storing expected string processed from hexadecimal representation, for example `0x8001` (prefix `0x` is optional).
     407              :     // - request.body.json.string *[string]*: request body document storing expected string.
     408              :     // - request.body.json.integer *[number]*: request body document storing expected integer.
     409              :     // - request.body.json.unsigned *[unsigned number]*: request body document storing expected unsigned integer.
     410              :     // - request.body.json.float *[float number]*: request body document storing expected float number.
     411              :     // - request.body.json.boolean *[boolean]*: request body document storing expected booolean.
     412              :     // - request.body.json.object *[json object]*: request body document storing expected object.
     413              :     // - request.body.json.jsonstring *[json string]*: request body document storing expected object, extracted from json-parsed string, as root node.
     414              :     // + request.body.json.string./<n1>/../<nN> *[string]*: request body node path storing expected string.
     415              :     // + request.body.json.integer./<n1>/../<nN> *[number]*: request body node path storing expected integer.
     416              :     // + request.body.json.unsigned./<n1>/../<nN> *[unsigned number]*: request body node path storing expected unsigned integer.
     417              :     // + request.body.json.float./<n1>/../<nN> *[float number]*: request body node path storing expected float number.
     418              :     // + request.body.json.boolean./<n1>/../<nN> *[boolean]*: request body node path storing expected booblean.
     419              :     // + request.body.json.object./<n1>/../<nN> *[json object]*: request body node path storing expected object.
     420              :     // + request.body.json.jsonstring./<n1>/../<nN> *[json string]*: request body node path storing expected object, extracted from json-parsed string, under provided path.
     421              : 
     422              :     // Regex needed:
     423              :     // SERVER MODE:
     424          195 :     static std::regex responseBodyJson_StringNode("^response.body.json.string.(.*)", std::regex::optimize);
     425          195 :     static std::regex responseBodyJson_IntegerNode("^response.body.json.integer.(.*)", std::regex::optimize);
     426          195 :     static std::regex responseBodyJson_UnsignedNode("^response.body.json.unsigned.(.*)", std::regex::optimize);
     427          195 :     static std::regex responseBodyJson_FloatNode("^response.body.json.float.(.*)", std::regex::optimize);
     428          195 :     static std::regex responseBodyJson_BooleanNode("^response.body.json.boolean.(.*)", std::regex::optimize);
     429          195 :     static std::regex responseBodyJson_ObjectNode("^response.body.json.object.(.*)", std::regex::optimize);
     430          195 :     static std::regex responseBodyJson_JsonStringNode("^response.body.json.jsonstring.(.*)", std::regex::optimize);
     431          195 :     static std::regex responseHeader("^response.header.(.*)", std::regex::optimize);
     432          195 :     static std::regex outStateMethodUri("^outState.(POST|GET|PUT|DELETE|HEAD)(\\..+)?", std::regex::optimize);
     433              : 
     434              :     // CLIENT MODE:
     435          195 :     static std::regex requestBodyJson_StringNode("^request.body.json.string.(.*)", std::regex::optimize);
     436          195 :     static std::regex requestBodyJson_IntegerNode("^request.body.json.integer.(.*)", std::regex::optimize);
     437          195 :     static std::regex requestBodyJson_UnsignedNode("^request.body.json.unsigned.(.*)", std::regex::optimize);
     438          195 :     static std::regex requestBodyJson_FloatNode("^request.body.json.float.(.*)", std::regex::optimize);
     439          195 :     static std::regex requestBodyJson_BooleanNode("^request.body.json.boolean.(.*)", std::regex::optimize);
     440          195 :     static std::regex requestBodyJson_ObjectNode("^request.body.json.object.(.*)", std::regex::optimize);
     441          195 :     static std::regex requestBodyJson_JsonStringNode("^request.body.json.jsonstring.(.*)", std::regex::optimize);
     442              : 
     443              :     // Only target
     444          195 :     static std::regex requestHeader_target("^request.header.(.*)", std::regex::optimize);
     445          195 :     static std::regex udpSocket("^udpSocket.(.*)", std::regex::optimize);
     446          195 :     static std::regex clientProvision("^clientProvision.(.*)", std::regex::optimize);
     447              : 
     448              :     // no need to try (controlled regex)
     449              :     //try {
     450              :     // SERVER_MODE
     451          195 :     if (targetSpec == "response.body.string") {
     452           64 :         target_type_ = TargetType::ResponseBodyString;
     453              :     }
     454          131 :     else if (targetSpec == "response.body.hexstring") {
     455            1 :         target_type_ = TargetType::ResponseBodyHexString;
     456              :     }
     457          130 :     else if (targetSpec == "response.body.json.string") { // whole document
     458            2 :         target_type_ = TargetType::ResponseBodyJson_String;
     459              :     }
     460          128 :     else if (std::regex_match(targetSpec, matches, responseBodyJson_StringNode)) { // nlohmann::json_pointer path
     461           16 :         target_ = matches.str(1);
     462           16 :         target_type_ = TargetType::ResponseBodyJson_String;
     463              :     }
     464          112 :     else if (targetSpec == "response.body.json.integer") { // whole document
     465            1 :         target_type_ = TargetType::ResponseBodyJson_Integer;
     466              :     }
     467          111 :     else if (std::regex_match(targetSpec, matches, responseBodyJson_IntegerNode)) { // nlohmann::json_pointer path
     468            5 :         target_ = matches.str(1);
     469            5 :         target_type_ = TargetType::ResponseBodyJson_Integer;
     470              :     }
     471          106 :     else if (targetSpec == "response.body.json.unsigned") { // whole document
     472            1 :         target_type_ = TargetType::ResponseBodyJson_Unsigned;
     473              :     }
     474          105 :     else if (std::regex_match(targetSpec, matches, responseBodyJson_UnsignedNode)) { // nlohmann::json_pointer path
     475            1 :         target_ = matches.str(1);
     476            1 :         target_type_ = TargetType::ResponseBodyJson_Unsigned;
     477              :     }
     478          104 :     else if (targetSpec == "response.body.json.float") { // whole document
     479            1 :         target_type_ = TargetType::ResponseBodyJson_Float;
     480              :     }
     481          103 :     else if (std::regex_match(targetSpec, matches, responseBodyJson_FloatNode)) { // nlohmann::json_pointer path
     482            1 :         target_ = matches.str(1);
     483            1 :         target_type_ = TargetType::ResponseBodyJson_Float;
     484              :     }
     485          102 :     else if (targetSpec == "response.body.json.boolean") { // whole document
     486            1 :         target_type_ = TargetType::ResponseBodyJson_Boolean;
     487              :     }
     488          101 :     else if (std::regex_match(targetSpec, matches, responseBodyJson_BooleanNode)) { // nlohmann::json_pointer path
     489            1 :         target_ = matches.str(1);
     490            1 :         target_type_ = TargetType::ResponseBodyJson_Boolean;
     491              :     }
     492          100 :     else if (targetSpec == "response.body.json.object") { // whole document
     493            7 :         target_type_ = TargetType::ResponseBodyJson_Object;
     494              :     }
     495           93 :     else if (std::regex_match(targetSpec, matches, responseBodyJson_ObjectNode)) { // nlohmann::json_pointer path
     496            8 :         target_ = matches.str(1);
     497            8 :         target_type_ = TargetType::ResponseBodyJson_Object;
     498              :     }
     499           85 :     else if (targetSpec == "response.body.json.jsonstring") { // whole document
     500            2 :         target_type_ = TargetType::ResponseBodyJson_JsonString;
     501              :     }
     502           83 :     else if (std::regex_match(targetSpec, matches, responseBodyJson_JsonStringNode)) { // nlohmann::json_pointer path
     503            1 :         target_ = matches.str(1);
     504            1 :         target_type_ = TargetType::ResponseBodyJson_JsonString;
     505              :     }
     506              :     // CLIENT MODE
     507           82 :     else if (targetSpec == "request.body.string") {
     508            0 :         target_type_ = TargetType::RequestBodyString;
     509              :     }
     510           82 :     else if (targetSpec == "request.body.hexstring") {
     511            0 :         target_type_ = TargetType::RequestBodyHexString;
     512              :     }
     513           82 :     else if (targetSpec == "request.body.json.string") { // whole document
     514            0 :         target_type_ = TargetType::RequestBodyJson_String;
     515              :     }
     516           82 :     else if (std::regex_match(targetSpec, matches, requestBodyJson_StringNode)) { // nlohmann::json_pointer path
     517            1 :         target_ = matches.str(1);
     518            1 :         target_type_ = TargetType::RequestBodyJson_String;
     519              :     }
     520           81 :     else if (targetSpec == "request.body.json.integer") { // whole document
     521            0 :         target_type_ = TargetType::RequestBodyJson_Integer;
     522              :     }
     523           81 :     else if (std::regex_match(targetSpec, matches, requestBodyJson_IntegerNode)) { // nlohmann::json_pointer path
     524            1 :         target_ = matches.str(1);
     525            1 :         target_type_ = TargetType::RequestBodyJson_Integer;
     526              :     }
     527           80 :     else if (targetSpec == "request.body.json.unsigned") { // whole document
     528            0 :         target_type_ = TargetType::RequestBodyJson_Unsigned;
     529              :     }
     530           80 :     else if (std::regex_match(targetSpec, matches, requestBodyJson_UnsignedNode)) { // nlohmann::json_pointer path
     531            0 :         target_ = matches.str(1);
     532            0 :         target_type_ = TargetType::RequestBodyJson_Unsigned;
     533              :     }
     534           80 :     else if (targetSpec == "request.body.json.float") { // whole document
     535            0 :         target_type_ = TargetType::RequestBodyJson_Float;
     536              :     }
     537           80 :     else if (std::regex_match(targetSpec, matches, requestBodyJson_FloatNode)) { // nlohmann::json_pointer path
     538            0 :         target_ = matches.str(1);
     539            0 :         target_type_ = TargetType::RequestBodyJson_Float;
     540              :     }
     541           80 :     else if (targetSpec == "request.body.json.boolean") { // whole document
     542            0 :         target_type_ = TargetType::RequestBodyJson_Boolean;
     543              :     }
     544           80 :     else if (std::regex_match(targetSpec, matches, requestBodyJson_BooleanNode)) { // nlohmann::json_pointer path
     545            0 :         target_ = matches.str(1);
     546            0 :         target_type_ = TargetType::RequestBodyJson_Boolean;
     547              :     }
     548           80 :     else if (targetSpec == "request.body.json.object") { // whole document
     549            0 :         target_type_ = TargetType::RequestBodyJson_Object;
     550              :     }
     551           80 :     else if (std::regex_match(targetSpec, matches, requestBodyJson_ObjectNode)) { // nlohmann::json_pointer path
     552            0 :         target_ = matches.str(1);
     553            0 :         target_type_ = TargetType::RequestBodyJson_Object;
     554              :     }
     555           80 :     else if (targetSpec == "request.body.json.jsonstring") { // whole document
     556            0 :         target_type_ = TargetType::RequestBodyJson_JsonString;
     557              :     }
     558           80 :     else if (std::regex_match(targetSpec, matches, requestBodyJson_JsonStringNode)) { // nlohmann::json_pointer path
     559            0 :         target_ = matches.str(1);
     560            0 :         target_type_ = TargetType::RequestBodyJson_JsonString;
     561              :     }
     562              : 
     563              :     // CLIENT MODE ADDITIONAL TARGET TYPES:
     564           80 :     else if (std::regex_match(targetSpec, matches, requestHeader_target)) { // request header name
     565            2 :         target_ = matches.str(1);
     566            2 :         target_type_ = TargetType::RequestHeader_t;
     567              :     }
     568           78 :     else if (targetSpec == "request.delayMs") {
     569            4 :         target_type_ = TargetType::RequestDelayMs;
     570              :     }
     571           74 :     else if (targetSpec == "request.timeoutMs") {
     572            1 :         target_type_ = TargetType::RequestTimeoutMs;
     573              :     }
     574           73 :     else if (targetSpec == "request.uri") {
     575           11 :         target_type_ = TargetType::RequestUri_t;
     576              :     }
     577           62 :     else if (targetSpec == "request.method") {
     578            1 :         target_type_ = TargetType::RequestMethod_t;
     579              :     }
     580              : 
     581           61 :     else if (std::regex_match(targetSpec, matches, responseHeader)) { // header name
     582            1 :         target_ = matches.str(1);
     583            1 :         target_type_ = TargetType::ResponseHeader_t;
     584              :     }
     585           60 :     else if (targetSpec == "response.statusCode") {
     586            6 :         target_type_ = TargetType::ResponseStatusCode_t;
     587              :     }
     588           54 :     else if (targetSpec == "response.delayMs") {
     589            1 :         target_type_ = TargetType::ResponseDelayMs;
     590              :     }
     591           53 :     else if (std::regex_match(targetSpec, matches, varId)) { // variable id
     592           17 :         target_ = matches.str(1);
     593           17 :         target_type_ = TargetType::TVar;
     594              :     }
     595           36 :     else if (std::regex_match(targetSpec, matches, gvarId)) { // global variable id
     596           10 :         target_ = matches.str(1);
     597           10 :         target_type_ = TargetType::TGVar;
     598              :     }
     599           26 :     else if (targetSpec == "outState") {
     600            6 :         target_type_ = TargetType::OutState;
     601              :     }
     602           20 :     else if (std::regex_match(targetSpec, matches, outStateMethodUri)) { // method
     603            2 :         target_ = matches.str(1); // <method>
     604            2 :         target2_ = matches.str(2); // .<uri>
     605            2 :         if (!target2_.empty()) {
     606            2 :             target2_ = target2_.substr(1); // remove the initial dot to store the uri
     607              :         }
     608            2 :         target_type_ = TargetType::OutState;
     609              :     }
     610           18 :     else if (std::regex_match(targetSpec, matches, txtFile)) { // path file
     611            3 :         target_ = matches.str(1);
     612            3 :         target_type_ = TargetType::TTxtFile;
     613              :     }
     614           15 :     else if (std::regex_match(targetSpec, matches, binFile)) { // path file
     615            2 :         target_ = matches.str(1);
     616            2 :         target_type_ = TargetType::TBinFile;
     617              :     }
     618           13 :     else if (std::regex_match(targetSpec, matches, udpSocket)) { // path file
     619            1 :         target_ = matches.str(1);
     620            1 :         target_type_ = TargetType::UDPSocket;
     621              :     }
     622           12 :     else if (std::regex_match(targetSpec, matches, serverEvent)) { // value content
     623            4 :         target_ = matches.str(1); // i.e. requestMethod=GET&requestUri=/app/v1/foo/bar%3Fid%3D5%26name%3Dtest&eventNumber=3
     624            4 :         target_type_ = TargetType::ServerEventToPurge;
     625            4 :         std::map<std::string, std::string> qmap = h2agent::model::extractQueryParameters(target_);
     626            4 :         std::map<std::string, std::string>::const_iterator it;
     627           16 :         for (auto const & qp: {
     628              :         "requestMethod", "requestUri", "eventNumber"
     629           20 :     }) { // tokenized vector order
     630           12 :             it = qmap.find(qp);
     631           20 :             target_tokenized_.push_back((it != qmap.end()) ? it->second:"");
     632              :         }
     633            4 :     }
     634            8 :     else if (std::regex_match(targetSpec, matches, clientEvent)) { // client event purge
     635            1 :         target_ = matches.str(1); // i.e. clientEndpointId=myEndpoint&requestMethod=GET&requestUri=/app/v1/foo/bar&eventNumber=3
     636            1 :         target_type_ = TargetType::ClientEventToPurge;
     637            1 :         std::map<std::string, std::string> qmap = h2agent::model::extractQueryParameters(target_);
     638            1 :         std::map<std::string, std::string>::const_iterator it;
     639            5 :         for (auto const & qp: {
     640              :         "clientEndpointId", "requestMethod", "requestUri", "eventNumber"
     641            6 :     }) { // tokenized vector order
     642            4 :             it = qmap.find(qp);
     643           12 :             target_tokenized_.push_back((it != qmap.end()) ? it->second:"");
     644              :         }
     645            1 :     }
     646            7 :     else if (targetSpec == "break") {
     647            4 :         target_type_ = TargetType::Break;
     648              :     }
     649            3 :     else if (std::regex_match(targetSpec, matches, clientProvision)) { // client provision id
     650            3 :         target_ = matches.str(1);
     651            3 :         target_type_ = TargetType::ClientProvision_t;
     652              :     }
     653              :     // PROTECTED BY SCHEMA:
     654              :     //else { // very strange to reach this:
     655              :     //    ert::tracing::Logger::error(ert::tracing::Logger::asString("Cannot identify target type for: %s", targetSpec.c_str()), ERT_FILE_LOCATION);
     656              :     //    return false;
     657              :     //}
     658              :     //}
     659              :     //catch (std::regex_error &e) {
     660              :     //    ert::tracing::Logger::error(e.what(), ERT_FILE_LOCATION);
     661              :     //    return false;
     662              :     //}
     663              : 
     664              :     //LOGDEBUG(ert::tracing::Logger::debug(asString(), ERT_FILE_LOCATION));
     665              : 
     666              :     // Variable patterns:
     667          195 :     collectVariablePatterns(source_, source_patterns_);
     668          195 :     if (collectFilterPatterns) collectVariablePatterns(filter_, filter_patterns_); // protected to avoid possible gathering of false patterns (i.e. complex regexp's)
     669          195 :     collectVariablePatterns(target_, target_patterns_);
     670          195 :     collectVariablePatterns(target2_, target2_patterns_);
     671              : 
     672          195 :     return true;
     673          197 : }
     674              : 
     675           11 : std::string Transformation::asString() const {
     676              : 
     677           11 :     std::stringstream ss;
     678              : 
     679              : 
     680              :     // SOURCE
     681           11 :     ss << "SourceType: " << SourceTypeAsText(source_type_);
     682           11 :     if (source_type_ != SourceType::RequestUri && source_type_ != SourceType::RequestUriPath && source_type_ != SourceType::Eraser && source_type_ != SourceType::Recvseq && source_type_ != SourceType::Sendseq && source_type_ != SourceType::Seq && source_type_ != SourceType::InState && source_type_ != SourceType::ResponseStatusCode) {
     683            9 :         ss << " | source_: " << source_;
     684              : 
     685            9 :         if (source_type_ == SourceType::RequestBody || source_type_ == SourceType::ResponseBody) {
     686            1 :             ss << " (empty: whole, path: node)";
     687              :         }
     688            8 :         else if (source_type_ == SourceType::Random) {
     689            1 :             ss << " | source_i1_: " << source_i1_ << " (Random min)" << " | source_i2_: " << source_i2_ << " (Random max)";
     690              :         }
     691            7 :         else if (source_type_ == SourceType::RandomSet || source_type_ == SourceType::ServerEvent || source_type_ == SourceType::ClientEvent) {
     692            0 :             ss << " | source_tokenized_:";
     693            0 :             for(auto it: source_tokenized_) {
     694            0 :                 ss << " '" << it << "'";
     695            0 :             }
     696            0 :         }
     697            7 :         else if (source_type_ == SourceType::STxtFile || source_type_ == SourceType::SBinFile) {
     698            1 :             ss << " (path file)";
     699              :         }
     700            6 :         else if (source_type_ == SourceType::Command) {
     701            1 :             ss << " (shell command expression)";
     702              :         }
     703              : 
     704            9 :         if (!source_patterns_.empty()) {
     705            1 :             ss << " | source variables:";
     706            2 :             for (auto it = source_patterns_.begin(); it != source_patterns_.end(); it ++) {
     707            1 :                 ss << " " << it->second;
     708              :             }
     709              :         }
     710              :     }
     711              : 
     712              :     // TARGET
     713           11 :     ss << " | TargetType: " << TargetTypeAsText(target_type_);
     714           11 :     if (target_type_ != TargetType::ResponseStatusCode_t &&
     715           11 :             target_type_ != TargetType::ResponseDelayMs &&
     716           11 :             target_type_ != TargetType::ResponseBodyString &&
     717            4 :             target_type_ != TargetType::ResponseBodyHexString &&
     718            4 :             target_type_ != TargetType::RequestDelayMs &&
     719            4 :             target_type_ != TargetType::RequestTimeoutMs &&
     720            4 :             target_type_ != TargetType::RequestBodyString &&
     721            4 :             target_type_ != TargetType::RequestBodyHexString ) {
     722              : 
     723            4 :         ss << " | target_: " << target_;
     724              : 
     725            4 :         if (target_type_ == TargetType::ResponseBodyJson_String || target_type_ == TargetType::ResponseBodyJson_Integer || target_type_ == TargetType::ResponseBodyJson_Unsigned || target_type_ == TargetType::ResponseBodyJson_Float || target_type_ == TargetType::ResponseBodyJson_Boolean || target_type_ == TargetType::ResponseBodyJson_Object ||
     726            3 :                 target_type_ == TargetType::RequestBodyJson_String || target_type_ == TargetType::RequestBodyJson_Integer || target_type_ == TargetType::RequestBodyJson_Unsigned || target_type_ == TargetType::RequestBodyJson_Float || target_type_ == TargetType::RequestBodyJson_Boolean || target_type_ == TargetType::RequestBodyJson_Object) {
     727            1 :             ss << " (empty: whole, path: node)";
     728              :         }
     729            3 :         else if (target_type_ == TargetType::OutState) {
     730            1 :             ss << " (empty: current method, method: another)" << " | target2_: " << target2_ << "(empty: current uri, uri: another)";
     731            1 :             if (!target2_patterns_.empty()) {
     732            1 :                 ss << " | target2 variables:";
     733            2 :                 for (auto it = target2_patterns_.begin(); it != target2_patterns_.end(); it ++) {
     734            1 :                     ss << " " << it->second;
     735              :                 }
     736              :             }
     737              :         }
     738            2 :         else if (target_type_ == TargetType::TTxtFile || target_type_ == TargetType::TBinFile) {
     739            1 :             ss << " (path file)";
     740              :         }
     741            1 :         else if (target_type_ == TargetType::UDPSocket) {
     742            0 :             ss << " (<path file>[|<write delay ms>])";
     743              :         }
     744              : 
     745            4 :         if (!target_patterns_.empty()) {
     746            1 :             ss << " | target variables:";
     747            2 :             for (auto it = target_patterns_.begin(); it != target_patterns_.end(); it ++) {
     748            1 :                 ss << " " << it->second;
     749              :             }
     750              :         }
     751              :     }
     752              : 
     753              :     // FILTER
     754           11 :     if (has_filter_) {
     755            4 :         ss << " | FilterType: " << FilterTypeAsText(filter_type_);
     756            4 :         if (filter_type_ != FilterType::Sum && filter_type_ != FilterType::Multiply) {
     757              :             /*<< " | filter_rgx_: ?"*/
     758            3 :             ss << " | filter_ " << filter_;
     759              : 
     760            3 :             if (filter_type_ == FilterType::RegexReplace) {
     761            1 :                 ss << " (fmt)";
     762              :             }
     763            2 :             else if (filter_type_ == FilterType::RegexCapture) {
     764            1 :                 ss << " (literal, although not actually needed, but useful to access & print on traces)";
     765              :             }
     766              :         }
     767              :         else {
     768            1 :             ss << " | filter_number_type_: " << filter_number_type_ << " (0: integer, 1: unsigned, 2: float)"
     769            1 :                << " | filter_i_: " << filter_i_ << " | filter_u_: " << filter_u_ << " | filter_f_: " << filter_f_;
     770              :         }
     771              : 
     772            4 :         if (!filter_patterns_.empty()) {
     773            1 :             ss << " | filter variables:";
     774            2 :             for (auto it = filter_patterns_.begin(); it != filter_patterns_.end(); it ++) {
     775            1 :                 ss << " " << it->second;
     776              :             }
     777              :         }
     778              :     }
     779              : 
     780           22 :     return ss.str();
     781           11 : }
     782              : 
     783              : 
     784              : }
     785              : }
     786              : 
        

Generated by: LCOV version 2.0-1