mirror of
https://github.com/yattee/yattee.git
synced 2026-08-26 08:52:32 +00:00
Chunk stateless feed requests to support more than 500 subscriptions (#967)
This commit is contained in:
committed by
GitHub
parent
c41016185c
commit
d46002e99a
153
YatteeTests/YatteeServerFeedChunkingTests.swift
Normal file
153
YatteeTests/YatteeServerFeedChunkingTests.swift
Normal file
@@ -0,0 +1,153 @@
|
||||
//
|
||||
// YatteeServerFeedChunkingTests.swift
|
||||
// YatteeTests
|
||||
//
|
||||
// Tests for chunking and merging of stateless feed requests against Yattee Server,
|
||||
// which rejects more than 500 channels per request.
|
||||
//
|
||||
|
||||
import Testing
|
||||
import Foundation
|
||||
@testable import Yattee
|
||||
|
||||
@Suite("Yattee Server Feed Chunking Tests")
|
||||
struct YatteeServerFeedChunkingTests {
|
||||
|
||||
private func video(id: String, published: Int64?) -> ServerFeedVideo {
|
||||
ServerFeedVideo(
|
||||
type: "video",
|
||||
videoId: id,
|
||||
title: "Video \(id)",
|
||||
author: "Author",
|
||||
authorId: "UC\(id)",
|
||||
lengthSeconds: 60,
|
||||
published: published,
|
||||
publishedText: nil,
|
||||
viewCount: nil,
|
||||
videoThumbnails: nil,
|
||||
extractor: "youtube",
|
||||
videoUrl: nil,
|
||||
isUpcoming: nil,
|
||||
premiereTimestamp: nil
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Chunking
|
||||
|
||||
@Test("Chunked splits arrays at the requested size")
|
||||
func chunkedSplitsAtSize() {
|
||||
let channels = Array(0..<1018)
|
||||
let chunks = channels.chunked(into: 500)
|
||||
#expect(chunks.map(\.count) == [500, 500, 18])
|
||||
#expect(chunks.flatMap { $0 } == channels)
|
||||
}
|
||||
|
||||
@Test("Chunked keeps lists within the size as a single chunk")
|
||||
func chunkedSingleChunk() {
|
||||
#expect(Array(0..<500).chunked(into: 500).count == 1)
|
||||
#expect(Array(0..<3).chunked(into: 500).map(\.count) == [3])
|
||||
#expect([Int]().chunked(into: 500).isEmpty)
|
||||
}
|
||||
|
||||
// MARK: - Feed Response Merging
|
||||
|
||||
@Test("Merging a single feed response returns it unchanged")
|
||||
func mergedSingleFeedResponsePassthrough() {
|
||||
// Deliberately unsorted: a single response must not be re-sorted or truncated.
|
||||
let response = StatelessFeedResponse(
|
||||
status: "pending",
|
||||
videos: [video(id: "a", published: 100), video(id: "b", published: 300)],
|
||||
total: 2,
|
||||
hasMore: true,
|
||||
readyCount: 1,
|
||||
pendingCount: 2,
|
||||
errorCount: nil,
|
||||
etaSeconds: 30
|
||||
)
|
||||
let merged = StatelessFeedResponse.merged([response], limit: 1)
|
||||
#expect(merged.videos.map(\.videoId) == ["a", "b"])
|
||||
#expect(merged.status == "pending")
|
||||
#expect(merged.total == 2)
|
||||
}
|
||||
|
||||
@Test("Merging feed responses sorts videos by published date and applies the limit")
|
||||
func mergedFeedResponsesSortsAndLimits() {
|
||||
let first = StatelessFeedResponse(
|
||||
status: "ready",
|
||||
videos: [video(id: "old", published: 100), video(id: "newest", published: 900)],
|
||||
total: 2,
|
||||
hasMore: false,
|
||||
readyCount: 500,
|
||||
pendingCount: 0,
|
||||
errorCount: 0,
|
||||
etaSeconds: nil
|
||||
)
|
||||
let second = StatelessFeedResponse(
|
||||
status: "ready",
|
||||
videos: [video(id: "middle", published: 500), video(id: "undated", published: nil)],
|
||||
total: 2,
|
||||
hasMore: false,
|
||||
readyCount: 18,
|
||||
pendingCount: 0,
|
||||
errorCount: 0,
|
||||
etaSeconds: nil
|
||||
)
|
||||
|
||||
let merged = StatelessFeedResponse.merged([first, second], limit: 3)
|
||||
#expect(merged.videos.map(\.videoId) == ["newest", "middle", "old"])
|
||||
#expect(merged.total == 4)
|
||||
#expect(merged.hasMore) // truncated from 4 to 3
|
||||
#expect(merged.readyCount == 518)
|
||||
#expect(merged.status == "ready")
|
||||
#expect(merged.isReady)
|
||||
}
|
||||
|
||||
@Test("Merged feed response is pending while any chunk is pending")
|
||||
func mergedFeedResponsePendingWhileAnyChunkPending() {
|
||||
let ready = StatelessFeedResponse(
|
||||
status: "ready", videos: [], total: 0, hasMore: false,
|
||||
readyCount: 500, pendingCount: 0, errorCount: 0, etaSeconds: nil
|
||||
)
|
||||
let pending = StatelessFeedResponse(
|
||||
status: "pending", videos: [], total: 0, hasMore: false,
|
||||
readyCount: 10, pendingCount: 8, errorCount: 0, etaSeconds: 45
|
||||
)
|
||||
|
||||
let merged = StatelessFeedResponse.merged([ready, pending], limit: 100)
|
||||
#expect(merged.status == "pending")
|
||||
#expect(!merged.isReady)
|
||||
#expect(merged.pendingCount == 8)
|
||||
#expect(merged.etaSeconds == 45)
|
||||
}
|
||||
|
||||
@Test("Merged feed response keeps counts nil when no chunk reports them")
|
||||
func mergedFeedResponseKeepsNilCounts() {
|
||||
let first = StatelessFeedResponse(
|
||||
status: "ready", videos: [], total: 0, hasMore: false,
|
||||
readyCount: nil, pendingCount: nil, errorCount: nil, etaSeconds: nil
|
||||
)
|
||||
let merged = StatelessFeedResponse.merged([first, first], limit: 100)
|
||||
#expect(merged.readyCount == nil)
|
||||
#expect(merged.pendingCount == nil)
|
||||
#expect(merged.errorCount == nil)
|
||||
#expect(merged.etaSeconds == nil)
|
||||
}
|
||||
|
||||
// MARK: - Feed Status Merging
|
||||
|
||||
@Test("Merging status responses sums counts and stays pending until all chunks are ready")
|
||||
func mergedStatusResponses() {
|
||||
let ready = StatelessFeedStatusResponse(status: "ready", readyCount: 500, pendingCount: 0, errorCount: 1)
|
||||
let pending = StatelessFeedStatusResponse(status: "pending", readyCount: 10, pendingCount: 8, errorCount: 0)
|
||||
|
||||
let merged = StatelessFeedStatusResponse.merged([ready, pending])
|
||||
#expect(merged.status == "pending")
|
||||
#expect(merged.readyCount == 510)
|
||||
#expect(merged.pendingCount == 8)
|
||||
#expect(merged.errorCount == 1)
|
||||
|
||||
let allReady = StatelessFeedStatusResponse.merged([ready, ready])
|
||||
#expect(allReady.isReady)
|
||||
#expect(allReady.readyCount == 1000)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user