/* Copyright (c) 2015, Arvid Norberg All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "libtorrent/session.hpp" #include "libtorrent/torrent_handle.hpp" #include "libtorrent/settings_pack.hpp" #include "libtorrent/alert_types.hpp" #include "libtorrent/deadline_timer.hpp" #include "settings.hpp" #include "create_torrent.hpp" #include "simulator/simulator.hpp" #include "simulator/utils.hpp" // for timer #include using namespace sim; using namespace libtorrent; const int num_torrents = 10; namespace lt = libtorrent; using sim::asio::ip::address_v4; std::unique_ptr make_io_service(sim::simulation& sim, int i) { char ep[30]; snprintf(ep, sizeof(ep), "50.0.%d.%d", (i + 1) >> 8, (i + 1) & 0xff); return std::unique_ptr(new sim::asio::io_service( sim, asio::ip::address_v4::from_string(ep))); } // this is the general template for these tests. create the session with custom // settings (Settings), set up the test, by adding torrents with certain // arguments (Setup), run the test and verify the end state (Test) template void run_test(Settings const& sett, Setup const& setup, Test const& test) { // setup the simulation sim::default_config network_cfg; sim::simulation sim{network_cfg}; std::unique_ptr ios = make_io_service(sim, 0); lt::session_proxy zombie; // setup settings pack to use for the session (customization point) lt::settings_pack pack = settings(); sett(pack); // create session std::shared_ptr ses = std::make_shared(pack, *ios); // set up test, like adding torrents (customization point) setup(*ses); // set up a timer to fire later, to verify everything we expected to happen // happened sim::timer t(sim, lt::seconds((num_torrents + 1) * 60) , [&](boost::system::error_code const& ec) { test(*ses); // shut down zombie = ses->abort(); ses.reset(); }); sim.run(); } TORRENT_TEST(dont_count_slow_torrents) { run_test( [](lt::settings_pack& sett) { // session settings sett.set_bool(lt::settings_pack::dont_count_slow_torrents, true); sett.set_int(lt::settings_pack::active_downloads, 1); sett.set_int(lt::settings_pack::active_seeds, 1); }, [](lt::session& ses) { // add torrents for (int i = 0; i < num_torrents; ++i) { lt::add_torrent_params params = create_torrent(i, false); params.flags |= lt::add_torrent_params::flag_auto_managed; params.flags |= lt::add_torrent_params::flag_paused; ses.async_add_torrent(params); } }, [](lt::session& ses) { // verify result std::vector alerts; ses.pop_alerts(&alerts); lt::time_point last = lt::time_point::min(); lt::time_point start_time = alerts[0]->timestamp(); int num_started = 0; for (alert* a : alerts) { printf("%-3d %s\n", int(duration_cast(a->timestamp() - start_time).count()), a->message().c_str()); if (alert_cast(a) == nullptr) continue; lt::time_point t = a->timestamp(); if (last != lt::time_point::min()) { // expect starting of new torrents to be spaced by 60 seconds // the division by 2 is to allow some slack (it's integer // division) TEST_EQUAL(duration_cast(t - last).count() / 2, 60 / 2); } last = t; ++num_started; } TEST_EQUAL(num_started, num_torrents); for (torrent_handle const& h : ses.get_torrents()) { TEST_CHECK(h.status().auto_managed); TEST_EQUAL(h.status().paused, false); } }); } TORRENT_TEST(count_slow_torrents) { run_test( [](settings_pack& sett) { // session settings sett.set_bool(settings_pack::dont_count_slow_torrents, false); sett.set_int(settings_pack::active_downloads, 1); sett.set_int(settings_pack::active_seeds, 1); }, [](lt::session& ses) { // add torrents for (int i = 0; i < num_torrents; ++i) { lt::add_torrent_params params = create_torrent(i, false); params.flags |= add_torrent_params::flag_auto_managed; params.flags |= add_torrent_params::flag_paused; ses.async_add_torrent(params); } }, [](lt::session& ses) { // verify result (only one should have been started, even though // they're all idle) std::vector alerts; ses.pop_alerts(&alerts); lt::time_point start_time = alerts[0]->timestamp(); int num_started = 0; for (alert* a : alerts) { printf("%-3d %s\n", int(duration_cast(a->timestamp() - start_time).count()), a->message().c_str()); if (alert_cast(a) == nullptr) continue; ++num_started; } TEST_EQUAL(num_started, 1); num_started = 0; for (torrent_handle const& h : ses.get_torrents()) { TEST_CHECK(h.status().auto_managed); num_started += !h.status().paused; } TEST_EQUAL(num_started, 1); }); } TORRENT_TEST(force_stopped_download) { run_test( [](settings_pack& sett) { // session settings sett.set_bool(settings_pack::dont_count_slow_torrents, true); sett.set_int(settings_pack::active_downloads, 10); sett.set_int(settings_pack::active_seeds, 10); }, [](lt::session& ses) { // add torrents for (int i = 0; i < num_torrents; ++i) { lt::add_torrent_params params = create_torrent(i, false); // torrents are paused and not auto-managed params.flags &= ~add_torrent_params::flag_auto_managed; params.flags |= add_torrent_params::flag_paused; ses.async_add_torrent(params); } }, [](lt::session& ses) { // verify result (none should have been started) std::vector alerts; ses.pop_alerts(&alerts); lt::time_point start_time = alerts[0]->timestamp(); for (alert* a : alerts) { printf("%-3d %s\n", int(duration_cast(a->timestamp() - start_time).count()), a->message().c_str()); // we don't expect any torrents being started or stopped, since // they're all force stopped TEST_CHECK(alert_cast(a) == nullptr); TEST_CHECK(alert_cast(a) == nullptr); } for (torrent_handle const& h : ses.get_torrents()) { TEST_CHECK(!h.status().auto_managed); TEST_CHECK(h.status().paused); } }); } TORRENT_TEST(force_started) { run_test( [](settings_pack& sett) { // session settings sett.set_bool(settings_pack::dont_count_slow_torrents, false); sett.set_int(settings_pack::active_downloads, 1); sett.set_int(settings_pack::active_seeds, 1); }, [](lt::session& ses) { // add torrents for (int i = 0; i < num_torrents; ++i) { lt::add_torrent_params params = create_torrent(i, false); // torrents are started and not auto-managed params.flags &= ~add_torrent_params::flag_auto_managed; params.flags &= ~add_torrent_params::flag_paused; ses.async_add_torrent(params); } }, [](lt::session& ses) { // verify result (none should have been started) std::vector alerts; ses.pop_alerts(&alerts); lt::time_point start_time = alerts[0]->timestamp(); for (alert* a : alerts) { printf("%-3d %s\n", int(duration_cast(a->timestamp() - start_time).count()), a->message().c_str()); // we don't expect any torrents being started or stopped, since // they're all force started TEST_CHECK(alert_cast(a) == nullptr); TEST_CHECK(alert_cast(a) == nullptr); } for (torrent_handle const& h : ses.get_torrents()) { TEST_CHECK(!h.status().auto_managed); TEST_CHECK(!h.status().paused); } }); } TORRENT_TEST(seed_limit) { run_test( [](settings_pack& sett) { // session settings // set the seed limit to 3 sett.set_bool(settings_pack::dont_count_slow_torrents, false); sett.set_int(settings_pack::active_checking, 1); sett.set_int(settings_pack::active_seeds, 3); }, [](lt::session& ses) { // add torrents // add 5 seeds for (int i = 0; i < num_torrents; ++i) { lt::add_torrent_params params = create_torrent(i, true); // torrents are paused and auto-managed params.flags |= add_torrent_params::flag_auto_managed; params.flags |= add_torrent_params::flag_paused; ses.async_add_torrent(params); } }, [](lt::session& ses) { // make sure only 3 got started std::vector alerts; ses.pop_alerts(&alerts); lt::time_point start_time = alerts[0]->timestamp(); int num_started = 0; int num_checking = 0; int num_seeding = 0; for (alert* a : alerts) { fprintf(stderr, "%-3d %s\n", int(duration_cast(a->timestamp() - start_time).count()), a->message().c_str()); if (alert_cast(a)) { ++num_started; fprintf(stderr, "started: %d checking: %d seeding: %d\n" , num_started, num_checking, num_seeding); } else if (alert_cast(a)) { TEST_CHECK(num_started > 0); --num_started; fprintf(stderr, "started: %d checking: %d seeding: %d\n" , num_started, num_checking, num_seeding); } else if (state_changed_alert* sc = alert_cast(a)) { if (sc->prev_state == torrent_status::checking_files) --num_checking; else if (sc->prev_state == torrent_status::seeding) --num_seeding; if (sc->state == torrent_status::checking_files) ++num_checking; else if (sc->state == torrent_status::seeding) ++num_seeding; fprintf(stderr, "started: %d checking: %d seeding: %d\n" , num_started, num_checking, num_seeding); // while at least one torrent is checking, there may be another // started torrent (the checking one), other than that, only 3 // torrents are allowed to be started and seeding TEST_CHECK(num_started <= 3 + 1); TEST_CHECK(num_started <= 1 || num_seeding > 0); } } TEST_EQUAL(num_started, 3); num_started = 0; for (torrent_handle const& h : ses.get_torrents()) { TEST_CHECK(h.status().auto_managed); TEST_CHECK(h.status().is_seeding); num_started += !h.status().paused; } TEST_EQUAL(num_started, 3); }); } TORRENT_TEST(download_limit) { run_test( [](settings_pack& sett) { // session settings // set the seed limit to 3 sett.set_bool(settings_pack::dont_count_slow_torrents, false); sett.set_int(settings_pack::active_checking, 1); sett.set_int(settings_pack::active_downloads, 3); }, [](lt::session& ses) { // add torrents // add 5 seeds for (int i = 0; i < num_torrents; ++i) { lt::add_torrent_params params = create_torrent(i, false); // torrents are paused and auto-managed params.flags |= add_torrent_params::flag_auto_managed; params.flags |= add_torrent_params::flag_paused; ses.async_add_torrent(params); } }, [](lt::session& ses) { // make sure only 3 got started std::vector alerts; ses.pop_alerts(&alerts); lt::time_point start_time = alerts[0]->timestamp(); int num_started = 0; int num_checking = 0; int num_downloading = 0; for (alert* a : alerts) { fprintf(stderr, "%-3d %s\n", int(duration_cast(a->timestamp() - start_time).count()), a->message().c_str()); if (alert_cast(a)) { ++num_started; fprintf(stderr, "started: %d checking: %d downloading: %d\n" , num_started, num_checking, num_downloading); } else if (alert_cast(a)) { TEST_CHECK(num_started > 0); --num_started; fprintf(stderr, "started: %d checking: %d downloading: %d\n" , num_started, num_checking, num_downloading); } else if (state_changed_alert* sc = alert_cast(a)) { if (sc->prev_state == torrent_status::checking_files) --num_checking; else if (sc->prev_state == torrent_status::downloading) --num_downloading; if (sc->state == torrent_status::checking_files) ++num_checking; else if (sc->state == torrent_status::downloading) ++num_downloading; fprintf(stderr, "started: %d checking: %d downloading: %d\n" , num_started, num_checking, num_downloading); // while at least one torrent is checking, there may be another // started torrent (the checking one), other than that, only 3 // torrents are allowed to be started and seeding TEST_CHECK(num_started <= 3 + 1); TEST_CHECK(num_started <= 1 || num_downloading > 0); } } TEST_EQUAL(num_started, 3); num_started = 0; for (torrent_handle const& h : ses.get_torrents()) { TEST_CHECK(h.status().auto_managed); TEST_CHECK(!h.status().is_finished); num_started += !h.status().paused; } TEST_EQUAL(num_started, 3); }); } // make sure torrents don't announce to the tracker when transitioning from // checking to paused downloading TORRENT_TEST(checking_announce) { run_test( [](settings_pack& sett) { // session settings // set the seed limit to 3 sett.set_bool(settings_pack::dont_count_slow_torrents, false); sett.set_int(settings_pack::active_checking, 1); // just set the tracker retry intervals really long, to make sure we // don't keep retrying the tracker (since there's nothing running // there, it will fail) sett.set_int(settings_pack::tracker_backoff, 100000); // only the first torrent added should ever announce sett.set_int(settings_pack::active_seeds, 1); }, [](lt::session& ses) { // add torrents // add 5 seeds for (int i = 0; i < num_torrents; ++i) { lt::add_torrent_params params = create_torrent(i, true); // torrents are paused and auto-managed params.flags |= add_torrent_params::flag_auto_managed; params.flags |= add_torrent_params::flag_paused; // we need this to get the tracker_announce_alert params.trackers.push_back("http://10.10.0.2/announce"); ses.async_add_torrent(params); } }, [](lt::session& ses) { // make sure only 3 got started std::vector alerts; ses.pop_alerts(&alerts); lt::time_point start_time = alerts[0]->timestamp(); int num_announce = 0; for (alert* a : alerts) { fprintf(stderr, "%-3d %s\n", int(duration_cast(a->timestamp() - start_time).count()), a->message().c_str()); if (alert_cast(a)) ++num_announce; } TEST_EQUAL(num_announce, 1); int num_started = 0; for (torrent_handle const& h : ses.get_torrents()) { TEST_CHECK(h.status().auto_managed); num_started += !h.status().paused; } TEST_EQUAL(num_started, 1); }); } TORRENT_TEST(paused_checking) { run_test( [](settings_pack& sett) { // session settings // set the seed limit to 3 sett.set_bool(settings_pack::dont_count_slow_torrents, true); sett.set_int(settings_pack::active_checking, 1); }, [](lt::session& ses) { // add torrents // add 5 seeds for (int i = 0; i < num_torrents; ++i) { lt::add_torrent_params params = create_torrent(i, true); // torrents are paused and auto-managed params.flags &= ~add_torrent_params::flag_auto_managed; params.flags |= add_torrent_params::flag_paused; ses.async_add_torrent(params); } }, [](lt::session& ses) { // make sure only 3 got started std::vector alerts; ses.pop_alerts(&alerts); lt::time_point start_time = alerts[0]->timestamp(); for (alert* a : alerts) { fprintf(stderr, "%-3d %s\n", int(duration_cast(a->timestamp() - start_time).count()), a->message().c_str()); if (state_changed_alert* sc = alert_cast(a)) { TEST_CHECK(sc->state == torrent_status::checking_files || sc->state == torrent_status::checking_resume_data); } } for (torrent_handle const& h : ses.get_torrents()) { // even though all torrents are seeding, libtorrent shouldn't know // that, because they should never have been checked (because they // were force stopped) TEST_CHECK(!h.status().is_seeding); TEST_CHECK(!h.status().auto_managed); TEST_CHECK(h.status().paused); } }); } // set the stop_when_ready flag and make sure we receive a paused alert *before* // a state_changed_alert TORRENT_TEST(stop_when_ready) { run_test( [](settings_pack& sett) {}, [](lt::session& ses) { // add torrents lt::add_torrent_params params = create_torrent(0, true); // torrents are started and auto-managed params.flags |= add_torrent_params::flag_auto_managed; params.flags |= add_torrent_params::flag_stop_when_ready; // we need this to get the tracker_announce_alert params.trackers.push_back("http://10.10.0.2/announce"); ses.async_add_torrent(params); }, [](lt::session& ses) { // verify result (none should have been started) std::vector alerts; ses.pop_alerts(&alerts); lt::time_point start_time = alerts[0]->timestamp(); int num_paused = 0; for (alert* a : alerts) { fprintf(stderr, "%-3d %s\n", int(duration_cast(a->timestamp() - start_time).count()), a->message().c_str()); if (alert_cast(a)) { ++num_paused; } if (state_changed_alert* sc = alert_cast(a)) { if (sc->state == torrent_status::seeding) { // once we turn into beeing a seed. we should have been paused // already. TEST_EQUAL(num_paused, 1); } } // there should not have been any announces. the torrent should have // been stopped *before* announcing. TEST_CHECK(alert_cast(a) == NULL); } for (torrent_handle const& h : ses.get_torrents()) { // the torrent should have been force-stopped (after checking was // donw, because we set the stop_when_ready flag). Force stopped // means not auto-managed and paused. torrent_status st = h.status(); TEST_CHECK(!st.auto_managed); TEST_EQUAL(st.paused, true); // it should be seeding. If it's not seeding it may not have had its // files checked. TEST_EQUAL(st.state, torrent_status::seeding); } }); } // This test makes sure that the fastresume check will still run, and // potentially fail, for stopped torrents. The actual checking of files won't // start until the torrent is un-paused/resumed though TORRENT_TEST(resume_reject_when_paused) { run_test( [](settings_pack& sett) { sett.set_int(settings_pack::alert_mask, alert::all_categories); }, [](lt::session& ses) { // add torrents lt::add_torrent_params params = create_torrent(0, true); // the torrent is not auto managed and paused. Once the resume data // check completes, it will stay paused but the resume_rejected_alert // will be posted params.flags &= ~add_torrent_params::flag_auto_managed; params.flags |= add_torrent_params::flag_paused; // an empty dictionary will be rejected params.resume_data = {'d', 'e'}; ses.async_add_torrent(params); }, [](lt::session& ses) { std::vector alerts; ses.pop_alerts(&alerts); lt::time_point start_time = alerts[0]->timestamp(); int num_piece_finished = 0; int resume_rejected = 0; int state_changed = 0; for (alert* a : alerts) { fprintf(stderr, "%-3d %-25s %s\n", int(duration_cast(a->timestamp() - start_time).count()) , a->what() , a->message().c_str()); if (alert_cast(a)) ++num_piece_finished; if (alert_cast(a)) ++resume_rejected; if (auto sc = alert_cast(a)) { if (sc->state == torrent_status::checking_files) ++state_changed; } } for (torrent_handle const& h : ses.get_torrents()) { // the torrent should have been force-stopped. Force stopped means // not auto-managed and paused. torrent_status st = h.status(); TEST_CHECK(!st.auto_managed); TEST_EQUAL(st.paused, true); // it should be checking files, because the resume data should have // failed validation. TEST_EQUAL(st.state, torrent_status::checking_files); } TEST_EQUAL(num_piece_finished, 0); TEST_EQUAL(resume_rejected, 1); TEST_EQUAL(state_changed, 1); }); } // this test adds the torrent in paused state and no resume data. Expecting the // resume check to complete and just transition into checking state, but without // actually checking anything TORRENT_TEST(no_resume_when_paused) { run_test( [](settings_pack& sett) { sett.set_int(settings_pack::alert_mask, alert::all_categories); }, [](lt::session& ses) { // add torrents lt::add_torrent_params params = create_torrent(0, true); // the torrent is not auto managed and paused. params.flags &= ~add_torrent_params::flag_auto_managed; params.flags |= add_torrent_params::flag_paused; ses.async_add_torrent(params); }, [](lt::session& ses) { std::vector alerts; ses.pop_alerts(&alerts); lt::time_point start_time = alerts[0]->timestamp(); int num_piece_finished = 0; int resume_rejected = 0; int state_changed = 0; for (alert* a : alerts) { fprintf(stderr, "%-3d %-25s %s\n", int(duration_cast(a->timestamp() - start_time).count()) , a->what() , a->message().c_str()); if (alert_cast(a)) ++num_piece_finished; if (alert_cast(a)) ++resume_rejected; if (auto sc = alert_cast(a)) { if (sc->state == torrent_status::checking_files) ++state_changed; } } for (torrent_handle const& h : ses.get_torrents()) { // the torrent should have been force-stopped. Force stopped means // not auto-managed and paused. torrent_status st = h.status(); TEST_CHECK(!st.auto_managed); TEST_EQUAL(st.paused, true); // it should be checking files, because the resume data should have // failed validation. TEST_EQUAL(st.state, torrent_status::checking_files); } TEST_EQUAL(num_piece_finished, 0); TEST_EQUAL(resume_rejected, 0); TEST_EQUAL(state_changed, 1); }); } // this is just asserting that when the files are checked we do in fact get // piece_finished_alerts. The other tests rely on this assumption TORRENT_TEST(no_resume_when_started) { run_test( [](settings_pack& sett) { sett.set_int(settings_pack::alert_mask, alert::all_categories); }, [](lt::session& ses) { // add torrents lt::add_torrent_params params = create_torrent(0, true); ses.async_add_torrent(params); }, [](lt::session& ses) { std::vector alerts; ses.pop_alerts(&alerts); lt::time_point start_time = alerts[0]->timestamp(); int num_piece_finished = 0; int state_changed = 0; for (alert* a : alerts) { fprintf(stderr, "%-3d %-25s %s\n", int(duration_cast(a->timestamp() - start_time).count()) , a->what() , a->message().c_str()); if (alert_cast(a)) ++num_piece_finished; if (auto sc = alert_cast(a)) { if (sc->state == torrent_status::checking_files) ++state_changed; } } TEST_EQUAL(num_piece_finished, 9); TEST_EQUAL(state_changed, 1); }); } // when setting active_seeds to 0, any completed torrent should be paused TORRENT_TEST(pause_completed_torrents) { run_test( [](settings_pack& sett) { // session settings sett.set_bool(settings_pack::dont_count_slow_torrents, true); sett.set_int(settings_pack::active_downloads, 1); sett.set_int(settings_pack::active_seeds, 0); }, [](lt::session& ses) { // add torrent lt::add_torrent_params params = create_torrent(0, true); params.flags |= add_torrent_params::flag_auto_managed; params.flags |= add_torrent_params::flag_paused; ses.async_add_torrent(params); }, [](lt::session& ses) { // verify result // the torrent should have been paused immediately as it completed, // since we don't allow any seeding torrents std::vector alerts; ses.pop_alerts(&alerts); lt::time_point start_time = alerts[0]->timestamp(); int num_started = 0; int num_finished = 0; int num_paused = 0; lt::time_point finished; lt::time_point paused; for (alert* a : alerts) { printf("%-3d %s\n", int(duration_cast(a->timestamp() - start_time).count()), a->message().c_str()); if (alert_cast(a)) ++num_started; if (alert_cast(a)) { ++num_finished; finished = a->timestamp(); } if (alert_cast(a)) { ++num_paused; paused = a->timestamp(); } } TEST_EQUAL(num_started, 1); TEST_EQUAL(num_finished, 1); TEST_EQUAL(num_paused, 1); if (num_finished > 0 && num_paused > 0) { TEST_CHECK(paused >= finished); TEST_CHECK(paused - finished < chrono::milliseconds(1)); } num_paused = 0; for (torrent_handle const& h : ses.get_torrents()) { TEST_CHECK(h.status().auto_managed); num_paused += h.status().paused; } TEST_EQUAL(num_paused, 1); }); } // TODO: assert that the torrent_paused_alert is posted when pausing // downloading, seeding, checking torrents as well as the graceful pause // TODO: test limits of tracker, DHT and LSD announces