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.