Files
SwiftLint/Tests/SwiftLintFrameworkTests/IntegrationTests.swift
Elliott WilliamsandJP Simard 3d5239429e Add AnyCollectingRule and isCollecting
All CollectingRules implement AnyCollectingRule, which is used to check
whether a linter will perform any collections and only print the
"Collecting" log message if so.
2019-07-18 14:59:42 -07:00

287 lines
12 KiB
Swift
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
import SourceKittenFramework
@testable import SwiftLintFramework
import XCTest
private let config: Configuration = {
let directory = #file.bridge()
.deletingLastPathComponent.bridge()
.deletingLastPathComponent.bridge()
.deletingLastPathComponent
_ = FileManager.default.changeCurrentDirectoryPath(directory)
return Configuration(path: Configuration.fileName)
}()
class IntegrationTests: XCTestCase {
func testSwiftLintLints() {
// This is as close as we're ever going to get to a self-hosting linter.
let swiftFiles = config.lintableFiles(inPath: "", forceExclude: false)
XCTAssert(swiftFiles.contains(where: { #file == $0.path }), "current file should be included")
let storage = RuleStorage()
let violations = swiftFiles.parallelFlatMap {
Linter(file: $0, configuration: config).collect(into: storage).styleViolations(using: storage)
}
violations.forEach { violation in
violation.location.file!.withStaticString {
XCTFail(violation.reason, file: $0, line: UInt(violation.location.line!))
}
}
}
func testSwiftLintAutoCorrects() {
let swiftFiles = config.lintableFiles(inPath: "", forceExclude: false)
let storage = RuleStorage()
let corrections = swiftFiles.parallelFlatMap {
Linter(file: $0, configuration: config).collect(into: storage).correct(using: storage)
}
for correction in corrections {
correction.location.file!.withStaticString {
XCTFail(correction.ruleDescription.description,
file: $0, line: UInt(correction.location.line!))
}
}
}
func testSimulateHomebrewTest() {
// Since this test uses the `swiftlint` binary built while building `SwiftLintPackageTests`,
// we run it only on macOS using SwiftPM.
#if os(macOS) && SWIFT_PACKAGE
let keyName = "SWIFTLINT_FRAMEWORK_TEST_ENABLE_SIMULATE_HOMEBREW_TEST"
guard ProcessInfo.processInfo.environment[keyName] != nil else {
print("""
Skipping the opt-in test `\(#function)`.
Set the `\(keyName)` environment variable to test `\(#function)`.
""")
return
}
guard let swiftlintURL = swiftlintBuiltBySwiftPM(),
let (testSwiftURL, seatbeltURL) = prepareSandbox() else {
return
}
defer {
try? FileManager.default.removeItem(at: testSwiftURL.deletingLastPathComponent())
try? FileManager.default.removeItem(at: seatbeltURL)
}
let swiftlintInSandboxArgs = ["sandbox-exec", "-f", seatbeltURL.path, "sh", "-c",
"SWIFTLINT_SWIFT_VERSION=3 \(swiftlintURL.path) --no-cache"]
let swiftlintResult = execute(swiftlintInSandboxArgs, in: testSwiftURL.deletingLastPathComponent())
let statusWithoutCrash: Int32 = 0
let stdoutWithoutCrash = """
\(testSwiftURL.path):1:1: \
warning: Trailing Newline Violation: Files should have a single trailing newline. (trailing_newline)
"""
let stderrWithoutCrash = """
Linting Swift files at paths \n\
Linting 'Test.swift' (1/1)
Connection invalid
Most rules will be skipped because sourcekitd has failed.
Done linting! Found 1 violation, 0 serious in 1 file.
"""
if #available(macOS 10.14.1, *) {
// Within a sandbox on macOS 10.14.1+, `swiftlint` crashes with "Test::Unit::AssertionFailedError"
// error in `libxpc.dylib` when calling `sourcekitd_send_request_sync`.
//
// Since Homebrew CI succeeded in bottling swiftlint 0.27.0 on release of macOS 10.14,
// `swiftlint` may not crash on macOS 10.14. But that is not confirmed.
XCTAssertNotEqual(swiftlintResult.status, statusWithoutCrash, "It is expected to crash.")
XCTAssertNotEqual(swiftlintResult.stdout, stdoutWithoutCrash)
XCTAssertNotEqual(swiftlintResult.stderr, stderrWithoutCrash)
} else {
XCTAssertEqual(swiftlintResult.status, statusWithoutCrash)
XCTAssertEqual(swiftlintResult.stdout, stdoutWithoutCrash)
XCTAssertEqual(swiftlintResult.stderr, stderrWithoutCrash)
}
#endif
}
func testSimulateHomebrewTestWithDisableSourceKit() {
// Since this test uses the `swiftlint` binary built while building `SwiftLintPackageTests`,
// we run it only on macOS using SwiftPM.
#if os(macOS) && SWIFT_PACKAGE
guard let swiftlintURL = swiftlintBuiltBySwiftPM(),
let (testSwiftURL, seatbeltURL) = prepareSandbox() else {
return
}
defer {
try? FileManager.default.removeItem(at: testSwiftURL.deletingLastPathComponent())
try? FileManager.default.removeItem(at: seatbeltURL)
}
let swiftlintInSandboxArgs = [
"sandbox-exec", "-f", seatbeltURL.path, "sh", "-c",
"SWIFTLINT_SWIFT_VERSION=3 SWIFTLINT_DISABLE_SOURCEKIT=1 \(swiftlintURL.path) --no-cache"
]
let swiftlintResult = execute(swiftlintInSandboxArgs, in: testSwiftURL.deletingLastPathComponent())
XCTAssertEqual(swiftlintResult.status, 0)
XCTAssertEqual(swiftlintResult.stdout, """
\(testSwiftURL.path):1:1: \
warning: Trailing Newline Violation: Files should have a single trailing newline. (trailing_newline)
""")
XCTAssertEqual(swiftlintResult.stderr, """
Linting Swift files at paths \n\
Linting 'Test.swift' (1/1)
SourceKit is disabled by `SWIFTLINT_DISABLE_SOURCEKIT`.
Most rules will be skipped because sourcekitd has failed.
Done linting! Found 1 violation, 0 serious in 1 file.
""")
#endif
}
}
private struct StaticStringImitator {
let string: String
func withStaticString(_ closure: (StaticString) -> Void) {
let isASCII = string.utf8.allSatisfy { $0 < 0x7f }
string.utf8CString.dropLast().withUnsafeBytes {
let imitator = Imitator(startPtr: $0.baseAddress!, utf8CodeUnitCount: $0.count, isASCII: isASCII)
closure(imitator.staticString)
}
}
struct Imitator {
let startPtr: UnsafeRawPointer
let utf8CodeUnitCount: Int
let flags: Int8
init(startPtr: UnsafeRawPointer, utf8CodeUnitCount: Int, isASCII: Bool) {
self.startPtr = startPtr
self.utf8CodeUnitCount = utf8CodeUnitCount
flags = isASCII ? 0x2 : 0x0
}
var staticString: StaticString {
return unsafeBitCast(self, to: StaticString.self)
}
}
}
private extension String {
func withStaticString(_ closure: (StaticString) -> Void) {
StaticStringImitator(string: self).withStaticString(closure)
}
}
#if os(macOS) && SWIFT_PACKAGE
private func execute(_ args: [String],
in directory: URL? = nil,
input: Data? = nil) -> (status: Int32, stdout: String, stderr: String) {
// swiftlint:disable:previous large_tuple
let process = Process()
process.launchPath = "/usr/bin/env"
process.arguments = args
if let directory = directory {
process.currentDirectoryPath = directory.path
}
let stdoutPipe = Pipe(), stderrPipe = Pipe()
process.standardOutput = stdoutPipe
process.standardError = stderrPipe
if let input = input {
let stdinPipe = Pipe()
process.standardInput = stdinPipe.fileHandleForReading
stdinPipe.fileHandleForWriting.write(input)
stdinPipe.fileHandleForWriting.closeFile()
}
let group = DispatchGroup(), queue = DispatchQueue.global()
var stdoutData: Data?, stderrData: Data?
process.launch()
queue.async(group: group) { stdoutData = stdoutPipe.fileHandleForReading.readDataToEndOfFile() }
queue.async(group: group) { stderrData = stderrPipe.fileHandleForReading.readDataToEndOfFile() }
process.waitUntilExit()
group.wait()
let stdout = stdoutData.flatMap { String(data: $0, encoding: .utf8) } ?? ""
let stderr = stderrData.flatMap { String(data: $0, encoding: .utf8) } ?? ""
return (process.terminationStatus, stdout, stderr)
}
private func prepareSandbox() -> (testSwiftURL: URL, seatbeltURL: URL)? {
// Since `/private/tmp` is hard coded in `/usr/local/Homebrew/Library/Homebrew/sandbox.rb`, we use them.
// /private/tmp
// ├── AADA6B05-2E06-4E7F-BA48-8B3AF44415E3
// │   └── Test.swift
// ├── AADA6B05-2E06-4E7F-BA48-8B3AF44415E3.sb
do {
// `/private/tmp` is standardized to `/tmp` that is symbolic link to `/private/tmp`.
let temporaryDirectoryURL = URL(fileURLWithPath: "/tmp").appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true)
let seatbeltURL = temporaryDirectoryURL.appendingPathExtension("sb")
try sandboxProfile().write(to: seatbeltURL, atomically: true, encoding: .utf8)
let testSwiftURL = temporaryDirectoryURL.appendingPathComponent("Test.swift")
try "import Foundation".write(to: testSwiftURL, atomically: true, encoding: .utf8)
return (testSwiftURL, seatbeltURL)
} catch {
XCTFail("\(error)")
return nil
}
}
private func sandboxProfile() -> String {
let homeDirectory = NSHomeDirectory()
return """
(version 1)
(debug deny) ; log all denied operations to /var/log/system.log
(allow file-write* (subpath "/private/tmp"))
(allow file-write* (subpath "/private/var/tmp"))
(allow file-write* (regex #"^/private/var/folders/[^/]+/[^/]+/[C,T]/"))
(allow file-write* (subpath "/private/tmp"))
(allow file-write* (subpath "\(homeDirectory)/Library/Caches/Homebrew"))
(allow file-write* (subpath "\(homeDirectory)/Library/Logs/Homebrew/swiftlint"))
(allow file-write* (subpath "\(homeDirectory)/Library/Developer"))
(allow file-write* (subpath "/usr/local/var/cache"))
(allow file-write* (subpath "/usr/local/var/homebrew/locks"))
(allow file-write* (subpath "/usr/local/var/log"))
(allow file-write* (subpath "/usr/local/var/run"))
(allow file-write*
(literal "/dev/ptmx")
(literal "/dev/dtracehelper")
(literal "/dev/null")
(literal "/dev/random")
(literal "/dev/zero")
(regex #"^/dev/fd/[0-9]+$")
(regex #"^/dev/ttys?[0-9]*$")
)
(deny file-write*) ; deny non-whitelist file write operations
(allow process-exec
(literal "/bin/ps")
(with no-sandbox)
) ; allow certain processes running without sandbox
(allow default) ; allow everything else
"""
}
private func swiftlintBuiltBySwiftPM() -> URL? {
#if DEBUG
let configuration = "debug"
#else
let configuration = "release"
#endif
let swiftBuildShowBinPathArgs = ["swift", "build", "--show-bin-path", "--configuration", configuration]
let binPathResult = execute(swiftBuildShowBinPathArgs)
guard binPathResult.status == 0 else {
let commandline = swiftBuildShowBinPathArgs.joined(separator: " ")
XCTFail("`\(commandline)` failed with status: \(binPathResult.status), error: \(binPathResult.stderr)")
return nil
}
let binPathString = binPathResult.stdout.components(separatedBy: CharacterSet.newlines).first!
let swiftlint = URL(fileURLWithPath: binPathString).appendingPathComponent("swiftlint")
guard FileManager.default.fileExists(atPath: swiftlint.path) else {
XCTFail("`swiftlint` does not exists.")
return nil
}
return swiftlint
}
#endif