From 951cab6905b04e4695ec8f6b0e3ff53551845a74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20R=C3=B6nnqvist?= Date: Thu, 20 Nov 2025 18:23:32 +0000 Subject: [PATCH 1/4] Add data loading test helpers for Swift Testing --- .../Testing+LoadingTestData.swift | 124 ++++++++++++ .../Testing+ParseDirective.swift | 183 ++++++++++++++++++ 2 files changed, 307 insertions(+) create mode 100644 Tests/SwiftDocCTests/Testing+LoadingTestData.swift create mode 100644 Tests/SwiftDocCTests/Testing+ParseDirective.swift diff --git a/Tests/SwiftDocCTests/Testing+LoadingTestData.swift b/Tests/SwiftDocCTests/Testing+LoadingTestData.swift new file mode 100644 index 0000000000..839f543916 --- /dev/null +++ b/Tests/SwiftDocCTests/Testing+LoadingTestData.swift @@ -0,0 +1,124 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2021-2025 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 Swift project authors + */ + +import Foundation +import Testing +@testable import SwiftDocC +import Markdown +import SwiftDocCTestUtilities + +// MARK: Using an in-memory file system + +/// Loads a documentation catalog from an in-memory test file system. +/// +/// - Parameters: +/// - catalog: The directory structure of the documentation catalog +/// - otherFileSystemDirectories: Any other directories in the test file system. +/// - diagnosticFilterLevel: The minimum severity for diagnostics to emit. +/// - logOutput: An output stream to capture log output from creating the context. +/// - configuration: Configuration for the created context. +/// - Returns: The loaded documentation bundle and context for the given catalog input. +func load( + catalog: Folder, + otherFileSystemDirectories: [Folder] = [], + diagnosticFilterLevel: DiagnosticSeverity = .warning, + logOutput: some TextOutputStream = LogHandle.none, + configuration: DocumentationContext.Configuration = .init() +) async throws -> DocumentationContext { + let fileSystem = try TestFileSystem(folders: [catalog] + otherFileSystemDirectories) + let catalogURL = URL(fileURLWithPath: "/\(catalog.name)") + + let diagnosticEngine = DiagnosticEngine(filterLevel: diagnosticFilterLevel) + diagnosticEngine.add(DiagnosticConsoleWriter(logOutput, formattingOptions: [], baseURL: catalogURL, highlight: true, dataProvider: fileSystem)) + + let (inputs, dataProvider) = try DocumentationContext.InputsProvider(fileManager: fileSystem) + .inputsAndDataProvider(startingPoint: catalogURL, options: .init()) + + defer { + diagnosticEngine.flush() // Write to the logOutput + } + return try await DocumentationContext(bundle: inputs, dataProvider: dataProvider, diagnosticEngine: diagnosticEngine, configuration: configuration) +} + +func makeEmptyContext() async throws -> DocumentationContext { + let bundle = DocumentationBundle( + info: DocumentationBundle.Info( + displayName: "Test", + id: "com.example.test" + ), + baseURL: URL(string: "https://example.com/example")!, + symbolGraphURLs: [], + markupURLs: [], + miscResourceURLs: [] + ) + + return try await DocumentationContext(bundle: bundle, dataProvider: TestFileSystem(folders: [])) +} + +// MARK: Using the real file system + +/// Loads a documentation bundle from the given source URL and creates a documentation context. +func loadFromDisk( + catalogURL: URL, + externalResolvers: [DocumentationBundle.Identifier: any ExternalDocumentationSource] = [:], + externalSymbolResolver: (any GlobalExternalSymbolResolver)? = nil, + fallbackResolver: (any ConvertServiceFallbackResolver)? = nil, + diagnosticEngine: DiagnosticEngine = .init(filterLevel: .hint), + configuration: DocumentationContext.Configuration = .init() +) async throws -> DocumentationContext { + var configuration = configuration + configuration.externalDocumentationConfiguration.sources = externalResolvers + configuration.externalDocumentationConfiguration.globalSymbolResolver = externalSymbolResolver + configuration.convertServiceConfiguration.fallbackResolver = fallbackResolver + configuration.externalMetadata.diagnosticLevel = diagnosticEngine.filterLevel + + let (bundle, dataProvider) = try DocumentationContext.InputsProvider() + .inputsAndDataProvider(startingPoint: catalogURL, options: .init()) + + return try await DocumentationContext(bundle: bundle, dataProvider: dataProvider, diagnosticEngine: diagnosticEngine, configuration: configuration) +} + +func loadFromDisk( + catalogName: String, + externalResolvers: [DocumentationBundle.Identifier: any ExternalDocumentationSource] = [:], + fallbackResolver: (any ConvertServiceFallbackResolver)? = nil, + configuration: DocumentationContext.Configuration = .init(), + sourceLocation: Testing.SourceLocation = #_sourceLocation +) async throws -> DocumentationContext { + try await loadFromDisk( + catalogURL: try #require(Bundle.module.url(forResource: catalogName, withExtension: "docc", subdirectory: "Test Bundles"), sourceLocation: sourceLocation), + externalResolvers: externalResolvers, + fallbackResolver: fallbackResolver, + configuration: configuration + ) +} + +// MARK: Render node loading helpers + +func renderNode(atPath path: String, fromOnDiskTestCatalogNamed catalogName: String, sourceLocation: Testing.SourceLocation = #_sourceLocation) async throws -> RenderNode { + let context = try await loadFromDisk(catalogName: catalogName) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: path, sourceLanguage: .swift)) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) + return try #require(translator.visit(node.semantic) as? RenderNode, sourceLocation: sourceLocation) +} + +extension RenderNode { + func applying(variant: String) throws -> RenderNode { + let variantData = try RenderNodeVariantOverridesApplier().applyVariantOverrides( + in: RenderJSONEncoder.makeEncoder().encode(self), + for: [.interfaceLanguage(variant)] + ) + + return try RenderJSONDecoder.makeDecoder().decode( + RenderNode.self, + from: variantData + ) + } +} diff --git a/Tests/SwiftDocCTests/Testing+ParseDirective.swift b/Tests/SwiftDocCTests/Testing+ParseDirective.swift new file mode 100644 index 0000000000..7672b6249a --- /dev/null +++ b/Tests/SwiftDocCTests/Testing+ParseDirective.swift @@ -0,0 +1,183 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2021-2025 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 Swift project authors + */ + +import Foundation +import Testing +@testable import SwiftDocC +import Markdown +import SwiftDocCTestUtilities + +// MARK: Using an in-memory file system + +func parseDirective( + _ directive: Directive.Type, + content: () -> String, + sourceLocation: Testing.SourceLocation = #_sourceLocation +) async throws -> (problemIdentifiers: [String], directive: Directive?) { + let source = URL(fileURLWithPath: "/path/to/test-source-\(ProcessInfo.processInfo.globallyUniqueString)") + let document = Document(parsing: content(), source: source, options: .parseBlockDirectives) + + let blockDirectiveContainer = try #require(document.child(at: 0) as? BlockDirective, sourceLocation: sourceLocation) + + var problems = [Problem]() + let inputs = try await makeEmptyContext().inputs + let directive = directive.init(from: blockDirectiveContainer, source: source, for: inputs, problems: &problems) + + let problemIDs = problems.map { problem -> String in + #expect(problem.diagnostic.source != nil, "Problem \(problem.diagnostic.identifier) is missing a source URL.", sourceLocation: sourceLocation) + let line = problem.diagnostic.range?.lowerBound.line.description ?? "unknown-line" + + return "\(line): \(problem.diagnostic.severity) – \(problem.diagnostic.identifier)" + }.sorted() + + return (problemIDs, directive) +} + +func parseDirective( + _ directive: Directive.Type, + catalog: Folder, + content: () -> String, + sourceLocation: Testing.SourceLocation = #_sourceLocation +) async throws -> ( + renderBlockContent: [RenderBlockContent], + problemIdentifiers: [String], + directive: Directive?, + collectedReferences: [String : any RenderReference] +) { + let context = try await load(catalog: catalog) + return try parseDirective(directive, context: context, content: content, sourceLocation: sourceLocation) +} + +func parseDirective( + _ directive: Directive.Type, + withAvailableAssetNames assetNames: [String], + content: () -> String, + sourceLocation: Testing.SourceLocation = #_sourceLocation +) async throws -> (renderBlockContent: [RenderBlockContent], problemIdentifiers: [String], directive: Directive?) { + let context = try await load(catalog: Folder(name: "Something.docc", content: assetNames.map { + DataFile(name: $0, data: Data()) + })) + + let (renderedContent, problems, directive, _) = try parseDirective(directive, context: context, content: content, sourceLocation: sourceLocation) + return (renderedContent, problems, directive) +} + +// MARK: Using the real file system + +func parseDirective( + _ directive: Directive.Type, + loadingOnDiskCatalogNamed catalogName: String? = nil, + content: () -> String, + sourceLocation: Testing.SourceLocation = #_sourceLocation +) async throws -> ( + renderBlockContent: [RenderBlockContent], + problemIdentifiers: [String], + directive: Directive? +) { + let (renderedContent, problems, directive, _) = try await parseDirective(directive, loadingOnDiskCatalogNamed: catalogName, content: content, sourceLocation: sourceLocation) + return (renderedContent, problems, directive) +} + +func parseDirective( + _ directive: Directive.Type, + loadingOnDiskCatalogNamed catalogName: String? = nil, + content: () -> String, + sourceLocation: Testing.SourceLocation = #_sourceLocation +) async throws -> ( + renderBlockContent: [RenderBlockContent], + problemIdentifiers: [String], + directive: Directive?, + collectedReferences: [String : any RenderReference] +) { + let context: DocumentationContext = if let catalogName { + try await loadFromDisk(catalogName: catalogName) + } else { + try await makeEmptyContext() + } + return try parseDirective(directive, context: context, content: content, sourceLocation: sourceLocation) +} + +// MARK: - Private implementation + +private func parseDirective( + _ directive: Directive.Type, + context: DocumentationContext, + content: () -> String, + sourceLocation: Testing.SourceLocation = #_sourceLocation +) throws -> ( + renderBlockContent: [RenderBlockContent], + problemIdentifiers: [String], + directive: Directive?, + collectedReferences: [String : any RenderReference] +) { + context.diagnosticEngine.clearDiagnostics() + + let source = URL(fileURLWithPath: "/path/to/test-source-\(ProcessInfo.processInfo.globallyUniqueString)") + let document = Document(parsing: content(), source: source, options: [.parseBlockDirectives, .parseSymbolLinks]) + + let blockDirectiveContainer = try #require(document.child(at: 0) as? BlockDirective, sourceLocation: sourceLocation) + + var analyzer = SemanticAnalyzer(source: source, bundle: context.inputs) + let result = analyzer.visit(blockDirectiveContainer) + context.diagnosticEngine.emit(analyzer.problems) + + var referenceResolver = MarkupReferenceResolver(context: context, rootReference: context.inputs.rootReference) + + _ = referenceResolver.visit(blockDirectiveContainer) + context.diagnosticEngine.emit(referenceResolver.problems) + + func problemIDs() throws -> [String] { + try context.problems.map { problem -> (line: Int, severity: String, id: String) in + #expect(problem.diagnostic.source != nil, "Problem \(problem.diagnostic.identifier) is missing a source URL.", sourceLocation: sourceLocation) + let line = try #require(problem.diagnostic.range, sourceLocation: sourceLocation).lowerBound.line + return (line, problem.diagnostic.severity.description, problem.diagnostic.identifier) + } + .sorted { lhs, rhs in + let (lhsLine, _, lhsID) = lhs + let (rhsLine, _, rhsID) = rhs + + if lhsLine != rhsLine { + return lhsLine < rhsLine + } else { + return lhsID < rhsID + } + } + .map { (line, severity, id) in + return "\(line): \(severity) – \(id)" + } + } + + guard let directive = result as? Directive else { + return ([], try problemIDs(), nil, [:]) + } + + var contentCompiler = RenderContentCompiler( + context: context, + identifier: ResolvedTopicReference( + bundleID: context.inputs.id, + path: "/test-path-123", + sourceLanguage: .swift + ) + ) + + let renderedContent = try #require(directive.render(with: &contentCompiler) as? [RenderBlockContent], sourceLocation: sourceLocation) + + let collectedReferences = contentCompiler.videoReferences + .mapValues { $0 as (any RenderReference) } + .merging( + contentCompiler.imageReferences, + uniquingKeysWith: { videoReference, _ in + Issue.record("Non-unique references.", sourceLocation: sourceLocation) + return videoReference + } + ) + + return (renderedContent, try problemIDs(), directive, collectedReferences) +} From 9cb71ea19feead53456ef403cc2879ed0bfe2c15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20R=C3=B6nnqvist?= Date: Thu, 20 Nov 2025 18:23:57 +0000 Subject: [PATCH 2/4] Update existing test helpers to call into the Swift Testing helpers where possible --- .../XCTestCase+LoadingTestData.swift | 82 +++++++------------ 1 file changed, 28 insertions(+), 54 deletions(-) diff --git a/Tests/SwiftDocCTests/XCTestCase+LoadingTestData.swift b/Tests/SwiftDocCTests/XCTestCase+LoadingTestData.swift index 4103f4f92b..4540d73984 100644 --- a/Tests/SwiftDocCTests/XCTestCase+LoadingTestData.swift +++ b/Tests/SwiftDocCTests/XCTestCase+LoadingTestData.swift @@ -25,17 +25,15 @@ extension XCTestCase { diagnosticEngine: DiagnosticEngine = .init(filterLevel: .hint), configuration: DocumentationContext.Configuration = .init() ) async throws -> (URL, DocumentationBundle, DocumentationContext) { - var configuration = configuration - configuration.externalDocumentationConfiguration.sources = externalResolvers - configuration.externalDocumentationConfiguration.globalSymbolResolver = externalSymbolResolver - configuration.convertServiceConfiguration.fallbackResolver = fallbackResolver - configuration.externalMetadata.diagnosticLevel = diagnosticEngine.filterLevel - - let (bundle, dataProvider) = try DocumentationContext.InputsProvider() - .inputsAndDataProvider(startingPoint: catalogURL, options: .init()) - - let context = try await DocumentationContext(bundle: bundle, dataProvider: dataProvider, diagnosticEngine: diagnosticEngine, configuration: configuration) - return (catalogURL, bundle, context) + let context = try await loadFromDisk( + catalogURL: catalogURL, + externalResolvers: externalResolvers, + externalSymbolResolver: externalSymbolResolver, + fallbackResolver: fallbackResolver, + diagnosticEngine: diagnosticEngine, + configuration: configuration + ) + return (catalogURL, context.inputs, context) } /// Loads a documentation catalog from an in-memory test file system. @@ -54,20 +52,14 @@ extension XCTestCase { logOutput: some TextOutputStream = LogHandle.none, configuration: DocumentationContext.Configuration = .init() ) async throws -> (DocumentationBundle, DocumentationContext) { - let fileSystem = try TestFileSystem(folders: [catalog] + otherFileSystemDirectories) - let catalogURL = URL(fileURLWithPath: "/\(catalog.name)") - - let diagnosticEngine = DiagnosticEngine(filterLevel: diagnosticFilterLevel) - diagnosticEngine.add(DiagnosticConsoleWriter(logOutput, formattingOptions: [], baseURL: catalogURL, highlight: true, dataProvider: fileSystem)) - - let (bundle, dataProvider) = try DocumentationContext.InputsProvider(fileManager: fileSystem) - .inputsAndDataProvider(startingPoint: catalogURL, options: .init()) - - let context = try await DocumentationContext(bundle: bundle, dataProvider: dataProvider, diagnosticEngine: diagnosticEngine, configuration: configuration) - - diagnosticEngine.flush() // Write to the logOutput - - return (bundle, context) + let context = try await SwiftDocCTests.load( + catalog: catalog, + otherFileSystemDirectories: otherFileSystemDirectories, + diagnosticFilterLevel: diagnosticFilterLevel, + logOutput: logOutput, + configuration: configuration + ) + return (context.inputs, context) } func testCatalogURL(named name: String, file: StaticString = #filePath, line: UInt = #line) throws -> URL { @@ -122,24 +114,25 @@ extension XCTestCase { configuration: DocumentationContext.Configuration = .init() ) async throws -> (URL, DocumentationBundle, DocumentationContext) { let catalogURL = try testCatalogURL(named: name) - return try await loadBundle(from: catalogURL, externalResolvers: externalResolvers, fallbackResolver: fallbackResolver, configuration: configuration) + let context = try await loadFromDisk(catalogURL: catalogURL, externalResolvers: externalResolvers, fallbackResolver: fallbackResolver, configuration: configuration) + return (catalogURL, context.inputs, context) } func testBundleAndContext(named name: String, externalResolvers: [DocumentationBundle.Identifier: any ExternalDocumentationSource] = [:]) async throws -> (DocumentationBundle, DocumentationContext) { - let (_, bundle, context) = try await testBundleAndContext(named: name, externalResolvers: externalResolvers) - return (bundle, context) + let context = try await loadFromDisk(catalogURL: try testCatalogURL(named: name), externalResolvers: externalResolvers) + return (context.inputs, context) } - func renderNode(atPath path: String, fromTestBundleNamed testBundleName: String) async throws -> RenderNode { - let (_, context) = try await testBundleAndContext(named: testBundleName) + func renderNode(atPath path: String, fromTestBundleNamed testCatalogName: String) async throws -> RenderNode { + let context = try await loadFromDisk(catalogURL: try testCatalogURL(named: testCatalogName)) let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: path, sourceLanguage: .swift)) var translator = RenderNodeTranslator(context: context, identifier: node.reference) return try XCTUnwrap(translator.visit(node.semantic) as? RenderNode) } func testBundle(named name: String) async throws -> DocumentationBundle { - let (bundle, _) = try await testBundleAndContext(named: name) - return bundle + let context = try await loadFromDisk(catalogURL: try testCatalogURL(named: name)) + return context.inputs } func testBundleFromRootURL(named name: String) throws -> DocumentationBundle { @@ -150,19 +143,8 @@ extension XCTestCase { } func testBundleAndContext() async throws -> (bundle: DocumentationBundle, context: DocumentationContext) { - let bundle = DocumentationBundle( - info: DocumentationBundle.Info( - displayName: "Test", - id: "com.example.test" - ), - baseURL: URL(string: "https://example.com/example")!, - symbolGraphURLs: [], - markupURLs: [], - miscResourceURLs: [] - ) - - let context = try await DocumentationContext(bundle: bundle, dataProvider: TestFileSystem(folders: [])) - return (bundle, context) + let context = try await makeEmptyContext() + return (context.inputs, context) } func parseDirective( @@ -347,14 +329,6 @@ extension XCTestCase { } func renderNodeApplying(variant: String, to renderNode: RenderNode) throws -> RenderNode { - let variantData = try RenderNodeVariantOverridesApplier().applyVariantOverrides( - in: RenderJSONEncoder.makeEncoder().encode(renderNode), - for: [.interfaceLanguage(variant)] - ) - - return try RenderJSONDecoder.makeDecoder().decode( - RenderNode.self, - from: variantData - ) + try renderNode.applying(variant: variant) } } From bf746cde6437443c377e25439c65d72b80d37bfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20R=C3=B6nnqvist?= Date: Thu, 20 Nov 2025 18:24:51 +0000 Subject: [PATCH 3/4] Draft new contributor information about adding and updating tests --- CONTRIBUTING.md | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index da6d76fb8f..670b3cee95 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -166,7 +166,7 @@ If you have commit access, you can run the required tests by commenting the foll If you do not have commit access, please ask one of the code owners to trigger them for you. For more details on Swift-DocC's continuous integration, see the -[Continous Integration](#continuous-integration) section below. +[Continuous Integration](#continuous-integration) section below. ### Introducing source breaking changes @@ -207,7 +207,23 @@ by navigating to the root of the repository and running the following: By running tests locally with the `test` script you will be best prepared for automated testing in CI as well. -### Testing in Xcode +### Adding new tests + +We recommend that you use [Swift Testing](https://developer.apple.com/documentation/testing) when you add new tests. +Currently there are few existing tests to draw inspiration from, so here are a few recommendations: + +- Prefer small test inputs that ideally use a virtual file system for both reading and writing. +- Consider using parameterized tests if you're making the same verifications in multiple configurations or on multiple elements. +- Think about what information would be helpful to someone else who might debug that test case if it fails in the future. +- Use `#require` rather that force unwrapping for behaviors that would change due to unexpected bugs in the code you're testing. + +If you're updating an existing test case with additional logic, we appreciate it if you also modernize that test, but we don't expect it. +If the test case is part of a large file, you can create new test suite which contains just the test case that you're modernizing. + +If you modernize an existing test case, consider not only the syntactical differences between Swift Testing and XCTest, +but also if there are any Swift Testing features or other changes that would make the test case easier to read, maintain, or debug. + +### Testing DocC's integration with Xcode You can test a locally built version of Swift-DocC in Xcode 13 or later by setting the `DOCC_EXEC` build setting to the path of your local `docc`: From 8c0a4afdb1aba2b0c531a5d664cc13afb87420bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20R=C3=B6nnqvist?= Date: Thu, 20 Nov 2025 18:25:42 +0000 Subject: [PATCH 4/4] Update a few test suites to Swift Testing to use as examples/inspiration for new tests --- .../NonInclusiveLanguageCheckerTests.swift | 192 +++++++++--------- .../ParseDirectiveArgumentsTests.swift | 53 ++--- 2 files changed, 115 insertions(+), 130 deletions(-) diff --git a/Tests/SwiftDocCTests/Checker/Checkers/NonInclusiveLanguageCheckerTests.swift b/Tests/SwiftDocCTests/Checker/Checkers/NonInclusiveLanguageCheckerTests.swift index 1073bc693e..87e928581e 100644 --- a/Tests/SwiftDocCTests/Checker/Checkers/NonInclusiveLanguageCheckerTests.swift +++ b/Tests/SwiftDocCTests/Checker/Checkers/NonInclusiveLanguageCheckerTests.swift @@ -8,13 +8,13 @@ See https://swift.org/CONTRIBUTORS.txt for Swift project authors */ -import XCTest +import Testing import Markdown @testable import SwiftDocC import SwiftDocCTestUtilities -class NonInclusiveLanguageCheckerTests: XCTestCase { - +struct NonInclusiveLanguageCheckerTests { + @Test func testMatchTermInTitle() throws { let source = """ # A Whitelisted title @@ -22,16 +22,17 @@ class NonInclusiveLanguageCheckerTests: XCTestCase { let document = Document(parsing: source) var checker = NonInclusiveLanguageChecker(sourceFile: nil) checker.visit(document) - XCTAssertEqual(checker.problems.count, 1) - - let problem = try XCTUnwrap(checker.problems.first) - let range = try XCTUnwrap(problem.diagnostic.range) - XCTAssertEqual(range.lowerBound.line, 1) - XCTAssertEqual(range.lowerBound.column, 5) - XCTAssertEqual(range.upperBound.line, 1) - XCTAssertEqual(range.upperBound.column, 16) + #expect(checker.problems.count == 1) + + let problem = try #require(checker.problems.first) + let range = try #require(problem.diagnostic.range) + #expect(range.lowerBound.line == 1) + #expect(range.lowerBound.column == 5) + #expect(range.upperBound.line == 1) + #expect(range.upperBound.column == 16) } + @Test func testMatchTermWithSpaces() throws { let source = """ # A White listed title @@ -41,30 +42,31 @@ class NonInclusiveLanguageCheckerTests: XCTestCase { let document = Document(parsing: source) var checker = NonInclusiveLanguageChecker(sourceFile: nil) checker.visit(document) - XCTAssertEqual(checker.problems.count, 3) - - let problem = try XCTUnwrap(checker.problems.first) - let range = try XCTUnwrap(problem.diagnostic.range) - XCTAssertEqual(range.lowerBound.line, 1) - XCTAssertEqual(range.lowerBound.column, 5) - XCTAssertEqual(range.upperBound.line, 1) - XCTAssertEqual(range.upperBound.column, 18) - - let problemTwo = try XCTUnwrap(checker.problems[1]) - let rangeTwo = try XCTUnwrap(problemTwo.diagnostic.range) - XCTAssertEqual(rangeTwo.lowerBound.line, 2) - XCTAssertEqual(rangeTwo.lowerBound.column, 5) - XCTAssertEqual(rangeTwo.upperBound.line, 2) - XCTAssertEqual(rangeTwo.upperBound.column, 20) - - let problemThree = try XCTUnwrap(checker.problems[2]) - let rangeThree = try XCTUnwrap(problemThree.diagnostic.range) - XCTAssertEqual(rangeThree.lowerBound.line, 3) - XCTAssertEqual(rangeThree.lowerBound.column, 5) - XCTAssertEqual(rangeThree.upperBound.line, 3) - XCTAssertEqual(rangeThree.upperBound.column, 17) + #expect(checker.problems.count == 3) + + let problem = try #require(checker.problems.first) + let range = try #require(problem.diagnostic.range) + #expect(range.lowerBound.line == 1) + #expect(range.lowerBound.column == 5) + #expect(range.upperBound.line == 1) + #expect(range.upperBound.column == 18) + + let problemTwo = try #require(checker.problems.dropFirst(1).first) + let rangeTwo = try #require(problemTwo.diagnostic.range) + #expect(rangeTwo.lowerBound.line == 2) + #expect(rangeTwo.lowerBound.column == 5) + #expect(rangeTwo.upperBound.line == 2) + #expect(rangeTwo.upperBound.column == 20) + + let problemThree = try #require(checker.problems.dropFirst(2).first) + let rangeThree = try #require(problemThree.diagnostic.range) + #expect(rangeThree.lowerBound.line == 3) + #expect(rangeThree.lowerBound.column == 5) + #expect(rangeThree.upperBound.line == 3) + #expect(rangeThree.upperBound.column == 17) } + @Test func testMatchTermInAbstract() throws { let source = """ # Title @@ -74,16 +76,17 @@ The blacklist is in the abstract. let document = Document(parsing: source) var checker = NonInclusiveLanguageChecker(sourceFile: nil) checker.visit(document) - XCTAssertEqual(checker.problems.count, 1) - - let problem = try XCTUnwrap(checker.problems.first) - let range = try XCTUnwrap(problem.diagnostic.range) - XCTAssertEqual(range.lowerBound.line, 3) - XCTAssertEqual(range.lowerBound.column, 5) - XCTAssertEqual(range.upperBound.line, 3) - XCTAssertEqual(range.upperBound.column, 14) + #expect(checker.problems.count == 1) + + let problem = try #require(checker.problems.first) + let range = try #require(problem.diagnostic.range) + #expect(range.lowerBound.line == 3) + #expect(range.lowerBound.column == 5) + #expect(range.upperBound.line == 3) + #expect(range.upperBound.column == 14) } + @Test func testMatchTermInParagraph() throws { let source = """ # Title @@ -98,16 +101,17 @@ master branch is the default. let document = Document(parsing: source) var checker = NonInclusiveLanguageChecker(sourceFile: nil) checker.visit(document) - XCTAssertEqual(checker.problems.count, 1) - - let problem = try XCTUnwrap(checker.problems.first) - let range = try XCTUnwrap(problem.diagnostic.range) - XCTAssertEqual(range.lowerBound.line, 8) - XCTAssertEqual(range.lowerBound.column, 1) - XCTAssertEqual(range.upperBound.line, 8) - XCTAssertEqual(range.upperBound.column, 7) + #expect(checker.problems.count == 1) + + let problem = try #require(checker.problems.first) + let range = try #require(problem.diagnostic.range) + #expect(range.lowerBound.line == 8) + #expect(range.lowerBound.column == 1) + #expect(range.upperBound.line == 8) + #expect(range.upperBound.column == 7) } + @Test func testMatchTermInList() throws { let source = """ - Item 1 is ok @@ -117,16 +121,17 @@ master branch is the default. let document = Document(parsing: source) var checker = NonInclusiveLanguageChecker(sourceFile: nil) checker.visit(document) - XCTAssertEqual(checker.problems.count, 1) - - let problem = try XCTUnwrap(checker.problems.first) - let range = try XCTUnwrap(problem.diagnostic.range) - XCTAssertEqual(range.lowerBound.line, 2) - XCTAssertEqual(range.lowerBound.column, 13) - XCTAssertEqual(range.upperBound.line, 2) - XCTAssertEqual(range.upperBound.column, 24) + #expect(checker.problems.count == 1) + + let problem = try #require(checker.problems.first) + let range = try #require(problem.diagnostic.range) + #expect(range.lowerBound.line == 2) + #expect(range.lowerBound.column == 13) + #expect(range.upperBound.line == 2) + #expect(range.upperBound.column == 24) } + @Test func testMatchTermInInlineCode() throws { let source = """ The name `MachineSlave` is unacceptable. @@ -134,16 +139,17 @@ The name `MachineSlave` is unacceptable. let document = Document(parsing: source) var checker = NonInclusiveLanguageChecker(sourceFile: nil) checker.visit(document) - XCTAssertEqual(checker.problems.count, 1) - - let problem = try XCTUnwrap(checker.problems.first) - let range = try XCTUnwrap(problem.diagnostic.range) - XCTAssertEqual(range.lowerBound.line, 1) - XCTAssertEqual(range.lowerBound.column, 18) - XCTAssertEqual(range.upperBound.line, 1) - XCTAssertEqual(range.upperBound.column, 23) + #expect(checker.problems.count == 1) + + let problem = try #require(checker.problems.first) + let range = try #require(problem.diagnostic.range) + #expect(range.lowerBound.line == 1) + #expect(range.lowerBound.column == 18) + #expect(range.upperBound.line == 1) + #expect(range.upperBound.column == 23) } + @Test func testMatchTermInCodeBlock() throws { let source = """ A code block: @@ -158,13 +164,13 @@ func aBlackListedFunc() { let document = Document(parsing: source) var checker = NonInclusiveLanguageChecker(sourceFile: nil) checker.visit(document) - XCTAssertEqual(checker.problems.count, 1) - let problem = try XCTUnwrap(checker.problems.first) - let range = try XCTUnwrap(problem.diagnostic.range) - XCTAssertEqual(range.lowerBound.line, 5) - XCTAssertEqual(range.lowerBound.column, 7) - XCTAssertEqual(range.upperBound.line, 5) - XCTAssertEqual(range.upperBound.column, 18) + #expect(checker.problems.count == 1) + let problem = try #require(checker.problems.first) + let range = try #require(problem.diagnostic.range) + #expect(range.lowerBound.line == 5) + #expect(range.lowerBound.column == 7) + #expect(range.upperBound.line == 5) + #expect(range.upperBound.column == 18) } private let nonInclusiveContent = """ @@ -177,36 +183,32 @@ func aBlackListedFunc() { - item three """ + @Test func testDisabledByDefault() async throws { // Create a test bundle with some non-inclusive content. let catalog = Folder(name: "unit-test.docc", content: [ TextFile(name: "Root.md", utf8Content: nonInclusiveContent) ]) - let (_, context) = try await loadBundle(catalog: catalog) + let context = try await load(catalog: catalog) - XCTAssertEqual(context.problems.count, 0) // Non-inclusive content is an info-level diagnostic, so it's filtered out. + #expect(context.problems.isEmpty) // Non-inclusive content is an info-level diagnostic, so it's filtered out. } - func testEnablingTheChecker() async throws { - // The expectations of the checker being run, depending on the diagnostic level - // set to to the documentation context for the compilation. - let expectations: [(DiagnosticSeverity, Bool)] = [ - (.hint, true), - (.information, true), - (.warning, false), - (.error, false), - ] - - for (severity, enabled) in expectations { - let catalog = Folder(name: "unit-test.docc", content: [ - TextFile(name: "Root.md", utf8Content: nonInclusiveContent) - ]) - var configuration = DocumentationContext.Configuration() - configuration.externalMetadata.diagnosticLevel = severity - let (_, context) = try await loadBundle(catalog: catalog, diagnosticFilterLevel: severity, configuration: configuration) - - // Verify that checker diagnostics were emitted or not, depending on the diagnostic level set. - XCTAssertEqual(context.problems.contains(where: { $0.diagnostic.identifier == "org.swift.docc.NonInclusiveLanguage" }), enabled) - } + @Test(arguments: [ + DiagnosticSeverity.hint: true, + DiagnosticSeverity.information: true, + DiagnosticSeverity.warning: false, + DiagnosticSeverity.error: false, + ]) + func testEnablingTheChecker(configuredDiagnosticSeverity: DiagnosticSeverity, expectsToIncludeANonInclusiveDiagnostic: Bool) async throws { + let catalog = Folder(name: "unit-test.docc", content: [ + TextFile(name: "Root.md", utf8Content: nonInclusiveContent) + ]) + var configuration = DocumentationContext.Configuration() + configuration.externalMetadata.diagnosticLevel = configuredDiagnosticSeverity + let context = try await load(catalog: catalog, diagnosticFilterLevel: configuredDiagnosticSeverity, configuration: configuration) + + // Verify that checker diagnostics were emitted or not, depending on the diagnostic level set. + #expect(context.problems.contains(where: { $0.diagnostic.identifier == "org.swift.docc.NonInclusiveLanguage" }) == expectsToIncludeANonInclusiveDiagnostic) } } diff --git a/Tests/SwiftDocCTests/Infrastructure/ParseDirectiveArgumentsTests.swift b/Tests/SwiftDocCTests/Infrastructure/ParseDirectiveArgumentsTests.swift index f8881ac128..49920dd78d 100644 --- a/Tests/SwiftDocCTests/Infrastructure/ParseDirectiveArgumentsTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/ParseDirectiveArgumentsTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -9,45 +9,28 @@ */ import Foundation -import XCTest - +import Testing import Markdown @testable import SwiftDocC -class ParseDirectiveArgumentsTests: XCTestCase { - func testEmitsWarningForMissingExpectedCharacter() throws { - let diagnostic = try XCTUnwrap( - parse(rawDirective: "@Directive(argument: multiple words)").first - ) - - XCTAssertEqual(diagnostic.identifier, "org.swift.docc.Directive.MissingExpectedCharacter") - XCTAssertEqual(diagnostic.severity, .warning) - } - - func testEmitsWarningForUnexpectedCharacter() throws { - let diagnostic = try XCTUnwrap( - parse(rawDirective: "@Directive(argumentA: value, argumentB: multiple words").first - ) - - XCTAssertEqual(diagnostic.identifier, "org.swift.docc.Directive.MissingExpectedCharacter") - XCTAssertEqual(diagnostic.severity, .warning) - } - - func testEmitsWarningsForDuplicateArgument() throws { - let diagnostic = try XCTUnwrap( - parse(rawDirective: "@Directive(argumentA: value, argumentA: value").first - ) - - XCTAssertEqual(diagnostic.identifier, "org.swift.docc.Directive.DuplicateArgument") - XCTAssertEqual(diagnostic.severity, .warning) - } - - func parse(rawDirective: String) -> [Diagnostic] { - let document = Document(parsing: rawDirective, options: .parseBlockDirectives) - +struct ParseDirectiveArgumentsTests { + @Test(arguments: [ + // Missing quotation marks around string parameter + "@Directive(argument: multiple words)": "org.swift.docc.Directive.MissingExpectedCharacter", + // Missing quotation marks around string parameter in 2nd parameter + "@Directive(argumentA: value, argumentB: multiple words)": "org.swift.docc.Directive.MissingExpectedCharacter", + // Duplicate argument + "@Directive(argumentA: value, argumentA: value)": "org.swift.docc.Directive.DuplicateArgument", + ]) + func testEmitsWarningsForInvalidMarkup(invalidMarkup: String, expectedDiagnosticID: String) throws { + let document = Document(parsing: invalidMarkup, options: .parseBlockDirectives) var problems = [Problem]() _ = (document.child(at: 0) as? BlockDirective)?.arguments(problems: &problems) - return problems.map(\.diagnostic) + + let diagnostic = try #require(problems.first?.diagnostic) + + #expect(diagnostic.identifier == expectedDiagnosticID) + #expect(diagnostic.severity == .warning) } }