1 //  (C) Copyright Gennadiy Rozental 2001.
2 //  Use, modification, and distribution are subject to the
3 //  Boost Software License, Version 1.0. (See accompanying file
4 //  LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
5 
6 //  See http://www.boost.org/libs/test for the library home page.
7 //
8 //!@file
9 //!@brief CLA parser
10 // ***************************************************************************
11 
12 #ifndef BOOST_TEST_UTILS_RUNTIME_CLA_PARSER_HPP
13 #define BOOST_TEST_UTILS_RUNTIME_CLA_PARSER_HPP
14 
15 // Boost.Test Runtime parameters
16 #include <boost/test/utils/runtime/argument.hpp>
17 #include <boost/test/utils/runtime/modifier.hpp>
18 #include <boost/test/utils/runtime/parameter.hpp>
19 
20 #include <boost/test/utils/runtime/cla/argv_traverser.hpp>
21 
22 // Boost.Test
23 #include <boost/test/utils/foreach.hpp>
24 #include <boost/test/utils/algorithm.hpp>
25 #include <boost/test/detail/throw_exception.hpp>
26 #include <boost/test/detail/global_typedef.hpp>
27 
28 #include <boost/algorithm/cxx11/all_of.hpp> // !! ?? unnecessary after cxx11
29 
30 // STL
31 // !! ?? #include <unordered_set>
32 #include <set>
33 #include <iostream>
34 
35 #include <boost/test/detail/suppress_warnings.hpp>
36 
37 namespace boost {
38 namespace runtime {
39 namespace cla {
40 
41 // ************************************************************************** //
42 // **************         runtime::cla::parameter_trie         ************** //
43 // ************************************************************************** //
44 
45 namespace rt_cla_detail {
46 
47 struct parameter_trie;
48 typedef shared_ptr<parameter_trie> parameter_trie_ptr;
49 typedef std::map<char,parameter_trie_ptr> trie_per_char;
50 typedef std::vector<boost::reference_wrapper<parameter_cla_id const> > param_cla_id_list;
51 
52 struct parameter_trie {
parameter_trieboost::runtime::cla::rt_cla_detail::parameter_trie53     parameter_trie() : m_has_final_candidate( false ) {}
54 
55     /// If subtrie corresponding to the char c exists returns it otherwise creates new
make_subtrieboost::runtime::cla::rt_cla_detail::parameter_trie56     parameter_trie_ptr  make_subtrie( char c )
57     {
58         trie_per_char::const_iterator it = m_subtrie.find( c );
59 
60         if( it == m_subtrie.end() )
61             it = m_subtrie.insert( std::make_pair( c, parameter_trie_ptr( new parameter_trie ) ) ).first;
62 
63         return it->second;
64     }
65 
66     /// Creates series of sub-tries per characters in a string
make_subtrieboost::runtime::cla::rt_cla_detail::parameter_trie67     parameter_trie_ptr  make_subtrie( cstring s )
68     {
69         parameter_trie_ptr res;
70 
71         BOOST_TEST_FOREACH( char, c, s )
72             res = (res ? res->make_subtrie( c ) : make_subtrie( c ));
73 
74         return res;
75     }
76 
77     /// Registers candidate parameter for this subtrie. If final, it needs to be unique
add_candidate_idboost::runtime::cla::rt_cla_detail::parameter_trie78     void                add_candidate_id( parameter_cla_id const& param_id, basic_param_ptr param_candidate, bool final )
79     {
80         BOOST_TEST_I_ASSRT( !m_has_final_candidate && (!final || m_id_candidates.empty()),
81           conflicting_param() << "Parameter cla id " << param_id.m_tag << " conflicts with the "
82                               << "parameter cla id " << m_id_candidates.back().get().m_tag );
83 
84         m_has_final_candidate = final;
85         m_id_candidates.push_back( ref(param_id) );
86 
87         if( m_id_candidates.size() == 1 )
88             m_param_candidate = param_candidate;
89         else
90             m_param_candidate.reset();
91     }
92 
93     /// Gets subtrie for specified char if present or nullptr otherwise
get_subtrieboost::runtime::cla::rt_cla_detail::parameter_trie94     parameter_trie_ptr  get_subtrie( char c ) const
95     {
96         trie_per_char::const_iterator it = m_subtrie.find( c );
97 
98         return it != m_subtrie.end() ? it->second : parameter_trie_ptr();
99     }
100 
101     // Data members
102     trie_per_char       m_subtrie;
103     param_cla_id_list   m_id_candidates;
104     basic_param_ptr     m_param_candidate;
105     bool                m_has_final_candidate;
106 };
107 
108 // ************************************************************************** //
109 // **************      runtime::cla::report_foreing_token      ************** //
110 // ************************************************************************** //
111 
112 static void
report_foreing_token(cstring program_name,cstring token)113 report_foreing_token( cstring program_name, cstring token )
114 {
115     std::cerr << "Boost.Test WARNING: token \"" << token << "\" does not correspond to the Boost.Test argument \n"
116               << "                    and should be placed after all Boost.Test arguments and the -- separator.\n"
117               << "                    For example: " << program_name << " --random -- " << token << "\n";
118 }
119 
120 } // namespace rt_cla_detail
121 
122 // ************************************************************************** //
123 // **************             runtime::cla::parser             ************** //
124 // ************************************************************************** //
125 
126 class parser {
127 public:
128     /// Initializes a parser and builds internal trie representation used for
129     /// parsing based on the supplied parameters
130 #ifndef BOOST_NO_CXX11_FUNCTION_TEMPLATE_DEFAULT_ARGS
131     template<typename Modifiers=nfp::no_params_type>
parser(parameters_store const & parameters,Modifiers const & m=nfp::no_params)132     parser( parameters_store const& parameters, Modifiers const& m = nfp::no_params )
133 #else
134     template<typename Modifiers>
135     parser( parameters_store const& parameters, Modifiers const& m )
136 #endif
137     {
138         nfp::opt_assign( m_end_of_param_indicator, m, end_of_params );
139         nfp::opt_assign( m_negation_prefix, m, negation_prefix );
140 
141         BOOST_TEST_I_ASSRT( algorithm::all_of( m_end_of_param_indicator.begin(),
142                                                m_end_of_param_indicator.end(),
143                                                parameter_cla_id::valid_prefix_char ),
144                             invalid_cla_id() << "End of parameters indicator can only consist of prefix characters." );
145 
146         BOOST_TEST_I_ASSRT( algorithm::all_of( m_negation_prefix.begin(),
147                                                m_negation_prefix.end(),
148                                                parameter_cla_id::valid_name_char ),
149                             invalid_cla_id() << "Negation prefix can only consist of prefix characters." );
150 
151         build_trie( parameters );
152     }
153 
154     // input processing method
155     int
parse(int argc,char ** argv,runtime::arguments_store & res)156     parse( int argc, char** argv, runtime::arguments_store& res )
157     {
158         // save program name for help message
159         m_program_name = argv[0];
160         cstring path_sep( "\\/" );
161 
162         cstring::iterator it = unit_test::utils::find_last_of( m_program_name.begin(), m_program_name.end(),
163                                                                 path_sep.begin(), path_sep.end() );
164         if( it != m_program_name.end() )
165             m_program_name.trim_left( it + 1 );
166 
167         // Set up the traverser
168         argv_traverser tr( argc, (char const**)argv );
169 
170         // Loop till we reach end of input
171         while( !tr.eoi() ) {
172             cstring curr_token = tr.current_token();
173 
174             cstring prefix;
175             cstring name;
176             cstring value_separator;
177             bool    negative_form = false;
178 
179             // Perform format validations and split the argument into prefix, name and separator
180             // False return value indicates end of params indicator is met
181             if( !validate_token_format( curr_token, prefix, name, value_separator, negative_form ) ) {
182                 // get rid of "end of params" token
183                 tr.next_token();
184                 break;
185             }
186 
187             // Locate trie corresponding to found prefix and skip it in the input
188             trie_ptr curr_trie = m_param_trie[prefix];
189 
190             if( !curr_trie ) {
191                 //  format_error() << "Unrecognized parameter prefix in the argument " << tr.current_token()
192                 rt_cla_detail::report_foreing_token( m_program_name, curr_token );
193                 tr.save_token();
194                 continue;
195             }
196 
197             curr_token.trim_left( prefix.size() );
198 
199             // Locate parameter based on a name and skip it in the input
200             locate_result locate_res = locate_parameter( curr_trie, name, curr_token );
201             parameter_cla_id const& found_id    = locate_res.first;
202             basic_param_ptr         found_param = locate_res.second;
203 
204             if( negative_form ) {
205                 BOOST_TEST_I_ASSRT( found_id.m_negatable,
206                                     format_error( found_param->p_name )
207                                         << "Parameter tag " << found_id.m_tag << " is not negatable." );
208 
209                 curr_token.trim_left( m_negation_prefix.size() );
210             }
211 
212             curr_token.trim_left( name.size() );
213 
214             bool should_go_to_next = true;
215             cstring value;
216 
217 
218             // Skip validations if parameter has optional value and we are at the end of token
219             if( !value_separator.is_empty() || !found_param->p_has_optional_value ) {
220 
221                 // we are given a separator or there is no optional value
222 
223                 // Validate and skip value separator in the input
224                 BOOST_TEST_I_ASSRT( found_id.m_value_separator == value_separator,
225                                     format_error( found_param->p_name )
226                                         << "Invalid separator for the parameter "
227                                         << found_param->p_name
228                                         << " in the argument " << tr.current_token() );
229 
230                 curr_token.trim_left( value_separator.size() );
231 
232                 // Deduce value source
233                 value = curr_token;
234                 if( value.is_empty() ) {
235                     tr.next_token();
236                     value = tr.current_token();
237                 }
238 
239                 BOOST_TEST_I_ASSRT( !value.is_empty(),
240                                     format_error( found_param->p_name )
241                                         << "Missing an argument value for the parameter "
242                                         << found_param->p_name
243                                         << " in the argument " << tr.current_token() );
244             }
245             else if( (value_separator.is_empty() && found_id.m_value_separator.empty()) ) {
246                 // Deduce value source
247                 value = curr_token;
248                 if( value.is_empty() ) {
249                     tr.next_token(); // tokenization broke the value, we check the next one
250 
251                     if(!found_param->p_has_optional_value) {
252                         // there is no separator and there is no optional value
253                         // we look for the value on the next token
254                         // example "-t XXXX" (no default)
255                         // and we commit this value as being the passed value
256                         value = tr.current_token();
257                     }
258                     else {
259                         // there is no separator and the value is optional
260                         // we check the next token
261                         // example "-c" (defaults to true)
262                         // and commit this as the value if this is not a token
263                         cstring value_check = tr.current_token();
264 
265                         cstring prefix_test, name_test, value_separator_test;
266                         bool negative_form_test;
267                         if( validate_token_format( value_check, prefix_test, name_test, value_separator_test, negative_form_test )
268                             && m_param_trie[prefix_test]) {
269                           // this is a token, we consume what we have
270                           should_go_to_next = false;
271                         }
272                         else {
273                           // this is a value, we commit it
274                           value = value_check;
275                         }
276                     }
277                 }
278             }
279 
280             // Validate against argument duplication
281             BOOST_TEST_I_ASSRT( !res.has( found_param->p_name ) || found_param->p_repeatable,
282                                 duplicate_arg( found_param->p_name )
283                                     << "Duplicate argument value for the parameter "
284                                     << found_param->p_name
285                                     << " in the argument " << tr.current_token() );
286 
287             // Produce argument value
288             found_param->produce_argument( value, negative_form, res );
289 
290             if(should_go_to_next) {
291                 tr.next_token();
292             }
293         }
294 
295         // generate the remainder and return it's size
296         return tr.remainder();
297     }
298 
299     // help/usage/version
300     void
version(std::ostream & ostr)301     version( std::ostream& ostr )
302     {
303        ostr << "Boost.Test module ";
304 
305 #if defined(BOOST_TEST_MODULE)
306        // we do not want to refer to the master test suite there
307        ostr << '\'' << BOOST_TEST_STRINGIZE( BOOST_TEST_MODULE ).trim( "\"" ) << "' ";
308 #endif
309 
310        ostr << "in executable '" << m_program_name << "'\n";
311        ostr << "Compiled from Boost version "
312             << BOOST_VERSION/100000      << "."
313             << BOOST_VERSION/100 % 1000  << "."
314             << BOOST_VERSION % 100       ;
315        ostr << " with ";
316 #if defined(BOOST_TEST_INCLUDED)
317        ostr << "header-only inclusion of";
318 #elif defined(BOOST_TEST_DYN_LINK)
319        ostr << "dynamic linking to";
320 #else
321        ostr << "static linking to";
322 #endif
323        ostr << " Boost.Test\n";
324        ostr << "- Compiler: " << BOOST_COMPILER << '\n'
325             << "- Platform: " << BOOST_PLATFORM << '\n'
326             << "- STL     : " << BOOST_STDLIB;
327        ostr << std::endl;
328     }
329 
330     void
usage(std::ostream & ostr,cstring param_name=cstring (),bool use_color=true)331     usage(std::ostream& ostr,
332           cstring param_name = cstring(),
333           bool use_color = true)
334     {
335         namespace utils = unit_test::utils;
336         namespace ut_detail = unit_test::ut_detail;
337 
338         if( !param_name.is_empty() ) {
339             basic_param_ptr param = locate_parameter( m_param_trie[help_prefix], param_name, "" ).second;
340             param->usage( ostr, m_negation_prefix );
341         }
342         else {
343             ostr << "\n  The program '" << m_program_name << "' is a Boost.Test module containing unit tests.";
344 
345             {
346               BOOST_TEST_SCOPE_SETCOLOR( use_color, ostr, term_attr::BRIGHT, term_color::ORIGINAL );
347               ostr << "\n\n  Usage\n    ";
348             }
349 
350             {
351                 BOOST_TEST_SCOPE_SETCOLOR( use_color, ostr, term_attr::BRIGHT, term_color::GREEN );
352                 ostr << m_program_name << " [Boost.Test argument]... ";
353             }
354             if( !m_end_of_param_indicator.empty() ) {
355                 BOOST_TEST_SCOPE_SETCOLOR( use_color, ostr, term_attr::BRIGHT, term_color::YELLOW );
356                 ostr << '[' << m_end_of_param_indicator << " [custom test module argument]...]";
357             }
358         }
359 
360         ostr << "\n\n  Use\n      ";
361         {
362 
363             BOOST_TEST_SCOPE_SETCOLOR( use_color, ostr, term_attr::BRIGHT, term_color::GREEN );
364             ostr << m_program_name << " --help";
365         }
366         ostr << "\n  or  ";
367         {
368             BOOST_TEST_SCOPE_SETCOLOR( use_color, ostr, term_attr::BRIGHT, term_color::GREEN );
369             ostr << m_program_name << " --help=<parameter name>";
370         }
371         ostr << "\n  for detailed help on Boost.Test parameters.\n";
372     }
373 
374     void
help(std::ostream & ostr,parameters_store const & parameters,cstring param_name,bool use_color=true)375     help(std::ostream& ostr,
376          parameters_store const& parameters,
377          cstring param_name,
378          bool use_color = true)
379     {
380         namespace utils = unit_test::utils;
381         namespace ut_detail = unit_test::ut_detail;
382 
383         if( !param_name.is_empty() ) {
384             basic_param_ptr param = locate_parameter( m_param_trie[help_prefix], param_name, "" ).second;
385             param->help( ostr, m_negation_prefix, use_color);
386             return;
387         }
388 
389         usage(ostr, cstring(), use_color);
390 
391         ostr << "\n\n";
392         {
393           BOOST_TEST_SCOPE_SETCOLOR( use_color, ostr, term_attr::BRIGHT, term_color::ORIGINAL );
394           ostr << "  Command line flags:\n";
395         }
396         runtime::commandline_pretty_print(
397             ostr,
398             "   ",
399             "The command line flags of Boost.Test are listed below. "
400             "All parameters are optional. You can specify parameter value either "
401             "as a command line argument or as a value of its corresponding environment "
402             "variable. If a flag is specified as a command line argument and an environment variable "
403             "at the same time, the command line takes precedence. "
404             "The command line argument "
405             "support name guessing, and works with shorter names as long as those are not ambiguous."
406         );
407 
408         if( !m_end_of_param_indicator.empty() ) {
409             ostr << "\n\n   All the arguments after the '";
410             {
411                 BOOST_TEST_SCOPE_SETCOLOR( use_color, ostr, term_attr::BRIGHT, term_color::YELLOW );
412                 ostr << m_end_of_param_indicator;
413             }
414             ostr << "' are ignored by Boost.Test.";
415         }
416 
417 
418         {
419           BOOST_TEST_SCOPE_SETCOLOR( use_color, ostr, term_attr::BRIGHT, term_color::ORIGINAL );
420           ostr << "\n\n  Environment variables:\n";
421         }
422         runtime::commandline_pretty_print(
423             ostr,
424             "   ",
425             "Every argument listed below may also be set by a corresponding environment"
426             "variable. For an argument '--argument_x=<value>', the corresponding "
427             "environment variable is 'BOOST_TEST_ARGUMENT_X=value"
428         );
429 
430 
431 
432         ostr << "\n\n  The following parameters are supported:\n";
433 
434         BOOST_TEST_FOREACH(
435             parameters_store::storage_type::value_type const&,
436             v,
437             parameters.all() )
438         {
439             basic_param_ptr param = v.second;
440             ostr << "\n";
441             param->usage( ostr, m_negation_prefix, use_color);
442         }
443 
444     }
445 
446 private:
447     typedef rt_cla_detail::parameter_trie_ptr   trie_ptr;
448     typedef rt_cla_detail::trie_per_char        trie_per_char;
449     typedef std::map<cstring,trie_ptr>          str_to_trie;
450 
451     void
build_trie(parameters_store const & parameters)452     build_trie( parameters_store const& parameters )
453     {
454         // Iterate over all parameters
455         BOOST_TEST_FOREACH( parameters_store::storage_type::value_type const&, v, parameters.all() ) {
456             basic_param_ptr param = v.second;
457 
458             // Register all parameter's ids in trie.
459             BOOST_TEST_FOREACH( parameter_cla_id const&, id, param->cla_ids() ) {
460                 // This is the trie corresponding to the prefix.
461                 trie_ptr next_trie = m_param_trie[id.m_prefix];
462                 if( !next_trie )
463                     next_trie = m_param_trie[id.m_prefix] = trie_ptr( new rt_cla_detail::parameter_trie );
464 
465                 // Build the trie, by following name's characters
466                 // and register this parameter as candidate on each level
467                 for( size_t index = 0; index < id.m_tag.size(); ++index ) {
468                     next_trie = next_trie->make_subtrie( id.m_tag[index] );
469 
470                     next_trie->add_candidate_id( id, param, index == (id.m_tag.size() - 1) );
471                 }
472             }
473         }
474     }
475 
476     bool
validate_token_format(cstring token,cstring & prefix,cstring & name,cstring & separator,bool & negative_form)477     validate_token_format( cstring token, cstring& prefix, cstring& name, cstring& separator, bool& negative_form )
478     {
479         // Match prefix
480         cstring::iterator it = token.begin();
481         while( it != token.end() && parameter_cla_id::valid_prefix_char( *it ) )
482             ++it;
483 
484         prefix.assign( token.begin(), it );
485 
486         if( prefix.empty() )
487             return true;
488 
489         // Match name
490         while( it != token.end() && parameter_cla_id::valid_name_char( *it ) )
491             ++it;
492 
493         name.assign( prefix.end(), it );
494 
495         if( name.empty() ) {
496             if( prefix == m_end_of_param_indicator )
497                 return false;
498 
499             BOOST_TEST_I_THROW( format_error() << "Invalid format for an actual argument " << token );
500         }
501 
502         // Match value separator
503         while( it != token.end() && parameter_cla_id::valid_separator_char( *it ) )
504             ++it;
505 
506         separator.assign( name.end(), it );
507 
508         // Match negation prefix
509         negative_form = !m_negation_prefix.empty() && ( name.substr( 0, m_negation_prefix.size() ) == m_negation_prefix );
510         if( negative_form )
511             name.trim_left( m_negation_prefix.size() );
512 
513         return true;
514     }
515 
516     // C++03: cannot have references as types
517     typedef std::pair<parameter_cla_id, basic_param_ptr> locate_result;
518 
519     locate_result
locate_parameter(trie_ptr curr_trie,cstring name,cstring token)520     locate_parameter( trie_ptr curr_trie, cstring name, cstring token )
521     {
522         std::vector<trie_ptr> typo_candidates;
523         std::vector<trie_ptr> next_typo_candidates;
524         trie_ptr next_trie;
525 
526         BOOST_TEST_FOREACH( char, c, name ) {
527             if( curr_trie ) {
528                 // locate next subtrie corresponding to the char
529                 next_trie = curr_trie->get_subtrie( c );
530 
531                 if( next_trie )
532                     curr_trie = next_trie;
533                 else {
534                     // Initiate search for typo candicates. We will account for 'wrong char' typo
535                     // 'missing char' typo and 'extra char' typo
536                     BOOST_TEST_FOREACH( trie_per_char::value_type const&, typo_cand, curr_trie->m_subtrie ) {
537                         // 'wrong char' typo
538                         typo_candidates.push_back( typo_cand.second );
539 
540                         // 'missing char' typo
541                         if( (next_trie = typo_cand.second->get_subtrie( c )) )
542                             typo_candidates.push_back( next_trie );
543                     }
544 
545                     // 'extra char' typo
546                     typo_candidates.push_back( curr_trie );
547 
548                     curr_trie.reset();
549                 }
550             }
551             else {
552                 // go over existing typo candidates and see if they are still viable
553                 BOOST_TEST_FOREACH( trie_ptr, typo_cand, typo_candidates ) {
554                     trie_ptr next_typo_cand = typo_cand->get_subtrie( c );
555 
556                     if( next_typo_cand )
557                         next_typo_candidates.push_back( next_typo_cand );
558                 }
559 
560                 next_typo_candidates.swap( typo_candidates );
561                 next_typo_candidates.clear();
562             }
563         }
564 
565         if( !curr_trie ) {
566             std::vector<cstring> typo_candidate_names;
567             std::set<parameter_cla_id const*> unique_typo_candidate; // !! ?? unordered_set
568             typo_candidate_names.reserve( typo_candidates.size() );
569 // !! ??            unique_typo_candidate.reserve( typo_candidates.size() );
570 
571             BOOST_TEST_FOREACH( trie_ptr, trie_cand, typo_candidates ) {
572                 // avoid ambiguos candidate trie
573                 if( trie_cand->m_id_candidates.size() > 1 )
574                     continue;
575 
576                 BOOST_TEST_FOREACH( parameter_cla_id const&, param_cand, trie_cand->m_id_candidates ) {
577                     if( !unique_typo_candidate.insert( &param_cand ).second )
578                         continue;
579 
580                     typo_candidate_names.push_back( param_cand.m_tag );
581                 }
582             }
583 
584 #ifndef BOOST_NO_CXX11_RVALUE_REFERENCES
585             BOOST_TEST_I_THROW( unrecognized_param( std::move(typo_candidate_names) )
586                                 << "An unrecognized parameter in the argument "
587                                 << token );
588 #else
589             BOOST_TEST_I_THROW( unrecognized_param( typo_candidate_names )
590                                 << "An unrecognized parameter in the argument "
591                                 << token );
592 #endif
593         }
594 
595         if( curr_trie->m_id_candidates.size() > 1 ) {
596             std::vector<cstring> amb_names;
597             BOOST_TEST_FOREACH( parameter_cla_id const&, param_id, curr_trie->m_id_candidates )
598                 amb_names.push_back( param_id.m_tag );
599 
600 #ifndef BOOST_NO_CXX11_RVALUE_REFERENCES
601             BOOST_TEST_I_THROW( ambiguous_param( std::move( amb_names ) )
602                                 << "An ambiguous parameter name in the argument " << token );
603 #else
604             BOOST_TEST_I_THROW( ambiguous_param( amb_names )
605                                 << "An ambiguous parameter name in the argument " << token );
606 #endif
607         }
608 
609         return locate_result( curr_trie->m_id_candidates.back().get(), curr_trie->m_param_candidate );
610     }
611 
612     // Data members
613     cstring     m_program_name;
614     std::string m_end_of_param_indicator;
615     std::string m_negation_prefix;
616     str_to_trie m_param_trie;
617 };
618 
619 } // namespace cla
620 } // namespace runtime
621 } // namespace boost
622 
623 #include <boost/test/detail/enable_warnings.hpp>
624 
625 #endif // BOOST_TEST_UTILS_RUNTIME_CLA_PARSER_HPP
626