|  | // Copyright 2016 The Tulsi Authors. All rights reserved. | 
|  | // | 
|  | // 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 XCTest | 
|  | @testable import BazelIntegrationTestCase | 
|  | @testable import TulsiGenerator | 
|  |  | 
|  |  | 
|  | // Base class for end-to-end tests that generate xcodeproj bundles and validate them against golden | 
|  | // versions. | 
|  | class EndToEndIntegrationTestCase : BazelIntegrationTestCase { | 
|  | enum Error: Swift.Error { | 
|  | /// A subdirectory for the Xcode project could not be created. | 
|  | case testSubdirectoryNotCreated | 
|  | /// The Xcode project could not be generated. | 
|  | case projectGenerationFailure(String) | 
|  | /// Unable to execute user_build.py script. | 
|  | case userBuildScriptInvocationFailure(String) | 
|  | } | 
|  |  | 
|  | let extraDebugFlags = ["--define=TULSI_TEST=dbg"] | 
|  | let extraReleaseFlags = ["--define=TULSI_TEST=rel"] | 
|  | let fakeBazelURL = URL(fileURLWithPath: "/fake/tulsi_test_bazel", isDirectory: false) | 
|  | let testTulsiVersion = "9.99.999.9999" | 
|  |  | 
|  | final func validateDiff(_ diffLines: [String], file: StaticString = #file, line: UInt = #line) { | 
|  | for diff in diffLines { | 
|  | XCTFail(diff, file: file, line: line) | 
|  | } | 
|  | } | 
|  |  | 
|  | final func diffProjectAt(_ projectURL: URL, | 
|  | againstGoldenProject resourceName: String, | 
|  | file: StaticString = #file, | 
|  | line: UInt = #line) -> [String] { | 
|  | guard let hashing = ProcessInfo.processInfo.environment["SWIFT_DETERMINISTIC_HASHING"], | 
|  | hashing == "1" else { | 
|  | XCTFail("Must define environment variable \"SWIFT_DETERMINISTIC_HASHING=1\", or golden tests will fail.") | 
|  | return [] | 
|  | } | 
|  | let bundle = Bundle(for: type(of: self)) | 
|  | let goldenProjectURL = workspaceRootURL.appendingPathComponent(fakeBazelWorkspace | 
|  | .resourcesPathBase, | 
|  | isDirectory: true) | 
|  | .appendingPathComponent("GoldenProjects/\(resourceName).xcodeproj", isDirectory: true) | 
|  | guard FileManager.default.fileExists(atPath: goldenProjectURL.path) else { | 
|  | assertionFailure("Missing required test resource file \(resourceName).xcodeproj") | 
|  | XCTFail("Missing required test resource file \(resourceName).xcodeproj", | 
|  | file: file, | 
|  | line: line) | 
|  | return [] | 
|  | } | 
|  |  | 
|  | var diffOutput = [String]() | 
|  | let semaphore = DispatchSemaphore(value: 0) | 
|  | let process = ProcessRunner.createProcess("/usr/bin/diff", | 
|  | arguments: ["-r", | 
|  | // For the sake of simplicity in | 
|  | // maintaining the golden data, copied | 
|  | // Tulsi artifacts are assumed to have | 
|  | // been installed correctly. | 
|  | "--exclude=.tulsi", | 
|  | projectURL.path, | 
|  | goldenProjectURL.path]) { | 
|  | completionInfo in | 
|  | defer { | 
|  | semaphore.signal() | 
|  | } | 
|  | if let stdout = NSString(data: completionInfo.stdout, encoding: String.Encoding.utf8.rawValue) { | 
|  | diffOutput = stdout.components(separatedBy: "\n").filter({ !$0.isEmpty }) | 
|  | } else { | 
|  | XCTFail("No output received for diff command", file: file, line: line) | 
|  | } | 
|  | } | 
|  | process.currentDirectoryPath = workspaceRootURL.path | 
|  | process.launch() | 
|  |  | 
|  | _ = semaphore.wait(timeout: DispatchTime.distantFuture) | 
|  | return diffOutput | 
|  | } | 
|  |  | 
|  | final func copyOutput(source: URL, outputDir: String) throws { | 
|  | if testUndeclaredOutputsDir != nil { | 
|  | guard let testOutputURL = makeTestSubdirectory(outputDir, | 
|  | rootDirectory: testUndeclaredOutputsDir, | 
|  | cleanupOnTeardown: false) else { | 
|  | throw Error.testSubdirectoryNotCreated | 
|  | } | 
|  | let testOutputProjURL = testOutputURL.appendingPathComponent(source.lastPathComponent) | 
|  | if FileManager.default.fileExists(atPath: testOutputProjURL.path) { | 
|  | try FileManager.default.removeItem(at: testOutputProjURL) | 
|  | } | 
|  | try FileManager.default.copyItem(at: source, to: testOutputProjURL) | 
|  | } | 
|  | } | 
|  |  | 
|  | final func validateBuildCommandForProject(_ projectURL: URL, | 
|  | swift: Bool = false, | 
|  | options: TulsiOptionSet = TulsiOptionSet(), | 
|  | targets: [String]) throws { | 
|  | let actualDebug = try userBuildCommandForProject(projectURL, release: false, targets: targets) | 
|  | let actualRelease = try userBuildCommandForProject(projectURL, release: true, targets: targets) | 
|  | let (debug, release) = expectedBuildCommands(swift: swift, options: options, targets: targets) | 
|  |  | 
|  | XCTAssertEqual(actualDebug, debug) | 
|  | XCTAssertEqual(actualRelease, release) | 
|  | } | 
|  |  | 
|  | final func expectedBuildCommands(swift: Bool, | 
|  | options: TulsiOptionSet, | 
|  | targets: [String]) -> (String, String) { | 
|  | let provider = BazelSettingsProvider(universalFlags: bazelUniversalFlags) | 
|  | let features = BazelBuildSettingsFeatures.enabledFeatures(options: options) | 
|  | let dbg = provider.tulsiFlags(hasSwift: swift, options: options, features: features).debug | 
|  | let rel = provider.tulsiFlags(hasSwift: swift, options: options, features: features).release | 
|  |  | 
|  | let config: PlatformConfiguration | 
|  | if let identifier = options[.ProjectGenerationPlatformConfiguration].commonValue, | 
|  | let parsedConfig = PlatformConfiguration(identifier: identifier) { | 
|  | config = parsedConfig | 
|  | } else { | 
|  | config = PlatformConfiguration.defaultConfiguration | 
|  | } | 
|  |  | 
|  | func buildCommand(extraBuildFlags: [String], tulsiFlags: BazelFlags) -> String { | 
|  | var args = [fakeBazelURL.path] | 
|  | args.append(contentsOf: bazelStartupOptions) | 
|  | args.append(contentsOf: tulsiFlags.startup) | 
|  | args.append("build") | 
|  | args.append(contentsOf: extraBuildFlags) | 
|  | args.append(contentsOf: bazelBuildOptions) | 
|  | args.append(contentsOf: config.bazelFlags) | 
|  | args.append(contentsOf: tulsiFlags.build) | 
|  | args.append("--tool_tag=tulsi:user_build") | 
|  | args.append(contentsOf: targets) | 
|  |  | 
|  | return args.map { $0.escapingForShell }.joined(separator: " ") | 
|  | } | 
|  |  | 
|  | let debugCommand = buildCommand(extraBuildFlags: extraDebugFlags, tulsiFlags: dbg) | 
|  | let releaseCommand = buildCommand(extraBuildFlags: extraReleaseFlags, tulsiFlags: rel) | 
|  |  | 
|  | return (debugCommand, releaseCommand) | 
|  | } | 
|  |  | 
|  | final func userBuildCommandForProject(_ projectURL: URL, | 
|  | release: Bool = false, | 
|  | targets: [String], | 
|  | file: StaticString = #file, | 
|  | line: UInt = #line) throws -> String { | 
|  | let expectedScriptURL = projectURL.appendingPathComponent(".tulsi/Scripts/user_build.py", | 
|  | isDirectory: false) | 
|  | let fileManager = FileManager.default | 
|  |  | 
|  | guard fileManager.fileExists(atPath: expectedScriptURL.path) else { | 
|  | throw Error.userBuildScriptInvocationFailure( | 
|  | "user_build.py script not found: expected at path \(expectedScriptURL.path)") | 
|  | } | 
|  |  | 
|  | var output = "<none>" | 
|  | let semaphore = DispatchSemaphore(value: 0) | 
|  | var args = [ | 
|  | "--norun", | 
|  | ] | 
|  | if release { | 
|  | args.append("--release") | 
|  | } | 
|  | args.append(contentsOf: targets) | 
|  |  | 
|  | let process = ProcessRunner.createProcess( | 
|  | expectedScriptURL.path, | 
|  | arguments: args, | 
|  | messageLogger: localizedMessageLogger | 
|  | ) { completionInfo in | 
|  | defer { | 
|  | semaphore.signal() | 
|  | } | 
|  | let exitcode = completionInfo.terminationStatus | 
|  | guard exitcode == 0 else { | 
|  | let stderr = | 
|  | String(data: completionInfo.stderr, encoding: .utf8) ?? "<no stderr>" | 
|  | XCTFail("user_build.py returned \(exitcode). stderr: \(stderr)", file: file, line: line) | 
|  | return | 
|  | } | 
|  | if let stdout = String(data: completionInfo.stdout, encoding: .utf8)?.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines), | 
|  | !stdout.isEmpty { | 
|  | output = stdout | 
|  | } else { | 
|  | let stderr = | 
|  | String(data: completionInfo.stderr, encoding: .utf8) ?? "<no stderr>" | 
|  | XCTFail("user_build.py had no stdout. stderr: \(stderr)", file: file, line: line) | 
|  | } | 
|  | } | 
|  | process.currentDirectoryPath = workspaceRootURL.path | 
|  | process.launch() | 
|  |  | 
|  | _ = semaphore.wait(timeout: DispatchTime.distantFuture) | 
|  | return output | 
|  | } | 
|  |  | 
|  | final func generateProjectNamed(_ projectName: String, | 
|  | buildTargets: [RuleInfo], | 
|  | pathFilters: [String], | 
|  | additionalFilePaths: [String] = [], | 
|  | outputDir: String, | 
|  | options: TulsiOptionSet = TulsiOptionSet()) throws -> URL { | 
|  | if !bazelStartupOptions.isEmpty { | 
|  | let startupFlags = bazelStartupOptions.joined(separator: " ") | 
|  | options[.BazelBuildStartupOptionsDebug].projectValue = startupFlags | 
|  | options[.BazelBuildStartupOptionsRelease].projectValue = startupFlags | 
|  | } | 
|  |  | 
|  | let debugBuildOptions = extraDebugFlags + bazelBuildOptions | 
|  | let releaseBuildOptions = extraReleaseFlags + bazelBuildOptions | 
|  |  | 
|  | options[.BazelBuildOptionsDebug].projectValue = debugBuildOptions.joined(separator: " ") | 
|  | options[.BazelBuildOptionsRelease].projectValue = releaseBuildOptions.joined(separator: " ") | 
|  |  | 
|  | let bazelURLParam = TulsiParameter(value: fakeBazelURL, source: .explicitlyProvided) | 
|  | let config = TulsiGeneratorConfig(projectName: projectName, | 
|  | buildTargets: buildTargets, | 
|  | pathFilters: Set<String>(pathFilters), | 
|  | additionalFilePaths: additionalFilePaths, | 
|  | options: options, | 
|  | bazelURL: bazelURLParam) | 
|  |  | 
|  | guard let outputFolderURL = makeXcodeProjPath(outputDir) else { | 
|  | throw Error.testSubdirectoryNotCreated | 
|  | } | 
|  |  | 
|  | let projectGenerator = TulsiXcodeProjectGenerator(workspaceRootURL: workspaceRootURL, | 
|  | config: config, | 
|  | extractorBazelURL: bazelURL, | 
|  | tulsiVersion: testTulsiVersion) | 
|  | // Bazel built-in preprocessor defines are suppressed in order to prevent any | 
|  | // environment-dependent variables from mismatching the golden data. | 
|  | projectGenerator.xcodeProjectGenerator.suppressCompilerDefines = true | 
|  | // Don't update shell command utilities. | 
|  | projectGenerator.xcodeProjectGenerator.suppressUpdatingShellCommands = true | 
|  | // The username is forced to a known value. | 
|  | projectGenerator.xcodeProjectGenerator.usernameFetcher = { "_TEST_USER_" } | 
|  | // The workspace symlink is forced to a known value. | 
|  | projectGenerator.xcodeProjectGenerator.redactWorkspaceSymlink = true | 
|  | let errorInfo: String | 
|  | do { | 
|  | let generatedProjURL = try projectGenerator.generateXcodeProjectInFolder(outputFolderURL) | 
|  | try copyOutput(source: generatedProjURL, outputDir: outputDir) | 
|  | return generatedProjURL | 
|  | } catch TulsiXcodeProjectGenerator.GeneratorError.unsupportedTargetType(let targetType) { | 
|  | errorInfo = "Unsupported target type: \(targetType)" | 
|  | } catch TulsiXcodeProjectGenerator.GeneratorError.serializationFailed(let details) { | 
|  | errorInfo = "General failure: \(details)" | 
|  | } catch let error { | 
|  | errorInfo = "Unexpected failure: \(error)" | 
|  | } | 
|  | throw Error.projectGenerationFailure(errorInfo) | 
|  | } | 
|  | } |