Character sets
Guile provides a data type that allows you to manipulate characters by group. For example: lower case letters, numbers, ...
In this chapter we will use character sets to create a procedure that checks whether a password contains a mix of upper and lower case letters. We assume that passwords are strings of characters containing only letters (we will not work with wrong inputs such as numbers or special characters).
Write the test first
Start with the simplest case, the null-zero-empty case, here the case where the password is empty!
;; 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")
Try and run the test
$ guile -L . char-sets-test.scm
The test will fail to compile and the compiler will raise the following warning:
WARNING: compilation of /home/jeko/Workspace/guile-handbook-examples/char-sets/char-sets-test.scm failed:
;;; no code for module (char-sets)
Write the minimal amount of code for the test to run and check the failing test output.
We're getting used to it, so create this char-sets
module!
;; char-sets.scm
(define-module (char-sets))
Then rerun the test. It returns the following message:
$ 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
The test is run (false positive!) but let's not rush! The compiler warns us of the following:
;;;; char-sets-test.scm:10:2: warning: possibly unbound variable `password-valid?
To get rid of the warning, set the password-valid?
procedure in the char-sets
module:
char-sets.scm
(define-module (char-sets))
(define-public (password-valid? password)
-1)
Run the test one more time :
$ 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
No more error or warnings as compile time, so you can look into why the test failed. The harness-char-sets.log
file indicates that the test failed because we are waiting for the value #f
but we get the value -1
.
Write enough code to make it pass
;; char-sets.scm
(define-module (char-sets))
(define-public (password-valid? password)
#f)
The test passes, let's move on to refactoring!
Refactor
In the test harness, there is one duplication I haven't worked on so far: the name of the test suite! You can remove the duplication by extracting the string in a 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)
In addition, the value #f
is used here to say the invalidity of a password. Let's make this explicit:
;; char-sets.scm
(define-module (char-sets))
(define-public INVALID #f)
(define-public (password-valid? password)
INVALID)
This variable can (and should) also be used in 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)
Finally, the empty password can also be extracted in a variable whose name will make its nature explicit:
;;char-sets-test.scm
(use-modules (srfi srfi-64)
(char-sets))
(define SUITE_NAME)
Write the test first
For the second test you have to deal with a case where the result is valid, otherwise you will not be able to see your test fail.
;; 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)
Try and run the test
$ guile -L . char-sets-test.scm
The test fails for the following reason:
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
Write the minimal amount of code for the test to run and check the failing test output
The code compiles correctly with nothing else to do at this step.
Write enough code to make it pass
This time the test waits for #t
but the value returned by our procedure is #f
. It's time to play with these famous character sets!
;; 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))
All tests should be green now.
Refactor
In the same way as before, you can make some variables/procedures more explicit.
;; 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))
The two constants VALID
and INVALID
are no longer defined in the char-sets
module and are de facto no more useful...
;; 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)
Write the test first
Let's make things a bit more interesting now! The next test should check that a password composed only of lower case letters is invalid.
;; 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)
Try and run the test
Execute the test with the command guile -L . char-sets-test.scm
to see in the test report that our new test fails because :
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
Write the minimal amount of code for the test to run and check the failing test output.
Still no compilation error.
Write just enough code to pass the 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))
Run the tests to see that everything is green!
Refactor
Some extractions of variables and procedures has to be made.
;; 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)))
Some work can be done in the test file too :
;; 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)
Write the test first
Let's now look at the case of the password in all capital letters.
;; 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)
Try and run the test
$ guile -L . char-sets-test.scm
No compilation errors and our last test failed!
Write the minimal amount of code for the test to run and check the output
%%%% 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
Write enough code to make it pass
This is because the rule for rejecting passwords that contain only capital letters is missing! You can add it as follows:
;; 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))))))
Thanks to the previous refactoring step, it was relatively easy! Now all the tests are passing!
Refactor
Note that the rules for the character sets char-set:upper-case
and char-set:lower-case
are defined according to the same pattern. I tried to write the rule for char-set:empty
following this same pattern and according to the tests it's OK! So it is possible to rewrite the whole thing:
;; 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))))
Let's finish with the upper case password variable :
;; 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)
We will not forget to add a docstring to the exposed procedure of our module.
This will be all for this chapter. I invite you to try to make the rules configurable; i.e. the user provides the character sets they want to reject. Start with the ones we have used so far. Then add others that you have created yourself!
Conclusion
- More TDD practice
- Character sets, membership
- Functional paradigm:
fold
.