reference-indexer.spec.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. import { ReferenceIndexer } from '@/features/ide-react/references/reference-indexer'
  2. import { generateMD5Hash } from '@/shared/utils/md5'
  3. import sinon from 'sinon'
  4. const entry1 = `@article{sample2023,
  5. author = {John Doe},
  6. title = {Sample Title},
  7. journal = {Sample Journal},
  8. year = {2023},
  9. date = {2023-01-01}
  10. }`
  11. const entry2 = `@book{example2022,
  12. author = {Jane Smith},
  13. title = {Example Book},
  14. journal = {Example Journal},
  15. year = {2022}
  16. date = {2022-05-15}
  17. }`
  18. const entry3 = `@article{sample2024,
  19. author = {John Doe},
  20. title = {Sample Title},
  21. journal = {Sample Journal},
  22. year = {2024},
  23. date = {2024-01-01}
  24. }`
  25. const entry4 = `@book{example2025,
  26. author = {Jane Smith},
  27. title = {Example Book},
  28. journal = {Example Journal},
  29. year = {2025}
  30. date = {2025-05-15}
  31. }`
  32. const snapshotWithData = ({
  33. docs,
  34. files,
  35. }: {
  36. docs?: Record<string, string>
  37. files?: Record<string, string>
  38. }) => {
  39. return {
  40. getDocPaths: sinon.spy(() => Object.keys(docs ?? {})),
  41. getDocContents: sinon.spy(path => (docs ? (docs[path] ?? null) : null)),
  42. getBinaryFilePathsWithHash: sinon.spy(() => {
  43. return Object.entries(files ?? {}).map(([path, content]) => ({
  44. path,
  45. hash: generateMD5Hash(content),
  46. size: content.length,
  47. }))
  48. }),
  49. getBinaryFileContents: sinon.spy(async path =>
  50. files ? (files[path] ?? null) : null
  51. ),
  52. }
  53. }
  54. const IGNORED_SIGNAL = new AbortController().signal
  55. describe('ReferenceIndexer', function () {
  56. it('it should index bib docs', async function () {
  57. const referencer = new ReferenceIndexer()
  58. const snapshot = snapshotWithData({
  59. docs: {
  60. 'refs.bib': entry1,
  61. 'refs2.bib': entry2,
  62. 'other.tex': 'Not a bib file',
  63. },
  64. })
  65. const result = await referencer.updateFromSnapshot(snapshot, {
  66. signal: IGNORED_SIGNAL,
  67. })
  68. expect(snapshot.getDocPaths).to.have.been.calledOnce
  69. expect(snapshot.getDocContents).to.have.been.calledTwice
  70. expect(snapshot.getDocContents).to.have.been.calledWith('refs.bib')
  71. expect(snapshot.getDocContents).to.have.been.calledWith('refs2.bib')
  72. expect(snapshot.getDocContents).to.not.have.been.calledWith('other.tex')
  73. expect(snapshot.getBinaryFileContents).to.not.have.been.called
  74. expect(result).to.deep.equal(new Set(['sample2023', 'example2022']))
  75. })
  76. it('it should index bib binary files', async function () {
  77. const referencer = new ReferenceIndexer()
  78. const snapshot = snapshotWithData({
  79. files: {
  80. 'refs.bib': entry1,
  81. 'refs2.bib': entry2,
  82. 'image.png': 'Not a bib file',
  83. },
  84. })
  85. const result = await referencer.updateFromSnapshot(snapshot, {
  86. signal: IGNORED_SIGNAL,
  87. })
  88. expect(snapshot.getDocPaths).to.have.been.calledOnce
  89. expect(snapshot.getDocContents).to.not.have.been.called
  90. expect(snapshot.getBinaryFilePathsWithHash).to.have.been.calledOnce
  91. expect(snapshot.getBinaryFileContents).to.have.been.calledTwice
  92. expect(snapshot.getBinaryFileContents).to.have.been.calledWith('refs.bib')
  93. expect(snapshot.getBinaryFileContents).to.have.been.calledWith('refs2.bib')
  94. expect(snapshot.getBinaryFileContents).to.not.have.been.calledWith(
  95. 'image.png'
  96. )
  97. expect(result).to.deep.equal(new Set(['sample2023', 'example2022']))
  98. })
  99. it('it should index both bib docs and binary files', async function () {
  100. const referencer = new ReferenceIndexer()
  101. const snapshot = snapshotWithData({
  102. docs: {
  103. 'refs.bib': entry1,
  104. 'other.tex': 'Not a bib file',
  105. },
  106. files: {
  107. 'refs2.bib': entry2,
  108. 'image.png': 'Not a bib file',
  109. },
  110. })
  111. const result = await referencer.updateFromSnapshot(snapshot, {
  112. signal: IGNORED_SIGNAL,
  113. })
  114. expect(snapshot.getDocPaths).to.have.been.calledOnce
  115. expect(snapshot.getDocContents).to.have.been.calledOnce
  116. expect(snapshot.getDocContents).to.have.been.calledWith('refs.bib')
  117. expect(snapshot.getDocContents).to.not.have.been.calledWith('other.tex')
  118. expect(snapshot.getBinaryFilePathsWithHash).to.have.been.calledOnce
  119. expect(snapshot.getBinaryFileContents).to.have.been.calledOnce
  120. expect(snapshot.getBinaryFileContents).to.have.been.calledWith('refs2.bib')
  121. expect(snapshot.getBinaryFileContents).to.not.have.been.calledWith(
  122. 'image.png'
  123. )
  124. expect(result).to.deep.equal(new Set(['sample2023', 'example2022']))
  125. })
  126. it('should not fetch binary files if unchanged', async function () {
  127. const referencer = new ReferenceIndexer()
  128. const initialSnapshot = snapshotWithData({
  129. files: {
  130. 'refs.bib': entry1,
  131. },
  132. })
  133. const initialResult = await referencer.updateFromSnapshot(initialSnapshot, {
  134. signal: IGNORED_SIGNAL,
  135. })
  136. expect(initialSnapshot.getDocPaths).to.have.been.calledOnce
  137. expect(initialSnapshot.getBinaryFilePathsWithHash).to.have.been.calledOnce
  138. expect(initialSnapshot.getBinaryFileContents).to.have.been.calledOnceWith(
  139. 'refs.bib'
  140. )
  141. expect(initialResult).to.deep.equal(new Set(['sample2023']))
  142. // Second snapshot with same files, should not fetch contents again
  143. const secondSnapshot = snapshotWithData({
  144. files: {
  145. 'refs.bib': entry1,
  146. },
  147. })
  148. const secondResult = await referencer.updateFromSnapshot(secondSnapshot, {
  149. signal: IGNORED_SIGNAL,
  150. })
  151. expect(secondSnapshot.getDocPaths).to.have.been.calledOnce
  152. expect(secondSnapshot.getBinaryFilePathsWithHash).to.have.been.calledOnce
  153. expect(secondSnapshot.getBinaryFileContents).to.not.have.been.called
  154. expect(secondResult).to.deep.equal(new Set(['sample2023']))
  155. })
  156. it('should fetch changed binary file', async function () {
  157. const referencer = new ReferenceIndexer()
  158. const initialSnapshot = snapshotWithData({
  159. files: {
  160. 'refs.bib': entry1,
  161. },
  162. })
  163. const initialResult = await referencer.updateFromSnapshot(initialSnapshot, {
  164. signal: IGNORED_SIGNAL,
  165. })
  166. expect(initialSnapshot.getDocPaths).to.have.been.calledOnce
  167. expect(initialSnapshot.getBinaryFilePathsWithHash).to.have.been.calledOnce
  168. expect(initialSnapshot.getBinaryFileContents).to.have.been.calledOnceWith(
  169. 'refs.bib'
  170. )
  171. expect(initialResult).to.deep.equal(new Set(['sample2023']))
  172. // Second snapshot with a different file, should fetch contents again
  173. const secondSnapshot = snapshotWithData({
  174. files: {
  175. 'refs.bib': entry2,
  176. },
  177. })
  178. const secondResult = await referencer.updateFromSnapshot(secondSnapshot, {
  179. signal: IGNORED_SIGNAL,
  180. })
  181. expect(secondSnapshot.getDocPaths).to.have.been.calledOnce
  182. expect(secondSnapshot.getBinaryFilePathsWithHash).to.have.been.calledOnce
  183. expect(initialSnapshot.getBinaryFileContents).to.have.been.calledOnceWith(
  184. 'refs.bib'
  185. )
  186. expect(secondResult).to.deep.equal(new Set(['example2022']))
  187. })
  188. it('should update changed doc', async function () {
  189. const referencer = new ReferenceIndexer()
  190. const initialSnapshot = snapshotWithData({
  191. docs: {
  192. 'refs.bib': entry1,
  193. },
  194. })
  195. const initialResult = await referencer.updateFromSnapshot(initialSnapshot, {
  196. signal: IGNORED_SIGNAL,
  197. })
  198. expect(initialResult).to.deep.equal(new Set(['sample2023']))
  199. const secondSnapshot = snapshotWithData({
  200. docs: {
  201. 'refs.bib': entry2,
  202. },
  203. })
  204. const secondResult = await referencer.updateFromSnapshot(secondSnapshot, {
  205. signal: IGNORED_SIGNAL,
  206. })
  207. expect(secondResult).to.deep.equal(new Set(['example2022']))
  208. })
  209. it('should notice deleted files', async function () {
  210. const referencer = new ReferenceIndexer()
  211. const initialSnapshot = snapshotWithData({
  212. files: {
  213. 'refs.bib': entry1,
  214. 'refs2.bib': entry2,
  215. },
  216. })
  217. const initialResult = await referencer.updateFromSnapshot(initialSnapshot, {
  218. signal: IGNORED_SIGNAL,
  219. })
  220. expect(initialSnapshot.getDocPaths).to.have.been.calledOnce
  221. expect(initialSnapshot.getBinaryFilePathsWithHash).to.have.been.calledOnce
  222. expect(initialSnapshot.getBinaryFileContents).to.have.been.calledTwice
  223. expect(initialResult).to.deep.equal(new Set(['sample2023', 'example2022']))
  224. // Second snapshot with one file removed, should update index
  225. const secondSnapshot = snapshotWithData({
  226. files: {
  227. 'refs.bib': entry1,
  228. },
  229. })
  230. const secondResult = await referencer.updateFromSnapshot(secondSnapshot, {
  231. signal: IGNORED_SIGNAL,
  232. })
  233. expect(secondSnapshot.getDocPaths).to.have.been.calledOnce
  234. expect(secondSnapshot.getBinaryFilePathsWithHash).to.have.been.calledOnce
  235. expect(secondSnapshot.getBinaryFileContents).to.not.have.been.called
  236. expect(secondResult).to.deep.equal(new Set(['sample2023']))
  237. })
  238. it('should notice deleted docs', async function () {
  239. const referencer = new ReferenceIndexer()
  240. const initialSnapshot = snapshotWithData({
  241. docs: {
  242. 'refs.bib': entry1,
  243. 'refs2.bib': entry2,
  244. },
  245. })
  246. const initialResult = await referencer.updateFromSnapshot(initialSnapshot, {
  247. signal: IGNORED_SIGNAL,
  248. })
  249. expect(initialSnapshot.getDocPaths).to.have.been.calledOnce
  250. expect(initialSnapshot.getDocContents).to.have.been.calledTwice
  251. expect(initialResult).to.deep.equal(new Set(['sample2023', 'example2022']))
  252. // Second snapshot with one doc removed, should update index
  253. const secondSnapshot = snapshotWithData({
  254. docs: {
  255. 'refs.bib': entry1,
  256. },
  257. })
  258. const secondResult = await referencer.updateFromSnapshot(secondSnapshot, {
  259. signal: IGNORED_SIGNAL,
  260. })
  261. expect(secondSnapshot.getDocPaths).to.have.been.calledOnce
  262. expect(secondSnapshot.getDocContents).to.have.been.calledOnce
  263. expect(secondResult).to.deep.equal(new Set(['sample2023']))
  264. })
  265. it('should abort when signalled', async function () {
  266. const referencer = new ReferenceIndexer()
  267. const snapshot = snapshotWithData({
  268. files: {
  269. 'refs.bib': entry1,
  270. 'refs2.bib': entry2,
  271. },
  272. })
  273. const controller = new AbortController()
  274. controller.abort()
  275. const result = await referencer.updateFromSnapshot(snapshot, {
  276. signal: controller.signal,
  277. })
  278. expect(result).to.deep.equal(new Set())
  279. })
  280. it('should respect data budget', async function () {
  281. async function testWithDataBudget(budget: number, keys: Set<string>) {
  282. const referencer = new ReferenceIndexer()
  283. const snapshot = snapshotWithData({
  284. docs: {
  285. 'a.bib': entry1, // 140 bytes
  286. 'b.bib': entry2, // 140 bytes
  287. 'c.bib': entry3, // 140 bytes
  288. 'd.bib': entry4, // 140 bytes
  289. },
  290. })
  291. const result = await referencer.updateFromSnapshot(snapshot, {
  292. signal: IGNORED_SIGNAL,
  293. dataLimit: budget,
  294. })
  295. expect(result).to.deep.equal(keys)
  296. }
  297. await testWithDataBudget(
  298. 1000,
  299. new Set(['sample2023', 'example2022', 'sample2024', 'example2025'])
  300. )
  301. await testWithDataBudget(300, new Set(['sample2023', 'example2022']))
  302. await testWithDataBudget(200, new Set(['sample2023']))
  303. await testWithDataBudget(100, new Set())
  304. })
  305. })