[m-rev.] for review: make test hard_coded/string_class test more comprehensive
Julien Fischer
jfischer at opturion.com
Sun Sep 20 20:08:30 AEST 2026
For review by anyone.
--------------------------------
Make the hard_coded/string_class test more comprehensive.
- Test each test predicate on every test string.
- Include is_empty/1 is_all_alnum/1 in the predicates to test.
- Add some tests the boundaries of the ASCII range.
- Add some tests involving ill-formed strings.
Generate the output as a table for ease of comparison.
tests/hard_coded/string_class.m:
tests/hard_coded/string_class.exp:
Make the above changes.
tests/hard_coded/string_class.exp2:
Alternative expected output for UTF-16 systems.
Julien.
diff --git a/tests/hard_coded/string_class.exp
b/tests/hard_coded/string_class.exp
index 58ab1656d..c71324457 100644
--- a/tests/hard_coded/string_class.exp
+++ b/tests/hard_coded/string_class.exp
@@ -1,40 +1,83 @@
-is_all_alpha("")
-is_all_alpha("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
-is_all_alpha("abcdefghijklmnopqrstuvwxyz")
-not is_all_alpha("0123456789")
-not is_all_alpha("_")
-not is_all_alpha("aΓΞΎεπ.")
-not is_all_alpha("ΓΞΎεπ")
+empty = is_all_empty alpha_ = is_all_alpha_or_underscore
+alpha = is_all_alpha alnum_ = is_all_alnum_or_underscore
+alnum = is_all_alnum match = all_match(ascii_alpha)
+digit = is_all_digits nonasc = all_match(nonascii)
-is_all_alpha_or_underscore("")
-is_all_alpha_or_underscore("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
-is_all_alpha_or_underscore("abcdefghijklmnopqrstuvwxyz")
-not is_all_alpha_or_underscore("0123456789")
-is_all_alpha_or_underscore("_")
-not is_all_alpha_or_underscore("aΓΞΎεπ.")
-not is_all_alpha_or_underscore("ΓΞΎεπ")
-
-is_all_alnum_or_underscore("")
-is_all_alnum_or_underscore("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
-is_all_alnum_or_underscore("abcdefghijklmnopqrstuvwxyz")
-is_all_alnum_or_underscore("0123456789")
-is_all_alnum_or_underscore("_")
-not is_all_alnum_or_underscore("aΓΞΎεπ.")
-not is_all_alnum_or_underscore("ΓΞΎεπ")
-
-is_all_digits("")
-not is_all_digits("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
-not is_all_digits("abcdefghijklmnopqrstuvwxyz")
-is_all_digits("0123456789")
-not is_all_digits("_")
-not is_all_digits("aΓΞΎεπ.")
-not is_all_digits("ΓΞΎεπ")
-
-all_match(nonascii) ("")
-not all_match(nonascii) ("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
-not all_match(nonascii) ("abcdefghijklmnopqrstuvwxyz")
-not all_match(nonascii) ("0123456789")
-not all_match(nonascii) ("_")
-not all_match(nonascii) ("aΓΞΎεπ.")
-all_match(nonascii) ("ΓΞΎεπ")
+alpha and match should agree on every well-formed string.
+string empty alpha alnum alpha_ alnum_
digit match nonasc
+"" T T T T T
T T T
+" " F F F F F
F F F
+" " F F F F F
F F F
+"\t" F F F F F
F F F
+"\n" F F F F F
F F F
+"\r" F F F F F
F F F
+" \t\n\r" F F F F F
F F F
+"\u00a0" F F F F F
F F T
+"\u2002" F F F F F
F F T
+"\u3000" F F F F F
F F T
+"\u200b" F F F F F
F F T
+"\ufeff" F F F F F
F F T
+"\u200e" F F F F F
F F T
+"\u0301" F F F F F
F F T
+"a" F T T T T
F T F
+"Z" F T T T T
F T F
+"0" F F T F T
T F F
+"_" F F F T T
F F F
+"." F F F F F
F F F
+" " F F F F F
F F F
+"ABCDEFGHIJKLMNOPQRSTUVWXYZ" F T T T T
F T F
+"abcdefghijklmnopqrstuvwxyz" F T T T T
F T F
+"0123456789" F F T F T
T F F
+"___" F F F T T
F F F
+"abc123" F F T F T
F F F
+"123abc" F F T F T
F F F
+"a1b2c3" F F T F T
F F F
+"abc_123" F F F F T
F F F
+"_abc" F F F T T
F F F
+"abc_" F F F T T
F F F
+"a_1" F F F F T
F F F
+"_1" F F F F T
F F F
+"!abc" F F F F F
F F F
+"ab!c" F F F F F
F F F
+"abc!" F F F F F
F F F
+"!123" F F F F F
F F F
+"12!3" F F F F F
F F F
+"123!" F F F F F
F F F
+"@" F F F F F
F F F
+"[" F F F F F
F F F
+"`" F F F F F
F F F
+"{" F F F F F
F F F
+"/" F F F F F
F F F
+":" F F F F F
F F F
+"@A" F F F F F
F F F
+"Z[" F F F F F
F F F
+"`a" F F F F F
F F F
+"z{" F F F F F
F F F
+"/0" F F F F F
F F F
+"9:" F F F F F
F F F
+" ab" F F F F F
F F F
+"ab " F F F F F
F F F
+"a b" F F F F F
F F F
+" 12" F F F F F
F F F
+"12 " F F F F F
F F F
+"1 2" F F F F F
F F F
+"\u00e9" F F F F F
F F T
+"\u00df" F F F F F
F F T
+"\u03be" F F F F F
F F T
+"\u5555" F F F F F
F F T
+"\U00010000" F F F F F
F F T
+"\u00b2" F F F F F
F F T
+"\u0660" F F F F F
F F T
+"\uff11" F F F F F
F F T
+"a\u00df\u03be\u5555\U00010000." F F F F F
F F F
+"\u00df\u03be\u5555\U00010000" F F F F F
F F T
+"abc\u00e9" F F F F F
F F F
+"\u00e9abc" F F F F F
F F F
+"\xf0\" F F F F F
F F F
+"\x9f\\x98\\x80\" F F F F F
F F F
+"abc\xf0\" F F F F F
F F F
+"\xf0\abc" F F F F F
F F F
+"ab\xf0\cd" F F F F F
F F F
+"123\x9f\\x98\\x80\" F F F F F
F F F
+"\U0001f600" F F F F F
F F T
diff --git a/tests/hard_coded/string_class.exp2
b/tests/hard_coded/string_class.exp2
new file mode 100644
index 000000000..ecff47f5a
--- /dev/null
+++ b/tests/hard_coded/string_class.exp2
@@ -0,0 +1,83 @@
+empty = is_all_empty alpha_ = is_all_alpha_or_underscore
+alpha = is_all_alpha alnum_ = is_all_alnum_or_underscore
+alnum = is_all_alnum match = all_match(ascii_alpha)
+digit = is_all_digits nonasc = all_match(nonascii)
+
+alpha and match should agree on every well-formed string.
+
+string empty alpha alnum alpha_ alnum_
digit match nonasc
+"" T T T T T
T T T
+" " F F F F F
F F F
+" " F F F F F
F F F
+"\t" F F F F F
F F F
+"\n" F F F F F
F F F
+"\r" F F F F F
F F F
+" \t\n\r" F F F F F
F F F
+"\u00a0" F F F F F
F F T
+"\u2002" F F F F F
F F T
+"\u3000" F F F F F
F F T
+"\u200b" F F F F F
F F T
+"\ufeff" F F F F F
F F T
+"\u200e" F F F F F
F F T
+"\u0301" F F F F F
F F T
+"a" F T T T T
F T F
+"Z" F T T T T
F T F
+"0" F F T F T
T F F
+"_" F F F T T
F F F
+"." F F F F F
F F F
+" " F F F F F
F F F
+"ABCDEFGHIJKLMNOPQRSTUVWXYZ" F T T T T
F T F
+"abcdefghijklmnopqrstuvwxyz" F T T T T
F T F
+"0123456789" F F T F T
T F F
+"___" F F F T T
F F F
+"abc123" F F T F T
F F F
+"123abc" F F T F T
F F F
+"a1b2c3" F F T F T
F F F
+"abc_123" F F F F T
F F F
+"_abc" F F F T T
F F F
+"abc_" F F F T T
F F F
+"a_1" F F F F T
F F F
+"_1" F F F F T
F F F
+"!abc" F F F F F
F F F
+"ab!c" F F F F F
F F F
+"abc!" F F F F F
F F F
+"!123" F F F F F
F F F
+"12!3" F F F F F
F F F
+"123!" F F F F F
F F F
+"@" F F F F F
F F F
+"[" F F F F F
F F F
+"`" F F F F F
F F F
+"{" F F F F F
F F F
+"/" F F F F F
F F F
+":" F F F F F
F F F
+"@A" F F F F F
F F F
+"Z[" F F F F F
F F F
+"`a" F F F F F
F F F
+"z{" F F F F F
F F F
+"/0" F F F F F
F F F
+"9:" F F F F F
F F F
+" ab" F F F F F
F F F
+"ab " F F F F F
F F F
+"a b" F F F F F
F F F
+" 12" F F F F F
F F F
+"12 " F F F F F
F F F
+"1 2" F F F F F
F F F
+"\u00e9" F F F F F
F F T
+"\u00df" F F F F F
F F T
+"\u03be" F F F F F
F F T
+"\u5555" F F F F F
F F T
+"\U00010000" F F F F F
F F T
+"\u00b2" F F F F F
F F T
+"\u0660" F F F F F
F F T
+"\uff11" F F F F F
F F T
+"a\u00df\u03be\u5555\U00010000." F F F F F
F F F
+"\u00df\u03be\u5555\U00010000" F F F F F
F F T
+"abc\u00e9" F F F F F
F F F
+"\u00e9abc" F F F F F
F F F
+"\ud83d" F F F F F
F F T
+"\ude00" F F F F F
F F T
+"abc\ud83d" F F F F F
F F F
+"\ud83dabc" F F F F F
F F F
+"ab\ud83dcd" F F F F F
F F F
+"123\ude00" F F F F F
F F F
+"\U0001f600" F F F F F
F F T
diff --git a/tests/hard_coded/string_class.m b/tests/hard_coded/string_class.m
index c98ea03a8..9e07c0750 100644
--- a/tests/hard_coded/string_class.m
+++ b/tests/hard_coded/string_class.m
@@ -1,6 +1,20 @@
%---------------------------------------------------------------------------%
% vim: ts=4 sw=4 et ft=mercury
%---------------------------------------------------------------------------%
+%
+% Test the string predicates that test if every code point in a string
+% belongs to some class of characters.
+%
+% Each row of the output gives the result of every such predicate on one
+% test string.
+%
+% The .exp file is for backends using UTF-8 string encoding.
+% The .exp2 file is for backends using UTF-16 string encoding.
+% The two outputs differ only in the rows for ill-formed strings: on UTF-8
+% backends an ill-formed code unit is a lone byte, while on UTF-16 backends
+% it is an unpaired surrogate code point.
+%
+%---------------------------------------------------------------------------%
:- module string_class.
:- interface.
@@ -18,37 +32,71 @@
:- import_module int.
:- import_module list.
:- import_module string.
+:- import_module uint8.
%---------------------------------------------------------------------------%
main(!IO) :-
- test_is_all(is_all_alpha, "is_all_alpha", !IO),
- test_is_all(is_all_alpha_or_underscore, "is_all_alpha_or_underscore", !IO),
- test_is_all(is_all_alnum_or_underscore, "is_all_alnum_or_underscore", !IO),
- test_is_all(is_all_digits, "is_all_digits", !IO),
- test_is_all(all_match(nonascii), "all_match(nonascii) ", !IO).
-
-:- pred test_is_all(pred(string)::in(pred(in) is semidet), string::in,
- io::di, io::uo) is det.
-
-test_is_all(Pred, Name, !IO) :-
- test_is_all_2(Pred, Name, "", !IO),
- test_is_all_2(Pred, Name, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", !IO),
- test_is_all_2(Pred, Name, "abcdefghijklmnopqrstuvwxyz", !IO),
- test_is_all_2(Pred, Name, "0123456789", !IO),
- test_is_all_2(Pred, Name, "_", !IO),
- test_is_all_2(Pred, Name, "aΓΞΎεπ.", !IO),
- test_is_all_2(Pred, Name, "ΓΞΎεπ", !IO),
- io.nl(!IO).
-
-:- pred test_is_all_2(pred(string)::in(pred(in) is semidet), string::in,
- string::in, io::di, io::uo) is det.
-
-test_is_all_2(Pred, Name, Chars, !IO) :-
- ( if Pred(Chars) then
- io.format("%s(""%s"")\n", [s(Name), s(Chars)], !IO)
- else
- io.format("not %s(""%s"")\n", [s(Name), s(Chars)], !IO)
+ io.write_string(legend, !IO),
+ write_row("string",
+ "empty", "alpha", "alnum", "alpha_", "alnum_", "digit",
+ "match", "nonasc", !IO),
+ list.foldl(test_string, test_strings, !IO).
+
+:- func legend = string.
+
+legend =
+ "empty = is_all_empty alpha_ = is_all_alpha_or_underscore\n" ++
+ "alpha = is_all_alpha alnum_ = is_all_alnum_or_underscore\n" ++
+ "alnum = is_all_alnum match = all_match(ascii_alpha)\n" ++
+ "digit = is_all_digits nonasc = all_match(nonascii)\n" ++
+ "\n" ++
+ "alpha and match should agree on every well-formed string.\n" ++
+ "\n".
+
+%---------------------------------------------------------------------------%
+
+:- pred test_string(string::in, io::di, io::uo) is det.
+
+test_string(Str, !IO) :-
+ write_row(describe(Str),
+ tf(is_empty(Str)),
+ tf(is_all_alpha(Str)),
+ tf(is_all_alnum(Str)),
+ tf(is_all_alpha_or_underscore(Str)),
+ tf(is_all_alnum_or_underscore(Str)),
+ tf(is_all_digits(Str)),
+ tf(all_match(ascii_alpha, Str)),
+ tf(all_match(nonascii, Str)),
+ !IO).
+
+:- pred write_row(string::in, string::in, string::in, string::in, string::in,
+ string::in, string::in, string::in, string::in, io::di, io::uo) is det.
+
+write_row(Str, Empty, Alpha, Alnum, AlphaU, AlnumU, Digit, Match, NonAscii,
+ !IO) :-
+ io.format("%-36s %-5s %-5s %-5s %-6s %-6s %-5s %-5s %-6s\n",
+ [s(Str), s(Empty), s(Alpha), s(Alnum), s(AlphaU), s(AlnumU),
+ s(Digit), s(Match), s(NonAscii)], !IO).
+
+ % Call the given predicate, returning "T" if it succeeds and "F" if it
+ % fails.
+ %
+:- func tf((pred)::in((pred) is semidet)) = (string::out) is det.
+
+tf(Pred) = ( if Pred then "T" else "F" ).
+
+%---------------------------------------------------------------------------%
+
+ % A local definition of the class of characters that is_all_alpha is
+ % documented to accept.
+ %
+:- pred ascii_alpha(char::in) is semidet.
+
+ascii_alpha(Char) :-
+ char.to_int(Char, Int),
+ ( 0'A =< Int, Int =< 0'Z
+ ; 0'a =< Int, Int =< 0'z
).
:- pred nonascii(char::in) is semidet.
@@ -56,3 +104,196 @@ test_is_all_2(Pred, Name, Chars, !IO) :-
nonascii(Char) :-
char.to_int(Char, Int),
Int > 0x7f.
+
+%---------------------------------------------------------------------------%
+
+:- func test_strings = list(string).
+
+test_strings = [
+ % The empty string.
+ "",
+
+ % Strings that look empty, but are not.
+ " ",
+ " ",
+ "\t",
+ "\n",
+ "\r",
+ " \t\n\r",
+ "\u00a0", % no-break space
+ "\u2002", % en space
+ "\u3000", % ideographic space
+ "\u200b", % zero width space
+ "\ufeff", % zero width no-break space (BOM)
+ "\u200e", % left-to-right mark
+ "\u0301", % combining acute accent β a mark with no base
+
+ % One character from each relevant class.
+ "a",
+ "Z",
+ "0",
+ "_",
+ ".",
+ " ",
+
+ % Whole classes.
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
+ "abcdefghijklmnopqrstuvwxyz",
+ "0123456789",
+ "___",
+
+ % Mixtures.
+ "abc123",
+ "123abc",
+ "a1b2c3",
+ "abc_123",
+ "_abc",
+ "abc_",
+ "a_1",
+ "_1",
+
+ % The nonmatching character first, in the middle, and last.
+ "!abc",
+ "ab!c",
+ "abc!",
+ "!123",
+ "12!3",
+ "123!",
+
+ % The characters immediately outside each accepted ASCII range:
+ % '@' is 'A'-1, '[' is 'Z'+1, '`' is 'a'-1, '{' is 'z'+1,
+ % '/' is '0'-1 and ':' is '9'+1.
+ "@",
+ "[",
+ "`",
+ "{",
+ "/",
+ ":",
+ "@A",
+ "Z[",
+ "`a",
+ "z{",
+ "/0",
+ "9:",
+
+ % Whitespace next to accepted characters.
+ " ab",
+ "ab ",
+ "a b",
+ " 12",
+ "12 ",
+ "1 2",
+
+ % Non-ASCII letters and digits. Every predicate here except the
+ % all_match(nonascii) column is documented to accept ASCII only,
+ % so all of these should fail.
+ "\u00e9", % e with acute accent
+ "\u00df", % sharp s
+ "\u03be", % greek small letter xi
+ "\u5555", % a CJK ideograph
+ "\U00010000", % a non-BMP code point
+ "\u00b2", % superscript two
+ "\u0660", % arabic-indic digit zero
+ "\uff11", % fullwidth digit one
+ "aΓΞΎεπ.", % from the original version of this test
+ "ΓΞΎεπ", % from the original verison of this test
+ "abc\u00e9",
+ "\u00e9abc",
+
+ % Ill-formed code unit sequences, alone and adjacent to characters that
+ % the predicates do accept.
+ ilseq_head,
+ ilseq_tail,
+ "abc" ++ ilseq_head,
+ ilseq_head ++ "abc",
+ "ab" ++ ilseq_head ++ "cd",
+ "123" ++ ilseq_tail,
+ ilseq_head ++ ilseq_tail
+].
+
+ % The first code unit of a code point whose encoding needs
+ % more than one code unit.
+ %
+:- func ilseq_head = string.
+
+ilseq_head = string.between(non_bmp, 0, 1).
+
+ % The remaining code units of that same code point above.
+ % Each is an ill-formed string on its own, on every backend.
+ %
+:- func ilseq_tail = string.
+
+ilseq_tail = string.between(non_bmp, 1, string.length(non_bmp)).
+
+:- func non_bmp = string.
+
+non_bmp = "\U0001F600".
+
+%---------------------------------------------------------------------------%
+
+:- func describe(string) = string.
+
+describe(Str) = Quoted :-
+ describe_loop(Str, 0, [], RevPieces),
+ Quoted = "\"" ++ string.append_list(list.reverse(RevPieces)) ++ "\"".
+
+:- pred describe_loop(string::in, int::in,
+ list(string)::in, list(string)::out) is det.
+
+describe_loop(Str, Index, !RevPieces) :-
+ ( if
+ string.index_next_repl(Str, Index, NextIndex, Char, MaybeReplaced)
+ then
+ (
+ MaybeReplaced = replaced_code_unit(CodeUnit),
+ Piece = string.format("\\x%02x\\",
+ [i(uint8.to_int(CodeUnit))])
+ ;
+ MaybeReplaced = not_replaced,
+ Piece = describe_char(Char)
+ ),
+ !:RevPieces = [Piece | !.RevPieces],
+ describe_loop(Str, NextIndex, !RevPieces)
+ else
+ true
+ ).
+
+:- func describe_char(char) = string.
+
+describe_char(Char) = Piece :-
+ char.to_int(Char, Int),
+ ( if
+ Char = ('\\')
+ then
+ Piece = "\\\\"
+ else if
+ Char = ('"')
+ then
+ Piece = "\\\""
+ else if
+ Char = '\t'
+ then
+ Piece = "\\t"
+ else if
+ Char = '\r'
+ then
+ Piece = "\\r"
+ else if
+ Char = '\n'
+ then
+ Piece = "\\n"
+ else if
+ 0x20 =< Int, Int =< 0x7e
+ then
+ Piece = char_to_string(Char)
+ else if
+ Int =< 0xffff
+ then
+ Piece = string.format("\\u%04x", [i(Int)])
+ else
+ Piece = string.format("\\U%08x", [i(Int)])
+ ).
+
+%---------------------------------------------------------------------------%
+:- end_module string_class.
+%---------------------------------------------------------------------------%
More information about the reviews
mailing list