Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ run:
test:
swift test

## Run live CLI integration tests
#
# Builds the release executable and runs network-dependent command tests against Apple documentation.
.PHONY: test-integration
test-integration: build
APPLE_DOCS_EXECUTABLE="$(CURDIR)/$(CLI_BINARY)" swift test --filter CLIIntegrationTests

## Run SwiftLint
#
# Checks project-owned Swift files using .swiftlint.yml. Warnings fail the target.
Expand Down
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ let package = Package(
.product(name: "ArgumentParser", package: "swift-argument-parser")
]),
.testTarget(name: "CLITests", dependencies: ["CLI"]),
.testTarget(name: "CLIIntegrationTests"),
],
swiftLanguageModes: [.v6]
)
48 changes: 48 additions & 0 deletions Tests/CLIIntegrationTests/AppleDocsCommand.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import Foundation

enum AppleDocsCommandError: Error, LocalizedError {
case failed(status: Int32, stderr: String)
case invalidUTF8(stream: String)
case missingExecutable

var errorDescription: String? {
switch self {
case .failed(let status, let stderr):
return "apple-docs exited with status \(status): \(stderr)"
case .invalidUTF8(let stream):
return "apple-docs returned invalid UTF-8 on \(stream)."
case .missingExecutable:
return "APPLE_DOCS_EXECUTABLE is not set. Run the tests with make test-integration."
}
}
}

func runAppleDocs(_ arguments: [String]) throws -> String {
guard let executablePath = ProcessInfo.processInfo.environment["APPLE_DOCS_EXECUTABLE"] else {
throw AppleDocsCommandError.missingExecutable
}

let process = Process()
let standardOutput = Pipe()
let standardError = Pipe()
process.executableURL = URL(fileURLWithPath: executablePath)
process.arguments = arguments
process.standardOutput = standardOutput
process.standardError = standardError

try process.run()
let outputData = standardOutput.fileHandleForReading.readDataToEndOfFile()
let errorData = standardError.fileHandleForReading.readDataToEndOfFile()
process.waitUntilExit()

guard let output = String(data: outputData, encoding: .utf8) else {
throw AppleDocsCommandError.invalidUTF8(stream: "standard output")
}
guard let error = String(data: errorData, encoding: .utf8) else {
throw AppleDocsCommandError.invalidUTF8(stream: "standard error")
}
guard process.terminationStatus == 0 else {
throw AppleDocsCommandError.failed(status: process.terminationStatus, stderr: error)
}
return output
}
102 changes: 102 additions & 0 deletions Tests/CLIIntegrationTests/AppleDocsCommandIntegrationTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import Foundation
import Testing

private let integrationTestsEnabled =
ProcessInfo.processInfo.environment["APPLE_DOCS_EXECUTABLE"] != nil

@Suite(
"CLI integration",
.enabled(if: integrationTestsEnabled, "Run with make test-integration."),
.serialized
)
struct AppleDocsCommandIntegrationTests {
@Test("returns Swift String documentation as JSON")
func returnsSwiftStringJSON() throws {
// -- Arrange --
let arguments = ["types", "view", "String", "--technology", "Swift", "--json"]

// -- Act --
let output = try runAppleDocs(arguments)
let document = try JSONDecoder().decode(TypeDocument.self, from: Data(output.utf8))

// -- Assert --
#expect(document.metadata.title == "String")
#expect(document.metadata.modules.map(\.name) == ["Swift"])
#expect(document.metadata.symbolKind == "struct")
}

@Test("returns Foundation URL documentation as JSON")
func returnsFoundationURLJSON() throws {
// -- Arrange --
let arguments = ["types", "view", "URL", "--technology", "Foundation", "--json"]

// -- Act --
let output = try runAppleDocs(arguments)
let document = try JSONDecoder().decode(TypeDocument.self, from: Data(output.utf8))

// -- Assert --
#expect(document.metadata.title == "URL")
#expect(document.metadata.modules.map(\.name) == ["Foundation"])
#expect(document.metadata.symbolKind == "struct")
}

@Test("renders Swift String documentation as text")
func rendersSwiftStringText() throws {
// -- Arrange --
let arguments = ["types", "view", "String", "--technology", "Swift"]

// -- Act --
let output = try runAppleDocs(arguments)

// -- Assert --
#expect(output.hasPrefix("String\nStructure · Swift\n"))
#expect(output.contains("Declaration\n\n @frozen struct String"))
}

@Test("lists stable technologies as JSON")
func listsStableTechnologies() throws {
// -- Arrange --
let arguments = ["technologies", "list", "--json"]

// -- Act --
let output = try runAppleDocs(arguments)
let technologies = try JSONDecoder().decode([Technology].self, from: Data(output.utf8))

// -- Assert --
#expect(
technologies.contains(
Technology(
identifier: "doc://com.apple.documentation/documentation/Foundation",
name: "Foundation"
)
)
)
#expect(
technologies.contains(
Technology(
identifier: "doc://com.apple.documentation/documentation/Swift",
name: "Swift"
)
)
)
}
}

private struct TypeDocument: Decodable {
let metadata: Metadata

struct Metadata: Decodable {
let modules: [Module]
let symbolKind: String
let title: String
}

struct Module: Decodable {
let name: String
}
}

private struct Technology: Decodable, Equatable {
let identifier: String
let name: String
}
Loading