mirror of
https://github.com/nicklockwood/SwiftFormat.git
synced 2026-06-16 10:34:34 +00:00
593 lines
18 KiB
Swift
593 lines
18 KiB
Swift
//
|
|
// CommandLineTests.swift
|
|
// SwiftFormat
|
|
//
|
|
// Created by Nick Lockwood on 10/01/2017.
|
|
// Copyright 2017 Nick Lockwood
|
|
//
|
|
// Distributed under the permissive MIT license
|
|
// Get the latest version from here:
|
|
//
|
|
// https://github.com/nicklockwood/SwiftFormat
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to deal
|
|
// in the Software without restriction, including without limitation the rights
|
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
// copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in all
|
|
// copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
// SOFTWARE.
|
|
//
|
|
|
|
import XCTest
|
|
@testable import SwiftFormat
|
|
|
|
private let projectDirectory = URL(fileURLWithPath: #file)
|
|
.deletingLastPathComponent().deletingLastPathComponent()
|
|
|
|
private func createTmpFile(_ path: String? = nil, contents: String) throws -> URL {
|
|
let path = path ?? (UUID().uuidString + ".swift")
|
|
let url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(path)
|
|
let directory = url.deletingLastPathComponent()
|
|
if !FileManager.default.fileExists(atPath: directory.path) {
|
|
try FileManager.default.createDirectory(
|
|
at: directory,
|
|
withIntermediateDirectories: true,
|
|
attributes: nil
|
|
)
|
|
}
|
|
try contents.write(to: url, atomically: true, encoding: .utf8)
|
|
return url
|
|
}
|
|
|
|
private func withTmpFile(_ path: String? = nil, contents: String, fn: (URL) -> Void) throws {
|
|
let path = path ?? (UUID().uuidString + ".swift")
|
|
let prefix = UUID().uuidString
|
|
let url = try createTmpFile("\(prefix)/\(path)", contents: contents)
|
|
fn(url)
|
|
try FileManager.default.removeItem(at: url)
|
|
}
|
|
|
|
private func withTmpFiles(_ files: [String: String], fn: (URL) throws -> Void) throws {
|
|
var urls = [URL]()
|
|
let prefix = UUID().uuidString
|
|
for (path, contents) in files {
|
|
try urls.append(createTmpFile("\(prefix)/\(path)", contents: contents))
|
|
}
|
|
for url in urls where url.pathExtension == "swift" {
|
|
try fn(url)
|
|
}
|
|
for url in urls {
|
|
try FileManager.default.removeItem(at: url)
|
|
}
|
|
}
|
|
|
|
class CommandLineTests: XCTestCase {
|
|
// MARK: stdin
|
|
|
|
func testStdin() {
|
|
CLI.print = { message, type in
|
|
switch type {
|
|
case .raw, .content:
|
|
XCTAssertEqual(message, "func foo() {\n bar()\n}\n")
|
|
case .error, .warning:
|
|
XCTFail()
|
|
case .info, .success:
|
|
break
|
|
}
|
|
}
|
|
var readCount = 0
|
|
CLI.readLine = {
|
|
readCount += 1
|
|
switch readCount {
|
|
case 1:
|
|
return "func foo()\n"
|
|
case 2:
|
|
return "{\n"
|
|
case 3:
|
|
return "bar()\n"
|
|
case 4:
|
|
return "}"
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
_ = processArguments([""], in: "")
|
|
readCount = 0
|
|
_ = processArguments(["", "stdin"], in: "")
|
|
}
|
|
|
|
func testExcludeStdinPath() throws {
|
|
CLI.print = { message, type in
|
|
switch type {
|
|
case .raw, .content:
|
|
XCTAssertEqual(message, "func foo() {\n}\n")
|
|
case .error, .warning:
|
|
XCTFail()
|
|
case .info, .success:
|
|
break
|
|
}
|
|
}
|
|
var readCount = 0
|
|
CLI.readLine = {
|
|
readCount += 1
|
|
switch readCount {
|
|
case 1:
|
|
return "func foo() {\n"
|
|
case 2:
|
|
return "}\n"
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
try withTmpFile(contents: "") { url in
|
|
_ = processArguments([
|
|
"",
|
|
"stdin",
|
|
"--stdinpath", url.path,
|
|
"--exclude", url.path,
|
|
], in: "")
|
|
}
|
|
}
|
|
|
|
func testExcludeStdinPath2() throws {
|
|
CLI.print = { message, type in
|
|
switch type {
|
|
case .raw, .content:
|
|
XCTAssertEqual(message, "func foo() {\n}\n")
|
|
case .error, .warning:
|
|
XCTFail()
|
|
case .info, .success:
|
|
break
|
|
}
|
|
}
|
|
var readCount = 0
|
|
CLI.readLine = {
|
|
readCount += 1
|
|
switch readCount {
|
|
case 1:
|
|
return "func foo() {\n"
|
|
case 2:
|
|
return "}\n"
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
try withTmpFiles([
|
|
".swiftformat": "--exclude *",
|
|
"foo.swift": "",
|
|
]) { url in
|
|
_ = processArguments([
|
|
"",
|
|
"stdin",
|
|
"--stdinpath", url.path,
|
|
], in: "")
|
|
}
|
|
}
|
|
|
|
func testExcludeStdinPath3() throws {
|
|
CLI.print = { message, type in
|
|
switch type {
|
|
case .raw, .content:
|
|
XCTAssertEqual(message, "func foo() {\n}\n")
|
|
case .error, .warning:
|
|
XCTFail()
|
|
case .info, .success:
|
|
break
|
|
}
|
|
}
|
|
var readCount = 0
|
|
CLI.readLine = {
|
|
readCount += 1
|
|
switch readCount {
|
|
case 1:
|
|
return "func foo() {\n"
|
|
case 2:
|
|
return "}\n"
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
try withTmpFiles([
|
|
".swiftformat": "--exclude foo",
|
|
"foo/bar/baz.swift": "",
|
|
]) { url in
|
|
_ = processArguments([
|
|
"",
|
|
"stdin",
|
|
"--stdinpath", url.path,
|
|
], in: "")
|
|
}
|
|
}
|
|
|
|
func testUnexcludeStdinPath() throws {
|
|
CLI.print = { message, type in
|
|
switch type {
|
|
case .raw, .content:
|
|
XCTAssertEqual(message, "func foo() {}\n")
|
|
case .error, .warning:
|
|
XCTFail()
|
|
case .info, .success:
|
|
break
|
|
}
|
|
}
|
|
var readCount = 0
|
|
CLI.readLine = {
|
|
readCount += 1
|
|
switch readCount {
|
|
case 1:
|
|
return "func foo() {\n"
|
|
case 2:
|
|
return "}\n"
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
try withTmpFiles([
|
|
".swiftformat": """
|
|
--exclude foo
|
|
--unexclude **/baz.*
|
|
""",
|
|
"foo/bar/baz.swift": "",
|
|
]) { url in
|
|
_ = processArguments([
|
|
"",
|
|
"stdin",
|
|
"--stdinpath", url.path,
|
|
], in: "")
|
|
}
|
|
}
|
|
|
|
// MARK: help
|
|
|
|
func testOptionsHelpText() {
|
|
for option in Descriptors.all {
|
|
XCTAssertFalse(
|
|
option.help.contains("\n"),
|
|
"Help for option --\(option.argumentName) contains linebreak"
|
|
)
|
|
}
|
|
}
|
|
|
|
func testHelpLineLength() {
|
|
CLI.print = { message, _ in
|
|
for line in message.components(separatedBy: "\n") {
|
|
XCTAssertLessThanOrEqual(line.count, 80, line)
|
|
}
|
|
}
|
|
printHelp(as: .content)
|
|
printOptions(as: .content)
|
|
for rule in FormatRules.all {
|
|
try! printRuleInfo(for: rule.name, as: .content)
|
|
}
|
|
}
|
|
|
|
func testHelpOptionsImplemented() {
|
|
CLI.print = { message, _ in
|
|
if message.hasPrefix("--") {
|
|
let name = String(message["--".endIndex ..< message.endIndex]).components(separatedBy: " ")[0]
|
|
XCTAssertTrue(commandLineArguments.contains(name), name)
|
|
}
|
|
}
|
|
printHelp(as: .content)
|
|
}
|
|
|
|
func testHelpOptionsDocumented() {
|
|
var arguments = Set(commandLineArguments).subtracting(deprecatedArguments)
|
|
CLI.print = { allHelpMessage, _ in
|
|
allHelpMessage
|
|
.components(separatedBy: "\n")
|
|
.forEach { message in
|
|
if message.hasPrefix("--") {
|
|
let name = String(message["--".endIndex ..< message.endIndex]).components(separatedBy: " ")[0]
|
|
XCTAssert(arguments.contains(name), "Unknown option --\(name) in help")
|
|
arguments.remove(name)
|
|
}
|
|
}
|
|
}
|
|
printHelp(as: .content)
|
|
printOptions(as: .content)
|
|
XCTAssert(arguments.isEmpty, "\(arguments.joined(separator: ",")) not listed in help")
|
|
}
|
|
|
|
// MARK: cache
|
|
|
|
func testHashIsFasterThanFormatting() throws {
|
|
let sourceFile = URL(fileURLWithPath: #file)
|
|
let source = try String(contentsOf: sourceFile, encoding: .utf8)
|
|
let hash = computeHash(source + ";")
|
|
|
|
let hashTime = timeEvent { _ = computeHash(source) == hash }
|
|
let formatTime = try timeEvent { _ = try format(source) }
|
|
XCTAssertLessThan(hashTime, formatTime)
|
|
}
|
|
|
|
func testCacheHit() throws {
|
|
let input = "let foo = bar"
|
|
XCTAssertEqual(computeHash(input), computeHash(input))
|
|
}
|
|
|
|
func testCacheMiss() throws {
|
|
let input = "let foo = bar"
|
|
let output = "let foo = bar\n"
|
|
XCTAssertNotEqual(computeHash(input), computeHash(output))
|
|
}
|
|
|
|
func testCachePotentialFalsePositive() throws {
|
|
let input = "let foo = bar;"
|
|
let output = "let foo = bar\n"
|
|
XCTAssertNotEqual(computeHash(input), computeHash(output))
|
|
}
|
|
|
|
func testCachePotentialFalsePositive2() throws {
|
|
let input = """
|
|
import Foo
|
|
import Bar
|
|
|
|
"""
|
|
let output = """
|
|
import Bar
|
|
import Foo
|
|
|
|
"""
|
|
XCTAssertNotEqual(computeHash(input), computeHash(output))
|
|
}
|
|
|
|
// MARK: end-to-end formatting
|
|
|
|
func testFormatting() {
|
|
CLI.print = { _, _ in }
|
|
let args = ". --dryrun --disable redundantSelf"
|
|
XCTAssertEqual(CLI.run(in: projectDirectory.path, with: args), .ok)
|
|
}
|
|
|
|
// MARK: rules
|
|
|
|
func testRulesNotMarkedAsDisabled() {
|
|
CLI.print = { message, _ in
|
|
XCTAssert(!message.contains("(disabled)") ||
|
|
FormatRules.disabledByDefault.contains(where: { message.contains($0) }))
|
|
}
|
|
XCTAssertEqual(CLI.run(in: projectDirectory.path, with: "--rules"), .ok)
|
|
}
|
|
|
|
func testEnableOverridesDisableAll() {
|
|
CLI.print = { message, _ in
|
|
XCTAssertFalse(message.contains("wrap (disabled)"))
|
|
}
|
|
XCTAssertEqual(CLI.run(in: projectDirectory.path,
|
|
with: "--disable all --enable wrap --rules"), .ok)
|
|
}
|
|
|
|
// MARK: quiet mode
|
|
|
|
func testQuietModeNoOutput() {
|
|
CLI.print = { message, _ in
|
|
XCTFail(message)
|
|
}
|
|
XCTAssertEqual(CLI.run(in: projectDirectory.path, with: "--quiet --dryrun"), .ok)
|
|
}
|
|
|
|
func testQuietModeAllowsContent() {
|
|
CLI.print = { message, type in
|
|
XCTAssertEqual(type, .content, message)
|
|
}
|
|
XCTAssertEqual(CLI.run(in: projectDirectory.path, with: "--quiet --help"), .ok)
|
|
}
|
|
|
|
func testQuietModeAllowsErrors() {
|
|
CLI.print = { message, type in
|
|
XCTAssertEqual(type, .error, message)
|
|
}
|
|
XCTAssertEqual(CLI.run(in: projectDirectory.path, with: "foobar.swift --quiet"), .error)
|
|
}
|
|
|
|
// MARK: split input paths
|
|
|
|
func testSplitInputPaths() {
|
|
CLI.print = { message, type in
|
|
XCTAssertEqual(type, .error, message)
|
|
}
|
|
XCTAssertEqual(CLI.run(in: projectDirectory.path, with: "Sources --dryrun Tests --rules indent"), .error)
|
|
}
|
|
|
|
// MARK: file list
|
|
|
|
func testParseFileList() {
|
|
let source = """
|
|
#foo
|
|
Package.swift #bar
|
|
|
|
#baz
|
|
Sources/Rules.swift
|
|
CommandLineTool/*.swift
|
|
"""
|
|
XCTAssertEqual(try parseFileList(source, in: projectDirectory.path), [
|
|
URL(fileURLWithPath: "\(projectDirectory.path)/Package.swift"),
|
|
URL(fileURLWithPath: "\(projectDirectory.path)/Sources/Rules.swift"),
|
|
URL(fileURLWithPath: "\(projectDirectory.path)/CommandLineTool/main.swift"),
|
|
])
|
|
}
|
|
|
|
// MARK: script input files
|
|
|
|
func testParseScriptInput() throws {
|
|
let result = try parseScriptInput(from: [
|
|
"SCRIPT_INPUT_FILE_COUNT": "2",
|
|
"SCRIPT_INPUT_FILE_0": "\(projectDirectory.path)/File1.swift",
|
|
"SCRIPT_INPUT_FILE_1": "\(projectDirectory.path)/File2.swift",
|
|
])
|
|
XCTAssertEqual(
|
|
result,
|
|
[
|
|
URL(fileURLWithPath: "\(projectDirectory.path)/File1.swift"),
|
|
URL(fileURLWithPath: "\(projectDirectory.path)/File2.swift"),
|
|
]
|
|
)
|
|
}
|
|
|
|
// MARK: config
|
|
|
|
func testBadConfigFails() {
|
|
var error = ""
|
|
CLI.print = { message, type in
|
|
if type == .error {
|
|
error += message + "\n"
|
|
}
|
|
}
|
|
XCTAssertEqual(CLI.run(in: projectDirectory.path, with: "Tests/BadConfig/Test.swift --unexclude Tests/BadConfig --config Tests/BadConfig/.swiftformat --lint"), .error)
|
|
XCTAssert(error.contains("'ifdef' is not a formatting rule"), error)
|
|
}
|
|
|
|
func testBadConfigFails2() {
|
|
var error = ""
|
|
CLI.print = { message, type in
|
|
if type == .error {
|
|
error += message + "\n"
|
|
}
|
|
}
|
|
XCTAssertEqual(CLI.run(in: projectDirectory.path, with: "Tests/BadConfig/Test.swift --unexclude Tests/BadConfig --lint"), .error)
|
|
XCTAssert(error.contains("'ifdef' is not a formatting rule"), error)
|
|
}
|
|
|
|
func testWarnIfOptionsSpecifiedForDisabledRule() {
|
|
CLI.print = { message, type in
|
|
if type == .warning {
|
|
XCTAssertEqual(
|
|
message,
|
|
"warning: --header option has no effect when fileHeader rule is disabled"
|
|
)
|
|
}
|
|
}
|
|
XCTAssertEqual(CLI.run(in: projectDirectory.path, with: "stdin --lint --rules indent --header foo"), .ok)
|
|
}
|
|
|
|
// MARK: reporter
|
|
|
|
func testJSONReporterEndToEnd() throws {
|
|
try withTmpFiles([
|
|
"foo.swift": "func foo() {\n}\n",
|
|
]) { url in
|
|
CLI.print = { message, type in
|
|
switch type {
|
|
case .raw:
|
|
XCTAssert(message.contains("\"rule_id\" : \"emptyBraces\""))
|
|
case .error, .warning:
|
|
break
|
|
case .info, .success:
|
|
break
|
|
case .content:
|
|
XCTFail()
|
|
}
|
|
}
|
|
_ = processArguments([
|
|
"",
|
|
"--lint",
|
|
"--reporter",
|
|
"json",
|
|
url.path,
|
|
], in: "")
|
|
}
|
|
}
|
|
|
|
func testJSONReporterInferredFromURL() throws {
|
|
let outputURL = try createTmpFile("report.json", contents: "")
|
|
try withTmpFiles([
|
|
"foo.swift": "func foo() {\n}\n",
|
|
]) { url in
|
|
CLI.print = { _, _ in }
|
|
_ = processArguments([
|
|
"",
|
|
"--lint",
|
|
"--report",
|
|
outputURL.path,
|
|
url.path,
|
|
], in: "")
|
|
}
|
|
let ouput = try String(contentsOf: outputURL)
|
|
XCTAssert(ouput.contains("\"rule_id\" : \"emptyBraces\""))
|
|
}
|
|
|
|
func testGithubActionsLogReporterEndToEnd() throws {
|
|
try withTmpFiles([
|
|
"foo.swift": "func foo() {\n}\n",
|
|
]) { url in
|
|
CLI.print = { message, type in
|
|
switch type {
|
|
case .raw:
|
|
XCTAssert(message.hasPrefix("::warning file=foo.swift,line=1::"))
|
|
case .error, .warning:
|
|
break
|
|
case .info, .success:
|
|
break
|
|
case .content:
|
|
XCTFail()
|
|
}
|
|
}
|
|
_ = processArguments([
|
|
"",
|
|
"--lint",
|
|
"--reporter",
|
|
"github-actions-log",
|
|
url.path,
|
|
],
|
|
environment: ["GITHUB_WORKSPACE": url.deletingLastPathComponent().path],
|
|
in: "")
|
|
}
|
|
}
|
|
|
|
func testGithubActionsLogReporterMisspelled() throws {
|
|
try withTmpFiles([
|
|
"foo.swift": "func foo() {\n}\n",
|
|
]) { url in
|
|
CLI.print = { message, type in
|
|
switch type {
|
|
case .raw, .warning, .info:
|
|
break
|
|
case .error:
|
|
XCTAssert(message.contains("did you mean 'github-actions-log'?"))
|
|
case .content, .success:
|
|
XCTFail()
|
|
}
|
|
}
|
|
_ = processArguments([
|
|
"",
|
|
"--lint",
|
|
"--reporter",
|
|
"github-action-log",
|
|
url.path,
|
|
], in: "")
|
|
}
|
|
}
|
|
|
|
// MARK: snapshot/regression tests
|
|
|
|
func testRegressionSuite() {
|
|
CLI.print = { message, _ in
|
|
Swift.print(message)
|
|
}
|
|
// NOTE: to update regression suite, run again without `--lint` argument
|
|
XCTAssertEqual(CLI.run(in: projectDirectory.path, with: "Sources,Tests,Snapshots --unexclude Snapshots --symlinks follow --cache ignore --lint"), .ok)
|
|
}
|
|
|
|
func testRegressionSuiteNotDisabled() throws {
|
|
let commandLineTests = try String(contentsOf: URL(fileURLWithPath: #file))
|
|
let range = try XCTUnwrap(
|
|
commandLineTests.range(of: "testRegressionSuiteNotDisabled()")
|
|
)
|
|
XCTAssert(commandLineTests[..<range.lowerBound].contains("""
|
|
with: "Sources,Tests,Snapshots --unexclude Snapshots --symlinks follow --cache ignore --lint")
|
|
"""))
|
|
}
|
|
}
|