128 lines
6.2 KiB
Swift
128 lines
6.2 KiB
Swift
import ConjureKit
|
|
import Foundation
|
|
import Testing
|
|
|
|
@Test func searchRanksExactPrefixSubstringAndFuzzyMatches() {
|
|
let names = ["Caramel", "My cl Tool", "Cloud", "Cold", "CL", "Unrelated"]
|
|
let applications = names.map(application)
|
|
|
|
#expect(ApplicationSearch.results(for: "cl", in: applications).map(\.name) == [
|
|
"CL", "Cloud", "My cl Tool", "Cold", "Caramel",
|
|
])
|
|
#expect(ApplicationSearch.results(for: "zq", in: applications).isEmpty)
|
|
}
|
|
|
|
@Test func searchFoldsAccentsAndTrimsWhitespace() {
|
|
let applications = ["Decafé", "Caféteria", "Café", "Camera"].map(application)
|
|
|
|
#expect(ApplicationSearch.results(for: " \n CAFÉ \t ", in: applications).map(\.name) == [
|
|
"Café", "Caféteria", "Decafé",
|
|
])
|
|
}
|
|
|
|
@Test func fuzzyMatchesPreferCompactSpansThenEarlierStartsAndAlphabeticalTies() {
|
|
let applications = ["AxxxxB", "xAyB", "AzB", "AyB", "B before A"].map(application)
|
|
|
|
#expect(ApplicationSearch.results(for: "ab", in: applications).map(\.name) == [
|
|
"AyB", "AzB", "xAyB", "AxxxxB",
|
|
])
|
|
}
|
|
|
|
@Test func emptySearchSortsAlphabeticallyAndBreaksDuplicateNameTiesByURL() {
|
|
let first = Application(url: URL(fileURLWithPath: "/First/Tools.app"), name: "Tools")
|
|
let second = Application(url: URL(fileURLWithPath: "/Second/Tools.app"), name: "Tools")
|
|
let applications = [second, first, application("Alpha")]
|
|
|
|
for query in ["", " \n\t "] {
|
|
#expect(ApplicationSearch.results(for: query, in: applications).map(\.url) == [
|
|
applications[2].url, first.url, second.url,
|
|
])
|
|
}
|
|
#expect(ApplicationSearch.results(for: "tools", in: applications).map(\.url) == [first.url, second.url])
|
|
}
|
|
|
|
@Test func discoveryFindsNestedAppsButNotHiddenInvalidOrBundledHelpers() throws {
|
|
let root = try temporaryDirectory()
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
|
|
let nested = root.appendingPathComponent("Utilities", isDirectory: true)
|
|
let alpha = try appBundle(in: nested, filename: "Alpha", name: "Alpha")
|
|
let parent = try appBundle(in: root, filename: "Parent", name: "Parent")
|
|
_ = try appBundle(in: parent.appendingPathComponent("Contents/Helpers"), filename: "Helper", name: "Helper")
|
|
_ = try appBundle(in: root, filename: ".Hidden", name: "Hidden")
|
|
_ = try appBundle(in: root, filename: "NoExecutable", name: "NoExecutable", executable: false)
|
|
_ = try appBundle(in: root, filename: "WrongType", name: "WrongType", packageType: "BNDL")
|
|
let fallback = try appBundle(in: root, filename: "Fallback", name: nil)
|
|
let display = try appBundle(in: root, filename: "Filename", name: "Internal", displayName: "Unlocalized")
|
|
let localization = display.appendingPathComponent("Contents/Resources/en.lproj", isDirectory: true)
|
|
try FileManager.default.createDirectory(at: localization, withIntermediateDirectories: true)
|
|
let localizedInfo = try PropertyListSerialization.data(
|
|
fromPropertyList: ["CFBundleDisplayName": "Display"], format: .xml, options: 0
|
|
)
|
|
try localizedInfo.write(to: localization.appendingPathComponent("InfoPlist.strings"))
|
|
let invalid = root.appendingPathComponent("Invalid.app", isDirectory: true)
|
|
_ = try appBundle(in: invalid, filename: "OtherHelper", name: "OtherHelper")
|
|
|
|
let applications = ApplicationCatalog.discover(in: [root, root.appendingPathComponent("Missing")])
|
|
#expect(applications.map(\.name) == ["Alpha", "Display", "Fallback", "Parent"])
|
|
#expect(applications.map(\.url) == [alpha, display, fallback, parent].map { $0.resolvingSymlinksInPath() })
|
|
}
|
|
|
|
@Test func discoveryCanonicalizesAliasesAndAvoidsDirectorySymlinkCycles() throws {
|
|
let root = try temporaryDirectory()
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
let nested = root.appendingPathComponent("Nested", isDirectory: true)
|
|
let app = try appBundle(in: nested, filename: "Editor", name: "Editor")
|
|
let alias = root.appendingPathComponent("Alias.app")
|
|
try FileManager.default.createSymbolicLink(at: alias, withDestinationURL: app)
|
|
try FileManager.default.createSymbolicLink(at: nested.appendingPathComponent("Loop"), withDestinationURL: root)
|
|
let linkedDirectory = root.appendingPathComponent("LinkedDirectory")
|
|
try FileManager.default.createSymbolicLink(at: linkedDirectory, withDestinationURL: nested)
|
|
|
|
let applications = ApplicationCatalog.discover(in: [root, linkedDirectory, root])
|
|
#expect(applications.count == 1)
|
|
#expect(applications.first?.id == app.resolvingSymlinksInPath())
|
|
#expect(Application(url: alias, name: "Alias").id == applications.first?.id)
|
|
}
|
|
|
|
private func application(_ name: String) -> Application {
|
|
Application(url: URL(fileURLWithPath: "/Applications/\(name).app"), name: name)
|
|
}
|
|
|
|
private func temporaryDirectory() throws -> URL {
|
|
let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
|
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
|
|
return url
|
|
}
|
|
|
|
@discardableResult
|
|
private func appBundle(
|
|
in directory: URL,
|
|
filename: String,
|
|
name: String?,
|
|
displayName: String? = nil,
|
|
executable: Bool = true,
|
|
packageType: String = "APPL"
|
|
) throws -> URL {
|
|
let url = directory.appendingPathComponent("\(filename).app", isDirectory: true)
|
|
let contents = url.appendingPathComponent("Contents", isDirectory: true)
|
|
let macOS = contents.appendingPathComponent("MacOS", isDirectory: true)
|
|
try FileManager.default.createDirectory(at: macOS, withIntermediateDirectories: true)
|
|
var info: [String: Any] = [
|
|
"CFBundlePackageType": packageType,
|
|
"CFBundleExecutable": "Launcher",
|
|
"CFBundleDevelopmentRegion": "en",
|
|
"CFBundleIdentifier": "test.conjure.\(UUID().uuidString)",
|
|
]
|
|
info["CFBundleName"] = name
|
|
info["CFBundleDisplayName"] = displayName
|
|
let data = try PropertyListSerialization.data(fromPropertyList: info, format: .xml, options: 0)
|
|
try data.write(to: contents.appendingPathComponent("Info.plist"))
|
|
if executable {
|
|
let binary = macOS.appendingPathComponent("Launcher")
|
|
try Data("#!/bin/sh\nexit 0\n".utf8).write(to: binary)
|
|
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: binary.path)
|
|
}
|
|
return url
|
|
}
|