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 
27 #include <boost/algorithm/cxx11/all_of.hpp> // !! ?? unnecessary after cxx11
28 
29 // STL
30 // !! ?? #include <unordered_set>
31 #include <set>
32 #include <iostream>
33 
34 #include <boost/test/detail/suppress_warnings.hpp>
35 
36 namespace boost {
37 namespace runtime {
38 namespace cla {
39 
40 // ************************************************************************** //
41 // **************         runtime::cla::parameter_trie         ************** //
42 // ************************************************************************** //
43 
44 namespace rt_cla_detail {
45 
46 struct parameter_trie;
47 typedef shared_ptr<parameter_trie> parameter_trie_ptr;
48 typedef std::map<char,parameter_trie_ptr> trie_per_char;
49 typedef std::vector<boost::reference_wrapper<parameter_cla_id const> > param_cla_id_list;
50 
51 struct parameter_trie {
parameter_trieboost::runtime::cla::rt_cla_detail::parameter_trie52     parameter_trie() : m_has_final_candidate( false ) {}
53 
54     /// If subtrie corresponding to the char c exists returns it otherwise creates new
make_subtrieboost::runtime::cla::rt_cla_detail::parameter_trie55     parameter_trie_ptr  make_subtrie( char c )
56     {
57         trie_per_char::const_iterator it = m_subtrie.find( c );
58 
59         if( it == m_subtrie.end() )
60             it = m_subtrie.insert( std::make_pair( c, parameter_trie_ptr( new parameter_trie ) ) ).first;
61 
62         return it->second;
63     }
64 
65     /// Creates series of sub-tries per characters in a string
make_subtrieboost::runtime::cla::rt_cla_detail::parameter_trie66     parameter_trie_ptr  make_subtrie( cstring s )
67     {
68         parameter_trie_ptr res;
69 
70         BOOST_TEST_FOREACH( char, c, s )
71             res = (res ? res->make_subtrie( c ) : make_subtrie( c ));
72 
73         return res;
74     }
75 
76     /// Registers candidate parameter for this subtrie. If final, it needs to be unique
add_candidate_idboost::runtime::cla::rt_cla_detail::parameter_trie77     void                add_candidate_id( parameter_cla_id const& param_id, basic_param_ptr param_candidate, bool final )
78     {
79         BOOST_TEST_I_ASSRT( !m_has_final_candidate && (!final || m_id_candidates.empty()),
80           conflicting_param() << "Parameter cla id " << param_id.m_tag << " conflicts with the "
81                               << "parameter cla id " << m_id_candidates.back().get().m_tag );
82 
83         m_has_final_candidate = final;
84         m_id_candidates.push_back( ref(param_id) );
85 
86         if( m_id_candidates.size() == 1 )
87             m_param_candidate = param_candidate;
88         else
89             m_param_candidate.reset();
90     }
91 
92     /// Gets subtrie for specified char if present or nullptr otherwise
get_subtrieboost::runtime::cla::rt_cla_detail::parameter_trie93     parameter_trie_ptr  get_subtrie( char c ) const
94     {
95         trie_per_char::const_iterator it = m_subtrie.find( c );
96 
97         return it != m_subtrie.end() ? it->second : parameter_trie_ptr();
98     }
99 
100     // Data members
101     trie_per_char       m_subtrie;
102     param_cla_id_list   m_id_candidates;
103     basic_param_ptr     m_param_candidate;
104     bool                m_has_final_candidate;
105 };
106 
107 // ************************************************************************** //
108 // **************      runtime::cla::report_foreing_token      ************** //
109 // ************************************************************************** //
110 
111 static void
report_foreing_token(cstring program_name,cstring token)112 report_foreing_token( cstring program_name, cstring token )
113 {
114     std::cerr << "Boost.Test WARNING: token \"" << token << "\" does not correspond to the Boost.Test argument \n"
115               << "                    and should be placed after all Boost.Test arguments and the -- separator.\n"
116               << "                    For example: " << program_name << " --random -- " << token << "\n";
117 }
118 
119 } // namespace rt_cla_detail
120 
121 // ************************************************************************** //
122 // **************             runtime::cla::parser             ************** //
123 // ************************************************************************** //
124 
125 class parser {
126 public:
127     /// Initializes a parser and builds internal trie representation used for
128     /// parsing based on the supplied parameters
129 #ifndef BOOST_NO_CXX11_FUNCTION_TEMPLATE_DEFAULT_ARGS
130     template<typename Modifiers=nfp::no_params_type>
parser(parameters_store const & parameters,Modifiers const & m=nfp::no_params)131     parser( parameters_store const& parameters, Modifiers const& m = nfp::no_params )
132 #else
133     template<typename Modifiers>
134     parser( parameters_store const& parameters, Modifiers const& m )
135 #endif
136     {
137         nfp::opt_assign( m_end_of_param_indicator, m, end_of_params );
138         nfp::opt_assign( m_negation_prefix, m, negation_prefix );
139 
140         BOOST_TEST_I_ASSRT( algorithm::all_of( m_end_of_param_indicator.begin(),
141                                                m_end_of_param_indicator.end(),
142                                                parameter_cla_id::valid_prefix_char ),
143                             invalid_cla_id() << "End of parameters indicator can only consist of prefix characters." );
144 
145         BOOST_TEST_I_ASSRT( algorithm::all_of( m_negation_prefix.begin(),
146                                                m_negation_prefix.end(),
147                                                parameter_cla_id::valid_name_char ),
148                             invalid_cla_id() << "Negation prefix can only consist of prefix characters." );
149 
150         build_trie( parameters );
151     }
152 
153     // input processing method
154     int
parse(int argc,char ** argv,runtime::arguments_store & res)155     parse( int argc, char** argv, runtime::arguments_store& res )
156     {
157         // save program name for help message
158         m_program_name = argv[0];
159         cstring path_sep( "\\/" );
160 
161         cstring::iterator it = unit_test::utils::find_last_of( m_program_name.begin(), m_program_name.end(),
162                                                                 path_sep.begin(), path_sep.end() );
163         if( it != m_program_name.end() )
164             m_program_name.trim_left( it + 1 );
165 
166         // Set up the traverser
167         argv_traverser tr( argc, (char const**)argv );
168 
169         // Loop till we reach end of input
170         while( !tr.eoi() ) {
171             cstring curr_token = tr.current_token();
172 
173             cstring prefix;
174             cstring name;
175             cstring value_separator;
176             bool    negative_form = false;
177 
178             // Perform format validations and split the argument into prefix, name and separator
179             // False return value indicates end of params indicator is met
180             if( !validate_token_format( curr_token, prefix, name, value_separator, negative_form ) ) {
181                 // get rid of "end of params" token
182                 tr.next_token();
183                 break;
184             }
185 
186             // Locate trie corresponding to found prefix and skip it in the input
187             trie_ptr curr_trie = m_param_trie[prefix];
188 
189             if( !curr_trie ) {
190                 //  format_error() << "Unrecognized parameter prefix in the argument " << tr.current_token()
191                 rt_cla_detail::report_foreing_token( m_program_name, curr_token );
192                 tr.save_token();
193                 continue;
194             }
195 
196             curr_token.trim_left( prefix.size() );
197 
198             // Locate parameter based on a name and skip it in the input
199             locate_result locate_res = locate_parameter( curr_trie, name, curr_token );
200             parameter_cla_id const& found_id    = locate_res.first;
201             basic_param_ptr         found_param = locate_res.second;
202 
203             if( negative_form ) {
204                 BOOST_TEST_I_ASSRT( found_id.m_negatable,
205                                     format_error( found_param->p_name )
206                                         << "Parameter tag " << found_id.m_tag << " is not negatable." );
207 
208                 curr_token.trim_left( m_negation_prefix.size() );
209             }
210 
211             curr_token.trim_left( name.size() );
212 
213             cstring value;
214 
215             // Skip validations if parameter has optional value and we are at the end of token
216             if( !value_separator.is_empty() || !found_param->p_has_optional_value ) {
217                 // Validate and skip value separator in the input
218                 BOOST_TEST_I_ASSRT( found_id.m_value_separator == value_separator,
219                                     format_error( found_param->p_name )
220                                         << "Invalid separator for the parameter "
221                                         << found_param->p_name
222                                         << " in the argument " << tr.current_token() );
223 
224                 curr_token.trim_left( value_separator.size() );
225 
226                 // Deduce value source
227                 value = curr_token;
228                 if( value.is_empty() ) {
229                     tr.next_token();
230                     value = tr.current_token();
231                 }
232 
233                 BOOST_TEST_I_ASSRT( !value.is_empty(),
234                                     format_error( found_param->p_name )
235                                         << "Missing an argument value for the parameter "
236                                         << found_param->p_name
237                                         << " in the argument " << tr.current_token() );
238             }
239 
240             // Validate against argument duplication
241             BOOST_TEST_I_ASSRT( !res.has( found_param->p_name ) || found_param->p_repeatable,
242                                 duplicate_arg( found_param->p_name )
243                                     << "Duplicate argument value for the parameter "
244                                     << found_param->p_name
245                                     << " in the argument " << tr.current_token() );
246 
247             // Produce argument value
248             found_param->produce_argument( value, negative_form, res );
249 
250             tr.next_token();
251         }
252 
253         // generate the remainder and return it's size
254         return tr.remainder();
255     }
256 
257     // help/usage
258     void
usage(std::ostream & ostr,cstring param_name=cstring ())259     usage( std::ostream& ostr, cstring param_name = cstring() )
260     {
261         if( !param_name.is_empty() ) {
262             basic_param_ptr param = locate_parameter( m_param_trie[help_prefix], param_name, "" ).second;
263             param->usage( ostr, m_negation_prefix );
264         }
265         else {
266             ostr << "Usage: " << m_program_name << " [Boost.Test argument]... ";
267             if( !m_end_of_param_indicator.empty() )
268                 ostr << m_end_of_param_indicator << " [custom test module argument]...";
269             ostr << "\n";
270         }
271 
272         ostr << "\nFor detailed help on Boost.Test parameters use:\n"
273              << "  " << m_program_name << " --help\n"
274              << "or\n"
275              << "  " << m_program_name << " --help=<parameter name>\n";
276     }
277 
278     void
help(std::ostream & ostr,parameters_store const & parameters,cstring param_name)279     help( std::ostream& ostr, parameters_store const& parameters, cstring param_name )
280     {
281         if( !param_name.is_empty() ) {
282             basic_param_ptr param = locate_parameter( m_param_trie[help_prefix], param_name, "" ).second;
283             param->help( ostr, m_negation_prefix );
284             return;
285         }
286 
287         ostr << "Usage: " << m_program_name << " [Boost.Test argument]... ";
288         if( !m_end_of_param_indicator.empty() )
289             ostr << m_end_of_param_indicator << " [custom test module argument]...";
290 
291         ostr << "\n\nBoost.Test arguments correspond to parameters listed below. "
292                 "All parameters are optional. You can use specify parameter value either "
293                 "as a command line argument or as a value of corresponding environment "
294                 "variable. In case if argument for the same parameter is specified in both "
295                 "places, command line is taking precedence. Command line argument format "
296                 "supports parameter name guessing, so you can use any unambiguous "
297                 "prefix to identify a parameter.";
298         if( !m_end_of_param_indicator.empty() )
299             ostr << " All the arguments after the " << m_end_of_param_indicator << " are ignored by the Boost.Test.";
300 
301         ostr << "\n\nBoost.Test supports following parameters:\n";
302 
303         BOOST_TEST_FOREACH( parameters_store::storage_type::value_type const&, v, parameters.all() ) {
304             basic_param_ptr param = v.second;
305 
306             param->usage( ostr, m_negation_prefix );
307         }
308 
309         ostr << "\nUse --help=<parameter name> to display detailed help for specific parameter.\n";
310     }
311 
312 private:
313     typedef rt_cla_detail::parameter_trie_ptr   trie_ptr;
314     typedef rt_cla_detail::trie_per_char        trie_per_char;
315     typedef std::map<cstring,trie_ptr>          str_to_trie;
316 
317     void
build_trie(parameters_store const & parameters)318     build_trie( parameters_store const& parameters )
319     {
320         // Iterate over all parameters
321         BOOST_TEST_FOREACH( parameters_store::storage_type::value_type const&, v, parameters.all() ) {
322             basic_param_ptr param = v.second;
323 
324             // Register all parameter's ids in trie.
325             BOOST_TEST_FOREACH( parameter_cla_id const&, id, param->cla_ids() ) {
326                 // This is the trie corresponding to the prefix.
327                 trie_ptr next_trie = m_param_trie[id.m_prefix];
328                 if( !next_trie )
329                     next_trie = m_param_trie[id.m_prefix] = trie_ptr( new rt_cla_detail::parameter_trie );
330 
331                 // Build the trie, by following name's characters
332                 // and register this parameter as candidate on each level
333                 for( size_t index = 0; index < id.m_tag.size(); ++index ) {
334                     next_trie = next_trie->make_subtrie( id.m_tag[index] );
335 
336                     next_trie->add_candidate_id( id, param, index == (id.m_tag.size() - 1) );
337                 }
338             }
339         }
340     }
341 
342     bool
validate_token_format(cstring token,cstring & prefix,cstring & name,cstring & separator,bool & negative_form)343     validate_token_format( cstring token, cstring& prefix, cstring& name, cstring& separator, bool& negative_form )
344     {
345         // Match prefix
346         cstring::iterator it = token.begin();
347         while( it != token.end() && parameter_cla_id::valid_prefix_char( *it ) )
348             ++it;
349 
350         prefix.assign( token.begin(), it );
351 
352         if( prefix.empty() )
353             return true;
354 
355         // Match name
356         while( it != token.end() && parameter_cla_id::valid_name_char( *it ) )
357             ++it;
358 
359         name.assign( prefix.end(), it );
360 
361         if( name.empty() ) {
362             if( prefix == m_end_of_param_indicator )
363                 return false;
364 
365             BOOST_TEST_I_THROW( format_error() << "Invalid format for an actual argument " << token );
366         }
367 
368         // Match value separator
369         while( it != token.end() && parameter_cla_id::valid_separator_char( *it ) )
370             ++it;
371 
372         separator.assign( name.end(), it );
373 
374         // Match negation prefix
375         negative_form = !m_negation_prefix.empty() && ( name.substr( 0, m_negation_prefix.size() ) == m_negation_prefix );
376         if( negative_form )
377             name.trim_left( m_negation_prefix.size() );
378 
379         return true;
380     }
381 
382     // C++03: cannot have references as types
383     typedef std::pair<parameter_cla_id, basic_param_ptr> locate_result;
384 
385     locate_result
locate_parameter(trie_ptr curr_trie,cstring name,cstring token)386     locate_parameter( trie_ptr curr_trie, cstring name, cstring token )
387     {
388         std::vector<trie_ptr> typo_candidates;
389         std::vector<trie_ptr> next_typo_candidates;
390         trie_ptr next_trie;
391 
392         BOOST_TEST_FOREACH( char, c, name ) {
393             if( curr_trie ) {
394                 // locate next subtrie corresponding to the char
395                 next_trie = curr_trie->get_subtrie( c );
396 
397                 if( next_trie )
398                     curr_trie = next_trie;
399                 else {
400                     // Initiate search for typo candicates. We will account for 'wrong char' typo
401                     // 'missing char' typo and 'extra char' typo
402                     BOOST_TEST_FOREACH( trie_per_char::value_type const&, typo_cand, curr_trie->m_subtrie ) {
403                         // 'wrong char' typo
404                         typo_candidates.push_back( typo_cand.second );
405 
406                         // 'missing char' typo
407                         if( (next_trie = typo_cand.second->get_subtrie( c )) )
408                             typo_candidates.push_back( next_trie );
409                     }
410 
411                     // 'extra char' typo
412                     typo_candidates.push_back( curr_trie );
413 
414                     curr_trie.reset();
415                 }
416             }
417             else {
418                 // go over existing typo candidates and see if they are still viable
419                 BOOST_TEST_FOREACH( trie_ptr, typo_cand, typo_candidates ) {
420                     trie_ptr next_typo_cand = typo_cand->get_subtrie( c );
421 
422                     if( next_typo_cand )
423                         next_typo_candidates.push_back( next_typo_cand );
424                 }
425 
426                 next_typo_candidates.swap( typo_candidates );
427                 next_typo_candidates.clear();
428             }
429         }
430 
431         if( !curr_trie ) {
432             std::vector<cstring> typo_candidate_names;
433             std::set<parameter_cla_id const*> unique_typo_candidate; // !! ?? unordered_set
434             typo_candidate_names.reserve( typo_candidates.size() );
435 // !! ??            unique_typo_candidate.reserve( typo_candidates.size() );
436 
437             BOOST_TEST_FOREACH( trie_ptr, trie_cand, typo_candidates ) {
438                 // avoid ambiguos candidate trie
439                 if( trie_cand->m_id_candidates.size() > 1 )
440                     continue;
441 
442                 BOOST_TEST_FOREACH( parameter_cla_id const&, param_cand, trie_cand->m_id_candidates ) {
443                     if( !unique_typo_candidate.insert( &param_cand ).second )
444                         continue;
445 
446                     typo_candidate_names.push_back( param_cand.m_tag );
447                 }
448             }
449 
450 #ifndef BOOST_NO_CXX11_RVALUE_REFERENCES
451             BOOST_TEST_I_THROW( unrecognized_param( std::move(typo_candidate_names) )
452                                 << "An unrecognized parameter in the argument "
453                                 << token );
454 #else
455             BOOST_TEST_I_THROW( unrecognized_param( typo_candidate_names )
456                                 << "An unrecognized parameter in the argument "
457                                 << token );
458 #endif
459         }
460 
461         if( curr_trie->m_id_candidates.size() > 1 ) {
462             std::vector<cstring> amb_names;
463             BOOST_TEST_FOREACH( parameter_cla_id const&, param_id, curr_trie->m_id_candidates )
464                 amb_names.push_back( param_id.m_tag );
465 
466 #ifndef BOOST_NO_CXX11_RVALUE_REFERENCES
467             BOOST_TEST_I_THROW( ambiguous_param( std::move( amb_names ) )
468                                 << "An ambiguous parameter name in the argument " << token );
469 #else
470             BOOST_TEST_I_THROW( ambiguous_param( amb_names )
471                                 << "An ambiguous parameter name in the argument " << token );
472 #endif
473         }
474 
475         return locate_result( curr_trie->m_id_candidates.back().get(), curr_trie->m_param_candidate );
476     }
477 
478     // Data members
479     cstring     m_program_name;
480     std::string m_end_of_param_indicator;
481     std::string m_negation_prefix;
482     str_to_trie m_param_trie;
483 };
484 
485 } // namespace cla
486 } // namespace runtime
487 } // namespace boost
488 
489 #include <boost/test/detail/enable_warnings.hpp>
490 
491 #endif // BOOST_TEST_UTILS_RUNTIME_CLA_PARSER_HPP
492