FileOtter/Tests/FileOtterTests/FileFnmatchTests.swift

159 lines
No EOL
6.7 KiB
Swift

//
// FileFnmatchTests.swift
// FileOtterTests
//
// Created by Sami Samhuri on 2025-08-19.
//
@testable import FileOtter
import XCTest
final class FileFnmatchTests: XCTestCase {
// MARK: - Basic Pattern Tests
func testExactMatch() throws {
XCTAssertTrue(File.fnmatch(pattern: "cat", path: "cat"))
XCTAssertFalse(File.fnmatch(pattern: "cat", path: "dog"))
}
func testPartialMatch() throws {
// Must match entire string
XCTAssertFalse(File.fnmatch(pattern: "cat", path: "category"))
XCTAssertFalse(File.fnmatch(pattern: "cat", path: "bobcat"))
}
// MARK: - Wildcard Tests
func testStarWildcard() throws {
XCTAssertTrue(File.fnmatch(pattern: "c*", path: "cats"))
XCTAssertTrue(File.fnmatch(pattern: "c*t", path: "cat"))
XCTAssertTrue(File.fnmatch(pattern: "c*t", path: "coat"))
XCTAssertTrue(File.fnmatch(pattern: "*at", path: "cat"))
XCTAssertTrue(File.fnmatch(pattern: "*at", path: "bat"))
XCTAssertFalse(File.fnmatch(pattern: "c*t", path: "dog"))
// Without pathname flag, * matches /
XCTAssertTrue(File.fnmatch(pattern: "c*t", path: "c/a/b/t"))
}
func testQuestionMarkWildcard() throws {
XCTAssertTrue(File.fnmatch(pattern: "c?t", path: "cat"))
XCTAssertTrue(File.fnmatch(pattern: "c?t", path: "cot"))
XCTAssertFalse(File.fnmatch(pattern: "c??t", path: "cat"))
XCTAssertTrue(File.fnmatch(pattern: "c??t", path: "coat"))
// ? doesn't match / with pathname flag
XCTAssertFalse(File.fnmatch(pattern: "c?t", path: "c/t", flags: .pathname))
}
// MARK: - Character Set Tests
func testCharacterSet() throws {
XCTAssertTrue(File.fnmatch(pattern: "ca[a-z]", path: "cat"))
XCTAssertFalse(File.fnmatch(pattern: "ca[0-9]", path: "cat"))
XCTAssertTrue(File.fnmatch(pattern: "ca[0-9]", path: "ca5"))
XCTAssertTrue(File.fnmatch(pattern: "[abc]at", path: "cat"))
XCTAssertTrue(File.fnmatch(pattern: "[abc]at", path: "bat"))
XCTAssertFalse(File.fnmatch(pattern: "[abc]at", path: "rat"))
}
func testNegatedCharacterSet() throws {
XCTAssertFalse(File.fnmatch(pattern: "ca[^t]", path: "cat"))
XCTAssertTrue(File.fnmatch(pattern: "ca[^t]", path: "cab"))
XCTAssertTrue(File.fnmatch(pattern: "ca[^t]", path: "can"))
XCTAssertFalse(File.fnmatch(pattern: "ca[^bcd]", path: "cab"))
XCTAssertTrue(File.fnmatch(pattern: "ca[^bcd]", path: "cat"))
}
// MARK: - Escape Tests
func testEscapedWildcard() throws {
// With noescape flag, backslash is literal
XCTAssertTrue(File.fnmatch(pattern: "\\*", path: "\\*", flags: .noescape))
XCTAssertTrue(File.fnmatch(pattern: "\\?", path: "\\?", flags: .noescape))
// Without noescape, backslash escapes the wildcard
XCTAssertTrue(File.fnmatch(pattern: "\\*", path: "*"))
XCTAssertTrue(File.fnmatch(pattern: "\\?", path: "?"))
XCTAssertFalse(File.fnmatch(pattern: "\\*", path: "anything"))
}
func testEscapeInBrackets() throws {
XCTAssertTrue(File.fnmatch(pattern: "[\\?]", path: "?"))
XCTAssertTrue(File.fnmatch(pattern: "[\\*]", path: "*"))
XCTAssertFalse(File.fnmatch(pattern: "[\\?]", path: "a"))
}
// MARK: - Flag Tests
func testCaseFoldFlag() throws {
XCTAssertFalse(File.fnmatch(pattern: "cat", path: "CAT", flags: []))
XCTAssertTrue(File.fnmatch(pattern: "cat", path: "CAT", flags: .casefold))
XCTAssertTrue(File.fnmatch(pattern: "CaT", path: "cat", flags: .casefold))
}
func testPathnameFlag() throws {
// Without pathname flag, * matches /
XCTAssertTrue(File.fnmatch(pattern: "*", path: "a/b", flags: []))
// With pathname flag, * doesn't match /
XCTAssertFalse(File.fnmatch(pattern: "*", path: "a/b", flags: .pathname))
XCTAssertTrue(File.fnmatch(pattern: "a/*", path: "a/b", flags: .pathname))
// ? also doesn't match / with pathname flag
XCTAssertFalse(File.fnmatch(pattern: "?", path: "/", flags: .pathname))
}
func testPeriodFlag() throws {
// By default, * doesn't match leading period (FNM_PERIOD is set)
XCTAssertFalse(File.fnmatch(pattern: "*", path: ".profile", flags: .period))
XCTAssertTrue(File.fnmatch(pattern: ".*", path: ".profile", flags: .period))
// Without period flag, * can match leading period
XCTAssertTrue(File.fnmatch(pattern: "*", path: ".profile", flags: []))
}
func testNoescapeFlag() throws {
// Without noescape, backslash escapes
XCTAssertTrue(File.fnmatch(pattern: "\\a", path: "a", flags: []))
XCTAssertFalse(File.fnmatch(pattern: "\\a", path: "\\a", flags: []))
// With noescape, backslash is literal
XCTAssertFalse(File.fnmatch(pattern: "\\a", path: "a", flags: .noescape))
XCTAssertTrue(File.fnmatch(pattern: "\\a", path: "\\a", flags: .noescape))
}
func testLeadingDirFlag() throws {
// FNM_LEADING_DIR allows pattern to match a leading portion
XCTAssertTrue(File.fnmatch(pattern: "*/foo", path: "bar/foo/baz", flags: .leadingDir))
XCTAssertTrue(File.fnmatch(pattern: "bar/foo", path: "bar/foo/baz", flags: .leadingDir))
XCTAssertFalse(File.fnmatch(pattern: "bar/foo", path: "bar/foo/baz", flags: []))
}
// MARK: - Complex Pattern Tests
func testComplexGlobPatterns() throws {
// Test various complex patterns
XCTAssertTrue(File.fnmatch(pattern: "*.txt", path: "file.txt"))
XCTAssertFalse(File.fnmatch(pattern: "*.txt", path: "file.md"))
// Multiple wildcards
XCTAssertTrue(File.fnmatch(pattern: "*.*", path: "file.txt"))
XCTAssertTrue(File.fnmatch(pattern: "test_*.rb", path: "test_file.rb"))
XCTAssertFalse(File.fnmatch(pattern: "test_*.rb", path: "spec_file.rb"))
}
func testHiddenFileMatching() throws {
// Without special flags, * matches everything including paths with /
XCTAssertTrue(File.fnmatch(pattern: "*", path: "dave/.profile", flags: []))
// With pathname flag, we need to be more specific
XCTAssertFalse(File.fnmatch(pattern: "*", path: "dave/.profile", flags: .pathname))
XCTAssertTrue(File.fnmatch(pattern: "dave/*", path: "dave/.profile", flags: .pathname))
XCTAssertTrue(File.fnmatch(pattern: "dave/.*", path: "dave/.profile", flags: .pathname))
// Hidden files in current directory
XCTAssertFalse(File.fnmatch(pattern: "*", path: ".hidden", flags: .period))
XCTAssertTrue(File.fnmatch(pattern: ".*", path: ".hidden", flags: .period))
}
}