/cmd/grv/query_scan_test.go

https://github.com/rgburke/grv · Go · 985 lines · 974 code · 11 blank · 0 comment · 16 complexity · 92fd9e35d7eef74362a6b6a3a61215af MD5 · raw file

  1. package main
  2. import (
  3. "errors"
  4. "strings"
  5. "testing"
  6. )
  7. func TestScanSingleQueryToken(t *testing.T) {
  8. var singleTokenTests = []struct {
  9. input string
  10. expectedToken QueryToken
  11. }{
  12. {
  13. input: "\n \t\r\v\f",
  14. expectedToken: QueryToken{
  15. tokenType: QtkWhiteSpace,
  16. value: "\n \t\r\v\f",
  17. startPos: QueryScannerPos{
  18. line: 1,
  19. col: 1,
  20. },
  21. endPos: QueryScannerPos{
  22. line: 2,
  23. col: 5,
  24. },
  25. },
  26. },
  27. {
  28. input: "",
  29. expectedToken: QueryToken{
  30. tokenType: QtkEOF,
  31. startPos: QueryScannerPos{
  32. line: 1,
  33. col: 1,
  34. },
  35. endPos: QueryScannerPos{
  36. line: 1,
  37. col: 1,
  38. },
  39. },
  40. },
  41. {
  42. input: "AuthorName",
  43. expectedToken: QueryToken{
  44. tokenType: QtkIdentifier,
  45. value: "AuthorName",
  46. startPos: QueryScannerPos{
  47. line: 1,
  48. col: 1,
  49. },
  50. endPos: QueryScannerPos{
  51. line: 1,
  52. col: 10,
  53. },
  54. },
  55. },
  56. {
  57. input: "1234",
  58. expectedToken: QueryToken{
  59. tokenType: QtkNumber,
  60. value: "1234",
  61. startPos: QueryScannerPos{
  62. line: 1,
  63. col: 1,
  64. },
  65. endPos: QueryScannerPos{
  66. line: 1,
  67. col: 4,
  68. },
  69. },
  70. },
  71. {
  72. input: "12.34",
  73. expectedToken: QueryToken{
  74. tokenType: QtkNumber,
  75. value: "12.34",
  76. startPos: QueryScannerPos{
  77. line: 1,
  78. col: 1,
  79. },
  80. endPos: QueryScannerPos{
  81. line: 1,
  82. col: 5,
  83. },
  84. },
  85. },
  86. {
  87. input: ".1234",
  88. expectedToken: QueryToken{
  89. tokenType: QtkNumber,
  90. value: ".1234",
  91. startPos: QueryScannerPos{
  92. line: 1,
  93. col: 1,
  94. },
  95. endPos: QueryScannerPos{
  96. line: 1,
  97. col: 5,
  98. },
  99. },
  100. },
  101. {
  102. input: "-1234",
  103. expectedToken: QueryToken{
  104. tokenType: QtkNumber,
  105. value: "-1234",
  106. startPos: QueryScannerPos{
  107. line: 1,
  108. col: 1,
  109. },
  110. endPos: QueryScannerPos{
  111. line: 1,
  112. col: 5,
  113. },
  114. },
  115. },
  116. {
  117. input: "-12.34",
  118. expectedToken: QueryToken{
  119. tokenType: QtkNumber,
  120. value: "-12.34",
  121. startPos: QueryScannerPos{
  122. line: 1,
  123. col: 1,
  124. },
  125. endPos: QueryScannerPos{
  126. line: 1,
  127. col: 6,
  128. },
  129. },
  130. },
  131. {
  132. input: "-.1234",
  133. expectedToken: QueryToken{
  134. tokenType: QtkNumber,
  135. value: "-.1234",
  136. startPos: QueryScannerPos{
  137. line: 1,
  138. col: 1,
  139. },
  140. endPos: QueryScannerPos{
  141. line: 1,
  142. col: 6,
  143. },
  144. },
  145. },
  146. {
  147. input: "12.3.4",
  148. expectedToken: QueryToken{
  149. tokenType: QtkInvalid,
  150. value: "12.3.",
  151. startPos: QueryScannerPos{
  152. line: 1,
  153. col: 1,
  154. },
  155. endPos: QueryScannerPos{
  156. line: 1,
  157. col: 5,
  158. },
  159. err: errors.New("Unexpected '.' character in number"),
  160. },
  161. },
  162. {
  163. input: "-1234-",
  164. expectedToken: QueryToken{
  165. tokenType: QtkInvalid,
  166. value: "-1234-",
  167. startPos: QueryScannerPos{
  168. line: 1,
  169. col: 1,
  170. },
  171. endPos: QueryScannerPos{
  172. line: 1,
  173. col: 6,
  174. },
  175. err: errors.New("Unexpected '-' character in number"),
  176. },
  177. },
  178. {
  179. input: "\"Bug fix\"",
  180. expectedToken: QueryToken{
  181. tokenType: QtkString,
  182. value: "Bug fix",
  183. startPos: QueryScannerPos{
  184. line: 1,
  185. col: 1,
  186. },
  187. endPos: QueryScannerPos{
  188. line: 1,
  189. col: 9,
  190. },
  191. },
  192. },
  193. {
  194. input: "\"\\tBug\\n\\tfix\"",
  195. expectedToken: QueryToken{
  196. tokenType: QtkString,
  197. value: "\tBug\n\tfix",
  198. startPos: QueryScannerPos{
  199. line: 1,
  200. col: 1,
  201. },
  202. endPos: QueryScannerPos{
  203. line: 1,
  204. col: 14,
  205. },
  206. },
  207. },
  208. {
  209. input: "\"Unterminated string",
  210. expectedToken: QueryToken{
  211. tokenType: QtkInvalid,
  212. value: "\"Unterminated string",
  213. startPos: QueryScannerPos{
  214. line: 1,
  215. col: 1,
  216. },
  217. endPos: QueryScannerPos{
  218. line: 1,
  219. col: 20,
  220. },
  221. err: errors.New("Unterminated string"),
  222. },
  223. },
  224. {
  225. input: "AND",
  226. expectedToken: QueryToken{
  227. tokenType: QtkAnd,
  228. value: "AND",
  229. startPos: QueryScannerPos{
  230. line: 1,
  231. col: 1,
  232. },
  233. endPos: QueryScannerPos{
  234. line: 1,
  235. col: 3,
  236. },
  237. },
  238. },
  239. {
  240. input: "OR",
  241. expectedToken: QueryToken{
  242. tokenType: QtkOr,
  243. value: "OR",
  244. startPos: QueryScannerPos{
  245. line: 1,
  246. col: 1,
  247. },
  248. endPos: QueryScannerPos{
  249. line: 1,
  250. col: 2,
  251. },
  252. },
  253. },
  254. {
  255. input: "NOT",
  256. expectedToken: QueryToken{
  257. tokenType: QtkNot,
  258. value: "NOT",
  259. startPos: QueryScannerPos{
  260. line: 1,
  261. col: 1,
  262. },
  263. endPos: QueryScannerPos{
  264. line: 1,
  265. col: 3,
  266. },
  267. },
  268. },
  269. {
  270. input: "=",
  271. expectedToken: QueryToken{
  272. tokenType: QtkCmpEq,
  273. value: "=",
  274. startPos: QueryScannerPos{
  275. line: 1,
  276. col: 1,
  277. },
  278. endPos: QueryScannerPos{
  279. line: 1,
  280. col: 1,
  281. },
  282. },
  283. },
  284. {
  285. input: "!=",
  286. expectedToken: QueryToken{
  287. tokenType: QtkCmpNe,
  288. value: "!=",
  289. startPos: QueryScannerPos{
  290. line: 1,
  291. col: 1,
  292. },
  293. endPos: QueryScannerPos{
  294. line: 1,
  295. col: 2,
  296. },
  297. },
  298. },
  299. {
  300. input: "!>",
  301. expectedToken: QueryToken{
  302. tokenType: QtkInvalid,
  303. value: "!>",
  304. startPos: QueryScannerPos{
  305. line: 1,
  306. col: 1,
  307. },
  308. endPos: QueryScannerPos{
  309. line: 1,
  310. col: 2,
  311. },
  312. err: errors.New("Expected '=' character after '!'"),
  313. },
  314. },
  315. {
  316. input: ">",
  317. expectedToken: QueryToken{
  318. tokenType: QtkCmpGt,
  319. value: ">",
  320. startPos: QueryScannerPos{
  321. line: 1,
  322. col: 1,
  323. },
  324. endPos: QueryScannerPos{
  325. line: 1,
  326. col: 1,
  327. },
  328. },
  329. },
  330. {
  331. input: ">=",
  332. expectedToken: QueryToken{
  333. tokenType: QtkCmpGe,
  334. value: ">=",
  335. startPos: QueryScannerPos{
  336. line: 1,
  337. col: 1,
  338. },
  339. endPos: QueryScannerPos{
  340. line: 1,
  341. col: 2,
  342. },
  343. },
  344. },
  345. {
  346. input: "<",
  347. expectedToken: QueryToken{
  348. tokenType: QtkCmpLt,
  349. value: "<",
  350. startPos: QueryScannerPos{
  351. line: 1,
  352. col: 1,
  353. },
  354. endPos: QueryScannerPos{
  355. line: 1,
  356. col: 1,
  357. },
  358. },
  359. },
  360. {
  361. input: "<=",
  362. expectedToken: QueryToken{
  363. tokenType: QtkCmpLe,
  364. value: "<=",
  365. startPos: QueryScannerPos{
  366. line: 1,
  367. col: 1,
  368. },
  369. endPos: QueryScannerPos{
  370. line: 1,
  371. col: 2,
  372. },
  373. },
  374. },
  375. {
  376. input: "(",
  377. expectedToken: QueryToken{
  378. tokenType: QtkLparen,
  379. value: "(",
  380. startPos: QueryScannerPos{
  381. line: 1,
  382. col: 1,
  383. },
  384. endPos: QueryScannerPos{
  385. line: 1,
  386. col: 1,
  387. },
  388. },
  389. },
  390. {
  391. input: ")",
  392. expectedToken: QueryToken{
  393. tokenType: QtkRparen,
  394. value: ")",
  395. startPos: QueryScannerPos{
  396. line: 1,
  397. col: 1,
  398. },
  399. endPos: QueryScannerPos{
  400. line: 1,
  401. col: 1,
  402. },
  403. },
  404. },
  405. {
  406. input: "GLOB",
  407. expectedToken: QueryToken{
  408. tokenType: QtkCmpGlob,
  409. value: "GLOB",
  410. startPos: QueryScannerPos{
  411. line: 1,
  412. col: 1,
  413. },
  414. endPos: QueryScannerPos{
  415. line: 1,
  416. col: 4,
  417. },
  418. },
  419. },
  420. {
  421. input: "REGEXP",
  422. expectedToken: QueryToken{
  423. tokenType: QtkCmpRegexp,
  424. value: "REGEXP",
  425. startPos: QueryScannerPos{
  426. line: 1,
  427. col: 1,
  428. },
  429. endPos: QueryScannerPos{
  430. line: 1,
  431. col: 6,
  432. },
  433. },
  434. },
  435. }
  436. for _, singleTokenTest := range singleTokenTests {
  437. scanner := NewQueryScanner(strings.NewReader(singleTokenTest.input))
  438. token, err := scanner.Scan()
  439. if err != nil {
  440. t.Errorf("Scan failed with error %v", err)
  441. } else if !token.Equal(&singleTokenTest.expectedToken) {
  442. t.Errorf("QueryToken does not match expected value. Expected %v, Actual %v", singleTokenTest.expectedToken, *token)
  443. }
  444. }
  445. }
  446. func TestOperatorsAreCaseInsensitive(t *testing.T) {
  447. var operatorTokenTests = []struct {
  448. input string
  449. expectedToken QueryToken
  450. }{
  451. {
  452. input: "and",
  453. expectedToken: QueryToken{
  454. tokenType: QtkAnd,
  455. value: "and",
  456. startPos: QueryScannerPos{
  457. line: 1,
  458. col: 1,
  459. },
  460. endPos: QueryScannerPos{
  461. line: 1,
  462. col: 3,
  463. },
  464. },
  465. },
  466. {
  467. input: "Or",
  468. expectedToken: QueryToken{
  469. tokenType: QtkOr,
  470. value: "Or",
  471. startPos: QueryScannerPos{
  472. line: 1,
  473. col: 1,
  474. },
  475. endPos: QueryScannerPos{
  476. line: 1,
  477. col: 2,
  478. },
  479. },
  480. },
  481. {
  482. input: "nOT",
  483. expectedToken: QueryToken{
  484. tokenType: QtkNot,
  485. value: "nOT",
  486. startPos: QueryScannerPos{
  487. line: 1,
  488. col: 1,
  489. },
  490. endPos: QueryScannerPos{
  491. line: 1,
  492. col: 3,
  493. },
  494. },
  495. },
  496. {
  497. input: "GlOb",
  498. expectedToken: QueryToken{
  499. tokenType: QtkCmpGlob,
  500. value: "GlOb",
  501. startPos: QueryScannerPos{
  502. line: 1,
  503. col: 1,
  504. },
  505. endPos: QueryScannerPos{
  506. line: 1,
  507. col: 4,
  508. },
  509. },
  510. },
  511. {
  512. input: "ReGeXp",
  513. expectedToken: QueryToken{
  514. tokenType: QtkCmpRegexp,
  515. value: "ReGeXp",
  516. startPos: QueryScannerPos{
  517. line: 1,
  518. col: 1,
  519. },
  520. endPos: QueryScannerPos{
  521. line: 1,
  522. col: 6,
  523. },
  524. },
  525. },
  526. }
  527. for _, operatorTokenTest := range operatorTokenTests {
  528. scanner := NewQueryScanner(strings.NewReader(operatorTokenTest.input))
  529. token, err := scanner.Scan()
  530. if err != nil {
  531. t.Errorf("Scan failed with error %v", err)
  532. } else if !token.Equal(&operatorTokenTest.expectedToken) {
  533. t.Errorf("QueryToken does not match expected value. Expected %v, Actual %v", operatorTokenTest.expectedToken, *token)
  534. }
  535. }
  536. }
  537. func TestScanMultipleTokens(t *testing.T) {
  538. var multiTokenTests = []struct {
  539. input string
  540. expectedTokens []QueryToken
  541. }{
  542. {
  543. input: "authorName=\"John Smith\"",
  544. expectedTokens: []QueryToken{
  545. {
  546. tokenType: QtkIdentifier,
  547. value: "authorName",
  548. startPos: QueryScannerPos{
  549. line: 1,
  550. col: 1,
  551. },
  552. endPos: QueryScannerPos{
  553. line: 1,
  554. col: 10,
  555. },
  556. },
  557. {
  558. tokenType: QtkCmpEq,
  559. value: "=",
  560. startPos: QueryScannerPos{
  561. line: 1,
  562. col: 11,
  563. },
  564. endPos: QueryScannerPos{
  565. line: 1,
  566. col: 11,
  567. },
  568. },
  569. {
  570. tokenType: QtkString,
  571. value: "John Smith",
  572. startPos: QueryScannerPos{
  573. line: 1,
  574. col: 12,
  575. },
  576. endPos: QueryScannerPos{
  577. line: 1,
  578. col: 23,
  579. },
  580. },
  581. {
  582. tokenType: QtkEOF,
  583. startPos: QueryScannerPos{
  584. line: 1,
  585. col: 23,
  586. },
  587. endPos: QueryScannerPos{
  588. line: 1,
  589. col: 23,
  590. },
  591. },
  592. },
  593. },
  594. {
  595. input: "authorDate >= \"2017-07-02 00:00:00\" AND (authorName = \"John Smith\" OR committerName = \"John Smith\")",
  596. expectedTokens: []QueryToken{
  597. {
  598. tokenType: QtkIdentifier,
  599. value: "authorDate",
  600. startPos: QueryScannerPos{
  601. line: 1,
  602. col: 1,
  603. },
  604. endPos: QueryScannerPos{
  605. line: 1,
  606. col: 10,
  607. },
  608. },
  609. {
  610. tokenType: QtkWhiteSpace,
  611. value: " ",
  612. startPos: QueryScannerPos{
  613. line: 1,
  614. col: 11,
  615. },
  616. endPos: QueryScannerPos{
  617. line: 1,
  618. col: 11,
  619. },
  620. },
  621. {
  622. tokenType: QtkCmpGe,
  623. value: ">=",
  624. startPos: QueryScannerPos{
  625. line: 1,
  626. col: 12,
  627. },
  628. endPos: QueryScannerPos{
  629. line: 1,
  630. col: 13,
  631. },
  632. },
  633. {
  634. tokenType: QtkWhiteSpace,
  635. value: " ",
  636. startPos: QueryScannerPos{
  637. line: 1,
  638. col: 14,
  639. },
  640. endPos: QueryScannerPos{
  641. line: 1,
  642. col: 14,
  643. },
  644. },
  645. {
  646. tokenType: QtkString,
  647. value: "2017-07-02 00:00:00",
  648. startPos: QueryScannerPos{
  649. line: 1,
  650. col: 15,
  651. },
  652. endPos: QueryScannerPos{
  653. line: 1,
  654. col: 35,
  655. },
  656. },
  657. {
  658. tokenType: QtkWhiteSpace,
  659. value: " ",
  660. startPos: QueryScannerPos{
  661. line: 1,
  662. col: 36,
  663. },
  664. endPos: QueryScannerPos{
  665. line: 1,
  666. col: 36,
  667. },
  668. },
  669. {
  670. tokenType: QtkAnd,
  671. value: "AND",
  672. startPos: QueryScannerPos{
  673. line: 1,
  674. col: 37,
  675. },
  676. endPos: QueryScannerPos{
  677. line: 1,
  678. col: 39,
  679. },
  680. },
  681. {
  682. tokenType: QtkWhiteSpace,
  683. value: " ",
  684. startPos: QueryScannerPos{
  685. line: 1,
  686. col: 40,
  687. },
  688. endPos: QueryScannerPos{
  689. line: 1,
  690. col: 40,
  691. },
  692. },
  693. {
  694. tokenType: QtkLparen,
  695. value: "(",
  696. startPos: QueryScannerPos{
  697. line: 1,
  698. col: 41,
  699. },
  700. endPos: QueryScannerPos{
  701. line: 1,
  702. col: 41,
  703. },
  704. },
  705. {
  706. tokenType: QtkIdentifier,
  707. value: "authorName",
  708. startPos: QueryScannerPos{
  709. line: 1,
  710. col: 42,
  711. },
  712. endPos: QueryScannerPos{
  713. line: 1,
  714. col: 51,
  715. },
  716. },
  717. {
  718. tokenType: QtkWhiteSpace,
  719. value: " ",
  720. startPos: QueryScannerPos{
  721. line: 1,
  722. col: 52,
  723. },
  724. endPos: QueryScannerPos{
  725. line: 1,
  726. col: 52,
  727. },
  728. },
  729. {
  730. tokenType: QtkCmpEq,
  731. value: "=",
  732. startPos: QueryScannerPos{
  733. line: 1,
  734. col: 53,
  735. },
  736. endPos: QueryScannerPos{
  737. line: 1,
  738. col: 53,
  739. },
  740. },
  741. {
  742. tokenType: QtkWhiteSpace,
  743. value: " ",
  744. startPos: QueryScannerPos{
  745. line: 1,
  746. col: 54,
  747. },
  748. endPos: QueryScannerPos{
  749. line: 1,
  750. col: 54,
  751. },
  752. },
  753. {
  754. tokenType: QtkString,
  755. value: "John Smith",
  756. startPos: QueryScannerPos{
  757. line: 1,
  758. col: 55,
  759. },
  760. endPos: QueryScannerPos{
  761. line: 1,
  762. col: 66,
  763. },
  764. },
  765. {
  766. tokenType: QtkWhiteSpace,
  767. value: " ",
  768. startPos: QueryScannerPos{
  769. line: 1,
  770. col: 67,
  771. },
  772. endPos: QueryScannerPos{
  773. line: 1,
  774. col: 67,
  775. },
  776. },
  777. {
  778. tokenType: QtkOr,
  779. value: "OR",
  780. startPos: QueryScannerPos{
  781. line: 1,
  782. col: 68,
  783. },
  784. endPos: QueryScannerPos{
  785. line: 1,
  786. col: 69,
  787. },
  788. },
  789. {
  790. tokenType: QtkWhiteSpace,
  791. value: " ",
  792. startPos: QueryScannerPos{
  793. line: 1,
  794. col: 70,
  795. },
  796. endPos: QueryScannerPos{
  797. line: 1,
  798. col: 70,
  799. },
  800. },
  801. {
  802. tokenType: QtkIdentifier,
  803. value: "committerName",
  804. startPos: QueryScannerPos{
  805. line: 1,
  806. col: 71,
  807. },
  808. endPos: QueryScannerPos{
  809. line: 1,
  810. col: 83,
  811. },
  812. },
  813. {
  814. tokenType: QtkWhiteSpace,
  815. value: " ",
  816. startPos: QueryScannerPos{
  817. line: 1,
  818. col: 84,
  819. },
  820. endPos: QueryScannerPos{
  821. line: 1,
  822. col: 84,
  823. },
  824. },
  825. {
  826. tokenType: QtkCmpEq,
  827. value: "=",
  828. startPos: QueryScannerPos{
  829. line: 1,
  830. col: 85,
  831. },
  832. endPos: QueryScannerPos{
  833. line: 1,
  834. col: 85,
  835. },
  836. },
  837. {
  838. tokenType: QtkWhiteSpace,
  839. value: " ",
  840. startPos: QueryScannerPos{
  841. line: 1,
  842. col: 86,
  843. },
  844. endPos: QueryScannerPos{
  845. line: 1,
  846. col: 86,
  847. },
  848. },
  849. {
  850. tokenType: QtkString,
  851. value: "John Smith",
  852. startPos: QueryScannerPos{
  853. line: 1,
  854. col: 87,
  855. },
  856. endPos: QueryScannerPos{
  857. line: 1,
  858. col: 98,
  859. },
  860. },
  861. {
  862. tokenType: QtkRparen,
  863. value: ")",
  864. startPos: QueryScannerPos{
  865. line: 1,
  866. col: 99,
  867. },
  868. endPos: QueryScannerPos{
  869. line: 1,
  870. col: 99,
  871. },
  872. },
  873. {
  874. tokenType: QtkEOF,
  875. startPos: QueryScannerPos{
  876. line: 1,
  877. col: 99,
  878. },
  879. endPos: QueryScannerPos{
  880. line: 1,
  881. col: 99,
  882. },
  883. },
  884. },
  885. },
  886. {
  887. input: "authorName GLOB \"%John%\"",
  888. expectedTokens: []QueryToken{
  889. {
  890. tokenType: QtkIdentifier,
  891. value: "authorName",
  892. startPos: QueryScannerPos{
  893. line: 1,
  894. col: 1,
  895. },
  896. endPos: QueryScannerPos{
  897. line: 1,
  898. col: 10,
  899. },
  900. },
  901. {
  902. tokenType: QtkWhiteSpace,
  903. value: " ",
  904. startPos: QueryScannerPos{
  905. line: 1,
  906. col: 11,
  907. },
  908. endPos: QueryScannerPos{
  909. line: 1,
  910. col: 11,
  911. },
  912. },
  913. {
  914. tokenType: QtkCmpGlob,
  915. value: "GLOB",
  916. startPos: QueryScannerPos{
  917. line: 1,
  918. col: 12,
  919. },
  920. endPos: QueryScannerPos{
  921. line: 1,
  922. col: 15,
  923. },
  924. },
  925. {
  926. tokenType: QtkWhiteSpace,
  927. value: " ",
  928. startPos: QueryScannerPos{
  929. line: 1,
  930. col: 16,
  931. },
  932. endPos: QueryScannerPos{
  933. line: 1,
  934. col: 16,
  935. },
  936. },
  937. {
  938. tokenType: QtkString,
  939. value: "%John%",
  940. startPos: QueryScannerPos{
  941. line: 1,
  942. col: 17,
  943. },
  944. endPos: QueryScannerPos{
  945. line: 1,
  946. col: 24,
  947. },
  948. },
  949. {
  950. tokenType: QtkEOF,
  951. startPos: QueryScannerPos{
  952. line: 1,
  953. col: 24,
  954. },
  955. endPos: QueryScannerPos{
  956. line: 1,
  957. col: 24,
  958. },
  959. },
  960. },
  961. },
  962. }
  963. for _, multiTokenTest := range multiTokenTests {
  964. scanner := NewQueryScanner(strings.NewReader(multiTokenTest.input))
  965. for _, expectedToken := range multiTokenTest.expectedTokens {
  966. token, err := scanner.Scan()
  967. if err != nil {
  968. t.Errorf("Scan failed with error %v", err)
  969. } else if !token.Equal(&expectedToken) {
  970. t.Errorf("QueryToken does not match expected value. Expected %v, Actual %v", expectedToken, *token)
  971. }
  972. }
  973. }
  974. }