From 209423722f2de859b93f8bb1d1c4707fd0b36f0a Mon Sep 17 00:00:00 2001 From: Vladislav Alekseev Date: Wed, 5 Sep 2018 19:03:47 +0300 Subject: [PATCH 1/6] MBS-2930: FileCache with tests --- Package.swift | 12 +++ Sources/Extensions/SHA256.swift | 24 ++++++ Sources/FileCache/FileCache.swift | 81 +++++++++++++++++++ Sources/FileCache/NameKeyer.swift | 5 ++ Sources/FileCache/SHA256NameKeyer.swift | 10 +++ Tests/ExtensionsTests/SHA256Tests.swift | 11 +++ Tests/FileCacheTests/FileCacheTests.swift | 24 ++++++ .../FileCacheTests/SHA256NameKeyerTests.swift | 11 +++ 8 files changed, 178 insertions(+) create mode 100644 Sources/Extensions/SHA256.swift create mode 100644 Sources/FileCache/FileCache.swift create mode 100644 Sources/FileCache/NameKeyer.swift create mode 100644 Sources/FileCache/SHA256NameKeyer.swift create mode 100644 Tests/ExtensionsTests/SHA256Tests.swift create mode 100644 Tests/FileCacheTests/FileCacheTests.swift create mode 100644 Tests/FileCacheTests/SHA256NameKeyerTests.swift diff --git a/Package.swift b/Package.swift index 8113a959..00d263fc 100644 --- a/Package.swift +++ b/Package.swift @@ -151,6 +151,18 @@ let package = Package( "Logging" ]), + .target( + name: "FileCache", + dependencies: [ + "Extensions", + "Utility" + ]), + .testTarget( + name: "FileCacheTests", + dependencies: [ + "FileCache" + ]), + .target( name: "HostDeterminer", dependencies: [ 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.swift b/Sources/FileCache/FileCache.swift new file mode 100644 index 00000000..d14bfe91 --- /dev/null +++ b/Sources/FileCache/FileCache.swift @@ -0,0 +1,81 @@ +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) throws -> 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 try 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) + 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 + + struct CachedItemInfo: Codable { + let fileName: String + } + + 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/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..09d3068c --- /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(try cache.contains(itemWithName: "item")) + + XCTAssertNoThrow(try cache.store(itemAtURL: URL(fileURLWithPath: #file), underName: "item")) + let cacheUrl = try cache.url(forItemWithName: "item") + XCTAssertTrue(try 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(try 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") + } +} From 304c6fae729ca04d45874f9b4e25b956dc8a5ccf Mon Sep 17 00:00:00 2001 From: Vladislav Alekseev Date: Thu, 6 Sep 2018 12:52:06 +0300 Subject: [PATCH 2/6] MBS-2930: URLResource + tests --- Package.swift | 17 +++++- Sources/FileCache/FileCache+URL.swift | 19 +++++++ Sources/FileCache/FileCache.swift | 6 +-- Sources/URLResource/BlockingHandler.swift | 30 +++++++++++ Sources/URLResource/Handler.swift | 6 +++ Sources/URLResource/URLResource.swift | 46 ++++++++++++++++ Tests/FileCacheTests/FileCacheTests.swift | 6 +-- Tests/URLResourceTests/URLResourceTests.swift | 54 +++++++++++++++++++ 8 files changed, 177 insertions(+), 7 deletions(-) create mode 100644 Sources/FileCache/FileCache+URL.swift create mode 100644 Sources/URLResource/BlockingHandler.swift create mode 100644 Sources/URLResource/Handler.swift create mode 100644 Sources/URLResource/URLResource.swift create mode 100644 Tests/URLResourceTests/URLResourceTests.swift diff --git a/Package.swift b/Package.swift index 00d263fc..7dde4cb5 100644 --- a/Package.swift +++ b/Package.swift @@ -321,6 +321,21 @@ let package = Package( dependencies: []), .testTarget( name: "SynchronousWaiterTests", - dependencies: ["SynchronousWaiter"]) + dependencies: ["SynchronousWaiter"]), + + .target( + name: "URLResource", + dependencies: [ + "FileCache", + "Utility" + ]), + .testTarget( + name: "URLResourceTests", + dependencies: [ + "FileCache", + "Swifter", + "URLResource", + "Utility" + ]) ] ) 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 index d14bfe91..d5a5e255 100644 --- a/Sources/FileCache/FileCache.swift +++ b/Sources/FileCache/FileCache.swift @@ -13,7 +13,7 @@ public final class FileCache { // MARK: - Public API - public func contains(itemWithName name: String) throws -> Bool { + public func contains(itemWithName name: String) -> Bool { do { let fileUrl = try url(forItemWithName: name) return fileManager.fileExists(atPath: fileUrl.path) @@ -28,7 +28,7 @@ public final class FileCache { } public func store(itemAtURL itemUrl: URL, underName name: String) throws { - if try contains(itemWithName: name) { + if contains(itemWithName: name) { try remove(itemWithName: name) } @@ -51,7 +51,7 @@ public final class FileCache { // MARK: - Internals - struct CachedItemInfo: Codable { + private struct CachedItemInfo: Codable { let fileName: String } diff --git a/Sources/URLResource/BlockingHandler.swift b/Sources/URLResource/BlockingHandler.swift new file mode 100644 index 00000000..be6bf443 --- /dev/null +++ b/Sources/URLResource/BlockingHandler.swift @@ -0,0 +1,30 @@ +import Basic +import Foundation + +public final class BlockingHandler: Handler { + + private let condition = NSCondition() + private var result: Result = Result.failure(HandlerError.timeout) + + public enum HandlerError: Error { + case timeout + case failure(Error) + } + + public init() {} + + public func wait(until limit: Date = Date.distantFuture) throws -> URL { + condition.wait(until: limit) + return try result.dematerialize() + } + + public func failedToGetContents(forUrl url: URL, error: Error) { + result = Result.failure(HandlerError.failure(error)) + condition.signal() + } + + public func resourceUrl(contentUrl: URL, forUrl url: URL) { + result = Result.success(contentUrl) + condition.signal() + } +} diff --git a/Sources/URLResource/Handler.swift b/Sources/URLResource/Handler.swift new file mode 100644 index 00000000..807fd723 --- /dev/null +++ b/Sources/URLResource/Handler.swift @@ -0,0 +1,6 @@ +import Foundation + +public protocol Handler { + func resourceUrl(contentUrl: URL, forUrl url: URL) + func failedToGetContents(forUrl url: URL, error: Error) +} diff --git a/Sources/URLResource/URLResource.swift b/Sources/URLResource/URLResource.swift new file mode 100644 index 00000000..24899917 --- /dev/null +++ b/Sources/URLResource/URLResource.swift @@ -0,0 +1,46 @@ +import FileCache +import Foundation + +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: Handler) { + if fileCache.contains(itemForURL: url) { + do { + let cacheUrl = try fileCache.urlForCachedContents(ofUrl: url) + handler.resourceUrl(contentUrl: cacheUrl, forUrl: url) + } catch { + handler.failedToGetContents(forUrl: url, error: error) + } + } else { + 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) + 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/Tests/FileCacheTests/FileCacheTests.swift b/Tests/FileCacheTests/FileCacheTests.swift index 09d3068c..044eb7ee 100644 --- a/Tests/FileCacheTests/FileCacheTests.swift +++ b/Tests/FileCacheTests/FileCacheTests.swift @@ -7,11 +7,11 @@ public final class FileCacheTests: XCTestCase { func testStorage() throws { let tempFolder = try TemporaryDirectory(removeTreeOnDeinit: true) let cache = FileCache(cachesUrl: URL(fileURLWithPath: tempFolder.path.asString)) - XCTAssertFalse(try cache.contains(itemWithName: "item")) + XCTAssertFalse(cache.contains(itemWithName: "item")) XCTAssertNoThrow(try cache.store(itemAtURL: URL(fileURLWithPath: #file), underName: "item")) let cacheUrl = try cache.url(forItemWithName: "item") - XCTAssertTrue(try cache.contains(itemWithName: "item")) + XCTAssertTrue(cache.contains(itemWithName: "item")) XCTAssertEqual(cacheUrl.lastPathComponent, URL(fileURLWithPath: #file).lastPathComponent) let expectedContents = try String(contentsOfFile: #file) @@ -19,6 +19,6 @@ public final class FileCacheTests: XCTestCase { XCTAssertEqual(expectedContents, actualContents) XCTAssertNoThrow(try cache.remove(itemWithName: "item")) - XCTAssertFalse(try cache.contains(itemWithName: "item")) + XCTAssertFalse(cache.contains(itemWithName: "item")) } } diff --git a/Tests/URLResourceTests/URLResourceTests.swift b/Tests/URLResourceTests/URLResourceTests.swift new file mode 100644 index 00000000..4f4703a4 --- /dev/null +++ b/Tests/URLResourceTests/URLResourceTests.swift @@ -0,0 +1,54 @@ +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 = BlockingHandler() + resource.fetchResource( + url: URL(string: "http://localhost:\(serverPort)/get/")!, + handler: handler) + let contentUrl = try handler.wait(until: Date().addingTimeInterval(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 = BlockingHandler() + resource.fetchResource( + url: URL(string: "http://localhost:\(serverPort)/get/")!, + handler: handler) + XCTAssertThrowsError(try handler.wait(until: Date().addingTimeInterval(5))) + } +} From b947a510a220b4b19b69a4529f3c9b4e7723150c Mon Sep 17 00:00:00 2001 From: Vladislav Alekseev Date: Thu, 6 Sep 2018 16:01:48 +0300 Subject: [PATCH 3/6] MBS-2930: resolve fb tools paths through ResourceLocation --- Package.swift | 17 +++++- Sources/AvitoRunner/Arguments.swift | 4 +- Sources/AvitoRunner/DistRunTestsCommand.swift | 7 ++- .../AvitoRunner/DumpRuntimeTestsCommand.swift | 10 +++- Sources/AvitoRunner/RunTestsCommand.swift | 15 +++-- .../DistWork/BucketConfigurationFactory.swift | 7 ++- Sources/FileCache/FileCache.swift | 3 +- .../AuxiliaryPathsFactory.swift | 25 ++++++++ .../ResourceLocationResolver.swift | 57 +++++++++++++++++++ Sources/Models/AuxiliaryPaths.swift | 17 +++++- Sources/Models/ResourceLocation.swift | 34 +++++++++++ Sources/URLResource/BlockingHandler.swift | 5 +- Sources/URLResource/URLResource.swift | 4 ++ .../DeployablesGeneratorTests.swift | 32 ++++++----- .../SimulatorPoolTests.swift | 6 +- 15 files changed, 206 insertions(+), 37 deletions(-) create mode 100644 Sources/ModelFactories/AuxiliaryPathsFactory.swift create mode 100644 Sources/ModelFactories/ResourceLocationResolver.swift create mode 100644 Sources/Models/ResourceLocation.swift diff --git a/Package.swift b/Package.swift index 7dde4cb5..2bd557b2 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", @@ -214,6 +218,16 @@ let package = Package( "Ansi" ]), + .target( + name: "ModelFactories", + dependencies: [ + "Extensions", + "FileCache", + "Models", + "URLResource", + "ZIPFoundation" + ]), + .target( name: "Models", dependencies: []), @@ -327,6 +341,7 @@ let package = Package( name: "URLResource", dependencies: [ "FileCache", + "Logging", "Utility" ]), .testTarget( 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..f4df0fab 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 @@ -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..8acc259e 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 @@ -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 fbxctest = arguments.get(fbxctest) else { throw ArgumentsError.argumentIsMissing(KnownStringArguments.fbxctest) } guard let xcTestBundle = arguments.get(xctestBundle), fileManager.fileExists(atPath: xcTestBundle) else { @@ -55,8 +56,13 @@ final class DumpRuntimeTestsCommand: Command { throw ArgumentsError.argumentIsMissing(KnownStringArguments.output) } + let resolver = ResourceLocationResolver.sharedResolver + let fbxctestPath = try resolver.resolvePathToBinary( + resourceLocation: ResourceLocation.from(fbxctest), + binaryName: "fbxctest") + let configuration = RuntimeDumpConfiguration( - fbxctest: fbxctest, + fbxctest: fbxctestPath, xcTestBundle: xcTestBundle, simulatorSettings: SimulatorSettings(simulatorLocalizationSettings: "", watchdogSettings: ""), testDestination: testDestinationConfigurations[0].testDestination, diff --git a/Sources/AvitoRunner/RunTestsCommand.swift b/Sources/AvitoRunner/RunTestsCommand.swift index b6dcf383..e23185d9 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 @@ -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/FileCache/FileCache.swift b/Sources/FileCache/FileCache.swift index d5a5e255..3b09041f 100644 --- a/Sources/FileCache/FileCache.swift +++ b/Sources/FileCache/FileCache.swift @@ -38,7 +38,7 @@ public final class FileCache { at: itemUrl, to: container.appendingPathComponent(filename, isDirectory: false)) - let itemInfo = CachedItemInfo(fileName: filename) + let itemInfo = CachedItemInfo(fileName: filename, timestamp: Date().timeIntervalSince1970) let data = try encoder.encode(itemInfo) try data.write(to: try cachedItemInfoFileUrl(forItemWithName: name), options: .atomicWrite) } @@ -53,6 +53,7 @@ public final class FileCache { private struct CachedItemInfo: Codable { let fileName: String + let timestamp: TimeInterval } private let encoder = JSONEncoder() diff --git a/Sources/ModelFactories/AuxiliaryPathsFactory.swift b/Sources/ModelFactories/AuxiliaryPathsFactory.swift new file mode 100644 index 00000000..313cea40 --- /dev/null +++ b/Sources/ModelFactories/AuxiliaryPathsFactory.swift @@ -0,0 +1,25 @@ +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 + { + try fileManager.createDirectory(atPath: tempFolder, withIntermediateDirectories: true, attributes: [:]) + + let resolver = ResourceLocationResolver.sharedResolver + let fbxctestPath = try resolver.resolvePathToBinary(resourceLocation: fbxctest, binaryName: "fbxctest") + let fbsimctlPath = try resolver.resolvePathToBinary(resourceLocation: fbsimctl, binaryName: "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..3960659e --- /dev/null +++ b/Sources/ModelFactories/ResourceLocationResolver.swift @@ -0,0 +1,57 @@ +import Extensions +import FileCache +import Foundation +import Models +import URLResource +import ZIPFoundation + +public final class ResourceLocationResolver { + private let fileCache: FileCache + private let urlResource: URLResource + + public enum PathValidationError: Error { + case binaryNotFoundAtPath(String) + } + + public static let sharedResolver: ResourceLocationResolver = { + guard let cachesUrl = try? FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) else { + let pathToBinaryContainer = ProcessInfo.processInfo.arguments[0].deletingLastPathComponent + return ResourceLocationResolver(cachesUrl: URL(fileURLWithPath: pathToBinaryContainer)) + } + return ResourceLocationResolver(cachesUrl: cachesUrl) + }() + + private init(cachesUrl: URL) { + self.fileCache = FileCache(cachesUrl: cachesUrl) + self.urlResource = URLResource(fileCache: fileCache, urlSession: URLSession.shared) + } + + public func resolvePathToBinary(resourceLocation: ResourceLocation, binaryName: String) throws -> String { + let resourceUrl = try resolvePath(resourceLocation: resourceLocation) + let path = resourceUrl.lastPathComponent == binaryName + ? resourceUrl.path + : resourceUrl.appendingPathComponent(binaryName, isDirectory: false).path + guard FileManager.default.fileExists(atPath: path) else { throw PathValidationError.binaryNotFoundAtPath(path) } + return path + } + + public func resolvePath(resourceLocation: ResourceLocation) throws -> URL { + switch resourceLocation { + case .localFilePath(let path): + return URL(fileURLWithPath: path) + case .remoteUrl(let url): + return try cachedContentsOfUrl(url) + } + } + + private func cachedContentsOfUrl(_ url: URL) throws -> URL { + let handler = BlockingHandler() + urlResource.fetchResource(url: url, handler: handler) + let zipUrl = try handler.wait() + let contentsUrl = zipUrl.appendingPathComponent("zip_contents", isDirectory: true) + if !FileManager.default.fileExists(atPath: contentsUrl.path) { + try FileManager.default.unzipItem(at: zipUrl, to: contentsUrl) + } + 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/BlockingHandler.swift b/Sources/URLResource/BlockingHandler.swift index be6bf443..c430ef6d 100644 --- a/Sources/URLResource/BlockingHandler.swift +++ b/Sources/URLResource/BlockingHandler.swift @@ -1,5 +1,6 @@ import Basic import Foundation +import Logging public final class BlockingHandler: Handler { @@ -13,17 +14,19 @@ public final class BlockingHandler: Handler { public init() {} - public func wait(until limit: Date = Date.distantFuture) throws -> URL { + public func wait(until limit: Date = Date().addingTimeInterval(60)) throws -> URL { condition.wait(until: limit) return try result.dematerialize() } public func failedToGetContents(forUrl url: URL, error: Error) { + log("Failed to fetch resource for '\(url)': \(error)") result = Result.failure(HandlerError.failure(error)) condition.signal() } public func resourceUrl(contentUrl: URL, forUrl url: URL) { + log("Obtained resource for '\(url)' at local url: '\(contentUrl)'") result = Result.success(contentUrl) condition.signal() } diff --git a/Sources/URLResource/URLResource.swift b/Sources/URLResource/URLResource.swift index 24899917..69c884e0 100644 --- a/Sources/URLResource/URLResource.swift +++ b/Sources/URLResource/URLResource.swift @@ -1,5 +1,6 @@ import FileCache import Foundation +import Logging public final class URLResource { private let fileCache: FileCache @@ -17,12 +18,14 @@ public final class URLResource { public func fetchResource(url: URL, handler: Handler) { 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) @@ -32,6 +35,7 @@ public final class URLResource { 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) 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/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) From dd6ba84d2216e9a9908e96bfea4c40d0eee66b0f Mon Sep 17 00:00:00 2001 From: Vladislav Alekseev Date: Thu, 6 Sep 2018 17:32:54 +0300 Subject: [PATCH 4/6] MBS-2930: use semaphore --- Package.swift | 4 +-- Sources/AvitoRunner/DistRunTestsCommand.swift | 4 +-- Sources/AvitoRunner/RunTestsCommand.swift | 4 +-- .../ResourceLocationResolver.swift | 35 +++++++++++++------ Sources/URLResource/BlockingHandler.swift | 21 +++++------ Tests/URLResourceTests/URLResourceTests.swift | 1 - 6 files changed, 41 insertions(+), 28 deletions(-) diff --git a/Package.swift b/Package.swift index 2bd557b2..3c9ea581 100644 --- a/Package.swift +++ b/Package.swift @@ -224,8 +224,8 @@ let package = Package( "Extensions", "FileCache", "Models", - "URLResource", - "ZIPFoundation" + "ProcessController", + "URLResource" ]), .target( diff --git a/Sources/AvitoRunner/DistRunTestsCommand.swift b/Sources/AvitoRunner/DistRunTestsCommand.swift index f4df0fab..7f5ba77d 100644 --- a/Sources/AvitoRunner/DistRunTestsCommand.swift +++ b/Sources/AvitoRunner/DistRunTestsCommand.swift @@ -188,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) } diff --git a/Sources/AvitoRunner/RunTestsCommand.swift b/Sources/AvitoRunner/RunTestsCommand.swift index e23185d9..3b15eb17 100644 --- a/Sources/AvitoRunner/RunTestsCommand.swift +++ b/Sources/AvitoRunner/RunTestsCommand.swift @@ -156,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) } diff --git a/Sources/ModelFactories/ResourceLocationResolver.swift b/Sources/ModelFactories/ResourceLocationResolver.swift index 3960659e..c841089b 100644 --- a/Sources/ModelFactories/ResourceLocationResolver.swift +++ b/Sources/ModelFactories/ResourceLocationResolver.swift @@ -1,25 +1,33 @@ import Extensions import FileCache import Foundation +import Logging import Models +import ProcessController import URLResource -import ZIPFoundation public final class ResourceLocationResolver { private let fileCache: FileCache private let urlResource: URLResource + private let fileManager = FileManager() - public enum PathValidationError: Error { + public enum ValidationError: Error { case binaryNotFoundAtPath(String) + case unpackProcessError } - public static let sharedResolver: ResourceLocationResolver = { - guard let cachesUrl = try? FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) else { + 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 - return ResourceLocationResolver(cachesUrl: URL(fileURLWithPath: pathToBinaryContainer)) + cacheContainer = URL(fileURLWithPath: pathToBinaryContainer) } - return ResourceLocationResolver(cachesUrl: cachesUrl) - }() + return cacheContainer.appendingPathComponent("ru.avito.Runner.cache", isDirectory: true) + } private init(cachesUrl: URL) { self.fileCache = FileCache(cachesUrl: cachesUrl) @@ -31,7 +39,7 @@ public final class ResourceLocationResolver { let path = resourceUrl.lastPathComponent == binaryName ? resourceUrl.path : resourceUrl.appendingPathComponent(binaryName, isDirectory: false).path - guard FileManager.default.fileExists(atPath: path) else { throw PathValidationError.binaryNotFoundAtPath(path) } + guard fileManager.fileExists(atPath: path) else { throw ValidationError.binaryNotFoundAtPath(path) } return path } @@ -48,9 +56,14 @@ public final class ResourceLocationResolver { let handler = BlockingHandler() urlResource.fetchResource(url: url, handler: handler) let zipUrl = try handler.wait() - let contentsUrl = zipUrl.appendingPathComponent("zip_contents", isDirectory: true) - if !FileManager.default.fileExists(atPath: contentsUrl.path) { - try FileManager.default.unzipItem(at: zipUrl, to: contentsUrl) + 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/URLResource/BlockingHandler.swift b/Sources/URLResource/BlockingHandler.swift index c430ef6d..04bf9535 100644 --- a/Sources/URLResource/BlockingHandler.swift +++ b/Sources/URLResource/BlockingHandler.swift @@ -1,10 +1,11 @@ import Basic +import Dispatch import Foundation import Logging public final class BlockingHandler: Handler { - private let condition = NSCondition() + private let semaphore = DispatchSemaphore(value: 0) private var result: Result = Result.failure(HandlerError.timeout) public enum HandlerError: Error { @@ -14,20 +15,20 @@ public final class BlockingHandler: Handler { public init() {} - public func wait(until limit: Date = Date().addingTimeInterval(60)) throws -> URL { - condition.wait(until: limit) + 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)) - condition.signal() - } - - public func resourceUrl(contentUrl: URL, forUrl url: URL) { - log("Obtained resource for '\(url)' at local url: '\(contentUrl)'") - result = Result.success(contentUrl) - condition.signal() + semaphore.signal() } } diff --git a/Tests/URLResourceTests/URLResourceTests.swift b/Tests/URLResourceTests/URLResourceTests.swift index 4f4703a4..89353545 100644 --- a/Tests/URLResourceTests/URLResourceTests.swift +++ b/Tests/URLResourceTests/URLResourceTests.swift @@ -30,7 +30,6 @@ final class URLResourceTests: XCTestCase { let expectedContents = "some fetched contents" server?["/get"] = { _ in HttpResponse.ok(.text(expectedContents)) } - let resource = URLResource(fileCache: fileCache, urlSession: URLSession.shared) let handler = BlockingHandler() resource.fetchResource( From bb4482d8fb59cd2e86cb0f666af5c8979033cc71 Mon Sep 17 00:00:00 2001 From: Vladislav Alekseev Date: Thu, 6 Sep 2018 17:51:28 +0300 Subject: [PATCH 5/6] MBS-2930: renaming --- .../AvitoRunner/DumpRuntimeTestsCommand.swift | 12 +++---- .../AuxiliaryPathsFactory.swift | 10 +++--- .../ResourceLocationResolver.swift | 32 +++++++++++-------- ...swift => BlockingURLResourceHandler.swift} | 2 +- Sources/URLResource/URLResource.swift | 2 +- ...Handler.swift => URLResourceHandler.swift} | 2 +- Tests/URLResourceTests/URLResourceTests.swift | 8 ++--- 7 files changed, 37 insertions(+), 31 deletions(-) rename Sources/URLResource/{BlockingHandler.swift => BlockingURLResourceHandler.swift} (93%) rename Sources/URLResource/{Handler.swift => URLResourceHandler.swift} (78%) diff --git a/Sources/AvitoRunner/DumpRuntimeTestsCommand.swift b/Sources/AvitoRunner/DumpRuntimeTestsCommand.swift index 8acc259e..df78a4a4 100644 --- a/Sources/AvitoRunner/DumpRuntimeTestsCommand.swift +++ b/Sources/AvitoRunner/DumpRuntimeTestsCommand.swift @@ -16,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 = { @@ -28,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) } @@ -46,7 +46,7 @@ final class DumpRuntimeTestsCommand: Command { } catch { throw ArgumentsError.argumentValueCannotBeUsed(KnownStringArguments.testDestinations, error) } - guard let fbxctest = arguments.get(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 { @@ -57,12 +57,10 @@ final class DumpRuntimeTestsCommand: Command { } let resolver = ResourceLocationResolver.sharedResolver - let fbxctestPath = try resolver.resolvePathToBinary( - resourceLocation: ResourceLocation.from(fbxctest), - binaryName: "fbxctest") + let fbxctest = try resolver.resolvePath(resourceLocation: ResourceLocation.from(fbxctestValue)).with(archivedFile: "fbxctest") let configuration = RuntimeDumpConfiguration( - fbxctest: fbxctestPath, + fbxctest: fbxctest, xcTestBundle: xcTestBundle, simulatorSettings: SimulatorSettings(simulatorLocalizationSettings: "", watchdogSettings: ""), testDestination: testDestinationConfigurations[0].testDestination, diff --git a/Sources/ModelFactories/AuxiliaryPathsFactory.swift b/Sources/ModelFactories/AuxiliaryPathsFactory.swift index 313cea40..c48ea001 100644 --- a/Sources/ModelFactories/AuxiliaryPathsFactory.swift +++ b/Sources/ModelFactories/AuxiliaryPathsFactory.swift @@ -8,14 +8,16 @@ public final class AuxiliaryPathsFactory { public func createWith( fbxctest: ResourceLocation, fbsimctl: ResourceLocation, - tempFolder: String) + tempFolder: String = "") throws -> AuxiliaryPaths { - try fileManager.createDirectory(atPath: tempFolder, withIntermediateDirectories: true, attributes: [:]) + if !tempFolder.isEmpty { + try fileManager.createDirectory(atPath: tempFolder, withIntermediateDirectories: true, attributes: [:]) + } let resolver = ResourceLocationResolver.sharedResolver - let fbxctestPath = try resolver.resolvePathToBinary(resourceLocation: fbxctest, binaryName: "fbxctest") - let fbsimctlPath = try resolver.resolvePathToBinary(resourceLocation: fbsimctl, binaryName: "fbsimctl") + 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, diff --git a/Sources/ModelFactories/ResourceLocationResolver.swift b/Sources/ModelFactories/ResourceLocationResolver.swift index c841089b..26ee7b14 100644 --- a/Sources/ModelFactories/ResourceLocationResolver.swift +++ b/Sources/ModelFactories/ResourceLocationResolver.swift @@ -16,6 +16,20 @@ public final class ResourceLocationResolver { 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 { @@ -34,26 +48,18 @@ public final class ResourceLocationResolver { self.urlResource = URLResource(fileCache: fileCache, urlSession: URLSession.shared) } - public func resolvePathToBinary(resourceLocation: ResourceLocation, binaryName: String) throws -> String { - let resourceUrl = try resolvePath(resourceLocation: resourceLocation) - let path = resourceUrl.lastPathComponent == binaryName - ? resourceUrl.path - : resourceUrl.appendingPathComponent(binaryName, isDirectory: false).path - guard fileManager.fileExists(atPath: path) else { throw ValidationError.binaryNotFoundAtPath(path) } - return path - } - - public func resolvePath(resourceLocation: ResourceLocation) throws -> URL { + public func resolvePath(resourceLocation: ResourceLocation) throws -> Result { switch resourceLocation { case .localFilePath(let path): - return URL(fileURLWithPath: path) + return Result.directlyAccessibleFile(path: path) case .remoteUrl(let url): - return try cachedContentsOfUrl(url) + let path = try cachedContentsOfUrl(url).path + return Result.contentsOfArchive(folderPath: path) } } private func cachedContentsOfUrl(_ url: URL) throws -> URL { - let handler = BlockingHandler() + let handler = BlockingURLResourceHandler() urlResource.fetchResource(url: url, handler: handler) let zipUrl = try handler.wait() let contentsUrl = zipUrl.deletingLastPathComponent().appendingPathComponent("zip_contents", isDirectory: true) diff --git a/Sources/URLResource/BlockingHandler.swift b/Sources/URLResource/BlockingURLResourceHandler.swift similarity index 93% rename from Sources/URLResource/BlockingHandler.swift rename to Sources/URLResource/BlockingURLResourceHandler.swift index 04bf9535..7a3736ff 100644 --- a/Sources/URLResource/BlockingHandler.swift +++ b/Sources/URLResource/BlockingURLResourceHandler.swift @@ -3,7 +3,7 @@ import Dispatch import Foundation import Logging -public final class BlockingHandler: Handler { +public final class BlockingURLResourceHandler: URLResourceHandler { private let semaphore = DispatchSemaphore(value: 0) private var result: Result = Result.failure(HandlerError.timeout) diff --git a/Sources/URLResource/URLResource.swift b/Sources/URLResource/URLResource.swift index 69c884e0..a6b8415a 100644 --- a/Sources/URLResource/URLResource.swift +++ b/Sources/URLResource/URLResource.swift @@ -15,7 +15,7 @@ public final class URLResource { self.urlSession = urlSession } - public func fetchResource(url: URL, handler: Handler) { + public func fetchResource(url: URL, handler: URLResourceHandler) { if fileCache.contains(itemForURL: url) { do { log("Found already cached resource for url '\(url)'") diff --git a/Sources/URLResource/Handler.swift b/Sources/URLResource/URLResourceHandler.swift similarity index 78% rename from Sources/URLResource/Handler.swift rename to Sources/URLResource/URLResourceHandler.swift index 807fd723..42fcfe53 100644 --- a/Sources/URLResource/Handler.swift +++ b/Sources/URLResource/URLResourceHandler.swift @@ -1,6 +1,6 @@ import Foundation -public protocol Handler { +public protocol URLResourceHandler { func resourceUrl(contentUrl: URL, forUrl url: URL) func failedToGetContents(forUrl url: URL, error: Error) } diff --git a/Tests/URLResourceTests/URLResourceTests.swift b/Tests/URLResourceTests/URLResourceTests.swift index 89353545..9a40968a 100644 --- a/Tests/URLResourceTests/URLResourceTests.swift +++ b/Tests/URLResourceTests/URLResourceTests.swift @@ -31,11 +31,11 @@ final class URLResourceTests: XCTestCase { server?["/get"] = { _ in HttpResponse.ok(.text(expectedContents)) } let resource = URLResource(fileCache: fileCache, urlSession: URLSession.shared) - let handler = BlockingHandler() + let handler = BlockingURLResourceHandler() resource.fetchResource( url: URL(string: "http://localhost:\(serverPort)/get/")!, handler: handler) - let contentUrl = try handler.wait(until: Date().addingTimeInterval(5)) + let contentUrl = try handler.wait(limit: 5) XCTAssertEqual(try String(contentsOf: contentUrl), expectedContents) } @@ -44,10 +44,10 @@ final class URLResourceTests: XCTestCase { server?["/get"] = { _ in HttpResponse.internalServerError } let resource = URLResource(fileCache: fileCache, urlSession: URLSession.shared) - let handler = BlockingHandler() + let handler = BlockingURLResourceHandler() resource.fetchResource( url: URL(string: "http://localhost:\(serverPort)/get/")!, handler: handler) - XCTAssertThrowsError(try handler.wait(until: Date().addingTimeInterval(5))) + XCTAssertThrowsError(try handler.wait(limit: 5)) } } From 96d4bac24f2036f15c8e0f29750c6d1fcc19496c Mon Sep 17 00:00:00 2001 From: Vladislav Alekseev Date: Thu, 6 Sep 2018 18:16:17 +0300 Subject: [PATCH 6/6] MBS-2930: less accuracy in unit test --- Tests/SynchronousWaiterTests/SynchronousWaiterTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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() {