chess_test.gno
15.36 Kb ยท 609 lines
1package chess
2
3import (
4 "fmt"
5 "os"
6 "std"
7 "strconv"
8 "strings"
9 "testing"
10 "time"
11
12 "gno.land/p/demo/avl"
13 "gno.land/p/morgan/chess"
14 "gno.land/p/morgan/chess/glicko2"
15)
16
17func cleanup() {
18 gameStore = avl.Tree{}
19 gameIDCounter = 0
20 user2Games = avl.Tree{}
21 playerStore = avl.Tree{}
22 leaderboard = [CategoryMax]leaderboardType{}
23 lobby = [tcLobbyMax][]lobbyPlayer{}
24 lobbyPlayer2Game = avl.Tree{}
25 playerRatings = [CategoryMax][]*glicko2.PlayerRating{}
26}
27
28func TestNewGame(cur realm, t *testing.T) {
29 cleanup()
30
31 g := xNewGame(cur, std.DerivePkgAddr("xx").String(), 0, 0)
32 println(g)
33}
34
35const (
36 white std.Address = "g1white"
37 black std.Address = "g1black"
38)
39
40/*
41syntax:
42
43 [<command> ][#[!][<buf>] <checker>]
44
45command is executed; result of command is stored in buffer.
46the test is split in lines. other white space is ignored (strings.Fields).
47
48<buf>: all commands below will generally store a string result value in the
49buffer "result", which is the default and thus may be omitted.
50if the command panics, the panic value is stored in the buffer "panic".
51(if it doesn't, buffer panic is set to an empty string).
52if following a command there is no checker on the #panic buffer, the line
53"#!panic empty" is implicitly added.
54if <buf> is preceded by ! (e.g. "#!panic empty"), then if the checker fails,
55processing is stopped on that line.
56
57<command>:
58
59 newgame [<white> <black> [<seconds> [<increment>]]]
60 stores game ID in buffer #id.
61 <white> and <black> are two addresses. if they are not passed, <white>
62 assumes value "white" and <black> "black"
63 move <player> <lan_move>
64 lan_move is in the same format as Move.String.
65 retrieves game id from #id.
66 draw <player>
67 drawoffer <player>
68 abort <player>
69 timeout <player>
70 (ClaimTimeout)
71 resign <player>
72 game [<id>]
73 if not given, id is retrieved from buffer #id.
74 player <player>
75 name <predicate>
76 sets the name of the test to predicate.
77 copy <dst> [<src>]
78 copies buffer src to buffer dst.
79 if src not specified, assumed result.
80 (don't specify the #; ie: copy oldresult result)
81 sleep <seconds>
82 sleep for the given amount of seconds (float).
83
84NOTE: for all values of <player>, including <white> and <black> in newgame,
85the addresses are passed prefixed by "g1", and the matching checkers should
86expect this.
87
88<checker>:
89
90 empty
91 the buffer should be empty.
92 equal <predicate>
93 predicate may start with #, which indicates a buffer.
94 contains [<predicate>...]
95 the buffer should contain all of the given predicates.
96 containssp <predicate>
97 the buffer should contain the given predicate, which contains spaces.
98*/
99var commandTests = [...]string{
100 ` name NewGameNegativeIncrement
101 newgame white black 10 -5 #panic containssp negative increment invalid
102 `,
103 ` name NewGameDouble
104 newgame
105 newgame #panic contains is not yet finished
106 `,
107 ` name NewGameWithSelf
108 newgame white white #panic contains game with yourself
109 `,
110 // ColoursInvert within games played by two players
111 ` name ColoursInvert
112 newgame
113 move white e2e4
114 move black e7e5
115 move white f1c4
116 resign white
117 newgame
118 # contains "white":"g1black" "black":"g1white"
119 #id equal 0000002
120 `,
121 // Otherwise, invert from p1's history.
122 ` name ColoursInvert3p
123 newgame p1 p2 #! contains "white":"g1p1" "black":"g1p2"
124 move p1 e2e4
125 abort p1
126 newgame p1 p3 # contains "white":"g1p3" "black":"g1p1"
127 `,
128 ` name ScholarsMate
129 newgame #id equal 0000001
130 move white e2e4
131 move black e7e5
132 move white f1c4
133 move black b8c6
134 move white d1f3
135 move black d7d6
136 move white f3f7
137 copy moveres
138 game # equal #moveres
139 # contains "state":"checkmated" "winner":"white"
140 # containssp r1bqkbnr/ppp2Qpp/2np4/4p3/2B1P3/8/PPPP1PPP/RNB1K1NR b KQkq - 0 4
141 player white
142 # contains "address":"g1white" "position":0 "wins":1 "losses":0 "draws":0
143 player black
144 # contains "address":"g1black" "position":1 "wins":0 "losses":1 "draws":0
145 `,
146 ` name DrawByAgreement
147 newgame
148 move white e2e4
149 move black e7e5
150 move white f1c4
151 move black b8c6
152 copy moveres
153 game # equal #moveres
154 # contains "open" "concluder":null "draw_offerer":null
155 drawoffer white
156 # contains "open" "concluder":null "draw_offerer":"g1white"
157 draw black
158 # contains "drawn_by_agreement" "concluder":"g1black" "draw_offerer":"g1white"
159 `,
160 ` name AbortFirstMove
161 newgame
162 abort white # contains "winner":"none" "concluder":"g1white"
163 `,
164
165 ` name ThreefoldRepetition
166 newgame
167
168 move white g1f3
169 move black g8f6
170 move white f3g1
171 move black f6g8
172
173 move white g1f3
174 move black g8f6
175 move white f3g1
176 move black f6g8
177
178 draw black # contains "winner":"draw" "concluder":"g1black"
179 # contains "state":"drawn_3_fold"
180 `,
181 ` name FivefoldRepetition
182 newgame
183
184 move white g1f3
185 move black g8f6
186 move white f3g1
187 move black f6g8
188
189 move white g1f3
190 move black g8f6
191 move white f3g1
192 move black f6g8
193
194 move white g1f3
195 move black g8f6
196 move white f3g1
197 move black f6g8
198
199 move white g1f3
200 move black g8f6
201 move white f3g1
202 move black f6g8
203
204 # contains "winner":"draw" "concluder":null "state":"drawn_5_fold"
205
206 move white g1f3 #panic contains game is already finished
207 `,
208 ` name TimeoutAborted
209 newgame white black 3
210 move white e2e4 #! contains "state":"open"
211 sleep 31
212 move black e7e5
213 game
214 # contains e2e4
215 # contains "aborted"
216 # contains "concluder":"g1black"
217 `,
218 ` name TimeoutAbandoned
219 newgame white black 1
220 move white e2e4
221 move black e7e5
222 sleep 61
223 timeout black
224 # contains "state":"timeout" "winner":"black"
225 `,
226}
227
228func TestCommands(t *testing.T) {
229 for _, command := range commandTests {
230 runCommandTest(t, command)
231 }
232}
233
234// testCommandRunner is used to represent the single testCommand types below.
235// the Run function is used to execute the actual command, after parsing.
236//
237// This could have been implemented with simple closures generated within the
238// parser, however it's not because of this issue:
239// https://github.com/gnolang/gno/issues/1135
240type testCommandRunner interface {
241 Run(t *testing.T, bufs map[string]string)
242}
243
244// testCommandChecker is a wrapper for a runner which is performing a check.
245// This is marked in order not to wrap the calls to them as a panic.
246type testCommandChecker struct{ testCommandRunner }
247
248func (testCommandChecker) Checker() {}
249
250type testCommandFunc func(t *testing.T, bufs map[string]string)
251
252func (tc testCommandFunc) Run(t *testing.T, bufs map[string]string) { tc(t, bufs) }
253
254// testCommandColorID represents a testCommand, which uses a function of the
255// form func(gameID string) string (hence ID), and that takes as the first
256// parameter a <player> which will be the caller.
257type testCommandColorID struct {
258 fn func(realm, string) string
259 addr std.Address
260}
261
262func newTestCommandColorID(fn func(realm, string) string, s string, addr string) testCommandRunner {
263 return &testCommandColorID{fn, std.Address("g1" + addr)}
264}
265
266func (tc *testCommandColorID) Run(t *testing.T, bufs map[string]string) {
267 testing.SetRealm(std.NewUserRealm(tc.addr))
268 bufs["result"] = tc.fn(cross, bufs["id"])
269}
270
271type testCommandNewGame struct {
272 w, b std.Address
273 seconds, incr int
274}
275
276func (tc *testCommandNewGame) Run(t *testing.T, bufs map[string]string) {
277 testing.SetRealm(std.NewUserRealm(tc.w))
278 res := xNewGame(cross, string(tc.b), tc.seconds, tc.incr)
279 bufs["result"] = res
280
281 const idMagicString = `"id":"`
282 idx := strings.Index(res, idMagicString)
283 if idx < 0 {
284 panic("id not found")
285 }
286 id := res[idx+len(idMagicString):]
287 id = id[:strings.IndexByte(id, '"')]
288 bufs["id"] = id
289}
290
291type testCommandMove struct {
292 addr std.Address
293 from, to string
294 promotion chess.Piece
295}
296
297func (tc *testCommandMove) Run(t *testing.T, bufs map[string]string) {
298 testing.SetRealm(std.NewUserRealm(tc.addr))
299 bufs["result"] = MakeMove(cross, bufs["id"], tc.from, tc.to, tc.promotion)
300}
301
302type testCommandGame struct {
303 idWanted string
304}
305
306func (tc *testCommandGame) Run(t *testing.T, bufs map[string]string) {
307 idl := tc.idWanted
308 if idl == "" {
309 idl = bufs["id"]
310 }
311 bufs["result"] = GetGame(idl)
312}
313
314type testCommandPlayer struct {
315 addr string
316}
317
318func (tc *testCommandPlayer) Run(t *testing.T, bufs map[string]string) {
319 bufs["result"] = GetPlayer(tc.addr)
320}
321
322type testCommandCopy struct {
323 dst, src string
324}
325
326func (tc *testCommandCopy) Run(t *testing.T, bufs map[string]string) {
327 bufs[tc.dst] = bufs[tc.src]
328}
329
330type testCommandSleep struct {
331 dur time.Duration
332}
333
334func (tc *testCommandSleep) Run(t *testing.T, bufs map[string]string) {
335 os.Sleep(tc.dur)
336}
337
338type testChecker struct {
339 fn func(t *testing.T, bufs map[string]string, tc *testChecker)
340 tf func(*testing.T, string, ...interface{})
341 bufp string
342 preds []string
343}
344
345func (*testChecker) Checker() {}
346func (tc *testChecker) Run(t *testing.T, bufs map[string]string) {
347 tc.fn(t, bufs, tc)
348}
349
350func parseCommandTest(t *testing.T, command string) (funcs []testCommandRunner, testName string) {
351 lines := strings.Split(command, "\n")
352 atoi := func(s string) int {
353 n, err := strconv.Atoi(s)
354 checkErr(err)
355 return n
356 }
357 // used to detect whether to auto-add a panic checker
358 var hasPanicChecker bool
359 panicChecker := func(lineNum int, testName string) testCommandRunner {
360 return testCommandChecker{testCommandFunc(
361 func(t *testing.T, bufs map[string]string) {
362 if bufs["panic"] != "" {
363 t.Fatalf("%s:%d: buffer \"panic\" is not empty (%q)", testName, lineNum, bufs["panic"])
364 }
365 },
366 )}
367 }
368
369 for lineNum, line := range lines {
370 flds := strings.Fields(line)
371 if len(flds) == 0 {
372 continue
373 }
374 command, checker := flds, ([]string)(nil)
375 for idx, fld := range flds {
376 if strings.HasPrefix(fld, "#") {
377 command, checker = flds[:idx], flds[idx:]
378 break
379 }
380 }
381 var cmd string
382 if len(command) > 0 {
383 cmd = command[0]
384
385 // there is a new command; if hasPanicChecker == false,
386 // it means the previous command did not have a panic checker.
387 // add it.
388 if !hasPanicChecker && len(funcs) > 0 {
389 // no lineNum+1 because it was the previous line
390 funcs = append(funcs, panicChecker(lineNum, testName))
391 }
392 }
393 switch cmd {
394 case "": // move on
395 case "newgame":
396 w, b := white, black
397 var seconds, incr int
398 switch len(command) {
399 case 1:
400 case 5:
401 incr = atoi(command[4])
402 fallthrough
403 case 4:
404 seconds = atoi(command[3])
405 fallthrough
406 case 3:
407 w, b = std.Address("g1"+command[1]), std.Address("g1"+command[2])
408 default:
409 panic("invalid newgame command " + line)
410 }
411 funcs = append(funcs,
412 &testCommandNewGame{w, b, seconds, incr},
413 )
414 case "move":
415 if len(command) != 3 {
416 panic("invalid move command " + line)
417 }
418 if len(command[2]) < 4 || len(command[2]) > 5 {
419 panic("invalid lan move " + command[2])
420 }
421 from, to := command[2][:2], command[2][2:4]
422 var promotion chess.Piece
423 if len(command[2]) == 5 {
424 promotion = chess.PieceFromChar(command[2][4])
425 if promotion == chess.PieceEmpty {
426 panic("invalid piece for promotion: " + string(command[2][4]))
427 }
428 }
429 funcs = append(funcs, &testCommandMove{
430 addr: std.Address("g1" + command[1]),
431 from: from,
432 to: to,
433 promotion: promotion,
434 })
435 case "abort":
436 funcs = append(funcs, newTestCommandColorID(Abort, "abort", command[1]))
437 case "draw":
438 funcs = append(funcs, newTestCommandColorID(Draw, "draw", command[1]))
439 case "drawoffer":
440 funcs = append(funcs, newTestCommandColorID(DrawOffer, "drawoffer", command[1]))
441 case "timeout":
442 funcs = append(funcs, newTestCommandColorID(ClaimTimeout, "timeout", command[1]))
443 case "resign":
444 funcs = append(funcs, newTestCommandColorID(Resign, "resign", command[1]))
445 case "game":
446 if len(command) > 2 {
447 panic("invalid game command " + line)
448 }
449 tc := &testCommandGame{}
450 if len(command) == 2 {
451 tc.idWanted = command[1]
452 }
453 funcs = append(funcs, tc)
454 case "player":
455 if len(command) != 2 {
456 panic("invalid player command " + line)
457 }
458 funcs = append(funcs, &testCommandPlayer{"g1" + command[1]})
459 case "name":
460 testName = strings.Join(command[1:], " ")
461 case "copy":
462 if len(command) > 3 || len(command) < 2 {
463 panic("invalid copy command " + line)
464 }
465 tc := &testCommandCopy{dst: command[1], src: "result"}
466 if len(command) == 3 {
467 tc.src = command[2]
468 }
469 funcs = append(funcs, tc)
470 case "sleep":
471 if len(command) != 2 {
472 panic("invalid sleep command " + line)
473 }
474 funcs = append(funcs, &testCommandSleep{
475 time.Duration(atoi(command[1])) * time.Second,
476 })
477 default:
478 panic("invalid command " + cmd)
479 }
480
481 if len(checker) == 0 {
482 continue
483 }
484 if len(checker) == 1 {
485 panic("no checker specified " + line)
486 }
487
488 bufp := checker[0]
489 useFatal := false
490 if len(bufp) > 1 && bufp[1] == '!' {
491 bufp = bufp[2:]
492 useFatal = true
493 } else {
494 bufp = bufp[1:]
495 }
496 if bufp == "" {
497 bufp = "result"
498 }
499 if bufp == "panic" && !hasPanicChecker {
500 hasPanicChecker = true
501 }
502 tf := func(ln int, testName string, useFatal bool) func(*testing.T, string, ...interface{}) {
503 return func(t *testing.T, s string, v ...interface{}) {
504 fn := t.Errorf
505 if useFatal {
506 fn = t.Fatalf
507 }
508 fn("%s:%d: "+s, append([]interface{}{testName, ln}, v...)...)
509 }
510 }(lineNum+1, testName, useFatal)
511
512 switch checker[1] {
513 case "empty":
514 if len(checker) != 2 {
515 panic("invalid empty checker " + line)
516 }
517 funcs = append(funcs, &testChecker{
518 fn: func(t *testing.T, bufs map[string]string, tc *testChecker) {
519 if bufs[tc.bufp] != "" {
520 tc.tf(t, "buffer %q is not empty (%v)", tc.bufp, bufs[tc.bufp])
521 }
522 },
523 tf: tf,
524 bufp: bufp,
525 })
526 case "equal":
527 pred := strings.Join(checker[2:], " ")
528 funcs = append(funcs, &testChecker{
529 fn: func(t *testing.T, bufs map[string]string, tc *testChecker) {
530 exp := tc.preds[0]
531 if exp[0] == '#' {
532 exp = bufs[exp[1:]]
533 }
534 if bufs[tc.bufp] != exp {
535 tc.tf(t, "buffer %q: want %v got %v", tc.bufp, exp, bufs[tc.bufp])
536 }
537 },
538 tf: tf,
539 bufp: bufp,
540 preds: []string{pred},
541 })
542 case "contains":
543 preds := checker[2:]
544 if len(preds) == 0 {
545 break
546 }
547 funcs = append(funcs, &testChecker{
548 fn: func(t *testing.T, bufs map[string]string, tc *testChecker) {
549 for _, pred := range tc.preds {
550 if !strings.Contains(bufs[tc.bufp], pred) {
551 tc.tf(t, "buffer %q: %v does not contain %v", tc.bufp, bufs[tc.bufp], pred)
552 }
553 }
554 },
555 tf: tf,
556 bufp: bufp,
557 preds: preds,
558 })
559 case "containssp":
560 pred := strings.Join(checker[2:], " ")
561 if pred == "" {
562 panic("invalid contanssp checker " + line)
563 }
564 funcs = append(funcs, &testChecker{
565 fn: func(t *testing.T, bufs map[string]string, tc *testChecker) {
566 if !strings.Contains(bufs[tc.bufp], tc.preds[0]) {
567 tc.tf(t, "buffer %q: %v does not contain %v", tc.bufp, bufs[tc.bufp], tc.preds[0])
568 }
569 },
570 tf: tf,
571 bufp: bufp,
572 preds: []string{pred},
573 })
574 default:
575 panic("invalid checker " + checker[1])
576 }
577 }
578 if !hasPanicChecker {
579 funcs = append(funcs, panicChecker(len(lines), testName))
580 }
581 return
582}
583
584func runCommandTest(t *testing.T, command string) {
585 funcs, testName := parseCommandTest(t, command)
586
587 t.Run(testName, func(t *testing.T) {
588 cleanup()
589 bufs := make(map[string]string, 3)
590 for _, f := range funcs {
591 if _, ok := f.(interface{ Checker() }); ok {
592 f.Run(t, bufs)
593 } else {
594 catchPanic(f, t, bufs)
595 }
596 }
597 })
598}
599
600func catchPanic(tc testCommandRunner, t *testing.T, bufs map[string]string) {
601 // XXX: should prefer testing.Recover, but see: https://github.com/gnolang/gno/issues/1650
602 e := revive(func() { tc.Run(t, bufs) })
603 if e == nil {
604 bufs["panic"] = ""
605 return
606 }
607 bufs["result"] = ""
608 bufs["panic"] = fmt.Sprint(e)
609}