@@ -7,100 +7,183 @@ import XCTest
7
7
/// DiagnosingTestCase is an XCTestCase subclass meant to inject diagnostic-specific testing
8
8
/// routines into specific formatting test cases.
9
9
open class DiagnosingTestCase : XCTestCase {
10
- /// Set during lint tests to indicate that we should check for unasserted diagnostics when the
11
- /// test is torn down and fail if there were any.
12
- public var shouldCheckForUnassertedDiagnostics = false
13
-
14
- /// A helper that will keep track of the findings that were emitted.
15
- private var consumer = TestingFindingConsumer ( )
16
-
17
- override open func setUp( ) {
18
- shouldCheckForUnassertedDiagnostics = false
19
- }
20
-
21
- override open func tearDown( ) {
22
- guard shouldCheckForUnassertedDiagnostics else { return }
23
-
24
- // This will emit a test failure if a diagnostic is thrown but we don't explicitly call
25
- // XCTAssertDiagnosed for it.
26
- for finding in consumer. emittedFindings {
27
- XCTFail ( " unexpected finding ' \( finding) ' emitted " )
28
- }
29
- }
30
-
31
10
/// Creates and returns a new `Context` from the given syntax tree and configuration.
32
11
///
33
- /// The returned context is configured with a diagnostic consumer that records diagnostics emitted
34
- /// during the tests, which can then be asserted using the `XCTAssertDiagnosed` and
35
- /// `XCTAssertNotDiagnosed` methods.
12
+ /// The returned context is configured with the given finding consumer to record findings emitted
13
+ /// during the tests, so that they can be asserted later using the `assertFindings` method.
36
14
@_spi ( Testing)
37
- public func makeContext( sourceFileSyntax: SourceFileSyntax , configuration: Configuration ? = nil )
38
- -> Context
39
- {
40
- consumer = TestingFindingConsumer ( )
15
+ public func makeContext(
16
+ sourceFileSyntax: SourceFileSyntax ,
17
+ configuration: Configuration ? = nil ,
18
+ findingConsumer: @escaping ( Finding ) -> Void
19
+ ) -> Context {
41
20
let context = Context (
42
21
configuration: configuration ?? Configuration ( ) ,
43
22
operatorTable: . standardOperators,
44
- findingConsumer: consumer . consume ,
23
+ findingConsumer: findingConsumer ,
45
24
fileURL: URL ( fileURLWithPath: " /tmp/test.swift " ) ,
46
25
sourceFileSyntax: sourceFileSyntax,
47
26
ruleNameCache: ruleNameCache)
48
27
return context
49
28
}
50
29
51
- /// Stops tracking diagnostics emitted during formatting/linting.
52
- ///
53
- /// This used by the pretty-printer tests to suppress any diagnostics that might be emitted during
54
- /// the second format pass (which checks for idempotence).
55
- public func stopTrackingDiagnostics( ) {
56
- consumer. stopTrackingFindings ( )
30
+ /// Asserts that the given list of findings matches a set of specs.
31
+ @_spi ( Testing)
32
+ public final func assertFindings(
33
+ expected specs: [ FindingSpec ] ,
34
+ markerLocations: [ String : Int ] ,
35
+ emittedFindings: [ Finding ] ,
36
+ context: Context ,
37
+ file: StaticString = #file,
38
+ line: UInt = #line
39
+ ) {
40
+ var emittedFindings = emittedFindings
41
+
42
+ // Check for a finding that matches each spec, removing it from the array if found.
43
+ for spec in specs {
44
+ assertAndRemoveFinding (
45
+ findingSpec: spec,
46
+ markerLocations: markerLocations,
47
+ emittedFindings: & emittedFindings,
48
+ context: context,
49
+ file: file,
50
+ line: line)
51
+ }
52
+
53
+ // Emit test failures for any findings that did not have matches.
54
+ for finding in emittedFindings {
55
+ let locationString : String
56
+ if let location = finding. location {
57
+ locationString = " line:col \( location. line) : \( location. column) "
58
+ } else {
59
+ locationString = " no location provided "
60
+ }
61
+ XCTFail (
62
+ " Unexpected finding ' \( finding. message) ' was emitted ( \( locationString) ) " ,
63
+ file: file,
64
+ line: line)
65
+ }
57
66
}
58
67
59
- /// Asserts that a specific diagnostic message was emitted.
60
- ///
61
- /// - Parameters:
62
- /// - message: The diagnostic message expected to be emitted.
63
- /// - file: The file in which failure occurred. Defaults to the file name of the test case in
64
- /// which this function was called.
65
- /// - line: The line number on which failure occurred. Defaults to the line number on which this
66
- /// function was called.
67
- public final func XCTAssertDiagnosed(
68
- _ message: Finding . Message ,
69
- line diagnosticLine: Int ? = nil ,
70
- column diagnosticColumn: Int ? = nil ,
68
+ private func assertAndRemoveFinding(
69
+ findingSpec: FindingSpec ,
70
+ markerLocations: [ String : Int ] ,
71
+ emittedFindings: inout [ Finding ] ,
72
+ context: Context ,
71
73
file: StaticString = #file,
72
74
line: UInt = #line
73
75
) {
74
- let wasEmitted : Bool
75
- if let diagnosticLine = diagnosticLine, let diagnosticColumn = diagnosticColumn {
76
- wasEmitted = consumer. popFinding (
77
- containing: message. text, atLine: diagnosticLine, column: diagnosticColumn)
78
- } else {
79
- wasEmitted = consumer. popFinding ( containing: message. text)
76
+ guard let utf8Offset = markerLocations [ findingSpec. marker] else {
77
+ XCTFail ( " Marker ' \( findingSpec. marker) ' was not found in the input " , file: file, line: line)
78
+ return
79
+ }
80
+
81
+ let markerLocation =
82
+ context. sourceLocationConverter. location ( for: AbsolutePosition ( utf8Offset: utf8Offset) )
83
+
84
+ // Find a finding that has the expected line/column location, ignoring the text.
85
+ // FIXME: We do this to provide a better error message if the finding is in the right place but
86
+ // doesn't have the right message, but this also introduces an order-sensitivity among the
87
+ // specs. Fix this if it becomes an issue.
88
+ let maybeIndex = emittedFindings. firstIndex {
89
+ markerLocation. line == $0. location? . line && markerLocation. column == $0. location? . column
90
+ }
91
+ guard let index = maybeIndex else {
92
+ XCTFail (
93
+ """
94
+ Finding ' \( findingSpec. message) ' was not emitted at marker ' \( findingSpec. marker) ' \
95
+ (line:col \( markerLocation. line) : \( markerLocation. column) , offset \( utf8Offset) )
96
+ """ ,
97
+ file: file,
98
+ line: line)
99
+ return
80
100
}
81
- if !wasEmitted {
82
- XCTFail ( " diagnostic ' \( message. text) ' not emitted " , file: file, line: line)
101
+
102
+ // Verify that the finding text also matches what we expect.
103
+ let matchedFinding = emittedFindings. remove ( at: index)
104
+ XCTAssertEqual (
105
+ matchedFinding. message. text,
106
+ findingSpec. message,
107
+ """
108
+ Finding emitted at marker ' \( findingSpec. marker) ' \
109
+ (line:col \( markerLocation. line) : \( markerLocation. column) , offset \( utf8Offset) ) \
110
+ had the wrong message
111
+ """ ,
112
+ file: file,
113
+ line: line)
114
+
115
+ // Assert that a note exists for each of the expected nodes in the finding.
116
+ var emittedNotes = matchedFinding. notes
117
+ for noteSpec in findingSpec. notes {
118
+ assertAndRemoveNote (
119
+ noteSpec: noteSpec,
120
+ markerLocations: markerLocations,
121
+ emittedNotes: & emittedNotes,
122
+ context: context,
123
+ file: file,
124
+ line: line)
125
+ }
126
+
127
+ // Emit test failures for any notes that weren't specified.
128
+ for note in emittedNotes {
129
+ let locationString : String
130
+ if let location = note. location {
131
+ locationString = " line:col \( location. line) : \( location. column) "
132
+ } else {
133
+ locationString = " no location provided "
134
+ }
135
+ XCTFail (
136
+ " Unexpected note ' \( note. message) ' was emitted ( \( locationString) ) " ,
137
+ file: file,
138
+ line: line)
83
139
}
84
140
}
85
141
86
- /// Asserts that a specific diagnostic message was not emitted.
87
- ///
88
- /// - Parameters:
89
- /// - message: The diagnostic message expected to not be emitted.
90
- /// - file: The file in which failure occurred. Defaults to the file name of the test case in
91
- /// which this function was called.
92
- /// - line: The line number on which failure occurred. Defaults to the line number on which this
93
- /// function was called.
94
- public final func XCTAssertNotDiagnosed(
95
- _ message: Finding . Message ,
142
+ private func assertAndRemoveNote(
143
+ noteSpec: NoteSpec ,
144
+ markerLocations: [ String : Int ] ,
145
+ emittedNotes: inout [ Finding . Note ] ,
146
+ context: Context ,
96
147
file: StaticString = #file,
97
148
line: UInt = #line
98
149
) {
99
- let wasEmitted = consumer. popFinding ( containing: message. text)
100
- XCTAssertFalse (
101
- wasEmitted,
102
- " diagnostic ' \( message. text) ' should not have been emitted " ,
103
- file: file, line: line)
150
+ guard let utf8Offset = markerLocations [ noteSpec. marker] else {
151
+ XCTFail ( " Marker ' \( noteSpec. marker) ' was not found in the input " , file: file, line: line)
152
+ return
153
+ }
154
+
155
+ let markerLocation =
156
+ context. sourceLocationConverter. location ( for: AbsolutePosition ( utf8Offset: utf8Offset) )
157
+
158
+ // FIXME: We do this to provide a better error message if the note is in the right place but
159
+ // doesn't have the right message, but this also introduces an order-sensitivity among the
160
+ // specs. Fix this if it becomes an issue.
161
+ let maybeIndex = emittedNotes. firstIndex {
162
+ markerLocation. line == $0. location? . line && markerLocation. column == $0. location? . column
163
+ }
164
+ guard let index = maybeIndex else {
165
+ XCTFail (
166
+ """
167
+ Note ' \( noteSpec. message) ' was not emitted at marker ' \( noteSpec. marker) ' \
168
+ (line:col \( markerLocation. line) : \( markerLocation. column) , offset \( utf8Offset) )
169
+ """ ,
170
+ file: file,
171
+ line: line)
172
+ return
173
+ }
174
+
175
+ // Verify that the note text also matches what we expect.
176
+ let matchedNote = emittedNotes. remove ( at: index)
177
+ XCTAssertEqual (
178
+ matchedNote. message. text,
179
+ noteSpec. message,
180
+ """
181
+ Note emitted at marker ' \( noteSpec. marker) ' \
182
+ (line:col \( markerLocation. line) : \( markerLocation. column) , offset \( utf8Offset) ) \
183
+ had the wrong message
184
+ """ ,
185
+ file: file,
186
+ line: line)
104
187
}
105
188
106
189
/// Asserts that the two strings are equal, providing Unix `diff`-style output if they are not.
@@ -113,7 +196,7 @@ open class DiagnosingTestCase: XCTestCase {
113
196
/// which this function was called.
114
197
/// - line: The line number on which failure occurred. Defaults to the line number on which this
115
198
/// function was called.
116
- public final func XCTAssertStringsEqualWithDiff (
199
+ public final func assertStringsEqualWithDiff (
117
200
_ actual: String ,
118
201
_ expected: String ,
119
202
_ message: String = " " ,
0 commit comments