Skip to content

Commit 57f74a8

Browse files
committed
refactoring harness
1 parent 702cd37 commit 57f74a8

8 files changed

Lines changed: 145 additions & 71 deletions

File tree

Tests/SyntaxDocTests/DocumentationExampleTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ internal struct DocumentationExampleTests {
1616
let failures = results.filter { !$0.success }
1717
if !failures.isEmpty {
1818
let failureReport = failures.map { result in
19-
"\(result.filePath):\(result.lineNumber) - \(result.error ?? "Unknown error")"
19+
"\(result.fileURL.path()):\(result.lineNumber) - \(result.error ?? "Unknown error")"
2020
}
2121
.joined(separator: "\n")
2222

Tests/SyntaxDocTests/DocumentationHarness/DocumentationFinder.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
// Created by Leo Dion on 9/4/25.
66
//
77

8-
//protocol DocumentationFinder {
8+
// protocol DocumentationFinder {
99
// func searchForFiles(in: )
10-
//}
11-
12-
10+
// }
Lines changed: 44 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,59 @@
11
import Foundation
22

3-
// MARK: - FileManager Extensions
3+
// MARK: - Documentation Error Types
44

55

6-
extension FileManager {
7-
/// Finds documentation files in multiple relative paths
8-
func findDocumentationFiles(in relativePaths: [String], relativeTo root: URL, pathExtensions: [String]) throws -> [String] {
9-
try relativePaths.flatMap{
10-
try findDocumentationFiles(in: $0, relativeTo: root, pathExtensions: pathExtensions)
11-
}
12-
}
136

14-
/// Finds documentation files in a single directory or file
15-
func findDocumentationFiles(in relativePath: String, relativeTo root: URL, pathExtensions: [String]) throws -> [String] {
16-
let fullPath = root.appendingPathComponent(relativePath)
17-
var documentationFiles: [String] = []
18-
19-
if fileExists(atPath: fullPath.path) {
20-
if pathExtensions.contains(where: { relativePath.hasSuffix("." + $0) }) {
21-
// Single file with matching extension
22-
documentationFiles.append(relativePath)
23-
} else {
24-
// Directory - recursively find files with specified extensions
25-
let foundFileURLs = try findMarkdownFiles(in: fullPath, pathExtensions: pathExtensions)
26-
let relativePaths = foundFileURLs.map { fileURL in
27-
String(fileURL.path.dropFirst(root.path.count + 1))
28-
}
29-
documentationFiles.append(contentsOf: relativePaths)
30-
}
7+
// MARK: - FileManager Extensions
8+
9+
extension FileManager: FileSearcher {
10+
private func searchItem(_ itemURL: URL, _ pathExtensions: [String]) throws(FileSearchError)
11+
-> [URL]
12+
{
13+
var documentationFiles = [URL]()
14+
15+
let itemResourceValues: URLResourceValues
16+
do {
17+
itemResourceValues = try itemURL.resourceValues(forKeys: [.isDirectoryKey])
18+
} catch {
19+
throw FileSearchError.cannotAccessPath(itemURL.path, underlying: error)
3120
}
3221

22+
if itemResourceValues.isDirectory == true {
23+
// Recursively call this method for subdirectories
24+
let subdirectoryFiles = try findDocumentationFiles(
25+
in: itemURL, pathExtensions: pathExtensions)
26+
documentationFiles.append(contentsOf: subdirectoryFiles)
27+
} else if pathExtensions.contains(where: { itemURL.path.hasSuffix("." + $0) }) {
28+
// Direct file with matching extension
29+
documentationFiles.append(itemURL)
30+
}
3331
return documentationFiles
3432
}
3533

36-
/// Recursively finds files with specified extensions in a directory
37-
func findMarkdownFiles(in directory: URL, pathExtensions: [String]) throws -> [URL] {
38-
let enumerator = self.enumerator(at: directory, includingPropertiesForKeys: nil)
39-
40-
var markdownFiles: [URL] = []
34+
internal func searchDirectory(at path: URL, forExtensions pathExtensions: [String])
35+
throws(FileSearchError) -> [URL]
36+
{
37+
let contents: [URL]
38+
do {
39+
contents = try contentsOfDirectory(at: path, includingPropertiesForKeys: [.isDirectoryKey])
40+
} catch {
41+
throw FileSearchError.cannotReadDirectory(path.path, underlying: error)
42+
}
4143

42-
while let fileURL = enumerator?.nextObject() as? URL {
43-
if pathExtensions.contains(fileURL.pathExtension) {
44-
markdownFiles.append(fileURL)
45-
}
44+
// Directory - recursively find files with specified extensions
45+
let documentationFiles: [URL]
46+
do {
47+
documentationFiles = try contents.flatMap({ itemURL in
48+
try searchItem(itemURL, pathExtensions)
49+
})
50+
} catch let fileSearchError as FileSearchError {
51+
throw fileSearchError
52+
} catch {
53+
assertionFailure("Should only be a FileSearchError: \(error.localizedDescription)")
54+
throw .unknownError(error)
4655
}
4756

48-
return markdownFiles
57+
return documentationFiles
4958
}
5059
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//
2+
// FileSearchError.swift
3+
// SyntaxKit
4+
//
5+
// Created by Leo Dion on 9/4/25.
6+
//
7+
8+
9+
enum FileSearchError: Error, LocalizedError {
10+
case cannotAccessPath(String, underlying: any Error)
11+
case cannotReadDirectory(String, underlying: any Error)
12+
case unknownError(any Error)
13+
14+
var errorDescription: String? {
15+
switch self {
16+
case .cannotAccessPath(let path, let underlying):
17+
return "Cannot access path '\(path)': \(underlying.localizedDescription)"
18+
case .cannotReadDirectory(let path, let underlying):
19+
return "Cannot read directory '\(path)': \(underlying.localizedDescription)"
20+
case .unknownError(let error):
21+
return "Unknown Error: \(error)"
22+
}
23+
}
24+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//
2+
// FileSearcher.swift
3+
// SyntaxKit
4+
//
5+
// Created by Leo Dion on 9/4/25.
6+
//
7+
8+
9+
10+
protocol FileSearcher {
11+
func searchDirectory(at path: URL, forExtensions pathExtensions: [String]) throws(FileSearchError)
12+
-> [URL]
13+
}
14+
15+
extension FileSearcher {
16+
func findDocumentationFiles(in path: URL, pathExtensions: [String]) throws(FileSearchError)
17+
-> [URL]
18+
{
19+
let resourceValues: URLResourceValues
20+
do {
21+
resourceValues = try path.resourceValues(forKeys: [.isDirectoryKey])
22+
} catch {
23+
throw FileSearchError.cannotAccessPath(path.path, underlying: error)
24+
}
25+
26+
if resourceValues.isDirectory == true {
27+
return try searchDirectory(at: path, forExtensions: pathExtensions)
28+
} else {
29+
// Single file - check if it has a matching extension
30+
if pathExtensions.contains(where: { path.path.hasSuffix("." + $0) }) {
31+
return [path]
32+
} else {
33+
return []
34+
}
35+
}
36+
}
37+
}

Tests/SyntaxDocTests/DocumentationTestHarness.swift

Lines changed: 35 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,20 @@ internal class DocumentationTestHarness {
88
/// Project root directory calculated from the current file location
99
private static let projectRoot: URL = {
1010
let currentFileURL = URL(fileURLWithPath: #filePath)
11-
return currentFileURL
11+
return
12+
currentFileURL
1213
.deletingLastPathComponent() // Tests/SyntaxDocTests
1314
.deletingLastPathComponent() // Tests
1415
.deletingLastPathComponent() // Project root
1516
}()
16-
17+
1718
/// Document paths to search for documentation files
1819
private static let docPaths = [
1920
"Sources/SyntaxKit/Documentation.docc",
2021
"README.md",
2122
"Examples",
2223
]
23-
24+
2425
/// Default file extensions for documentation files
2526
private static let defaultPathExtensions = ["md"]
2627
/// Validates all code examples in all documentation files
@@ -37,17 +38,17 @@ internal class DocumentationTestHarness {
3738
}
3839

3940
/// Validates code examples in a specific file
40-
internal func validateExamplesInFile(_ filePath: String) async throws -> [ValidationResult] {
41-
let fullPath = try resolveFilePath(filePath)
42-
let content = try String(contentsOf: URL(fileURLWithPath: fullPath))
41+
internal func validateExamplesInFile(_ fileURL: URL) async throws -> [ValidationResult] {
42+
// let fullPath = try resolveFilePath(filePath)
43+
let content = try String(contentsOf: fileURL)
4344

4445
let codeBlocks = extractSwiftCodeBlocks(from: content)
4546
var results: [ValidationResult] = []
4647

4748
for (index, codeBlock) in codeBlocks.enumerated() {
4849
let result = await validateCodeBlock(
4950
code: codeBlock.code,
50-
filePath: filePath,
51+
fileURL: fileURL,
5152
blockIndex: index,
5253
lineNumber: codeBlock.lineNumber,
5354
blockType: codeBlock.blockType
@@ -122,10 +123,10 @@ internal class DocumentationTestHarness {
122123

123124
/// Determines the type of code block based on context
124125
private func determineBlockType(from line: String) -> CodeBlockType {
125-
// Look for type hints in the markdown
126-
if line.contains("Package.swift") {
127-
return .packageManifest
128-
} else if line.contains("bash") || line.contains("shell") {
126+
// if line.contains("Package.swift") {
127+
// return .packageManifest
128+
// }
129+
if line.contains("bash") || line.contains("shell") {
129130
return .shellCommand
130131
} else {
131132
return .example
@@ -135,28 +136,28 @@ internal class DocumentationTestHarness {
135136
/// Validates a single code block
136137
private func validateCodeBlock(
137138
code: String,
138-
filePath: String,
139+
fileURL: URL,
139140
blockIndex: Int,
140141
lineNumber: Int,
141142
blockType: CodeBlockType
142143
) async -> ValidationResult? {
143144
switch blockType {
144145
case .example:
145146
// Test compilation and basic execution
146-
return await validateSwiftExample(code, filePath: filePath, lineNumber: lineNumber)
147+
return await validateSwiftExample(code, fileURL: fileURL, lineNumber: lineNumber)
147148

148149
case .packageManifest:
149150
#if canImport(Foundation) && (os(macOS) || os(Linux))
150151
// Package.swift files need special handling
151-
return await validatePackageManifest(code, filePath: filePath, lineNumber: lineNumber)
152+
return await validatePackageManifest(code, fileURL: fileURL, lineNumber: lineNumber)
152153
#else
153154
return nil
154155
#endif
155156
case .shellCommand:
156157
// Skip shell commands for now
157158
return ValidationResult(
158159
success: true,
159-
filePath: filePath,
160+
fileURL: fileURL,
160161
lineNumber: lineNumber,
161162
testType: .skipped,
162163
error: nil
@@ -167,7 +168,7 @@ internal class DocumentationTestHarness {
167168
/// Validates a Swift code example
168169
private func validateSwiftExample(
169170
_ code: String,
170-
filePath: String,
171+
fileURL: URL,
171172
lineNumber: Int
172173
) async -> ValidationResult {
173174
do {
@@ -181,7 +182,7 @@ internal class DocumentationTestHarness {
181182
if !compileResult.success {
182183
return ValidationResult(
183184
success: false,
184-
filePath: filePath,
185+
fileURL: fileURL,
185186
lineNumber: lineNumber,
186187
testType: .compilation,
187188
error: "Compilation failed: \(compileResult.error ?? "Unknown error")"
@@ -193,7 +194,7 @@ internal class DocumentationTestHarness {
193194
// let executeResult = try await executeCompiledSwift(tempFile)
194195
return ValidationResult(
195196
success: true,
196-
filePath: filePath,
197+
fileURL: fileURL,
197198
lineNumber: lineNumber,
198199
testType: .execution,
199200
error: nil
@@ -202,7 +203,7 @@ internal class DocumentationTestHarness {
202203
// Just compilation test for non-runnable code
203204
return ValidationResult(
204205
success: true,
205-
filePath: filePath,
206+
fileURL: fileURL,
206207
lineNumber: lineNumber,
207208
testType: .compilation,
208209
error: nil
@@ -211,7 +212,7 @@ internal class DocumentationTestHarness {
211212
} catch {
212213
return ValidationResult(
213214
success: false,
214-
filePath: filePath,
215+
fileURL: fileURL,
215216
lineNumber: lineNumber,
216217
testType: .compilation,
217218
error: "Test setup failed: \(error.localizedDescription)"
@@ -223,7 +224,7 @@ internal class DocumentationTestHarness {
223224
/// Validates a Package.swift manifest
224225
private func validatePackageManifest(
225226
_ code: String,
226-
filePath: String,
227+
fileURL: URL,
227228
lineNumber: Int
228229
) async -> ValidationResult {
229230
do {
@@ -255,15 +256,15 @@ internal class DocumentationTestHarness {
255256

256257
return ValidationResult(
257258
success: success,
258-
filePath: filePath,
259+
fileURL: fileURL,
259260
lineNumber: lineNumber,
260261
testType: .compilation,
261262
error: error
262263
)
263264
} catch {
264265
return ValidationResult(
265266
success: false,
266-
filePath: filePath,
267+
fileURL: fileURL,
267268
lineNumber: lineNumber,
268269
testType: .compilation,
269270
error: "Package validation setup failed: \(error.localizedDescription)"
@@ -380,22 +381,26 @@ internal class DocumentationTestHarness {
380381
#endif
381382

382383
/// Finds all documentation files containing code examples
383-
@available(*, deprecated, message: "Use findDocumentationFiles(in:relativeTo:pathExtensions:) instead")
384-
private static func findDocumentationFiles() throws -> [String] {
385-
try FileManager.default.findDocumentationFiles(in: Self.docPaths, relativeTo: Self.projectRoot, pathExtensions: Self.defaultPathExtensions)
384+
@available(*, deprecated, message: "Use findDocumentationFiles(in:pathExtensions:) instead")
385+
private static func findDocumentationFiles() throws -> [URL] {
386+
try Self.docPaths.flatMap { docPath in
387+
let absolutePath = Self.projectRoot.appendingPathComponent(docPath)
388+
return try FileManager.default.findDocumentationFiles(
389+
in: absolutePath, pathExtensions: Self.defaultPathExtensions)
390+
}
386391
}
387392

388393
/// Resolves a relative file path to absolute path (public for use by test methods)
389-
internal func resolveRelativePath(_ filePath: String) throws -> String {
394+
internal func resolveRelativePath(_ filePath: String) throws -> URL {
390395
try resolveFilePath(filePath)
391396
}
392397

393398
/// Resolves a relative file path to absolute path
394-
private func resolveFilePath(_ filePath: String) throws -> String {
399+
private func resolveFilePath(_ filePath: String) throws -> URL {
395400
if filePath.hasPrefix("/") {
396-
return filePath
401+
return .init(filePath: filePath)
397402
} else {
398-
return Self.projectRoot.appendingPathComponent(filePath).path
403+
return Self.projectRoot.appendingPathComponent(filePath)
399404
}
400405
}
401406
}

Tests/SyntaxDocTests/DocumentationTestTypes/CodeBlockType.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Foundation
22

33
internal enum CodeBlockType {
44
case example
5+
@available(*, unavailable)
56
case packageManifest
67
case shellCommand
78
}

Tests/SyntaxDocTests/DocumentationTestTypes/ValidationResult.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Foundation
22

33
internal struct ValidationResult {
44
internal let success: Bool
5-
internal let filePath: String
5+
internal let fileURL: URL
66
internal let lineNumber: Int
77
internal let testType: TestType
88
internal let error: String?

0 commit comments

Comments
 (0)