1
1
mirror of https://github.com/kean/Nuke.git synced 2024-11-28 12:04:01 +03:00

Compare commits

...

16 Commits

Author SHA1 Message Date
Alex Grebenyuk
ae2c57a7df
Merge b62cc4346a into 1d748d4eac 2024-11-17 19:05:18 +00:00
kean
b62cc4346a Update ImageDecoderTests 2024-11-17 14:05:11 -05:00
kean
4e0bca9161 Update ImageCacheTests 2024-11-17 13:39:19 -05:00
kean
52771a052b Update ImageRequestTests 2024-11-17 13:32:07 -05:00
kean
13d363e609 Update TaskTests 2024-11-17 12:52:21 -05:00
kean
9f7eb9cafb Enable complete concurrency checking in unit tests 2024-11-17 12:41:19 -05:00
kean
26487ea9e6 Use expect instead of require 2024-11-17 09:33:53 -05:00
kean
bec2101b77 Update RateLimiterTests to swift-testing 2024-11-17 09:20:44 -05:00
kean
ef82a7d183 Enable Swift 6 2024-11-17 08:19:59 -05:00
kean
7e684d13e3 Add retroactive conformances in test targets 2024-11-17 08:16:30 -05:00
kean
28967695f4 Fix SwiftPM build on macOS 2024-11-17 08:10:07 -05:00
Alex Grebenyuk
1d748d4eac
Update README.md 2024-10-07 12:33:59 -04:00
Alex Grebenyuk
840b743058
Update README.md 2024-10-07 12:22:22 -04:00
Alex Grebenyuk
ef56fdd004
Update README.md 2024-10-07 07:23:39 -04:00
Alex Grebenyuk
639832849c
Update README.md 2024-09-17 06:37:33 -04:00
Alex Grebenyuk
0e953da391
Update README.md 2024-08-21 17:25:06 -04:00
10 changed files with 723 additions and 760 deletions

View File

@ -1837,7 +1837,6 @@
OTHER_SWIFT_FLAGS = "-D DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = com.github.kean.nukeui;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_STRICT_CONCURRENCY = complete;
};
name = Debug;
};
@ -1857,7 +1856,6 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.github.kean.nukeui;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_STRICT_CONCURRENCY = complete;
};
name = Release;
};
@ -1875,6 +1873,7 @@
OTHER_SWIFT_FLAGS = "-D DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = "com.github.kean.nukeui-unit-tests";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
@ -1891,6 +1890,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = "com.github.kean.nukeui-unit-tests";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
};
name = Release;
};
@ -1907,6 +1907,7 @@
PRODUCT_BUNDLE_IDENTIFIER = "com.github.kean.Nuke-Thread-Safety-Tests";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_VERSION = 5.0;
};
name = Debug;
};
@ -1922,6 +1923,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = "com.github.kean.Nuke-Thread-Safety-Tests";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
};
name = Release;
};
@ -1942,7 +1944,6 @@
PRODUCT_BUNDLE_IDENTIFIER = "com.github.kean.nuke-extensions";
PRODUCT_NAME = NukeExtensions;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_STRICT_CONCURRENCY = complete;
};
name = Debug;
};
@ -1962,7 +1963,6 @@
);
PRODUCT_BUNDLE_IDENTIFIER = "com.github.kean.nuke-extensions";
PRODUCT_NAME = NukeExtensions;
SWIFT_STRICT_CONCURRENCY = complete;
};
name = Release;
};
@ -1984,6 +1984,7 @@
PRODUCT_BUNDLE_IDENTIFIER = com.github.kean.NukeExtensionsTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_VERSION = 5.0;
};
name = Debug;
};
@ -2004,6 +2005,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.github.kean.NukeExtensionsTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
};
name = Release;
};
@ -2024,7 +2026,6 @@
OTHER_SWIFT_FLAGS = "-D DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = com.github.kean.nukevideo;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_STRICT_CONCURRENCY = complete;
};
name = Debug;
};
@ -2044,7 +2045,6 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.github.kean.nukevideo;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_STRICT_CONCURRENCY = complete;
};
name = Release;
};
@ -2061,6 +2061,8 @@
OTHER_SWIFT_FLAGS = "-D DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = "com.github.kean.Nuke-Tests";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 5.0;
};
name = Debug;
};
@ -2076,6 +2078,8 @@
);
PRODUCT_BUNDLE_IDENTIFIER = "com.github.kean.Nuke-Tests";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 5.0;
};
name = Release;
};
@ -2140,6 +2144,7 @@
PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Nuke Tests Host.app/Nuke Tests Host";
};
name = Debug;
@ -2161,6 +2166,7 @@
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Nuke Tests Host.app/Nuke Tests Host";
};
name = Release;
@ -2222,7 +2228,7 @@
SUPPORTED_PLATFORMS = "watchsimulator watchos macosx iphonesimulator iphoneos driverkit appletvsimulator appletvos";
SUPPORTS_MACCATALYST = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
SWIFT_VERSION = 6.0;
TVOS_DEPLOYMENT_TARGET = 13.0;
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
@ -2279,7 +2285,7 @@
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "watchsimulator watchos macosx iphonesimulator iphoneos driverkit appletvsimulator appletvos";
SUPPORTS_MACCATALYST = YES;
SWIFT_VERSION = 5.0;
SWIFT_VERSION = 6.0;
TVOS_DEPLOYMENT_TARGET = 13.0;
VALIDATE_PRODUCT = YES;
VERSIONING_SYSTEM = "apple-generic";
@ -2305,7 +2311,6 @@
OTHER_SWIFT_FLAGS = "-D DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = com.github.kean.nuke;
PRODUCT_NAME = Nuke;
SWIFT_STRICT_CONCURRENCY = complete;
};
name = Debug;
};
@ -2325,7 +2330,6 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.github.kean.nuke;
PRODUCT_NAME = Nuke;
SWIFT_STRICT_CONCURRENCY = complete;
};
name = Release;
};

View File

@ -18,19 +18,29 @@ The framework is lean and compiles in under 2 seconds[¹](#footnote-1). It has a
## Sponsors
[**Lapse**](https://www.lapse.com): friends *not followers*.
<a href="https://www.lapse.com"><img src="https://github.com/user-attachments/assets/28bb4f4b-affc-4580-879d-5aeef633b80c" height="140px" alt="Lapse Logo"></a>
Nuke is also supported by:
<table>
<tr>
<td valign="center">
<a href="https://proxyman.io"><img src="https://kean.blog/images/logos/proxyman.png" height="40px" alt="Proxyman Logo"></a>
<td valign="center" align="center">
<a href="https://proxyman.io">
<img src="https://kean.blog/images/logos/proxyman.png" height="50px" alt="Proxyman Logo">
<p>Proxyman</p>
</a>
</td>
<td valign="center">
<a href="https://www.namiml.com#gh-light-mode-only"><img src="https://kean.blog/images/logos/nami-light.png#gh-light-mode-only" height="40px" alt="Nami Logo"></a><a href="https://www.namiml.com#gh-dark-mode-only"><img src="https://kean.blog/images/logos/nami-dark.png#gh-dark-mode-only" height="40px" alt="Nami Logo"></a>
<td valign="center" align="center">
<a href="https://backspace.travel">
<img src="https://avatars.githubusercontent.com/u/175125884?s=200&v=4" height="50px", alt="Backspace Travel"/>
<p>Backspace Travel</p>
</a>
</td>
</tr>
</table>
> [Support](https://github.com/sponsors/kean) Nuke on GitHub Sponsors.
## Installation
Nuke supports [Swift Package Manager](https://www.swift.org/package-manager/), which is the recommended option. If that doesn't work for you, you can use binary frameworks attached to the [releases](https://github.com/kean/Nuke/releases).

View File

@ -56,8 +56,8 @@ final class Cache<Key: Hashable, Value>: @unchecked Sendable {
self.memoryPressure.resume()
#if os(iOS) || os(tvOS) || os(visionOS)
Task {
await registerForEnterBackground()
Task { @MainActor in
registerForEnterBackground()
}
#endif
}
@ -70,7 +70,7 @@ final class Cache<Key: Hashable, Value>: @unchecked Sendable {
}
#if os(iOS) || os(tvOS) || os(visionOS)
@MainActor private func registerForEnterBackground() {
private func registerForEnterBackground() {
notificationObserver = NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil) { [weak self] _ in
self?.clearCacheOnEnterBackground()
}

View File

@ -32,8 +32,10 @@ extension ImageEncoding {
}
}
// note: @unchecked was added to surpress build errors with NSImage on macOS
/// Image encoding context used when selecting which encoder to use.
public struct ImageEncodingContext: Sendable {
public struct ImageEncodingContext: @unchecked Sendable {
public let request: ImageRequest
public let image: PlatformImage
public let urlResponse: URLResponse?

View File

@ -5,7 +5,7 @@
import Foundation
import Nuke
extension ImagePipeline.Error: Equatable {
extension ImagePipeline.Error: @retroactive Equatable {
public static func == (lhs: ImagePipeline.Error, rhs: ImagePipeline.Error) -> Bool {
switch (lhs, rhs) {
case (.dataMissingInCache, .dataMissingInCache): return true
@ -22,7 +22,7 @@ extension ImagePipeline.Error: Equatable {
}
}
extension ImageResponse: Equatable {
extension ImageResponse: @retroactive Equatable {
public static func == (lhs: ImageResponse, rhs: ImageResponse) -> Bool {
return lhs.image === rhs.image
}

View File

@ -2,422 +2,397 @@
//
// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean).
import XCTest
import Foundation
import Testing
@testable import Nuke
#if canImport(UIKit)
import UIKit
#endif
@Suite struct ImageCacheTests {
let cache = ImageCache()
init() {
cache.entryCostLimit = 2
}
// MARK: - Basics
@Test func cacheCreation() {
#expect(cache.totalCount == 0)
#expect(cache[Test.request] == nil)
}
@Test func imageIsStored() {
// When
cache[Test.request] = Test.container
// Then
#expect(cache.totalCount == 1)
#expect(cache[Test.request] != nil)
}
// MARK: - Subscript
@Test func imageIsStoredUsingSubscript() {
// When
cache[Test.request] = Test.container
// Then
#expect(cache[Test.request] != nil)
}
// MARK: - Count
@Test func totalCountChanges() {
#expect(cache.totalCount == 0)
cache[request1] = Test.container
#expect(cache.totalCount == 1)
cache[request2] = Test.container
#expect(cache.totalCount == 2)
cache[request2] = nil
#expect(cache.totalCount == 1)
cache[request1] = nil
#expect(cache.totalCount == 0)
}
@Test func countLimitChanges() {
// When
cache.countLimit = 1
// Then
#expect(cache.countLimit == 1)
}
@Test func ttlChanges() {
// when
cache.ttl = 1
// Then
#expect(cache.ttl == 1)
}
@Test func itemsAreRemoveImmediatelyWhenCountLimitIsReached() {
// Given
cache.countLimit = 1
// When
cache[request1] = Test.container
cache[request2] = Test.container
// Then
#expect(cache[request1] == nil)
#expect(cache[request2] != nil)
}
@Test func trimToCount() {
// Given
cache[request1] = Test.container
cache[request2] = Test.container
// When
cache.trim(toCount: 1)
// Then
#expect(cache[request1] == nil)
#expect(cache[request2] != nil)
}
@Test func imagesAreRemovedOnCountLimitChange() {
// Given
cache.countLimit = 2
cache[request1] = Test.container
cache[request2] = Test.container
// When
cache.countLimit = 1
// Then
#expect(cache[request1] == nil)
#expect(cache[request2] != nil)
}
// MARK: Cost
#if canImport(UIKit)
@Test func defaultImageCost() {
#expect(cache.cost(for: ImageContainer(image: Test.image)) == 1228800)
}
@Test func totalCostChanges() {
let imageCost = cache.cost(for: ImageContainer(image: Test.image))
#expect(cache.totalCost == 0)
cache[request1] = Test.container
#expect(cache.totalCost == imageCost)
cache[request2] = Test.container
#expect(cache.totalCost == 2 * imageCost)
cache[request2] = nil
#expect(cache.totalCost == imageCost)
cache[request1] = nil
#expect(cache.totalCost == 0)
}
@Test func costLimitChanged() {
// Given
let cost = cache.cost(for: ImageContainer(image: Test.image))
// When
cache.costLimit = Int(Double(cost) * 1.5)
// Then
#expect(cache.costLimit == Int(Double(cost) * 1.5))
}
@Test func itemsAreRemoveImmediatelyWhenCostLimitIsReached() {
// Given
let cost = cache.cost(for: ImageContainer(image: Test.image))
cache.costLimit = Int(Double(cost) * 1.5)
// When/Then
cache[request1] = Test.container
// LRU item is released
cache[request2] = Test.container
#expect(cache[request1] == nil)
#expect(cache[request2] != nil)
}
@Test func entryCostLimitEntryStored() {
// Given
let container = ImageContainer(image: Test.image)
let cost = cache.cost(for: container)
cache.costLimit = Int(Double(cost) * 15)
cache.entryCostLimit = 0.1
// When
cache[Test.request] = container
// Then
#expect(cache[Test.request] != nil)
#expect(cache.totalCount == 1)
}
@Test func entryCostLimitEntryNotStored() {
// Given
let container = ImageContainer(image: Test.image)
let cost = cache.cost(for: container)
cache.costLimit = Int(Double(cost) * 3)
cache.entryCostLimit = 0.1
// When
cache[Test.request] = container
// Then
#expect(cache[Test.request] == nil)
#expect(cache.totalCount == 0)
}
@Test func trimToCost() {
// Given
cache.costLimit = Int.max
cache[request1] = Test.container
cache[request2] = Test.container
// When
let cost = cache.cost(for: ImageContainer(image: Test.image))
cache.trim(toCost: Int(Double(cost) * 1.5))
// Then
#expect(cache[request1] == nil)
#expect(cache[request2] != nil)
}
@Test func imagesAreRemovedOnCostLimitChange() {
// Given
let cost = cache.cost(for: ImageContainer(image: Test.image))
cache.costLimit = Int(Double(cost) * 2.5)
cache[request1] = Test.container
cache[request2] = Test.container
// When
cache.costLimit = cost
// Then
#expect(cache[request1] == nil)
#expect(cache[request2] != nil)
}
@Test func imageContainerWithoutAssociatedDataCost() {
// Given
let data = Test.data(name: "cat", extension: "gif")
let image = PlatformImage(data: data)!
let container = ImageContainer(image: image, data: nil)
// Then
#expect(cache.cost(for: container) == 558000)
}
@Test func imageContainerWithAssociatedDataCost() {
// Given
let data = Test.data(name: "cat", extension: "gif")
let image = PlatformImage(data: data)!
let container = ImageContainer(image: image, data: data)
// Then
#expect(cache.cost(for: container) == 558000 + 427672)
}
#endif
// MARK: LRU
@Test func leastRecentItemsAreRemoved() {
// Given
let cost = cache.cost(for: ImageContainer(image: Test.image))
cache.costLimit = Int(Double(cost) * 2.5)
cache[request1] = Test.container
cache[request2] = Test.container
cache[request3] = Test.container
// Then
#expect(cache[request1] == nil)
#expect(cache[request2] != nil)
#expect(cache[request3] != nil)
}
@Test func itemsAreTouched() {
// Given
let cost = cache.cost(for: ImageContainer(image: Test.image))
cache.costLimit = Int(Double(cost) * 2.5)
cache[request1] = Test.container
cache[request2] = Test.container
_ = cache[request1] // Touched image
// When
cache[request3] = Test.container
// Then
#expect(cache[request1] != nil)
#expect(cache[request2] == nil)
#expect(cache[request3] != nil)
}
// MARK: Misc
@Test func removeAll() {
// GIVEN
cache[request1] = Test.container
cache[request2] = Test.container
// WHEN
cache.removeAll()
// THEN
#expect(cache.totalCount == 0)
#expect(cache.totalCost == 0)
}
#if canImport(UIKit)
@Test @MainActor func someImagesAreRemovedOnDidEnterBackground() async {
// GIVEN
cache.costLimit = Int.max
cache.countLimit = 10 // 1 out of 10 images should remain
for i in 0..<10 {
cache[_request(index: i)] = Test.container
}
#expect(cache.totalCount == 10)
// WHEN
let task = Task { @MainActor in
NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil)
// THEN
#expect(cache.totalCount == 1)
}
await task.value
}
@Test @MainActor func someImagesAreRemovedBasedOnCostOnDidEnterBackground() async {
// GIVEN
let cost = cache.cost(for: ImageContainer(image: Test.image))
cache.costLimit = cost * 10
cache.countLimit = Int.max
for index in 0..<10 {
let request = ImageRequest(url: URL(string: "http://example.com/img\(index)")!)
cache[request] = Test.container
}
#expect(cache.totalCount == 10)
// WHEN
let task = Task { @MainActor in
NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil)
// THEN
#expect(cache.totalCount == 1)
}
await task.value
}
#endif
}
@Suite struct InternalCacheTTLTests {
let cache = Cache<Int, Int>(costLimit: 1000, countLimit: 1000)
// MARK: TTL
@Test func ttl() {
// Given
cache.set(1, forKey: 1, cost: 1, ttl: 0.05) // 50 ms
#expect(cache.value(forKey: 1) != nil)
// When
usleep(55 * 1000)
// Then
#expect(cache.value(forKey: 1) == nil)
}
@Test func defaultTTLIsUsed() {
// Given
cache.conf.ttl = 0.05// 50 ms
cache.set(1, forKey: 1, cost: 1)
#expect(cache.value(forKey: 1) != nil)
// When
usleep(55 * 1000)
// Then
#expect(cache.value(forKey: 1) == nil)
}
@Test func defaultToNonExpiringEntries() {
// Given
cache.set(1, forKey: 1, cost: 1)
#expect(cache.value(forKey: 1) != nil)
// When
usleep(55 * 1000)
// Then
#expect(cache.value(forKey: 1) != nil)
}
}
private func _request(index: Int) -> ImageRequest {
return ImageRequest(url: URL(string: "http://example.com/img\(index)")!)
}
private let request1 = _request(index: 1)
private let request2 = _request(index: 2)
private let request3 = _request(index: 3)
class ImageCacheTests: XCTestCase, @unchecked Sendable {
var cache: ImageCache!
override func setUp() {
super.setUp()
cache = ImageCache()
cache.entryCostLimit = 2
}
// MARK: - Basics
@MainActor
func testCacheCreation() {
XCTAssertEqual(cache.totalCount, 0)
XCTAssertNil(cache[Test.request])
}
@MainActor
func testThatImageIsStored() {
// When
cache[Test.request] = Test.container
// Then
XCTAssertEqual(cache.totalCount, 1)
XCTAssertNotNil(cache[Test.request])
}
// MARK: - Subscript
@MainActor
func testThatImageIsStoredUsingSubscript() {
// When
cache[Test.request] = Test.container
// Then
XCTAssertNotNil(cache[Test.request])
}
// MARK: - Count
@MainActor
func testThatTotalCountChanges() {
XCTAssertEqual(cache.totalCount, 0)
cache[request1] = Test.container
XCTAssertEqual(cache.totalCount, 1)
cache[request2] = Test.container
XCTAssertEqual(cache.totalCount, 2)
cache[request2] = nil
XCTAssertEqual(cache.totalCount, 1)
cache[request1] = nil
XCTAssertEqual(cache.totalCount, 0)
}
@MainActor
func testThatCountLimitChanges() {
// When
cache.countLimit = 1
// Then
XCTAssertEqual(cache.countLimit, 1)
}
@MainActor
func testThatTTLChanges() {
//when
cache.ttl = 1
// Then
XCTAssertEqual(cache.ttl, 1)
}
@MainActor
func testThatItemsAreRemoveImmediatelyWhenCountLimitIsReached() {
// Given
cache.countLimit = 1
// When
cache[request1] = Test.container
cache[request2] = Test.container
// Then
XCTAssertNil(cache[request1])
XCTAssertNotNil(cache[request2])
}
@MainActor
func testTrimToCount() {
// Given
cache[request1] = Test.container
cache[request2] = Test.container
// When
cache.trim(toCount: 1)
// Then
XCTAssertNil(cache[request1])
XCTAssertNotNil(cache[request2])
}
@MainActor
func testThatImagesAreRemovedOnCountLimitChange() {
// Given
cache.countLimit = 2
cache[request1] = Test.container
cache[request2] = Test.container
// When
cache.countLimit = 1
// Then
XCTAssertNil(cache[request1])
XCTAssertNotNil(cache[request2])
}
// MARK: Cost
#if !os(macOS)
@MainActor
func testDefaultImageCost() {
XCTAssertEqual(cache.cost(for: ImageContainer(image: Test.image)), 1228800)
}
@MainActor
func testThatTotalCostChanges() {
let imageCost = cache.cost(for: ImageContainer(image: Test.image))
XCTAssertEqual(cache.totalCost, 0)
cache[request1] = Test.container
XCTAssertEqual(cache.totalCost, imageCost)
cache[request2] = Test.container
XCTAssertEqual(cache.totalCost, 2 * imageCost)
cache[request2] = nil
XCTAssertEqual(cache.totalCost, imageCost)
cache[request1] = nil
XCTAssertEqual(cache.totalCost, 0)
}
@MainActor
func testThatCostLimitChanged() {
// Given
let cost = cache.cost(for: ImageContainer(image: Test.image))
// When
cache.costLimit = Int(Double(cost) * 1.5)
// Then
XCTAssertEqual(cache.costLimit, Int(Double(cost) * 1.5))
}
@MainActor
func testThatItemsAreRemoveImmediatelyWhenCostLimitIsReached() {
// Given
let cost = cache.cost(for: ImageContainer(image: Test.image))
cache.costLimit = Int(Double(cost) * 1.5)
// When/Then
cache[request1] = Test.container
// LRU item is released
cache[request2] = Test.container
XCTAssertNil(cache[request1])
XCTAssertNotNil(cache[request2])
}
@MainActor
func testEntryCostLimitEntryStored() {
// Given
let container = ImageContainer(image: Test.image)
let cost = cache.cost(for: container)
cache.costLimit = Int(Double(cost) * 15)
cache.entryCostLimit = 0.1
// When
cache[Test.request] = container
// Then
XCTAssertNotNil(cache[Test.request])
XCTAssertEqual(cache.totalCount, 1)
}
@MainActor
func testEntryCostLimitEntryNotStored() {
// Given
let container = ImageContainer(image: Test.image)
let cost = cache.cost(for: container)
cache.costLimit = Int(Double(cost) * 3)
cache.entryCostLimit = 0.1
// When
cache[Test.request] = container
// Then
XCTAssertNil(cache[Test.request])
XCTAssertEqual(cache.totalCount, 0)
}
@MainActor
func testTrimToCost() {
// Given
cache.costLimit = Int.max
cache[request1] = Test.container
cache[request2] = Test.container
// When
let cost = cache.cost(for: ImageContainer(image: Test.image))
cache.trim(toCost: Int(Double(cost) * 1.5))
// Then
XCTAssertNil(cache[request1])
XCTAssertNotNil(cache[request2])
}
@MainActor
func testThatImagesAreRemovedOnCostLimitChange() {
// Given
let cost = cache.cost(for: ImageContainer(image: Test.image))
cache.costLimit = Int(Double(cost) * 2.5)
cache[request1] = Test.container
cache[request2] = Test.container
// When
cache.costLimit = cost
// Then
XCTAssertNil(cache[request1])
XCTAssertNotNil(cache[request2])
}
@MainActor
func testImageContainerWithoutAssociatedDataCost() {
// Given
let data = Test.data(name: "cat", extension: "gif")
let image = PlatformImage(data: data)!
let container = ImageContainer(image: image, data: nil)
// Then
XCTAssertEqual(cache.cost(for: container), 558000)
}
@MainActor
func testImageContainerWithAssociatedDataCost() {
// Given
let data = Test.data(name: "cat", extension: "gif")
let image = PlatformImage(data: data)!
let container = ImageContainer(image: image, data: data)
// Then
XCTAssertEqual(cache.cost(for: container), 558000 + 427672)
}
#endif
// MARK: LRU
@MainActor
func testThatLeastRecentItemsAreRemoved() {
// Given
let cost = cache.cost(for: ImageContainer(image: Test.image))
cache.costLimit = Int(Double(cost) * 2.5)
cache[request1] = Test.container
cache[request2] = Test.container
cache[request3] = Test.container
// Then
XCTAssertNil(cache[request1])
XCTAssertNotNil(cache[request2])
XCTAssertNotNil(cache[request3])
}
@MainActor
func testThatItemsAreTouched() {
// Given
let cost = cache.cost(for: ImageContainer(image: Test.image))
cache.costLimit = Int(Double(cost) * 2.5)
cache[request1] = Test.container
cache[request2] = Test.container
_ = cache[request1] // Touched image
// When
cache[request3] = Test.container
// Then
XCTAssertNotNil(cache[request1])
XCTAssertNil(cache[request2])
XCTAssertNotNil(cache[request3])
}
// MARK: Misc
@MainActor
func testRemoveAll() {
// GIVEN
cache[request1] = Test.container
cache[request2] = Test.container
// WHEN
cache.removeAll()
// THEN
XCTAssertEqual(cache.totalCount, 0)
XCTAssertEqual(cache.totalCost, 0)
}
#if os(iOS) || os(tvOS) || os(visionOS)
@MainActor
func testThatSomeImagesAreRemovedOnDidEnterBackground() async {
// GIVEN
cache.costLimit = Int.max
cache.countLimit = 10 // 1 out of 10 images should remain
for i in 0..<10 {
cache[_request(index: i)] = Test.container
}
XCTAssertEqual(cache.totalCount, 10)
// WHEN
let task = Task { @MainActor in
NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil)
// THEN
XCTAssertEqual(cache.totalCount, 1)
}
await task.value
}
@MainActor
func testThatSomeImagesAreRemovedBasedOnCostOnDidEnterBackground() async {
// GIVEN
let cost = cache.cost(for: ImageContainer(image: Test.image))
cache.costLimit = cost * 10
cache.countLimit = Int.max
for index in 0..<10 {
let request = ImageRequest(url: URL(string: "http://example.com/img\(index)")!)
cache[request] = Test.container
}
XCTAssertEqual(cache.totalCount, 10)
// WHEN
let task = Task { @MainActor in
NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil)
// THEN
XCTAssertEqual(cache.totalCount, 1)
}
await task.value
}
#endif
}
class InternalCacheTTLTests: XCTestCase {
let cache = Cache<Int, Int>(costLimit: 1000, countLimit: 1000)
// MARK: TTL
@MainActor
func testTTL() {
// Given
cache.set(1, forKey: 1, cost: 1, ttl: 0.05) // 50 ms
XCTAssertNotNil(cache.value(forKey: 1))
// When
usleep(55 * 1000)
// Then
XCTAssertNil(cache.value(forKey: 1))
}
@MainActor
func testDefaultTTLIsUsed() {
// Given
cache.conf.ttl = 0.05// 50 ms
cache.set(1, forKey: 1, cost: 1)
XCTAssertNotNil(cache.value(forKey: 1))
// When
usleep(55 * 1000)
// Then
XCTAssertNil(cache.value(forKey: 1))
}
@MainActor
func testDefaultToNonExpiringEntries() {
// Given
cache.set(1, forKey: 1, cost: 1)
XCTAssertNotNil(cache.value(forKey: 1))
// When
usleep(55 * 1000)
// Then
XCTAssertNotNil(cache.value(forKey: 1))
}
}

View File

@ -2,237 +2,238 @@
//
// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean).
import XCTest
import Foundation
import Testing
@testable import Nuke
class ImageDecoderTests: XCTestCase {
func testDecodePNG() throws {
@Suite struct ImageDecoderTests {
@Test func decodePNG() throws {
// Given
let data = Test.data(name: "fixture", extension: "png")
let decoder = ImageDecoders.Default()
// When
let container = try XCTUnwrap(decoder.decode(data))
let container = try #require(try decoder.decode(data))
// Then
XCTAssertEqual(container.type, .png)
XCTAssertFalse(container.isPreview)
XCTAssertNil(container.data)
XCTAssertTrue(container.userInfo.isEmpty)
#expect(container.type == .png)
#expect(!container.isPreview)
#expect(container.data == nil)
#expect(container.userInfo.isEmpty)
}
func testDecodeJPEG() throws {
@Test func decodeJPEG() throws {
// Given
let data = Test.data(name: "baseline", extension: "jpeg")
let decoder = ImageDecoders.Default()
// When
let container = try XCTUnwrap(decoder.decode(data))
let container = try #require(try decoder.decode(data))
// Then
XCTAssertEqual(container.type, .jpeg)
XCTAssertFalse(container.isPreview)
XCTAssertNil(container.data)
XCTAssertTrue(container.userInfo.isEmpty)
#expect(container.type == .jpeg)
#expect(!container.isPreview)
#expect(container.data == nil)
#expect(container.userInfo.isEmpty)
}
func testDecodingProgressiveJPEG() {
@Test func decodingProgressiveJPEG() {
let data = Test.data(name: "progressive", extension: "jpeg")
let decoder = ImageDecoders.Default()
// Just before the Start Of Frame
XCTAssertNil(decoder.decodePartiallyDownloadedData(data[0...358]))
XCTAssertEqual(decoder.numberOfScans, 0)
#expect(decoder.decodePartiallyDownloadedData(data[0...358]) == nil)
#expect(decoder.numberOfScans == 0)
// Right after the Start Of Frame
XCTAssertNil(decoder.decodePartiallyDownloadedData(data[0...359]))
XCTAssertEqual(decoder.numberOfScans, 0) // still haven't finished the first scan
#expect(decoder.decodePartiallyDownloadedData(data[0...359]) == nil)
#expect(decoder.numberOfScans == 0) // still haven't finished the first scan // still haven't finished the first scan
// Just before the first Start Of Scan
XCTAssertNil(decoder.decodePartiallyDownloadedData(data[0...438]))
XCTAssertEqual(decoder.numberOfScans, 0) // still haven't finished the first scan
#expect(decoder.decodePartiallyDownloadedData(data[0...438]) == nil)
#expect(decoder.numberOfScans == 0) // still haven't finished the first scan // still haven't finished the first scan
// Found the first Start Of Scan
XCTAssertNil(decoder.decodePartiallyDownloadedData(data[0...439]))
XCTAssertEqual(decoder.numberOfScans, 1)
#expect(decoder.decodePartiallyDownloadedData(data[0...439]) == nil)
#expect(decoder.numberOfScans == 1)
// Found the second Start of Scan
let scan1 = decoder.decodePartiallyDownloadedData(data[0...2952])
XCTAssertNotNil(scan1)
XCTAssertEqual(scan1?.isPreview, true)
#expect(scan1 != nil)
#expect(scan1?.isPreview == true)
if let image = scan1?.image {
#if os(macOS)
XCTAssertEqual(image.size.width, 450)
XCTAssertEqual(image.size.height, 300)
#expect(image.size.width == 450)
#expect(image.size.height == 300)
#else
XCTAssertEqual(image.size.width * image.scale, 450)
XCTAssertEqual(image.size.height * image.scale, 300)
#expect(image.size.width * image.scale == 450)
#expect(image.size.height * image.scale == 300)
#endif
}
XCTAssertEqual(decoder.numberOfScans, 2)
XCTAssertEqual(scan1?.userInfo[.scanNumberKey] as? Int, 2)
#expect(decoder.numberOfScans == 2)
#expect(scan1?.userInfo[.scanNumberKey] as? Int == 2)
// Feed all data and see how many scans are there
// In practice the moment we finish receiving data we call
// `decode(data: data, isCompleted: true)` so we might not scan all the
// of the bytes and encounter all of the scans (e.g. the final chunk
// of data that we receive contains multiple scans).
XCTAssertNotNil(decoder.decodePartiallyDownloadedData(data))
XCTAssertEqual(decoder.numberOfScans, 10)
#expect(decoder.decodePartiallyDownloadedData(data) != nil)
#expect(decoder.numberOfScans == 10)
}
func testDecodeGIF() throws {
@Test func decodeGIF() throws {
// Given
let data = Test.data(name: "cat", extension: "gif")
let decoder = ImageDecoders.Default()
// When
let container = try XCTUnwrap(decoder.decode(data))
let container = try #require(try decoder.decode(data))
// Then
XCTAssertEqual(container.type, .gif)
XCTAssertFalse(container.isPreview)
XCTAssertNotNil(container.data)
XCTAssertTrue(container.userInfo.isEmpty)
#expect(container.type == .gif)
#expect(!container.isPreview)
#expect(container.data != nil)
#expect(container.userInfo.isEmpty)
}
func testDecodeHEIC() throws {
@Test func decodeHEIC() throws {
// Given
let data = Test.data(name: "img_751", extension: "heic")
let decoder = ImageDecoders.Default()
// When
let container = try XCTUnwrap(decoder.decode(data))
let container = try #require(try decoder.decode(data))
// Then
XCTAssertNil(container.type) // TODO: update when HEIF support is added
XCTAssertFalse(container.isPreview)
XCTAssertNil(container.data)
XCTAssertTrue(container.userInfo.isEmpty)
#expect(container.type == nil) // TODO: update when HEIF support is added // TODO: update when HEIF support is added
#expect(!container.isPreview)
#expect(container.data == nil)
#expect(container.userInfo.isEmpty)
}
func testDecodingGIFDataAttached() throws {
@Test func decodingGIFDataAttached() throws {
let data = Test.data(name: "cat", extension: "gif")
XCTAssertNotNil(try ImageDecoders.Default().decode(data).data)
#expect(try ImageDecoders.Default().decode(data).data != nil)
}
func testDecodingGIFPreview() throws {
@Test func decodingGIFPreview() throws {
let data = Test.data(name: "cat", extension: "gif")
XCTAssertEqual(data.count, 427672) // 427 KB
#expect(data.count == 427672) // 427 KB // 427 KB
let chunk = data[...60000] // 6 KB
let response = try ImageDecoders.Default().decode(chunk)
XCTAssertEqual(response.image.sizeInPixels, CGSize(width: 500, height: 279))
#expect(response.image.sizeInPixels == CGSize(width: 500, height: 279))
}
func testDecodingGIFPreviewGeneratedOnlyOnce() throws {
@Test func decodingGIFPreviewGeneratedOnlyOnce() throws {
let data = Test.data(name: "cat", extension: "gif")
XCTAssertEqual(data.count, 427672) // 427 KB
#expect(data.count == 427672) // 427 KB // 427 KB
let chunk = data[...60000] // 6 KB
let context = ImageDecodingContext.mock(data: chunk)
let decoder = try XCTUnwrap(ImageDecoders.Default(context: context))
XCTAssertNotNil(decoder.decodePartiallyDownloadedData(chunk))
XCTAssertNil(decoder.decodePartiallyDownloadedData(chunk))
let decoder = try #require(ImageDecoders.Default(context: context))
#expect(decoder.decodePartiallyDownloadedData(chunk) != nil)
#expect(decoder.decodePartiallyDownloadedData(chunk) == nil)
}
func testDecodingPNGDataNotAttached() throws {
@Test func decodingPNGDataNotAttached() throws {
let data = Test.data(name: "fixture", extension: "png")
let container = try ImageDecoders.Default().decode(data)
XCTAssertNil(container.data)
#expect(container.data == nil)
}
#if os(iOS) || os(tvOS) || os(macOS) || os(visionOS)
func testDecodeBaselineWebP() throws {
@Test func decodeBaselineWebP() throws {
if #available(OSX 11.0, iOS 14.0, watchOS 7.0, tvOS 999.0, *) {
let data = Test.data(name: "baseline", extension: "webp")
let container = try ImageDecoders.Default().decode(data)
XCTAssertEqual(container.image.sizeInPixels, CGSize(width: 550, height: 368))
XCTAssertNil(container.data)
#expect(container.image.sizeInPixels == CGSize(width: 550, height: 368))
#expect(container.data == nil)
}
}
#endif
}
class ImageTypeTests: XCTestCase {
@Suite struct ImageTypeTests {
// MARK: PNG
func testDetectPNG() {
@Test func detectPNG() {
let data = Test.data(name: "fixture", extension: "png")
XCTAssertNil(AssetType(data[0..<1]))
XCTAssertNil(AssetType(data[0..<7]))
XCTAssertEqual(AssetType(data[0..<8]), .png)
XCTAssertEqual(AssetType(data), .png)
#expect(AssetType(data[0..<1]) == nil)
#expect(AssetType(data[0..<7]) == nil)
#expect(AssetType(data[0..<8]) == .png)
#expect(AssetType(data) == .png)
}
// MARK: GIF
func testDetectGIF() {
@Test func detectGIF() {
let data = Test.data(name: "cat", extension: "gif")
XCTAssertEqual(AssetType(data), .gif)
#expect(AssetType(data) == .gif)
}
// MARK: JPEG
func testDetectBaselineJPEG() {
@Test func detectBaselineJPEG() {
let data = Test.data(name: "baseline", extension: "jpeg")
XCTAssertNil(AssetType(data[0..<1]))
XCTAssertNil(AssetType(data[0..<2]))
XCTAssertEqual(AssetType(data[0..<3]), .jpeg)
XCTAssertEqual(AssetType(data), .jpeg)
#expect(AssetType(data[0..<1]) == nil)
#expect(AssetType(data[0..<2]) == nil)
#expect(AssetType(data[0..<3]) == .jpeg)
#expect(AssetType(data) == .jpeg)
}
func testDetectProgressiveJPEG() {
@Test func detectProgressiveJPEG() {
let data = Test.data(name: "progressive", extension: "jpeg")
// Not enough data
XCTAssertNil(AssetType(Data()))
XCTAssertNil(AssetType(data[0..<2]))
#expect(AssetType(Data()) == nil)
#expect(AssetType(data[0..<2]) == nil)
// Enough to determine image format
XCTAssertEqual(AssetType(data[0..<3]), .jpeg)
XCTAssertEqual(AssetType(data[0..<33]), .jpeg)
#expect(AssetType(data[0..<3]) == .jpeg)
#expect(AssetType(data[0..<33]) == .jpeg)
// Full image
XCTAssertEqual(AssetType(data), .jpeg)
#expect(AssetType(data) == .jpeg)
}
// MARK: WebP
func testDetectBaselineWebP() {
@Test func detectBaselineWebP() {
let data = Test.data(name: "baseline", extension: "webp")
XCTAssertNil(AssetType(data[0..<1]))
XCTAssertNil(AssetType(data[0..<2]))
XCTAssertEqual(AssetType(data[0..<12]), .webp)
XCTAssertEqual(AssetType(data), .webp)
#expect(AssetType(data[0..<1]) == nil)
#expect(AssetType(data[0..<2]) == nil)
#expect(AssetType(data[0..<12]) == .webp)
#expect(AssetType(data) == .webp)
}
}
class ImagePropertiesTests: XCTestCase {
@Suite struct ImagePropertiesTests {
// MARK: JPEG
func testDetectBaselineJPEG() {
@Test func detectBaselineJPEG() {
let data = Test.data(name: "baseline", extension: "jpeg")
XCTAssertNil(ImageProperties.JPEG(data[0..<1]))
XCTAssertNil(ImageProperties.JPEG(data[0..<2]))
XCTAssertNil(ImageProperties.JPEG(data[0..<3]))
XCTAssertEqual(ImageProperties.JPEG(data)?.isProgressive, false)
#expect(ImageProperties.JPEG(data[0..<1]) == nil)
#expect(ImageProperties.JPEG(data[0..<2]) == nil)
#expect(ImageProperties.JPEG(data[0..<3]) == nil)
#expect(ImageProperties.JPEG(data)?.isProgressive == false)
}
func testDetectProgressiveJPEG() {
@Test func detectProgressiveJPEG() {
let data = Test.data(name: "progressive", extension: "jpeg")
// Not enough data
XCTAssertNil(ImageProperties.JPEG(Data()))
XCTAssertNil(ImageProperties.JPEG(data[0..<2]))
#expect(ImageProperties.JPEG(Data()) == nil)
#expect(ImageProperties.JPEG(data[0..<2]) == nil)
// Enough to determine image format
XCTAssertNil(ImageProperties.JPEG(data[0..<3]))
XCTAssertNil(ImageProperties.JPEG(data[0...30]))
#expect(ImageProperties.JPEG(data[0..<3]) == nil)
#expect(ImageProperties.JPEG(data[0...30]) == nil)
// Just before the first scan
XCTAssertNil(ImageProperties.JPEG(data[0...358]))
XCTAssertEqual(ImageProperties.JPEG(data[0...359])?.isProgressive, true)
#expect(ImageProperties.JPEG(data[0...358]) == nil)
#expect(ImageProperties.JPEG(data[0...359])?.isProgressive == true)
// Full image
XCTAssertEqual(ImageProperties.JPEG(data[0...359])?.isProgressive, true)
#expect(ImageProperties.JPEG(data[0...359])?.isProgressive == true)
}
}

View File

@ -2,12 +2,13 @@
//
// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean).
import XCTest
import Testing
import Foundation
@testable import Nuke
class ImageRequestTests: XCTestCase {
@Suite struct ImageRequestTests {
// The compiler picks up the new version
func testInit() {
@Test func testInit() {
_ = ImageRequest(url: Test.url)
_ = ImageRequest(url: Test.url, processors: [])
_ = ImageRequest(url: Test.url, processors: [])
@ -15,13 +16,13 @@ class ImageRequestTests: XCTestCase {
_ = ImageRequest(url: Test.url, options: [.reloadIgnoringCachedData])
}
func testExpressibleByStringLiteral() {
@Test func expressibleByStringLiteral() {
let _: ImageRequest = "https://example.com/image.jpeg"
}
// MARK: - CoW
func testCopyOnWrite() {
@Test func copyOnWrite() {
// GIVEN
var request = ImageRequest(url: URL(string: "http://test.com/1.png"))
request.options.insert(.disableMemoryCacheReads)
@ -35,165 +36,165 @@ class ImageRequestTests: XCTestCase {
copy.priority = .low
// THEN
XCTAssertEqual(copy.options.contains(.disableMemoryCacheReads), true)
XCTAssertEqual(copy.userInfo["key"] as? String, "3")
XCTAssertEqual((copy.processors.first as? MockImageProcessor)?.identifier, "4")
XCTAssertEqual(request.priority, .high) // Original request no updated
XCTAssertEqual(copy.priority, .low)
#expect(copy.options.contains(.disableMemoryCacheReads) == true)
#expect(copy.userInfo["key"] as? String == "3")
#expect((copy.processors.first as? MockImageProcessor)?.identifier == "4")
#expect(request.priority == .high) // Original request no updated // Original request no updated
#expect(copy.priority == .low)
}
// MARK: - Misc
// Just to make sure that comparison works as expected.
func testPriorityComparison() {
@Test func priorityComparison() {
typealias Priority = ImageRequest.Priority
XCTAssertTrue(Priority.veryLow < Priority.veryHigh)
XCTAssertTrue(Priority.low < Priority.normal)
XCTAssertTrue(Priority.normal == Priority.normal)
#expect(Priority.veryLow < Priority.veryHigh)
#expect(Priority.low < Priority.normal)
#expect(Priority.normal == Priority.normal)
}
func testUserInfoKey() {
@Test func userInfoKey() {
// WHEN
let request = ImageRequest(url: Test.url, userInfo: [.init("a"): 1])
// THEN
XCTAssertNotNil(request.userInfo["a"])
#expect(request.userInfo["a"] != nil)
}
}
class ImageRequestCacheKeyTests: XCTestCase {
func testDefaults() {
@Suite struct ImageRequestCacheKeyTests {
@Test func defaults() {
let request = Test.request
AssertHashableEqual(MemoryCacheKey(request), MemoryCacheKey(request)) // equal to itself
expectHashableMatch(MemoryCacheKey(request), MemoryCacheKey(request)) // equal to itself
}
func testRequestsWithTheSameURLsAreEquivalent() {
@Test func requestsWithTheSameURLsAreEquivalent() {
let lhs = ImageRequest(url: Test.url)
let rhs = ImageRequest(url: Test.url)
AssertHashableEqual(MemoryCacheKey(lhs), MemoryCacheKey(rhs))
expectHashableMatch(MemoryCacheKey(lhs), MemoryCacheKey(rhs))
}
func testRequestsWithDefaultURLRequestAndURLAreEquivalent() {
@Test func requestsWithDefaultURLRequestAndURLAreEquivalent() {
let lhs = ImageRequest(url: Test.url)
let rhs = ImageRequest(urlRequest: URLRequest(url: Test.url))
AssertHashableEqual(MemoryCacheKey(lhs), MemoryCacheKey(rhs))
expectHashableMatch(MemoryCacheKey(lhs), MemoryCacheKey(rhs))
}
func testRequestsWithDifferentURLsAreNotEquivalent() {
@Test func requestsWithDifferentURLsAreNotEquivalent() {
let lhs = ImageRequest(url: URL(string: "http://test.com/1.png"))
let rhs = ImageRequest(url: URL(string: "http://test.com/2.png"))
XCTAssertNotEqual(MemoryCacheKey(lhs), MemoryCacheKey(rhs))
#expect(MemoryCacheKey(lhs) != MemoryCacheKey(rhs))
}
func testRequestsWithTheSameProcessorsAreEquivalent() {
@Test func requestsWithTheSameProcessorsAreEquivalent() {
let lhs = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "1")])
let rhs = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "1")])
AssertHashableEqual(MemoryCacheKey(lhs), MemoryCacheKey(rhs))
expectHashableMatch(MemoryCacheKey(lhs), MemoryCacheKey(rhs))
}
func testRequestsWithDifferentProcessorsAreNotEquivalent() {
@Test func requestsWithDifferentProcessorsAreNotEquivalent() {
let lhs = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "1")])
let rhs = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "2")])
XCTAssertNotEqual(MemoryCacheKey(lhs), MemoryCacheKey(rhs))
#expect(MemoryCacheKey(lhs) != MemoryCacheKey(rhs))
}
func testURLRequestParametersAreIgnored() {
@Test func uRLRequestParametersAreIgnored() {
let lhs = ImageRequest(urlRequest: URLRequest(url: Test.url, cachePolicy: .reloadRevalidatingCacheData, timeoutInterval: 50))
let rhs = ImageRequest(urlRequest: URLRequest(url: Test.url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 0))
AssertHashableEqual(MemoryCacheKey(lhs), MemoryCacheKey(rhs))
expectHashableMatch(MemoryCacheKey(lhs), MemoryCacheKey(rhs))
}
func testSettingDefaultProcessorManually() {
@Test func settingDefaultProcessorManually() {
let lhs = ImageRequest(url: Test.url)
let rhs = ImageRequest(url: Test.url, processors: lhs.processors)
AssertHashableEqual(MemoryCacheKey(lhs), MemoryCacheKey(rhs))
expectHashableMatch(MemoryCacheKey(lhs), MemoryCacheKey(rhs))
}
}
class ImageRequestLoadKeyTests: XCTestCase {
func testDefaults() {
@Suite struct ImageRequestLoadKeyTests {
@Test func defaults() {
let request = ImageRequest(url: Test.url)
AssertHashableEqual(TaskFetchOriginalDataKey(request), TaskFetchOriginalDataKey(request))
expectHashableMatch(TaskFetchOriginalDataKey(request), TaskFetchOriginalDataKey(request))
}
func testRequestsWithTheSameURLsAreEquivalent() {
@Test func requestsWithTheSameURLsAreEquivalent() {
let lhs = ImageRequest(url: Test.url)
let rhs = ImageRequest(url: Test.url)
AssertHashableEqual(TaskFetchOriginalDataKey(lhs), TaskFetchOriginalDataKey(rhs))
expectHashableMatch(TaskFetchOriginalDataKey(lhs), TaskFetchOriginalDataKey(rhs))
}
func testRequestsWithDifferentURLsAreNotEquivalent() {
@Test func requestsWithDifferentURLsAreNotEquivalent() {
let lhs = ImageRequest(url: URL(string: "http://test.com/1.png"))
let rhs = ImageRequest(url: URL(string: "http://test.com/2.png"))
XCTAssertNotEqual(TaskFetchOriginalDataKey(lhs), TaskFetchOriginalDataKey(rhs))
#expect(TaskFetchOriginalDataKey(lhs) != TaskFetchOriginalDataKey(rhs))
}
func testRequestsWithTheSameProcessorsAreEquivalent() {
@Test func requestsWithTheSameProcessorsAreEquivalent() {
let lhs = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "1")])
let rhs = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "1")])
AssertHashableEqual(TaskFetchOriginalDataKey(lhs), TaskFetchOriginalDataKey(rhs))
expectHashableMatch(TaskFetchOriginalDataKey(lhs), TaskFetchOriginalDataKey(rhs))
}
func testRequestsWithDifferentProcessorsAreEquivalent() {
@Test func requestsWithDifferentProcessorsAreEquivalent() {
let lhs = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "1")])
let rhs = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "2")])
AssertHashableEqual(TaskFetchOriginalDataKey(lhs), TaskFetchOriginalDataKey(rhs))
expectHashableMatch(TaskFetchOriginalDataKey(lhs), TaskFetchOriginalDataKey(rhs))
}
func testRequestWithDifferentURLRequestParametersAreNotEquivalent() {
@Test func requestWithDifferentURLRequestParametersAreNotEquivalent() {
let lhs = ImageRequest(urlRequest: URLRequest(url: Test.url, cachePolicy: .reloadRevalidatingCacheData, timeoutInterval: 50))
let rhs = ImageRequest(urlRequest: URLRequest(url: Test.url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 0))
XCTAssertNotEqual(TaskFetchOriginalDataKey(lhs), TaskFetchOriginalDataKey(rhs))
#expect(TaskFetchOriginalDataKey(lhs) != TaskFetchOriginalDataKey(rhs))
}
func testMockImageProcessorCorrectlyImplementsIdentifiers() {
XCTAssertEqual(MockImageProcessor(id: "1").identifier, MockImageProcessor(id: "1").identifier)
XCTAssertEqual(MockImageProcessor(id: "1").hashableIdentifier, MockImageProcessor(id: "1").hashableIdentifier)
@Test func mockImageProcessorCorrectlyImplementsIdentifiers() {
#expect(MockImageProcessor(id: "1").identifier == MockImageProcessor(id: "1").identifier)
#expect(MockImageProcessor(id: "1").hashableIdentifier == MockImageProcessor(id: "1").hashableIdentifier)
XCTAssertNotEqual(MockImageProcessor(id: "1").identifier, MockImageProcessor(id: "2").identifier)
XCTAssertNotEqual(MockImageProcessor(id: "1").hashableIdentifier, MockImageProcessor(id: "2").hashableIdentifier)
#expect(MockImageProcessor(id: "1").identifier != MockImageProcessor(id: "2").identifier)
#expect(MockImageProcessor(id: "1").hashableIdentifier != MockImageProcessor(id: "2").hashableIdentifier)
}
}
class ImageRequestImageIdTests: XCTestCase {
func testThatCacheKeyUsesAbsoluteURLByDefault() {
@Suite struct ImageRequestImageIdTests {
@Test func thatCacheKeyUsesAbsoluteURLByDefault() {
let lhs = ImageRequest(url: Test.url)
let rhs = ImageRequest(url: Test.url.appendingPathComponent("?token=1"))
XCTAssertNotEqual(MemoryCacheKey(lhs), MemoryCacheKey(rhs))
#expect(MemoryCacheKey(lhs) != MemoryCacheKey(rhs))
}
func testThatCacheKeyUsesFilteredURLWhenSet() {
@Test func thatCacheKeyUsesFilteredURLWhenSet() {
let lhs = ImageRequest(url: Test.url, userInfo: [.imageIdKey: Test.url.absoluteString])
let rhs = ImageRequest(url: Test.url.appendingPathComponent("?token=1"), userInfo: [.imageIdKey: Test.url.absoluteString])
AssertHashableEqual(MemoryCacheKey(lhs), MemoryCacheKey(rhs))
expectHashableMatch(MemoryCacheKey(lhs), MemoryCacheKey(rhs))
}
func testThatCacheKeyForProcessedImageDataUsesAbsoluteURLByDefault() {
@Test func thatCacheKeyForProcessedImageDataUsesAbsoluteURLByDefault() {
let lhs = ImageRequest(url: Test.url)
let rhs = ImageRequest(url: Test.url.appendingPathComponent("?token=1"))
XCTAssertNotEqual(MemoryCacheKey(lhs), MemoryCacheKey(rhs))
#expect(MemoryCacheKey(lhs) != MemoryCacheKey(rhs))
}
func testThatCacheKeyForProcessedImageDataUsesFilteredURLWhenSet() {
@Test func thatCacheKeyForProcessedImageDataUsesFilteredURLWhenSet() {
let lhs = ImageRequest(url: Test.url, userInfo: [.imageIdKey: Test.url.absoluteString])
let rhs = ImageRequest(url: Test.url.appendingPathComponent("?token=1"), userInfo: [.imageIdKey: Test.url.absoluteString])
AssertHashableEqual(MemoryCacheKey(lhs), MemoryCacheKey(rhs))
expectHashableMatch(MemoryCacheKey(lhs), MemoryCacheKey(rhs))
}
func testThatLoadKeyForProcessedImageDoesntUseFilteredURL() {
@Test func thatLoadKeyForProcessedImageDoesntUseFilteredURL() {
let lhs = ImageRequest(url: Test.url, userInfo: [.imageIdKey: Test.url.absoluteString])
let rhs = ImageRequest(url: Test.url.appendingPathComponent("?token=1"), userInfo: [.imageIdKey: Test.url.absoluteString])
XCTAssertNotEqual(TaskLoadImageKey(lhs), TaskLoadImageKey(rhs))
#expect(TaskLoadImageKey(lhs) != TaskLoadImageKey(rhs))
}
func testThatLoadKeyForOriginalImageDoesntUseFilteredURL() {
@Test func thatLoadKeyForOriginalImageDoesntUseFilteredURL() {
let lhs = ImageRequest(url: Test.url, userInfo: [.imageIdKey: Test.url.absoluteString])
let rhs = ImageRequest(url: Test.url.appendingPathComponent("?token=1"), userInfo: [.imageIdKey: Test.url.absoluteString])
XCTAssertNotEqual(TaskFetchOriginalDataKey(lhs), TaskFetchOriginalDataKey(rhs))
#expect(TaskFetchOriginalDataKey(lhs) != TaskFetchOriginalDataKey(rhs))
}
}
private func AssertHashableEqual<T: Hashable>(_ lhs: T, _ rhs: T, file: StaticString = #file, line: UInt = #line) {
XCTAssertEqual(lhs.hashValue, rhs.hashValue, file: file, line: line)
XCTAssertEqual(lhs, rhs, file: file, line: line)
private func expectHashableMatch<T: Hashable>(_ lhs: T, _ rhs: T) {
#expect(lhs.hashValue == rhs.hashValue)
#expect(lhs == rhs)
}

View File

@ -2,74 +2,46 @@
//
// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean).
import XCTest
import Testing
@testable import Nuke
class RateLimiterTests: XCTestCase {
var rateLimiter: RateLimiter!
@Suite @ImagePipelineActor struct RateLimiterTests {
let rateLimiter = RateLimiter(rate: 10, burst: 2)
override func setUp() {
super.setUp()
// Note: we set very short rate to avoid bucket form being refilled too quickly
rateLimiter = RateLimiter(rate: 10, burst: 2)
}
@ImagePipelineActor
func testThatBurstIsExecutedimmediately() {
// Given
@Test func burstIsExecutedImmediately() {
var isExecuted = Array(repeating: false, count: 4)
// When
for i in isExecuted.indices {
rateLimiter.execute {
isExecuted[i] = true
return true
}
}
// Then
XCTAssertEqual(isExecuted, [true, true, false, false], "Expect first 2 items to be executed immediately")
#expect(isExecuted == [true, true, false, false], "Expect first 2 items to be executed immediately")
}
@ImagePipelineActor
func testThatNotExecutedItemDoesntExtractFromBucket() {
// Given
@Test func posponedItemsDoNotExtractFromBucket() {
var isExecuted = Array(repeating: false, count: 4)
// When
for i in isExecuted.indices {
rateLimiter.execute {
isExecuted[i] = true
return i != 1 // important!
}
}
// Then
XCTAssertEqual(isExecuted, [true, true, true, false], "Expect first 2 items to be executed immediately")
#expect(isExecuted == [true, true, true, false], "Expect first 2 items to be executed immediately")
}
@ImagePipelineActor
func testOverflow() {
// Given
var isExecuted = Array(repeating: false, count: 3)
// When
let expectation = self.expectation(description: "All work executed")
expectation.expectedFulfillmentCount = isExecuted.count
for i in isExecuted.indices {
rateLimiter.execute {
isExecuted[i] = true
expectation.fulfill()
return true
@Test func overflow() async {
let count = 3
await confirmation(expectedCount: count) { done in
for _ in 0..<count {
await withUnsafeContinuation { continuation in
rateLimiter.execute {
done()
continuation.resume(returning: ())
return true
}
}
}
}
// When time is passed
wait()
// Then
XCTAssertEqual(isExecuted, [true, true, true], "Expect 3rd item to be executed after a short delay")
}
}

View File

@ -2,14 +2,14 @@
//
// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean).
import XCTest
import Testing
import Foundation
@testable import Nuke
@ImagePipelineActor
class TaskTests: XCTestCase {
@Suite @ImagePipelineActor struct TaskTests {
// MARK: - Starter
func testStarterCalledOnFirstSubscription() {
@Test func starterCalledOnFirstSubscription() {
// Given
var startCount = 0
_ = SimpleTask<Int, Error>(starter: { _ in
@ -17,10 +17,10 @@ class TaskTests: XCTestCase {
})
// Then
XCTAssertEqual(startCount, 0)
#expect(startCount == 0)
}
func testStarterCalledWhenSubscriptionIsAdded() {
@Test func starterCalledWhenSubscriptionIsAdded() {
// Given
var startCount = 0
let task = SimpleTask<Int, Error>(starter: { _ in
@ -31,10 +31,10 @@ class TaskTests: XCTestCase {
_ = task.subscribe { _ in }
// Then started is called
XCTAssertEqual(startCount, 1)
#expect(startCount == 1)
}
func testStarterOnlyCalledOnce() {
@Test func starterOnlyCalledOnce() {
// Given
var startCount = 0
let task = SimpleTask<Int, Error>(starter: { _ in
@ -46,10 +46,10 @@ class TaskTests: XCTestCase {
_ = task.subscribe { _ in }
// Then started is only called once
XCTAssertEqual(startCount, 1)
#expect(startCount == 1)
}
func testStarterIsDeallocated() {
@Test func tarterIsDeallocated() {
// Given
class Foo {
}
@ -64,18 +64,18 @@ class TaskTests: XCTestCase {
})
}
XCTAssertNotNil(weakFoo, "Foo is retained by starter")
#expect(weakFoo != nil, "Foo is retained by starter")
// When first subscription is added and starter is called
_ = task.subscribe { _ in }
// Then
XCTAssertNil(weakFoo, "Started wasn't deallocated")
#expect(weakFoo == nil, "Started wasn't deallocated")
}
// MARK: - Subscribe
func testWhenSubscriptionAddedEventsAreForwarded() {
@Test func whenSubscriptionAddedEventsAreForwarded() {
// Given
let task = SimpleTask<Int, MyError>(starter: {
$0.send(progress: TaskProgress(completed: 1, total: 2))
@ -91,7 +91,7 @@ class TaskTests: XCTestCase {
}
// Then
XCTAssertEqual(recordedEvents, [
#expect(recordedEvents == [
.progress(TaskProgress(completed: 1, total: 2)),
.value(1, isCompleted: false),
.progress(TaskProgress(completed: 2, total: 2)),
@ -99,7 +99,7 @@ class TaskTests: XCTestCase {
])
}
func testBothSubscriptionsReceiveEvents() {
@Test func bothSubscriptionsReceiveEvents() {
// Given
let task = AsyncTask<Int, MyError>()
@ -107,20 +107,20 @@ class TaskTests: XCTestCase {
var eventCount = 0
_ = task.subscribe { event in
XCTAssertEqual(event, .value(1, isCompleted: false))
#expect(event == .value(1, isCompleted: false))
eventCount += 1 }
_ = task.subscribe { event in
XCTAssertEqual(event, .value(1, isCompleted: false))
#expect(event == .value(1, isCompleted: false))
eventCount += 1
}
task.send(value: 1)
// Then
XCTAssertEqual(eventCount, 2)
#expect(eventCount == 2)
}
func testCantSubscribeToAlreadyCancelledTask() {
@Test func cantSubscribeToAlreadyCancelledTask() {
// Given
let task = SimpleTask<Int, MyError>(starter: { _ in })
let subscription = task.subscribe { _ in }
@ -129,10 +129,10 @@ class TaskTests: XCTestCase {
subscription?.unsubscribe()
// Then
XCTAssertNil(task.subscribe { _ in })
#expect(task.subscribe { _ in } == nil)
}
func testCantSubscribeToAlreadySucceededTask() {
@Test func cantSubscribeToAlreadySucceededTask() {
// Given
let task = AsyncTask<Int, MyError>()
_ = task.subscribe { _ in }
@ -141,10 +141,10 @@ class TaskTests: XCTestCase {
task.send(value: 1, isCompleted: true)
// Then
XCTAssertNil(task.subscribe { _ in })
#expect(task.subscribe { _ in } == nil)
}
func testCantSubscribeToAlreadyFailedTasks() {
@Test func cantSubscribeToAlreadyFailedTasks() {
// Given
let task = AsyncTask<Int, MyError>()
_ = task.subscribe { _ in }
@ -153,29 +153,27 @@ class TaskTests: XCTestCase {
task.send(error: .init(raw: "1"))
// Then
XCTAssertNil(task.subscribe { _ in })
#expect(task.subscribe { _ in } == nil)
}
func testSubscribeToTaskWithSynchronousCompletionReturnsNil() {
@Test func subscribeToTaskWithSynchronousCompletionReturnsNil() async {
// Given
let task = SimpleTask<Int, MyError> { (task) in
task.send(value: 0, isCompleted: true)
}
// When
let expectation = self.expectation(description: "Observer called")
let subscription = task.subscribe { _ in
expectation.fulfill()
// When/Then
await withUnsafeContinuation { continuation in
let subscription = task.subscribe { _ in
continuation.resume()
}
#expect(subscription == nil)
}
// Then
XCTAssertNil(subscription)
wait()
}
// MARK: - Ubsubscribe
func testWhenSubscriptionIsRemovedNoEventsAreSent() {
@Test func whenSubscriptionIsRemovedNoEventsAreSent() {
// Given
let task = AsyncTask<Int, MyError>()
var recordedEvents = [AsyncTask<Int, MyError>.Event]()
@ -186,10 +184,10 @@ class TaskTests: XCTestCase {
task.send(value: 1)
// Then
XCTAssertTrue(recordedEvents.isEmpty, "Expect no events to be received by observer after subscription is removed")
#expect(recordedEvents.isEmpty, "Expect no events to be received by observer after subscription is removed")
}
func testWhenSubscriptionIsRemovedTaskBecomesDisposed() {
@Test func whenSubscriptionIsRemovedTaskBecomesDisposed() {
// Given
let task = AsyncTask<Int, MyError>()
let subscription = task.subscribe { _ in }
@ -198,10 +196,10 @@ class TaskTests: XCTestCase {
subscription?.unsubscribe()
// Then
XCTAssertTrue(task.isDisposed, "Expect task to be marked as disposed")
#expect(task.isDisposed, "Expect task to be marked as disposed")
}
func testWhenSubscriptionIsRemovedOnCancelIsCalled() {
@Test func whenSubscriptionIsRemovedOnCancelIsCalled() {
// Given
let task = AsyncTask<Int, MyError>()
let subscription = task.subscribe { _ in }
@ -215,39 +213,39 @@ class TaskTests: XCTestCase {
subscription?.unsubscribe()
// Then
XCTAssertTrue(onCancelledIsCalled)
#expect(onCancelledIsCalled)
}
func testWhenSubscriptionIsRemovedOperationIsCancelled() {
@Test func whenSubscriptionIsRemovedOperationIsCancelled() {
// Given
let operation = Foundation.Operation()
let task = SimpleTask<Int, MyError>(starter: { $0.operation = operation })
let subscription = task.subscribe { _ in }
XCTAssertFalse(operation.isCancelled)
#expect(!operation.isCancelled)
// When
subscription?.unsubscribe()
// Then
XCTAssertTrue(operation.isCancelled)
#expect(operation.isCancelled)
}
func testWhenSubscriptionIsRemovedDependencyIsCancelled() {
@Test func whenSubscriptionIsRemovedDependencyIsCancelled() {
// Given
let operation = Foundation.Operation()
let dependency = SimpleTask<Int, MyError>(starter: { $0.operation = operation })
let task = SimpleTask<Int, MyError>(starter: { $0.dependency = dependency.subscribe { _ in } })
let subscription = task.subscribe { _ in }
XCTAssertFalse(operation.isCancelled)
#expect(!operation.isCancelled)
// When
subscription?.unsubscribe()
// Then
XCTAssertTrue(operation.isCancelled)
#expect(operation.isCancelled)
}
func testWhenOneOfTwoSubscriptionsAreRemovedTaskNotCancelled() {
@Test func whenOneOfTwoSubscriptionsAreRemovedTaskNotCancelled() {
// Given
let operation = Foundation.Operation()
let task = SimpleTask<Int, MyError>(starter: { $0.operation = operation })
@ -258,10 +256,10 @@ class TaskTests: XCTestCase {
subscription1?.unsubscribe()
// Then
XCTAssertFalse(operation.isCancelled)
#expect(!operation.isCancelled)
}
func testWhenTwoOfTwoSubscriptionsAreRemovedTaskIsCancelled() {
@Test func whenTwoOfTwoSubscriptionsAreRemovedTaskIsCancelled() {
// Given
let operation = Foundation.Operation()
let task = SimpleTask<Int, MyError>(starter: { $0.operation = operation })
@ -273,12 +271,12 @@ class TaskTests: XCTestCase {
subscription2?.unsubscribe()
// Then
XCTAssertTrue(operation.isCancelled)
#expect(operation.isCancelled)
}
// MARK: - Priority
func testWhenPriorityIsUpdatedOperationPriorityAlsoUpdated() {
@Test func whenPriorityIsUpdatedOperationPriorityAlsoUpdated() {
// Given
let operation = Foundation.Operation()
let task = SimpleTask<Int, MyError>(starter: { $0.operation = operation })
@ -288,10 +286,10 @@ class TaskTests: XCTestCase {
subscription?.setPriority(.high)
// Then
XCTAssertEqual(operation.queuePriority, .high)
#expect(operation.queuePriority == .high)
}
func testWhenTaskChangesOperationPriorityUpdated() { // Or sets operation later
@Test func whenTaskChangesOperationPriorityUpdated() { // Or sets operation later
// Given
let task = AsyncTask<Int, MyError>()
let subscription = task.subscribe { _ in }
@ -302,10 +300,10 @@ class TaskTests: XCTestCase {
task.operation = operation
// Then
XCTAssertEqual(operation.queuePriority, .high)
#expect(operation.queuePriority == .high)
}
func testThatPriorityCanBeLowered() {
@Test func priorityCanBeLowered() {
// Given
let operation = Foundation.Operation()
let task = SimpleTask<Int, MyError>(starter: { $0.operation = operation })
@ -315,10 +313,10 @@ class TaskTests: XCTestCase {
subscription?.setPriority(.low)
// Then
XCTAssertEqual(operation.queuePriority, .low)
#expect(operation.queuePriority == .low)
}
func testThatPriorityEqualMaximumPriorityOfAllSubscriptions() {
@Test func priorityEqualMaximumPriorityOfAllSubscriptions() {
// Given
let operation = Foundation.Operation()
let task = SimpleTask<Int, MyError>(starter: { $0.operation = operation })
@ -330,10 +328,10 @@ class TaskTests: XCTestCase {
subscription2?.setPriority(.high)
// Then
XCTAssertEqual(operation.queuePriority, .high)
#expect(operation.queuePriority == .high)
}
func testWhenSubscriptionIsRemovedPriorityIsUpdated() {
@Test func subscriptionIsRemovedPriorityIsUpdated() {
// Given
let operation = Foundation.Operation()
let task = SimpleTask<Int, MyError>(starter: { $0.operation = operation })
@ -347,10 +345,10 @@ class TaskTests: XCTestCase {
subscription2?.unsubscribe()
// Then
XCTAssertEqual(operation.queuePriority, .low)
#expect(operation.queuePriority == .low)
}
func testWhenSubscriptionLowersPriorityButExistingSubscriptionHasHigherPriporty() {
@Test func whenSubscriptionLowersPriorityButExistingSubscriptionHasHigherPriporty() {
// Given
let operation = Foundation.Operation()
let task = SimpleTask<Int, MyError>(starter: { $0.operation = operation })
@ -362,10 +360,10 @@ class TaskTests: XCTestCase {
subscription1?.setPriority(.low)
// Then order of updating sub
XCTAssertEqual(operation.queuePriority, .high)
#expect(operation.queuePriority == .high)
}
func testPriorityOfDependencyUpdated() {
@Test func priorityOfDependencyUpdated() {
// Given
let operation = Foundation.Operation()
let dependency = SimpleTask<Int, MyError>(starter: { $0.operation = operation })
@ -376,12 +374,12 @@ class TaskTests: XCTestCase {
subscription?.setPriority(.high)
// Then
XCTAssertEqual(operation.queuePriority, .high)
#expect(operation.queuePriority == .high)
}
// MARK: - Dispose
func testExecutingTaskIsntDisposed() {
@Test func executingTaskIsntDisposed() {
// Given
let task = AsyncTask<Int, MyError>()
var isDisposeCalled = false
@ -392,11 +390,11 @@ class TaskTests: XCTestCase {
task.send(value: 1) // Casually sending value
// Then
XCTAssertFalse(isDisposeCalled)
XCTAssertFalse(task.isDisposed)
#expect(!isDisposeCalled)
#expect(!task.isDisposed)
}
func testThatTaskIsDisposedWhenCancelled() {
@Test func taskIsDisposedWhenCancelled() {
// Given
let task = SimpleTask<Int, MyError>(starter: { _ in })
var isDisposeCalled = false
@ -407,11 +405,11 @@ class TaskTests: XCTestCase {
subscription?.unsubscribe()
// Then
XCTAssertTrue(isDisposeCalled)
XCTAssertTrue(task.isDisposed)
#expect(isDisposeCalled)
#expect(task.isDisposed)
}
func testThatTaskIsDisposedWhenCompletedWithSuccess() {
@Test func taskIsDisposedWhenCompletedWithSuccess() {
// Given
let task = AsyncTask<Int, MyError>()
var isDisposeCalled = false
@ -422,11 +420,11 @@ class TaskTests: XCTestCase {
task.send(value: 1, isCompleted: true)
// Then
XCTAssertTrue(isDisposeCalled)
XCTAssertTrue(task.isDisposed)
#expect(isDisposeCalled)
#expect(task.isDisposed)
}
func testThatTaskIsDisposedWhenCompletedWithFailure() {
@Test func taskIsDisposedWhenCompletedWithFailure() {
// Given
let task = AsyncTask<Int, MyError>()
var isDisposeCalled = false
@ -437,8 +435,8 @@ class TaskTests: XCTestCase {
task.send(error: .init(raw: "1"))
// Then
XCTAssertTrue(isDisposeCalled)
XCTAssertTrue(task.isDisposed)
#expect(isDisposeCalled)
#expect(task.isDisposed)
}
}