Sources/Mockolo/Executor.swift (161 lines of code) (raw):

// // Copyright (c) 2018. Uber Technologies // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // import Foundation import MockoloFramework import ArgumentParser struct Executor: ParsableCommand { static var configuration = CommandConfiguration(commandName: "mockolo", abstract: "Mockolo: Swift mock generator.") let defaultTimeout: Int // MARK: - Private @Flag(name: .long, help: "If set, generated *CallCount vars will be allowed to set manually.") private var allowSetCallCount: Bool = false @Option(help: "A custom annotation string used to indicate if a type should be mocked (default = @mockable).") private var annotation: String = String.mockAnnotation @Option(name: [.customShort("j"), .long], help: ArgumentHelp( "Maximum number of threads to execute concurrently (default = number of cores on the running machine).", valueName: "n")) private var concurrencyLimit: Int? @Option(name: [.customShort("c"), .long], parsing: .upToNextOption, help: "If set, custom module imports (separated by a space) will be added to the final import statement list.") private var customImports: [String] = [] @Flag(name: .long, help: "Whether to enable args history for all functions (default = false). To enable history per function, use the 'history' keyword in the annotation argument.") private var enableArgsHistory: Bool = false @Flag(name: .long, help: "Whether to disable generating Combine streams in mocks (default = false). Set this to true to control how your streams are created in your mocks.") private var disableCombineDefaultValues: Bool = false @Option(name: .long, parsing: .upToNextOption, help: "If set, listed modules (separated by a space) will be excluded from the import statements in the mock output.") private var excludeImports: [String] = [] @Option(name: [.customShort("x"), .customLong("exclude-suffixes")], parsing: .upToNextOption, help: "List of filename suffix(es) without the file extensions to exclude from parsing (separated by a space).", completion: .file()) private var exclusionSuffixes: [String] = [] @Option(name: .long, help: "A custom header documentation to be added to the beginning of a generated mock file.") private var header: String? private static let validLoggingLevels = [0, 1, 2, 3] @Option(name: [.short, .long], help: ArgumentHelp( "The logging level to use. Default is set to 0 (info only). Set 1 for verbose, 2 for warning, and 3 for error.", valueName: "n")) private var loggingLevel: Int = 0 @Option(help: "If set, #if [macro] / #endif will be added to the generated mock file content to guard compilation.") private var macro: String? @Flag(name: .long, help: "If set, it will mock all types (protocols and classes) with a mock annotation (default is set to false and only mocks protocols with a mock annotation).") private var mockAll: Bool = false @Option(name: .customLong("mock-filelist"), help: "Path to a file containing a list of dependent files (separated by a new line) of modules this target depends on.", completion: .file()) private var mockFileList: String? @Flag(name: .long, help: "If set, generated mock classes will have the 'final' attributes (default is set to false).") private var mockFinal: Bool = false @Option(name: [.customLong("mocks", withSingleDash: true), .customLong("mockfiles")], parsing: .upToNextOption, help: "List of mock files (separated by a space) from modules this target depends on. If the --mock-filelist value exists, this will be ignored.", completion: .file()) private var mockFilePaths: [String] = [] @Option(name: [.customShort("d"), .customLong("destination")], help: "Output file path containing the generated Swift mock classes. If no value is given, the program will exit.", completion: .file()) private var outputFilePath: String @Option(name: [.customShort("s"), .customLong("sourcedirs")], parsing: .upToNextOption, help: "Paths to the directories containing source files to generate mocks for (separated by a space). If the --filelist or --sourcefiles values exist, they will be ignored.", completion: .file()) private var sourceDirs: [String] = [] @Option(name: [.customShort("f"), .customLong("filelist")], help: "Path to a file containing a list of source file paths (delimited by a new line). If the --sourcedirs value exists, this will be ignored.", completion: .file()) private var sourceFileList: String? @Option(name: [.customLong("srcs", withSingleDash: true), .customLong("sourcefiles")], parsing: .upToNextOption, help: "List of source files (separated by a space) to generate mocks for. If the --sourcedirs or --filelist value exists, this will be ignored.", completion: .file()) private var sourceFiles: [String] = [] @Option(name: [.long, .customShort("i")], parsing: .upToNextOption, help: "If set, @testable import statements will be added for each module name in this list (separated by a space).") private var testableImports: [String] = [] @Flag(name: .long, help: "If set, a common template function will be called from all functions in mock classes (default is set to false).") private var useTemplateFunc: Bool = false init() { self.defaultTimeout = 20 } private func fullPath(_ path: String) -> String { if path.hasPrefix("/") { return path } if path.hasPrefix("~") { let home = FileManager.default.homeDirectoryForCurrentUser.path return path.replacingOccurrences(of: "~", with: home, range: path.range(of: "~")) } return FileManager.default.currentDirectoryPath + "/" + path } mutating func validate() throws { guard Executor.validLoggingLevels.contains(loggingLevel) else { throw ValidationError("Please specify a valid logging level in the range: \(Executor.validLoggingLevels)") } srcDirs = self.sourceDirs.map(fullPath) // If source file list exists, source files value will be overridden (see the usage in setupArguments above) if let srcList = sourceFileList { let text = try? String(contentsOfFile: srcList, encoding: String.Encoding.utf8) srcs = text?.components(separatedBy: "\n").filter{!$0.isEmpty}.map(fullPath) ?? [] } else { srcs = sourceFiles.map(fullPath) } if srcDirs.isEmpty && srcs.isEmpty { throw ValidationError("Missing source files or directories") } } // Source paths to be used in `run` private var srcDirs: [String] = [] private var srcs: [String] = [] mutating func run() throws { let shouldOutputRunStatusMessages: Bool = loggingLevel < 3 if shouldOutputRunStatusMessages { print("Start...") } defer { if shouldOutputRunStatusMessages { print("Done.") } } let outputFilePath = fullPath(self.outputFilePath) var mockFilePaths: [String]? // First see if a list of mock files are stored in a file if let mockList = self.mockFileList { let text = try? String(contentsOfFile: mockList, encoding: String.Encoding.utf8) mockFilePaths = text?.components(separatedBy: "\n").filter{!$0.isEmpty}.map(fullPath) } else { // If not, see if a list of mock files are directly passed in mockFilePaths = self.mockFilePaths.map(fullPath) } do { try generate(sourceDirs: srcDirs, sourceFiles: srcs, parser: SourceParser(), exclusionSuffixes: exclusionSuffixes, mockFilePaths: mockFilePaths, annotation: annotation, header: header, macro: macro, declType: mockAll ? .all : .protocolType, useTemplateFunc: useTemplateFunc, allowSetCallCount: allowSetCallCount, enableFuncArgsHistory: enableArgsHistory, disableCombineDefaultValues: disableCombineDefaultValues, mockFinal: mockFinal, testableImports: testableImports, customImports: customImports, excludeImports: excludeImports, to: outputFilePath, loggingLevel: loggingLevel, concurrencyLimit: concurrencyLimit) log("Done. Exiting program.", level: .info) } catch { fatalError("Generation error: \(error)") } } }