diff --git a/Logger.xcodeproj/project.pbxproj b/Logger.xcodeproj/project.pbxproj index 7355162..0b36d05 100644 --- a/Logger.xcodeproj/project.pbxproj +++ b/Logger.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 2E58D35D21316C3500BEF81A /* LogStringConvertibleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E58D35C21316C3500BEF81A /* LogStringConvertibleTests.swift */; }; + 2E58D35F21316C3500BEF81A /* libLogger.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2EBF4B3A2122AA34008E4117 /* libLogger.a */; }; 2EBF4B3E2122AA34008E4117 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EBF4B3D2122AA34008E4117 /* Logger.swift */; }; 2EBF4B452122ACD6008E4117 /* LogStringConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EBF4B442122ACD6008E4117 /* LogStringConvertible.swift */; }; 2EBF4B4A2122AF53008E4117 /* AgregateLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EBF4B472122AF53008E4117 /* AgregateLogger.swift */; }; @@ -15,9 +17,25 @@ 2EBF4B512122B06E008E4117 /* NSFileHandle+Swift.m in Sources */ = {isa = PBXBuildFile; fileRef = 2EBF4B502122B06E008E4117 /* NSFileHandle+Swift.m */; }; 2EBF4B572122B598008E4117 /* DiskLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EBF4B542122B598008E4117 /* DiskLogger.swift */; }; 2EBF4B582122B598008E4117 /* FileWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EBF4B552122B598008E4117 /* FileWriter.swift */; }; - 2EBF4B592122B598008E4117 /* Logrotate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EBF4B562122B598008E4117 /* Logrotate.swift */; }; + 2EBF4B592122B598008E4117 /* FileRotate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EBF4B562122B598008E4117 /* FileRotate.swift */; }; + 2ED077D721329CA30058EEFC /* LoggetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2ED077D621329CA30058EEFC /* LoggetTests.swift */; }; + 2ED077D92132A4820058EEFC /* AgregateLoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2ED077D82132A4820058EEFC /* AgregateLoggerTests.swift */; }; + 2ED077DB2132B0320058EEFC /* FileSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2ED077DA2132B0320058EEFC /* FileSystem.swift */; }; + 2ED103E12135C61100EB3683 /* FileRotateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2ED103E02135C61100EB3683 /* FileRotateTests.swift */; }; + 2ED103E32135D3FB00EB3683 /* FileWriterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2ED103E22135D3FB00EB3683 /* FileWriterTests.swift */; }; + 2ED103E52138553B00EB3683 /* DiskLoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2ED103E42138553B00EB3683 /* DiskLoggerTests.swift */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 2E58D36021316C3500BEF81A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 2EBF4B322122AA34008E4117 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2EBF4B392122AA34008E4117; + remoteInfo = Logger; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXCopyFilesBuildPhase section */ 2EBF4B382122AA34008E4117 /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; @@ -31,6 +49,9 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 2E58D35A21316C3500BEF81A /* UnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 2E58D35C21316C3500BEF81A /* LogStringConvertibleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogStringConvertibleTests.swift; sourceTree = ""; }; + 2E58D35E21316C3500BEF81A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 2EBF4B3A2122AA34008E4117 /* libLogger.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libLogger.a; sourceTree = BUILT_PRODUCTS_DIR; }; 2EBF4B3D2122AA34008E4117 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; 2EBF4B442122ACD6008E4117 /* LogStringConvertible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogStringConvertible.swift; sourceTree = ""; }; @@ -42,10 +63,24 @@ 2EBF4B532122B2AA008E4117 /* Logger-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Logger-Bridging-Header.h"; sourceTree = ""; }; 2EBF4B542122B598008E4117 /* DiskLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiskLogger.swift; sourceTree = ""; }; 2EBF4B552122B598008E4117 /* FileWriter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileWriter.swift; sourceTree = ""; }; - 2EBF4B562122B598008E4117 /* Logrotate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logrotate.swift; sourceTree = ""; }; + 2EBF4B562122B598008E4117 /* FileRotate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileRotate.swift; sourceTree = ""; }; + 2ED077D621329CA30058EEFC /* LoggetTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggetTests.swift; sourceTree = ""; }; + 2ED077D82132A4820058EEFC /* AgregateLoggerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgregateLoggerTests.swift; sourceTree = ""; }; + 2ED077DA2132B0320058EEFC /* FileSystem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileSystem.swift; sourceTree = ""; }; + 2ED103E02135C61100EB3683 /* FileRotateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileRotateTests.swift; sourceTree = ""; }; + 2ED103E22135D3FB00EB3683 /* FileWriterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileWriterTests.swift; sourceTree = ""; }; + 2ED103E42138553B00EB3683 /* DiskLoggerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiskLoggerTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 2E58D35721316C3500BEF81A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2E58D35F21316C3500BEF81A /* libLogger.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 2EBF4B372122AA34008E4117 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -56,10 +91,25 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 2E58D35B21316C3500BEF81A /* UnitTests */ = { + isa = PBXGroup; + children = ( + 2E58D35C21316C3500BEF81A /* LogStringConvertibleTests.swift */, + 2ED077D621329CA30058EEFC /* LoggetTests.swift */, + 2ED077D82132A4820058EEFC /* AgregateLoggerTests.swift */, + 2ED103E42138553B00EB3683 /* DiskLoggerTests.swift */, + 2ED103E22135D3FB00EB3683 /* FileWriterTests.swift */, + 2ED103E02135C61100EB3683 /* FileRotateTests.swift */, + 2E58D35E21316C3500BEF81A /* Info.plist */, + ); + path = UnitTests; + sourceTree = ""; + }; 2EBF4B312122AA34008E4117 = { isa = PBXGroup; children = ( 2EBF4B3C2122AA34008E4117 /* Logger */, + 2E58D35B21316C3500BEF81A /* UnitTests */, 2EBF4B3B2122AA34008E4117 /* Products */, ); sourceTree = ""; @@ -68,6 +118,7 @@ isa = PBXGroup; children = ( 2EBF4B3A2122AA34008E4117 /* libLogger.a */, + 2E58D35A21316C3500BEF81A /* UnitTests.xctest */, ); name = Products; sourceTree = ""; @@ -99,9 +150,10 @@ children = ( 2EBF4B542122B598008E4117 /* DiskLogger.swift */, 2EBF4B552122B598008E4117 /* FileWriter.swift */, - 2EBF4B562122B598008E4117 /* Logrotate.swift */, + 2EBF4B562122B598008E4117 /* FileRotate.swift */, 2EBF4B4F2122B06E008E4117 /* NSFileHandle+Swift.h */, 2EBF4B502122B06E008E4117 /* NSFileHandle+Swift.m */, + 2ED077DA2132B0320058EEFC /* FileSystem.swift */, ); path = DiskLogger; sourceTree = ""; @@ -109,6 +161,24 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 2E58D35921316C3500BEF81A /* UnitTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2E58D36221316C3500BEF81A /* Build configuration list for PBXNativeTarget "UnitTests" */; + buildPhases = ( + 2E58D35621316C3500BEF81A /* Sources */, + 2E58D35721316C3500BEF81A /* Frameworks */, + 2E58D35821316C3500BEF81A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 2E58D36121316C3500BEF81A /* PBXTargetDependency */, + ); + name = UnitTests; + productName = UnitTests; + productReference = 2E58D35A21316C3500BEF81A /* UnitTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 2EBF4B392122AA34008E4117 /* Logger */ = { isa = PBXNativeTarget; buildConfigurationList = 2EBF4B412122AA34008E4117 /* Build configuration list for PBXNativeTarget "Logger" */; @@ -136,6 +206,9 @@ LastUpgradeCheck = 1000; ORGANIZATIONNAME = "Wojciech Nagrodzki"; TargetAttributes = { + 2E58D35921316C3500BEF81A = { + CreatedOnToolsVersion = 10.0; + }; 2EBF4B392122AA34008E4117 = { CreatedOnToolsVersion = 10.0; LastSwiftMigration = 1000; @@ -155,11 +228,35 @@ projectRoot = ""; targets = ( 2EBF4B392122AA34008E4117 /* Logger */, + 2E58D35921316C3500BEF81A /* UnitTests */, ); }; /* End PBXProject section */ +/* Begin PBXResourcesBuildPhase section */ + 2E58D35821316C3500BEF81A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ + 2E58D35621316C3500BEF81A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2E58D35D21316C3500BEF81A /* LogStringConvertibleTests.swift in Sources */, + 2ED103E32135D3FB00EB3683 /* FileWriterTests.swift in Sources */, + 2ED103E52138553B00EB3683 /* DiskLoggerTests.swift in Sources */, + 2ED077D92132A4820058EEFC /* AgregateLoggerTests.swift in Sources */, + 2ED103E12135C61100EB3683 /* FileRotateTests.swift in Sources */, + 2ED077D721329CA30058EEFC /* LoggetTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 2EBF4B362122AA34008E4117 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -170,15 +267,58 @@ 2EBF4B3E2122AA34008E4117 /* Logger.swift in Sources */, 2EBF4B512122B06E008E4117 /* NSFileHandle+Swift.m in Sources */, 2EBF4B4B2122AF53008E4117 /* ConsoleLogger.swift in Sources */, + 2ED077DB2132B0320058EEFC /* FileSystem.swift in Sources */, 2EBF4B4A2122AF53008E4117 /* AgregateLogger.swift in Sources */, - 2EBF4B592122B598008E4117 /* Logrotate.swift in Sources */, + 2EBF4B592122B598008E4117 /* FileRotate.swift in Sources */, 2EBF4B452122ACD6008E4117 /* LogStringConvertible.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 2E58D36121316C3500BEF81A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2EBF4B392122AA34008E4117 /* Logger */; + targetProxy = 2E58D36021316C3500BEF81A /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin XCBuildConfiguration section */ + 2E58D36321316C3500BEF81A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = UnitTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.wnagrodzki.UnitTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 2E58D36421316C3500BEF81A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = UnitTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.wnagrodzki.UnitTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; 2EBF4B3F2122AA34008E4117 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -337,6 +477,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 2E58D36221316C3500BEF81A /* Build configuration list for PBXNativeTarget "UnitTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2E58D36321316C3500BEF81A /* Debug */, + 2E58D36421316C3500BEF81A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 2EBF4B352122AA34008E4117 /* Build configuration list for PBXProject "Logger" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Logger.xcodeproj/xcshareddata/xcschemes/Logger.xcscheme b/Logger.xcodeproj/xcshareddata/xcschemes/Logger.xcscheme new file mode 100644 index 0000000..1b5258f --- /dev/null +++ b/Logger.xcodeproj/xcshareddata/xcschemes/Logger.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Logger/Loggers/DiskLogger/DiskLogger.swift b/Logger/Loggers/DiskLogger/DiskLogger.swift index d4cb80f..c2a44d0 100644 --- a/Logger/Loggers/DiskLogger/DiskLogger.swift +++ b/Logger/Loggers/DiskLogger/DiskLogger.swift @@ -24,16 +24,71 @@ import Foundation +/// Write failed as allowed size limit would be exceeded. +public struct SizeLimitedFileQuotaReached: Error {} + +/// Allows writing to a file while respecting allowed size limit. +protocol SizeLimitedFile { + + /// Synchronously writes `data` at the end of the file. + /// + /// - Parameter data: The data to be written. + /// - Throws: Throws an error if no free space is left on the file system, or if any other writing error occurs. + /// Throws `SizeLimitedFileQuotaReached` if allowed size limit would be exceeded. + func write(_ data: Data) throws + + /// Writes all in-memory data to permanent storage and closes the file. + func synchronizeAndCloseFile() +} + +protocol SizeLimitedFileFactory { + + /// Returns newly initialized SizeLimitedFile instance. + /// + /// - Parameters: + /// - fileURL: URL of the file. + /// - fileSizeLimit: Maximum size the file can reach in bytes. + /// - Throws: An error that may occur while the file is being opened for writing. + func makeInstance(fileURL: URL, fileSizeLimit: UInt64) throws -> SizeLimitedFile +} + +/// Allows log files rotation. +protocol Logrotate { + + /// Rotates log files `rotations` number of times. + /// + /// First deletes file at `.`. + /// Next moves files located at: + /// + /// `, .1, .2 ... .` + /// + /// to `.1, .2 ... .` + func rotate() throws +} + +protocol LogrotateFactory { + + /// Returns newly initialized Logrotate instance. + /// + /// - Parameters: + /// - fileURL: URL of the log file. + /// - rotations: Number of times log files are rotated before being removed. + func makeInstance(fileURL: URL, rotations: Int) -> Logrotate +} + /// Logger that writes messages into the file at specified URL with log rotation support. public final class DiskLogger: Logger { private let fileURL: URL private let fileSizeLimit: UInt64 private let rotations: Int + private let fileSystem: FileSystem + private let sizeLimitedFileFactory: SizeLimitedFileFactory + private let logrotateFactory: LogrotateFactory private let formatter: DateFormatter private let queue = DispatchQueue(label: "com.wnagrodzki.DiskLogger", qos: .background, attributes: [], autoreleaseFrequency: .workItem, target: nil) private var buffer = Data() - private var fileWriter: FileWriter! + private var sizeLimitedFile: SizeLimitedFile! /// Initializes new DiskLogger instance. /// @@ -41,10 +96,17 @@ public final class DiskLogger: Logger { /// - fileURL: URL of the log file. /// - fileSizeLimit: Maximum size log file can reach in bytes. Attempt to exceeding that limit triggers log files rotation. /// - rotations: Number of times log files are rotated before being removed. - public init(fileURL: URL, fileSizeLimit: UInt64, rotations: Int) { + public convenience init(fileURL: URL, fileSizeLimit: UInt64, rotations: Int) { + self.init(fileURL: fileURL, fileSizeLimit: fileSizeLimit, rotations: rotations, fileSystem: FileManager.default, sizeLimitedFileFactory: FileWriterFactory(), logrotateFactory: FileRotateFactory()) + } + + init(fileURL: URL, fileSizeLimit: UInt64, rotations: Int, fileSystem: FileSystem, sizeLimitedFileFactory: SizeLimitedFileFactory, logrotateFactory: LogrotateFactory) { self.fileURL = fileURL self.fileSizeLimit = fileSizeLimit self.rotations = rotations + self.fileSystem = fileSystem + self.sizeLimitedFileFactory = sizeLimitedFileFactory + self.logrotateFactory = logrotateFactory formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS" formatter.timeZone = TimeZone(secondsFromGMT: 0) @@ -61,43 +123,67 @@ public final class DiskLogger: Logger { self.buffer.append(data) do { - try self.openFileWriter() + try self.openSizeLimitedFile() do { try self.writeBuffer() } - catch is FileWriter.FileSizeLimitReached { - self.closeFileWriter() + catch is SizeLimitedFileQuotaReached { + self.closeSizeLimitedFile() try self.rotateLogFiles() + try self.openSizeLimitedFile() + try self.writeBuffer() } } catch { - let message = self.formatter.string(from: Date()) + " <" + LogLevel.warning.logDescription + "> " + String(describing: error) + let message = self.formatter.string(from: Date()) + " <" + LogLevel.warning.logDescription + "> " + String(describing: error) + "\n" let data = Data(message.utf8) self.buffer.append(data) } } } - private func openFileWriter() throws { - guard fileWriter == nil else { return } - if FileManager.default.fileExists(atPath: fileURL.path) == false { - FileManager.default.createFile(atPath: fileURL.path, contents: nil, attributes: nil) + private func openSizeLimitedFile() throws { + guard sizeLimitedFile == nil else { return } + if fileSystem.itemExists(at: fileURL) == false { + _ = fileSystem.createFile(at: fileURL) } - fileWriter = try FileWriter(fileURL: fileURL, fileSizeLimit: fileSizeLimit) + sizeLimitedFile = try sizeLimitedFileFactory.makeInstance(fileURL: fileURL, fileSizeLimit: fileSizeLimit) } private func writeBuffer() throws { - try fileWriter.write(buffer) + try sizeLimitedFile.write(buffer) buffer.removeAll() } - private func closeFileWriter() { - self.fileWriter.synchronizeAndCloseFile() - self.fileWriter = nil + private func closeSizeLimitedFile() { + self.sizeLimitedFile.synchronizeAndCloseFile() + self.sizeLimitedFile = nil } private func rotateLogFiles() throws { - let logrotate = Logrotate(fileURL: fileURL, rotations: rotations) + let logrotate = logrotateFactory.makeInstance(fileURL: fileURL, rotations: rotations) try logrotate.rotate() } } + +private class FileRotateFactory: LogrotateFactory { + func makeInstance(fileURL: URL, rotations: Int) -> Logrotate { + return FileRotate(fileURL: fileURL, rotations: rotations, fileSystem: FileManager.default) + } +} + +private class FileWriterFactory: SizeLimitedFileFactory { + func makeInstance(fileURL: URL, fileSizeLimit: UInt64) throws -> SizeLimitedFile { + return try FileWriter(fileURL: fileURL, fileSizeLimit: fileSizeLimit, fileFactory: FileHandleFactory()) + } +} + +private class FileHandleFactory: FileFactory { + func makeInstance(forWritingTo: URL) throws -> File { + return try FileHandle(forWritingTo: forWritingTo) + } +} + +extension FileHandle: File { + +} diff --git a/Logger/Loggers/DiskLogger/FileSystem.swift b/Logger/Loggers/DiskLogger/FileSystem.swift new file mode 100644 index 0000000..b47b316 --- /dev/null +++ b/Logger/Loggers/DiskLogger/FileSystem.swift @@ -0,0 +1,43 @@ +// +// MIT License +// +// Copyright (c) 2018 Wojciech Nagrodzki +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +public protocol FileSystem { + func itemExists(at URL: URL) -> Bool + func removeItem(at URL: URL) throws + func moveItem(at srcURL: URL, to dstURL: URL) throws + func createFile(at URL: URL) -> Bool +} + +extension FileManager: FileSystem { + + public func itemExists(at URL: URL) -> Bool { + return fileExists(atPath: URL.path) + } + + public func createFile(at URL: URL) -> Bool { + return createFile(atPath: URL.path, contents: nil, attributes: nil) + } +} diff --git a/Logger/Loggers/DiskLogger/FileWriter.swift b/Logger/Loggers/DiskLogger/FileWriter.swift index 1083812..be1ff30 100644 --- a/Logger/Loggers/DiskLogger/FileWriter.swift +++ b/Logger/Loggers/DiskLogger/FileWriter.swift @@ -24,13 +24,21 @@ import Foundation +protocol File { + func seekToEndOfFile() -> UInt64 + func swift_write(_ data: Data) throws + func synchronizeFile() + func closeFile() +} + +protocol FileFactory { + func makeInstance(forWritingTo: URL) throws -> File +} + /// Allows writing to a file while respecting allowed size limit. final class FileWriter { - /// Write failed as allowed size limit would be exceeded for the file. - struct FileSizeLimitReached: Error {} - - private let handle: FileHandle + private let file: File private let sizeLimit: UInt64 private var currentSize: UInt64 @@ -40,29 +48,26 @@ final class FileWriter { /// - fileURL: URL of the file. /// - fileSizeLimit: Maximum size the file can reach in bytes. /// - Throws: An error that may occur while the file is being opened for writing. - init(fileURL: URL, fileSizeLimit: UInt64) throws { - handle = try FileHandle(forWritingTo: fileURL) + init(fileURL: URL, fileSizeLimit: UInt64, fileFactory: FileFactory) throws { + file = try fileFactory.makeInstance(forWritingTo: fileURL) self.sizeLimit = fileSizeLimit - currentSize = handle.seekToEndOfFile() + currentSize = file.seekToEndOfFile() } +} + +extension FileWriter: SizeLimitedFile { - /// Synchronously writes `data` at the end of the file. - /// - /// - Parameter data: The data to be written. - /// - Throws: Throws an error if no free space is left on the file system, or if any other writing error occurs. - /// Throws `FileSizeLimitReached` if allowed size limit would be exceeded for the file. func write(_ data: Data) throws { let dataSize = UInt64(data.count) guard currentSize + dataSize <= sizeLimit else { - throw FileSizeLimitReached() + throw SizeLimitedFileQuotaReached() } - try handle.swift_write(data) + try file.swift_write(data) currentSize += dataSize } - /// Writes all in-memory data to permanent storage and closes the file. func synchronizeAndCloseFile() { - handle.synchronizeFile() - handle.closeFile() + file.synchronizeFile() + file.closeFile() } } diff --git a/Logger/Loggers/DiskLogger/Logrotate.swift b/Logger/Loggers/DiskLogger/Filerotate.swift similarity index 74% rename from Logger/Loggers/DiskLogger/Logrotate.swift rename to Logger/Loggers/DiskLogger/Filerotate.swift index c377d2e..0963cfa 100644 --- a/Logger/Loggers/DiskLogger/Logrotate.swift +++ b/Logger/Loggers/DiskLogger/Filerotate.swift @@ -24,31 +24,27 @@ import Foundation -/// Allows log files rotation. -final class Logrotate { +final class FileRotate { private let fileURL: URL private let rotations: Int + private let fileSystem: FileSystem /// Initializes new Logrotate instance. /// /// - Parameters: /// - fileURL: URL of the log file. /// - rotations: Number of times log files are rotated before being removed. - init(fileURL: URL, rotations: Int) { + init(fileURL: URL, rotations: Int, fileSystem: FileSystem) { precondition(rotations > 0) self.fileURL = fileURL self.rotations = rotations + self.fileSystem = fileSystem } +} + +extension FileRotate: Logrotate { - /// Rotates log files `rotations` number of times. - /// - /// First deletes file at `.`. - /// Next moves files located at: - /// - /// `, .1, .2 ... .` - /// - /// to `.1, .2 ... .` func rotate() throws { let range = 1...rotations let pathExtensions = range.map { "\($0)" } @@ -58,12 +54,12 @@ final class Logrotate { let toDelete = rotatedURLs.last! let toMove = zip(allURLs, rotatedURLs).reversed() - if FileManager.default.fileExists(atPath: toDelete.path) { - try FileManager.default.removeItem(at: toDelete) + if fileSystem.itemExists(at: toDelete) { + try fileSystem.removeItem(at: toDelete) } for (oldURL, newURL) in toMove { - guard FileManager.default.fileExists(atPath: oldURL.path) else { continue } - try FileManager.default.moveItem(at: oldURL, to: newURL) + guard fileSystem.itemExists(at: oldURL) else { continue } + try fileSystem.moveItem(at: oldURL, to: newURL) } } } diff --git a/UnitTests/AgregateLoggerTests.swift b/UnitTests/AgregateLoggerTests.swift new file mode 100644 index 0000000..c3c2704 --- /dev/null +++ b/UnitTests/AgregateLoggerTests.swift @@ -0,0 +1,68 @@ +// +// MIT License +// +// Copyright (c) 2018 Wojciech Nagrodzki +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import XCTest +@testable import Logger + +class AgregateLoggerTests: XCTestCase { + + func testCallForwarding() { + let loggerA = LoggerMock() + let loggerB = LoggerMock() + let agregateLogger = AgregateLogger(loggers: [loggerA, loggerB]) + agregateLogger.log("0", level: .emergency) + agregateLogger.log("1", level: .alert) + agregateLogger.log("2", level: .critical) + agregateLogger.log("3", level: .error) + agregateLogger.log("4", level: .warning) + agregateLogger.log("5", level: .notice) + agregateLogger.log("6", level: .informational) + agregateLogger.log("7", level: .debug) + XCTAssertEqual(loggerA, loggerB) + } +} + +private class LoggerMock: Logger { + + struct Log: Equatable { + let time: Date + let level: LogLevel + let location: String + let message: String + } + + private var logs = [Log]() + + func log(time: Date, level: LogLevel, location: String, message: @autoclosure () -> String) { + let log = Log(time: time, level: level, location: location, message: message()) + logs.append(log) + } +} + +extension LoggerMock: Equatable { + + static func == (lhs: LoggerMock, rhs: LoggerMock) -> Bool { + return lhs.logs == rhs.logs + } +} diff --git a/UnitTests/DiskLoggerTests.swift b/UnitTests/DiskLoggerTests.swift new file mode 100644 index 0000000..aaededc --- /dev/null +++ b/UnitTests/DiskLoggerTests.swift @@ -0,0 +1,241 @@ +// +// MIT License +// +// Copyright (c) 2018 Wojciech Nagrodzki +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import XCTest +@testable import Logger + +class DiskLoggerTests: XCTestCase { + + let logURL = URL(fileURLWithPath: "/var/log/application.log") + + func testLoggingMessageToFile() { + let expectation = XCTestExpectation(description: "write(_:) was called on SizeLimitedFile") + expectation.expectedFulfillmentCount = 1 + + let filesystem = FileSystemStub() + let sizeLimitedFileFactory = SizeLimitedFileMockFactory(writeCall: expectation) + let logrotateFactory = LogrotateMockFactory() + let logger = DiskLogger(fileURL: logURL, + fileSizeLimit: 1024, + rotations: 1, + fileSystem: filesystem, + sizeLimitedFileFactory: sizeLimitedFileFactory, + logrotateFactory: logrotateFactory) + + logger.log("message", level: .critical) + + let result = XCTWaiter().wait(for: [expectation], timeout: 1.0) + XCTAssertEqual(result, .completed) + + XCTAssertEqual(sizeLimitedFileFactory.files.count, 1) + + // "2018-08-30 17:23:11.514 DiskLoggerTests:46 testWritingMessageToFile() a message\n" + let loggedMessage = String(decoding: sizeLimitedFileFactory.files[0].data, as: UTF8.self) + let expectedSuffix = " DiskLoggerTests:46 testLoggingMessageToFile() message\n" + + XCTAssertTrue(loggedMessage.hasSuffix(expectedSuffix)) + } + + func testExceedingFileSizeLimit() { + let expectation = XCTestExpectation(description: "write(_:) was called on SizeLimitedFile") + expectation.expectedFulfillmentCount = 2 + + let filesystem = FileSystemStub() + let sizeLimitedFileFactory = SizeLimitedFileMockFactory(writeCall: expectation) + let logrotateFactory = LogrotateMockFactory() + let logger = DiskLogger(fileURL: logURL, + fileSizeLimit: 91, + rotations: 1, + fileSystem: filesystem, + sizeLimitedFileFactory: sizeLimitedFileFactory, + logrotateFactory: logrotateFactory) + + logger.log("1st message", level: .critical) + logger.log("2st message", level: .critical) + + let result = XCTWaiter().wait(for: [expectation], timeout: 1.0) + XCTAssertEqual(result, .completed) + + XCTAssertEqual(sizeLimitedFileFactory.files.count, 2) + + // "2018-08-31 18:29:34.748 DiskLoggerTests:75 testExceedingFileSizeLimit() 2st message\n" + let loggedMessage = String(decoding: sizeLimitedFileFactory.files[1].data, as: UTF8.self) + let expectedSuffix = " DiskLoggerTests:75 testExceedingFileSizeLimit() 2st message\n" + + XCTAssertTrue(loggedMessage.hasSuffix(expectedSuffix)) + } + + func testErrorDuringLogProcess() { + let expectation = XCTestExpectation(description: "write(_:) was called on SizeLimitedFile") + expectation.expectedFulfillmentCount = 2 + + let filesystem = FileSystemStub() + let sizeLimitedFileFactory = UnwritableFileStubFactory(writeCall: expectation) + let logrotateFactory = LogrotateMockFactory() + let logger = DiskLogger(fileURL: logURL, + fileSizeLimit: 91, + rotations: 1, + fileSystem: filesystem, + sizeLimitedFileFactory: sizeLimitedFileFactory, + logrotateFactory: logrotateFactory) + + logger.log("1st message", level: .critical) + logger.log("2st message", level: .critical) + + let result = XCTWaiter().wait(for: [expectation], timeout: 1.0) + XCTAssertEqual(result, .completed) + + XCTAssertEqual(sizeLimitedFileFactory.files.count, 1) + + // "2018-08-31 18:29:34.748 DiskLoggerTests:75 testExceedingFileSizeLimit() 2st message\n" + let loggedMessage = String(decoding: sizeLimitedFileFactory.files[0].data, as: UTF8.self) + let expectedOccurence = " WriteFailed()" + + XCTAssertTrue(loggedMessage.contains(expectedOccurence)) + } +} + +private class FileSystemStub: FileSystem { + func itemExists(at URL: URL) -> Bool { + return false + } + + func removeItem(at URL: URL) throws { + + } + + func moveItem(at srcURL: URL, to dstURL: URL) throws { + + } + + func createFile(at URL: URL) -> Bool { + return true + } +} + +private class SizeLimitedFileMockFactory: SizeLimitedFileFactory { + + private(set) var files = [SizeLimitedFileMock]() + private let writeCall: XCTestExpectation + + init(writeCall: XCTestExpectation) { + self.writeCall = writeCall + } + + func makeInstance(fileURL: URL, fileSizeLimit: UInt64) throws -> SizeLimitedFile { + let mock = SizeLimitedFileMock(writeCall: writeCall, fileSizeLimit: fileSizeLimit) + files.append(mock) + return mock + } +} + +private class SizeLimitedFileMock: SizeLimitedFile { + + private(set) var data = Data() + private(set) var synchronizeAndCloseFileCallCount = 0 + private let writeCall: XCTestExpectation + private let fileSizeLimit: UInt64 + + init(writeCall: XCTestExpectation, fileSizeLimit: UInt64) { + self.writeCall = writeCall + self.fileSizeLimit = fileSizeLimit + } + + func write(_ data: Data) throws { + guard data.count + self.data.count <= fileSizeLimit else { + throw SizeLimitedFileQuotaReached() + } + self.data.append(data) + writeCall.fulfill() + } + + func synchronizeAndCloseFile() { + synchronizeAndCloseFileCallCount += 1 + } +} + +private class UnwritableFileStubFactory: SizeLimitedFileFactory { + + private(set) var files = [UnwritableFileStub]() + private let writeCall: XCTestExpectation + + init(writeCall: XCTestExpectation) { + self.writeCall = writeCall + } + + func makeInstance(fileURL: URL, fileSizeLimit: UInt64) throws -> SizeLimitedFile { + let file = UnwritableFileStub(writeCall: writeCall, fileSizeLimit: fileSizeLimit) + files.append(file) + return file + } +} + +private class UnwritableFileStub: SizeLimitedFile { + + struct WriteFailed: Error {} + + private(set) var data = Data() + private let writeCall: XCTestExpectation + private var didThrowError = false + + init(writeCall: XCTestExpectation, fileSizeLimit: UInt64) { + self.writeCall = writeCall + } + + func write(_ data: Data) throws { + if didThrowError { + self.data.append(data) + writeCall.fulfill() + } + else { + didThrowError = true + writeCall.fulfill() + throw WriteFailed() + } + } + + func synchronizeAndCloseFile() { + + } +} + +private class LogrotateMockFactory: LogrotateFactory { + + private(set) var logrotates = [LogrotateMock]() + + func makeInstance(fileURL: URL, rotations: Int) -> Logrotate { + let mock = LogrotateMock() + logrotates.append(mock) + return mock + } +} + +private class LogrotateMock: Logrotate { + + private(set) var rotateCallCount = 0 + + func rotate() throws { + rotateCallCount += 1 + } +} diff --git a/UnitTests/FileRotateTests.swift b/UnitTests/FileRotateTests.swift new file mode 100644 index 0000000..13ca7c0 --- /dev/null +++ b/UnitTests/FileRotateTests.swift @@ -0,0 +1,181 @@ +// +// MIT License +// +// Copyright (c) 2018 Wojciech Nagrodzki +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import XCTest +@testable import Logger + +class FileRotateTests: XCTestCase { + + let logURL = URL(fileURLWithPath: "/var/log/application.log") + let log1URL = URL(fileURLWithPath: "/var/log/application.log.1") + let log2URL = URL(fileURLWithPath: "/var/log/application.log.2") + let log3URL = URL(fileURLWithPath: "/var/log/application.log.3") + + func test_1rotation_0files() { + let fileSystem = FileSystemMock(files: []) + let logrotate = FileRotate(fileURL: logURL, rotations: 1, fileSystem: fileSystem) + try? logrotate.rotate() + + let actual = fileSystem.files + let expected = Set() + XCTAssertEqual(actual, expected) + } + + func test_1rotation_1file() { + let fileSystem = FileSystemMock(files: [logURL]) + let logrotate = FileRotate(fileURL: logURL, rotations: 1, fileSystem: fileSystem) + try? logrotate.rotate() + + let actual = fileSystem.files + let expected = Set([log1URL]) + XCTAssertEqual(actual, expected) + } + + func test_1rotation_2files() { + let fileSystem = FileSystemMock(files: [logURL, log1URL]) + let logrotate = FileRotate(fileURL: logURL, rotations: 1, fileSystem: fileSystem) + try? logrotate.rotate() + + let actual = fileSystem.files + let expected = Set([log1URL]) + XCTAssertEqual(actual, expected) + } + + func test_1rotation_3files() { + let fileSystem = FileSystemMock(files: [logURL, log1URL, log2URL]) + let logrotate = FileRotate(fileURL: logURL, rotations: 1, fileSystem: fileSystem) + try? logrotate.rotate() + + let actual = fileSystem.files + let expected = Set([log1URL, log2URL]) + XCTAssertEqual(actual, expected) + } + + func test_2rotations_0files() { + let fileSystem = FileSystemMock(files: []) + let logrotate = FileRotate(fileURL: logURL, rotations: 2, fileSystem: fileSystem) + try? logrotate.rotate() + + let actual = fileSystem.files + let expected = Set() + XCTAssertEqual(actual, expected) + } + + func test_2rotations_1file() { + let fileSystem = FileSystemMock(files: [logURL]) + let logrotate = FileRotate(fileURL: logURL, rotations: 2, fileSystem: fileSystem) + try? logrotate.rotate() + + let actual = fileSystem.files + let expected = Set([log1URL]) + XCTAssertEqual(actual, expected) + } + + func test_2rotations_2files() { + let fileSystem = FileSystemMock(files: [logURL, log1URL]) + let logrotate = FileRotate(fileURL: logURL, rotations: 2, fileSystem: fileSystem) + try? logrotate.rotate() + + let actual = fileSystem.files + let expected = Set([log1URL, log2URL]) + XCTAssertEqual(actual, expected) + } + + func test_2rotations_3files() { + let fileSystem = FileSystemMock(files: [logURL, log1URL, log2URL]) + let logrotate = FileRotate(fileURL: logURL, rotations: 2, fileSystem: fileSystem) + try? logrotate.rotate() + + let actual = fileSystem.files + let expected = Set([log1URL, log2URL]) + XCTAssertEqual(actual, expected) + } + + func test_2rotations_4files() { + let fileSystem = FileSystemMock(files: [logURL, log1URL, log2URL, log3URL]) + let logrotate = FileRotate(fileURL: logURL, rotations: 2, fileSystem: fileSystem) + try? logrotate.rotate() + + let actual = fileSystem.files + let expected = Set([log1URL, log2URL, log3URL]) + XCTAssertEqual(actual, expected) + } + + func testErrorPropagation() { + let fileSystem = BrokenFileSystem() + let logrotate = FileRotate(fileURL: logURL, rotations: 1, fileSystem: fileSystem) + + XCTAssertThrowsError(try logrotate.rotate(), "An error when removing or moving an item") { (error) in + XCTAssertTrue(error is BrokenFileSystem.IOError) + } + } +} + +private class FileSystemMock: FileSystem { + + private(set) var files = Set() + + init(files: Set) { + self.files = files + } + + func itemExists(at URL: URL) -> Bool { + return files.contains(URL) + } + + func removeItem(at URL: URL) throws { + files.remove(URL) + } + + func moveItem(at srcURL: URL, to dstURL: URL) throws { + files.remove(srcURL) + files.insert(dstURL) + } + + func createFile(at URL: URL) -> Bool { + files.insert(URL) + return true + } +} + +private class BrokenFileSystem: FileSystem { + + struct IOError: Error { } + + func itemExists(at URL: URL) -> Bool { + return true + } + + func removeItem(at URL: URL) throws { + throw IOError() + } + + func moveItem(at srcURL: URL, to dstURL: URL) throws { + throw IOError() + } + + func createFile(at URL: URL) -> Bool { + return false + } +} diff --git a/UnitTests/FileWriterTests.swift b/UnitTests/FileWriterTests.swift new file mode 100644 index 0000000..cb3ebc7 --- /dev/null +++ b/UnitTests/FileWriterTests.swift @@ -0,0 +1,107 @@ +// +// MIT License +// +// Copyright (c) 2018 Wojciech Nagrodzki +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import XCTest +@testable import Logger + +class FileWriterTests: XCTestCase { + + let logURL = URL(fileURLWithPath: "/var/log/application.log") + + func testFileOpeningFailure() { + let factory = UnopenableFileFactory() + XCTAssertThrowsError(try FileWriter(fileURL: logURL, fileSizeLimit: 0, fileFactory: factory), "file open failure") { (error) in + XCTAssertTrue(error is UnopenableFileFactory.OpenFileError) + } + } + + func testKeepingFileSizeLimit() throws { + let factory = FileMockFactory() + let writer = try FileWriter(fileURL: logURL, fileSizeLimit: 1, fileFactory: factory) + let data = Data(bytes: [0]) + try writer.write(data) + + XCTAssertEqual(factory.mock.writtenData, data) + } + + func testExceedingFileSizeLimit() throws { + let factory = FileMockFactory() + let writer = try FileWriter(fileURL: logURL, fileSizeLimit: 1, fileFactory: factory) + let data = Data(bytes: [0, 0]) + + XCTAssertThrowsError(try writer.write(data), "file size limit exceeded") { (error) in + XCTAssertTrue(error is SizeLimitedFileQuotaReached) + } + + XCTAssertEqual(factory.mock.writtenData.count, 0) + } + + func testSynchronizingAndClosingFile() throws { + let factory = FileMockFactory() + let writer = try FileWriter(fileURL: logURL, fileSizeLimit: 1, fileFactory: factory) + writer.synchronizeAndCloseFile() + XCTAssertTrue(factory.mock.synchronizeFileCallCount == 1 && factory.mock.closeFileCallCount == 1) + } +} + +private class UnopenableFileFactory: FileFactory { + + struct OpenFileError: Error {} + + func makeInstance(forWritingTo: URL) throws -> File { + throw OpenFileError() + } +} + +private class FileMockFactory: FileFactory { + + let mock = FileMock() + + func makeInstance(forWritingTo: URL) throws -> File { + return mock + } +} + +private class FileMock: File { + + private(set) var writtenData = Data() + private(set) var synchronizeFileCallCount = 0 + private(set) var closeFileCallCount = 0 + + func seekToEndOfFile() -> UInt64 { + return 0 + } + + func swift_write(_ data: Data) throws { + self.writtenData.append(data) + } + + func synchronizeFile() { + synchronizeFileCallCount += 1 + } + + func closeFile() { + closeFileCallCount += 1 + } +} diff --git a/UnitTests/Info.plist b/UnitTests/Info.plist new file mode 100644 index 0000000..6c40a6c --- /dev/null +++ b/UnitTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/UnitTests/LogStringConvertibleTests.swift b/UnitTests/LogStringConvertibleTests.swift new file mode 100644 index 0000000..58bc9b2 --- /dev/null +++ b/UnitTests/LogStringConvertibleTests.swift @@ -0,0 +1,50 @@ +// +// MIT License +// +// Copyright (c) 2018 Wojciech Nagrodzki +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import XCTest +@testable import Logger + +class LogStringConvertibleTests: XCTestCase { + + func testZeroObjectsDescription() { + let objects = [Int]() + XCTAssertEqual(objects.logDescription, "[]") + } + + func testOneObjectDescription() { + let objects = [1] + XCTAssertEqual(objects.logDescription, "[1]") + } + + func testTwoObjectsDescription() { + let objects = [1, 2] + XCTAssertEqual(objects.logDescription, "[1, 2]") + } +} + +extension Int: LogStringConvertible { + public var logDescription: String { + return String(describing: self) + } +} diff --git a/UnitTests/LoggetTests.swift b/UnitTests/LoggetTests.swift new file mode 100644 index 0000000..a753ca7 --- /dev/null +++ b/UnitTests/LoggetTests.swift @@ -0,0 +1,55 @@ +// +// MIT License +// +// Copyright (c) 2018 Wojciech Nagrodzki +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import XCTest +@testable import Logger + +class LoggetTests: XCTestCase { + + func testLocation() { + let mock = LoggerMock() + mock.log("", level: .critical) + XCTAssertEqual(mock.location, "LoggetTests:32 testLocation()") + } + + func testLogLevelLogDescription() { + XCTAssertEqual(LogLevel.emergency.logDescription, "emerg") + XCTAssertEqual(LogLevel.alert.logDescription, "alert") + XCTAssertEqual(LogLevel.critical.logDescription, "crit") + XCTAssertEqual(LogLevel.error.logDescription, "err") + XCTAssertEqual(LogLevel.warning.logDescription, "warning") + XCTAssertEqual(LogLevel.notice.logDescription, "notice") + XCTAssertEqual(LogLevel.informational.logDescription, "info") + XCTAssertEqual(LogLevel.debug.logDescription, "debug") + } +} + +private class LoggerMock: Logger { + + var location: String? + + func log(time: Date, level: LogLevel, location: String, message: @autoclosure () -> String) { + self.location = location + } +}