diff --git a/Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift b/Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift index 5a3fcdbd2c8..167900d68e0 100644 --- a/Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift +++ b/Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift @@ -34,8 +34,10 @@ public struct NoteSpec { public let column: Int /// The file and line at which this ``NoteSpec`` was created, so that assertion failures can be reported at its location. - internal let originatorFile: StaticString + internal let originatorFileID: String + internal let originatorFilePath: StaticString internal let originatorLine: UInt + internal let originatorColumn: UInt /// Creates a new ``NoteSpec`` that describes a note tests are expecting to be generated by a macro expansion. /// @@ -43,32 +45,65 @@ public struct NoteSpec { /// - message: The expected message of the note /// - line: The line to which the note is expected to point /// - column: The column to which the note is expected to point - /// - originatorFile: The file at which this ``NoteSpec`` was created, so that assertion failures can be reported at its location. + /// - originatorFileID: The file ID at which this ``NoteSpec`` was created, so that assertion failures can be reported at its location. + /// - originatorFilePath: The file path at which this ``NoteSpec`` was created, so that assertion failures can be reported at its location. /// - originatorLine: The line at which this ``NoteSpec`` was created, so that assertion failures can be reported at its location. + /// - originatorColumn: The column at which this ``NoteSpec`` was created, so that assertion failures can be reported at its location. public init( message: String, line: Int, column: Int, - originatorFile: StaticString = #file, - originatorLine: UInt = #line + originatorFileID: String = #fileID, + originatorFilePath: StaticString = #filePath, + originatorLine: UInt = #line, + originatorColumn: UInt = #column ) { self.message = message self.line = line self.column = column - self.originatorFile = originatorFile + self.originatorFileID = originatorFileID + self.originatorFilePath = originatorFilePath self.originatorLine = originatorLine + self.originatorColumn = originatorColumn } } func assertNote( _ note: Note, in expansionContext: BasicMacroExpansionContext, - expected spec: NoteSpec + expected spec: NoteSpec, + failingWith failureHandler: ((_ message: String, _ fileID: String, _ filePath: StaticString, _ line: UInt, _ column: UInt) -> Void)? = nil ) { - assertStringsEqualWithDiff(note.message, spec.message, "message of note does not match", file: spec.originatorFile, line: spec.originatorLine) + let failureHandler = failureHandler ?? defaultFailureHandler + if note.message != spec.message { + let message = describeDifferenceBetweenStrings(note.message, spec.message, "message of note does not match") + failureHandler( + message, + spec.originatorFileID, + spec.originatorFilePath, + spec.originatorLine, + spec.originatorColumn + ) + } let location = expansionContext.location(for: note.position, anchoredAt: note.node, fileName: "") - XCTAssertEqual(location.line, spec.line, "line of note does not match", file: spec.originatorFile, line: spec.originatorLine) - XCTAssertEqual(location.column, spec.column, "column of note does not match", file: spec.originatorFile, line: spec.originatorLine) + if location.line != spec.line { + failureHandler( + "line \(location.line) of note does not match \(spec.line)", + spec.originatorFileID, + spec.originatorFilePath, + spec.originatorLine, + spec.originatorColumn + ) + } + if location.column != spec.column { + failureHandler( + "column \(location.column) of note does not match \(spec.column)", + spec.originatorFileID, + spec.originatorFilePath, + spec.originatorLine, + spec.originatorColumn + ) + } } // MARK: - Fix-It @@ -82,31 +117,50 @@ public struct FixItSpec { public let message: String /// The file and line at which this ``NoteSpec`` was created, so that assertion failures can be reported at its location. - internal let originatorFile: StaticString + internal let originatorFileID: String + internal let originatorFilePath: StaticString internal let originatorLine: UInt + internal let originatorColumn: UInt /// Creates a new ``FixItSpec`` that describes a Fix-It tests are expecting to be generated by a macro expansion. /// /// - Parameters: /// - message: The expected message of the note - /// - originatorFile: The file at which this ``NoteSpec`` was created, so that assertion failures can be reported at its location. + /// - originatorFileID: The file ID at which this ``NoteSpec`` was created, so that assertion failures can be reported at its location. + /// - originatorFilePath: The file path at which this ``NoteSpec`` was created, so that assertion failures can be reported at its location. /// - originatorLine: The line at which this ``NoteSpec`` was created, so that assertion failures can be reported at its location. + /// - originatorColumn: The column at which this ``NoteSpec`` was created, so that assertion failures can be reported at its location. public init( message: String, - originatorFile: StaticString = #file, - originatorLine: UInt = #line + originatorFileID: String = #fileID, + originatorFilePath: StaticString = #filePath, + originatorLine: UInt = #line, + originatorColumn: UInt = #column ) { self.message = message - self.originatorFile = originatorFile + self.originatorFileID = originatorFileID + self.originatorFilePath = originatorFilePath self.originatorLine = originatorLine + self.originatorColumn = originatorColumn } } func assertFixIt( _ fixIt: FixIt, - expected spec: FixItSpec + expected spec: FixItSpec, + failingWith failureHandler: ((_ message: String, _ fileID: String, _ filePath: StaticString, _ line: UInt, _ column: UInt) -> Void)? = nil ) { - assertStringsEqualWithDiff(fixIt.message.message, spec.message, "message of Fix-It does not match", file: spec.originatorFile, line: spec.originatorLine) + let failureHandler = failureHandler ?? defaultFailureHandler + if fixIt.message.message != spec.message { + let message = describeDifferenceBetweenStrings(fixIt.message.message, spec.message, "message of Fix-It does not match") + failureHandler( + message, + spec.originatorFileID, + spec.originatorFilePath, + spec.originatorLine, + spec.originatorColumn + ) + } } // MARK: - Diagnostic @@ -138,8 +192,10 @@ public struct DiagnosticSpec { public let fixIts: [FixItSpec] /// The file and line at which this ``NoteSpec`` was created, so that assertion failures can be reported at its location. - internal let originatorFile: StaticString + internal let originatorFileID: String + internal let originatorFilePath: StaticString internal let originatorLine: UInt + internal let originatorColumn: UInt /// Creates a new ``DiagnosticSpec`` that describes a diagnostic tests are expecting to be generated by a macro expansion. /// @@ -152,8 +208,10 @@ public struct DiagnosticSpec { /// - highlights: If not empty, the text fragments the diagnostic is expected to highlight /// - notes: The notes that are expected to be attached to the diagnostic /// - fixIts: The messages of the Fix-Its the diagnostic is expected to produce - /// - originatorFile: The file at which this ``NoteSpec`` was created, so that assertion failures can be reported at its location. + /// - originatorFileID: The file ID at which this ``NoteSpec`` was created, so that assertion failures can be reported at its location. + /// - originatorFilePath: The file path at which this ``NoteSpec`` was created, so that assertion failures can be reported at its location. /// - originatorLine: The line at which this ``NoteSpec`` was created, so that assertion failures can be reported at its location. + /// - originatorColumn: The column at which this ``NoteSpec`` was created, so that assertion failures can be reported at its location. public init( id: MessageID? = nil, message: String, @@ -163,8 +221,10 @@ public struct DiagnosticSpec { highlights: [String]? = nil, notes: [NoteSpec] = [], fixIts: [FixItSpec] = [], - originatorFile: StaticString = #file, - originatorLine: UInt = #line + originatorFileID: String = #fileID, + originatorFilePath: StaticString = #filePath, + originatorLine: UInt = #line, + originatorColumn: UInt = #column ) { self.id = id self.message = message @@ -174,8 +234,10 @@ public struct DiagnosticSpec { self.highlights = highlights self.notes = notes self.fixIts = fixIts - self.originatorFile = originatorFile + self.originatorFileID = originatorFileID + self.originatorFilePath = originatorFilePath self.originatorLine = originatorLine + self.originatorColumn = originatorColumn } } @@ -189,7 +251,7 @@ extension DiagnosticSpec { } @_disfavoredOverload - @available(*, deprecated, message: "Use init(id:message:line:column:severity:highlights:notes:fixIts:originatorFile:originatorLine:) instead") + @available(*, deprecated, message: "Use init(id:message:line:column:severity:highlights:notes:fixIts:originatorFileID:originatorFilePath:originatorLine:originatorColumn:) instead") public init( id: MessageID? = nil, message: String, @@ -218,70 +280,132 @@ extension DiagnosticSpec { func assertDiagnostic( _ diag: Diagnostic, in expansionContext: BasicMacroExpansionContext, - expected spec: DiagnosticSpec + expected spec: DiagnosticSpec, + failingWith failureHandler: ((_ message: String, _ fileID: String, _ filePath: StaticString, _ line: UInt, _ column: UInt) -> Void)? = nil ) { - if let id = spec.id { - XCTAssertEqual(diag.diagnosticID, id, "diagnostic ID does not match", file: spec.originatorFile, line: spec.originatorLine) + let failureHandler = failureHandler ?? defaultFailureHandler + + if let id = spec.id, diag.diagnosticID != id { + failureHandler( + "diagnostic ID \(diag.diagnosticID) does not match \(id)", + spec.originatorFileID, + spec.originatorFilePath, + spec.originatorLine, + spec.originatorColumn + ) + } + if diag.message != spec.message { + let message = describeDifferenceBetweenStrings(diag.message, spec.message, "message does not match") + failureHandler( + message, + spec.originatorFileID, + spec.originatorFilePath, + spec.originatorLine, + spec.originatorColumn + ) } - assertStringsEqualWithDiff(diag.message, spec.message, "message does not match", file: spec.originatorFile, line: spec.originatorLine) let location = expansionContext.location(for: diag.position, anchoredAt: diag.node, fileName: "") - XCTAssertEqual(location.line, spec.line, "line does not match", file: spec.originatorFile, line: spec.originatorLine) - XCTAssertEqual(location.column, spec.column, "column does not match", file: spec.originatorFile, line: spec.originatorLine) - - XCTAssertEqual(spec.severity, diag.diagMessage.severity, "severity does not match", file: spec.originatorFile, line: spec.originatorLine) + if location.line != spec.line { + failureHandler( + "line \(location.line) does not match \(spec.line)", + spec.originatorFileID, + spec.originatorFilePath, + spec.originatorLine, + spec.originatorColumn + ) + } + if location.column != spec.column { + failureHandler( + "column \(location.column) does not match \(spec.column)", + spec.originatorFileID, + spec.originatorFilePath, + spec.originatorLine, + spec.originatorColumn + ) + } + if spec.severity != diag.diagMessage.severity { + failureHandler( + "severity \(spec.severity) does not match \(diag.diagMessage.severity)", + spec.originatorFileID, + spec.originatorFilePath, + spec.originatorLine, + spec.originatorColumn + ) + } if let highlights = spec.highlights { if diag.highlights.count != highlights.count { - XCTFail( + failureHandler( """ Expected \(highlights.count) highlights but received \(diag.highlights.count): \(diag.highlights.map(\.trimmedDescription).joined(separator: "\n")) """, - file: spec.originatorFile, - line: spec.originatorLine + spec.originatorFileID, + spec.originatorFilePath, + spec.originatorLine, + spec.originatorColumn ) } else { - for (actual, expected) in zip(diag.highlights, highlights) { - assertStringsEqualWithDiff( - actual.trimmedDescription, - expected, - "highlight does not match", - file: spec.originatorFile, - line: spec.originatorLine - ) + for (actual, expected) in zip(diag.highlights.lazy.map(\.trimmedDescription), highlights) { + if actual != expected { + let message = describeDifferenceBetweenStrings(actual, expected, "highlight does not match") + failureHandler( + message, + spec.originatorFileID, + spec.originatorFilePath, + spec.originatorLine, + spec.originatorColumn + ) + } } } } if diag.notes.count != spec.notes.count { - XCTFail( + failureHandler( """ Expected \(spec.notes.count) notes but received \(diag.notes.count): \(diag.notes.map(\.debugDescription).joined(separator: "\n")) """, - file: spec.originatorFile, - line: spec.originatorLine + spec.originatorFileID, + spec.originatorFilePath, + spec.originatorLine, + spec.originatorColumn ) } else { for (note, expectedNote) in zip(diag.notes, spec.notes) { - assertNote(note, in: expansionContext, expected: expectedNote) + assertNote(note, in: expansionContext, expected: expectedNote, failingWith: failureHandler) } } if diag.fixIts.count != spec.fixIts.count { - XCTFail( + failureHandler( """ Expected \(spec.fixIts.count) Fix-Its but received \(diag.fixIts.count): \(diag.fixIts.map(\.message.message).joined(separator: "\n")) """, - file: spec.originatorFile, - line: spec.originatorLine + spec.originatorFileID, + spec.originatorFilePath, + spec.originatorLine, + spec.originatorColumn ) } else { for (fixIt, expectedFixIt) in zip(diag.fixIts, spec.fixIts) { - assertFixIt(fixIt, expected: expectedFixIt) + assertFixIt(fixIt, expected: expectedFixIt, failingWith: failureHandler) } } } +private func defaultFailureHandler( + _ message: String, + fileID: String = #fileID, + filePath: StaticString = #filePath, + line: UInt = #line, + column: UInt = #column +) { + // FIXME: detect if XCTest is currently running a test and only call XCTFail() if it is. + // FIXME: support swift-testing. + XCTFail(message, file: filePath, line: line) +} + /// Assert that expanding the given macros in the original source produces /// the given expanded source code. /// @@ -299,6 +423,7 @@ func assertDiagnostic( /// - testModuleName: The name of the test module to use. /// - testFileName: The name of the test file name to use. /// - indentationWidth: The indentation width used in the expansion. +/// - failureHandler: A closure to call if the assertion fails. This closure may be called more than once. If `nil`, `XCTFail()` is called. public func assertMacroExpansion( _ originalSource: String, expandedSource expectedExpandedSource: String, @@ -309,9 +434,14 @@ public func assertMacroExpansion( testModuleName: String = "TestModule", testFileName: String = "test.swift", indentationWidth: Trivia = .spaces(4), - file: StaticString = #file, - line: UInt = #line + failingWith failureHandler: ((_ message: String, _ fileID: String, _ filePath: StaticString, _ line: UInt, _ column: UInt) -> Void)? = nil, + fileID: String = #fileID, + filePath: StaticString = #filePath, + line: UInt = #line, + column: UInt = #column ) { + let failureHandler = failureHandler ?? defaultFailureHandler + // Parse the original source file. let origSourceFile = Parser.parse(source: originalSource) @@ -323,7 +453,7 @@ public func assertMacroExpansion( let expandedSourceFile = origSourceFile.expand(macros: macros, in: context, indentationWidth: indentationWidth) let diags = ParseDiagnosticsGenerator.diagnostics(for: expandedSourceFile) if !diags.isEmpty { - XCTFail( + failureHandler( """ Expanded source should not contain any syntax errors, but contains: \(DiagnosticsFormatter.annotatedSource(tree: expandedSourceFile, diags: diags)) @@ -331,35 +461,44 @@ public func assertMacroExpansion( Expanded syntax tree was: \(expandedSourceFile.debugDescription) """, - file: file, - line: line + fileID, + filePath, + line, + column ) } - assertStringsEqualWithDiff( - expandedSourceFile.description.trimmingCharacters(in: .newlines), - expectedExpandedSource.trimmingCharacters(in: .newlines), - "Macro expansion did not produce the expected expanded source", - additionalInfo: """ - Actual expanded source: - \(expandedSourceFile) - """, - file: file, - line: line - ) + do { + let expandedSourceFile = expandedSourceFile.description.trimmingCharacters(in: .newlines) + let expectedExpandedSource = expectedExpandedSource.trimmingCharacters(in: .newlines) + if expandedSourceFile != expectedExpandedSource { + let message = describeDifferenceBetweenStrings( + expandedSourceFile, + expectedExpandedSource, + "Macro expansion did not produce the expected expanded source", + additionalInfo: """ + Actual expanded source: + \(expandedSourceFile) + """ + ) + failureHandler(message, fileID, filePath, line, column) + } + } if context.diagnostics.count != diagnostics.count { - XCTFail( + failureHandler( """ Expected \(diagnostics.count) diagnostics but received \(context.diagnostics.count): \(context.diagnostics.map(\.debugDescription).joined(separator: "\n")) """, - file: file, - line: line + fileID, + filePath, + line, + column ) } else { for (actualDiag, expectedDiag) in zip(context.diagnostics, diagnostics) { - assertDiagnostic(actualDiag, in: context, expected: expectedDiag) + assertDiagnostic(actualDiag, in: context, expected: expectedDiag, failingWith: failureHandler) } } @@ -376,12 +515,14 @@ public func assertMacroExpansion( let fixedTree = FixItApplier.apply(edits: edits, to: origSourceFile) let fixedTreeDescription = fixedTree.description - assertStringsEqualWithDiff( - fixedTreeDescription.trimmingTrailingWhitespace(), - expectedFixedSource.trimmingTrailingWhitespace(), - file: file, - line: line - ) + do { + let fixedTreeDescription = fixedTreeDescription.trimmingTrailingWhitespace() + let expectedFixedSource = expectedFixedSource.trimmingTrailingWhitespace() + if fixedTreeDescription != expectedFixedSource { + let message = describeDifferenceBetweenStrings(fixedTreeDescription, expectedFixedSource) + failureHandler(message, fileID, filePath, line, column) + } + } } } diff --git a/Sources/_SwiftSyntaxTestSupport/AssertEqualWithDiff.swift b/Sources/_SwiftSyntaxTestSupport/AssertEqualWithDiff.swift index f230e86690a..6b81fd41498 100644 --- a/Sources/_SwiftSyntaxTestSupport/AssertEqualWithDiff.swift +++ b/Sources/_SwiftSyntaxTestSupport/AssertEqualWithDiff.swift @@ -88,6 +88,16 @@ public func failStringsEqualWithDiff( file: StaticString = #file, line: UInt = #line ) { + let fullMessage = describeDifferenceBetweenStrings(actual, expected, message, additionalInfo: additionalInfo()) + XCTFail(fullMessage, file: file, line: line) +} + +public func describeDifferenceBetweenStrings( + _ actual: String, + _ expected: String, + _ message: String = "", + additionalInfo: @autoclosure () -> String? = nil +) -> String { let stringComparison: String // Use `CollectionDifference` on supported platforms to get `diff`-like line-based output. On @@ -151,5 +161,5 @@ public func failStringsEqualWithDiff( \(additional) """ } - XCTFail(fullMessage, file: file, line: line) + return fullMessage }