LCOV - code coverage report
Current view: top level - tools - Tools.cpp (source / functions) Hit Total Coverage
Test: plumed test coverage Lines: 258 292 88.4 %
Date: 2024-10-18 14:00:25 Functions: 52 57 91.2 %

          Line data    Source code
       1             : /* +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
       2             :    Copyright (c) 2011-2023 The plumed team
       3             :    (see the PEOPLE file at the root of the distribution for a list of names)
       4             : 
       5             :    See http://www.plumed.org for more information.
       6             : 
       7             :    This file is part of plumed, version 2.
       8             : 
       9             :    plumed is free software: you can redistribute it and/or modify
      10             :    it under the terms of the GNU Lesser General Public License as published by
      11             :    the Free Software Foundation, either version 3 of the License, or
      12             :    (at your option) any later version.
      13             : 
      14             :    plumed is distributed in the hope that it will be useful,
      15             :    but WITHOUT ANY WARRANTY; without even the implied warranty of
      16             :    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      17             :    GNU Lesser General Public License for more details.
      18             : 
      19             :    You should have received a copy of the GNU Lesser General Public License
      20             :    along with plumed.  If not, see <http://www.gnu.org/licenses/>.
      21             : +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ */
      22             : #include "Tools.h"
      23             : #include "AtomNumber.h"
      24             : #include "Exception.h"
      25             : #include "IFile.h"
      26             : #include "lepton/Lepton.h"
      27             : #include <cstring>
      28             : #include <iostream>
      29             : #include <map>
      30             : #include <iomanip>
      31             : #include <filesystem>
      32             : 
      33             : namespace PLMD {
      34             : 
      35             : template<class T>
      36    21985274 : bool Tools::convertToAny(const std::string & str,T & t) {
      37    41484550 :   std::istringstream istr(str.c_str());
      38    21985274 :   bool ok=static_cast<bool>(istr>>t);
      39    21985274 :   if(!ok) return false;
      40             :   std::string remaining;
      41    21942574 :   istr>>remaining;
      42    21942574 :   return remaining.length()==0;
      43    21985274 : }
      44             : 
      45     2480526 : bool Tools::convertNoexcept(const std::string & str,int & t) {
      46     2480526 :   return convertToInt(str,t);
      47             : }
      48             : 
      49           3 : bool Tools::convertNoexcept(const std::string & str,long int & t) {
      50           3 :   return convertToInt(str,t);
      51             : }
      52             : 
      53          87 : bool Tools::convertNoexcept(const std::string & str,long long int & t) {
      54          87 :   return convertToInt(str,t);
      55             : }
      56             : 
      57     3989224 : bool Tools::convertNoexcept(const std::string & str,unsigned & t) {
      58     3989224 :   return convertToInt(str,t);
      59             : }
      60             : 
      61          93 : bool Tools::convertNoexcept(const std::string & str,long unsigned & t) {
      62          93 :   return convertToInt(str,t);
      63             : }
      64             : 
      65          54 : bool Tools::convertNoexcept(const std::string & str,long long unsigned & t) {
      66          54 :   return convertToInt(str,t);
      67             : }
      68             : 
      69     1117690 : bool Tools::convertNoexcept(const std::string & str,AtomNumber &a) {
      70             :   // Note: AtomNumber's are NOT converted as int, so as to
      71             :   // avoid using lepton conversions.
      72             :   unsigned i;
      73     1117690 :   bool r=convertToAny(str,i);
      74     1117690 :   if(r) a.setSerial(i);
      75     1117690 :   return r;
      76             : }
      77             : 
      78             : template<class T>
      79     6469987 : bool Tools::convertToInt(const std::string & str,T & t) {
      80             :   // First try standard conversion
      81     6469987 :   if(convertToAny(str,t)) return true;
      82             :   // Then use lepton
      83             :   try {
      84     2008031 :     double r=lepton::Parser::parse(str).evaluate(lepton::Constants());
      85             : 
      86             :     // now sanity checks on the resulting number
      87             : 
      88             :     // it should not overflow the requested int type:
      89             :     // (see https://stackoverflow.com/a/526092)
      90     2008031 :     if(r>std::nextafter(std::numeric_limits<T>::max(), 0)) return false;
      91     2008031 :     if(r<std::nextafter(std::numeric_limits<T>::min(), 0)) return false;
      92             : 
      93             :     // do the actual conversion
      94     2008031 :     auto tmp=static_cast<T>(std::round(r));
      95             : 
      96             :     // it should be *very close* to itself if converted back to double
      97     2008031 :     double diff= r-static_cast<double>(tmp);
      98     2008031 :     if(diff*diff > 1e-20) return false;
      99             :     // this is to accomodate small numerical errors and allow e.g. exp(log(7)) to be integer
     100             : 
     101             :     // it should be change if incremented or decremented by one (see https://stackoverflow.com/a/43656140)
     102     2008027 :     if(r == static_cast<double>(tmp-1)) return false;
     103     2008026 :     if(r == static_cast<double>(tmp+1)) return false;
     104             : 
     105             :     // everything is fine, then store in t
     106     2008026 :     t=tmp;
     107     2008026 :     return true;
     108           0 :   } catch(const PLMD::lepton::Exception& exc) {
     109             :   }
     110           0 :   return false;
     111             : }
     112             : 
     113             : 
     114             : template<class T>
     115    14392125 : bool Tools::convertToReal(const std::string & str,T & t) {
     116    14392125 :   if(convertToAny(str,t)) return true;
     117       68222 :   if(str=="PI" || str=="+PI" || str=="+pi" || str=="pi") {
     118        8573 :     t=pi; return true;
     119       17090 :   } else if(str=="-PI" || str=="-pi") {
     120        8486 :     t=-pi; return true;
     121             :   }
     122             :   try {
     123          59 :     t=lepton::Parser::parse(str).evaluate(lepton::Constants());
     124          24 :     return true;
     125          35 :   } catch(const PLMD::lepton::Exception& exc) {
     126             :   }
     127          35 :   if( str.find("PI")!=std::string::npos ) {
     128           0 :     std::size_t pi_start=str.find_first_of("PI");
     129           0 :     if(str.substr(pi_start)!="PI") return false;
     130           0 :     std::istringstream nstr(str.substr(0,pi_start));
     131           0 :     T ff=0.0; bool ok=static_cast<bool>(nstr>>ff);
     132           0 :     if(!ok) return false;
     133           0 :     t=ff*pi;
     134           0 :     std::string remains; nstr>>remains;
     135           0 :     return remains.length()==0;
     136          35 :   } else if( str.find("pi")!=std::string::npos ) {
     137          13 :     std::size_t pi_start=str.find_first_of("pi");
     138          26 :     if(str.substr(pi_start)!="pi") return false;
     139          13 :     std::istringstream nstr(str.substr(0,pi_start));
     140          13 :     T ff=0.0; bool ok=static_cast<bool>(nstr>>ff);
     141          13 :     if(!ok) return false;
     142          13 :     t=ff*pi;
     143          13 :     std::string remains; nstr>>remains;
     144          13 :     return remains.length()==0;
     145          35 :   } else if(str=="NAN") {
     146           0 :     t=std::numeric_limits<double>::quiet_NaN();
     147           0 :     return true;
     148             :   }
     149             :   return false;
     150             : }
     151             : 
     152           0 : bool Tools::convertNoexcept(const std::string & str,float & t) {
     153           0 :   return convertToReal(str,t);
     154             : }
     155             : 
     156    14390917 : bool Tools::convertNoexcept(const std::string & str,double & t) {
     157    14390917 :   return convertToReal(str,t);
     158             : }
     159             : 
     160        1208 : bool Tools::convertNoexcept(const std::string & str,long double & t) {
     161        1208 :   return convertToReal(str,t);
     162             : }
     163             : 
     164      549107 : bool Tools::convertNoexcept(const std::string & str,std::string & t) {
     165             :   t=str;
     166      549107 :   return true;
     167             : }
     168             : 
     169     3541148 : std::vector<std::string> Tools::getWords(std::string_view line,const char* separators,int * parlevel,const char* parenthesis, const bool& delete_parenthesis) {
     170     3541148 :   plumed_massert(std::strlen(parenthesis)==1,"multiple parenthesis type not available");
     171     3541148 :   plumed_massert(parenthesis[0]=='(' || parenthesis[0]=='[' || parenthesis[0]=='{',
     172             :                  "only ( [ { allowed as parenthesis");
     173     3541148 :   if(!separators) separators=" \t\n";
     174     3541148 :   const std::string sep(separators);
     175     3541148 :   char openpar=parenthesis[0];
     176             :   char closepar;
     177             :   if(openpar=='(') closepar=')';
     178     3541148 :   if(openpar=='[') closepar=']';
     179     3541148 :   if(openpar=='{') closepar='}';
     180             :   std::vector<std::string> words;
     181             :   std::string word;
     182             :   int parenthesisLevel=0;
     183     3541148 :   if(parlevel) parenthesisLevel=*parlevel;
     184   290398576 :   for(unsigned i=0; i<line.length(); i++) {
     185             :     bool found=false;
     186             :     bool onParenthesis=false;
     187   286857428 :     if( (line[i]==openpar || line[i]==closepar) && delete_parenthesis ) onParenthesis=true;
     188   286857428 :     if(line[i]==closepar) {
     189        4120 :       parenthesisLevel--;
     190        4120 :       plumed_assert(parenthesisLevel>=0) << "Extra closed parenthesis in '" << line << "'";
     191             :     }
     192  1164124805 :     if(parenthesisLevel==0) for(unsigned j=0; j<sep.length(); j++) if(line[i]==sep[j]) found=true;
     193             : // If at parenthesis level zero (outer)
     194   286857428 :     if(!(parenthesisLevel==0 && (found||onParenthesis))) word.push_back(line[i]);
     195             :     //if(onParenthesis) word.push_back(' ');
     196   286857428 :     if(line[i]==openpar) parenthesisLevel++;
     197   286857428 :     if(found && word.length()>0) {
     198    17609051 :       if(!parlevel) plumed_assert(parenthesisLevel==0) << "Unmatching parenthesis in '" << line << "'";
     199    17609051 :       words.push_back(word);
     200             :       word.clear();
     201             :     }
     202             :   }
     203     3541148 :   if(word.length()>0) {
     204     3419445 :     if(!parlevel) plumed_assert(parenthesisLevel==0) << "Unmatching parenthesis in '" << line << "'";
     205     3419445 :     words.push_back(word);
     206             :   }
     207     3541148 :   if(parlevel) *parlevel=parenthesisLevel;
     208     3541148 :   return words;
     209           0 : }
     210             : 
     211     1348558 : void Tools::getWordsSimple(gch::small_vector<std::string_view> & words,std::string_view line) {
     212             :   words.clear();
     213     1348558 :   auto ptr=line.data();
     214     1348558 :   std::size_t size=0;
     215    14520417 :   for(unsigned i=0; i<line.length(); i++) {
     216    13171859 :     const bool is_separator=(line[i]==' ');
     217    13171859 :     if(!is_separator) {
     218    13130198 :       size++;
     219       41661 :     } else if(size==0) {
     220           0 :       ptr++;
     221             :     } else {
     222             :       words.emplace_back(ptr,size);
     223       41661 :       ptr=&line[i]+1;
     224       41661 :       size=0;
     225             :     }
     226             :   }
     227     1348558 :   if(size>0) {
     228             :     words.emplace_back(ptr,size);
     229             :   }
     230     1348558 : }
     231             : 
     232       19581 : bool Tools::getParsedLine(IFile& ifile,std::vector<std::string> & words, bool trimcomments) {
     233       19581 :   std::string line("");
     234             :   words.clear();
     235             :   bool stat;
     236             :   bool inside=false;
     237       19581 :   int parlevel=0;
     238             :   bool mergenext=false;
     239       40104 :   while((stat=ifile.getline(line))) {
     240       39081 :     if(trimcomments) trimComments(line);
     241       39081 :     trim(line);
     242       39081 :     if(line.length()==0) continue;
     243       32549 :     std::vector<std::string> w=getWords(line,NULL,&parlevel,"{",trimcomments);
     244       32549 :     if(!w.empty()) {
     245       46134 :       if(inside && *(w.begin())=="...") {
     246             :         inside=false;
     247        1273 :         if(w.size()==2) plumed_massert(w[1]==words[0],"second word in terminating \"...\" "+w[1]+" line, if present, should be equal to first word of directive: "+words[0]);
     248        1273 :         plumed_massert(w.size()<=2,"terminating \"...\" lines cannot consist of more than two words");
     249        1273 :         w.clear(); if(!trimcomments) words.push_back("...");
     250       30987 :       } else if(*(w.end()-1)=="...") {
     251             :         inside=true;
     252             :         w.erase(w.end()-1);
     253             :       };
     254             :       int i0=0;
     255       32260 :       if(mergenext && words.size()>0 && w.size()>0) {
     256         128 :         words[words.size()-1]+=" "+w[0];
     257             :         i0=1;
     258             :       }
     259      118546 :       for(unsigned i=i0; i<w.size(); ++i) words.push_back(w[i]);
     260             :     }
     261       32549 :     mergenext=(parlevel>0);
     262       32549 :     if(!inside)break;
     263       13991 :     if(!trimcomments && parlevel==0) words.push_back("@newline");
     264       13991 :     else if(!trimcomments) words[words.size()-1] += " @newline";
     265       32549 :   }
     266       19581 :   plumed_massert(parlevel==0,"non matching parenthesis");
     267       19581 :   if(words.size()>0) return true;
     268             :   return stat;
     269             : }
     270             : 
     271             : 
     272     3319889 : bool Tools::getline(FILE* fp,std::string & line) {
     273             :   line="";
     274             :   const int bufferlength=1024;
     275             :   char buffer[bufferlength];
     276             :   bool ret;
     277  3402886225 :   for(int i=0; i<bufferlength; i++) buffer[i]='\0';
     278     3319889 :   while((ret=fgets(buffer,bufferlength,fp))) {
     279     3318788 :     line.append(buffer);
     280     3318788 :     unsigned ss=std::strlen(buffer);
     281     3318788 :     if(ss>0) if(buffer[ss-1]=='\n') break;
     282             :   };
     283     3319889 :   if(line.length()>0) if(*(line.end()-1)=='\n') line.erase(line.end()-1);
     284     3319889 :   if(line.length()>0) if(*(line.end()-1)=='\r') line.erase(line.end()-1);
     285     3319889 :   return ret;
     286             : }
     287             : 
     288     1489989 : void Tools::trim(std::string & s) {
     289     1489989 :   auto n=s.find_last_not_of(" \t");
     290     1489989 :   if(n!=std::string::npos) s.resize(n+1);
     291     1489989 : }
     292             : 
     293        1080 : void Tools::ltrim(std::string & s) {
     294        1080 :   auto n=s.find_first_not_of(" \t");
     295        1080 :   if(n!=std::string::npos) {
     296        2160 :     s = s.substr(n, s.length()-n);
     297             :     s.shrink_to_fit();
     298             :   }
     299        1080 : }
     300             : 
     301     1613038 : void Tools::trimComments(std::string & s) {
     302     1613038 :   auto n=s.find_first_of("#");
     303     1613038 :   if(n!=std::string::npos) s.resize(n);
     304     1613038 : }
     305             : 
     306     2416415 : bool Tools::caseInSensStringCompare(const std::string & str1, const std::string &str2)
     307             : {
     308     2416415 :   return ((str1.size() == str2.size()) && std::equal(str1.begin(), str1.end(), str2.begin(), [](char c1, char c2) {
     309     7131406 :     return (c1 == c2 || std::toupper(c1) == std::toupper(c2));
     310     2416415 :   }));
     311             : }
     312             : 
     313      615595 : bool Tools::getKey(std::vector<std::string>& line,const std::string & key,std::string & s,int rep) {
     314             :   s.clear();
     315     2517088 :   for(auto p=line.begin(); p!=line.end(); ++p) {
     316     2416349 :     if((*p).length()==0) continue;
     317     2416349 :     std::string x=(*p).substr(0,key.length());
     318     2416349 :     if(caseInSensStringCompare(x,key)) {
     319      514856 :       if((*p).length()==key.length())return false;
     320      514855 :       std::string tmp=(*p).substr(key.length(),(*p).length());
     321             :       line.erase(p);
     322             :       s=tmp;
     323      514855 :       const std::string multi("@replicas:");
     324      514855 :       if(rep>=0 && startWith(s,multi)) {
     325          24 :         s=s.substr(multi.length(),s.length());
     326          24 :         std::vector<std::string> words=getWords(s,"\t\n ,");
     327          24 :         plumed_massert(rep<static_cast<int>(words.size()),"Number of fields in " + s + " not consistent with number of replicas");
     328          24 :         s=words[rep];
     329          24 :       }
     330             :       return true;
     331             :     }
     332             :   };
     333             :   return false;
     334             : }
     335             : 
     336       48279 : void Tools::interpretRanges(std::vector<std::string>&s) {
     337             :   std::vector<std::string> news;
     338      612316 :   for(const auto & p :s) {
     339      564037 :     news.push_back(p);
     340      564037 :     size_t dash=p.find("-");
     341      564708 :     if(dash==std::string::npos) continue;
     342             :     int first;
     343        6078 :     if(!Tools::convertToAny(p.substr(0,dash),first)) continue;
     344        2368 :     int stride=1;
     345             :     int second;
     346        2368 :     size_t colon=p.substr(dash+1).find(":");
     347        2368 :     if(colon!=std::string::npos) {
     348         195 :       if(!Tools::convertToAny(p.substr(dash+1).substr(0,colon),second) ||
     349         260 :           !Tools::convertToAny(p.substr(dash+1).substr(colon+1),stride)) continue;
     350             :     } else {
     351        4606 :       if(!Tools::convertToAny(p.substr(dash+1),second)) continue;
     352             :     }
     353        2368 :     news.resize(news.size()-1);
     354        2368 :     if(first<=second) {
     355        2367 :       plumed_massert(stride>0,"interpreting ranges "+ p + ", stride should be positive");
     356      566151 :       for(int i=first; i<=second; i+=stride) {
     357             :         std::string ss;
     358      563784 :         convert(i,ss);
     359      563784 :         news.push_back(ss);
     360             :       }
     361             :     } else {
     362           1 :       plumed_massert(stride<0,"interpreting ranges "+ p + ", stride should be positive");
     363           3 :       for(int i=first; i>=second; i+=stride) {
     364             :         std::string ss;
     365           2 :         convert(i,ss);
     366           2 :         news.push_back(ss);
     367             :       }
     368             :     }
     369             :   }
     370       48279 :   s=news;
     371       48279 : }
     372             : 
     373       72909 : void Tools::interpretLabel(std::vector<std::string>&s) {
     374       72909 :   if(s.size()<2)return;
     375       72570 :   std::string s0=s[0];
     376       72570 :   unsigned l=s0.length();
     377       72570 :   if(l<1) return;
     378       72570 :   if(s0[l-1]==':') {
     379             :     s[0]=s[1];
     380      136080 :     s[1]="LABEL="+s0.substr(0,l-1);
     381             :   }
     382       72570 :   std::transform(s[0].begin(), s[0].end(), s[0].begin(), ::toupper);
     383             : }
     384             : 
     385        9142 : std::vector<std::string> Tools::ls(const std::string&d) {
     386             :   std::vector<std::string> result;
     387      146770 :   for (auto const& dir_entry : std::filesystem::directory_iterator{d}) {
     388      358032 :     result.push_back(dir_entry.path().filename());
     389             :   }
     390        9142 :   return result;
     391           0 : }
     392             : 
     393        4362 : void Tools::stripLeadingAndTrailingBlanks( std::string& str ) {
     394        4362 :   std::size_t first=str.find_first_not_of(' ');
     395        4362 :   std::size_t last=str.find_last_not_of(' ');
     396        8687 :   if( first<=last && first!=std::string::npos) str=str.substr(first,last+1);
     397        4362 : }
     398             : 
     399       13069 : std::string Tools::extension(const std::string&s) {
     400       13069 :   size_t n=s.find_last_of(".");
     401             :   std::string ext;
     402       13069 :   if(n!=std::string::npos && n+1<s.length() && n+5>=s.length()) {
     403        9289 :     ext=s.substr(n+1);
     404        9289 :     if(ext.find("/")!=std::string::npos) ext="";
     405        9289 :     std::string base=s.substr(0,n);
     406        9289 :     if(base.length()==0) ext="";
     407        9289 :     if(base.length()>0 && base[base.length()-1]=='/') ext="";
     408             :   }
     409       13069 :   return ext;
     410             : }
     411             : 
     412           0 : double Tools::bessel0( const double& val ) {
     413           0 :   if (std::abs(val)<3.75) {
     414           0 :     double y = Tools::fastpow( val/3.75, 2 );
     415           0 :     return 1 + y*(3.5156229 +y*(3.0899424 + y*(1.2067492+y*(0.2659732+y*(0.0360768+y*0.0045813)))));
     416             :   }
     417           0 :   double ax=std::abs(val), y=3.75/ax, bx=std::exp(ax)/std::sqrt(ax);
     418           0 :   ax=0.39894228+y*(0.01328592+y*(0.00225319+y*(-0.00157565+y*(0.00916281+y*(-0.02057706+y*(0.02635537+y*(-0.01647633+y*0.00392377)))))));
     419           0 :   return ax*bx;
     420             : }
     421             : 
     422   437878862 : bool Tools::startWith(const std::string & full,const std::string &start) {
     423   437878862 :   return (full.substr(0,start.length())==start);
     424             : }
     425             : 
     426      395237 : bool Tools::findKeyword(const std::vector<std::string>&line,const std::string&key) {
     427      395237 :   const std::string search(key+"=");
     428   433045734 :   for(const auto & p : line) {
     429   432798877 :     if(startWith(p,search)) return true;
     430             :   }
     431             :   return false;
     432             : }
     433             : 
     434         521 : Tools::DirectoryChanger::DirectoryChanger(const char*path):
     435         521 :   path(std::filesystem::current_path())
     436             : {
     437         521 :   if(!path) return;
     438         521 :   if(std::strlen(path)==0) return;
     439           4 :   std::filesystem::current_path(path);
     440             : }
     441             : 
     442         520 : Tools::DirectoryChanger::~DirectoryChanger() {
     443             :   try {
     444         520 :     std::filesystem::current_path(path);
     445           0 :   } catch(std::filesystem::filesystem_error & e) {
     446           0 :     std::fprintf(stderr,"+++ WARNING: cannot cd back to directory %s\n",path.c_str());
     447           0 :   }
     448         520 : }
     449             : 
     450       19176 : std::unique_ptr<std::lock_guard<std::mutex>> Tools::molfile_lock() {
     451             :   static std::mutex mtx;
     452       19176 :   return Tools::make_unique<std::lock_guard<std::mutex>>(mtx);
     453             : }
     454             : 
     455             : /// Internal tool, I am keeping it private for now
     456             : namespace {
     457             : 
     458             : class process_one_exception {
     459             :   std::string & msg;
     460             :   bool first=true;
     461         216 :   void update() {
     462         216 :     if(!first) msg+="\n\nThe above exception was the direct cause of the following exception:\n";
     463         216 :     first=false;
     464         216 :   }
     465             : public:
     466         202 :   process_one_exception(std::string & msg):
     467         202 :     msg(msg)
     468             :   {}
     469         215 :   void operator()(const std::exception & e) {
     470         215 :     update();
     471         215 :     msg+=e.what();
     472         215 :   }
     473           0 :   void operator()(const std::string & e) {
     474           0 :     update();
     475           0 :     msg+=e;
     476           0 :   }
     477           1 :   void operator()(const char* e) {
     478           1 :     update();
     479           1 :     msg+=e;
     480           1 :   }
     481             : };
     482             : 
     483             : template<class T>
     484         216 : static void process_all_exceptions(T&& f) {
     485             :   try {
     486             :     // First throw the current exception
     487         216 :     throw;
     488         230 :   } catch(const std::nested_exception & e) {
     489             :     // If nested, we go recursive
     490             :     // notice that we apply function f only if exception is also a std::exception
     491             :     try {
     492          14 :       e.rethrow_nested();
     493          28 :     } catch(...) {
     494          14 :       process_all_exceptions(f);
     495             :     }
     496          14 :     auto d=dynamic_cast<const std::exception*>(&e);
     497          14 :     if(d) f(*d);
     498         402 :   } catch(const std::exception &e) {
     499             :     // If not nested, we end recursion
     500         201 :     f(e);
     501           0 :   } catch(const std::string &e) {
     502             :     // If not nested, we end recursion
     503           0 :     f(e);
     504           2 :   } catch(const char* e) {
     505             :     // If not nested, we end recursion
     506           1 :     f(e);
     507           0 :   } catch(...) {
     508             :     // If not nested and of unknown type, we stop the chain
     509             :   }
     510         216 : }
     511             : 
     512             : }
     513             : 
     514         202 : std::string Tools::concatenateExceptionMessages() {
     515             :   std::string msg;
     516         202 :   process_all_exceptions(process_one_exception(msg));
     517         202 :   return msg;
     518             : }
     519             : 
     520             : }

Generated by: LCOV version 1.16