- 分支:
pr3/upload-infrastructure - 基准: 已包含 PR2-JSM-3.0-merged 的 main
- 模块名:
Aether3DCore - 目标合同版本:
PR3-API-1.0
文件: /Users/kaidongwang/Documents/progecttwo/progect2/progect2/Package.swift
精确修改位置: 第 59 行和第 73 行之间
// 当前 (第 59 行):
exclude: ["Constants", "Audit/COVERAGE_GAPS_ANALYSIS.md", "Golden"],
// 修改为:
exclude: ["Constants", "Upload", "Audit/COVERAGE_GAPS_ANALYSIS.md", "Golden"], // 在 ConstantsTests 的闭合括号 `)` 之后 (第 73 行之后), 添加:
,
.testTarget(
name: "UploadTests",
dependencies: ["Aether3DCore"],
path: "Tests/Upload"
) .testTarget(
name: "Aether3DCoreTests",
dependencies: ["Aether3DCore"],
path: "Tests",
exclude: ["Constants", "Upload", "Audit/COVERAGE_GAPS_ANALYSIS.md", "Golden"], // 添加 "Upload"
resources: [
.process("QualityPreCheck/Fixtures/CoverageDeltaEndiannessFixture.json"),
.process("QualityPreCheck/Fixtures/CoverageGridPackingFixture.json"),
.process("QualityPreCheck/Fixtures/CanonicalJSONFloatFixture.json")
]
),
.testTarget(
name: "ConstantsTests",
dependencies: [
"Aether3DCore",
.product(name: "Crypto", package: "swift-crypto")
],
path: "Tests/Constants"
),
.testTarget(
name: "UploadTests",
dependencies: ["Aether3DCore"],
path: "Tests/Upload"
)执行以下 shell 命令:
# 创建 Core/Upload 目录
mkdir -p Core/Upload
# 创建 Tests/Upload 目录
mkdir -p Tests/Upload
# 验证
ls -la Core/Upload
ls -la Tests/Upload完整代码见 PR3_UPLOAD_INFRASTRUCTURE_DETAILED_PROMPT.md 第 208-535 行
关键常量:
public enum UploadConstants {
public static let UPLOAD_CONTRACT_VERSION = "PR3-UPLOAD-1.0"
public static let CHUNK_SIZE_MIN_BYTES: Int = 2 * 1024 * 1024 // 2MB
public static let CHUNK_SIZE_DEFAULT_BYTES: Int = 5 * 1024 * 1024 // 5MB
public static let CHUNK_SIZE_MAX_BYTES: Int = 20 * 1024 * 1024 // 20MB
public static let MAX_PARALLEL_CHUNK_UPLOADS: Int = 4
public static let NETWORK_SPEED_SLOW_MBPS: Double = 5.0
public static let NETWORK_SPEED_NORMAL_MBPS: Double = 50.0
public static let NETWORK_SPEED_FAST_MBPS: Double = 100.0
// ... 其余常量
}完整代码见 PR3_UPLOAD_INFRASTRUCTURE_DETAILED_PROMPT.md 第 567-982 行
关键类型:
public enum NetworkSpeedClass: String, Codable, CaseIterable { ... }
public struct SpeedSample: Codable, Equatable { ... }
public final class NetworkSpeedMonitor { ... }// ============================================================================
// CONSTITUTIONAL CONTRACT - DO NOT EDIT WITHOUT RFC
// Contract Version: PR3-API-1.0
// Module: Upload Infrastructure - Adaptive Chunk Sizer
// Cross-Platform: macOS + Linux (pure Foundation)
// ============================================================================
import Foundation
/// Chunk sizing strategy enumeration.
public enum ChunkSizingStrategy: String, Codable {
case fixed = "fixed"
case adaptive = "adaptive"
case aggressive = "aggressive"
}
/// Adaptive chunk sizer configuration.
public struct AdaptiveChunkConfig: Codable, Equatable {
public let strategy: ChunkSizingStrategy
public let minChunkSize: Int
public let maxChunkSize: Int
public let targetUploadTime: TimeInterval
public init(
strategy: ChunkSizingStrategy = .adaptive,
minChunkSize: Int = UploadConstants.CHUNK_SIZE_MIN_BYTES,
maxChunkSize: Int = UploadConstants.CHUNK_SIZE_MAX_BYTES,
targetUploadTime: TimeInterval = 10.0
) {
self.strategy = strategy
self.minChunkSize = minChunkSize
self.maxChunkSize = maxChunkSize
self.targetUploadTime = targetUploadTime
}
}
/// Adaptive chunk sizer for network-aware chunk sizing.
public final class AdaptiveChunkSizer {
private let config: AdaptiveChunkConfig
private let speedMonitor: NetworkSpeedMonitor
public init(config: AdaptiveChunkConfig = AdaptiveChunkConfig(), speedMonitor: NetworkSpeedMonitor) {
self.config = config
self.speedMonitor = speedMonitor
}
/// Calculate optimal chunk size based on current network conditions.
public func calculateChunkSize() -> Int {
switch config.strategy {
case .fixed:
return UploadConstants.CHUNK_SIZE_DEFAULT_BYTES
case .adaptive:
return speedMonitor.getRecommendedChunkSize()
case .aggressive:
let speedClass = speedMonitor.getSpeedClass()
return speedClass.allowsAggressiveOptimization
? config.maxChunkSize
: speedMonitor.getRecommendedChunkSize()
}
}
/// Calculate optimal chunk size for a specific file size.
public func calculateChunkSize(forFileSize fileSize: Int64) -> Int {
let baseSize = calculateChunkSize()
// For small files, use smaller chunks
if fileSize < Int64(baseSize * 2) {
return max(config.minChunkSize, Int(fileSize / 2))
}
return baseSize
}
/// Get recommended parallel upload count.
public func getRecommendedParallelCount() -> Int {
return speedMonitor.getRecommendedParallelCount()
}
}// ============================================================================
// CONSTITUTIONAL CONTRACT - DO NOT EDIT WITHOUT RFC
// Contract Version: PR3-API-1.0
// Module: Upload Infrastructure - Upload Session
// Cross-Platform: macOS + Linux (pure Foundation)
// ============================================================================
import Foundation
/// Upload session state enumeration.
public enum UploadSessionState: String, Codable, CaseIterable {
case initialized = "initialized"
case uploading = "uploading"
case paused = "paused"
case stalled = "stalled"
case completing = "completing"
case completed = "completed"
case failed = "failed"
case cancelled = "cancelled"
public var isTerminal: Bool {
switch self {
case .completed, .failed, .cancelled:
return true
default:
return false
}
}
}
/// Chunk state enumeration.
public enum ChunkState: String, Codable {
case pending = "pending"
case uploading = "uploading"
case completed = "completed"
case failed = "failed"
}
/// Chunk status tracking.
public struct ChunkStatus: Codable, Equatable {
public let index: Int
public let offset: Int64
public let size: Int
public var state: ChunkState
public var retryCount: Int
public var lastError: String?
public init(index: Int, offset: Int64, size: Int) {
self.index = index
self.offset = offset
self.size = size
self.state = .pending
self.retryCount = 0
self.lastError = nil
}
}
/// Upload session for managing a single file upload.
public final class UploadSession {
public let sessionId: String
public let fileSize: Int64
public let fileName: String
public private(set) var state: UploadSessionState
public private(set) var chunks: [ChunkStatus]
public private(set) var uploadedBytes: Int64
public let createdAt: Date
public private(set) var updatedAt: Date
private let queue = DispatchQueue(label: "com.app.upload.session", qos: .userInitiated)
public init(sessionId: String = UUID().uuidString, fileName: String, fileSize: Int64, chunkSize: Int) {
self.sessionId = sessionId
self.fileName = fileName
self.fileSize = fileSize
self.state = .initialized
self.uploadedBytes = 0
self.createdAt = Date()
self.updatedAt = Date()
// Calculate chunks
var chunks: [ChunkStatus] = []
var offset: Int64 = 0
var index = 0
while offset < fileSize {
let remainingBytes = fileSize - offset
let currentChunkSize = min(Int(remainingBytes), chunkSize)
chunks.append(ChunkStatus(index: index, offset: offset, size: currentChunkSize))
offset += Int64(currentChunkSize)
index += 1
}
self.chunks = chunks
}
/// Get progress as a percentage (0.0 - 1.0).
public var progress: Double {
guard fileSize > 0 else { return 0 }
return Double(uploadedBytes) / Double(fileSize)
}
/// Get number of completed chunks.
public var completedChunkCount: Int {
return queue.sync { chunks.filter { $0.state == .completed }.count }
}
/// Get total chunk count.
public var totalChunkCount: Int {
return chunks.count
}
/// Update session state.
public func updateState(_ newState: UploadSessionState) {
queue.sync {
self.state = newState
self.updatedAt = Date()
}
}
/// Mark chunk as completed.
public func markChunkCompleted(index: Int) {
queue.sync {
guard index < chunks.count else { return }
chunks[index].state = .completed
uploadedBytes = chunks.filter { $0.state == .completed }
.reduce(0) { $0 + Int64($1.size) }
updatedAt = Date()
}
}
/// Mark chunk as failed.
public func markChunkFailed(index: Int, error: String) {
queue.sync {
guard index < chunks.count else { return }
chunks[index].state = .failed
chunks[index].retryCount += 1
chunks[index].lastError = error
updatedAt = Date()
}
}
/// Get next pending chunk.
public func getNextPendingChunk() -> ChunkStatus? {
return queue.sync {
chunks.first { $0.state == .pending }
}
}
/// Check if session has expired.
public func isExpired(maxAge: TimeInterval = UploadConstants.SESSION_MAX_AGE_SECONDS) -> Bool {
return Date().timeIntervalSince(createdAt) > maxAge
}
}// ============================================================================
// CONSTITUTIONAL CONTRACT - DO NOT EDIT WITHOUT RFC
// Contract Version: PR3-API-1.0
// Module: Upload Infrastructure - Chunk Manager
// Cross-Platform: macOS + Linux (pure Foundation)
// ============================================================================
import Foundation
/// Chunk upload error enumeration.
public enum ChunkUploadError: Error, Equatable {
case networkError(String)
case serverError(Int)
case timeout
case cancelled
case maxRetriesExceeded
}
/// Chunk manager delegate protocol.
public protocol ChunkManagerDelegate: AnyObject {
func chunkManager(_ manager: ChunkManager, didStartChunk index: Int)
func chunkManager(_ manager: ChunkManager, didCompleteChunk index: Int)
func chunkManager(_ manager: ChunkManager, didFailChunk index: Int, error: ChunkUploadError)
func chunkManager(_ manager: ChunkManager, didUpdateProgress progress: Double)
}
/// Chunk manager for coordinating parallel chunk uploads.
public final class ChunkManager {
public weak var delegate: ChunkManagerDelegate?
private let session: UploadSession
private let speedMonitor: NetworkSpeedMonitor
private let chunkSizer: AdaptiveChunkSizer
private let queue = DispatchQueue(label: "com.app.upload.chunkmanager", qos: .userInitiated)
private var activeUploads: Set<Int> = []
private var isCancelled: Bool = false
public init(session: UploadSession, speedMonitor: NetworkSpeedMonitor, chunkSizer: AdaptiveChunkSizer) {
self.session = session
self.speedMonitor = speedMonitor
self.chunkSizer = chunkSizer
}
/// Get current number of active uploads.
public var activeUploadCount: Int {
return queue.sync { activeUploads.count }
}
/// Get recommended parallel count based on network conditions.
public var recommendedParallelCount: Int {
return chunkSizer.getRecommendedParallelCount()
}
/// Check if upload should continue.
public var shouldContinue: Bool {
return queue.sync { !isCancelled && !session.state.isTerminal }
}
/// Cancel all uploads.
public func cancel() {
queue.sync {
isCancelled = true
activeUploads.removeAll()
}
session.updateState(.cancelled)
}
/// Mark chunk upload as started.
public func markChunkStarted(index: Int) {
queue.sync {
activeUploads.insert(index)
}
delegate?.chunkManager(self, didStartChunk: index)
}
/// Mark chunk upload as completed.
public func markChunkCompleted(index: Int, bytesTransferred: Int64, duration: TimeInterval) {
queue.sync {
activeUploads.remove(index)
}
session.markChunkCompleted(index: index)
speedMonitor.recordSample(bytesTransferred: bytesTransferred, durationSeconds: duration)
delegate?.chunkManager(self, didCompleteChunk: index)
delegate?.chunkManager(self, didUpdateProgress: session.progress)
}
/// Mark chunk upload as failed.
public func markChunkFailed(index: Int, error: ChunkUploadError) {
queue.sync {
activeUploads.remove(index)
}
session.markChunkFailed(index: index, error: "\(error)")
delegate?.chunkManager(self, didFailChunk: index, error: error)
}
/// Calculate retry delay with decorrelated jitter.
public func calculateRetryDelay(attempt: Int) -> TimeInterval {
let baseDelay = UploadConstants.RETRY_BASE_DELAY_SECONDS
let maxDelay = UploadConstants.RETRY_MAX_DELAY_SECONDS
let jitter = UploadConstants.RETRY_JITTER_FACTOR
let exponentialDelay = baseDelay * pow(2.0, Double(attempt))
let cappedDelay = min(exponentialDelay, maxDelay)
let jitterRange = cappedDelay * jitter
let randomJitter = Double.random(in: -jitterRange...jitterRange)
return max(baseDelay, cappedDelay + randomJitter)
}
}// ============================================================================
// CONSTITUTIONAL CONTRACT - DO NOT EDIT WITHOUT RFC
// Contract Version: PR3-API-1.0
// Module: Upload Infrastructure - Resume Manager
// Cross-Platform: macOS + Linux (pure Foundation)
// ============================================================================
import Foundation
/// Upload resume manager for persisting and recovering upload sessions.
public final class UploadResumeManager {
private let userDefaults: UserDefaults
private let keyPrefix: String
private let queue = DispatchQueue(label: "com.app.upload.resumemanager", qos: .utility)
public init(userDefaults: UserDefaults = .standard, keyPrefix: String = UploadConstants.SESSION_PERSISTENCE_KEY_PREFIX) {
self.userDefaults = userDefaults
self.keyPrefix = keyPrefix
}
/// Save session state for later resume.
public func saveSession(_ session: UploadSession) {
queue.async { [weak self] in
guard let self = self else { return }
let key = self.keyPrefix + session.sessionId
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
if let data = try? encoder.encode(SessionSnapshot(session: session)) {
self.userDefaults.set(data, forKey: key)
}
}
}
/// Load session state for resume.
public func loadSession(sessionId: String) -> SessionSnapshot? {
return queue.sync { [weak self] in
guard let self = self else { return nil }
let key = self.keyPrefix + sessionId
guard let data = self.userDefaults.data(forKey: key) else { return nil }
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return try? decoder.decode(SessionSnapshot.self, from: data)
}
}
/// Delete session state.
public func deleteSession(sessionId: String) {
queue.async { [weak self] in
guard let self = self else { return }
let key = self.keyPrefix + sessionId
self.userDefaults.removeObject(forKey: key)
}
}
/// Get all saved session IDs.
public func getAllSessionIds() -> [String] {
return queue.sync { [weak self] in
guard let self = self else { return [] }
return self.userDefaults.dictionaryRepresentation().keys
.filter { $0.hasPrefix(self.keyPrefix) }
.map { String($0.dropFirst(self.keyPrefix.count)) }
}
}
/// Clean up expired sessions.
public func cleanupExpiredSessions(maxAge: TimeInterval = UploadConstants.SESSION_MAX_AGE_SECONDS) {
queue.async { [weak self] in
guard let self = self else { return }
let sessionIds = self.getAllSessionIds()
let cutoff = Date().addingTimeInterval(-maxAge)
for sessionId in sessionIds {
if let snapshot = self.loadSession(sessionId: sessionId),
snapshot.createdAt < cutoff {
self.deleteSession(sessionId: sessionId)
}
}
}
}
}
/// Session snapshot for persistence.
public struct SessionSnapshot: Codable {
public let sessionId: String
public let fileName: String
public let fileSize: Int64
public let chunks: [ChunkStatus]
public let uploadedBytes: Int64
public let createdAt: Date
public let state: UploadSessionState
public init(session: UploadSession) {
self.sessionId = session.sessionId
self.fileName = session.fileName
self.fileSize = session.fileSize
self.chunks = session.chunks
self.uploadedBytes = session.uploadedBytes
self.createdAt = session.createdAt
self.state = session.state
}
}// ============================================================================
// CONSTITUTIONAL CONTRACT - DO NOT EDIT WITHOUT RFC
// Contract Version: PR3-API-1.0
// Module: Upload Infrastructure - Progress Tracker
// Cross-Platform: macOS + Linux (pure Foundation)
// ============================================================================
import Foundation
/// Upload progress event.
public struct UploadProgressEvent: Equatable {
public let sessionId: String
public let progress: Double
public let uploadedBytes: Int64
public let totalBytes: Int64
public let speedBps: Double
public let estimatedRemainingSeconds: TimeInterval?
public let timestamp: Date
public init(
sessionId: String,
progress: Double,
uploadedBytes: Int64,
totalBytes: Int64,
speedBps: Double,
estimatedRemainingSeconds: TimeInterval?,
timestamp: Date = Date()
) {
self.sessionId = sessionId
self.progress = progress
self.uploadedBytes = uploadedBytes
self.totalBytes = totalBytes
self.speedBps = speedBps
self.estimatedRemainingSeconds = estimatedRemainingSeconds
self.timestamp = timestamp
}
}
/// Upload progress tracker delegate.
public protocol UploadProgressTrackerDelegate: AnyObject {
func progressTracker(_ tracker: UploadProgressTracker, didUpdateProgress event: UploadProgressEvent)
}
/// Upload progress tracker for aggregating and reporting progress.
public final class UploadProgressTracker {
public weak var delegate: UploadProgressTrackerDelegate?
private let session: UploadSession
private let speedMonitor: NetworkSpeedMonitor
private let queue = DispatchQueue(label: "com.app.upload.progresstracker", qos: .userInitiated)
private var lastReportedProgress: Double = 0.0
private var lastReportTime: Date = .distantPast
public init(session: UploadSession, speedMonitor: NetworkSpeedMonitor) {
self.session = session
self.speedMonitor = speedMonitor
}
/// Update and report progress if threshold is met.
public func updateProgress() {
queue.async { [weak self] in
guard let self = self else { return }
let currentProgress = self.session.progress
let now = Date()
// Check throttle
let timeSinceLastReport = now.timeIntervalSince(self.lastReportTime)
if timeSinceLastReport < UploadConstants.PROGRESS_THROTTLE_INTERVAL_SECONDS {
return
}
// Check minimum increment
let progressDelta = abs(currentProgress - self.lastReportedProgress)
if progressDelta < UploadConstants.MIN_PROGRESS_INCREMENT_PERCENT / 100.0 {
return
}
self.lastReportedProgress = currentProgress
self.lastReportTime = now
let speedBps = self.speedMonitor.getSpeedBps()
let remainingBytes = self.session.fileSize - self.session.uploadedBytes
let estimatedRemaining: TimeInterval? = speedBps > 0
? Double(remainingBytes) / speedBps
: nil
let event = UploadProgressEvent(
sessionId: self.session.sessionId,
progress: currentProgress,
uploadedBytes: self.session.uploadedBytes,
totalBytes: self.session.fileSize,
speedBps: speedBps,
estimatedRemainingSeconds: estimatedRemaining
)
DispatchQueue.main.async {
self.delegate?.progressTracker(self, didUpdateProgress: event)
}
}
}
/// Force report current progress (ignoring throttle).
public func forceReportProgress() {
queue.async { [weak self] in
guard let self = self else { return }
self.lastReportedProgress = self.session.progress
self.lastReportTime = Date()
let speedBps = self.speedMonitor.getSpeedBps()
let remainingBytes = self.session.fileSize - self.session.uploadedBytes
let estimatedRemaining: TimeInterval? = speedBps > 0
? Double(remainingBytes) / speedBps
: nil
let event = UploadProgressEvent(
sessionId: self.session.sessionId,
progress: self.session.progress,
uploadedBytes: self.session.uploadedBytes,
totalBytes: self.session.fileSize,
speedBps: speedBps,
estimatedRemainingSeconds: estimatedRemaining
)
DispatchQueue.main.async {
self.delegate?.progressTracker(self, didUpdateProgress: event)
}
}
}
}完整代码见 PR3_UPLOAD_INFRASTRUCTURE_DETAILED_PROMPT.md 第 1030-1214 行
关键修改: 将 @testable import YourModuleName 改为:
@testable import Aether3DCore// ============================================================================
// CONSTITUTIONAL CONTRACT - DO NOT EDIT WITHOUT RFC
// Contract Version: PR3-API-1.0
// Module: Upload Infrastructure Tests - Adaptive Chunk Sizer
// Cross-Platform: macOS + Linux
// ============================================================================
import XCTest
@testable import Aether3DCore
final class AdaptiveChunkSizerTests: XCTestCase {
var speedMonitor: NetworkSpeedMonitor!
var sizer: AdaptiveChunkSizer!
override func setUp() {
super.setUp()
speedMonitor = NetworkSpeedMonitor()
sizer = AdaptiveChunkSizer(speedMonitor: speedMonitor)
}
override func tearDown() {
sizer = nil
speedMonitor = nil
super.tearDown()
}
func testFixedStrategy() {
let config = AdaptiveChunkConfig(strategy: .fixed)
let fixedSizer = AdaptiveChunkSizer(config: config, speedMonitor: speedMonitor)
XCTAssertEqual(fixedSizer.calculateChunkSize(), UploadConstants.CHUNK_SIZE_DEFAULT_BYTES)
}
func testAdaptiveStrategySlowNetwork() {
// Simulate slow network
for _ in 0..<5 {
speedMonitor.recordSample(bytesTransferred: 250_000, durationSeconds: 1.0)
}
XCTAssertEqual(sizer.calculateChunkSize(), UploadConstants.CHUNK_SIZE_MIN_BYTES)
}
func testAdaptiveStrategyFastNetwork() {
// Simulate fast network
for _ in 0..<5 {
speedMonitor.recordSample(bytesTransferred: 10_000_000, durationSeconds: 1.0)
}
XCTAssertGreaterThan(sizer.calculateChunkSize(), UploadConstants.CHUNK_SIZE_DEFAULT_BYTES)
}
func testSmallFileSizing() {
let smallFileSize: Int64 = 3 * 1024 * 1024 // 3MB
let chunkSize = sizer.calculateChunkSize(forFileSize: smallFileSize)
XCTAssertLessThanOrEqual(chunkSize, Int(smallFileSize))
}
}// ============================================================================
// CONSTITUTIONAL CONTRACT - DO NOT EDIT WITHOUT RFC
// Contract Version: PR3-API-1.0
// Module: Upload Infrastructure Tests - Upload Session
// Cross-Platform: macOS + Linux
// ============================================================================
import XCTest
@testable import Aether3DCore
final class UploadSessionTests: XCTestCase {
var session: UploadSession!
override func setUp() {
super.setUp()
session = UploadSession(
fileName: "test.mp4",
fileSize: 50 * 1024 * 1024, // 50MB
chunkSize: 5 * 1024 * 1024 // 5MB chunks
)
}
override func tearDown() {
session = nil
super.tearDown()
}
func testInitialState() {
XCTAssertEqual(session.state, .initialized)
XCTAssertEqual(session.totalChunkCount, 10) // 50MB / 5MB
XCTAssertEqual(session.completedChunkCount, 0)
XCTAssertEqual(session.progress, 0.0)
}
func testChunkCompletion() {
session.markChunkCompleted(index: 0)
XCTAssertEqual(session.completedChunkCount, 1)
XCTAssertEqual(session.progress, 0.1, accuracy: 0.01)
}
func testStateTransition() {
session.updateState(.uploading)
XCTAssertEqual(session.state, .uploading)
session.updateState(.completed)
XCTAssertEqual(session.state, .completed)
XCTAssertTrue(session.state.isTerminal)
}
func testChunkFailure() {
session.markChunkFailed(index: 0, error: "Network error")
let chunk = session.chunks[0]
XCTAssertEqual(chunk.state, .failed)
XCTAssertEqual(chunk.retryCount, 1)
XCTAssertEqual(chunk.lastError, "Network error")
}
func testGetNextPendingChunk() {
let pending = session.getNextPendingChunk()
XCTAssertNotNil(pending)
XCTAssertEqual(pending?.index, 0)
session.markChunkCompleted(index: 0)
let nextPending = session.getNextPendingChunk()
XCTAssertEqual(nextPending?.index, 1)
}
}// ============================================================================
// CONSTITUTIONAL CONTRACT - DO NOT EDIT WITHOUT RFC
// Contract Version: PR3-API-1.0
// Module: Upload Infrastructure Tests - Chunk Manager
// Cross-Platform: macOS + Linux
// ============================================================================
import XCTest
@testable import Aether3DCore
final class ChunkManagerTests: XCTestCase {
var session: UploadSession!
var speedMonitor: NetworkSpeedMonitor!
var chunkSizer: AdaptiveChunkSizer!
var manager: ChunkManager!
override func setUp() {
super.setUp()
session = UploadSession(fileName: "test.mp4", fileSize: 50 * 1024 * 1024, chunkSize: 5 * 1024 * 1024)
speedMonitor = NetworkSpeedMonitor()
chunkSizer = AdaptiveChunkSizer(speedMonitor: speedMonitor)
manager = ChunkManager(session: session, speedMonitor: speedMonitor, chunkSizer: chunkSizer)
}
override func tearDown() {
manager = nil
chunkSizer = nil
speedMonitor = nil
session = nil
super.tearDown()
}
func testInitialState() {
XCTAssertEqual(manager.activeUploadCount, 0)
XCTAssertTrue(manager.shouldContinue)
}
func testChunkLifecycle() {
manager.markChunkStarted(index: 0)
XCTAssertEqual(manager.activeUploadCount, 1)
manager.markChunkCompleted(index: 0, bytesTransferred: 5 * 1024 * 1024, duration: 1.0)
XCTAssertEqual(manager.activeUploadCount, 0)
XCTAssertEqual(session.completedChunkCount, 1)
}
func testCancel() {
manager.markChunkStarted(index: 0)
manager.cancel()
XCTAssertFalse(manager.shouldContinue)
XCTAssertEqual(session.state, .cancelled)
}
func testRetryDelay() {
let delay0 = manager.calculateRetryDelay(attempt: 0)
let delay1 = manager.calculateRetryDelay(attempt: 1)
let delay2 = manager.calculateRetryDelay(attempt: 2)
XCTAssertGreaterThanOrEqual(delay0, UploadConstants.RETRY_BASE_DELAY_SECONDS * 0.5)
XCTAssertLessThanOrEqual(delay2, UploadConstants.RETRY_MAX_DELAY_SECONDS * 1.5)
// Delays should generally increase (though jitter may cause variation)
XCTAssertLessThan(delay0, delay2 * 2)
}
}// ============================================================================
// CONSTITUTIONAL CONTRACT - DO NOT EDIT WITHOUT RFC
// Contract Version: PR3-API-1.0
// Module: Upload Infrastructure Tests - Resume Manager
// Cross-Platform: macOS + Linux
// ============================================================================
import XCTest
@testable import Aether3DCore
final class UploadResumeManagerTests: XCTestCase {
var resumeManager: UploadResumeManager!
var testDefaults: UserDefaults!
override func setUp() {
super.setUp()
testDefaults = UserDefaults(suiteName: "UploadResumeManagerTests")!
resumeManager = UploadResumeManager(userDefaults: testDefaults, keyPrefix: "test.upload.session.")
}
override func tearDown() {
// Clean up test defaults
testDefaults.removePersistentDomain(forName: "UploadResumeManagerTests")
resumeManager = nil
testDefaults = nil
super.tearDown()
}
func testSaveAndLoadSession() {
let session = UploadSession(fileName: "test.mp4", fileSize: 1024 * 1024, chunkSize: 512 * 1024)
session.markChunkCompleted(index: 0)
resumeManager.saveSession(session)
// Wait for async save
let expectation = XCTestExpectation(description: "Save complete")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
let loaded = self.resumeManager.loadSession(sessionId: session.sessionId)
XCTAssertNotNil(loaded)
XCTAssertEqual(loaded?.sessionId, session.sessionId)
XCTAssertEqual(loaded?.fileName, "test.mp4")
expectation.fulfill()
}
wait(for: [expectation], timeout: 2.0)
}
func testDeleteSession() {
let session = UploadSession(fileName: "test.mp4", fileSize: 1024, chunkSize: 512)
resumeManager.saveSession(session)
let expectation = XCTestExpectation(description: "Delete complete")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.resumeManager.deleteSession(sessionId: session.sessionId)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
let loaded = self.resumeManager.loadSession(sessionId: session.sessionId)
XCTAssertNil(loaded)
expectation.fulfill()
}
}
wait(for: [expectation], timeout: 3.0)
}
}注意: 只有在所有 Upload 功能完成并测试通过后才执行此步骤。
// 将第 3 行:
// Contract Version: PR2-JSM-3.0-merged
// 改为:
// Contract Version: PR3-API-1.0
// 将 CONTRACT_VERSION 常量值更新为:
public static let CONTRACT_VERSION = "PR3-API-1.0"# 1. 验证 Package.swift 语法
swift package describe
# 2. 验证新 target 被识别
swift package describe 2>&1 | grep -i upload
# 3. 构建
swift build
# 4. 运行所有测试
swift test
# 5. 单独运行 Upload 测试
swift test --filter UploadTests
# 6. 验证无 Apple-only imports
grep -rE "import UIKit|import AppKit|import Network" Core/Upload/ && echo "FAIL" || echo "PASS"| 操作 | 路径 | 状态 |
|---|---|---|
| MODIFY | Package.swift |
添加 UploadTests target + exclude |
| CREATE | Core/Constants/UploadConstants.swift |
新文件 |
| CREATE | Core/Upload/NetworkSpeedMonitor.swift |
新文件 |
| CREATE | Core/Upload/AdaptiveChunkSizer.swift |
新文件 |
| CREATE | Core/Upload/UploadSession.swift |
新文件 |
| CREATE | Core/Upload/ChunkManager.swift |
新文件 |
| CREATE | Core/Upload/UploadResumeManager.swift |
新文件 |
| CREATE | Core/Upload/UploadProgressTracker.swift |
新文件 |
| CREATE | Tests/Upload/NetworkSpeedMonitorTests.swift |
新文件 |
| CREATE | Tests/Upload/AdaptiveChunkSizerTests.swift |
新文件 |
| CREATE | Tests/Upload/UploadSessionTests.swift |
新文件 |
| CREATE | Tests/Upload/ChunkManagerTests.swift |
新文件 |
| CREATE | Tests/Upload/UploadResumeManagerTests.swift |
新文件 |
总计: 1 个修改 + 12 个新建 = 13 个文件操作