Jeux de caractères

Guile met à disposition un type de données qui permet de manipuler les caractères par groupe. Par exemple : les lettres minuscules, les chiffres, …

Dans ce chapitre, nous allons utiliser les jeux de caractères pour créer une procédure qui vérifie si un mot de passe contient bien un mélange de lettres minuscules et majuscules. On assume que les mots de passe sont des chaines de caractères ne contenant que des lettres (on ne travaillera pas avec les nombres ou caractères spéciaux).

Écrire le test d’abord

Commences par le cas le plus simple, le cas nul, le cas où le mot de passe est vide !

;; 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")

Essayer et lancer le test

$ guile -L . char-sets-test.scm

Le test va échouer et le compilateur va lever l'avertissement suivant :

;;; WARNING: compilation of /home/jeko/Workspace/guile-handbook-examples/char-sets/char-sets-test.scm failed:
;;; no code for module (char-sets)

Écrire le minimum de code pour faire compiler le test et vérifier la raison de son échec

On commence à avoir l'habitude, crées donc ce module char-sets !

;; char-sets.scm

(define-module (char-sets))

Puis relances le test. Ce qui retourne le message suivant :

$ 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: possibly 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

Le test est exécuté mais ne nous précipitons pas ! Le compilateur nous averti de la chose suivante :

;;; char-sets-test.scm:10:2: warning: possibly unbound variable `password-valid?'

Pour lever l'avertissement, définis la procédure password-valid? dans le module char-sets :

;; char-sets.scm

(define-module (char-sets))

(define-public (password-valid? password)
  -1)

Relances le test :

$ 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

Plus d'erreur ou d'avertissement, tu peux donc te pencher sur la raison de l'échec du test. Le fichier harness-char-sets.log indique que le test échoue car nous attendons la valeur #f mais nous obtenons la valeur -1.

Écrire juste assez de code pour faire passer le test

;; char-sets.scm

(define-module (char-sets))

(define-public (password-valid? password)
  #f)

Le test passe, passons au réusinage !

Réusiner

Dans le harnais de test, il y a une dupplication sur laquelle je n'ai pas travaillé jusqu'à présent : le nom de la suite de tests ! Tu peux supprimer la dupplication en extrayant la chaine de caractère dans une variable :

;; 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)

De plus, la valeur #f est ici utilisée pour signifier l'invalidité d'un mot de passe. Rendons ça explicite :

;; char-sets.scm

(define-module (char-sets))

(define-public INVALID #f)

(define-public (password-valid? password)
  INVALID)

Cette variable peut aussi (et doit) être utilisée dans les tests.

;; 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)

Pour terminer, le mot de passe vide peut également être extrait dans une variable dont le nom rendra explicite sa nature :

;; 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-end SUITE_NAME)

Écrire le test d’abord

Pour le deuxième test, tu dois traiter un cas ou le résultat est valide, le cas échéant tu ne pourras pas voir ton test échouer.

;; 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)

Essayer et lancer le test

$ guile -L . char-sets-test.scm

Le test échoue pour la raison suivante :

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

Écrire le minimum de code pour faire compiler le test et vérifier la raison de son échec

Le code compile correctement sans rien d'autre à faire.

Écrire juste assez de code pour faire passer le test

Cette fois, le test attend #t mais la valeur retournée par notre procédure est #f. C'est l'heure de jouer avec ces fameux jeux de caractères !

;; 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))

En relançant les tests, tout est au vert.

Réusiner

De la même manière que précédemment, tu peux rendre quelques variables/procédures plus explicites.

;; 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))

Les deux constantes VALID et INVALID ne sont plus définies dans le module char-sets et ne sont finalement pas si utiles…

;; 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)

Écrire le test d’abord

Rendons les choses un peu plus intéressantes maintenant ! Le prochain test doit vérifier qu'un mot de passe composé uniquement de minuscules est invalide.

;; 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)

Essayer et lancer le test

Exécutes le test avec la commande guile -L . char-sets-test.scm pour voir dans le rapport de tests que notre nouveau test échoue car :

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

Écrire le minimum de code pour faire compiler le test et vérifier la raison de son échec

Toujours pas d'erreur de compilation.

Écrire juste assez de code pour faire passer le 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))

Relances les tests pour voir que tout est au vert !

Réusiner

Quelques extractions de variables et de procédures à prévoir.

;; 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)))

Le fichier de test peut être retouché également :

;; 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)

Écrire le test d’abord

Penchons nous maintenant sur le cas du mot de passe tout en majuscule.

;; 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)

Essayer et lancer le test

$ guile -L . char-sets-test.scm

Pas d'erreur de compilation et notre dernier test échoue !

Écrire le minimum de code pour faire compiler le test et vérifier la raison de son échec

%%%% Starting test harness-char-sets  (Writing full log to "harness-char-sets.log")
char-sets-test.scm:12: FAIL upper-case-only
# of expected passes      3
# of unexpected failures  1

Écrire juste assez de code pour faire passer le test

C'est parce qu'il manque la règle pour rejeter les mots de passe qui ne contiennent que des majuscules ! Tu peux l'ajouter comme suit :

;; 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)))

Grâce à l'étape précédente de réusinage, c'était relativement facile ! À présent, tous les tests passent !

Réusiner

On remarque que les règles pour les jeux de caractères char-set:upper-case et char-set:lower-case sont définies suivant un même pattern. J'ai essayé d'écrire la règle pour char-set:empty suivant ce pattern et d'après les tests c'est OK ! Donc il est possible de réusiner le tout :

;; 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? charset char))
      password-char-set))))

On extrait la variable du mot de passe en majuscule pour terminer

;; 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)

On n'oubliera pas d'ajouter une petite docstring à la procédure exposée de notre module.

Et voilà ! Ça sera tout pour ce chapitre. Je t'invite à essayer de rendre les règles configurables ; c'est-à-dire que l'utilisateur fourni les jeux de caractères qu'il veut rejeter. Commences par ceux que l'on a utilisé jusqu'ici. Ensuite, ajoutes-en d'autres que tu auras créé toi-même !

Conclusion

  • Plus de pratique du TDD
  • Jeux de caractères, appartenance
  • Paradigme fonctionnel : fold