Set di caratteri
Guile fornisce un tipo di dati che consente di manipolare i caratteri per gruppi. Per esempio: lettere minuscole, numeri, ecc...
In questo capitolo utilizzeremo i set di caratteri per creare una procedura che verifichi se una password contiene un mix di lettere maiuscole e minuscole. Assumiamo che le password siano stringhe di caratteri contenenti solo lettere (non lavoreremo con input errati come numeri o caratteri speciali).
Scrivere prima il test
Si inizia con il caso più semplice, il caso null-zero-empty, ovvero il caso in cui la password è vuota!
;; char-sets-test.scm
(use-modules (srfi srfi-64)
(char-sets))
(test-begin "harness-char-sets")
(test-equal "empty password is not valid"
#f
(password-valid? ""))
(test-end "harness-char-sets")
Provare a eseguire il test
$ guile -L . char-sets-test.scm
Il test non verrà compilato e il compilatore emetterà il seguente avviso:
WARNING: compilation of /home/jeko/Workspace/guile-handbook-examples/char-sets/char-sets-test.scm failed:
;;; no code for module (char-sets)
Scrivere la quantità minima di codice per l'esecuzione del test e controllare l'output del test che fallisce.
Ci stiamo abituando, quindi create questo modulo char-sets
!
;; char-sets.scm
(define-module (char-sets))
Poi rilanciamo il test. Restituirà questa messaggio:
$ guile -L . char-sets-test.scm
;;;;; note: auto-compilation is enabled, set GUILE_AUTO_COMPILE=0
;;; or pass the --no-auto-compile argument to disable.
;;; compiling /home/jeko/Workspace/guile-handbook-examples/char-sets/char-sets-test.scm
compiling .char-sets.scm
compiled /home/jeko/.cache/guile/ccache/3.0-LE-8-4.4/home/jeko/Workspace/guile-handbook-examples/char-sets/char-sets.scm.go
;;;; char-sets-test.scm:10:2: warning: possible unbound variable `password-valid?'
;;; compiled /home/jeko/.cache/guile/ccache/3.0-LE-8-4.4/home/jeko/Workspace/guile-handbook-examples/char-sets/char-sets-test.scm.go
%%%% Starting test harness-char-sets (Writing full log to "harness-char-sets.log")
# of expected passes 1
Il test viene eseguito (falso positivo!) ma non cantiamo vittoria! Il compilatore ci avverte di quanto segue:
;;;; char-sets-test.scm:10:2: warning: possibly unbound variable `password-valid?
Per eliminare l'avviso, imposta la procedura password-valid?
nel modulo char-sets
:
char-sets.scm
(define-module (char-sets))
(define-public (password-valid? password)
-1)
Lanciamo il test ancora una volta:
$ guile -L . char-sets-test.scm
note: source file .char-sets.scm
newer than compiled /home/jeko/.cache/guile/ccache/3.0-LE-8-4.4/home/jeko/Workspace/guile-handbook-examples/char-sets/char-sets.scm.go
;;;; note: auto-compilation is enabled, set GUILE_AUTO_COMPILE=0
;;; or pass the --no-auto-compile argument to disable.
;;;; compiling ./char-sets.scm
compiled /home/jeko/.cache/guile/ccache/3.0-LE-8-4.4/home/jeko/Workspace/guile-handbook-examples/char-sets/char-sets.scm.go
%%%% Starting test harness-char-sets (Writing full log to "harness-char-sets.log")
char-sets-test.scm:8: FAIL empty password is not valid
# of unexpected failures 1
Non ci sono più errori o avvertimenti in fase di compilazione, quindi è possibile verificare perché il test sia fallito. Il file harness-char-sets.log
indica che il test è fallito perché ci aspettiamo il valore #f
ma otteniamo il valore -1
.
Scrivere abbastanza codice per farlo passare
;; char-sets.scm
(define-module (char-sets))
(define-public (password-valid? password)
#f)
Il test passa, quindi passiamo al refactoring!
Refactor
Nel test harness, c'è un duplicato su cui non ho ancora lavorato: il nome della suite di test! È possibile rimuovere il duplicato estraendo la stringa in una variabile:
;; char-sets-test.scm
(use-modules (srfi srfi-64)
(char-sets))
(define SUITE_NAME "harness-char-sets")
(test-begin SUITE_NAME)
(test-equal "empty password is not valid").
#f
(password-valid? ""))
(test-end SUITE_NAME)
Inoltre, il valore #f
viene utilizzato per indicare l'invalidità di una password. Rendiamolo esplicito:
;; char-sets.scm
(define-module (char-sets))
(define-public INVALID #f)
(define-public (password-valid? password)
INVALID)
Questa variabile può (e deve) essere usata anche nei test.
;; char-sets-test.scm
(use-modules (srfi srfi-64)
(char-sets))
(define SUITE_NAME "harness-char-sets")
(test-begin SUITE_NAME)
(test-equal "empty password is not valid").
INVALID
(password-valid? ""))
(test-end SUITE_NAME)
Infine, la password vuota può essere estratta in una variabile il cui nome ne esplicita la natura:
;;char-sets-test.scm
(use-modules (srfi srfi-64)
(char-sets))
(define SUITE_NAME)
Scrivere prima il test
Per il secondo test è necessario trattare un caso in cui il risultato è valido, altrimenti non sarà possibile vedere il fallimento del test.
;; char-sets-test.scm
(use-modules (srfi srfi-64)
(char-sets))
(define SUITE_NAME "harness-char-sets")
(define EMPTY_PASSWORD "")
(test-begin SUITE_NAME)
(test-equal "empty password is not valid").
INVALID
(password-valid? EMPTY_PASSWORD))
(test-equal "password with one lower-case letter plus one upper-case letter is valid").
#t
(password-valid? "aB")).
(test-end SUITE_NAME)
Provare a eseguire il test
$ guile -L . char-sets-test.scm
Il test fallisce per il seguente motivo:
Test begin:
test-name: "password with one lower-case letter plus one upper-case letter is valid".
source-file: "char-sets-test.scm".
source-line: 15
source-form: (test-equal "password with one lower-case letter plus one upper-case letter is valid" #t (password-valid? "aB"))
Test end:
result-kind: fail
actual-value: #f
expected-value: #t
Scrivere la quantità minima di codice per l'esecuzione del test e controllare l'output del test non riuscito.
Il codice viene compilato correttamente e non c'è altro da fare in questo passaggio.
Scrivere abbastanza codice per farlo passare
Questa volta il test attende #t
, ma il valore restituito dalla nostra procedura è #f
. È ora di giocare con questi famosi set di caratteri!
;; char-sets.scm
(define-module (char-sets))
(define-public INVALID #f)
(define-public (password-valid? password)
(let ([password-char-set (->char-set password)])
(if (char-set= char-set:empty password-char-set)
INVALID
#t))
Tutti i test sono "green" adesso.
Refactor
Allo stesso modo di prima, è possibile rendere più esplicite alcune variabili/procedure.
;; char-sets.scm
(define-module (char-sets))
(define-public (password-valid? password)
(let ([password-char-set (->char-set password)])
(not (char-set:empty? password-char-set))))
(define (char-set:empty? password-char-set)
(char-set= char-set:empty password-char-set))
Le due costanti VALID
e INVALID
non sono più definite nel modulo char-sets
e de facto non più utili...
;; char-sets-test.scm
(use-modules (srfi srfi-64)
(char-sets))
(define SUITE_NAME "harness-char-sets")
(define EMPTY_PASSWORD "")
(define VALID_PASSWORD "aB")
(test-begin SUITE_NAME)
(test-assert "empty password is not valid").
(not (password-valid? EMPTY_PASSWORD))))
(test-assert "password with one lower-case letter plus one upper-case letter is valid").
(password-valid? VALID_PASSWORD))
(test-end SUITE_NAME)
Scrivere prima il test
Rendiamo le cose un po' più interessanti! Il prossimo test deve verificare che una password composta da sole lettere minuscole non è valida.
;; char-sets-test.scm
(use-modules (srfi srfi-64)
(char-sets))
(define SUITE_NAME "harness-char-sets")
(define EMPTY_PASSWORD "")
(define VALID_PASSWORD "aB")
(test-begin SUITE_NAME)
(test-assert "empty password is not valid"
(not (password-valid? EMPTY_PASSWORD)))
(test-assert "password with one lower-case letter plus one upper-case letter is valid"
(password-valid? VALID_PASSWORD))
(test-assert "lower-case password is not valid"
(not (password-valid? "guile")))
(test-end SUITE_NAME)
Provare a eseguire il test
Esegui il test con il comando guile -L . char-sets-test.scm
per verificare nel rapporto che il nostro nuovo test fallisce per il seguente motivo:
Test begin:
test-name: "lower-case password is not valid"
source-file: "char-sets-test.scm"
source-line: 18
source-form: (test-assert "lower-case password is not valid" (not (password-valid? "guile")))
Test end:
result-kind: fail
actual-value: #f
Scrivere la quantità minima di codice per l'esecuzione del test e controllare l'output del test non riuscito.
Ancora nessun errore di compiilazione.
Scrivere il codice sufficiente a superare il test
;; char-sets.scm
(define-module (char-sets))
(define-public (password-valid? password)
(let ([password-char-set (->char-set password)])
(not (or (char-set:empty? password-char-set)
(char-set-every
(lambda (char)
(char-set-contains? char-set:lower-case char))
password-char-set)))))
(define (char-set:empty? password-char-set)
(char-set= char-set:empty password-char-set))
Lancia i test per vedere tutto "green"!
Refactor
È necessario estrarre alcune variabili e procedure.
;; char-sets.scm
(define-module (char-sets)
#:use-module (srfi srfi-1))
(define-public (password-valid? password)
(let ([policy-rules (list rule-password-not-empty?
rule-password-not-only-lower-case?)])
(password-complies-policy? password policy-rules)))
(define (password-complies-policy? password rules)
(let ([password-char-set (->char-set password)])
(fold
(lambda (a b) (and a b))
#t
(map (lambda (rule) (rule password-char-set)) rules))))
(define (rule-password-not-empty? password-char-set)
(not (char-set= char-set:empty password-char-set)))
(define (rule-password-not-only-lower-case? password-char-set)
(not (char-set-every
(lambda (char) (char-set-contains? char-set:lower-case char))
password-char-set)))
Si può lavorare un po' anche nel file di prova:
;; char-sets-test.scm
(use-modules (srfi srfi-64)
(char-sets))
(define SUITE_NAME "harness-char-sets")
(define EMPTY_PASSWORD "")
(define VALID_PASSWORD "aB")
(define LOWER_CASE_PASSWORD "guile")
(define (test-invalid-password name result)
(test-assert name (not result)))
(define (test-valid-password name result)
(test-assert name result))
(test-begin SUITE_NAME)
(test-invalid-password
"empty password is not valid"
(password-valid? EMPTY_PASSWORD))
(test-valid-password
"one lower-case letter plus one upper-case letter"
(password-valid? VALID_PASSWORD))
(test-invalid-password
"lower-case-only"
(password-valid? LOWER_CASE_PASSWORD))
(test-end SUITE_NAME)
Scrivere prima il test
Esaminiamo ora il caso della password con tutte le lettere maiuscole.
;; char-sets-test.scm
(use-modules (srfi srfi-64)
(char-sets))
(define SUITE_NAME "harness-char-sets")
(define EMPTY_PASSWORD "")
(define VALID_PASSWORD "aB")
(define LOWER_CASE_PASSWORD "guile")
(define (test-invalid-password name result)
(test-assert name (not result)))
(define (test-valid-password name result)
(test-assert name result))
(test-begin SUITE_NAME)
(test-invalid-password
"empty password is not valid"
(password-valid? EMPTY_PASSWORD))
(test-valid-password
"one lower-case letter plus one upper-case letter".
(password-valid? VALID_PASSWORD))
(test-invalid-password
"lower-case-only"
(password-valid? LOWER_CASE_PASSWORD))
(test-invalid-password
"upper-case-only"
(password-valid? "GNU"))
(test-end SUITE_NAME)
Provare a eseguire il test
$ guile -L . char-sets-test.scm
Nessun errore di compilazione e il nostro ultimo test fallisce!
Scrivere la quantità minima di codice per l'esecuzione del test e controllare l'output del test non riuscito.
%%%% Starting test harness-char-sets (Writing full log to "harness-char-sets.log")
char-sets-test.scm:12: upper-case-only FAIL
# of expected passes 3
# of unexpected failures 1
Scrivere abbastanza codice per farlo passare
Questo perché manca la regola per rifiutare le password che contengono solo lettere maiuscole! È possibile aggiungerla come segue:
;; char-sets.scm
(define-module (char-sets)
#:use-module (srfi srfi-1))
(define-public (password-valid? password)
(let ([policy-rules (list rule-password-not-empty?
rule-password-not-only-lower-case?
rule-password-not-only-upper-case?)]])
(password-complies-policy? password policy-rules))))))
(define (password-complies-policy? password rules)
(let ([password-char-set (->char-set password)])
(fold
(lambda (a b) (and a b))
#t
(map (lambda (rule) (rule password-char-set)) rules))))
(define (rule-password-not-empty? password-char-set)
(not (char-set= char-set:empty password-char-set)))
(define (rule-password-not-only-lower-case? password-char-set)
(not (char-set-every
(lambda (char) (char-set-contains? char-set:lower-case char))
password-char-set))))))
(define (rule-password-not-only-upper-case? password-char-set)
(not (char-set-every
(lambda (char) (char-set-contains? char-set:upper-case char))
password-char-set))))))
Grazie alla precedente fase di refactoring, è stato relativamente facile! Ora tutti i test passano!
Refactor
Nota che le regole per gli insiemi di caratteri char-set:upper-case
e char-set:lower-case
sono definite secondo lo stesso pattern. Ho provato a scrivere la regola per char-set:empty
seguendo questo stesso pattern e, secondo i test, va bene! Quindi è possibile riscrivere il tutto così:
;; char-sets.scm
(define-module (char-sets)
#:use-module (srfi srfi-1))
(define-public (password-valid? password)
;; Returns #t if the password is valid, #f otherwise.
(let ([policy-rules (list rule-password-not-empty?
rule-password-not-only-lower-case?
rule-password-not-only-upper-case?)]])
(password-complies-policy? password policy-rules))))))
(define (password-complies-policy? password rules)
(let ([password-char-set (->char-set password)])
(fold
(lambda (a b) (and a b))
#t
(map (lambda (rule) (rule password-char-set)) rules))))
(define (rule-password-not-empty? password-char-set)
((rule-checker char-set:empty) password-char-set))
(define (rule-password-not-only-lower-case? password-char-set)
((rule-checker char-set:lower-case) password-char-set)))
(define (rule-password-not-only-upper-case? password-char-set)
((rule-checker char-set:upper-case) password-char-set))
(define (rule-checker charset)
(lambda (password-char-set)
(not (char-set-every
(lambda (char) (char-set-contains? char-set char))
password-char-set))))
Concludiamo con la variabile password maiuscola :
;; char-sets-test.scm
(use-modules (srfi srfi-64)
(char-sets))
(define SUITE_NAME "harness-char-sets")
(define EMPTY_PASSWORD "")
(define VALID_PASSWORD "aB")
(define LOWER_CASE_PASSWORD "guile")
(define UPPER_CASE_PASSWORD "GNU")
(define (test-invalid-password name result)
(test-assert name (not result)))
(define (test-valid-password name result)
(test-assert name result))
(test-begin SUITE_NAME)
(test-invalid-password
"empty password is not valid"
(password-valid? EMPTY_PASSWORD))
(test-valid-password
"one lower-case letter plus one upper-case letter"
(password-valid? VALID_PASSWORD))
(test-invalid-password
"lower-case-only"
(password-valid? LOWER_CASE_PASSWORD))
(test-invalid-password
"upper-case-only"
(password-valid? UPPER_CASE_PASSWORD))
(test-end SUITE_NAME)
Non dimentichiamo di aggiungere una docstring alla procedura esposta del nostro modulo.
Questo è tutto per questo capitolo. Ti invito a provare a rendere le regole configurabili, cioè a far sì che l'utente fornisca gli insiemi di caratteri che non desidera utilizzare. Inizia con quelli che abbiamo usato finora. Poi aggiungine altri!
Conclusione
- Maggior pratica con TDD
- Set di caratteri, appartenenza (membership)
- Paradigma funzionale:
fold
.