tree_test.go 14 KB

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