import Foundation import SaplingCore import SaplingWorkspace import XCTest final class WorkspaceManagerTests: XCTestCase { private var temporaryRoots: [URL] = [] override func tearDownWithError() throws { for url in temporaryRoots { try? FileManager.default.removeItem(at: url) } temporaryRoots = [] } func testOpenWorkspaceBuildsFilesystemTree() async throws { let rootURL = try makeTemporaryWorkspace() try createDirectory("Notes", in: rootURL) try writeFile("Notes/Index.md", in: rootURL, contents: "# Notes") try writeFile("Notes/todo.txt", in: rootURL, contents: "todo") try createDirectory("Research/Archive", in: rootURL) try writeFile("Research/Archive/Paper.markdown", in: rootURL, contents: "# Paper") let workspace = try await LocalWorkspaceManager().openWorkspace(at: rootURL) XCTAssertEqual(workspace.rootURL, rootURL) XCTAssertEqual(workspace.items.map(\.displayName), ["Notes", "Research"]) let notes = try XCTUnwrap(workspace.folder(named: "Notes")) XCTAssertEqual(notes.children.map(\.displayName), ["Index.md", "todo.txt"]) XCTAssertEqual(try XCTUnwrap(notes.file(named: "Index.md")).kind, .markdown) XCTAssertEqual(try XCTUnwrap(notes.file(named: "todo.txt")).kind, .other) let archive = try XCTUnwrap(workspace.folder(named: "Research")?.folder(named: "Archive")) XCTAssertEqual(try XCTUnwrap(archive.file(named: "Paper.markdown")).kind, .markdown) } func testGitRepositoriesAreDetectedAsProjectsWithChildren() async throws { let rootURL = try makeTemporaryWorkspace() try createGitRepository("Sapling", in: rootURL) try writeFile("Sapling/README.md", in: rootURL, contents: "# Sapling") try createGitRepository("Research", in: rootURL) try writeFile("Research/Notes.md", in: rootURL, contents: "# Research") try createDirectory("Ordinary", in: rootURL) let workspace = try await LocalWorkspaceManager().openWorkspace(at: rootURL) XCTAssertEqual(workspace.items.map(\.displayName), ["Ordinary", "Research", "Sapling"]) let research = try XCTUnwrap(workspace.project(named: "Research")) let sapling = try XCTUnwrap(workspace.project(named: "Sapling")) XCTAssertEqual(research.gitRepository.statusSummary, .unknown) XCTAssertEqual(sapling.gitRepository.rootURL.lastPathComponent, "Sapling") XCTAssertEqual(research.children.map(\.displayName), ["Notes.md"]) XCTAssertNil(research.folder(named: ".git")) } func testHiddenFilesAndGitInternalsAreExcluded() async throws { let rootURL = try makeTemporaryWorkspace() try writeFile(".hidden.md", in: rootURL, contents: "# Hidden") try createGitRepository("Project", in: rootURL) try writeFile("Project/.git/config", in: rootURL, contents: "[core]") try writeFile("Project/Visible.md", in: rootURL, contents: "# Visible") let workspace = try await LocalWorkspaceManager().openWorkspace(at: rootURL) XCTAssertEqual(workspace.items.map(\.displayName), ["Project"]) let project = try XCTUnwrap(workspace.project(named: "Project")) XCTAssertEqual(project.children.map(\.displayName), ["Visible.md"]) } func testLargeFolderHierarchyScansWithinReasonableTime() async throws { let rootURL = try makeTemporaryWorkspace() for index in 0..<250 { let folderPath = String(format: "Folder-%03d/Nested", index) try createDirectory(folderPath, in: rootURL) try writeFile("\(folderPath)/Note-\(index).md", in: rootURL, contents: "# \(index)") } try createGitRepository("Project-000", in: rootURL) try writeFile("Project-000/README.md", in: rootURL, contents: "# Project") let start = Date() let workspace = try await LocalWorkspaceManager().openWorkspace(at: rootURL) let duration = Date().timeIntervalSince(start) XCTAssertEqual(workspace.items.count, 251) XCTAssertNotNil(workspace.project(named: "Project-000")) XCTAssertLessThan(duration, 2.0) } private func makeTemporaryWorkspace() throws -> URL { let url = FileManager.default.temporaryDirectory .appendingPathComponent("SaplingWorkspaceTests-\(UUID().uuidString)", isDirectory: true) try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) temporaryRoots.append(url) return url } private func createDirectory(_ path: String, in rootURL: URL) throws { try FileManager.default.createDirectory( at: rootURL.appendingPathComponent(path, isDirectory: true), withIntermediateDirectories: true ) } private func createGitRepository(_ path: String, in rootURL: URL) throws { try createDirectory(path, in: rootURL) try createDirectory("\(path)/.git", in: rootURL) } private func writeFile(_ path: String, in rootURL: URL, contents: String) throws { let url = rootURL.appendingPathComponent(path) try FileManager.default.createDirectory( at: url.deletingLastPathComponent(), withIntermediateDirectories: true ) try contents.write(to: url, atomically: true, encoding: .utf8) } } private extension Workspace { func folder(named name: String) -> WorkspaceFolder? { items.compactMap { item -> WorkspaceFolder? in guard case .folder(let folder) = item, folder.name == name else { return nil } return folder }.first } func project(named name: String) -> Project? { items.compactMap { item -> Project? in guard case .project(let project) = item, project.name == name else { return nil } return project }.first } } private extension WorkspaceFolder { func folder(named name: String) -> WorkspaceFolder? { children.compactMap { item -> WorkspaceFolder? in guard case .folder(let folder) = item, folder.name == name else { return nil } return folder }.first } func file(named name: String) -> WorkspaceFile? { children.compactMap { item -> WorkspaceFile? in guard case .file(let file) = item, file.name == name else { return nil } return file }.first } } private extension Project { func folder(named name: String) -> WorkspaceFolder? { children.compactMap { item -> WorkspaceFolder? in guard case .folder(let folder) = item, folder.name == name else { return nil } return folder }.first } }