tree_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677
  1. // Copyright 2013 Julien Schmidt. All rights reserved.
  2. // Use of this source code is governed by a BSD-style license that can be found
  3. // at https://github.com/julienschmidt/httprouter/blob/master/LICENSE
  4. package gin
  5. import (
  6. "fmt"
  7. "reflect"
  8. "strings"
  9. "testing"
  10. )
  11. func printChildren(n *node, prefix string) {
  12. fmt.Printf(" %02d:%02d %s%s[%d] %v %t %d \r\n", n.priority, n.maxParams, prefix, n.path, len(n.children), n.handlers, n.wildChild, n.nType)
  13. for l := len(n.path); l > 0; l-- {
  14. prefix += " "
  15. }
  16. for _, child := range n.children {
  17. printChildren(child, prefix)
  18. }
  19. }
  20. // Used as a workaround since we can't compare functions or their addressses
  21. var fakeHandlerValue string
  22. func fakeHandler(val string) HandlersChain {
  23. return HandlersChain{func(c *Context) {
  24. fakeHandlerValue = val
  25. }}
  26. }
  27. type testRequests []struct {
  28. path string
  29. nilHandler bool
  30. route string
  31. ps Params
  32. }
  33. func checkRequests(t *testing.T, tree *node, requests testRequests, unescapes ...bool) {
  34. unescape := false
  35. if len(unescapes) >= 1 {
  36. unescape = unescapes[0]
  37. }
  38. for _, request := range requests {
  39. handler, ps, _ := tree.getValue(request.path, nil, unescape)
  40. if handler == nil {
  41. if !request.nilHandler {
  42. t.Errorf("handle mismatch for route '%s': Expected non-nil handle", request.path)
  43. }
  44. } else if request.nilHandler {
  45. t.Errorf("handle mismatch for route '%s': Expected nil handle", request.path)
  46. } else {
  47. handler[0](nil)
  48. if fakeHandlerValue != request.route {
  49. t.Errorf("handle mismatch for route '%s': Wrong handle (%s != %s)", request.path, fakeHandlerValue, request.route)
  50. }
  51. }
  52. if !reflect.DeepEqual(ps, request.ps) {
  53. t.Errorf("Params mismatch for route '%s'", request.path)
  54. }
  55. }
  56. }
  57. func checkPriorities(t *testing.T, n *node) uint32 {
  58. var prio uint32
  59. for i := range n.children {
  60. prio += checkPriorities(t, n.children[i])
  61. }
  62. if n.handlers != nil {
  63. prio++
  64. }
  65. if n.priority != prio {
  66. t.Errorf(
  67. "priority mismatch for node '%s': is %d, should be %d",
  68. n.path, n.priority, prio,
  69. )
  70. }
  71. return prio
  72. }
  73. func checkMaxParams(t *testing.T, n *node) uint8 {
  74. var maxParams uint8
  75. for i := range n.children {
  76. params := checkMaxParams(t, n.children[i])
  77. if params > maxParams {
  78. maxParams = params
  79. }
  80. }
  81. if n.nType > root && !n.wildChild {
  82. maxParams++
  83. }
  84. if n.maxParams != maxParams {
  85. t.Errorf(
  86. "maxParams mismatch for node '%s': is %d, should be %d",
  87. n.path, n.maxParams, maxParams,
  88. )
  89. }
  90. return maxParams
  91. }
  92. func TestCountParams(t *testing.T) {
  93. if countParams("/path/:param1/static/*catch-all") != 2 {
  94. t.Fail()
  95. }
  96. if countParams(strings.Repeat("/:param", 256)) != 255 {
  97. t.Fail()
  98. }
  99. }
  100. func TestTreeAddAndGet(t *testing.T) {
  101. tree := &node{}
  102. routes := [...]string{
  103. "/hi",
  104. "/contact",
  105. "/co",
  106. "/c",
  107. "/a",
  108. "/ab",
  109. "/doc/",
  110. "/doc/go_faq.html",
  111. "/doc/go1.html",
  112. "/α",
  113. "/β",
  114. }
  115. for _, route := range routes {
  116. tree.addRoute(route, fakeHandler(route))
  117. }
  118. //printChildren(tree, "")
  119. checkRequests(t, tree, testRequests{
  120. {"/a", false, "/a", nil},
  121. {"/", true, "", nil},
  122. {"/hi", false, "/hi", nil},
  123. {"/contact", false, "/contact", nil},
  124. {"/co", false, "/co", nil},
  125. {"/con", true, "", nil}, // key mismatch
  126. {"/cona", true, "", nil}, // key mismatch
  127. {"/no", true, "", nil}, // no matching child
  128. {"/ab", false, "/ab", nil},
  129. {"/α", false, "/α", nil},
  130. {"/β", false, "/β", nil},
  131. })
  132. checkPriorities(t, tree)
  133. checkMaxParams(t, tree)
  134. }
  135. func TestTreeWildcard(t *testing.T) {
  136. tree := &node{}
  137. routes := [...]string{
  138. "/",
  139. "/cmd/:tool/:sub",
  140. "/cmd/:tool/",
  141. "/src/*filepath",
  142. "/search/",
  143. "/search/:query",
  144. "/user_:name",
  145. "/user_:name/about",
  146. "/files/:dir/*filepath",
  147. "/doc/",
  148. "/doc/go_faq.html",
  149. "/doc/go1.html",
  150. "/info/:user/public",
  151. "/info/:user/project/:project",
  152. }
  153. for _, route := range routes {
  154. tree.addRoute(route, fakeHandler(route))
  155. }
  156. //printChildren(tree, "")
  157. checkRequests(t, tree, testRequests{
  158. {"/", false, "/", nil},
  159. {"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}},
  160. {"/cmd/test", true, "", Params{Param{"tool", "test"}}},
  161. {"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{"tool", "test"}, Param{"sub", "3"}}},
  162. {"/src/", false, "/src/*filepath", Params{Param{"filepath", "/"}}},
  163. {"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}},
  164. {"/search/", false, "/search/", nil},
  165. {"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}},
  166. {"/search/someth!ng+in+ünìcodé/", true, "", Params{Param{"query", "someth!ng+in+ünìcodé"}}},
  167. {"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}},
  168. {"/user_gopher/about", false, "/user_:name/about", Params{Param{"name", "gopher"}}},
  169. {"/files/js/inc/framework.js", false, "/files/:dir/*filepath", Params{Param{"dir", "js"}, Param{"filepath", "/inc/framework.js"}}},
  170. {"/info/gordon/public", false, "/info/:user/public", Params{Param{"user", "gordon"}}},
  171. {"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{"user", "gordon"}, Param{"project", "go"}}},
  172. })
  173. checkPriorities(t, tree)
  174. checkMaxParams(t, tree)
  175. }
  176. func TestUnescapeParameters(t *testing.T) {
  177. tree := &node{}
  178. routes := [...]string{
  179. "/",
  180. "/cmd/:tool/:sub",
  181. "/cmd/:tool/",
  182. "/src/*filepath",
  183. "/search/:query",
  184. "/files/:dir/*filepath",
  185. "/info/:user/project/:project",
  186. "/info/:user",
  187. }
  188. for _, route := range routes {
  189. tree.addRoute(route, fakeHandler(route))
  190. }
  191. //printChildren(tree, "")
  192. unescape := true
  193. checkRequests(t, tree, testRequests{
  194. {"/", false, "/", nil},
  195. {"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}},
  196. {"/cmd/test", true, "", Params{Param{"tool", "test"}}},
  197. {"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}},
  198. {"/src/some/file+test.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file test.png"}}},
  199. {"/src/some/file++++%%%%test.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file++++%%%%test.png"}}},
  200. {"/src/some/file%2Ftest.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file/test.png"}}},
  201. {"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng in ünìcodé"}}},
  202. {"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{"user", "gordon"}, Param{"project", "go"}}},
  203. {"/info/slash%2Fgordon", false, "/info/:user", Params{Param{"user", "slash/gordon"}}},
  204. {"/info/slash%2Fgordon/project/Project%20%231", false, "/info/:user/project/:project", Params{Param{"user", "slash/gordon"}, Param{"project", "Project #1"}}},
  205. {"/info/slash%%%%", false, "/info/:user", Params{Param{"user", "slash%%%%"}}},
  206. {"/info/slash%%%%2Fgordon/project/Project%%%%20%231", false, "/info/:user/project/:project", Params{Param{"user", "slash%%%%2Fgordon"}, Param{"project", "Project%%%%20%231"}}},
  207. }, unescape)
  208. checkPriorities(t, tree)
  209. checkMaxParams(t, tree)
  210. }
  211. func catchPanic(testFunc func()) (recv interface{}) {
  212. defer func() {
  213. recv = recover()
  214. }()
  215. testFunc()
  216. return
  217. }
  218. type testRoute struct {
  219. path string
  220. conflict bool
  221. }
  222. func testRoutes(t *testing.T, routes []testRoute) {
  223. tree := &node{}
  224. for _, route := range routes {
  225. recv := catchPanic(func() {
  226. tree.addRoute(route.path, nil)
  227. })
  228. if route.conflict {
  229. if recv == nil {
  230. t.Errorf("no panic for conflicting route '%s'", route.path)
  231. }
  232. } else if recv != nil {
  233. t.Errorf("unexpected panic for route '%s': %v", route.path, recv)
  234. }
  235. }
  236. //printChildren(tree, "")
  237. }
  238. func TestTreeWildcardConflict(t *testing.T) {
  239. routes := []testRoute{
  240. {"/cmd/:tool/:sub", false},
  241. {"/cmd/vet", true},
  242. {"/src/*filepath", false},
  243. {"/src/*filepathx", true},
  244. {"/src/", true},
  245. {"/src1/", false},
  246. {"/src1/*filepath", true},
  247. {"/src2*filepath", true},
  248. {"/search/:query", false},
  249. {"/search/invalid", true},
  250. {"/user_:name", false},
  251. {"/user_x", true},
  252. {"/user_:name", false},
  253. {"/id:id", false},
  254. {"/id/:id", true},
  255. }
  256. testRoutes(t, routes)
  257. }
  258. func TestTreeChildConflict(t *testing.T) {
  259. routes := []testRoute{
  260. {"/cmd/vet", false},
  261. {"/cmd/:tool/:sub", true},
  262. {"/src/AUTHORS", false},
  263. {"/src/*filepath", true},
  264. {"/user_x", false},
  265. {"/user_:name", true},
  266. {"/id/:id", false},
  267. {"/id:id", true},
  268. {"/:id", true},
  269. {"/*filepath", true},
  270. }
  271. testRoutes(t, routes)
  272. }
  273. func TestTreeDupliatePath(t *testing.T) {
  274. tree := &node{}
  275. routes := [...]string{
  276. "/",
  277. "/doc/",
  278. "/src/*filepath",
  279. "/search/:query",
  280. "/user_:name",
  281. }
  282. for _, route := range routes {
  283. recv := catchPanic(func() {
  284. tree.addRoute(route, fakeHandler(route))
  285. })
  286. if recv != nil {
  287. t.Fatalf("panic inserting route '%s': %v", route, recv)
  288. }
  289. // Add again
  290. recv = catchPanic(func() {
  291. tree.addRoute(route, nil)
  292. })
  293. if recv == nil {
  294. t.Fatalf("no panic while inserting duplicate route '%s", route)
  295. }
  296. }
  297. //printChildren(tree, "")
  298. checkRequests(t, tree, testRequests{
  299. {"/", false, "/", nil},
  300. {"/doc/", false, "/doc/", nil},
  301. {"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}},
  302. {"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}},
  303. {"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}},
  304. })
  305. }
  306. func TestEmptyWildcardName(t *testing.T) {
  307. tree := &node{}
  308. routes := [...]string{
  309. "/user:",
  310. "/user:/",
  311. "/cmd/:/",
  312. "/src/*",
  313. }
  314. for _, route := range routes {
  315. recv := catchPanic(func() {
  316. tree.addRoute(route, nil)
  317. })
  318. if recv == nil {
  319. t.Fatalf("no panic while inserting route with empty wildcard name '%s", route)
  320. }
  321. }
  322. }
  323. func TestTreeCatchAllConflict(t *testing.T) {
  324. routes := []testRoute{
  325. {"/src/*filepath/x", true},
  326. {"/src2/", false},
  327. {"/src2/*filepath/x", true},
  328. }
  329. testRoutes(t, routes)
  330. }
  331. func TestTreeCatchAllConflictRoot(t *testing.T) {
  332. routes := []testRoute{
  333. {"/", false},
  334. {"/*filepath", true},
  335. }
  336. testRoutes(t, routes)
  337. }
  338. func TestTreeDoubleWildcard(t *testing.T) {
  339. const panicMsg = "only one wildcard per path segment is allowed"
  340. routes := [...]string{
  341. "/:foo:bar",
  342. "/:foo:bar/",
  343. "/:foo*bar",
  344. }
  345. for _, route := range routes {
  346. tree := &node{}
  347. recv := catchPanic(func() {
  348. tree.addRoute(route, nil)
  349. })
  350. if rs, ok := recv.(string); !ok || !strings.HasPrefix(rs, panicMsg) {
  351. t.Fatalf(`"Expected panic "%s" for route '%s', got "%v"`, panicMsg, route, recv)
  352. }
  353. }
  354. }
  355. /*func TestTreeDuplicateWildcard(t *testing.T) {
  356. tree := &node{}
  357. routes := [...]string{
  358. "/:id/:name/:id",
  359. }
  360. for _, route := range routes {
  361. ...
  362. }
  363. }*/
  364. func TestTreeTrailingSlashRedirect(t *testing.T) {
  365. tree := &node{}
  366. routes := [...]string{
  367. "/hi",
  368. "/b/",
  369. "/search/:query",
  370. "/cmd/:tool/",
  371. "/src/*filepath",
  372. "/x",
  373. "/x/y",
  374. "/y/",
  375. "/y/z",
  376. "/0/:id",
  377. "/0/:id/1",
  378. "/1/:id/",
  379. "/1/:id/2",
  380. "/aa",
  381. "/a/",
  382. "/admin",
  383. "/admin/:category",
  384. "/admin/:category/:page",
  385. "/doc",
  386. "/doc/go_faq.html",
  387. "/doc/go1.html",
  388. "/no/a",
  389. "/no/b",
  390. "/api/hello/:name",
  391. }
  392. for _, route := range routes {
  393. recv := catchPanic(func() {
  394. tree.addRoute(route, fakeHandler(route))
  395. })
  396. if recv != nil {
  397. t.Fatalf("panic inserting route '%s': %v", route, recv)
  398. }
  399. }
  400. //printChildren(tree, "")
  401. tsrRoutes := [...]string{
  402. "/hi/",
  403. "/b",
  404. "/search/gopher/",
  405. "/cmd/vet",
  406. "/src",
  407. "/x/",
  408. "/y",
  409. "/0/go/",
  410. "/1/go",
  411. "/a",
  412. "/admin/",
  413. "/admin/config/",
  414. "/admin/config/permissions/",
  415. "/doc/",
  416. }
  417. for _, route := range tsrRoutes {
  418. handler, _, tsr := tree.getValue(route, nil, false)
  419. if handler != nil {
  420. t.Fatalf("non-nil handler for TSR route '%s", route)
  421. } else if !tsr {
  422. t.Errorf("expected TSR recommendation for route '%s'", route)
  423. }
  424. }
  425. noTsrRoutes := [...]string{
  426. "/",
  427. "/no",
  428. "/no/",
  429. "/_",
  430. "/_/",
  431. "/api/world/abc",
  432. }
  433. for _, route := range noTsrRoutes {
  434. handler, _, tsr := tree.getValue(route, nil, false)
  435. if handler != nil {
  436. t.Fatalf("non-nil handler for No-TSR route '%s", route)
  437. } else if tsr {
  438. t.Errorf("expected no TSR recommendation for route '%s'", route)
  439. }
  440. }
  441. }
  442. func TestTreeRootTrailingSlashRedirect(t *testing.T) {
  443. tree := &node{}
  444. recv := catchPanic(func() {
  445. tree.addRoute("/:test", fakeHandler("/:test"))
  446. })
  447. if recv != nil {
  448. t.Fatalf("panic inserting test route: %v", recv)
  449. }
  450. handler, _, tsr := tree.getValue("/", nil, false)
  451. if handler != nil {
  452. t.Fatalf("non-nil handler")
  453. } else if tsr {
  454. t.Errorf("expected no TSR recommendation")
  455. }
  456. }
  457. func TestTreeFindCaseInsensitivePath(t *testing.T) {
  458. tree := &node{}
  459. routes := [...]string{
  460. "/hi",
  461. "/b/",
  462. "/ABC/",
  463. "/search/:query",
  464. "/cmd/:tool/",
  465. "/src/*filepath",
  466. "/x",
  467. "/x/y",
  468. "/y/",
  469. "/y/z",
  470. "/0/:id",
  471. "/0/:id/1",
  472. "/1/:id/",
  473. "/1/:id/2",
  474. "/aa",
  475. "/a/",
  476. "/doc",
  477. "/doc/go_faq.html",
  478. "/doc/go1.html",
  479. "/doc/go/away",
  480. "/no/a",
  481. "/no/b",
  482. }
  483. for _, route := range routes {
  484. recv := catchPanic(func() {
  485. tree.addRoute(route, fakeHandler(route))
  486. })
  487. if recv != nil {
  488. t.Fatalf("panic inserting route '%s': %v", route, recv)
  489. }
  490. }
  491. // Check out == in for all registered routes
  492. // With fixTrailingSlash = true
  493. for _, route := range routes {
  494. out, found := tree.findCaseInsensitivePath(route, true)
  495. if !found {
  496. t.Errorf("Route '%s' not found!", route)
  497. } else if string(out) != route {
  498. t.Errorf("Wrong result for route '%s': %s", route, string(out))
  499. }
  500. }
  501. // With fixTrailingSlash = false
  502. for _, route := range routes {
  503. out, found := tree.findCaseInsensitivePath(route, false)
  504. if !found {
  505. t.Errorf("Route '%s' not found!", route)
  506. } else if string(out) != route {
  507. t.Errorf("Wrong result for route '%s': %s", route, string(out))
  508. }
  509. }
  510. tests := []struct {
  511. in string
  512. out string
  513. found bool
  514. slash bool
  515. }{
  516. {"/HI", "/hi", true, false},
  517. {"/HI/", "/hi", true, true},
  518. {"/B", "/b/", true, true},
  519. {"/B/", "/b/", true, false},
  520. {"/abc", "/ABC/", true, true},
  521. {"/abc/", "/ABC/", true, false},
  522. {"/aBc", "/ABC/", true, true},
  523. {"/aBc/", "/ABC/", true, false},
  524. {"/abC", "/ABC/", true, true},
  525. {"/abC/", "/ABC/", true, false},
  526. {"/SEARCH/QUERY", "/search/QUERY", true, false},
  527. {"/SEARCH/QUERY/", "/search/QUERY", true, true},
  528. {"/CMD/TOOL/", "/cmd/TOOL/", true, false},
  529. {"/CMD/TOOL", "/cmd/TOOL/", true, true},
  530. {"/SRC/FILE/PATH", "/src/FILE/PATH", true, false},
  531. {"/x/Y", "/x/y", true, false},
  532. {"/x/Y/", "/x/y", true, true},
  533. {"/X/y", "/x/y", true, false},
  534. {"/X/y/", "/x/y", true, true},
  535. {"/X/Y", "/x/y", true, false},
  536. {"/X/Y/", "/x/y", true, true},
  537. {"/Y/", "/y/", true, false},
  538. {"/Y", "/y/", true, true},
  539. {"/Y/z", "/y/z", true, false},
  540. {"/Y/z/", "/y/z", true, true},
  541. {"/Y/Z", "/y/z", true, false},
  542. {"/Y/Z/", "/y/z", true, true},
  543. {"/y/Z", "/y/z", true, false},
  544. {"/y/Z/", "/y/z", true, true},
  545. {"/Aa", "/aa", true, false},
  546. {"/Aa/", "/aa", true, true},
  547. {"/AA", "/aa", true, false},
  548. {"/AA/", "/aa", true, true},
  549. {"/aA", "/aa", true, false},
  550. {"/aA/", "/aa", true, true},
  551. {"/A/", "/a/", true, false},
  552. {"/A", "/a/", true, true},
  553. {"/DOC", "/doc", true, false},
  554. {"/DOC/", "/doc", true, true},
  555. {"/NO", "", false, true},
  556. {"/DOC/GO", "", false, true},
  557. }
  558. // With fixTrailingSlash = true
  559. for _, test := range tests {
  560. out, found := tree.findCaseInsensitivePath(test.in, true)
  561. if found != test.found || (found && (string(out) != test.out)) {
  562. t.Errorf("Wrong result for '%s': got %s, %t; want %s, %t",
  563. test.in, string(out), found, test.out, test.found)
  564. return
  565. }
  566. }
  567. // With fixTrailingSlash = false
  568. for _, test := range tests {
  569. out, found := tree.findCaseInsensitivePath(test.in, false)
  570. if test.slash {
  571. if found { // test needs a trailingSlash fix. It must not be found!
  572. t.Errorf("Found without fixTrailingSlash: %s; got %s", test.in, string(out))
  573. }
  574. } else {
  575. if found != test.found || (found && (string(out) != test.out)) {
  576. t.Errorf("Wrong result for '%s': got %s, %t; want %s, %t",
  577. test.in, string(out), found, test.out, test.found)
  578. return
  579. }
  580. }
  581. }
  582. }
  583. func TestTreeInvalidNodeType(t *testing.T) {
  584. const panicMsg = "invalid node type"
  585. tree := &node{}
  586. tree.addRoute("/", fakeHandler("/"))
  587. tree.addRoute("/:page", fakeHandler("/:page"))
  588. // set invalid node type
  589. tree.children[0].nType = 42
  590. // normal lookup
  591. recv := catchPanic(func() {
  592. tree.getValue("/test", nil, false)
  593. })
  594. if rs, ok := recv.(string); !ok || rs != panicMsg {
  595. t.Fatalf("Expected panic '"+panicMsg+"', got '%v'", recv)
  596. }
  597. // case-insensitive lookup
  598. recv = catchPanic(func() {
  599. tree.findCaseInsensitivePath("/test", true)
  600. })
  601. if rs, ok := recv.(string); !ok || rs != panicMsg {
  602. t.Fatalf("Expected panic '"+panicMsg+"', got '%v'", recv)
  603. }
  604. }