diff --git a/Package.swift b/Package.swift index 8113a959..3c9ea581 100644 --- a/Package.swift +++ b/Package.swift @@ -37,6 +37,8 @@ let package = Package( "DistWork", "JunitReporting", "LaunchdUtils", + "ModelFactories", + "Models", "ProcessController", "SSHDeployer", "ScheduleStrategy", @@ -90,7 +92,8 @@ let package = Package( name: "DistRunTests", dependencies: [ "Deployer", - "DistRun" + "DistRun", + "ModelFactories" ]), .target( @@ -98,6 +101,7 @@ let package = Package( dependencies: [ "Extensions", "Logging", + "ModelFactories", "Models", "RESTMethods", "Scheduler", @@ -151,6 +155,18 @@ let package = Package( "Logging" ]), + .target( + name: "FileCache", + dependencies: [ + "Extensions", + "Utility" + ]), + .testTarget( + name: "FileCacheTests", + dependencies: [ + "FileCache" + ]), + .target( name: "HostDeterminer", dependencies: [ @@ -202,6 +218,16 @@ let package = Package( "Ansi" ]), + .target( + name: "ModelFactories", + dependencies: [ + "Extensions", + "FileCache", + "Models", + "ProcessController", + "URLResource" + ]), + .target( name: "Models", dependencies: []), @@ -309,6 +335,22 @@ let package = Package( dependencies: []), .testTarget( name: "SynchronousWaiterTests", - dependencies: ["SynchronousWaiter"]) + dependencies: ["SynchronousWaiter"]), + + .target( + name: "URLResource", + dependencies: [ + "FileCache", + "Logging", + "Utility" + ]), + .testTarget( + name: "URLResourceTests", + dependencies: [ + "FileCache", + "Swifter", + "URLResource", + "Utility" + ]) ] ) diff --git a/Sources/AvitoRunner/Arguments.swift b/Sources/AvitoRunner/Arguments.swift index ef0c4b1e..a5c8557a 100644 --- a/Sources/AvitoRunner/Arguments.swift +++ b/Sources/AvitoRunner/Arguments.swift @@ -52,7 +52,7 @@ private let knownStringArguments: [KnownStringArguments: ArgumentDescriptionHold comment: "A JSON file with test destination configurations. For runtime dump first destination will be used."), KnownStringArguments.fbxctest: ArgumentDescriptionHolder( name: "--fbxctest", - comment: "Path to fbxctest binary"), + comment: "Local path to fbxctest binary, or URL to ZIP archive"), KnownStringArguments.xctestBundle: ArgumentDescriptionHolder( name: "--xctest-bundle", comment: "Path to .xctest bundle with your tests"), @@ -67,7 +67,7 @@ private let knownStringArguments: [KnownStringArguments: ArgumentDescriptionHold comment: "Where the Chrome trace should be created"), KnownStringArguments.fbsimctl: ArgumentDescriptionHolder( name: "--fbsimctl", - comment: "Path to fbsimctl binary"), + comment: "Local path to fbsimctl binary, or URL to ZIP archive"), KnownStringArguments.app: ArgumentDescriptionHolder( name: "--app", comment: "Path to your app that will be tested by the UI tests"), diff --git a/Sources/AvitoRunner/DistRunTestsCommand.swift b/Sources/AvitoRunner/DistRunTestsCommand.swift index 96cba5c2..7f5ba77d 100644 --- a/Sources/AvitoRunner/DistRunTestsCommand.swift +++ b/Sources/AvitoRunner/DistRunTestsCommand.swift @@ -3,6 +3,7 @@ import Deployer import DistRun import Foundation import Logging +import ModelFactories import Models import ScheduleStrategy import Utility @@ -187,10 +188,10 @@ final class DistRunTestsCommand: Command { throw ArgumentsError.argumentIsMissing(KnownStringArguments.watchdogSettings) } - guard let fbxctest = arguments.get(self.fbxctest), fileManager.fileExists(atPath: fbxctest) else { + guard let fbxctest = arguments.get(self.fbxctest) else { throw ArgumentsError.argumentIsMissing(KnownStringArguments.fbxctest) } - guard let fbsimctl = arguments.get(self.fbsimctl), fileManager.fileExists(atPath: fbsimctl) else { + guard let fbsimctl = arguments.get(self.fbsimctl) else { throw ArgumentsError.argumentIsMissing(KnownStringArguments.fbsimctl) } @@ -239,9 +240,9 @@ final class DistRunTestsCommand: Command { numberOfSimulators: numberOfSimulators, environment: environmentValues, scheduleStrategy: scheduleStrategy), - auxiliaryPaths: AuxiliaryPaths( - fbxctest: fbxctest, - fbsimctl: fbsimctl, + auxiliaryPaths: try AuxiliaryPathsFactory().createWith( + fbxctest: ResourceLocation.from(fbxctest), + fbsimctl: ResourceLocation.from(fbsimctl), tempFolder: NSTemporaryDirectory()), buildArtifacts: BuildArtifacts( appBundle: app, diff --git a/Sources/AvitoRunner/DumpRuntimeTestsCommand.swift b/Sources/AvitoRunner/DumpRuntimeTestsCommand.swift index 8a270a56..df78a4a4 100644 --- a/Sources/AvitoRunner/DumpRuntimeTestsCommand.swift +++ b/Sources/AvitoRunner/DumpRuntimeTestsCommand.swift @@ -4,6 +4,7 @@ import DistRun import Foundation import JunitReporting import Logging +import ModelFactories import Models import RuntimeDump import Scheduler @@ -15,7 +16,7 @@ final class DumpRuntimeTestsCommand: Command { let overview = "Dumps all available runtime tests into JSON file" private let testDestinations: OptionArgument - private let fbxctest: OptionArgument + private let fbxctestValue: OptionArgument private let xctestBundle: OptionArgument private let output: OptionArgument private let encoder: JSONEncoder = { @@ -27,7 +28,7 @@ final class DumpRuntimeTestsCommand: Command { required init(parser: ArgumentParser) { let subparser = parser.add(subparser: command, overview: overview) testDestinations = subparser.add(stringArgument: KnownStringArguments.testDestinations) - fbxctest = subparser.add(stringArgument: KnownStringArguments.fbxctest) + fbxctestValue = subparser.add(stringArgument: KnownStringArguments.fbxctest) xctestBundle = subparser.add(stringArgument: KnownStringArguments.xctestBundle) output = subparser.add(stringArgument: KnownStringArguments.output) } @@ -45,7 +46,7 @@ final class DumpRuntimeTestsCommand: Command { } catch { throw ArgumentsError.argumentValueCannotBeUsed(KnownStringArguments.testDestinations, error) } - guard let fbxctest = arguments.get(fbxctest), fileManager.fileExists(atPath: fbxctest) else { + guard let fbxctestValue = arguments.get(fbxctestValue) else { throw ArgumentsError.argumentIsMissing(KnownStringArguments.fbxctest) } guard let xcTestBundle = arguments.get(xctestBundle), fileManager.fileExists(atPath: xcTestBundle) else { @@ -55,6 +56,9 @@ final class DumpRuntimeTestsCommand: Command { throw ArgumentsError.argumentIsMissing(KnownStringArguments.output) } + let resolver = ResourceLocationResolver.sharedResolver + let fbxctest = try resolver.resolvePath(resourceLocation: ResourceLocation.from(fbxctestValue)).with(archivedFile: "fbxctest") + let configuration = RuntimeDumpConfiguration( fbxctest: fbxctest, xcTestBundle: xcTestBundle, diff --git a/Sources/AvitoRunner/RunTestsCommand.swift b/Sources/AvitoRunner/RunTestsCommand.swift index b6dcf383..3b15eb17 100644 --- a/Sources/AvitoRunner/RunTestsCommand.swift +++ b/Sources/AvitoRunner/RunTestsCommand.swift @@ -4,11 +4,12 @@ import Extensions import Foundation import JunitReporting import Logging +import ModelFactories import Models -import RuntimeDump import Runner -import Scheduler +import RuntimeDump import ScheduleStrategy +import Scheduler import SimulatorPool import Utility @@ -155,10 +156,10 @@ final class RunTestsCommand: Command { throw ArgumentsError.argumentIsMissing(KnownStringArguments.watchdogSettings) } - guard let fbxctest = arguments.get(self.fbxctest), fileManager.fileExists(atPath: fbxctest) else { + guard let fbxctest = arguments.get(self.fbxctest) else { throw ArgumentsError.argumentIsMissing(KnownStringArguments.fbxctest) } - guard let fbsimctl = arguments.get(self.fbsimctl), fileManager.fileExists(atPath: fbsimctl) else { + guard let fbsimctl = arguments.get(self.fbsimctl) else { throw ArgumentsError.argumentIsMissing(KnownStringArguments.fbsimctl) } @@ -181,11 +182,6 @@ final class RunTestsCommand: Command { guard let tempFolder = arguments.get(self.tempFolder) else { throw ArgumentsError.argumentIsMissing(KnownStringArguments.tempFolder) } - do { - try fileManager.createDirectory(atPath: tempFolder, withIntermediateDirectories: true, attributes: nil) - } catch let error { - throw ArgumentsError.argumentValueCannotBeUsed(KnownStringArguments.tempFolder, error) - } let videoPath = arguments.get(self.videoPath) let oslogPath = arguments.get(self.oslogPath) @@ -216,7 +212,10 @@ final class RunTestsCommand: Command { numberOfSimulators: numberOfSimulators, environment: environmentValues, scheduleStrategy: scheduleStrategy), - auxiliaryPaths: AuxiliaryPaths(fbxctest: fbxctest, fbsimctl: fbsimctl, tempFolder: tempFolder), + auxiliaryPaths: AuxiliaryPathsFactory().createWith( + fbxctest: ResourceLocation.from(fbxctest), + fbsimctl: ResourceLocation.from(fbsimctl), + tempFolder: tempFolder), buildArtifacts: BuildArtifacts( appBundle: app, runner: runner, diff --git a/Sources/DistWork/BucketConfigurationFactory.swift b/Sources/DistWork/BucketConfigurationFactory.swift index 2326075a..89271b6c 100644 --- a/Sources/DistWork/BucketConfigurationFactory.swift +++ b/Sources/DistWork/BucketConfigurationFactory.swift @@ -1,5 +1,6 @@ import Foundation import Logging +import ModelFactories import Models import Runner import Scheduler @@ -41,7 +42,6 @@ final class BucketConfigurationFactory { /remote_path/some_run_id/avitoRunner/tempFolder/someUUID */ let tempFolder = packagePath(containerPath, .avitoRunner).appending(pathComponent: "tempFolder") - try FileManager.default.createDirectory(atPath: tempFolder, withIntermediateDirectories: true, attributes: nil) /* All paths below are resolved against containerPath. @@ -79,7 +79,10 @@ final class BucketConfigurationFactory { let watchdogSettings = try fileInPackageIfExists(containerPath, .watchdogSettings) let configuration = SchedulerConfiguration( - auxiliaryPaths: AuxiliaryPaths(fbxctest: fbxctest, fbsimctl: fbsimctl, tempFolder: tempFolder), + auxiliaryPaths: try AuxiliaryPathsFactory().createWith( + fbxctest: ResourceLocation.from(fbxctest), + fbsimctl: ResourceLocation.from(fbsimctl), + tempFolder: tempFolder), testType: .uiTest, buildArtifacts: BuildArtifacts( appBundle: app, diff --git a/Sources/Extensions/SHA256.swift b/Sources/Extensions/SHA256.swift new file mode 100644 index 00000000..9e3b6e9d --- /dev/null +++ b/Sources/Extensions/SHA256.swift @@ -0,0 +1,24 @@ +import Foundation + +extension Data { + public func avito_sha256Hash() -> Data { + let transform = SecDigestTransformCreate(kSecDigestSHA2, 256, nil) + SecTransformSetAttribute(transform, kSecTransformInputAttributeName, self as CFTypeRef, nil) + return SecTransformExecute(transform, nil) as! Data + } +} + +extension String { + public enum AvitoSHA256Error: Error { + case unableToHash + } + + public func avito_sha256Hash(encoding: String.Encoding = .utf8) throws -> String { + guard let dataToHash = self.data(using: encoding) else { throw AvitoSHA256Error.unableToHash } + let hashedData = dataToHash.avito_sha256Hash() + + return hashedData.reduce("") { (result, byte) -> String in + result + String(format:"%02x", UInt8(byte)) + } + } +} diff --git a/Sources/FileCache/FileCache+URL.swift b/Sources/FileCache/FileCache+URL.swift new file mode 100644 index 00000000..e427dad2 --- /dev/null +++ b/Sources/FileCache/FileCache+URL.swift @@ -0,0 +1,19 @@ +import Foundation + +public extension FileCache { + private func itemNameForUrl(_ url: URL) -> String { + return url.absoluteString + } + + func contains(itemForURL url: URL) -> Bool { + return self.contains(itemWithName: itemNameForUrl(url)) + } + + func urlForCachedContents(ofUrl url: URL) throws -> URL { + return try self.url(forItemWithName: itemNameForUrl(url)) + } + + func store(contentsUrl: URL, ofUrl url: URL) throws { + try self.store(itemAtURL: contentsUrl, underName: itemNameForUrl(url)) + } +} diff --git a/Sources/FileCache/FileCache.swift b/Sources/FileCache/FileCache.swift new file mode 100644 index 00000000..3b09041f --- /dev/null +++ b/Sources/FileCache/FileCache.swift @@ -0,0 +1,82 @@ +import Extensions +import Foundation + +public final class FileCache { + private let cachesUrl: URL + private let nameKeyer: NameKeyer + private let fileManager = FileManager() + + public init(cachesUrl: URL, nameHasher: NameKeyer = SHA256NameKeyer()) { + self.cachesUrl = cachesUrl + self.nameKeyer = nameHasher + } + + // MARK: - Public API + + public func contains(itemWithName name: String) -> Bool { + do { + let fileUrl = try url(forItemWithName: name) + return fileManager.fileExists(atPath: fileUrl.path) + } catch { + return false + } + } + + public func remove(itemWithName name: String) throws { + let container = try containerUrl(forItemWithName: name) + try fileManager.removeItem(at: container) + } + + public func store(itemAtURL itemUrl: URL, underName name: String) throws { + if contains(itemWithName: name) { + try remove(itemWithName: name) + } + + let container = try containerUrl(forItemWithName: name) + let filename = itemUrl.lastPathComponent + try fileManager.copyItem( + at: itemUrl, + to: container.appendingPathComponent(filename, isDirectory: false)) + + let itemInfo = CachedItemInfo(fileName: filename, timestamp: Date().timeIntervalSince1970) + let data = try encoder.encode(itemInfo) + try data.write(to: try cachedItemInfoFileUrl(forItemWithName: name), options: .atomicWrite) + } + + public func url(forItemWithName name: String) throws -> URL { + let itemInfo = try cachedItemInfo(forItemWithName: name) + let container = try containerUrl(forItemWithName: name) + return container.appendingPathComponent(itemInfo.fileName, isDirectory: false) + } + + // MARK: - Internals + + private struct CachedItemInfo: Codable { + let fileName: String + let timestamp: TimeInterval + } + + private let encoder = JSONEncoder() + private let decoder = JSONDecoder() + + private func containerUrl(forItemWithName name: String) throws -> URL { + let key = try nameKeyer.key(forName: name) + let containerUrl = cachesUrl.appendingPathComponent(key, isDirectory: true) + if !fileManager.fileExists(atPath: containerUrl.path) { + try fileManager.createDirectory(at: containerUrl, withIntermediateDirectories: true) + } + return containerUrl + } + + private func cachedItemInfoFileUrl(forItemWithName name: String) throws -> URL { + let key = try nameKeyer.key(forName: name) + let container = try containerUrl(forItemWithName: name) + return container.appendingPathComponent(key, isDirectory: false).appendingPathExtension("json") + } + + private func cachedItemInfo(forItemWithName name: String) throws -> CachedItemInfo { + let infoFileUrl = try cachedItemInfoFileUrl(forItemWithName: name) + let data = try Data(contentsOf: infoFileUrl) + return try decoder.decode(CachedItemInfo.self, from: data) + } +} diff --git a/Sources/FileCache/NameKeyer.swift b/Sources/FileCache/NameKeyer.swift new file mode 100644 index 00000000..64e148d7 --- /dev/null +++ b/Sources/FileCache/NameKeyer.swift @@ -0,0 +1,5 @@ +import Foundation + +public protocol NameKeyer { + func key(forName name: String) throws -> String +} diff --git a/Sources/FileCache/SHA256NameKeyer.swift b/Sources/FileCache/SHA256NameKeyer.swift new file mode 100644 index 00000000..ade621a3 --- /dev/null +++ b/Sources/FileCache/SHA256NameKeyer.swift @@ -0,0 +1,10 @@ +import Extensions +import Foundation + +public final class SHA256NameKeyer: NameKeyer { + public init() {} + + public func key(forName name: String) throws -> String { + return try name.avito_sha256Hash() + } +} diff --git a/Sources/ModelFactories/AuxiliaryPathsFactory.swift b/Sources/ModelFactories/AuxiliaryPathsFactory.swift new file mode 100644 index 00000000..c48ea001 --- /dev/null +++ b/Sources/ModelFactories/AuxiliaryPathsFactory.swift @@ -0,0 +1,27 @@ +import Foundation +import Models + +public final class AuxiliaryPathsFactory { + private let fileManager = FileManager() + public init() {} + + public func createWith( + fbxctest: ResourceLocation, + fbsimctl: ResourceLocation, + tempFolder: String = "") + throws -> AuxiliaryPaths + { + if !tempFolder.isEmpty { + try fileManager.createDirectory(atPath: tempFolder, withIntermediateDirectories: true, attributes: [:]) + } + + let resolver = ResourceLocationResolver.sharedResolver + let fbxctestPath = try resolver.resolvePath(resourceLocation: fbxctest).with(archivedFile: "fbxctest") + let fbsimctlPath = try resolver.resolvePath(resourceLocation: fbsimctl).with(archivedFile: "fbsimctl") + + return AuxiliaryPaths.withoutValidatingValues( + fbxctest: fbxctestPath, + fbsimctl: fbsimctlPath, + tempFolder: tempFolder) + } +} diff --git a/Sources/ModelFactories/ResourceLocationResolver.swift b/Sources/ModelFactories/ResourceLocationResolver.swift new file mode 100644 index 00000000..26ee7b14 --- /dev/null +++ b/Sources/ModelFactories/ResourceLocationResolver.swift @@ -0,0 +1,76 @@ +import Extensions +import FileCache +import Foundation +import Logging +import Models +import ProcessController +import URLResource + +public final class ResourceLocationResolver { + private let fileCache: FileCache + private let urlResource: URLResource + private let fileManager = FileManager() + + public enum ValidationError: Error { + case binaryNotFoundAtPath(String) + case unpackProcessError + } + + public enum Result { + case directlyAccessibleFile(path: String) + case contentsOfArchive(folderPath: String) + + public func with(archivedFile: String) -> String { + switch self { + case .directlyAccessibleFile(let path): + return path + case .contentsOfArchive(let folderPath): + return folderPath.appending(pathComponent: archivedFile) + } + } + } + + public static let sharedResolver = ResourceLocationResolver(cachesUrl: cachesUrl()) + + private static func cachesUrl() -> URL { + let cacheContainer: URL + if let cachesUrl = try? FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) { + cacheContainer = cachesUrl + } else { + let pathToBinaryContainer = ProcessInfo.processInfo.arguments[0].deletingLastPathComponent + cacheContainer = URL(fileURLWithPath: pathToBinaryContainer) + } + return cacheContainer.appendingPathComponent("ru.avito.Runner.cache", isDirectory: true) + } + + private init(cachesUrl: URL) { + self.fileCache = FileCache(cachesUrl: cachesUrl) + self.urlResource = URLResource(fileCache: fileCache, urlSession: URLSession.shared) + } + + public func resolvePath(resourceLocation: ResourceLocation) throws -> Result { + switch resourceLocation { + case .localFilePath(let path): + return Result.directlyAccessibleFile(path: path) + case .remoteUrl(let url): + let path = try cachedContentsOfUrl(url).path + return Result.contentsOfArchive(folderPath: path) + } + } + + private func cachedContentsOfUrl(_ url: URL) throws -> URL { + let handler = BlockingURLResourceHandler() + urlResource.fetchResource(url: url, handler: handler) + let zipUrl = try handler.wait() + let contentsUrl = zipUrl.deletingLastPathComponent().appendingPathComponent("zip_contents", isDirectory: true) + if !fileManager.fileExists(atPath: contentsUrl.path) { + log("Will unzip '\(zipUrl)' into '\(contentsUrl)'") + let controller = ProcessController( + subprocess: Subprocess(arguments: ["/usr/bin/unzip", zipUrl.path, "-d", contentsUrl.path]), + maximumAllowedSilenceDuration: nil) + controller.startAndListenUntilProcessDies() + guard controller.terminationStatus() == 0 else { throw ValidationError.unpackProcessError } + } + return contentsUrl + } +} diff --git a/Sources/Models/AuxiliaryPaths.swift b/Sources/Models/AuxiliaryPaths.swift index 72235cb5..4cc536b6 100644 --- a/Sources/Models/AuxiliaryPaths.swift +++ b/Sources/Models/AuxiliaryPaths.swift @@ -1,5 +1,9 @@ import Foundation +/** + * AuxiliaryPaths represents auxillary tools that are used by the runner. It is recommended to create this model + * using AuxiliaryPathsFactory which will validate the arguments and supports URLs. + */ public struct AuxiliaryPaths: Hashable { /** Absolute path to fbxctest binary. */ public let fbxctest: String @@ -10,9 +14,20 @@ public struct AuxiliaryPaths: Hashable { /** Where the runner can store temporary stuff. */ public let tempFolder: String - public init(fbxctest: String, fbsimctl: String, tempFolder: String) { + private init(fbxctest: String, fbsimctl: String, tempFolder: String) { self.fbxctest = fbxctest self.fbsimctl = fbsimctl self.tempFolder = tempFolder } + + /** CONSIDER using AuxiliaryPathsFactory. Creates a model with the given values without any validation. */ + public static func withoutValidatingValues( + fbxctest: String, + fbsimctl: String, + tempFolder: String) -> AuxiliaryPaths + { + return AuxiliaryPaths(fbxctest: fbxctest, fbsimctl: fbsimctl, tempFolder: tempFolder) + } + + public static let empty = AuxiliaryPaths(fbxctest: "", fbsimctl: "", tempFolder: "") } diff --git a/Sources/Models/ResourceLocation.swift b/Sources/Models/ResourceLocation.swift new file mode 100644 index 00000000..55d7ade5 --- /dev/null +++ b/Sources/Models/ResourceLocation.swift @@ -0,0 +1,34 @@ +import Foundation + +public enum ResourceLocation { + case localFilePath(String) + case remoteUrl(URL) + + public enum ValidationError: Error { + case cannotCreateUrl(String) + case fileDoesNotExist(String) + case unsupportedUrlScheme(URL) + } + + public static func from(_ string: String) throws -> ResourceLocation { + guard var components = URLComponents(string: string) else { throw ValidationError.cannotCreateUrl(string) } + if components.scheme == nil { + components.scheme = "file" + } + guard let url = components.url else { throw ValidationError.cannotCreateUrl(string) } + if url.isFileURL { + return try withPathString(url.path) + } else { + return try withUrl(url) + } + } + + private static func withUrl(_ url: URL) throws -> ResourceLocation { + return ResourceLocation.remoteUrl(url) + } + + private static func withPathString(_ string: String) throws -> ResourceLocation { + guard FileManager.default.fileExists(atPath: string) else { throw ValidationError.fileDoesNotExist(string) } + return ResourceLocation.localFilePath(string) + } +} diff --git a/Sources/URLResource/BlockingURLResourceHandler.swift b/Sources/URLResource/BlockingURLResourceHandler.swift new file mode 100644 index 00000000..7a3736ff --- /dev/null +++ b/Sources/URLResource/BlockingURLResourceHandler.swift @@ -0,0 +1,34 @@ +import Basic +import Dispatch +import Foundation +import Logging + +public final class BlockingURLResourceHandler: URLResourceHandler { + + private let semaphore = DispatchSemaphore(value: 0) + private var result: Result = Result.failure(HandlerError.timeout) + + public enum HandlerError: Error { + case timeout + case failure(Error) + } + + public init() {} + + public func wait(limit: TimeInterval = 20.0) throws -> URL { + _ = semaphore.wait(timeout: .now() + limit) + return try result.dematerialize() + } + + public func resourceUrl(contentUrl: URL, forUrl url: URL) { + log("Obtained resource for '\(url)' at local url: '\(contentUrl)'") + result = Result.success(contentUrl) + semaphore.signal() + } + + public func failedToGetContents(forUrl url: URL, error: Error) { + log("Failed to fetch resource for '\(url)': \(error)") + result = Result.failure(HandlerError.failure(error)) + semaphore.signal() + } +} diff --git a/Sources/URLResource/URLResource.swift b/Sources/URLResource/URLResource.swift new file mode 100644 index 00000000..a6b8415a --- /dev/null +++ b/Sources/URLResource/URLResource.swift @@ -0,0 +1,50 @@ +import FileCache +import Foundation +import Logging + +public final class URLResource { + private let fileCache: FileCache + private let urlSession: URLSession + + public enum `Error`: Swift.Error { + case unknownError(response: URLResponse?) + } + + public init(fileCache: FileCache, urlSession: URLSession) { + self.fileCache = fileCache + self.urlSession = urlSession + } + + public func fetchResource(url: URL, handler: URLResourceHandler) { + if fileCache.contains(itemForURL: url) { + do { + log("Found already cached resource for url '\(url)'") + let cacheUrl = try fileCache.urlForCachedContents(ofUrl: url) + handler.resourceUrl(contentUrl: cacheUrl, forUrl: url) + } catch { + handler.failedToGetContents(forUrl: url, error: error) + } + } else { + log("Will fetch resource for url '\(url)'") + let task = urlSession.downloadTask(with: url) { (localUrl: URL?, response: URLResponse?, error: Swift.Error?) in + if let error = error { + handler.failedToGetContents(forUrl: url, error: error) + } else if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 { + handler.failedToGetContents(forUrl: url, error: Error.unknownError(response: response)) + } else if let localUrl = localUrl { + do { + try self.fileCache.store(contentsUrl: localUrl, ofUrl: url) + let cachedUrl = try self.fileCache.urlForCachedContents(ofUrl: url) + log("Stored resource for '\(url)' in file cache") + handler.resourceUrl(contentUrl: cachedUrl, forUrl: url) + } catch { + handler.failedToGetContents(forUrl: url, error: error) + } + } else { + handler.failedToGetContents(forUrl: url, error: Error.unknownError(response: response)) + } + } + task.resume() + } + } +} diff --git a/Sources/URLResource/URLResourceHandler.swift b/Sources/URLResource/URLResourceHandler.swift new file mode 100644 index 00000000..42fcfe53 --- /dev/null +++ b/Sources/URLResource/URLResourceHandler.swift @@ -0,0 +1,6 @@ +import Foundation + +public protocol URLResourceHandler { + func resourceUrl(contentUrl: URL, forUrl url: URL) + func failedToGetContents(forUrl url: URL, error: Error) +} diff --git a/Tests/DistRunTests/DeployablesGeneratorTests.swift b/Tests/DistRunTests/DeployablesGeneratorTests.swift index f8aeb032..8feec95c 100644 --- a/Tests/DistRunTests/DeployablesGeneratorTests.swift +++ b/Tests/DistRunTests/DeployablesGeneratorTests.swift @@ -1,6 +1,7 @@ import Extensions import Deployer @testable import DistRun +import ModelFactories import Models import XCTest @@ -15,19 +16,21 @@ class DeployablesGeneratorTests: XCTestCase { override func setUp() { super.setUp() - - let generator = DeployablesGenerator( - targetAvitoRunnerPath: "AvitoRunner", - auxiliaryPaths: AuxiliaryPaths(fbxctest: String(#file), fbsimctl: String(#file), tempFolder: ""), - buildArtifacts: defaultBuildArtifacts, - environmentFilePath: String(#file), - targetEnvironmentPath: "env.json", - simulatorSettings: SimulatorSettings( - simulatorLocalizationSettings: String(#file), - watchdogSettings: String(#file)), - targetSimulatorLocalizationSettingsPath: "sim.json", - targetWatchdogSettingsPath: "wd.json") do { + let generator = DeployablesGenerator( + targetAvitoRunnerPath: "AvitoRunner", + auxiliaryPaths: try AuxiliaryPathsFactory().createWith( + fbxctest: ResourceLocation.from(String(#file)), + fbsimctl: ResourceLocation.from(String(#file)), + tempFolder: ""), + buildArtifacts: defaultBuildArtifacts, + environmentFilePath: String(#file), + targetEnvironmentPath: "env.json", + simulatorSettings: SimulatorSettings( + simulatorLocalizationSettings: String(#file), + watchdogSettings: String(#file)), + targetSimulatorLocalizationSettingsPath: "sim.json", + targetWatchdogSettingsPath: "wd.json") self.deployables = try generator.deployables() } catch { self.continueAfterFailure = false @@ -117,7 +120,10 @@ class DeployablesGeneratorTests: XCTestCase { func testOptionalWatchdogAndSimulatorLocalizationSettongs() throws { let generator = DeployablesGenerator( targetAvitoRunnerPath: "AvitoRunner", - auxiliaryPaths: AuxiliaryPaths(fbxctest: String(#file), fbsimctl: String(#file), tempFolder: ""), + auxiliaryPaths: try AuxiliaryPathsFactory().createWith( + fbxctest: ResourceLocation.from(String(#file)), + fbsimctl: ResourceLocation.from(String(#file)), + tempFolder: ""), buildArtifacts: defaultBuildArtifacts, environmentFilePath: String(#file), targetEnvironmentPath: "env.json", diff --git a/Tests/ExtensionsTests/SHA256Tests.swift b/Tests/ExtensionsTests/SHA256Tests.swift new file mode 100644 index 00000000..152f5030 --- /dev/null +++ b/Tests/ExtensionsTests/SHA256Tests.swift @@ -0,0 +1,11 @@ +import Extensions +import Foundation +import XCTest + +final class SHA256Tests: XCTestCase { + func test() throws { + XCTAssertEqual( + try "string".avito_sha256Hash().uppercased(), + "473287F8298DBA7163A897908958F7C0EAE733E25D2E027992EA2EDC9BED2FA8") + } +} diff --git a/Tests/FileCacheTests/FileCacheTests.swift b/Tests/FileCacheTests/FileCacheTests.swift new file mode 100644 index 00000000..044eb7ee --- /dev/null +++ b/Tests/FileCacheTests/FileCacheTests.swift @@ -0,0 +1,24 @@ +import Basic +import FileCache +import Foundation +import XCTest + +public final class FileCacheTests: XCTestCase { + func testStorage() throws { + let tempFolder = try TemporaryDirectory(removeTreeOnDeinit: true) + let cache = FileCache(cachesUrl: URL(fileURLWithPath: tempFolder.path.asString)) + XCTAssertFalse(cache.contains(itemWithName: "item")) + + XCTAssertNoThrow(try cache.store(itemAtURL: URL(fileURLWithPath: #file), underName: "item")) + let cacheUrl = try cache.url(forItemWithName: "item") + XCTAssertTrue(cache.contains(itemWithName: "item")) + XCTAssertEqual(cacheUrl.lastPathComponent, URL(fileURLWithPath: #file).lastPathComponent) + + let expectedContents = try String(contentsOfFile: #file) + let actualContents = try String(contentsOfFile: cacheUrl.path) + XCTAssertEqual(expectedContents, actualContents) + + XCTAssertNoThrow(try cache.remove(itemWithName: "item")) + XCTAssertFalse(cache.contains(itemWithName: "item")) + } +} diff --git a/Tests/FileCacheTests/SHA256NameKeyerTests.swift b/Tests/FileCacheTests/SHA256NameKeyerTests.swift new file mode 100644 index 00000000..5d7904d5 --- /dev/null +++ b/Tests/FileCacheTests/SHA256NameKeyerTests.swift @@ -0,0 +1,11 @@ +import FileCache +import Foundation +import XCTest + +final class SHA256NameKeyerTests: XCTestCase { + func test() { + XCTAssertEqual( + try SHA256NameKeyer().key(forName: "input").uppercased(), + "C96C6D5BE8D08A12E7B5CDC1B207FA6B2430974C86803D8891675E76FD992C20") + } +} diff --git a/Tests/SimulatorPoolTests/SimulatorPoolTests.swift b/Tests/SimulatorPoolTests/SimulatorPoolTests.swift index 91e7c536..d39f93fd 100644 --- a/Tests/SimulatorPoolTests/SimulatorPoolTests.swift +++ b/Tests/SimulatorPoolTests/SimulatorPoolTests.swift @@ -9,7 +9,7 @@ class SimulatorPoolTests: XCTestCase { let pool = SimulatorPool( numberOfSimulators: 1, testDestination: try TestDestination(deviceType: "", iOSVersion: "11.0"), - auxiliaryPaths: AuxiliaryPaths(fbxctest: "", fbsimctl: "", tempFolder: "")) + auxiliaryPaths: AuxiliaryPaths.empty) _ = try pool.allocateSimulator() XCTAssertThrowsError(_ = try pool.allocateSimulator(), "Expected to throw") { error in XCTAssertEqual(error as? BorrowError, BorrowError.noSimulatorsLeft) @@ -21,7 +21,7 @@ class SimulatorPoolTests: XCTestCase { let pool = SimulatorPool( numberOfSimulators: UInt(numberOfThreads), testDestination: try TestDestination(deviceType: "", iOSVersion: "11.0"), - auxiliaryPaths: AuxiliaryPaths(fbxctest: "", fbsimctl: "", tempFolder: "")) + auxiliaryPaths: AuxiliaryPaths.empty) let queue = OperationQueue() queue.maxConcurrentOperationCount = Int(numberOfThreads) @@ -45,7 +45,7 @@ class SimulatorPoolTests: XCTestCase { let pool = SimulatorPool( numberOfSimulators: 1, testDestination: try TestDestination(deviceType: "Fake Device", iOSVersion: "11.3"), - auxiliaryPaths: AuxiliaryPaths(fbxctest: "", fbsimctl: "", tempFolder: ""), + auxiliaryPaths: AuxiliaryPaths.empty, automaticCleanupTiumeout: 1) let simulatorController = try pool.allocateSimulator() pool.freeSimulator(simulatorController) diff --git a/Tests/SynchronousWaiterTests/SynchronousWaiterTests.swift b/Tests/SynchronousWaiterTests/SynchronousWaiterTests.swift index 8fa94a53..20308ab7 100644 --- a/Tests/SynchronousWaiterTests/SynchronousWaiterTests.swift +++ b/Tests/SynchronousWaiterTests/SynchronousWaiterTests.swift @@ -8,7 +8,7 @@ class SynchronousWaiterTest: XCTestCase { let start = Date() SynchronousWaiter.wait(pollPeriod: 0.01, timeout: expectedDuration) let actualDuration = Date().timeIntervalSince(start) - XCTAssertEqual(actualDuration, expectedDuration, accuracy: 0.03) + XCTAssertEqual(actualDuration, expectedDuration, accuracy: 0.1) } func testWaitFromEmptyRunLoop() { diff --git a/Tests/URLResourceTests/URLResourceTests.swift b/Tests/URLResourceTests/URLResourceTests.swift new file mode 100644 index 00000000..9a40968a --- /dev/null +++ b/Tests/URLResourceTests/URLResourceTests.swift @@ -0,0 +1,53 @@ +import Basic +import FileCache +import Swifter +import URLResource +import XCTest + +final class URLResourceTests: XCTestCase { + var temporaryDirectory: TemporaryDirectory! + var server: HttpServer? + var serverPort = 0 + var fileCache: FileCache! + + override func setUp() { + do { + temporaryDirectory = try TemporaryDirectory(removeTreeOnDeinit: true) + fileCache = FileCache(cachesUrl: URL(fileURLWithPath: temporaryDirectory.path.asString)) + server = HttpServer() + try server?.start(0) + serverPort = try server?.port() ?? 0 + } catch { + XCTFail("Failed: \(error)") + } + } + + override func tearDown() { + server?.stop() + } + + func testWithAvailableResource() throws { + let expectedContents = "some fetched contents" + server?["/get"] = { _ in HttpResponse.ok(.text(expectedContents)) } + + let resource = URLResource(fileCache: fileCache, urlSession: URLSession.shared) + let handler = BlockingURLResourceHandler() + resource.fetchResource( + url: URL(string: "http://localhost:\(serverPort)/get/")!, + handler: handler) + let contentUrl = try handler.wait(limit: 5) + + XCTAssertEqual(try String(contentsOf: contentUrl), expectedContents) + } + + func testWithUnavailableResource() throws { + server?["/get"] = { _ in HttpResponse.internalServerError } + + let resource = URLResource(fileCache: fileCache, urlSession: URLSession.shared) + let handler = BlockingURLResourceHandler() + resource.fetchResource( + url: URL(string: "http://localhost:\(serverPort)/get/")!, + handler: handler) + XCTAssertThrowsError(try handler.wait(limit: 5)) + } +}