Skip to content

Commit d212a6f

Browse files
authored
Merge pull request swiftlang#142 from allevato/test-cleanup
Factor out common DiagnosingTestCase support.
2 parents 03c5dec + 6e6ee4c commit d212a6f

File tree

42 files changed

+429
-383
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+429
-383
lines changed

Package.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ let package = Package(
4949
name: "SwiftFormatPrettyPrint",
5050
dependencies: ["SwiftFormatCore", "SwiftFormatConfiguration"]
5151
),
52+
.target(
53+
name: "SwiftFormatTestSupport",
54+
dependencies: ["SwiftFormatCore", "SwiftFormatConfiguration"]
55+
),
5256
.target(
5357
name: "SwiftFormatWhitespaceLinter",
5458
dependencies: [
@@ -78,6 +82,7 @@ let package = Package(
7882
"SwiftFormatCore",
7983
"SwiftFormatPrettyPrint",
8084
"SwiftFormatRules",
85+
"SwiftFormatTestSupport",
8186
"SwiftSyntax",
8287
]
8388
),
@@ -88,6 +93,7 @@ let package = Package(
8893
"SwiftFormatCore",
8994
"SwiftFormatPrettyPrint",
9095
"SwiftFormatRules",
96+
"SwiftFormatTestSupport",
9197
"SwiftSyntax",
9298
]
9399
),
@@ -96,6 +102,7 @@ let package = Package(
96102
dependencies: [
97103
"SwiftFormatConfiguration",
98104
"SwiftFormatCore",
105+
"SwiftFormatTestSupport",
99106
"SwiftFormatWhitespaceLinter",
100107
"SwiftSyntax",
101108
]
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import SwiftFormatConfiguration
2+
import SwiftFormatCore
3+
import SwiftSyntax
4+
import XCTest
5+
6+
/// DiagnosingTestCase is an XCTestCase subclass meant to inject diagnostic-specific testing
7+
/// routines into specific formatting test cases.
8+
open class DiagnosingTestCase: XCTestCase {
9+
/// Set during lint tests to indicate that we should check for unasserted diagnostics when the
10+
/// test is torn down and fail if there were any.
11+
public var shouldCheckForUnassertedDiagnostics = false
12+
13+
/// A helper that will keep track of the diagnostics that were emitted.
14+
private var consumer = DiagnosticTrackingConsumer()
15+
16+
override open func setUp() {
17+
shouldCheckForUnassertedDiagnostics = false
18+
}
19+
20+
override open func tearDown() {
21+
guard shouldCheckForUnassertedDiagnostics else { return }
22+
23+
// This will emit a test failure if a diagnostic is thrown but we don't explicitly call
24+
// XCTAssertDiagnosed for it.
25+
for diag in consumer.emittedDiagnostics {
26+
XCTFail("unexpected diagnostic '\(diag)' emitted")
27+
}
28+
}
29+
30+
/// Creates and returns a new `Context` from the given syntax tree and configuration.
31+
///
32+
/// The returned context is configured with a diagnostic consumer that records diagnostics emitted
33+
/// during the tests, which can then be asserted using the `XCTAssertDiagnosed` and
34+
/// `XCTAssertNotDiagnosed` methods.
35+
public func makeContext(sourceFileSyntax: SourceFileSyntax, configuration: Configuration? = nil)
36+
-> Context
37+
{
38+
let context = Context(
39+
configuration: configuration ?? Configuration(),
40+
diagnosticEngine: DiagnosticEngine(),
41+
fileURL: URL(fileURLWithPath: "/tmp/test.swift"),
42+
sourceFileSyntax: sourceFileSyntax)
43+
consumer = DiagnosticTrackingConsumer()
44+
context.diagnosticEngine?.addConsumer(consumer)
45+
return context
46+
}
47+
48+
/// Stops tracking diagnostics emitted during formatting/linting.
49+
///
50+
/// This used by the pretty-printer tests to suppress any diagnostics that might be emitted during
51+
/// the second format pass (which checks for idempotence).
52+
public func stopTrackingDiagnostics() {
53+
consumer.stopTrackingDiagnostics()
54+
}
55+
56+
/// Asserts that a specific diagnostic message was emitted.
57+
///
58+
/// - Parameters:
59+
/// - message: The diagnostic message expected to be emitted.
60+
/// - file: The file in which failure occurred. Defaults to the file name of the test case in
61+
/// which this function was called.
62+
/// - line: The line number on which failure occurred. Defaults to the line number on which this
63+
/// function was called.
64+
public final func XCTAssertDiagnosed(
65+
_ message: Diagnostic.Message,
66+
line diagnosticLine: Int? = nil,
67+
column diagnosticColumn: Int? = nil,
68+
file: StaticString = #file,
69+
line: UInt = #line
70+
) {
71+
let wasEmitted: Bool
72+
if let diagnosticLine = diagnosticLine, let diagnosticColumn = diagnosticColumn {
73+
wasEmitted = consumer.popDiagnostic(
74+
containing: message.text, atLine: diagnosticLine, column: diagnosticColumn)
75+
} else {
76+
wasEmitted = consumer.popDiagnostic(containing: message.text)
77+
}
78+
if !wasEmitted {
79+
XCTFail("diagnostic '\(message.text)' not emitted", file: file, line: line)
80+
}
81+
}
82+
83+
/// Asserts that a specific diagnostic message was not emitted.
84+
///
85+
/// - Parameters:
86+
/// - message: The diagnostic message expected to not be emitted.
87+
/// - file: The file in which failure occurred. Defaults to the file name of the test case in
88+
/// which this function was called.
89+
/// - line: The line number on which failure occurred. Defaults to the line number on which this
90+
/// function was called.
91+
public final func XCTAssertNotDiagnosed(
92+
_ message: Diagnostic.Message,
93+
file: StaticString = #file,
94+
line: UInt = #line
95+
) {
96+
let wasEmitted = consumer.popDiagnostic(containing: message.text)
97+
XCTAssertFalse(
98+
wasEmitted,
99+
"diagnostic '\(message.text)' should not have been emitted",
100+
file: file, line: line)
101+
}
102+
103+
/// Asserts that the two strings are equal, providing Unix `diff`-style output if they are not.
104+
///
105+
/// - Parameters:
106+
/// - actual: The actual string.
107+
/// - expected: The expected string.
108+
/// - message: An optional description of the failure.
109+
/// - file: The file in which failure occurred. Defaults to the file name of the test case in
110+
/// which this function was called.
111+
/// - line: The line number on which failure occurred. Defaults to the line number on which this
112+
/// function was called.
113+
public final func XCTAssertStringsEqualWithDiff(
114+
_ actual: String,
115+
_ expected: String,
116+
_ message: String = "",
117+
file: StaticString = #file,
118+
line: UInt = #line
119+
) {
120+
// Use `CollectionDifference` on supported platforms to get `diff`-like line-based output. On
121+
// older platforms, fall back to simple string comparison.
122+
if #available(macOS 10.15, *) {
123+
let actualLines = actual.components(separatedBy: .newlines)
124+
let expectedLines = expected.components(separatedBy: .newlines)
125+
126+
let difference = actualLines.difference(from: expectedLines)
127+
if difference.isEmpty { return }
128+
129+
var result = ""
130+
131+
var insertions = [Int: String]()
132+
var removals = [Int: String]()
133+
134+
for change in difference {
135+
switch change {
136+
case .insert(let offset, let element, _):
137+
insertions[offset] = element
138+
case .remove(let offset, let element, _):
139+
removals[offset] = element
140+
}
141+
}
142+
143+
var expectedLine = 0
144+
var actualLine = 0
145+
146+
while expectedLine < expectedLines.count || actualLine < actualLines.count {
147+
if let removal = removals[expectedLine] {
148+
result += "-\(removal)\n"
149+
expectedLine += 1
150+
} else if let insertion = insertions[actualLine] {
151+
result += "+\(insertion)\n"
152+
actualLine += 1
153+
} else {
154+
result += " \(expectedLines[expectedLine])\n"
155+
expectedLine += 1
156+
actualLine += 1
157+
}
158+
}
159+
160+
let failureMessage = "Actual output (+) differed from expected output (-):\n\(result)"
161+
let fullMessage = message.isEmpty ? failureMessage : "\(message) - \(failureMessage)"
162+
XCTFail(fullMessage, file: file, line: line)
163+
} else {
164+
// Fall back to simple string comparison on platforms that don't support CollectionDifference.
165+
let failureMessage = "Actual output differed from expected output:"
166+
let fullMessage = message.isEmpty ? failureMessage : "\(message) - \(failureMessage)"
167+
XCTAssertEqual(actual, expected, fullMessage, file: file, line: line)
168+
}
169+
}
170+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import SwiftSyntax
14+
15+
/// Information about a diagnostic tracked by `DiagnosticTrackingConsumer`.
16+
struct EmittedDiagnostic {
17+
/// The message text of the diagnostic.
18+
var message: String
19+
20+
/// The line number of the diagnostic, if it was provided.
21+
var line: Int?
22+
23+
/// The column number of the diagnostic, if it was provided.
24+
var column: Int?
25+
26+
/// Creates an emitted diagnostic from the given SwiftSyntax `Diagnostic`.
27+
init(_ diagnostic: Diagnostic) {
28+
self.message = diagnostic.message.text
29+
self.line = diagnostic.location?.line
30+
self.column = diagnostic.location?.column
31+
}
32+
33+
/// Creates an emitted diagnostic from the given SwiftSyntax `Note`.
34+
init(_ note: Note) {
35+
self.message = note.message.text
36+
self.line = note.location?.line
37+
self.column = note.location?.column
38+
}
39+
}
40+
41+
/// Tracks the diagnostics that were emitted and allows them to be .
42+
class DiagnosticTrackingConsumer: DiagnosticConsumer {
43+
/// The diagnostics that have been emitted.
44+
private(set) var emittedDiagnostics = [EmittedDiagnostic]()
45+
46+
/// Indicates whether diagnostics are being tracked.
47+
private var isTracking = true
48+
49+
func handle(_ diagnostic: Diagnostic) {
50+
guard isTracking else { return }
51+
52+
emittedDiagnostics.append(EmittedDiagnostic(diagnostic))
53+
for note in diagnostic.notes {
54+
emittedDiagnostics.append(EmittedDiagnostic(note))
55+
}
56+
}
57+
58+
func finalize() {}
59+
60+
/// Pops the first diagnostic that contains the given text and occurred at the given location from
61+
/// the collection of emitted diagnostics, if possible.
62+
///
63+
/// - Parameters:
64+
/// - text: The message text to match.
65+
/// - line: The expected line number of the diagnostic.
66+
/// - column: The expected column number of the diagnostic.
67+
/// - Returns: True if a diagnostic was found and popped, or false otherwise.
68+
func popDiagnostic(containing text: String, atLine line: Int, column: Int) -> Bool {
69+
let maybeIndex = emittedDiagnostics.firstIndex {
70+
$0.message.contains(text) && line == $0.line && column == $0.column
71+
}
72+
guard let index = maybeIndex else { return false }
73+
74+
emittedDiagnostics.remove(at: index)
75+
return true
76+
}
77+
78+
/// Pops the first diagnostic that contains the given text (regardless of location) from the
79+
/// collection of emitted diagnostics, if possible.
80+
///
81+
/// - Parameter text: The message text to match.
82+
/// - Returns: True if a diagnostic was found and popped, or false otherwise.
83+
func popDiagnostic(containing text: String) -> Bool {
84+
let maybeIndex = emittedDiagnostics.firstIndex { $0.message.contains(text) }
85+
guard let index = maybeIndex else { return false }
86+
87+
emittedDiagnostics.remove(at: index)
88+
return true
89+
}
90+
91+
/// Stops tracking diagnostics.
92+
func stopTrackingDiagnostics() {
93+
isTracking = false
94+
}
95+
}

0 commit comments

Comments
 (0)