Boost C++ Libraries: Ticket #12953: access to master_test_suite().{argc, argv} https://svn.boost.org/trac10/ticket/12953 <p> Hello, </p> <p> for testing I use a "loader" class to read input and expected files from filesystem. These files are used by the data-driven test cases. The path to these files are given from command line. </p> <p> Due to the fact that datasets are creating static objects on file scope, I can't access them by calling master_test_suite().{argc, argv} outside of e.g. BOOST_AUTO_TEST_CASE where the "loader" exist. </p> <p> Using the Fixture approach fails to compile, see attachement. Further, as far I understood, the files would be loaded again for each data driven test, which is not intended. </p> <p> Following the docs, I didn't found a way to get access to the (in best case from boost.test args pruned) argc/argv. Is it a missing feature and bug? </p> <p> The initial thread is on ML "[boost] [Boost.Test] access to boost::unit_test::framework::master_test_suite().{argc, argv} outside from BOOST_TEST" </p> <p> Thanks, Olaf </p> en-us Boost C++ Libraries /htdocs/site/boost.png https://svn.boost.org/trac10/ticket/12953 Trac 1.4.3 anonymous Fri, 07 Apr 2017 09:19:33 GMT attachment set https://svn.boost.org/trac10/ticket/12953 https://svn.boost.org/trac10/ticket/12953 <ul> <li><strong>attachment</strong> → <span class="trac-field-new">bt_dataset_fixture.cpp</span> </li> </ul> Ticket Raffi Enficiaud Sun, 09 Apr 2017 08:41:57 GMT <link>https://svn.boost.org/trac10/ticket/12953#comment:1 </link> <guid isPermaLink="false">https://svn.boost.org/trac10/ticket/12953#comment:1</guid> <description> <p> Thanks for the report, I believe it is better to continue this thread here :) </p> </description> <category>Ticket</category> </item> <item> <dc:creator>Raffi Enficiaud</dc:creator> <pubDate>Tue, 29 Aug 2017 21:40:31 GMT</pubDate> <title>owner, status, type changed https://svn.boost.org/trac10/ticket/12953#comment:2 https://svn.boost.org/trac10/ticket/12953#comment:2 <ul> <li><strong>owner</strong> changed from <span class="trac-author">Gennadiy Rozental</span> to <span class="trac-author">Raffi Enficiaud</span> </li> <li><strong>status</strong> <span class="trac-field-old">new</span> → <span class="trac-field-new">assigned</span> </li> <li><strong>type</strong> <span class="trac-field-old">Bugs</span> → <span class="trac-field-new">Feature Requests</span> </li> </ul> Ticket Raffi Enficiaud Tue, 02 Jan 2018 22:19:53 GMT <link>https://svn.boost.org/trac10/ticket/12953#comment:3 </link> <guid isPermaLink="false">https://svn.boost.org/trac10/ticket/12953#comment:3</guid> <description> <p> Hi there, </p> <p> I have an idea for addressing this, would you have some bandwidth to test? </p> </description> <category>Ticket</category> </item> <item> <dc:creator>Raffi Enficiaud</dc:creator> <pubDate>Wed, 03 Jan 2018 00:28:24 GMT</pubDate> <title/> <link>https://svn.boost.org/trac10/ticket/12953#comment:4 </link> <guid isPermaLink="false">https://svn.boost.org/trac10/ticket/12953#comment:4</guid> <description> <p> There is an implementation tentative in the branch <code>topic/12953-access-master_test_suite-in-datatest-cases</code>. It would be really helpful if you have some time to test the branch with your code. </p> </description> <category>Ticket</category> </item> <item> <dc:creator>Raffi Enficiaud</dc:creator> <pubDate>Wed, 03 Jan 2018 00:43:31 GMT</pubDate> <title>milestone changed https://svn.boost.org/trac10/ticket/12953#comment:5 https://svn.boost.org/trac10/ticket/12953#comment:5 <ul> <li><strong>milestone</strong> <span class="trac-field-old">To Be Determined</span> → <span class="trac-field-new">Boost 1.66.0</span> </li> </ul> Ticket ope.devel@… Sat, 23 Jun 2018 06:14:19 GMT <link>https://svn.boost.org/trac10/ticket/12953#comment:6 </link> <guid isPermaLink="false">https://svn.boost.org/trac10/ticket/12953#comment:6</guid> <description> <p> Hi Raffi, </p> <p> sorry for the later answer, I lost over the time this thread :-( Thanks for investigation but I assume it doesn't work yet (or I mis omething) </p> <p> $ bin/bt_dataset_args -- test </p> <p> <a class="missing wiki">ArgCount</a> = 0 Mock <a class="missing wiki">ArgCount</a> = 0 Running 1 test case... </p> <p> Unfortuately I don't see something to add an attachement here, therefore I embedd my test code. The '--' comment line separate different translation units, if it matters. The loader is re-used several time for completly different tests. </p> <p> Thanks, Olaf </p> <pre class="wiki">//------------------------------------------------------------------------------ #define BOOST_TEST_MODULE "Cool Parser Test Suite" #include &lt;boost/test/included/unit_test.hpp&gt; //------------------------------------------------------------------------------ #include &lt;boost/test/data/test_case.hpp&gt; #include &lt;boost/filesystem/fstream.hpp&gt; #include &lt;iostream&gt; namespace testsuite { namespace fs = boost::filesystem; struct app_mock { int const argc; char** const argv; app_mock() : argc(boost::unit_test::framework::master_test_suite().argc) , argv(boost::unit_test::framework::master_test_suite().argv) { } }; BOOST_AUTO_TEST_CASE( app_mocker ) { app_mock app; } class dataset_loader { public: typedef std::vector&lt;fs::path&gt; pathname_type; typedef std::vector&lt;std::string&gt; data_type; public: pathname_type const&amp; test_case_name() const { return m_test_case; } data_type const&amp; input() const { return m_input; } data_type const&amp; expect() const { return m_expected; } public: dataset_loader(fs::path const&amp; path); private: void read_files(fs::path const&amp; path); private: pathname_type m_test_case; data_type m_input; data_type m_expected; std::string input_extension; std::string expected_extension; }; } // namespace testsuite namespace testsuite { dataset_loader::dataset_loader(fs::path const&amp; path) : input_extension{".input" } , expected_extension{ ".expected" } { BOOST_TEST_INFO("dataset_loader load test files from " &lt;&lt; path); auto const argc{boost::unit_test::framework::master_test_suite().argc}; auto const argv{boost::unit_test::framework::master_test_suite().argv}; std::cout &lt;&lt; "ArgCount = " &lt;&lt; argc &lt;&lt; '\n'; for(unsigned i = 0; i != argc; i++) { std::cout &lt;&lt; "ArgValue = " &lt;&lt; argv[i] &lt;&lt; '\n'; } app_mock m; std::cout &lt;&lt; "Mock ArgCount = " &lt;&lt; m.argc &lt;&lt; '\n'; for(int i = 0; i != m.argc; i++) { std::cout &lt;&lt; "Mock ArgValue = " &lt;&lt; m.argv[i] &lt;&lt; '\n'; } read_files(path); } void dataset_loader::read_files(fs::path const&amp; path) { // real code suppressed, cmd line args for load prefix and extensions required BOOST_TEST_INFO("read files " &lt;&lt; path &lt;&lt; input_extension); } } // namespace testsuite //------------------------------------------------------------------------------ #include &lt;boost/test/unit_test.hpp&gt; BOOST_AUTO_TEST_SUITE( concrete_testsuite ) using ::boost::unit_test::data::monomorphic::operator^; struct concrete_dataset : public testsuite::dataset_loader { concrete_dataset() : dataset_loader{ "test_case/number_42" } { } } const concrete_dataset; BOOST_DATA_TEST_CASE(cool_test, concrete_dataset.input() ^ concrete_dataset.expect(), input, expect) { // ... BOOST_TEST(input == expect); } BOOST_AUTO_TEST_SUITE_END() </pre> </description> <category>Ticket</category> </item> <item> <dc:creator>Raffi Enficiaud</dc:creator> <pubDate>Sat, 23 Jun 2018 08:20:23 GMT</pubDate> <title/> <link>https://svn.boost.org/trac10/ticket/12953#comment:7 </link> <guid isPermaLink="false">https://svn.boost.org/trac10/ticket/12953#comment:7</guid> <description> <p> Thanks for the reply. Have you tested the branch I mentioned or the develop/master branch? Thanks for posting the code, I will try on my end and keep you posted. </p> </description> <category>Ticket</category> </item> <item> <author>ope.devel@…</author> <pubDate>Sun, 24 Jun 2018 19:01:58 GMT</pubDate> <title/> <link>https://svn.boost.org/trac10/ticket/12953#comment:8 </link> <guid isPermaLink="false">https://svn.boost.org/trac10/ticket/12953#comment:8</guid> <description> <p> Hello Raffi, </p> <p> yes, same result applies to the branch you mentioned and 1.67.0 </p> </description> <category>Ticket</category> </item> <item> <dc:creator>Raffi Enficiaud</dc:creator> <pubDate>Mon, 25 Jun 2018 20:56:55 GMT</pubDate> <title/> <link>https://svn.boost.org/trac10/ticket/12953#comment:9 </link> <guid isPermaLink="false">https://svn.boost.org/trac10/ticket/12953#comment:9</guid> <description> <p> Thanks for testing. In fact, it could not work as it was, as the concrete_dataset is built already at the declaration of the data test case. </p> <p> I made another declaration that creates a lazy evaluation of the dataset. In order to use it, you have to use a <code>make_delayed</code> construct instead. Please have a look at this: </p> <p> <a class="ext-link" href="https://github.com/boostorg/test/blob/topic/12953-access-master_test_suite-in-datatest-cases/test/test-organization-ts/dataset-master-test-suite-accessible-test.cpp#L80"><span class="icon">​</span>https://github.com/boostorg/test/blob/topic/12953-access-master_test_suite-in-datatest-cases/test/test-organization-ts/dataset-master-test-suite-accessible-test.cpp#L80</a> </p> <p> on the same branch as the one mentioned before. </p> <p> Only the zip operation over datasets is currently honouring this new construct, I will do the rest today. The test is adapted from yours as you can see. </p> <p> Let me know if this does work for you. </p> <p> Thanks, Raffi </p> </description> <category>Ticket</category> </item> <item> <author>ope-devel@…</author> <pubDate>Tue, 26 Jun 2018 12:54:58 GMT</pubDate> <title/> <link>https://svn.boost.org/trac10/ticket/12953#comment:10 </link> <guid isPermaLink="false">https://svn.boost.org/trac10/ticket/12953#comment:10</guid> <description> <p> Hello Raffi, </p> <p> thank your for your effort! But I'm not sure if I can adapted theses solution 1:1. In the real project I use the dataset as aggregate of test files and informations, the 'expected' file and as 3rd field the test case file name: </p> <pre class="wiki">class dataset_loader { public: typedef std::vector&lt;fs::path&gt; pathname_type; typedef std::vector&lt;std::string&gt; data_type; public: pathname_type const&amp; test_case_name() const { return m_test_case; } data_type const&amp; input() const { return m_input; } data_type const&amp; expect() const { return m_expected; } public: dataset_loader(fs::path const&amp; path, std::string const&amp; relative_path, // relative path from testsuite std::string const&amp; input_extension // test case file input file extension ); private: void read_files(fs::path const&amp; path); std::string read_file(fs::path const&amp; file_path); private: fs::path m_prefix_dir; pathname_type m_test_case; data_type m_input; data_type m_expected; std::string input_extension; std::string expected_extension; }; </pre><p> The class checks if there is for each 'input' file a complementary 'expect' file, otherwise the input is discarded. This allows me to skip specific tests by only simply renaming the input file. Your approach creates in my use case for the test bench 2 (isn't it?) objects to load the files. At zip time I get an error if the element numbers doesn't match. Hence, I have to take care to rename the 'expect' file too - which is a burden and error prone. Further, as far I see, no way to get the case name (not right, I see a long path from boost.test IIRC). The failure test output e.g. "/_0" gives an enumeration id, with test case name I can specify the concrete file name, see notes on bottom. </p> <p> Here is what happens under the hood (error handling not shown :-) </p> <pre class="wiki">dataset_loader::dataset_loader(fs::path const&amp; path, std::string const&amp; relative_path, std::string const&amp; input_extension_ ) : m_prefix_dir{ CMAKE_TESTSUITE_PREFIX_READ_PATH } // =&gt; argv [1] , input_extension{input_extension_} // =&gt; argv[2] , expected_extension{ ".expected" } // =&gt; argv[3] { BOOST_TEST_INFO("dataset_loader load test files from " &lt;&lt; path); fs::path p = m_prefix_dir / fs::path(relative_path) / path; read_files(p); assert(m_input.size() == m_expected.size() &amp;&amp; "dataset_loader test vector size mismatch"); } void dataset_loader::read_files(fs::path const&amp; path) { try { if(fs::exists(path) &amp;&amp; fs::is_directory(path)) { std::vector&lt;fs::path&gt; dir_list { }; std::copy(fs::directory_iterator(path), fs::directory_iterator(), std::back_inserter(dir_list)); for(auto const&amp; file : dir_list) { if (fs::extension(file) == input_extension) { m_test_case.emplace_back( file.parent_path().filename() / file.stem() ); fs::path const input_file = file; fs::path const expect_file = fs::change_extension(file, expected_extension); m_input.emplace_back( read_file(input_file )); m_expected.emplace_back(read_file(expect_file)); } } } else { /* error message */ } } catch(std::exception const&amp; e) { /* ... */ } } std::string dataset_loader::read_file(fs::path const&amp; file_path) { fs::ifstream file{ file_path }; std::ostringstream ss{}; ss &lt;&lt; file.rdbuf(); return ss.str(); } </pre><p> where the test case directory structure is: </p> <pre class="wiki">$ROOT /test_case /concrete_test case_a_000.{input,expected} case_a_001.{input,expected} case_b_000.{input,expected} case_b_001.{input,expected} /other_concrete_test .... .. ~ 150 files each of input,expected </pre><p> and the test case self: </p> <pre class="wiki">struct foo_dataset : public testsuite::dataset_loader { foo_dataset() : dataset_loader{ "test_case/parse_identifier", "../parse/syntax", ".pls" } { } } const foo_dataset; BOOST_DATA_TEST_CASE( basic_syntax, foo_dataset.input() ^ foo_dataset.expect() ^ foo_dataset.test_case_name(), input, expected, test_case_name) { ... } // ... and &gt; 100 BOOST_DATA_TEST_CASE files with different path and relative_path // constructor args </pre><p> Imo, using the solution you provided would result into 2 dataset_loader instances, loading 'input' and 'expected'. How to get the bullet-proof features (filter missing paired test cases at load time and test_case name) using it? </p> <p> Thank you, Olaf </p> </description> <category>Ticket</category> </item> <item> <dc:creator>Raffi Enficiaud</dc:creator> <pubDate>Tue, 26 Jun 2018 18:36:41 GMT</pubDate> <title/> <link>https://svn.boost.org/trac10/ticket/12953#comment:11 </link> <guid isPermaLink="false">https://svn.boost.org/trac10/ticket/12953#comment:11</guid> <description> <p> Hi Olaf, </p> <p> Thank you for your precise reply. The keys for adapting those developments to your problem are: </p> <ul><li>you should instanciate a dataset, otherwise it gets ... instanciated at in the global scope we do not have access to the <code>master_test_suite</code> <code>argc</code> and <code>argv</code>. This is what happens with your <code>foo_dataset</code> </li><li>to ensure that the returned elements are all consistent, you can play with the arity of the dataset. So instead of creating 2/3 as you do, you just create one that returns tuples. Those tuples will get expanded automatically. </li><li>the zip size constraint is happening at the access to the dataset. If this is constructed lazylly as in the branch, this access is delayed. </li></ul><p> Here is an adaptation of your problem to the new code: </p> <pre class="wiki">class dataset_loader_arity3 { public: typedef std::vector&lt;std::string&gt; data_type; data_type m_expected; data_type m_input; typedef std::string sample; enum { arity = 3 }; public: dataset_loader_arity3(std::string some_additional) : m_some_additional(some_additional) { int argc = boost::unit_test::framework::master_test_suite().argc; char** argv = boost::unit_test::framework::master_test_suite().argv; for(unsigned i = 1; i != argc; i++) { std::string current(argv[i]); std::cout &lt;&lt; "current " &lt;&lt; current &lt;&lt; std::endl; if(current.find("--param1") != std::string::npos) { m_expected.push_back(current); } else { m_input.push_back(current); } } } struct iterator { iterator( data_type::const_iterator v_expected, data_type::const_iterator v_input, std::string additional) : m_input(v_input) , m_expected(v_expected) , m_additional(additional) {} // bug in joins, see 13380. We should return a non temporary std::tuple&lt;std::string, std::string, std::string&gt; operator*() const { return std::tuple&lt;std::string, std::string, std::string&gt;(*m_input, *m_expected, *m_input + " -" + m_additional + "- " + *m_expected); } void operator++() { ++m_input; ++m_expected; } private: data_type::const_iterator m_input, m_expected; std::string m_additional; }; boost::unit_test::data::size_t size() const { return m_input.size(); } // iterator iterator begin() const { return iterator(m_expected.begin(), m_input.begin(), m_some_additional); } private: std::string m_some_additional; }; namespace boost { namespace unit_test { namespace data { namespace monomorphic { template &lt;&gt; struct is_dataset&lt;dataset_loader_arity3&gt; : boost::mpl::true_ {}; } } } } BOOST_DATA_TEST_CASE(master_access_make_ds_with_arity, boost::unit_test::data::make_delayed&lt;dataset_loader_arity3&gt;( "something-in-the-middle"), input, expected, additional) { std::cout &lt;&lt; "input: " &lt;&lt; input &lt;&lt; " -- expected: " &lt;&lt; expected &lt;&lt; " -- additional: " &lt;&lt; additional &lt;&lt; std::endl; BOOST_TEST(true); } </pre><p> You can see in <code>master_access_make_ds_with_arity</code> that 3 variables are given to the test case. I construct 2 of them from the command line, and the third is created on the fly. This last one depends on a static/global variable in the example, that is passed to the <code>ctor</code> of the dataset when needed. </p> <p> All the magic is happening in the <code>iterator</code> of the dataset. I think this example is relatively close to yours. Let me know if you have any further question, but I think on my side, the ticket is more or less addressed :) </p> <p> Best, Raffi </p> </description> <category>Ticket</category> </item> <item> <author>ope-devel@…</author> <pubDate>Wed, 27 Jun 2018 06:42:18 GMT</pubDate> <title/> <link>https://svn.boost.org/trac10/ticket/12953#comment:12 </link> <guid isPermaLink="false">https://svn.boost.org/trac10/ticket/12953#comment:12</guid> <description> <p> Hello Raffi, </p> <p> thanks a lot; <em>master_access_make_ds_with_arity</em> solves it in the manner I considered to use before. </p> <p> Is it correct, that inside the BOOST_DATA_TEST_CASE I've access to argc/argv anywhere? </p> <p> The use case is further using an own 'reporter': </p> <pre class="wiki">BOOST_DATA_TEST_CASE( foo, foo_dataset.input() ^ foo_dataset.expect() ^ foo_dataset.test_case_name(), input, expected, test_case_name) { ... testing_parser&lt;attribute_type&gt; parse; auto [parse_ok, parse_result] = parse(input, parser, test_case_name); BOOST_TEST(parse_ok); BOOST_REQUIRE_MESSAGE(parse_ok, report_diagnostic(test_case_name, input, parse_result) ); BOOST_TEST(parse_result == expected, btt::per_element()); BOOST_REQUIRE_MESSAGE(current_test_passing(), report_diagnostic(test_case_name, input, parse_result) ); } </pre><p> where </p> <pre class="wiki">std::string report_diagnostic( fs::path const&amp; test_case_name, std::string const&amp; input, std::string const&amp; result ) { ... // only write in case of failed test if(!current_test_passing()) { test_case_result_writer result_writer(test_case_name); result_writer.write(result); } return ss.str(); } </pre><p> where the path to be written (is hardcoded at this time) shall be given as argv too. </p> <pre class="wiki">struct test_case_result_writer { test_case_result_writer(fs::path const&amp; test_case) : m_dest_dir{ CMAKE_BT_TEST_CASE_WRITE_PATH } // ARGV[4] , m_test_case{ test_case } { /* FixMe: Gather write path 'm_dest_dir' from argv */} .... void write(std::string const&amp; parse_result) { fs::path const full_pathname = m_dest_dir / "test_case" / m_test_case; fs::path const write_path = full_pathname.parent_path(); std::string const ext{ ".parsed" }; if(!create_directory(write_path)) { ... } fs::path const filename = full_pathname.filename().replace_extension(ext); write_file(write_path / filename, parse_result); } </pre><p> So the dataset_loader can be used independing of what to check/load. </p> <p> One point using <em>dataset-master-test-suite-accessible-test.cpp</em>: </p> <pre class="wiki">&gt; bin\bt_issue12953.exe -- --param1=1 --param2=2 Running 22 test cases... *** No errors detected </pre><p> but: </p> <pre class="wiki">bin\bt_issue12953.exe --list_content Test setup error: Can't zip datasets of different sizes </pre><p> How to handle this? </p> <p> Best, Olaf </p> <p> PS: As you can see <em>current_test_passing()</em> serves arround another problem not related to the feature request. </p> </description> <category>Ticket</category> </item> <item> <dc:creator>Raffi Enficiaud</dc:creator> <pubDate>Wed, 27 Jun 2018 07:17:48 GMT</pubDate> <title/> <link>https://svn.boost.org/trac10/ticket/12953#comment:13 </link> <guid isPermaLink="false">https://svn.boost.org/trac10/ticket/12953#comment:13</guid> <description> <p> Hi Olaf, </p> <p> So yes, once the test starts (in <code>BOOST_DATA_TEST_CASE</code>) you have access to the <code>argv</code> as usual. The only issue you were facing is that <code>argv</code> should be used in your setup to create the tests and it was not supported. If you need to pass an additional parameter to the test for the reporting you have, I would rather suggest to access it through the <code>master_test_suite_t</code> <code>argv</code>. The other option would be to change the iterator of the dataset to return this parameter. In the example I posted, there is a third parameter that is given to the test. </p> <p> You can get the current test case name directly from the framework. See <a class="ext-link" href="https://stackoverflow.com/a/13027251/1617295"><span class="icon">​</span>here</a> for instance. </p> <p> For the error raised by passing <code>--list_content</code>, this is just a problem in handling the datasets, not a boost.test issue (I got it running for the example I gave you by disabling all the other tests from the file <code>test/test-organization-ts/dataset-master-test-suite-accessible-test.cpp</code> in this branch). </p> <p> Best, Raffi </p> </description> <category>Ticket</category> </item> <item> <dc:creator>Raffi Enficiaud</dc:creator> <pubDate>Wed, 27 Jun 2018 08:29:51 GMT</pubDate> <title>milestone changed https://svn.boost.org/trac10/ticket/12953#comment:14 https://svn.boost.org/trac10/ticket/12953#comment:14 <ul> <li><strong>milestone</strong> <span class="trac-field-old">Boost 1.66.0</span> → <span class="trac-field-new">Boost 1.68.0</span> </li> </ul> Ticket anonymous Wed, 27 Jun 2018 08:38:30 GMT <link>https://svn.boost.org/trac10/ticket/12953#comment:15 </link> <guid isPermaLink="false">https://svn.boost.org/trac10/ticket/12953#comment:15</guid> <description> <p> Thank you Raffi, </p> <p> I think, the feature request is adressed. With respect to the error raised by passing <em>-- list_content</em>, I open another issue or write to the ML, isn't it? </p> <p> Just seen, I'm looking forward to Milestone 1.68.0 </p> <p> Thanks, Olaf </p> </description> <category>Ticket</category> </item> <item> <dc:creator>Raffi Enficiaud</dc:creator> <pubDate>Wed, 27 Jun 2018 12:17:35 GMT</pubDate> <title/> <link>https://svn.boost.org/trac10/ticket/12953#comment:16 </link> <guid isPermaLink="false">https://svn.boost.org/trac10/ticket/12953#comment:16</guid> <description> <p> Hi, </p> <p> It is in master now. Should be available for 1.68. Concerning <code>--list_content</code>, as I said I am not reproducing it on my end. In all case, better create another ticket. </p> <p> Cheers, Raffi </p> </description> <category>Ticket</category> </item> <item> <dc:creator>Raffi Enficiaud</dc:creator> <pubDate>Wed, 01 Aug 2018 17:07:51 GMT</pubDate> <title>status changed; resolution set https://svn.boost.org/trac10/ticket/12953#comment:17 https://svn.boost.org/trac10/ticket/12953#comment:17 <ul> <li><strong>status</strong> <span class="trac-field-old">assigned</span> → <span class="trac-field-new">closed</span> </li> <li><strong>resolution</strong> → <span class="trac-field-new">fixed</span> </li> </ul> Ticket