Skip to content

[test only] Combine many of the various SGF test helpers #955

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Aug 20, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import SymbolKit
/// Translates a symbol's details into a render nodes's details section.
struct PlistDetailsSectionTranslator: RenderSectionTranslator, Decodable {

func generatePlistDetailsRenderSection(_ symbol: Symbol, plistDetails: SymbolGraph.Symbol.PlistDetails) -> PlistDetailsRenderSection {
private func generatePlistDetailsRenderSection(_ symbol: Symbol, plistDetails: SymbolGraph.Symbol.PlistDetails) -> PlistDetailsRenderSection {
PlistDetailsRenderSection(
details: PlistDetailsRenderSection.Details(
rawKey: plistDetails.rawKey,
Expand All @@ -28,9 +28,6 @@ struct PlistDetailsSectionTranslator: RenderSectionTranslator, Decodable {
}

func translateSection(for symbol: Symbol, renderNode: inout RenderNode, renderNodeTranslator: inout RenderNodeTranslator) -> VariantCollection<CodableContentSection?>? {
guard let mixinVariant = symbol.mixinsVariants.allValues.first(where: { mixin in
mixin.variant.keys.contains(SymbolGraph.Symbol.PlistDetails.mixinKey)
}) else { return nil }
guard let plistDetails = symbol.mixinsVariants.allValues.mapFirst(where: { mixin in
mixin.variant[SymbolGraph.Symbol.PlistDetails.mixinKey] as? SymbolGraph.Symbol.PlistDetails
}) else {
Expand Down
157 changes: 148 additions & 9 deletions Sources/SwiftDocCTestUtilities/SymbolGraphCreation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,167 @@
import Foundation
import XCTest
import SymbolKit
import SwiftDocC

// MARK: - Symbol Graph objects

extension XCTestCase {
public func makeSymbolGraph(

package func makeSymbolGraph(
moduleName: String,
platform: SymbolGraph.Platform = .init(),
symbols: [SymbolGraph.Symbol] = [],
relationships: [SymbolGraph.Relationship] = []
) -> SymbolGraph {
return SymbolGraph(
metadata: SymbolGraph.Metadata(
formatVersion: SymbolGraph.SemanticVersion(major: 0, minor: 6, patch: 0),
generator: "unit-test"
),
module: SymbolGraph.Module(
name: moduleName,
platform: platform
),
metadata: makeMetadata(),
module: makeModule(moduleName: moduleName, platform: platform),
symbols: symbols,
relationships: relationships
)
}

package func makeMetadata(major: Int = 0, minor: Int = 6, patch: Int = 0) -> SymbolGraph.Metadata {
SymbolGraph.Metadata(
formatVersion: SymbolGraph.SemanticVersion(major: major, minor: minor, patch: patch),
generator: "unit-test"
)
}

package func makeModule(moduleName: String, platform: SymbolGraph.Platform = .init()) -> SymbolGraph.Module {
SymbolGraph.Module(name: moduleName, platform: platform)
}

// MARK: Line List

package func makeLineList(
docComment: String,
startOffset: SymbolGraph.LineList.SourceRange.Position = defaultSymbolPosition,
url: URL = defaultSymbolURL
) -> SymbolGraph.LineList {
SymbolGraph.LineList(
// Create a `LineList/Line` for each line of the doc comment and calculate a realistic range for each line.
docComment.components(separatedBy: .newlines)
.enumerated()
.map { lineOffset, line in
SymbolGraph.LineList.Line(
text: line,
range: SymbolGraph.LineList.SourceRange(
start: .init(line: startOffset.line + lineOffset, character: startOffset.character),
end: .init(line: startOffset.line + lineOffset, character: startOffset.character + line.count)
)
)
},
// We want to include the file:// scheme here
uri: url.absoluteString
)
}

package func makeMixins(_ mixins: [any Mixin]) -> [String: any Mixin] {
[String: any Mixin](
mixins.map { (type(of: $0).mixinKey, $0) },
uniquingKeysWith: { old, _ in old /* Keep the first encountered value */ }
)
}

// MARK: Symbol

package func makeSymbol(
id: String,
language: SourceLanguage = .swift,
kind kindID: SymbolGraph.Symbol.KindIdentifier,
pathComponents: [String],
docComment: String? = nil,
accessLevel: SymbolGraph.Symbol.AccessControl = .init(rawValue: "public"), // Defined internally in SwiftDocC
location: (position: SymbolGraph.LineList.SourceRange.Position, url: URL)? = (defaultSymbolPosition, defaultSymbolURL),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're duplicating these defaults 3 times in this file, it would be nice if we only used them once. I think here we could make it a non-optional value and save us needing to have defaults later on:
https://github.com/apple/swift-docc/blob/64ec387cab27ce898f74df36eff2d0d548b77658/Sources/SwiftDocCTestUtilities/SymbolGraphCreation.swift#L108-L109

Suggested change
location: (position: SymbolGraph.LineList.SourceRange.Position, url: URL)? = (defaultSymbolPosition, defaultSymbolURL),
location: (position: SymbolGraph.LineList.SourceRange.Position, url: URL) = (defaultSymbolPosition, defaultSymbolURL),

and then we won't need the defaults:

makeLineList(
    docComment: $0,
    startOffset: location.position,
    url: location.url
)

Copy link
Contributor Author

@d-ronnqvist d-ronnqvist Jul 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are some cases where real world data doesn't have a location, for example if the symbol graph was extracted from a binary instead of from source code.

I prefer to keep the ability to create such a value. The idea is that makeLineList, makeMixins, makeMetadata, makeModule, etc. all should be usable from any test. That's why each of them duplicate the default values so that the caller doesn't have to.

signature: SymbolGraph.Symbol.FunctionSignature? = nil,
otherMixins: [any Mixin] = []
) -> SymbolGraph.Symbol {
precondition(!pathComponents.isEmpty, "Need at least one path component to name the symbol")

var mixins = otherMixins // Earlier mixins are prioritized if there are duplicates
if let location {
mixins.append(SymbolGraph.Symbol.Location(uri: location.url.absoluteString /* we want to include the file:// scheme */, position: location.position))
}
if let signature {
mixins.append(signature)
}

return SymbolGraph.Symbol(
identifier: SymbolGraph.Symbol.Identifier(precise: id, interfaceLanguage: language.id),
names: makeSymbolNames(name: pathComponents.first!),
pathComponents: pathComponents,
docComment: docComment.map {
makeLineList(
docComment: $0,
startOffset: location?.position ?? defaultSymbolPosition,
url: location?.url ?? defaultSymbolURL
)
},
accessLevel: accessLevel,
kind: makeSymbolKind(kindID),
mixins: makeMixins(mixins)
)
}

package func makeSymbolNames(name: String) -> SymbolGraph.Symbol.Names {
SymbolGraph.Symbol.Names(
title: name,
navigator: [.init(kind: .identifier, spelling: name, preciseIdentifier: nil)],
subHeading: [.init(kind: .identifier, spelling: name, preciseIdentifier: nil)],
prose: nil
)
}

package func makeSymbolKind(_ kindID: SymbolGraph.Symbol.KindIdentifier) -> SymbolGraph.Symbol.Kind {
var documentationNodeKind: DocumentationNode.Kind {
switch kindID {
case .associatedtype: .associatedType
case .class: .class
case .deinit: .deinitializer
case .enum: .enumeration
case .case: .enumerationCase
case .func: .function
case .operator: .operator
case .`init`: .initializer
case .ivar: .instanceVariable
case .macro: .macro
case .method: .instanceMethod
case .namespace: .namespace
case .property: .instanceProperty
case .protocol: .protocol
case .snippet: .snippet
case .struct: .structure
case .subscript: .instanceSubscript
case .typeMethod: .typeMethod
case .typeProperty: .typeProperty
case .typeSubscript: .typeSubscript
case .typealias: .typeAlias
case .union: .union
case .var: .globalVariable
case .module: .module
case .extension: .extension
case .dictionary: .dictionary
case .dictionaryKey: .dictionaryKey
case .httpRequest: .httpRequest
case .httpParameter: .httpParameter
case .httpResponse: .httpResponse
case .httpBody: .httpBody
default: .unknown
}
}
return SymbolGraph.Symbol.Kind(parsedIdentifier: kindID, displayName: documentationNodeKind.name)
}
}

// MARK: Constants

private let defaultSymbolPosition = SymbolGraph.LineList.SourceRange.Position(line: 11, character: 17) // an arbitrary non-zero start position
private let defaultSymbolURL = URL(fileURLWithPath: "/Users/username/path/to/SomeFile.swift")

// MARK: - JSON strings

extension XCTestCase {
public func makeSymbolGraphString(moduleName: String, symbols: String = "", relationships: String = "", platform: String = "") -> String {
return """
{
Expand Down
53 changes: 12 additions & 41 deletions Tests/SwiftDocCTests/Infrastructure/AutoCapitalizationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,57 +18,28 @@ class AutoCapitalizationTests: XCTestCase {

// MARK: Test helpers

private let start = SymbolGraph.LineList.SourceRange.Position(line: 7, character: 6) // an arbitrary non-zero start position
private let symbolURL = URL(fileURLWithPath: "/path/to/SomeFile.swift")

private func makeSymbolGraph(docComment: String, parameters: [String]) -> SymbolGraph {
makeSymbolGraph(
docComment: docComment,
sourceLanguage: .swift,
parameters: parameters.map { ($0, nil) },
returnValue: .init(kind: .typeIdentifier, spelling: "ReturnValue", preciseIdentifier: "return-value-id")
)
}

private func makeSymbolGraph(
docComment: String?,
sourceLanguage: SourceLanguage,
parameters: [(name: String, externalName: String?)],
returnValue: SymbolGraph.Symbol.DeclarationFragments.Fragment
) -> SymbolGraph {
let uri = symbolURL.absoluteString // we want to include the file:// scheme here
func makeLineList(text: String) -> SymbolGraph.LineList {
return .init(text.splitByNewlines.enumerated().map { lineOffset, line in
.init(text: line, range: .init(start: .init(line: start.line + lineOffset, character: start.character),
end: .init(line: start.line + lineOffset, character: start.character + line.count)))
}, uri: uri)
}

return makeSymbolGraph(
moduleName: "ModuleName",
symbols: [
.init(
identifier: .init(precise: "symbol-id", interfaceLanguage: sourceLanguage.id),
names: .init(title: "functionName(...)", navigator: nil, subHeading: nil, prose: nil),
makeSymbol(
id: "symbol-id",
kind: .func,
pathComponents: ["functionName(...)"],
docComment: docComment.map { makeLineList(text: $0) },
accessLevel: .public, kind: .init(parsedIdentifier: .func, displayName: "Function"),
mixins: [
SymbolGraph.Symbol.Location.mixinKey: SymbolGraph.Symbol.Location(uri: uri, position: start),

SymbolGraph.Symbol.FunctionSignature.mixinKey: SymbolGraph.Symbol.FunctionSignature(
parameters: parameters.map {
.init(name: $0.name, externalName: $0.externalName, declarationFragments: [], children: [])
},
returns: [returnValue]
)
]
docComment: docComment,
signature: .init(
parameters: parameters.map {
.init(name: $0, externalName: nil, declarationFragments: [], children: [])
},
returns: [
.init(kind: .typeIdentifier, spelling: "ReturnValue", preciseIdentifier: "return-value-id")
]
)
)
]
)
}


// MARK: End-to-end integration tests

func testParametersCapitalization() throws {
Expand Down
37 changes: 9 additions & 28 deletions Tests/SwiftDocCTests/Infrastructure/AutomaticCurationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ class AutomaticCurationTests: XCTestCase {
JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(
moduleName: "ModuleName",
symbols: [
makeSymbol(identifier: containerID, kind: .class, pathComponents: ["SomeClass"]),
makeSymbol(identifier: memberID, kind: kind, pathComponents: ["SomeClass", "someMember"]),
makeSymbol(id: containerID, kind: .class, pathComponents: ["SomeClass"]),
makeSymbol(id: memberID, kind: kind, pathComponents: ["SomeClass", "someMember"]),
],
relationships: [
.init(source: memberID, target: containerID, kind: .memberOf, targetFallback: nil),
Expand Down Expand Up @@ -64,15 +64,17 @@ class AutomaticCurationTests: XCTestCase {
// func someFunction() { }
// }
makeSymbol(
identifier: extensionID,
id: extensionID,
kind: .extension,
// The extension has the path component of the extended type
pathComponents: ["Something"],
// Specify the extended symbol's symbol kind
swiftExtension: .init(extendedModule: "ExtendedModule", typeKind: nonExtensionKind, constraints: [])
otherMixins: [
SymbolGraph.Symbol.Swift.Extension(extendedModule: "ExtendedModule", typeKind: nonExtensionKind, constraints: [])
]
),
// No matter what type `ExtendedModule.Something` is, always add a function in the extension
makeSymbol(identifier: memberID, kind: .func, pathComponents: ["Something", "someFunction()"]),
makeSymbol(id: memberID, kind: .func, pathComponents: ["Something", "someFunction()"]),
],
relationships: [
.init(source: extensionID, target: containerID, kind: .extensionTo, targetFallback: "ExtendedModule.Something"),
Expand Down Expand Up @@ -790,8 +792,8 @@ class AutomaticCurationTests: XCTestCase {
let exampleDocumentation = Folder(name: "CatalogName.docc", content: [
JSONFile(name: "ModuleName.symbols.json",
content: makeSymbolGraph(moduleName: "ModuleName", symbols: [
makeSymbol(identifier: containerID, kind: .class, pathComponents: ["SomeClass"]),
makeSymbol(identifier: memberID, kind: kind, pathComponents: ["SomeClass", "someMember"]),
makeSymbol(id: containerID, kind: .class, pathComponents: ["SomeClass"]),
makeSymbol(id: memberID, kind: kind, pathComponents: ["SomeClass", "someMember"]),
], relationships: [
.init(source: memberID, target: containerID, kind: .memberOf, targetFallback: nil),
])),
Expand Down Expand Up @@ -837,24 +839,3 @@ class AutomaticCurationTests: XCTestCase {
}
}
}

private func makeSymbol(
identifier: String,
kind: SymbolGraph.Symbol.KindIdentifier,
pathComponents: [String],
swiftExtension: SymbolGraph.Symbol.Swift.Extension? = nil
) -> SymbolGraph.Symbol {
var mixins = [String: Mixin]()
if let swiftExtension {
mixins[SymbolGraph.Symbol.Swift.Extension.mixinKey] = swiftExtension
}
return SymbolGraph.Symbol(
identifier: .init(precise: identifier, interfaceLanguage: SourceLanguage.swift.id),
names: .init(title: pathComponents.last!, navigator: nil, subHeading: nil, prose: nil),
pathComponents: pathComponents,
docComment: nil,
accessLevel: .public,
kind: .init(parsedIdentifier: kind, displayName: "Kind Display Name"),
mixins: mixins
)
}
Loading