1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
|
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2018 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
import Common
import Foundation
import TSCBasic
/// Keeps track of the detected failures and their status (expected/unexpected)
/// across multiple wrapper invocations
public struct IssueManager {
let activeConfig: String
let expectedIssuesFile: URL
let resultsFile: URL
let encoder: JSONEncoder
let activeRequests: Set<RequestKind>
/// - parameters:
/// - expectedFailuresFile: the URL of a JSON file describing the
/// expected failures
/// - resultsFile: the URL of the file to use to store failures across
/// multiple invocations
init(activeConfig: String, expectedIssuesFile: URL, resultsFile: URL,
activeRequests: Set<RequestKind>) {
self.activeConfig = activeConfig
self.expectedIssuesFile = expectedIssuesFile
self.resultsFile = resultsFile
self.activeRequests = activeRequests
self.encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
}
/// Updates this instance's backing resultsFile to account for the files
/// processed by this invocation of the wrapper, and the issue it detected,
/// if any.
///
/// - parameters:
/// - for: the set of files processed
/// - issue: the detected issue, if any
func update(for files: Set<String>, issue: StressTesterIssue?) throws -> ExpectedIssue? {
return try localFileSystem.withLock(on: AbsolutePath(resultsFile.path), type: .exclusive) {
let failureSpecs = try getIssueSpecifications(applicableTo: files)
var state = try getCurrentState()
var matchingSpec: ExpectedIssue? = nil
let added = files.subtracting(state.processedFiles)
state.unmatchedExpectedIssues.append(contentsOf: failureSpecs.filter { spec in
added.contains {spec.isApplicable(toPath: $0)}
})
state.processedFiles.formUnion(files)
if let issue = issue {
if let match = failureSpecs.first(where: {$0.matches(issue)}) {
state.expectedIssues[match.issueUrl, default: []].append(issue)
state.expectedIssueMessages[match.issueUrl, default: []].append(String(describing: issue))
state.unmatchedExpectedIssues.removeAll(where: {$0 == match})
matchingSpec = match
} else if !issue.isSoftError {
let xfail = ExpectedIssue(matching: issue, issueUrl: "<issue url>",
config: activeConfig)
let json = try encoder.encode(xfail)
state.issues.append(issue)
state.issueMessages.append("""
\(String(describing: issue))
Add the following entry to the expected failures JSON file to mark it as expected:
\(String(data: json, encoding: .utf8)!)"
""")
}
}
let data = try encoder.encode(state)
FileManager.default.createFile(atPath: resultsFile.path, contents: data)
return matchingSpec
}
}
private func getIssueSpecifications(applicableTo files: Set<String>) throws -> [ExpectedIssue] {
let data = try Data(contentsOf: expectedIssuesFile)
return try JSONDecoder().decode([ExpectedIssue].self, from: data)
.filter { spec in
return spec.applicableConfigs.contains(activeConfig) &&
files.contains { spec.isApplicable(toPath: $0) } &&
activeRequests.contains(where: { spec.issueDetail.enabledFor(request: $0) })
}
}
private func getCurrentState() throws -> IssueManagerState {
guard FileManager.default.fileExists(atPath: resultsFile.path) else {
return IssueManagerState()
}
let data = try Data(contentsOf: resultsFile)
return try JSONDecoder().decode(IssueManagerState.self, from: data)
}
}
/// Holds the state of the IssueManager that will be serialized across
/// invocations
fileprivate struct IssueManagerState: Codable {
var expectedIssues = Dictionary<String, [StressTesterIssue]>()
var expectedIssueMessages = Dictionary<String, [String]>()
var issues = [StressTesterIssue]()
var issueMessages = [String]()
var processedFiles = Set<String>()
var unmatchedExpectedIssues = [ExpectedIssue]()
}
extension StressTesterIssue: Codable {
private enum CodingKeys: String, CodingKey {
case kind, sourceKitError, status, file, arguments
}
private enum BaseMessage: String, Codable {
case failed, errored
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
switch try container.decode(BaseMessage.self, forKey: .kind) {
case .failed:
let sourceKitError = try container.decode(SourceKitError.self, forKey: .sourceKitError)
let arguments = try container.decode(String.self, forKey: .arguments)
self = .failed(sourceKitError: sourceKitError, arguments: arguments)
case .errored:
let status = try container.decode(Int32.self, forKey: .status)
let file = try container.decode(String.self, forKey: .file)
let arguments = try container.decode(String.self, forKey: .arguments)
self = .errored(status: status, file: file, arguments: arguments)
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .failed(let sourceKitError, let arguments):
try container.encode(BaseMessage.failed, forKey: .kind)
try container.encode(sourceKitError, forKey: .sourceKitError)
try container.encode(arguments, forKey: .arguments)
case .errored(let status, let file, let arguments):
try container.encode(BaseMessage.errored, forKey: .kind)
try container.encode(status, forKey: .status)
try container.encode(file, forKey: .file)
try container.encode(arguments, forKey: .arguments)
}
}
}
|