textlab_test.gno
4.90 Kb · 146 lines
1package textlab
2
3import (
4 "testing"
5
6 "gno.land/p/moul/x/daily/levenshtein/v0"
7 "gno.land/p/moul/x/daily/soundex/v0"
8 "gno.land/p/nt/uassert/v0"
9)
10
11// TestRomanOf covers the one package here whose input is a number rather than
12// a word, in both directions plus the two ways it can be neither.
13func TestRomanOf(t *testing.T) {
14 tests := []struct{ in, want string }{
15 {"1987", "MCMLXXXVII"},
16 {"1", "I"},
17 {"3999", "MMMCMXCIX"},
18 {"0", "out of range (1..3999)"},
19 {"-5", "out of range (1..3999)"},
20 {"4000", "out of range (1..3999)"},
21 {"MCMLXXXVII", "1987 (read as roman)"},
22 {"mcmlxxxvii", "1987 (read as roman)"},
23 {"knight", "not a number"},
24 }
25 for _, tt := range tests {
26 uassert.Equal(t, tt.want, romanOf(tt.in), "romanOf("+tt.in+")")
27 }
28}
29
30// TestTheDisagreement is the claim the realm is built to show, pinned in both
31// directions. Measured 2026-09-23; the numbers in the package doc and on the
32// page come from here, so if either package changes its mind this fails rather
33// than the page quietly stating something false.
34func TestTheDisagreement(t *testing.T) {
35 // One edit apart, and yet soundex says unrelated: it keys on the first
36 // letter, and the silent K changes it.
37 uassert.Equal(t, "K523", soundex.Encode("knight"))
38 uassert.Equal(t, "N230", soundex.Encode("night"))
39 uassert.False(t, soundex.Match("knight", "night"), "different soundex codes")
40 uassert.Equal(t, 1, levenshtein.Distance("knight", "night"))
41 uassert.Equal(t, 83, levenshtein.Similarity("knight", "night"))
42
43 // And the other way: two edits apart, identical codes.
44 uassert.Equal(t, "R163", soundex.Encode("robert"))
45 uassert.Equal(t, "R163", soundex.Encode("rupert"))
46 uassert.True(t, soundex.Match("robert", "rupert"), "same soundex code")
47 uassert.Equal(t, 2, levenshtein.Distance("robert", "rupert"))
48}
49
50// TestRomanOfDoesNotAbortOnOrdinaryWords is the defect this realm shipped with
51// until a test found it: romannum.FromRoman PANICS on a non-numeral instead of
52// returning zero, so the DEFAULT view aborted. "knight" is not a roman numeral,
53// and neither is most of what anyone would put in the URL.
54func TestRomanOfDoesNotAbortOnOrdinaryWords(t *testing.T) {
55 for _, w := range []string{"knight", "night", "a|b", "", "hello world", "MMM!"} {
56 uassert.Equal(t, "not a number", romanOf(w), "romanOf("+w+")")
57 }
58 uassert.True(t, isRomanNumeral("MCMLXXXVII"))
59 uassert.False(t, isRomanNumeral("KNIGHT"))
60 uassert.False(t, isRomanNumeral(""))
61}
62
63// TestInputIsBounded pins the ceiling. levenshtein.Matrix is O(len(a)*len(b)),
64// so an unbounded pair out of a URL is a way to make a query expensive for
65// whoever serves it.
66func TestInputIsBounded(t *testing.T) {
67 long := ""
68 for i := 0; i < maxInput+1; i++ {
69 long += "x"
70 }
71 uassert.True(t, tooLong(long))
72 uassert.False(t, tooLong("knight"))
73
74 out := Render(long)
75 uassert.True(t, len(out) < 600, "an over-long input renders a refusal, not a matrix")
76}
77
78// TestTableCellsAreEscaped: the input comes out of a URL and lands in a table
79// cell, where an unescaped pipe eats a column.
80func TestTableCellsAreEscaped(t *testing.T) {
81 out := Render("a|b")
82 uassert.False(t, contains(out, "| a|b |"), "a raw pipe reached a table cell")
83}
84
85func contains(h, n string) bool {
86 for i := 0; i+len(n) <= len(h); i++ {
87 if h[i:i+len(n)] == n {
88 return true
89 }
90 }
91 return false
92}
93
94// ExampleRender pins the root view, which is the default word and the "try
95// two" list. Nothing here moves with the chain.
96func ExampleRender() {
97 print(Render(""))
98 // Output:
99 // # allinone: textlab
100 //
101 // Eight packages over one input. [source](https://mygnoscan.moul.p2p.team/realm/r/moul/x/allinone/textlab/v0)
102 //
103 // ## Input: knight
104 //
105 // | package | answer |
106 // | --- | --- |
107 // | `x/daily/soundex` | K523 |
108 // | `x/daily/rot13` | xavtug |
109 // | `x/daily/piglatin` | ightknay |
110 // | `x/daily/romannum` | not a number |
111 //
112 // ## Try two
113 //
114 // - [knight vs night](/r/moul/x/allinone/textlab/v0:knight/night) (one edit apart, yet soundex says unrelated)
115 // - [robert vs rupert](/r/moul/x/allinone/textlab/v0:robert/rupert) (two edits apart, yet soundex says identical)
116 // - [1987 as a roman numeral](/r/moul/x/allinone/textlab/v0:1987)
117}
118
119// ExampleRender_pair pins the comparison view, which is the realm's whole
120// argument: the same pair, judged opposite ways by the two measures.
121func ExampleRender_pair() {
122 print(Render("knight/night"))
123 // Output:
124 // # allinone: textlab
125 //
126 // Eight packages over one input. [source](https://mygnoscan.moul.p2p.team/realm/r/moul/x/allinone/textlab/v0)
127 //
128 // ## Input: knight
129 //
130 // | package | answer |
131 // | --- | --- |
132 // | `x/daily/soundex` | K523 |
133 // | `x/daily/rot13` | xavtug |
134 // | `x/daily/piglatin` | ightknay |
135 // | `x/daily/romannum` | not a number |
136 //
137 // ## Compared with night
138 //
139 // | measure | answer |
140 // | --- | --- |
141 // | `soundex` of knight | K523 |
142 // | `soundex` of night | N230 |
143 // | sound alike | no |
144 // | `levenshtein` distance | 1 |
145 // | `levenshtein` similarity | 83% |
146}