diff --git a/docs/rpc-spec.md b/docs/rpc-spec.md index e93c64b58..0f6829be4 100644 --- a/docs/rpc-spec.md +++ b/docs/rpc-spec.md @@ -141,32 +141,33 @@ Request arguments: | Key | Value Type | Value Description |:--|:--|:-- -| `bandwidthPriority` | number | this torrent's bandwidth tr_priority_t -| `downloadLimit` | number | maximum download speed (kB/s) -| `downloadLimited` | boolean | true if `downloadLimit` is honored -| `files-unwanted` | array | indices of file(s) to not download -| `files-wanted` | array | indices of file(s) to download -| `group` | string | The name of this torrent's bandwidth group -| `honorsSessionLimits` | boolean | true if session upload limits are honored -| `ids` | array | torrent list, as described in 3.1 -| `labels` | array | array of string labels -| `location` | string | new location of the torrent's content -| `peer-limit` | number | maximum number of peers -| `priority-high` | array | indices of high-priority file(s) -| `priority-low` | array | indices of low-priority file(s) -| `priority-normal` | array | indices of normal-priority file(s) -| `queuePosition` | number | position of this torrent in its queue [0...n) -| `seedIdleLimit` | number | torrent-level number of minutes of seeding inactivity -| `seedIdleMode` | number | which seeding inactivity to use. See tr_idlelimit -| `seedRatioLimit` | double | torrent-level seeding ratio -| `seedRatioMode` | number | which ratio to use. See tr_ratiolimit -| `sequential_download` | boolean | download torrent pieces sequentially -| `trackerAdd` | array | **DEPRECATED** use trackerList instead -| `trackerList` | string | string of announce URLs, one per line, and a blank line between [tiers](https://www.bittorrent.org/beps/bep_0012.html). -| `trackerRemove` | array | **DEPRECATED** use trackerList instead -| `trackerReplace` | array | **DEPRECATED** use trackerList instead -| `uploadLimit` | number | maximum upload speed (kB/s) -| `uploadLimited` | boolean | true if `uploadLimit` is honored +| `bandwidthPriority` | number | this torrent's bandwidth tr_priority_t +| `downloadLimit` | number | maximum download speed (kB/s) +| `downloadLimited` | boolean | true if `downloadLimit` is honored +| `files-unwanted` | array | indices of file(s) to not download +| `files-wanted` | array | indices of file(s) to download +| `group` | string | The name of this torrent's bandwidth group +| `honorsSessionLimits` | boolean | true if session upload limits are honored +| `ids` | array | torrent list, as described in 3.1 +| `labels` | array | array of string labels +| `location` | string | new location of the torrent's content +| `peer-limit` | number | maximum number of peers +| `priority-high` | array | indices of high-priority file(s) +| `priority-low` | array | indices of low-priority file(s) +| `priority-normal` | array | indices of normal-priority file(s) +| `queuePosition` | number | position of this torrent in its queue [0...n) +| `seedIdleLimit` | number | torrent-level number of minutes of seeding inactivity +| `seedIdleMode` | number | which seeding inactivity to use. See tr_idlelimit +| `seedRatioLimit` | double | torrent-level seeding ratio +| `seedRatioMode` | number | which ratio to use. See tr_ratiolimit +| `sequential_download` | boolean | download torrent pieces sequentially +| `sequential_download_from_piece` | number | download from a specific piece when sequential download is enabled +| `trackerAdd` | array | **DEPRECATED** use trackerList instead +| `trackerList` | string | string of announce URLs, one per line, and a blank line between [tiers](https://www.bittorrent.org/beps/bep_0012.html). +| `trackerRemove` | array | **DEPRECATED** use trackerList instead +| `trackerReplace` | array | **DEPRECATED** use trackerList instead +| `uploadLimit` | number | maximum upload speed (kB/s) +| `uploadLimited` | boolean | true if `uploadLimit` is honored Just as an empty `ids` value is shorthand for "all ids", using an empty array for `files-wanted`, `files-unwanted`, `priority-high`, `priority-low`, or @@ -273,6 +274,7 @@ The 'source' column here corresponds to the data structure there. | `seedRatioLimit`| double| tr_torrent | `seedRatioMode`| number| tr_ratiolimit | `sequential_download`| boolean| tr_torrent +| `sequential_download_from_piece`| number| tr_torrent | `sizeWhenDone`| number| tr_stat | `startDate`| number| tr_stat | `status`| number (see below)| tr_stat @@ -462,20 +464,21 @@ Request arguments: | Key | Value Type | Description |:--|:--|:-- -| `cookies` | string | pointer to a string of one or more cookies. -| `download-dir` | string | path to download the torrent to -| `filename` | string | filename or URL of the .torrent file -| `labels` | array | array of string labels -| `metainfo` | string | base64-encoded .torrent content -| `paused` | boolean | if true, don't start the torrent -| `peer-limit` | number | maximum number of peers -| `bandwidthPriority` | number | torrent's bandwidth tr_priority_t -| `files-wanted` | array | indices of file(s) to download -| `files-unwanted` | array | indices of file(s) to not download -| `priority-high` | array | indices of high-priority file(s) -| `priority-low` | array | indices of low-priority file(s) -| `priority-normal` | array | indices of normal-priority file(s) -| `sequential_download` | boolean | download torrent pieces sequentially +| `cookies` | string | pointer to a string of one or more cookies. +| `download-dir` | string | path to download the torrent to +| `filename` | string | filename or URL of the .torrent file +| `labels` | array | array of string labels +| `metainfo` | string | base64-encoded .torrent content +| `paused` | boolean | if true, don't start the torrent +| `peer-limit` | number | maximum number of peers +| `bandwidthPriority` | number | torrent's bandwidth tr_priority_t +| `files-wanted` | array | indices of file(s) to download +| `files-unwanted` | array | indices of file(s) to not download +| `priority-high` | array | indices of high-priority file(s) +| `priority-low` | array | indices of low-priority file(s) +| `priority-normal` | array | indices of normal-priority file(s) +| `sequential_download` | boolean | download torrent pieces sequentially +| `sequential_download_from_piece` | number | download from a specific piece when sequential download is enabled Either `filename` **or** `metainfo` **must** be included. All other arguments are optional. @@ -1040,6 +1043,9 @@ Transmission 4.1.0 (`rpc-version-semver` 5.4.0, `rpc-version`: 18) | `torrent-add` | new arg `sequential_download` | `torrent-get` | new arg `sequential_download` | `torrent-set` | new arg `sequential_download` +| `torrent-add` | new arg `sequential_download_from_piece` +| `torrent-get` | new arg `sequential_download_from_piece` +| `torrent-set` | new arg `sequential_download_from_piece` | `torrent-get` | new arg `files.begin_piece` | `torrent-get` | new arg `files.end_piece` | `port-test` | new arg `ip_protocol` diff --git a/libtransmission/peer-mgr-wishlist.cc b/libtransmission/peer-mgr-wishlist.cc index a6cf3f454..8232b6aef 100644 --- a/libtransmission/peer-mgr-wishlist.cc +++ b/libtransmission/peer-mgr-wishlist.cc @@ -276,7 +276,8 @@ private: tr_piece_index_t const piece, tr_piece_index_t const n_pieces, tr_piece_index_t const random_salt, - bool const is_sequential) + bool const is_sequential, + tr_piece_index_t const sequential_download_from_piece) { if (!is_sequential) { @@ -294,7 +295,19 @@ private: return 1U; } - return piece + 1U; + if (sequential_download_from_piece <= 1) + { + return piece + 1U; + } + + // Rotate remaining pieces + // 1 2 3 4 5 -> 3 4 5 1 2 if sequential_download_from_piece is 3 + if (piece < sequential_download_from_piece) + { + return n_pieces - (sequential_download_from_piece - piece); + } + + return piece - sequential_download_from_piece + 2U; } // --- @@ -304,6 +317,7 @@ private: auto n_old_c = std::size(candidates_); auto salter = tr_salt_shaker{}; auto const is_sequential = mediator_.is_sequential_download(); + auto const sequential_download_from_piece = mediator_.sequential_download_from_piece(); auto const n_pieces = mediator_.piece_count(); std::sort( @@ -322,7 +336,7 @@ private: } else { - auto const salt = get_salt(piece, n_pieces, salter(), is_sequential); + auto const salt = get_salt(piece, n_pieces, salter(), is_sequential, sequential_download_from_piece); candidates_.emplace_back(piece, salt, &mediator_); } } @@ -352,10 +366,11 @@ private: { auto salter = tr_salt_shaker{}; auto const is_sequential = mediator_.is_sequential_download(); + auto const sequential_download_from_piece = mediator_.sequential_download_from_piece(); auto const n_pieces = mediator_.piece_count(); for (auto& candidate : candidates_) { - candidate.salt = get_salt(candidate.piece, n_pieces, salter(), is_sequential); + candidate.salt = get_salt(candidate.piece, n_pieces, salter(), is_sequential, sequential_download_from_piece); } std::sort(std::begin(candidates_), std::end(candidates_)); @@ -431,11 +446,15 @@ Wishlist::Impl::Impl(Mediator& mediator_in) mediator_in.observe_sent_request([this](tr_torrent*, tr_peer*, tr_block_span_t bs) { requested_block_span(bs); }), // salt mediator_in.observe_sequential_download_changed([this](tr_torrent*, bool) { recalculate_salt(); }), + // salt + mediator_in.observe_sequential_download_from_piece_changed([this](tr_torrent*, tr_piece_index_t) + { recalculate_salt(); }), } } , mediator_{ mediator_in } { auto salter = tr_salt_shaker{}; auto const is_sequential = mediator_.is_sequential_download(); + auto const sequential_download_from_piece = mediator_.sequential_download_from_piece(); auto const n_pieces = mediator_.piece_count(); candidates_.reserve(n_pieces); for (tr_piece_index_t piece = 0U; piece < n_pieces; ++piece) @@ -445,7 +464,7 @@ Wishlist::Impl::Impl(Mediator& mediator_in) continue; } - auto const salt = get_salt(piece, n_pieces, salter(), is_sequential); + auto const salt = get_salt(piece, n_pieces, salter(), is_sequential, sequential_download_from_piece); candidates_.emplace_back(piece, salt, &mediator_); } std::sort(std::begin(candidates_), std::end(candidates_)); diff --git a/libtransmission/peer-mgr-wishlist.h b/libtransmission/peer-mgr-wishlist.h index 1a3c5b821..f21f07720 100644 --- a/libtransmission/peer-mgr-wishlist.h +++ b/libtransmission/peer-mgr-wishlist.h @@ -34,6 +34,7 @@ public: [[nodiscard]] virtual bool client_has_piece(tr_piece_index_t piece) const = 0; [[nodiscard]] virtual bool client_wants_piece(tr_piece_index_t piece) const = 0; [[nodiscard]] virtual bool is_sequential_download() const = 0; + [[nodiscard]] virtual tr_piece_index_t sequential_download_from_piece() const = 0; [[nodiscard]] virtual size_t count_piece_replication(tr_piece_index_t piece) const = 0; [[nodiscard]] virtual tr_block_span_t block_span(tr_piece_index_t piece) const = 0; [[nodiscard]] virtual tr_piece_index_t piece_count() const = 0; @@ -66,6 +67,8 @@ public: libtransmission::SimpleObservable::Observer observer) = 0; [[nodiscard]] virtual libtransmission::ObserverTag observe_sequential_download_changed( libtransmission::SimpleObservable::Observer observer) = 0; + [[nodiscard]] virtual libtransmission::ObserverTag observe_sequential_download_from_piece_changed( + libtransmission::SimpleObservable::Observer observer) = 0; virtual ~Mediator() = default; }; diff --git a/libtransmission/peer-mgr.cc b/libtransmission/peer-mgr.cc index 3e62ac783..ef246537e 100644 --- a/libtransmission/peer-mgr.cc +++ b/libtransmission/peer-mgr.cc @@ -384,6 +384,7 @@ public: [[nodiscard]] bool client_has_piece(tr_piece_index_t piece) const override; [[nodiscard]] bool client_wants_piece(tr_piece_index_t piece) const override; [[nodiscard]] bool is_sequential_download() const override; + [[nodiscard]] tr_piece_index_t sequential_download_from_piece() const override; [[nodiscard]] size_t count_piece_replication(tr_piece_index_t piece) const override; [[nodiscard]] tr_block_span_t block_span(tr_piece_index_t piece) const override; [[nodiscard]] tr_piece_index_t piece_count() const override; @@ -417,6 +418,8 @@ public: libtransmission::SimpleObservable::Observer observer) override; [[nodiscard]] libtransmission::ObserverTag observe_sequential_download_changed( libtransmission::SimpleObservable::Observer observer) override; + [[nodiscard]] libtransmission::ObserverTag observe_sequential_download_from_piece_changed( + libtransmission::SimpleObservable::Observer observer) override; private: tr_torrent& tor_; @@ -1020,6 +1023,11 @@ bool tr_swarm::WishlistMediator::is_sequential_download() const return tor_.is_sequential_download(); } +tr_piece_index_t tr_swarm::WishlistMediator::sequential_download_from_piece() const +{ + return tor_.sequential_download_from_piece(); +} + size_t tr_swarm::WishlistMediator::count_piece_replication(tr_piece_index_t piece) const { auto const op = [piece](size_t acc, auto const& peer) @@ -1133,6 +1141,12 @@ libtransmission::ObserverTag tr_swarm::WishlistMediator::observe_sequential_down return tor_.sequential_download_changed_.observe(std::move(observer)); } +libtransmission::ObserverTag tr_swarm::WishlistMediator::observe_sequential_download_from_piece_changed( + libtransmission::SimpleObservable::Observer observer) +{ + return tor_.sequential_download_from_piece_changed_.observe(std::move(observer)); +} + // --- struct tr_peerMgr diff --git a/libtransmission/quark.cc b/libtransmission/quark.cc index d1dc2977c..35d8d0fc6 100644 --- a/libtransmission/quark.cc +++ b/libtransmission/quark.cc @@ -321,6 +321,7 @@ auto constexpr MyStatic = std::array{ "seederCount"sv, "seeding-time-seconds"sv, "sequential_download"sv, + "sequential_download_from_piece"sv, "session-count"sv, "session-id"sv, "sessionCount"sv, diff --git a/libtransmission/quark.h b/libtransmission/quark.h index 6c1ece7f3..def959bf9 100644 --- a/libtransmission/quark.h +++ b/libtransmission/quark.h @@ -323,6 +323,7 @@ enum // NOLINT(performance-enum-size) TR_KEY_seederCount, TR_KEY_seeding_time_seconds, TR_KEY_sequential_download, + TR_KEY_sequential_download_from_piece, TR_KEY_session_count, TR_KEY_session_id, TR_KEY_sessionCount, diff --git a/libtransmission/resume.cc b/libtransmission/resume.cc index beb61ebdb..c1b53fc6d 100644 --- a/libtransmission/resume.cc +++ b/libtransmission/resume.cc @@ -771,6 +771,15 @@ tr_resume::fields_t load_from_file(tr_torrent* tor, tr_torrent::ResumeHelper& he } } + if ((fields_to_load & tr_resume::SequentialDownloadFromPiece) != 0) + { + if (auto i = map.value_if(TR_KEY_sequential_download_from_piece); i) + { + tor->set_sequential_download_from_piece(*i); + fields_loaded |= tr_resume::SequentialDownloadFromPiece; + } + } + if ((fields_to_load & tr_resume::Peers) != 0) { fields_loaded |= load_peers(map, tor); @@ -880,6 +889,15 @@ auto set_from_ctor( } } + if ((fields & tr_resume::SequentialDownloadFromPiece) != 0) + { + if (auto const& val = ctor.sequential_download_from_piece(mode); val) + { + tor->set_sequential_download_from_piece(*val); + ret |= tr_resume::SequentialDownloadFromPiece; + } + } + return ret; } @@ -945,6 +963,7 @@ void save(tr_torrent* const tor, tr_torrent::ResumeHelper const& helper) map.try_emplace(TR_KEY_bandwidth_priority, tor->get_priority()); map.try_emplace(TR_KEY_paused, !helper.start_when_stable()); map.try_emplace(TR_KEY_sequential_download, tor->is_sequential_download()); + map.try_emplace(TR_KEY_sequential_download_from_piece, tor->sequential_download_from_piece()); save_peers(map, tor); if (tor->has_metainfo()) diff --git a/libtransmission/resume.h b/libtransmission/resume.h index c9449bdb6..8bb393443 100644 --- a/libtransmission/resume.h +++ b/libtransmission/resume.h @@ -45,6 +45,7 @@ auto inline constexpr Name = fields_t{ 1 << 21 }; auto inline constexpr Labels = fields_t{ 1 << 22 }; auto inline constexpr Group = fields_t{ 1 << 23 }; auto inline constexpr SequentialDownload = fields_t{ 1 << 24 }; +auto inline constexpr SequentialDownloadFromPiece = fields_t{ 1 << 25 }; auto inline constexpr All = ~fields_t{ 0 }; diff --git a/libtransmission/rpcimpl.cc b/libtransmission/rpcimpl.cc index 3114315c6..d957f61e7 100644 --- a/libtransmission/rpcimpl.cc +++ b/libtransmission/rpcimpl.cc @@ -581,6 +581,7 @@ namespace make_torrent_field_helpers case TR_KEY_seedRatioLimit: case TR_KEY_seedRatioMode: case TR_KEY_sequential_download: + case TR_KEY_sequential_download_from_piece: case TR_KEY_sizeWhenDone: case TR_KEY_source: case TR_KEY_startDate: @@ -676,6 +677,7 @@ namespace make_torrent_field_helpers case TR_KEY_seedRatioLimit: return tor.seed_ratio(); case TR_KEY_seedRatioMode: return tor.seed_ratio_mode(); case TR_KEY_sequential_download: return tor.is_sequential_download(); + case TR_KEY_sequential_download_from_piece: return tor.sequential_download_from_piece(); case TR_KEY_sizeWhenDone: return st.sizeWhenDone; case TR_KEY_source: return tor.source(); case TR_KEY_startDate: return st.startDate; @@ -887,6 +889,17 @@ char const* set_file_priorities(tr_torrent* tor, tr_priority_t priority, tr_vari return nullptr; // no error } +char const* set_sequential_download_from_piece(tr_torrent& tor, tr_piece_index_t piece) +{ + if (piece >= tor.piece_count()) + { + return "piece to sequentially download from is outside pieces range"; + } + + tor.set_sequential_download_from_piece(piece); + return nullptr; // no error +} + [[nodiscard]] char const* set_file_dls(tr_torrent* tor, bool wanted, tr_variant::Vector const& files_vec) { auto const [indices, errmsg] = get_file_indices(tor, files_vec); @@ -1031,6 +1044,11 @@ char const* torrentSet(tr_session* session, tr_variant::Map const& args_in, tr_v tor->set_sequential_download(*val); } + if (auto const val = args_in.value_if(TR_KEY_sequential_download_from_piece); val && errmsg == nullptr) + { + errmsg = set_sequential_download_from_piece(*tor, *val); + } + if (auto const val = args_in.value_if(TR_KEY_downloadLimited)) { tor->use_speed_limit(TR_DOWN, *val); @@ -1475,6 +1493,11 @@ char const* torrentAdd(tr_session* session, tr_variant::Map const& args_in, tr_r ctor.set_sequential_download(TR_FORCE, *val); } + if (auto const val = args_in.value_if(TR_KEY_sequential_download_from_piece); val) + { + ctor.set_sequential_download_from_piece(TR_FORCE, *val); + } + tr_logAddTrace(fmt::format("torrentAdd: filename is '{}'", filename)); if (isCurlURL(filename)) diff --git a/libtransmission/torrent-ctor.h b/libtransmission/torrent-ctor.h index bd84c3c99..26d6d1b68 100644 --- a/libtransmission/torrent-ctor.h +++ b/libtransmission/torrent-ctor.h @@ -218,11 +218,22 @@ public: optional_args_[mode].sequential_download_ = seq; } + [[nodiscard]] constexpr auto const& sequential_download_from_piece(tr_ctorMode const mode) const noexcept + { + return optional_args_[mode].sequential_download_from_piece_; + } + + constexpr void set_sequential_download_from_piece(tr_ctorMode const mode, tr_piece_index_t const piece) noexcept + { + optional_args_[mode].sequential_download_from_piece_ = piece; + } + private: struct OptionalArgs { std::optional paused_; std::optional sequential_download_; + std::optional sequential_download_from_piece_; std::optional peer_limit_; std::string download_dir_; }; diff --git a/libtransmission/torrent.h b/libtransmission/torrent.h index 0a2ab5896..a6ad3cff2 100644 --- a/libtransmission/torrent.h +++ b/libtransmission/torrent.h @@ -773,6 +773,23 @@ struct tr_torrent return sequential_download_; } + bool set_sequential_download_from_piece(tr_piece_index_t piece) noexcept + { + auto const is_valid = piece < piece_count(); + if (is_valid && piece != sequential_download_from_piece_) + { + sequential_download_from_piece_ = piece; + sequential_download_from_piece_changed_.emit(this, piece); + return true; + } + return false; + } + + [[nodiscard]] constexpr auto sequential_download_from_piece() const noexcept + { + return sequential_download_from_piece_; + } + [[nodiscard]] constexpr bool is_running() const noexcept { return is_running_; @@ -995,6 +1012,7 @@ struct tr_torrent libtransmission::SimpleObservable files_wanted_changed_; libtransmission::SimpleObservable priority_changed_; libtransmission::SimpleObservable sequential_download_changed_; + libtransmission::SimpleObservable sequential_download_from_piece_changed_; CumulativeCount bytes_corrupt_; CumulativeCount bytes_downloaded_; @@ -1413,6 +1431,8 @@ private: bool sequential_download_ = false; + tr_piece_index_t sequential_download_from_piece_ = 0; + // start the torrent after all the startup scaffolding is done, // e.g. fetching metadata from peers and/or verifying the torrent bool start_when_stable_ = false; diff --git a/tests/libtransmission/peer-mgr-wishlist-test.cc b/tests/libtransmission/peer-mgr-wishlist-test.cc index 395c6fb75..e15a56f0e 100644 --- a/tests/libtransmission/peer-mgr-wishlist-test.cc +++ b/tests/libtransmission/peer-mgr-wishlist-test.cc @@ -32,6 +32,7 @@ protected: mutable std::set client_wants_piece_; tr_piece_index_t piece_count_ = 0; bool is_sequential_download_ = false; + tr_piece_index_t sequential_download_from_piece_ = 0; PeerMgrWishlistTest& parent_; @@ -60,6 +61,11 @@ protected: return is_sequential_download_; } + [[nodiscard]] tr_piece_index_t sequential_download_from_piece() const override + { + return sequential_download_from_piece_; + } + [[nodiscard]] size_t count_piece_replication(tr_piece_index_t piece) const override { return piece_replication_[piece]; @@ -159,6 +165,12 @@ protected: { return parent_.sequential_download_changed_.observe(std::move(observer)); } + + [[nodiscard]] libtransmission::ObserverTag observe_sequential_download_from_piece_changed( + libtransmission::SimpleObservable::Observer observer) override + { + return parent_.sequential_download_from_piece_changed_.observe(std::move(observer)); + } }; libtransmission::SimpleObservable files_wanted_changed_; @@ -174,6 +186,7 @@ protected: libtransmission::SimpleObservable piece_completed_; libtransmission::SimpleObservable priority_changed_; libtransmission::SimpleObservable sequential_download_changed_; + libtransmission::SimpleObservable sequential_download_from_piece_changed_; static auto constexpr PeerHasAllPieces = [](tr_piece_index_t) { @@ -352,6 +365,62 @@ TEST_F(PeerMgrWishlistTest, sequentialDownload) } } +TEST_F(PeerMgrWishlistTest, sequentialDownloadFromPiece) +{ + auto const get_spans = [this](size_t n_wanted) + { + auto mediator = MockMediator{ *this }; + + // setup: four pieces, all missing + mediator.piece_count_ = 4; + mediator.block_span_[0] = { 0, 100 }; + mediator.block_span_[1] = { 100, 200 }; + mediator.block_span_[2] = { 200, 300 }; + mediator.block_span_[3] = { 300, 400 }; + + // peer has all pieces + mediator.piece_replication_[0] = 1; + mediator.piece_replication_[1] = 1; + mediator.piece_replication_[2] = 1; + mediator.piece_replication_[3] = 1; + + // and we want all pieces + for (tr_piece_index_t i = 0; i < 4; ++i) + { + mediator.client_wants_piece_.insert(i); + } + + // we enabled sequential download, from piece 2 + mediator.is_sequential_download_ = true; + mediator.sequential_download_from_piece_ = 2; + + return Wishlist{ mediator }.next(n_wanted, PeerHasAllPieces); + }; + + // First and last piece come first in sequential download mode regardless + // of "sequential download from piece", piece 2 comes next. + // NB: when all other things are equal in the wishlist, pieces are + // picked at random so this test -could- pass even if there's a bug. + // So test several times to shake out any randomness + static auto constexpr NumRuns = 1000; + + for (int run = 0; run < NumRuns; ++run) + { + auto requested = tr_bitfield{ 400 }; + auto const spans = get_spans(300); + for (auto const& [begin, end] : spans) + { + requested.set_span(begin, end); + } + EXPECT_EQ(300U, requested.count()); + EXPECT_EQ(100U, requested.count(0, 100)); + EXPECT_EQ(0U, requested.count(100, 200)); + // piece 2 should be downloaded before piece 1 + EXPECT_EQ(100U, requested.count(200, 300)); + EXPECT_EQ(100U, requested.count(300, 400)); + } +} + TEST_F(PeerMgrWishlistTest, doesNotRequestTooManyBlocks) { auto mediator = MockMediator{ *this }; @@ -1453,6 +1522,69 @@ TEST_F(PeerMgrWishlistTest, settingSequentialDownloadResortsCandidates) } } +TEST_F(PeerMgrWishlistTest, sequentialDownloadFromPieceResortsCandidates) +{ + auto const get_spans = [this](size_t n_wanted) + { + auto mediator = MockMediator{ *this }; + + // setup: four pieces, all missing + mediator.piece_count_ = 4; + mediator.block_span_[0] = { 0, 100 }; + mediator.block_span_[1] = { 100, 200 }; + mediator.block_span_[2] = { 200, 300 }; + mediator.block_span_[3] = { 300, 400 }; + + // peer has all pieces + mediator.piece_replication_[0] = 1; + mediator.piece_replication_[1] = 1; + mediator.piece_replication_[2] = 1; + mediator.piece_replication_[3] = 1; + + // and we want all pieces + for (tr_piece_index_t i = 0; i < 4; ++i) + { + mediator.client_wants_piece_.insert(i); + } + + // allow the wishlist to build its cache + auto wishlist = Wishlist{ mediator }; + + // we enabled sequential download, from piece 2 + mediator.is_sequential_download_ = true; + sequential_download_changed_.emit(nullptr, true); + mediator.sequential_download_from_piece_ = 2; + sequential_download_from_piece_changed_.emit(nullptr, 2); + + // the sequential download setting was changed, + // the candidate list should be resorted + return wishlist.next(n_wanted, PeerHasAllPieces); + }; + + // First and last piece come first in sequential download mode regardless + // of "sequential download from piece", piece 2 comes next. + // NB: when all other things are equal in the wishlist, pieces are + // picked at random so this test -could- pass even if there's a bug. + // So test several times to shake out any randomness + static auto constexpr NumRuns = 1000; + + for (int run = 0; run < NumRuns; ++run) + { + auto requested = tr_bitfield{ 400 }; + auto const spans = get_spans(300); + for (auto const& [begin, end] : spans) + { + requested.set_span(begin, end); + } + EXPECT_EQ(300U, requested.count()); + EXPECT_EQ(100U, requested.count(0, 100)); + EXPECT_EQ(0U, requested.count(100, 200)); + // piece 2 should be downloaded before piece 1 + EXPECT_EQ(100U, requested.count(200, 300)); + EXPECT_EQ(100U, requested.count(300, 400)); + } +} + TEST_F(PeerMgrWishlistTest, setFileWantedUpdatesCandidateListAdd) { auto const get_spans = [this](size_t n_wanted)