97 lines
5.5 KiB
Swift
97 lines
5.5 KiB
Swift
import XCTest
|
|
@testable import TVAnarchyCore
|
|
|
|
/// Locks the torrent wire formats against real `cli.ts` output.
|
|
final class TorrentDecoderTests: XCTestCase {
|
|
func testTransferRowDecodeRPC() throws {
|
|
// Rich JSON-RPC shape from `cli.ts tx-list` (transmissionListRich).
|
|
let json = #"""
|
|
[{"id":1,"name":"Done.S05","percentDone":1,"status":6,"doneDate":1749329821,"addedDate":1749300000,
|
|
"haveValid":8760000000,"sizeWhenDone":8760000000,"rateDownload":0,"rateUpload":1024,"uploadRatio":0.5,"eta":-1},
|
|
{"id":11,"name":"WIP","percentDone":0.15,"status":4,"doneDate":0,"addedDate":1749329000,
|
|
"haveValid":300000000,"sizeWhenDone":2000000000,"rateDownload":1048576,"rateUpload":0,"uploadRatio":0,"eta":3720}]
|
|
"""#
|
|
let rows = try JSONDecoder().decode([TorrentRow].self, from: Data(json.utf8))
|
|
XCTAssertEqual(rows.count, 2)
|
|
|
|
let done = rows[0]
|
|
XCTAssertTrue(done.isComplete)
|
|
XCTAssertEqual(done.statusLabel, "Seeding")
|
|
XCTAssertNotNil(done.completedAt)
|
|
XCTAssertEqual(done.completedAt, Date(timeIntervalSince1970: 1749329821))
|
|
XCTAssertFalse(done.upText.isEmpty) // 1024 B/s → formatted
|
|
|
|
let wip = rows[1]
|
|
XCTAssertFalse(wip.isComplete)
|
|
XCTAssertTrue(wip.isDownloading)
|
|
XCTAssertEqual(wip.progress, 0.15, accuracy: 0.001)
|
|
XCTAssertNil(wip.completedAt) // doneDate 0 → not completed
|
|
XCTAssertEqual(wip.etaText, "1h 2m") // 3720s
|
|
}
|
|
|
|
func testRecencySort() {
|
|
func row(_ id: Int, done: Int, added: Int) -> TorrentRow {
|
|
let j = #"{"id":\#(id),"name":"t\#(id)","percentDone":\#(done > 0 ? 1 : 0),"status":0,"doneDate":\#(done),"addedDate":\#(added),"haveValid":0,"sizeWhenDone":0,"rateDownload":0,"rateUpload":0,"uploadRatio":0,"eta":-1}"#
|
|
return try! JSONDecoder().decode(TorrentRow.self, from: Data(j.utf8))
|
|
}
|
|
let unsorted = [row(1, done: 0, added: 50), row(2, done: 100, added: 10), row(3, done: 200, added: 20)]
|
|
let sorted = unsorted.sorted(by: DownloadsController.byRecency)
|
|
XCTAssertEqual(sorted.map(\.id), [3, 2, 1]) // newest-completed first; in-progress last
|
|
}
|
|
|
|
func testTransferRowDecodesHealthFields() throws {
|
|
// The list poll now also carries error/errorString/peersConnected (added to
|
|
// RPC_FIELDS) — they're optional so a pre-Part-E cli still decodes, but when
|
|
// present they must drive the health classifier.
|
|
let json = #"""
|
|
[{"id":7,"name":"Stuck","percentDone":0.4,"status":4,"doneDate":0,"addedDate":1749300000,
|
|
"haveValid":1,"sizeWhenDone":10,"rateDownload":0,"rateUpload":0,"uploadRatio":0,"eta":-1,
|
|
"error":0,"errorString":"","peersConnected":0}]
|
|
"""#
|
|
let row = try JSONDecoder().decode([TorrentRow].self, from: Data(json.utf8))[0]
|
|
XCTAssertEqual(row.peersConnected, 0)
|
|
// 0 peers + 0 rate, stuck past the dead threshold → dead.
|
|
XCTAssertEqual(row.health(secondsStuck: 400), .dead)
|
|
// Same row, errored takes precedence regardless of peers/time.
|
|
let errJSON = #"{"id":8,"name":"E","percentDone":0.4,"status":4,"doneDate":0,"addedDate":1,"haveValid":1,"sizeWhenDone":10,"rateDownload":0,"rateUpload":0,"uploadRatio":0,"eta":-1,"error":3,"errorString":"tracker gone","peersConnected":5}"#
|
|
let errored = try JSONDecoder().decode(TorrentRow.self, from: Data(errJSON.utf8))
|
|
XCTAssertEqual(errored.health(secondsStuck: 10), .errored)
|
|
}
|
|
|
|
func testTorrentDetailDecode() throws {
|
|
// Wire shape of `cli.ts tx-detail <id>` (transmissionDetail).
|
|
let json = #"""
|
|
{"id":11,"name":"WIP.S01","percentDone":0.15,"status":4,"error":0,"errorString":"",
|
|
"eta":3720,"rateDownload":1048576,"rateUpload":0,"uploadRatio":0,
|
|
"peersConnected":3,"peersSendingToUs":2,"peersGettingFromUs":1,
|
|
"downloadDir":"/bigdisk/_/media/tv",
|
|
"trackerStats":[
|
|
{"host":"udp://tracker.one:1337","announceState":1,"lastAnnounceResult":"Success","lastAnnounceSucceeded":true,"seederCount":42,"leecherCount":7},
|
|
{"host":"udp://dead.tracker:80","announceState":0,"lastAnnounceResult":"Could not connect to tracker","lastAnnounceSucceeded":false,"seederCount":-1,"leecherCount":-1}
|
|
]}
|
|
"""#
|
|
let d = try JSONDecoder().decode(TorrentDetail.self, from: Data(json.utf8))
|
|
XCTAssertEqual(d.id, 11)
|
|
XCTAssertFalse(d.isComplete)
|
|
XCTAssertFalse(d.hasError)
|
|
XCTAssertEqual(d.peersText, "3 connected · ↓2 ↑1")
|
|
XCTAssertEqual(d.trackerStats.count, 2)
|
|
XCTAssertTrue(d.trackerStats[0].lastAnnounceSucceeded)
|
|
XCTAssertEqual(d.trackerStats[0].swarmText, "42 seed · 7 leech")
|
|
XCTAssertEqual(d.trackerStats[1].swarmText, "— seed · — leech") // -1 → unknown
|
|
XCTAssertEqual(d.health(), .downloading)
|
|
}
|
|
|
|
func testSearchResultDecodeAndAddable() throws {
|
|
let json = #"""
|
|
[{"filename":"Show.S01.1080p","source":"1337x","size":"3.2 GB","seeders":45,"leechers":3,"magnet":"magnet:?xt=urn:btih:abc"},
|
|
{"filename":"NoMagnet","source":"tpb","size":"1 GB","seeders":2,"leechers":0,"magnet":null}]
|
|
"""#
|
|
let results = try JSONDecoder().decode([TorrentResult].self, from: Data(json.utf8))
|
|
XCTAssertEqual(results.count, 2)
|
|
XCTAssertTrue(results[0].addable)
|
|
XCTAssertEqual(results[0].seeders, 45)
|
|
XCTAssertFalse(results[1].addable) // null magnet → not addable
|
|
XCTAssertNotEqual(results[0].id, results[1].id)
|
|
}
|
|
}
|