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.50) let version = "4.2.0" // Since we can't pass SWIFT_VERSION=4 to sourcekit, it returns 4.2.0 #elseif swift(>=4.1.2) let version = "4.1.2" #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.2) let version = "4.1.2" // Since we can't pass SWIFT_VERSION=3 to sourcekit, it returns 4.1.2 #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()) } }