mirror of
https://github.com/kean/Pulse.git
synced 2026-05-30 21:07:33 +00:00
781 lines
28 KiB
Swift
781 lines
28 KiB
Swift
// The MIT License (MIT)
|
|
//
|
|
// Copyright (c) 2020-2022 Alexander Grebenyuk (github.com/kean).
|
|
|
|
import XCTest
|
|
import Foundation
|
|
import CoreData
|
|
@testable import Pulse
|
|
|
|
final class LoggerStoreTests: XCTestCase {
|
|
let directory = TemporaryDirectory()
|
|
var storeURL: URL!
|
|
var date: Date = Date()
|
|
|
|
var store: LoggerStore!
|
|
|
|
override func setUp() {
|
|
super.setUp()
|
|
|
|
try? FileManager.default.createDirectory(at: directory.url, withIntermediateDirectories: true, attributes: nil)
|
|
storeURL = directory.url.appending(filename: "test-store")
|
|
store = try! LoggerStore(storeURL: storeURL, options: [.create, .synchronous])
|
|
}
|
|
|
|
override func tearDown() {
|
|
super.tearDown()
|
|
|
|
try? store.destroy()
|
|
directory.remove()
|
|
|
|
try? FileManager.default.removeItem(at: URL.temp)
|
|
}
|
|
|
|
// MARK: - Init
|
|
|
|
func testInitStoreMissing() throws {
|
|
// GIVEN
|
|
let storeURL = directory.url.appending(filename: UUID().uuidString)
|
|
|
|
// WHEN/THEN
|
|
XCTAssertThrowsError(try LoggerStore(storeURL: storeURL))
|
|
}
|
|
|
|
func testInitCreateStoreURL() throws {
|
|
// GIVEN
|
|
let storeURL = directory.url.appending(filename: UUID().uuidString)
|
|
let options: LoggerStore.Options = [.create, .synchronous]
|
|
|
|
// WHEN
|
|
let firstStore = try LoggerStore(storeURL: storeURL, options: options)
|
|
populate(store: firstStore)
|
|
|
|
let secondStore = try LoggerStore(storeURL: storeURL)
|
|
|
|
// THEN data is persisted
|
|
XCTAssertEqual(try secondStore.allMessages().count, 10)
|
|
|
|
// CLEANUP
|
|
try? firstStore.destroy()
|
|
try? secondStore.destroy()
|
|
}
|
|
|
|
func testInitCreateStoreIntermediateDirectoryMissing() throws {
|
|
// GIVEN
|
|
let storeURL = directory.url
|
|
.appending(directory: UUID().uuidString)
|
|
.appending(filename: UUID().uuidString)
|
|
let options: LoggerStore.Options = [.create]
|
|
|
|
// WHEN/THEN
|
|
XCTAssertThrowsError(try LoggerStore(storeURL: storeURL, options: options))
|
|
}
|
|
|
|
func testInitStoreWithURL() throws {
|
|
// GIVEN
|
|
populate(store: store)
|
|
XCTAssertEqual(try store.allMessages().count, 10)
|
|
|
|
let originalStore = store
|
|
try? store.close()
|
|
|
|
// WHEN loading the store with the same url
|
|
store = try LoggerStore(storeURL: storeURL)
|
|
|
|
// THEN data is persisted
|
|
XCTAssertEqual(try store.allMessages().count, 10)
|
|
|
|
// CLEANUP
|
|
try? originalStore?.destroy()
|
|
}
|
|
|
|
func testInitWithArchiveURL() throws {
|
|
// GIVEN
|
|
let storeURL = directory.url.appending(filename: "logs-archive-v2.pulse")
|
|
try Resources.pulseArchive.write(to: storeURL)
|
|
|
|
// WHEN
|
|
let store = try XCTUnwrap(LoggerStore(storeURL: storeURL))
|
|
defer { try? store.destroy() }
|
|
|
|
// THEN entities can be opened
|
|
XCTAssertEqual(try store.viewContext.count(for: LoggerMessageEntity.self), 15)
|
|
XCTAssertEqual(try store.viewContext.count(for: NetworkTaskEntity.self), 8)
|
|
XCTAssertEqual(try store.viewContext.count(for: LoggerBlobHandleEntity.self), 7)
|
|
|
|
// THEN data stored in external storage also persist
|
|
let request = try XCTUnwrap(store.viewContext.first(NetworkTaskEntity.self) {
|
|
$0.predicate = NSPredicate(format: "url == %@", "https://github.com/repos")
|
|
})
|
|
XCTAssertEqual(request.responseBodySize, 165061)
|
|
let blob = benchmark(title: "Access Blob") {
|
|
return request.responseBody?.data
|
|
}
|
|
XCTAssertEqual(blob?.count, 165061)
|
|
}
|
|
|
|
func testInitWithArchiveURLNoExtension() throws {
|
|
// GIVEN
|
|
let storeURL = directory.url.appending(filename: "logs-archive-v2")
|
|
try Resources.pulseArchive.write(to: storeURL)
|
|
|
|
// WHEN
|
|
let store = try XCTUnwrap(LoggerStore(storeURL: storeURL))
|
|
defer { try? store.destroy() }
|
|
|
|
// THEN entities can be opened
|
|
XCTAssertEqual(try store.viewContext.count(for: LoggerMessageEntity.self), 15)
|
|
XCTAssertEqual(try store.viewContext.count(for: NetworkTaskEntity.self), 8)
|
|
XCTAssertEqual(try store.viewContext.count(for: LoggerBlobHandleEntity.self), 7)
|
|
|
|
// THEN data stored in external storage also persist
|
|
let request = try XCTUnwrap(store.viewContext.first(NetworkTaskEntity.self) {
|
|
$0.predicate = NSPredicate(format: "url == %@", "https://github.com/repos")
|
|
})
|
|
XCTAssertEqual(request.responseBodySize, 165061)
|
|
XCTAssertEqual(request.responseBody?.data?.count, 165061)
|
|
}
|
|
|
|
func testInitWithPackageURL() throws {
|
|
// GIVEN
|
|
let storeURL = try makePulsePackage()
|
|
|
|
// WHEN
|
|
let store = try XCTUnwrap(LoggerStore(storeURL: storeURL))
|
|
defer { try? store.destroy() }
|
|
|
|
// THEN entities can be opened
|
|
XCTAssertEqual(try store.viewContext.count(for: LoggerMessageEntity.self), 10)
|
|
XCTAssertEqual(try store.viewContext.count(for: NetworkTaskEntity.self), 3)
|
|
XCTAssertEqual(try store.viewContext.count(for: LoggerBlobHandleEntity.self), 3)
|
|
}
|
|
|
|
func testInitWithPackageURLNoExtension() throws {
|
|
// GIVEN
|
|
let storeURL = try makePulsePackage()
|
|
|
|
// WHEN
|
|
let store = try XCTUnwrap(LoggerStore(storeURL: storeURL))
|
|
defer { try? store.destroy() }
|
|
|
|
// THEN entities can be opened
|
|
XCTAssertEqual(try store.viewContext.count(for: LoggerMessageEntity.self), 10)
|
|
XCTAssertEqual(try store.viewContext.count(for: NetworkTaskEntity.self), 3)
|
|
XCTAssertEqual(try store.viewContext.count(for: LoggerBlobHandleEntity.self), 3)
|
|
}
|
|
|
|
// MARK: - Copy (Package)
|
|
|
|
func testCopyDirectory() throws {
|
|
// GIVEN
|
|
populate(store: store)
|
|
|
|
let copyURL = directory.url.appending(filename: "copy.pulse")
|
|
|
|
// WHEN
|
|
try store.copy(to: copyURL)
|
|
|
|
// THEN
|
|
try? store.close()
|
|
|
|
// THEN
|
|
let copy = try LoggerStore(storeURL: copyURL)
|
|
defer { try? copy.destroy() }
|
|
|
|
XCTAssertEqual(try copy.allMessages().count, 10)
|
|
XCTAssertEqual(try copy.allTasks().count, 3)
|
|
}
|
|
|
|
func testCopyCreatesInto() throws {
|
|
// GIVEN
|
|
let store = makeStore()
|
|
defer { try? store.destroy() }
|
|
|
|
populate(store: store)
|
|
date = Date()
|
|
let copyURL = directory.url.appending(filename: "copy.pulse")
|
|
try store.copy(to: copyURL)
|
|
|
|
// WHEN
|
|
let info = try LoggerStore.Info.make(storeURL: copyURL)
|
|
|
|
XCTAssertEqual(info.storeVersion, "2.0.3")
|
|
XCTAssertEqual(info.messageCount, 7)
|
|
XCTAssertEqual(info.taskCount, 3)
|
|
XCTAssertEqual(info.blobCount, 3)
|
|
XCTAssertEqual(info.creationDate, date)
|
|
XCTAssertEqual(info.modifiedDate, date)
|
|
}
|
|
|
|
func testCopyWithPredicate() throws {
|
|
// GIVEN
|
|
populate(store: store)
|
|
|
|
let copyURL = directory.url.appending(filename: "copy.pulse")
|
|
|
|
// WHEN
|
|
try store.copy(to: copyURL, predicate: NSPredicate(format: "level == %i", LoggerStore.Level.trace.rawValue))
|
|
|
|
// THEN
|
|
try? store.close()
|
|
|
|
// THEN all non-trace messages are removed, as well as network messages
|
|
// and associated blobs
|
|
let copy = try LoggerStore(storeURL: copyURL)
|
|
defer { try? copy.destroy() }
|
|
|
|
let context = copy.viewContext
|
|
XCTAssertEqual(try context.count(for: LoggerMessageEntity.self), 2)
|
|
XCTAssertEqual(try context.count(for: NetworkTaskEntity.self), 0)
|
|
XCTAssertEqual(try context.count(for: NetworkTaskProgressEntity.self), 0)
|
|
XCTAssertEqual(try context.count(for: LoggerBlobHandleEntity.self), 0)
|
|
}
|
|
|
|
func testCopyToNonExistingFolder() throws {
|
|
// GIVEN
|
|
populate(store: store)
|
|
|
|
let invalidURL = directory.url
|
|
.appending(directory: UUID().uuidString)
|
|
.appending(filename: "copy.pulse")
|
|
|
|
// WHEN/THEN
|
|
XCTAssertThrowsError(try store.copy(to: invalidURL))
|
|
}
|
|
|
|
func testCopyButFileExists() throws {
|
|
// GIVEN
|
|
populate(store: store)
|
|
|
|
let copyURL = directory.url.appending(filename: "copy.pulse")
|
|
|
|
try store.copy(to: copyURL)
|
|
|
|
// WHEN/THEN
|
|
XCTAssertThrowsError(try store.copy(to: copyURL))
|
|
|
|
print(copyURL)
|
|
}
|
|
|
|
// MARK: - Copy (Archive)
|
|
|
|
func testCopyFile() throws {
|
|
// GIVEN
|
|
let storeURL = directory.url.appending(filename: "logs-archive-v2.pulse")
|
|
try Resources.pulseArchive.write(to: storeURL)
|
|
|
|
let store = try XCTUnwrap(LoggerStore(storeURL: storeURL))
|
|
let copyURL = directory.url.appending(filename: "copy.pulse")
|
|
|
|
// WHEN
|
|
try store.copy(to: copyURL)
|
|
|
|
// THEN
|
|
let copy = try LoggerStore(storeURL: copyURL)
|
|
defer { try? copy.destroy() }
|
|
|
|
XCTAssertEqual(try copy.allMessages().count, 15)
|
|
XCTAssertEqual(try copy.allTasks().count, 8)
|
|
}
|
|
|
|
// MARK: - File (Readonly)
|
|
|
|
// TODO: this type of store is no longer immuatble
|
|
func _testOpenFileDatabaseImmutable() throws {
|
|
// GIVEN
|
|
let storeURL = directory.url.appending(filename: "logs-archive-v2.pulse")
|
|
try Resources.pulseArchive.write(to: storeURL)
|
|
|
|
let store = try XCTUnwrap(LoggerStore(storeURL: storeURL))
|
|
XCTAssertEqual(try store.allMessages().count, 23)
|
|
|
|
// WHEN/THEN
|
|
store.storeMessage(label: "test", level: .info, message: "test", metadata: nil)
|
|
|
|
// THEN nothing is written
|
|
XCTAssertEqual(try store.allMessages().count, 23)
|
|
|
|
let storeCopy = try XCTUnwrap(LoggerStore(storeURL: storeURL))
|
|
XCTAssertEqual(try storeCopy.allMessages().count, 23)
|
|
}
|
|
|
|
// MARK: - Expiration
|
|
|
|
func testSizeLimit() throws {
|
|
let store = try! LoggerStore(
|
|
storeURL: directory.url.appending(filename: UUID().uuidString),
|
|
options: [.create, .synchronous],
|
|
configuration: .init(sizeLimit: 5000)
|
|
)
|
|
defer { try? store.destroy() }
|
|
|
|
// GIVEN
|
|
let context = store.viewContext
|
|
|
|
store.storeRequest(URLRequest(url: URL(string: "example.com/delete-this")!), response: nil, error: nil, data: "hello".data(using: .utf8))
|
|
for index in 1...500 {
|
|
store.storeMessage(label: "default", level: .debug, message: "\(index)", metadata: {
|
|
index % 50 == 0 ? ["key": .stringConvertible(index)] : nil
|
|
}())
|
|
}
|
|
|
|
let copyURL = directory.url
|
|
.appending(filename: UUID().uuidString)
|
|
.appendingPathExtension("pulse")
|
|
try store.copy(to: copyURL)
|
|
|
|
// SANITY
|
|
var messages = try store.allMessages()
|
|
XCTAssertEqual(messages.count, 501)
|
|
XCTAssertEqual(messages.last?.text, "500")
|
|
|
|
XCTAssertEqual(try context.count(for: LoggerMessageEntity.self), 501)
|
|
XCTAssertEqual(try context.count(for: NetworkTaskEntity.self), 1)
|
|
XCTAssertEqual(try context.count(for: LoggerBlobHandleEntity.self), 1)
|
|
context.reset()
|
|
|
|
// WHEN
|
|
store.syncSweep()
|
|
|
|
// THEN
|
|
let copyURL2 = directory.url
|
|
.appending(filename: UUID().uuidString)
|
|
.appendingPathExtension("pulse")
|
|
try store.copy(to: copyURL2)
|
|
|
|
// THEN unwanted messages were removed
|
|
messages = try context.fetch(LoggerMessageEntity.self)
|
|
XCTAssertEqual(messages.count, 351)
|
|
XCTAssertEqual(messages.last?.text, "500") // Latest stored
|
|
|
|
// THEN metadata, network requests, blobs are removed
|
|
XCTAssertEqual(try context.count(for: LoggerMessageEntity.self), 351)
|
|
XCTAssertEqual(try context.count(for: NetworkTaskEntity.self), 0)
|
|
XCTAssertEqual(try context.count(for: LoggerBlobHandleEntity.self), 0)
|
|
}
|
|
|
|
func testMaxAgeSweep() throws {
|
|
// GIVEN the store with 5 minute max age
|
|
let store = makeStore {
|
|
$0.maxAge = 300
|
|
}
|
|
defer { try? store.destroy() }
|
|
|
|
// GIVEN some messages stored before the cutoff date
|
|
date = Date().addingTimeInterval(-1000)
|
|
store.storeMessage(label: "deleted", level: .debug, message: "test")
|
|
store.storeRequest(URLRequest(url: URL(string: "example.com/deleted")!), response: nil, error: nil, data: nil)
|
|
|
|
// GIVEN some messages stored after
|
|
date = Date()
|
|
store.storeMessage(label: "kept", level: .debug, message: "test")
|
|
store.storeRequest(URLRequest(url: URL(string: "example.com/kept")!), response: nil, error: nil, data: nil)
|
|
|
|
// ASSERT
|
|
let context = store.backgroundContext
|
|
XCTAssertEqual(try context.count(for: LoggerMessageEntity.self), 4)
|
|
XCTAssertEqual(try context.count(for: NetworkTaskEntity.self), 2)
|
|
XCTAssertEqual(try context.count(for: LoggerBlobHandleEntity.self), 0)
|
|
context.reset()
|
|
|
|
// WHEN
|
|
store.syncSweep()
|
|
|
|
// THEN
|
|
XCTAssertEqual(try context.count(for: LoggerMessageEntity.self), 2)
|
|
XCTAssertEqual(try context.count(for: NetworkTaskEntity.self), 1)
|
|
XCTAssertEqual(try context.count(for: NetworkTaskProgressEntity.self), 0)
|
|
XCTAssertEqual(try context.count(for: LoggerBlobHandleEntity.self), 0)
|
|
|
|
XCTAssertEqual(try store.allMessages().first?.label.name, "kept")
|
|
XCTAssertEqual(try store.allTasks().first?.url, "example.com/kept")
|
|
}
|
|
|
|
func testMaxAgeSweepBlobIsDeletedWhenEntityIsDeleted() throws {
|
|
// GIVEN the store with 5 minute max age
|
|
let store = makeStore {
|
|
$0.maxAge = 300
|
|
}
|
|
defer { try? store.destroy() }
|
|
|
|
// GIVEN a request with response body stored
|
|
date = Date().addingTimeInterval(-1000)
|
|
let responseData = "body".data(using: .utf8)!
|
|
store.storeRequest(URLRequest(url: URL(string: "example.com/deleted")!), response: nil, error: nil, data: responseData)
|
|
|
|
// ASSERT
|
|
XCTAssertEqual(try store.backgroundContext.count(for: LoggerBlobHandleEntity.self), 1)
|
|
XCTAssertEqual(try store.backgroundContext.first(LoggerBlobHandleEntity.self)?.data, responseData)
|
|
|
|
// WHEN
|
|
date = Date()
|
|
store.syncSweep()
|
|
|
|
// THEN associated data is deleted
|
|
XCTAssertEqual(try store.backgroundContext.count(for: LoggerMessageEntity.self), 0)
|
|
XCTAssertEqual(try store.backgroundContext.count(for: NetworkTaskEntity.self), 0)
|
|
XCTAssertEqual(try store.backgroundContext.count(for: NetworkTaskProgressEntity.self), 0)
|
|
XCTAssertEqual(try store.backgroundContext.count(for: LoggerBlobHandleEntity.self), 0)
|
|
}
|
|
|
|
func testMaxAgeSweepBlobIsDeletedWhenBothEntitiesReferencingItAre() throws {
|
|
// GIVEN the store with 5 minute max age
|
|
let store = makeStore {
|
|
$0.maxAge = 300
|
|
}
|
|
defer { try? store.destroy() }
|
|
|
|
// GIVEN a request with response body stored
|
|
date = Date().addingTimeInterval(-1000)
|
|
let responseData = "body".data(using: .utf8)!
|
|
store.storeRequest(URLRequest(url: URL(string: "example.com/deleted1")!), response: nil, error: nil, data: responseData)
|
|
store.storeRequest(URLRequest(url: URL(string: "example.com/deleted2")!), response: nil, error: nil, data: responseData)
|
|
|
|
// ASSERT
|
|
XCTAssertEqual(try store.backgroundContext.count(for: LoggerBlobHandleEntity.self), 1)
|
|
XCTAssertEqual(try store.backgroundContext.first(LoggerBlobHandleEntity.self)?.data, responseData)
|
|
|
|
// WHEN
|
|
date = Date()
|
|
store.syncSweep()
|
|
|
|
// THEN associated data is deleted
|
|
XCTAssertEqual(try store.backgroundContext.count(for: LoggerBlobHandleEntity.self), 0)
|
|
}
|
|
|
|
func testMaxAgeSweepBlobIsKeptIfOnlyOneReferencingEntityIsDeleted() throws {
|
|
// GIVEN the store with 5 minute max age
|
|
let store = makeStore {
|
|
$0.maxAge = 300
|
|
}
|
|
defer { try? store.destroy() }
|
|
|
|
// GIVEN a request with response body stored
|
|
date = Date().addingTimeInterval(-1000)
|
|
let responseData = "body".data(using: .utf8)!
|
|
store.storeRequest(URLRequest(url: URL(string: "example.com/deleted")!), response: nil, error: nil, data: responseData)
|
|
|
|
// GIVEN a request that's not deleted
|
|
date = Date()
|
|
store.storeRequest(URLRequest(url: URL(string: "example.com/kept")!), response: nil, error: nil, data: responseData)
|
|
|
|
// ASSERT
|
|
XCTAssertEqual(try store.backgroundContext.count(for: NetworkTaskEntity.self), 2)
|
|
XCTAssertEqual(try store.backgroundContext.count(for: LoggerBlobHandleEntity.self), 1)
|
|
XCTAssertEqual(try store.backgroundContext.first(LoggerBlobHandleEntity.self)?.data, responseData)
|
|
|
|
// WHEN
|
|
date = Date()
|
|
store.syncSweep()
|
|
|
|
// THEN associated data is deleted
|
|
XCTAssertEqual(try store.backgroundContext.count(for: NetworkTaskEntity.self), 1)
|
|
XCTAssertEqual(try store.backgroundContext.count(for: LoggerBlobHandleEntity.self), 1)
|
|
}
|
|
|
|
func testBlobSizeLimitSweep() throws {
|
|
// GIVEN store with blob size limit
|
|
let store = makeStore {
|
|
$0.maxAge = 300
|
|
$0.isBlobCompressionEnabled = false
|
|
$0.sizeLimit = 700 // will trigger sweep
|
|
$0.expectedBlobRatio = 1.0
|
|
$0.trimRatio = 0.5 // will remove items until 350 bytes are used
|
|
}
|
|
defer { try? store.destroy() }
|
|
|
|
let now = Date()
|
|
|
|
func storeRequest(id: String, offset: TimeInterval) {
|
|
date = now.addingTimeInterval(offset)
|
|
// Make sure data doesn't get deduplicated
|
|
let data = Data(count: 300) + id.data(using: .utf8)!
|
|
|
|
store.storeRequest(URLRequest(url: URL(string: "example.com/\(id)")!), response: nil, error: nil, data: data)
|
|
}
|
|
|
|
storeRequest(id: "1", offset: -100)
|
|
storeRequest(id: "2", offset: 0)
|
|
storeRequest(id: "3", offset: -200)
|
|
|
|
// ASSERT
|
|
XCTAssertEqual(try store.backgroundContext.count(for: LoggerBlobHandleEntity.self), 3)
|
|
|
|
// WHEN
|
|
store.syncSweep()
|
|
|
|
// THEN
|
|
let tasks = try store.backgroundContext.fetch(NetworkTaskEntity.self)
|
|
XCTAssertEqual(tasks.count, 3) // Keeps the requests
|
|
XCTAssertEqual(tasks.compactMap(\.responseBody).count, 1)
|
|
XCTAssertEqual(try store.backgroundContext.count(for: LoggerBlobHandleEntity.self), 1)
|
|
}
|
|
|
|
func testBlobSizeLimitSweepLargeBlob() throws {
|
|
// GIVEN store with blob size limit
|
|
let store = makeStore {
|
|
$0.isBlobCompressionEnabled = false
|
|
$0.sizeLimit = 100 // will trigger sweep
|
|
$0.expectedBlobRatio = 1.0
|
|
}
|
|
defer { try? store.destroy() }
|
|
|
|
let responseData = Data(count: 256 * 1024)
|
|
store.storeRequest(URLRequest(url: URL(string: "example.com/1")!), response: nil, error: nil, data: responseData)
|
|
|
|
// ASSERT it's stored in a file system
|
|
XCTAssertEqual(try store.backgroundContext.count(for: LoggerBlobHandleEntity.self), 1)
|
|
let request = try store.backgroundContext.first(NetworkTaskEntity.self)
|
|
let key = try XCTUnwrap(request?.responseBody?.key)
|
|
XCTAssertEqual(store.getBlobData(forKey: key.hexString), responseData)
|
|
|
|
// WHEN
|
|
store.syncSweep()
|
|
|
|
// THEN the file is deleted from the file system
|
|
XCTAssertEqual(try store.backgroundContext.count(for: LoggerBlobHandleEntity.self), 0)
|
|
XCTAssertNil(store.getBlobData(forKey: key.hexString))
|
|
}
|
|
|
|
// MARK: - Migration
|
|
|
|
func testMigrationFromVersion0_2ToLatest() throws {
|
|
// GIVEN store created with the model from Pulse 0.2
|
|
let storeURL = directory.url
|
|
.appending(filename: UUID().uuidString)
|
|
try? FileManager.default.createDirectory(at: storeURL, withIntermediateDirectories: true, attributes: nil)
|
|
let databaseURL = storeURL
|
|
.appending(filename: "logs.sqlite")
|
|
try Resources.outdatedDatabase.write(to: databaseURL)
|
|
|
|
// WHEN migrating to the store with the latest model
|
|
let store = try LoggerStore(storeURL: storeURL, options: [.synchronous])
|
|
defer { try? store.destroy() }
|
|
|
|
// THEN automatic migration is performed and new field are populated with
|
|
// empty values
|
|
let messages = try store.allMessages()
|
|
XCTAssertEqual(messages.count, 0, "Previously recorded messages are going to be lost")
|
|
|
|
// WHEN
|
|
populate(store: store)
|
|
|
|
// THEN can write new messages
|
|
XCTAssertEqual(try store.allMessages().count, 10)
|
|
}
|
|
|
|
// MARK: - Remove All
|
|
|
|
func testRemoveAll() throws {
|
|
// GIVEN
|
|
populate(store: store)
|
|
store.storeMessage(label: "with meta", level: .debug, message: "test", metadata: ["hey": .string("this is meta yo")], file: #file, function: #function, line: #line)
|
|
|
|
let context = store.viewContext
|
|
XCTAssertEqual(try context.count(for: LoggerMessageEntity.self), 11)
|
|
XCTAssertEqual(try context.count(for: LoggerLabelEntity.self), 5)
|
|
XCTAssertEqual(try context.count(for: NetworkTaskEntity.self), 3)
|
|
XCTAssertEqual(try context.count(for: NetworkRequestEntity.self), 6)
|
|
XCTAssertEqual(try context.count(for: NetworkResponseEntity.self), 5)
|
|
XCTAssertEqual(try context.count(for: NetworkTransactionMetricsEntity.self), 3)
|
|
XCTAssertEqual(try context.count(for: LoggerBlobHandleEntity.self), 3)
|
|
|
|
// WHEN
|
|
store.removeAll()
|
|
|
|
// THEN both message and metadata are removed
|
|
XCTAssertEqual(try context.count(for: LoggerMessageEntity.self), 0)
|
|
XCTAssertEqual(try context.count(for: LoggerLabelEntity.self), 0)
|
|
XCTAssertEqual(try context.count(for: NetworkTaskEntity.self), 0)
|
|
XCTAssertEqual(try context.count(for: NetworkRequestEntity.self), 0)
|
|
XCTAssertEqual(try context.count(for: NetworkResponseEntity.self), 0)
|
|
XCTAssertEqual(try context.count(for: NetworkTransactionMetricsEntity.self), 0)
|
|
XCTAssertEqual(try context.count(for: LoggerBlobHandleEntity.self), 0)
|
|
}
|
|
|
|
func testRemoveAllWithLargeBlob() throws {
|
|
// GIVEN large blob
|
|
let store = makeStore {
|
|
$0.isBlobCompressionEnabled = false
|
|
}
|
|
defer { try? store.destroy() }
|
|
|
|
let responseData = Data(count: 256 * 1024)
|
|
store.storeRequest(URLRequest(url: URL(string: "example.com/1")!), response: nil, error: nil, data: responseData)
|
|
|
|
// ASSERT it's stored in a file system
|
|
XCTAssertEqual(try store.backgroundContext.count(for: LoggerBlobHandleEntity.self), 1)
|
|
let request = try store.backgroundContext.first(NetworkTaskEntity.self)
|
|
let key = try XCTUnwrap(request?.responseBody?.key)
|
|
XCTAssertEqual(store.getBlobData(forKey: key.hexString), responseData)
|
|
|
|
// WHEN
|
|
store.removeAll()
|
|
|
|
// THEN the file is deleted from the file system
|
|
XCTAssertEqual(try store.backgroundContext.count(for: LoggerBlobHandleEntity.self), 0)
|
|
XCTAssertNil(store.getBlobData(forKey: key.hexString))
|
|
|
|
// WHEN store new files after removal
|
|
store.storeRequest(URLRequest(url: URL(string: "example.com/1")!), response: nil, error: nil, data: responseData)
|
|
|
|
// THEN you can store more files after removal
|
|
XCTAssertEqual(store.getBlobData(forKey: key.hexString), responseData)
|
|
}
|
|
|
|
// MARK: - Store Request
|
|
|
|
func testStoreRequest() throws {
|
|
// WHEN
|
|
populate(store: store)
|
|
|
|
// THEN
|
|
let request = try XCTUnwrap(store.viewContext.first(NetworkTaskEntity.self) {
|
|
$0.predicate = NSPredicate(format: "url == %@", "https://github.com/login")
|
|
})
|
|
|
|
XCTAssertEqual(request.url, "https://github.com/login")
|
|
XCTAssertEqual(request.host?.value, "github.com")
|
|
XCTAssertEqual(request.httpMethod, "GET")
|
|
XCTAssertEqual(request.type, .dataTask)
|
|
XCTAssertEqual(request.statusCode, 200)
|
|
XCTAssertEqual(request.state, .success)
|
|
XCTAssertEqual(request.isFromCache, false)
|
|
XCTAssertEqual(request.redirectCount, 0)
|
|
|
|
// Details
|
|
XCTAssertEqual(request.originalRequest?.url, "https://github.com/login")
|
|
}
|
|
|
|
// MARK: - Image Support
|
|
|
|
#if os(iOS)
|
|
func testImageThumbnailsAreStored() throws {
|
|
// GIVEN
|
|
let image = try makeMockImage()
|
|
XCTAssertEqual(image.size, CGSize(width: 2048, height: 2048))
|
|
let imageData = try XCTUnwrap(image.pngData())
|
|
|
|
// WHEN
|
|
let url = try XCTUnwrap(URL(string: "https://example.com/image"))
|
|
|
|
let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: "http/2.0", headerFields: [
|
|
"Content-Type": "image/png"
|
|
])
|
|
|
|
store.storeRequest(URLRequest(url: url), response: response, error: nil, data: imageData)
|
|
|
|
// THEN
|
|
let request = try XCTUnwrap(try store.backgroundContext.first(NetworkTaskEntity.self))
|
|
let processedData = try XCTUnwrap(request.responseBody?.data)
|
|
let thumbnail = try XCTUnwrap(UIImage(data: processedData))
|
|
XCTAssertEqual(thumbnail.size, CGSize(width: 512, height: 512))
|
|
|
|
// THEN original image size saved
|
|
let metadata = try XCTUnwrap(request.metadata)
|
|
XCTAssertEqual(metadata["ResponsePixelWidth"].flatMap { Int($0) }, 2048)
|
|
XCTAssertEqual(metadata["ResponsePixelHeight"].flatMap { Int($0) }, 2048)
|
|
}
|
|
|
|
private func makeMockImage() throws -> UIImage {
|
|
UIGraphicsBeginImageContextWithOptions(CGSize(width: 2048, height: 2048), true, 1)
|
|
let context = try XCTUnwrap(UIGraphicsGetCurrentContext())
|
|
|
|
UIColor.red.setFill()
|
|
context.fill(CGRect(x: 0, y: 0, width: 2048, height: 2048))
|
|
|
|
let image = UIGraphicsGetImageFromCurrentImageContext()
|
|
UIGraphicsEndImageContext()
|
|
return try XCTUnwrap(image)
|
|
}
|
|
#endif
|
|
|
|
// MARK: - Info
|
|
|
|
func testGetStoreInfo() throws {
|
|
// GIVEN
|
|
populate(store: store)
|
|
|
|
// WHEN
|
|
let info = try store.info()
|
|
|
|
// THEN
|
|
XCTAssertEqual(info.storeVersion, "2.0.3")
|
|
XCTAssertEqual(info.messageCount, 7)
|
|
XCTAssertEqual(info.taskCount, 3)
|
|
XCTAssertEqual(info.blobCount, 3)
|
|
}
|
|
|
|
// MARK: - Helpers
|
|
|
|
private func makeStore(options: LoggerStore.Options = [.create, .synchronous], _ closure: (inout LoggerStore.Configuration) -> Void = { _ in }) -> LoggerStore {
|
|
var configuration = LoggerStore.Configuration()
|
|
configuration.makeCurrentDate = { [unowned self] in self.date }
|
|
closure(&configuration)
|
|
|
|
return try! LoggerStore(
|
|
storeURL: directory.url.appending(filename: UUID().uuidString),
|
|
options: options,
|
|
configuration: configuration
|
|
)
|
|
}
|
|
|
|
private func makePulsePackage() throws -> URL {
|
|
let storeURL = directory.url.appending(filename: UUID().uuidString)
|
|
let store = try LoggerStore(storeURL: storeURL, options: [.create, .synchronous])
|
|
populate(store: store)
|
|
try? store.close()
|
|
return storeURL
|
|
}
|
|
|
|
// MARK: - Measure Export Speed & Size
|
|
|
|
func testMeasureExportSize() throws {
|
|
// GIVEN
|
|
let storeURL = try makePulsePackage()
|
|
|
|
// WHEN
|
|
let store = try XCTUnwrap(LoggerStore(storeURL: storeURL))
|
|
defer { try? store.destroy() }
|
|
|
|
let copyURL = directory.url.appending(filename: "compressed.pulse")
|
|
try benchmark(title: "Archive") {
|
|
try store.copy(to: copyURL)
|
|
}
|
|
|
|
let size = (try Files.attributesOfItem(atPath: copyURL.path)[.size] as? Int64) ?? 0
|
|
print("Package: \(try storeURL.directoryTotalSize()). Archive: \(size)")
|
|
}
|
|
|
|
func _testMeasureExportSizeLarge() throws {
|
|
// GIVEN
|
|
let store = makeStore {
|
|
// Thumbnail generation significantly impacts the right speed
|
|
$0.isStoringOnlyImageThumbnails = false
|
|
}
|
|
defer { try? store.destroy() }
|
|
|
|
benchmark(title: "populate") {
|
|
for _ in 0..<1000 {
|
|
populate(store: store)
|
|
}
|
|
}
|
|
|
|
let copyURL = directory.url.appending(filename: "compressed.pulse")
|
|
try benchmark(title: "archive") {
|
|
try store.copy(to: copyURL)
|
|
}
|
|
|
|
let size = (try Files.attributesOfItem(atPath: copyURL.path)[.size] as? Int64) ?? 0
|
|
let compressed = try Data(contentsOf: copyURL).compressed()
|
|
print("Package: \(try store.storeURL.directoryTotalSize())\nArchive: \(size) (\(compressed) compressed)")
|
|
|
|
let info = try benchmark(title: "Open") {
|
|
try LoggerStore(storeURL: copyURL)
|
|
}
|
|
XCTAssertEqual(try info.viewContext.count(for: NetworkRequestEntity.self), 6000)
|
|
XCTAssertEqual(try info.viewContext.count(for: NetworkResponseEntity.self), 5000)
|
|
XCTAssertEqual(try info.viewContext.count(for: LoggerMessageEntity.self), 10000)
|
|
}
|
|
}
|