Files
SwiftLint/Tests/SwiftLintFrameworkTests/LinterCacheTests.swift
T
JP Simard b83e0991b9 Remove all file headers
The MIT license doesn't require that all files be prepended with this
licensing or copyright information. Realm confirmed that they're ok with this
change. This will enable some companies to contribute to SwiftLint and the
date & authorship information will remain accessible via git source control.
2018-05-04 13:42:02 -07:00

370 lines
18 KiB
Swift

import Foundation
@testable import SwiftLintFramework
import XCTest
private struct CacheTestHelper {
fileprivate let configuration: Configuration
private let ruleList: RuleList
private let ruleDescription: RuleDescription
private let cache: LinterCache
private var fileManager: TestFileManager {
// swiftlint:disable:next force_cast
return cache.fileManager as! TestFileManager
}
fileprivate init(dict: [String: Any], cache: LinterCache) {
ruleList = RuleList(rules: RuleWithLevelsMock.self)
ruleDescription = ruleList.list.values.first!.description
configuration = Configuration(dict: dict, ruleList: ruleList)!
self.cache = cache
}
fileprivate func makeViolations(file: String) -> [StyleViolation] {
touch(file: file)
return [
StyleViolation(ruleDescription: ruleDescription,
severity: .warning,
location: Location(file: file, line: 10, character: 2),
reason: "Something is not right."),
StyleViolation(ruleDescription: ruleDescription,
severity: .error,
location: Location(file: file, line: 5, character: nil),
reason: "Something is wrong.")
]
}
fileprivate func makeConfig(dict: [String: Any]) -> Configuration {
return Configuration(dict: dict, ruleList: ruleList)!
}
fileprivate func touch(file: String) {
fileManager.stubbedModificationDateByPath[file] = Date()
}
fileprivate func remove(file: String) {
fileManager.stubbedModificationDateByPath[file] = nil
}
fileprivate func fileCount() -> Int {
return fileManager.stubbedModificationDateByPath.count
}
}
private class TestFileManager: LintableFileManager {
fileprivate func filesToLint(inPath: String, rootDirectory: String? = nil) -> [String] {
return []
}
fileprivate var stubbedModificationDateByPath = [String: Date]()
fileprivate func modificationDate(forFileAtPath path: String) -> Date? {
return stubbedModificationDateByPath[path]
}
}
class LinterCacheTests: XCTestCase {
// MARK: Test Helpers
private var cache = LinterCache(fileManager: TestFileManager())
private func makeCacheTestHelper(dict: [String: Any]) -> CacheTestHelper {
return CacheTestHelper(dict: dict, cache: cache)
}
private func cacheAndValidate(violations: [StyleViolation], forFile: String, configuration: Configuration,
file: StaticString = #file, line: UInt = #line) {
cache.cache(violations: violations, forFile: forFile, configuration: configuration)
cache = cache.flushed()
XCTAssertEqual(cache.violations(forFile: forFile, configuration: configuration)!,
violations, file: file, line: line)
}
private func cacheAndValidateNoViolationsTwoFiles(configuration: Configuration,
file: StaticString = #file, line: UInt = #line) {
let (file1, file2) = ("file1.swift", "file2.swift")
// swiftlint:disable:next force_cast
let fileManager = cache.fileManager as! TestFileManager
fileManager.stubbedModificationDateByPath = [file1: Date(), file2: Date()]
cacheAndValidate(violations: [], forFile: file1, configuration: configuration, file: file, line: line)
cacheAndValidate(violations: [], forFile: file2, configuration: configuration, file: file, line: line)
}
private func validateNewConfigDoesntHitCache(dict: [String: Any], initialConfig: Configuration,
file: StaticString = #file, line: UInt = #line) {
let newConfig = Configuration(dict: dict)!
let (file1, file2) = ("file1.swift", "file2.swift")
XCTAssertNil(cache.violations(forFile: file1, configuration: newConfig), file: file, line: line)
XCTAssertNil(cache.violations(forFile: file2, configuration: newConfig), file: file, line: line)
XCTAssertEqual(cache.violations(forFile: file1, configuration: initialConfig)!, [], file: file, line: line)
XCTAssertEqual(cache.violations(forFile: file2, configuration: initialConfig)!, [], file: file, line: line)
}
// MARK: Cache Initialization
func testInitThrowsWhenUsingInvalidCacheFormat() {
let cache = [["version": "0.1.0"]]
checkError(LinterCacheError.invalidFormat) {
_ = try LinterCache(cache: cache)
}
}
func testSaveThrowsWithNoLocation() throws {
let cache = try LinterCache(cache: [:])
checkError(LinterCacheError.noLocation) {
try cache.save()
}
}
func testInitSucceeds() {
XCTAssertNotNil(try? LinterCache(cache: [:]))
}
// MARK: Cache Reuse
// Two subsequent lints with no changes reuses cache
func testUnchangedFilesReusesCache() {
let helper = makeCacheTestHelper(dict: ["whitelist_rules": ["mock"]])
let file = "foo.swift"
let violations = helper.makeViolations(file: file)
cacheAndValidate(violations: violations, forFile: file, configuration: helper.configuration)
helper.touch(file: file)
XCTAssertNil(cache.violations(forFile: file, configuration: helper.configuration))
}
func testConfigFileReorderedReusesCache() {
let helper = makeCacheTestHelper(dict: ["whitelist_rules": ["mock"], "disabled_rules": []])
let file = "foo.swift"
let violations = helper.makeViolations(file: file)
cacheAndValidate(violations: violations, forFile: file, configuration: helper.configuration)
let configuration2 = helper.makeConfig(dict: ["disabled_rules": [], "whitelist_rules": ["mock"]])
XCTAssertEqual(cache.violations(forFile: file, configuration: configuration2)!, violations)
}
func testConfigFileWhitespaceAndCommentsChangedOrAddedOrRemovedReusesCache() throws {
let helper = makeCacheTestHelper(dict: try YamlParser.parse("whitelist_rules:\n - mock"))
let file = "foo.swift"
let violations = helper.makeViolations(file: file)
cacheAndValidate(violations: violations, forFile: file, configuration: helper.configuration)
let configuration2 = helper.makeConfig(dict: ["disabled_rules": [], "whitelist_rules": ["mock"]])
XCTAssertEqual(cache.violations(forFile: file, configuration: configuration2)!, violations)
let configYamlWithComment = try YamlParser.parse("# comment1\nwhitelist_rules:\n - mock # comment2")
let configuration3 = helper.makeConfig(dict: configYamlWithComment)
XCTAssertEqual(cache.violations(forFile: file, configuration: configuration3)!, violations)
XCTAssertEqual(cache.violations(forFile: file, configuration: helper.configuration)!, violations)
}
func testConfigFileUnrelatedKeysChangedOrAddedOrRemovedReusesCache() {
let helper = makeCacheTestHelper(dict: ["whitelist_rules": ["mock"], "reporter": "json"])
let file = "foo.swift"
let violations = helper.makeViolations(file: file)
cacheAndValidate(violations: violations, forFile: file, configuration: helper.configuration)
let configuration2 = helper.makeConfig(dict: ["whitelist_rules": ["mock"], "reporter": "xcode"])
XCTAssertEqual(cache.violations(forFile: file, configuration: configuration2)!, violations)
let configuration3 = helper.makeConfig(dict: ["whitelist_rules": ["mock"]])
XCTAssertEqual(cache.violations(forFile: file, configuration: configuration3)!, violations)
}
// MARK: Sing-File Cache Invalidation
// Two subsequent lints with a file touch in between causes just that one
// file to be re-linted, with the cache used for all other files
func testChangedFileCausesJustThatFileToBeLintWithCacheUsedForAllOthers() {
let helper = makeCacheTestHelper(dict: ["whitelist_rules": ["mock"], "reporter": "json"])
let (file1, file2) = ("file1.swift", "file2.swift")
let violations1 = helper.makeViolations(file: file1)
let violations2 = helper.makeViolations(file: file2)
cacheAndValidate(violations: violations1, forFile: file1, configuration: helper.configuration)
cacheAndValidate(violations: violations2, forFile: file2, configuration: helper.configuration)
helper.touch(file: file2)
XCTAssertEqual(cache.violations(forFile: file1, configuration: helper.configuration)!, violations1)
XCTAssertNil(cache.violations(forFile: file2, configuration: helper.configuration))
}
func testFileRemovedPreservesThatFileInTheCacheAndDoesntCauseAnyOtherFilesToBeLinted() {
let helper = makeCacheTestHelper(dict: ["whitelist_rules": ["mock"], "reporter": "json"])
let (file1, file2) = ("file1.swift", "file2.swift")
let violations1 = helper.makeViolations(file: file1)
let violations2 = helper.makeViolations(file: file2)
cacheAndValidate(violations: violations1, forFile: file1, configuration: helper.configuration)
cacheAndValidate(violations: violations2, forFile: file2, configuration: helper.configuration)
XCTAssertEqual(helper.fileCount(), 2)
helper.remove(file: file2)
XCTAssertEqual(cache.violations(forFile: file1, configuration: helper.configuration)!, violations1)
XCTAssertEqual(helper.fileCount(), 1)
}
// MARK: All-File Cache Invalidation
func testCustomRulesChangedOrAddedOrRemovedCausesAllFilesToBeReLinted() {
let initialConfig = Configuration(
dict: [
"whitelist_rules": ["custom_rules", "rule1"],
"custom_rules": ["rule1": ["regex": "([n,N]inja)"]]
],
ruleList: RuleList(rules: CustomRules.self)
)!
cacheAndValidateNoViolationsTwoFiles(configuration: initialConfig)
// Change
validateNewConfigDoesntHitCache(
dict: [
"whitelist_rules": ["custom_rules", "rule1"],
"custom_rules": ["rule1": ["regex": "([n,N]injas)"]]
],
initialConfig: initialConfig
)
// Addition
validateNewConfigDoesntHitCache(
dict: [
"whitelist_rules": ["custom_rules", "rule1"],
"custom_rules": ["rule1": ["regex": "([n,N]injas)"], "rule2": ["regex": "([k,K]ittens)"]]
],
initialConfig: initialConfig
)
// Removal
validateNewConfigDoesntHitCache(dict: ["whitelist_rules": ["custom_rules"]], initialConfig: initialConfig)
}
func testDisabledRulesChangedOrAddedOrRemovedCausesAllFilesToBeReLinted() {
let initialConfig = Configuration(dict: ["disabled_rules": ["nesting"]])!
cacheAndValidateNoViolationsTwoFiles(configuration: initialConfig)
// Change
validateNewConfigDoesntHitCache(dict: ["disabled_rules": ["todo"]], initialConfig: initialConfig)
// Addition
validateNewConfigDoesntHitCache(dict: ["disabled_rules": ["nesting", "todo"]], initialConfig: initialConfig)
// Removal
validateNewConfigDoesntHitCache(dict: ["disabled_rules": []], initialConfig: initialConfig)
}
func testOptInRulesChangedOrAddedOrRemovedCausesAllFilesToBeReLinted() {
let initialConfig = Configuration(dict: ["opt_in_rules": ["attributes"]])!
cacheAndValidateNoViolationsTwoFiles(configuration: initialConfig)
// Change
validateNewConfigDoesntHitCache(dict: ["opt_in_rules": ["empty_count"]], initialConfig: initialConfig)
// Rules addition
validateNewConfigDoesntHitCache(dict: ["opt_in_rules": ["attributes", "empty_count"]],
initialConfig: initialConfig)
// Removal
validateNewConfigDoesntHitCache(dict: ["opt_in_rules": []], initialConfig: initialConfig)
}
func testEnabledRulesChangedOrAddedOrRemovedCausesAllFilesToBeReLinted() {
let initialConfig = Configuration(dict: ["enabled_rules": ["attributes"]])!
cacheAndValidateNoViolationsTwoFiles(configuration: initialConfig)
// Change
validateNewConfigDoesntHitCache(dict: ["enabled_rules": ["empty_count"]], initialConfig: initialConfig)
// Addition
validateNewConfigDoesntHitCache(dict: ["enabled_rules": ["attributes", "empty_count"]],
initialConfig: initialConfig)
// Removal
validateNewConfigDoesntHitCache(dict: ["enabled_rules": []], initialConfig: initialConfig)
}
func testWhitelistRulesChangedOrAddedOrRemovedCausesAllFilesToBeReLinted() {
let initialConfig = Configuration(dict: ["whitelist_rules": ["nesting"]])!
cacheAndValidateNoViolationsTwoFiles(configuration: initialConfig)
// Change
validateNewConfigDoesntHitCache(dict: ["whitelist_rules": ["todo"]], initialConfig: initialConfig)
// Addition
validateNewConfigDoesntHitCache(dict: ["whitelist_rules": ["nesting", "todo"]], initialConfig: initialConfig)
// Removal
validateNewConfigDoesntHitCache(dict: ["whitelist_rules": []], initialConfig: initialConfig)
}
func testRuleConfigurationChangedOrAddedOrRemovedCausesAllFilesToBeReLinted() {
let initialConfig = Configuration(dict: ["line_length": 120])!
cacheAndValidateNoViolationsTwoFiles(configuration: initialConfig)
// Change
validateNewConfigDoesntHitCache(dict: ["line_length": 100], initialConfig: initialConfig)
// Addition
validateNewConfigDoesntHitCache(dict: ["line_length": 100, "number_separator": ["minimum_length": 5]],
initialConfig: initialConfig)
// Removal
validateNewConfigDoesntHitCache(dict: [:], initialConfig: initialConfig)
}
func testSwiftVersionChangedRemovedCausesAllFilesToBeReLinted() {
let fileManager = TestFileManager()
cache = LinterCache(fileManager: fileManager)
let helper = makeCacheTestHelper(dict: [:])
let file = "foo.swift"
let violations = helper.makeViolations(file: file)
cacheAndValidate(violations: violations, forFile: file, configuration: helper.configuration)
let thisSwiftVersionCache = cache
let differentSwiftVersion: SwiftVersion = (SwiftVersion.current >= .four) ? .three : .four
cache = LinterCache(fileManager: fileManager, swiftVersion: differentSwiftVersion)
XCTAssertNotNil(thisSwiftVersionCache.violations(forFile: file, configuration: helper.configuration))
XCTAssertNil(cache.violations(forFile: file, configuration: helper.configuration))
}
func testDetectSwiftVersion() {
#if swift(>=4.2.0)
let version = "4.2.0"
#elseif swift(>=4.1.1)
let version = "4.1.1"
#elseif swift(>=4.1.0)
let version = "4.1.0"
#elseif swift(>=4.0.3)
let version = "4.0.3"
#elseif swift(>=4.0.2)
let version = "4.0.2"
#elseif swift(>=4.0.1)
let version = "4.0.1"
#elseif swift(>=4.0.0)
let version = "4.0.0"
#elseif swift(>=3.4.0)
let version = "4.2.0" // Since we can't pass SWIFT_VERSION=3 to sourcekit, it returns 4.2.0
#elseif swift(>=3.3.1)
let version = "4.1.1" // Since we can't pass SWIFT_VERSION=3 to sourcekit, it returns 4.1.1
#elseif swift(>=3.3.0)
let version = "4.1.0" // Since we can't pass SWIFT_VERSION=3 to sourcekit, it returns 4.1.0
#elseif swift(>=3.2.3)
let version = "4.0.3" // Since we can't pass SWIFT_VERSION=3 to sourcekit, it returns 4.0.3
#elseif swift(>=3.2.2)
let version = "4.0.2" // Since we can't pass SWIFT_VERSION=3 to sourcekit, it returns 4.0.2
#elseif swift(>=3.2.1)
let version = "4.0.1" // Since we can't pass SWIFT_VERSION=3 to sourcekit, it returns 4.0.1
#else // if swift(>=3.2.0)
let version = "4.0.0" // Since we can't pass SWIFT_VERSION=3 to sourcekit, it returns 4.0.0
#endif
XCTAssertEqual(SwiftVersion.current.rawValue, version)
}
// MARK: JSON output
func testCacheToJSONDoesntCrash() {
// swiftlint:disable line_length
let key1 = "[\"/SwiftLint/source\",[[\"block_based_kvo\",\"warning\"],[\"class_delegate_protocol\",\"warning\"],[\"closing_brace\",\"warning\"],[\"closure_parameter_position\",\"warning\"],[\"colon\",\"warning, flexible_right_spacing: false, apply_to_dictionaries: true\"],[\"comma\",\"warning\"],[\"compiler_protocol_init\",\"warning\"],[\"control_statement\",\"warning\"],[\"custom_rules\",\"\"],[\"cyclomatic_complexity\",\"warning: 10, error: 20, ignores_case_statements: false\"],[\"discarded_notification_center_observer\",\"warning\"],[\"discouraged_direct_init\",\"warning, types: [\"Bundle\", \"Bundle.init\", \"UIDevice\", \"UIDevice.init\"]\"],[\"dynamic_inline\",\"error\"],[\"empty_enum_arguments\",\"warning\"],[\"empty_parameters\",\"warning\"]]]"
let key2 = "[\"/SwiftLint/Source\",[[\"block_based_kvo\",\"warning\"],[\"class_delegate_protocol\",\"warning\"],[\"closing_brace\",\"warning\"],[\"closure_parameter_position\",\"warning\"],[\"colon\",\"warning, flexible_right_spacing: false, apply_to_dictionaries: true\"],[\"comma\",\"warning\"],[\"compiler_protocol_init\",\"warning\"],[\"control_statement\",\"warning\"],[\"custom_rules\",\"\"],[\"cyclomatic_complexity\",\"warning: 10, error: 20, ignores_case_statements: false\"],[\"discarded_notification_center_observer\",\"warning\"],[\"discouraged_direct_init\",\"warning, types: [\"Bundle\", \"Bundle.init\", \"UIDevice\", \"UIDevice.init\"]\"],[\"dynamic_inline\",\"error\"],[\"empty_enum_arguments\",\"warning\"],[\"empty_parameters\",\"warning\"]]]"
// swiftlint:enable line_length
let dict = [key1: "test", key2: "test2"]
XCTAssertNoThrow(try dict.toJSON())
}
}