Black box end-to-end test to generate Buttons.
Generates an xcodeproj for Buttons and runs tests to ensure project works.

PiperOrigin-RevId: 212677610
diff --git a/Tulsi.tulsiproj/Configs/Tulsi.tulsigen b/Tulsi.tulsiproj/Configs/Tulsi.tulsigen
index 8f8f69b..4d8c99a 100644
--- a/Tulsi.tulsiproj/Configs/Tulsi.tulsigen
+++ b/Tulsi.tulsiproj/Configs/Tulsi.tulsigen
@@ -3,6 +3,7 @@
     "src/..."
   ],
   "buildTargets" : [
+    "//src/TulsiEndToEndTests:TulsiEndToEndTest",
     "//src/TulsiGeneratorIntegrationTests:AspectTests",
     "//src/TulsiGeneratorIntegrationTests:EndToEndGenerationTests",
     "//src/TulsiGeneratorIntegrationTests:PlatformDependentEndToEndGenerationTests",
diff --git a/src/TulsiEndToEndTests/BUILD b/src/TulsiEndToEndTests/BUILD
new file mode 100644
index 0000000..cbe6f7c
--- /dev/null
+++ b/src/TulsiEndToEndTests/BUILD
@@ -0,0 +1,17 @@
+licenses(["notice"])  # Apache 2.0
+
+load("//src/TulsiGeneratorIntegrationTests:tulsi_integration_test.bzl", "tulsi_integration_test")
+
+test_suite(
+    name = "TulsiEndToEndTests",
+)
+
+tulsi_integration_test(
+    name = "TulsiEndToEndTest",
+    srcs = ["TulsiEndToEndTest.swift"],
+    data = [
+        "Resources/Buttons.tulsiproj",
+        "//:tulsi.zip",
+        "@build_bazel_rules_apple//examples/multi_platform/Buttons:all_files",
+    ],
+)
diff --git a/src/TulsiEndToEndTests/Resources/Buttons.tulsiproj/Configs/Buttons.tulsigen b/src/TulsiEndToEndTests/Resources/Buttons.tulsiproj/Configs/Buttons.tulsigen
new file mode 100644
index 0000000..a1ec320
--- /dev/null
+++ b/src/TulsiEndToEndTests/Resources/Buttons.tulsiproj/Configs/Buttons.tulsigen
@@ -0,0 +1,50 @@
+{
+  "sourceFilters" : [
+  ],
+  "buildTargets" : [
+    "\/\/build_bazel_rules_apple/examples\/multi_platform\/Buttons:ButtonsTests",
+    "\/\/build_bazel_rules_apple/examples\/multi_platform\/Buttons:ButtonsUITests"
+  ],
+  "projectName" : "Buttons",
+  "optionSet" : {
+    "BazelBuildOptionsDebug" : {
+      "p" : "$(inherited)"
+    },
+    "BazelBuildStartupOptionsRelease" : {
+      "p" : "$(inherited)"
+    },
+    "LaunchActionPreActionScript" : {
+      "p" : "$(inherited)"
+    },
+    "BazelBuildOptionsRelease" : {
+      "p" : "$(inherited)"
+    },
+    "EnvironmentVariables" : {
+      "p" : "$(inherited)"
+    },
+    "BuildActionPreActionScript" : {
+      "p" : "$(inherited)"
+    },
+    "CommandlineArguments" : {
+      "p" : "$(inherited)"
+    },
+    "TestActionPreActionScript" : {
+      "p" : "$(inherited)"
+    },
+    "TestActionPostActionScript" : {
+      "p" : "$(inherited)"
+    },
+    "BuildActionPostActionScript" : {
+      "p" : "$(inherited)"
+    },
+    "BazelBuildStartupOptionsDebug" : {
+      "p" : "$(inherited)"
+    },
+    "LaunchActionPostActionScript" : {
+      "p" : "$(inherited)"
+    }
+  },
+  "additionalFilePaths" : [
+    "build_bazel_rules_apple/examples\/multi_platform\/Buttons\/BUILD"
+  ]
+}
diff --git a/src/TulsiEndToEndTests/Resources/Buttons.tulsiproj/project.tulsiconf b/src/TulsiEndToEndTests/Resources/Buttons.tulsiproj/project.tulsiconf
new file mode 100644
index 0000000..339f649
--- /dev/null
+++ b/src/TulsiEndToEndTests/Resources/Buttons.tulsiproj/project.tulsiconf
@@ -0,0 +1,17 @@
+{
+  "configDefaults" : {
+    "optionSet" : {
+      "BazelBuildOptionsDebug" : {
+        "p" : "--ios_minimum_os=9.0 --workspace_status_command=/usr/bin/true"
+      },
+      "BazelBuildOptionsRelease" : {
+        "p" : "--ios_minimum_os=9.0 --workspace_status_command=/usr/bin/true"
+      }
+    }
+  },
+  "projectName" : "Buttons",
+  "packages" : [
+    "build_bazel_rules_apple/examples\/multi_platform\/Buttons"
+  ],
+  "workspaceRoot" : "..\/..\/..\/..\/..\/.."
+}
diff --git a/src/TulsiEndToEndTests/TulsiEndToEndTest.swift b/src/TulsiEndToEndTests/TulsiEndToEndTest.swift
new file mode 100644
index 0000000..c43091c
--- /dev/null
+++ b/src/TulsiEndToEndTests/TulsiEndToEndTest.swift
@@ -0,0 +1,177 @@
+
+// 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
+
+
+// End to end tests that generate an xcodeproj with the Tulsi binary and runs tests on the xcodeproj
+// to verify it was generated correctly.
+class TulsiEndToEndTest: BazelIntegrationTestCase {
+  let fileManager = FileManager.default
+
+  override func setUp() {
+    super.setUp()
+
+    let runfilesWorkspaceURL = fakeBazelWorkspace.runfilesWorkspaceURL
+
+    // Takes a short path to data files and adds them to the correct location in the workspace.
+    func copyDataToFakeWorkspace(_ path: String) -> Bool {
+      let sourceURL = runfilesWorkspaceURL.appendingPathComponent(path, isDirectory: true)
+      let destURL = workspaceRootURL.appendingPathComponent(path, isDirectory: true)
+      do {
+        if fileManager.fileExists(atPath: destURL.path) {
+          try fileManager.removeItem(at: destURL)
+        }
+        // Symlinks cause issues with Tulsi and Storyboards so must deep copy any data files.
+        try fileManager.deepCopyItem(at: sourceURL, to: destURL)
+        return true
+      } catch let e as NSError {
+        print(e.localizedDescription)
+        return false
+      }
+    }
+
+    if (!copyDataToFakeWorkspace("build_bazel_rules_apple/examples/multi_platform/Buttons")) {
+      XCTFail("Failed to copy Buttons files to fake execroot.")
+    }
+
+    if (!copyDataToFakeWorkspace("src/TulsiEndToEndTests/Resources")) {
+      XCTFail("Failed to copy Buttons tulsiproj to fake execroot.")
+    }
+
+    // Unzip the Tulsi.app bundle to the temp space.
+    let semaphore = DispatchSemaphore(value: 0)
+    let tulsiZipPath = "tulsi.zip"
+    let tulsiZipURL = runfilesWorkspaceURL.appendingPathComponent(tulsiZipPath, isDirectory: false)
+    let process = TulsiProcessRunner.createProcess("/usr/bin/unzip",
+                                                   arguments: [tulsiZipURL.path,
+                                                               "-d",
+                                                               workspaceRootURL.path]) {
+      completionInfo in
+        if let error = String(data: completionInfo.stderr, encoding: .utf8), !error.isEmpty {
+          XCTFail(error)
+        }
+        semaphore.signal()
+    }
+    process.launch()
+    _ = semaphore.wait(timeout: DispatchTime.distantFuture)
+  }
+
+  func generateXcodeProject(tulsiProject path: String, config: String) -> URL{
+    let tulsiBinURL = workspaceRootURL.appendingPathComponent("Tulsi.app/Contents/MacOS/Tulsi", isDirectory: false)
+    XCTAssert(fileManager.fileExists(atPath: tulsiBinURL.path), "Tulsi binary is missing.")
+
+    let projectURL = workspaceRootURL.appendingPathComponent(path, isDirectory: true)
+    XCTAssert(fileManager.fileExists(atPath: projectURL.path), "Tulsi project is missing.")
+    let configPath = projectURL.path + ":" + config
+
+    // Generate Xcode project with Tulsi.
+    let semaphore = DispatchSemaphore(value: 0)
+    let process = TulsiProcessRunner.createProcess(tulsiBinURL.path,
+                                                   arguments: ["--",
+                                                               "--genconfig",
+                                                               configPath,
+                                                               "--outputfolder",
+                                                               workspaceRootURL.path,
+                                                               "--bazel",
+                                                               bazelURL.path,
+                                                               "--no-open-xcode"]) {
+      completionInfo in
+        if let stdoutput = String(data: completionInfo.stdout, encoding: .utf8) {
+          print(stdoutput)
+        }
+        if let erroutput = String(data: completionInfo.stderr, encoding: .utf8) {
+          print(erroutput)
+        }
+        semaphore.signal()
+    }
+    process.launch()
+    _ = semaphore.wait(timeout: DispatchTime.distantFuture)
+
+    let filename = TulsiGeneratorConfig.sanitizeFilename("\(config).xcodeproj")
+    let xcodeProjectURL = workspaceRootURL.appendingPathComponent(filename, isDirectory: true)
+    return xcodeProjectURL
+  }
+
+  func testXcodeProject(_ xcodeProjectURL: URL, scheme: String) {
+    // Run Xcode tests.
+    let semaphore = DispatchSemaphore(value: 0)
+    let xcodeTest = TulsiProcessRunner.createProcess("/usr/bin/xcodebuild",
+                                                       arguments: ["test",
+                                                                   "-project",
+                                                                   xcodeProjectURL.path,
+                                                                   "-scheme",
+                                                                   scheme,
+                                                                   "-destination",
+                                                                   "platform=iOS Simulator,name=iPhone 8,OS=11.2"]) {
+      completionInfo in
+        if let stdoutput = String(data: completionInfo.stdout, encoding: .utf8),
+          let result = stdoutput.split(separator: "\n").last {
+          XCTAssertEqual(String(result), "** TEST SUCCEEDED **", "xcodebuild did not return test success.")
+        } else if let error = String(data: completionInfo.stderr, encoding: .utf8), !error.isEmpty {
+          XCTFail(error)
+        } else {
+          XCTFail("Xcode project tests did not return  success.")
+        }
+        semaphore.signal()
+    }
+    xcodeTest.launch()
+    _ = semaphore.wait(timeout: DispatchTime.distantFuture)
+  }
+
+  func testButtons() throws {
+    let buttonsProjectPath = "src/TulsiEndToEndTests/Resources/Buttons.tulsiproj"
+    let xcodeProjectURL = generateXcodeProject(tulsiProject: buttonsProjectPath,
+                                               config: "Buttons")
+    XCTAssert(fileManager.fileExists(atPath: xcodeProjectURL.path), "Xcode project was not generated.")
+    testXcodeProject(xcodeProjectURL, scheme: "ButtonsTests")
+  }
+
+  func testInvalidConfig() throws {
+    let buttonsProjectPath = "src/TulsiEndToEndTests/Resources/Buttons.tulsiproj"
+    let xcodeProjectURL = generateXcodeProject(tulsiProject: buttonsProjectPath,
+                                               config: "InvalidConfig")
+    XCTAssertFalse(fileManager.fileExists(atPath: xcodeProjectURL.path), "Xcode project was generated despite invalid config.")
+  }
+}
+
+extension FileManager {
+  // Performs a deep copy of the item at sourceURL, resolving any symlinks along the way.
+  func deepCopyItem(at sourceURL: URL, to destURL: URL) throws {
+    do {
+      try self.createDirectory(atPath: destURL.deletingLastPathComponent().path, withIntermediateDirectories: true)
+      try self.copyItem(at: sourceURL, to: destURL)
+
+      let path = destURL.path
+      if let paths = self.subpaths(atPath: path) {
+        for subpath in paths {
+          let fullSubpath = path + "/" + subpath
+          if let attributes = try? self.attributesOfItem(atPath: fullSubpath) {
+            // If a file is a symbolic link, find the original file, remove the symlink, and copy
+            // over the original file.
+            if attributes[FileAttributeKey.type] as? FileAttributeType == FileAttributeType.typeSymbolicLink {
+              let resolvedPath = try self.destinationOfSymbolicLink(atPath: fullSubpath)
+              try self.removeItem(atPath: fullSubpath)
+              try self.copyItem(atPath: resolvedPath, toPath: fullSubpath)
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
diff --git a/src/TulsiGenerator/TulsiApplicationSupport.swift b/src/TulsiGenerator/TulsiApplicationSupport.swift
index 0bf9b82..1208c35 100644
--- a/src/TulsiGenerator/TulsiApplicationSupport.swift
+++ b/src/TulsiGenerator/TulsiApplicationSupport.swift
@@ -20,6 +20,10 @@
   let tulsiFolder: URL
 
   init?(fileManager: FileManager = .default) {
+    // Fail if we are running in a test so that we don't install files to ~/Library/Application Support.
+    if ProcessInfo.processInfo.environment["TEST_SRCDIR"] != nil {
+      return nil
+    }
     /// Fetching the appName this way will result in failure for our tests, which is intentional as
     /// we don't want to install files to ~/Library/Application Support when testing.
     guard let appName = Bundle.main.infoDictionary?["CFBundleName"] as? String else { return nil }
diff --git a/src/TulsiGeneratorIntegrationTests/BUILD b/src/TulsiGeneratorIntegrationTests/BUILD
index 7c54018..706166e 100644
--- a/src/TulsiGeneratorIntegrationTests/BUILD
+++ b/src/TulsiGeneratorIntegrationTests/BUILD
@@ -17,6 +17,7 @@
     resources = [
         "//:strings",
     ],
+    visibility = ["//:__subpackages__"],
     deps = ["//src/TulsiGenerator:tulsi_generator_lib"],
 )
 
diff --git a/src/TulsiGeneratorIntegrationTests/BazelFakeWorkspace.swift b/src/TulsiGeneratorIntegrationTests/BazelFakeWorkspace.swift
index 532f610..907b0bb 100644
--- a/src/TulsiGeneratorIntegrationTests/BazelFakeWorkspace.swift
+++ b/src/TulsiGeneratorIntegrationTests/BazelFakeWorkspace.swift
@@ -19,6 +19,7 @@
 class BazelFakeWorkspace {
   let resourcesPathBase = "src/TulsiGeneratorIntegrationTests/Resources"
   var runfilesURL: URL
+  var runfilesWorkspaceURL: URL
   var fakeExecroot: URL
   var workspaceRootURL: URL
   var bazelURL: URL
@@ -26,6 +27,7 @@
 
   init(runfilesURL: URL, tempDirURL: URL) {
     self.runfilesURL = runfilesURL
+    self.runfilesWorkspaceURL = runfilesURL.appendingPathComponent("__main__", isDirectory: true)
     self.fakeExecroot = tempDirURL.appendingPathComponent("fake_execroot", isDirectory: true)
     self.workspaceRootURL = fakeExecroot.appendingPathComponent("__main__", isDirectory: true)
     self.bazelURL = BazelLocator.bazelURL!