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}