import Testing import Foundation @testable import FaceProvider @Suite("Gesichtsvergleich") struct FaceMatcherTests { /// Ein Vektor, der sich um `offset` von der Grundform unterscheidet. private func vector(_ offset: Float, count: Int = 768) -> [Float] { (0.. FaceMatcher { FaceMatcher(templates: offsets.enumerated().map { FaceTemplate(pose: "p\($0.offset)", vector: vector($0.element)) }, requiredConfirmations: confirmations, maximumAttempts: attempts) } // MARK: - Abstand @Test("Derselbe Vektor hat Abstand null") func identicalIsZero() { #expect(FaceMatcher.distance(vector(0), vector(0)) == 0) } @Test("Verschieden lange Vektoren ergeben keinen Abstand") func lengthMismatchIsNil() { // Zwei verschieden lange Vektoren zu vergleichen ergäbe eine Zahl ohne // Bedeutung — und die sähe aus wie ein sehr guter Treffer. #expect(FaceMatcher.distance([1, 2, 3], [1, 2]) == nil) #expect(FaceMatcher.distance([], []) == nil) } @Test("Der kleinste Abstand über alle Posen zählt") func closestPoseWins() { // Die Posen sind absichtlich verschieden; ein Mittelwert bestrafte // genau die Vielfalt, für die sie aufgenommen wurden. let m = matcher(offsets: [0, 0.5]) let d = m.distance(to: vector(0.5)) #expect(d == 0) } // MARK: - Entscheidung @Test("Erst nach genug Bestätigungen wird geöffnet") func needsSeveralConfirmations() { // Die Kamera liefert dreißig Bilder je Sekunde — unter dreißig ist // irgendwann eines dabei, das zufällig passt. let m = matcher(confirmations: 3) var session = FaceMatcher.Session() #expect(m.evaluate(vector(0), in: &session) == .undecided(confirmations: 1)) #expect(m.evaluate(vector(0), in: &session) == .undecided(confirmations: 2)) #expect(m.evaluate(vector(0), in: &session) == .accepted) } @Test("Ein fremdes Gesicht wird abgelehnt") func strangerIsRejected() { let m = matcher() var session = FaceMatcher.Session() #expect(m.evaluate(vector(5), in: &session) == .undecided(confirmations: 0)) #expect(session.attempts == 1) } @Test("Ein Fehlversuch setzt die Bestätigungen zurück") func failureResetsConfirmations() { // Sonst summierten sich zufällige Treffer über Minuten zu einem Ja. let m = matcher(confirmations: 3) var session = FaceMatcher.Session() _ = m.evaluate(vector(0), in: &session) _ = m.evaluate(vector(0), in: &session) #expect(session.confirmations == 2) _ = m.evaluate(vector(5), in: &session) #expect(session.confirmations == 0) } @Test("Nach genug Fehlversuchen ist gesperrt") func locksOutAfterAttempts() { let m = matcher(attempts: 3) var session = FaceMatcher.Session() _ = m.evaluate(vector(5), in: &session) _ = m.evaluate(vector(5), in: &session) #expect(m.evaluate(vector(5), in: &session) == .lockedOut) } @Test("Nach der Sperre hilft auch das richtige Gesicht nicht mehr") func lockoutIsFinal() { // Ab hier bleibt nur das Passwort — sonst wäre die Sperre eine // Verzögerung und keine Grenze. let m = matcher(attempts: 2) var session = FaceMatcher.Session() _ = m.evaluate(vector(5), in: &session) _ = m.evaluate(vector(5), in: &session) #expect(m.evaluate(vector(0), in: &session) == .lockedOut) } @Test("Ohne hinterlegtes Gesicht öffnet nichts") func noEnrollmentNeverOpens() { // Ein leerer Vergleich darf kein Freifahrtschein sein. let m = FaceMatcher(templates: []) var session = FaceMatcher.Session() #expect(!m.isEnrolled) #expect(m.evaluate(vector(0), in: &session) == .undecided(confirmations: 0)) #expect(session.attempts == 1) } @Test("Ein Vektor falscher Länge gilt als Fehlversuch, nicht als Treffer") func wrongLengthIsAFailure() { let m = matcher() var session = FaceMatcher.Session() #expect(m.evaluate([1, 2, 3], in: &session) == .undecided(confirmations: 0)) #expect(session.attempts == 1) } @Test("Die Schwelle liegt näher am Ähnlichen als am Verschiedenen") func thresholdLeansStrict() { // Aus dem Spike: ähnlich 0,13 — verschieden 0,47. Ein falsches Ja ist // ein Fremder am entsperrten Rechner, ein falsches Nein nur ein // Passwort, das man ohnehin kennt. #expect(FaceMatcher.defaultThreshold < (0.13 + 0.47) / 2) #expect(FaceMatcher.defaultThreshold > 0.13) } }