SummarizedUpdatesManagerTests.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874
  1. import sinon from 'sinon'
  2. import { expect } from 'chai'
  3. import { strict as esmock } from 'esmock'
  4. const MODULE_PATH = '../../../../app/js/SummarizedUpdatesManager.js'
  5. // A sufficiently large amount of time to make the algorithm process updates
  6. // separately
  7. const LATER = 1000000
  8. describe('SummarizedUpdatesManager', function () {
  9. beforeEach(async function () {
  10. this.historyId = 'history-id-123'
  11. this.projectId = 'project-id-123'
  12. this.firstChunk = { chunk: { startVersion: 0 } }
  13. this.secondChunk = { chunk: { startVersion: 1 } }
  14. this.ChunkTranslator = {
  15. convertToSummarizedUpdates: sinon.stub(),
  16. }
  17. this.HistoryApiManager = {
  18. shouldUseProjectHistory: sinon.stub().yields(null, true),
  19. }
  20. this.HistoryStoreManager = {
  21. getMostRecentChunk: sinon.stub(),
  22. getChunkAtVersion: sinon.stub(),
  23. }
  24. this.UpdatesProcessor = {
  25. processUpdatesForProject: sinon.stub().withArgs(this.projectId).yields(),
  26. }
  27. this.WebApiManager = {
  28. getHistoryId: sinon.stub().yields(null, this.historyId),
  29. }
  30. this.LabelsManager = {
  31. getLabels: sinon.stub().yields(null, []),
  32. }
  33. this.SummarizedUpdatesManager = await esmock(MODULE_PATH, {
  34. '../../../../app/js/ChunkTranslator.js': this.ChunkTranslator,
  35. '../../../../app/js/HistoryApiManager.js': this.HistoryApiManager,
  36. '../../../../app/js/HistoryStoreManager.js': this.HistoryStoreManager,
  37. '../../../../app/js/UpdatesProcessor.js': this.UpdatesProcessor,
  38. '../../../../app/js/WebApiManager.js': this.WebApiManager,
  39. '../../../../app/js/LabelsManager.js': this.LabelsManager,
  40. })
  41. this.callback = sinon.stub()
  42. })
  43. describe('getSummarizedProjectUpdates', function () {
  44. describe('chunk management', function () {
  45. describe('when there is a single empty chunk', function () {
  46. setupChunks([[]])
  47. expectSummaries('returns an empty list of updates', {}, [])
  48. })
  49. describe('when there is a single non-empty chunk', function () {
  50. setupChunks([[makeUpdate()]])
  51. expectSummaries('returns summarized updates', {}, [makeSummary()])
  52. })
  53. describe('when there are multiple chunks', function () {
  54. setupChunks([
  55. [makeUpdate({ startTs: 0, v: 1 })],
  56. [makeUpdate({ startTs: LATER, v: 2 })],
  57. ])
  58. describe('and requesting many summaries', function () {
  59. expectSummaries('returns many update summaries', {}, [
  60. makeSummary({ startTs: LATER, fromV: 2 }),
  61. makeSummary({ startTs: 0, fromV: 1 }),
  62. ])
  63. })
  64. describe('and requesting a single summary', function () {
  65. expectSummaries('returns a single update summary', { min_count: 1 }, [
  66. makeSummary({ startTs: LATER, fromV: 2 }),
  67. ])
  68. })
  69. })
  70. describe('when there are too many chunks', function () {
  71. // Set up 10 chunks
  72. const chunks = []
  73. for (let v = 1; v <= 10; v++) {
  74. chunks.push([
  75. makeUpdate({
  76. startTs: v * 100, // values: 100 - 1000
  77. v, // values: 1 - 10
  78. }),
  79. ])
  80. }
  81. setupChunks(chunks)
  82. // Verify that we stop summarizing after 5 chunks
  83. expectSummaries('summarizes the 5 latest chunks', {}, [
  84. makeSummary({ startTs: 600, endTs: 1010, fromV: 6, toV: 11 }),
  85. ])
  86. })
  87. describe('when requesting updates before a specific version', function () {
  88. // Chunk 1 contains 5 updates that were made close to each other and 5
  89. // other updates that were made later.
  90. const chunk1 = []
  91. for (let v = 1; v <= 5; v++) {
  92. chunk1.push(
  93. makeUpdate({
  94. startTs: v * 100, // values: 100 - 500
  95. v, // values: 1 - 5
  96. })
  97. )
  98. }
  99. for (let v = 6; v <= 10; v++) {
  100. chunk1.push(
  101. makeUpdate({
  102. startTs: LATER + v * 100, // values: 1000600 - 1001000
  103. v, // values: 6 - 10
  104. })
  105. )
  106. }
  107. // Chunk 2 contains 5 updates that were made close to the latest updates in
  108. // chunk 1.
  109. const chunk2 = []
  110. for (let v = 11; v <= 15; v++) {
  111. chunk2.push(
  112. makeUpdate({
  113. startTs: LATER + v * 100, // values: 1001100 - 1001500
  114. v, // values: 11 - 15
  115. })
  116. )
  117. }
  118. setupChunks([chunk1, chunk2])
  119. expectSummaries(
  120. 'summarizes the updates in a single chunk if the chunk is sufficient',
  121. { before: 14, min_count: 1 },
  122. [
  123. makeSummary({
  124. startTs: LATER + 1100,
  125. endTs: LATER + 1310,
  126. fromV: 11,
  127. toV: 14,
  128. }),
  129. ]
  130. )
  131. expectSummaries(
  132. 'summarizes the updates in many chunks otherwise',
  133. { before: 14, min_count: 2 },
  134. [
  135. makeSummary({
  136. startTs: LATER + 600,
  137. endTs: LATER + 1310,
  138. fromV: 6,
  139. toV: 14,
  140. }),
  141. makeSummary({
  142. startTs: 100,
  143. endTs: 510,
  144. fromV: 1,
  145. toV: 6,
  146. }),
  147. ]
  148. )
  149. })
  150. })
  151. describe('update summarization', function () {
  152. describe('updates that are close in time', function () {
  153. setupChunks([
  154. [
  155. makeUpdate({
  156. users: ['user1'],
  157. startTs: 0,
  158. v: 4,
  159. }),
  160. makeUpdate({
  161. users: ['user2'],
  162. startTs: 20,
  163. v: 5,
  164. }),
  165. ],
  166. ])
  167. expectSummaries('should merge the updates', {}, [
  168. makeSummary({
  169. users: ['user1', 'user2'],
  170. startTs: 0,
  171. endTs: 30,
  172. fromV: 4,
  173. toV: 6,
  174. }),
  175. ])
  176. })
  177. describe('updates that are far apart in time', function () {
  178. setupChunks([
  179. [
  180. makeUpdate({
  181. users: ['user1'],
  182. startTs: 100,
  183. v: 4,
  184. }),
  185. makeUpdate({
  186. users: ['user2'],
  187. startTs: LATER,
  188. v: 5,
  189. }),
  190. ],
  191. ])
  192. expectSummaries('should not merge the updates', {}, [
  193. makeSummary({
  194. users: ['user2'],
  195. startTs: LATER,
  196. endTs: LATER + 10,
  197. fromV: 5,
  198. toV: 6,
  199. }),
  200. makeSummary({
  201. users: ['user1'],
  202. startTs: 100,
  203. endTs: 110,
  204. fromV: 4,
  205. toV: 5,
  206. }),
  207. ])
  208. })
  209. describe('mergeable updates in different chunks', function () {
  210. setupChunks([
  211. [
  212. makeUpdate({
  213. pathnames: ['main.tex'],
  214. users: ['user1'],
  215. startTs: 10,
  216. v: 4,
  217. }),
  218. makeUpdate({
  219. pathnames: ['main.tex'],
  220. users: ['user2'],
  221. startTs: 30,
  222. v: 5,
  223. }),
  224. ],
  225. [
  226. makeUpdate({
  227. pathnames: ['chapter.tex'],
  228. users: ['user1'],
  229. startTs: 40,
  230. v: 6,
  231. }),
  232. makeUpdate({
  233. pathnames: ['chapter.tex'],
  234. users: ['user1'],
  235. startTs: 50,
  236. v: 7,
  237. }),
  238. ],
  239. ])
  240. expectSummaries('should merge the updates', {}, [
  241. makeSummary({
  242. pathnames: ['main.tex', 'chapter.tex'],
  243. users: ['user1', 'user2'],
  244. startTs: 10,
  245. endTs: 60,
  246. fromV: 4,
  247. toV: 8,
  248. }),
  249. ])
  250. })
  251. describe('null user values after regular users', function () {
  252. setupChunks([
  253. [
  254. makeUpdate({
  255. users: ['user1'],
  256. startTs: 0,
  257. v: 4,
  258. }),
  259. makeUpdate({
  260. users: [null],
  261. startTs: 20,
  262. v: 5,
  263. }),
  264. ],
  265. ])
  266. expectSummaries('should include the null values', {}, [
  267. makeSummary({
  268. users: [null, 'user1'],
  269. startTs: 0,
  270. endTs: 30,
  271. fromV: 4,
  272. toV: 6,
  273. }),
  274. ])
  275. })
  276. describe('null user values before regular users', function () {
  277. setupChunks([
  278. [
  279. makeUpdate({
  280. users: [null],
  281. startTs: 0,
  282. v: 4,
  283. }),
  284. makeUpdate({
  285. users: ['user1'],
  286. startTs: 20,
  287. v: 5,
  288. }),
  289. ],
  290. ])
  291. expectSummaries('should include the null values', {}, [
  292. makeSummary({
  293. users: [null, 'user1'],
  294. startTs: 0,
  295. endTs: 30,
  296. fromV: 4,
  297. toV: 6,
  298. }),
  299. ])
  300. })
  301. describe('multiple null user values', function () {
  302. setupChunks([
  303. [
  304. makeUpdate({
  305. users: ['user1'],
  306. startTs: 10,
  307. v: 4,
  308. }),
  309. makeUpdate({
  310. users: [null],
  311. startTs: 20,
  312. v: 5,
  313. }),
  314. makeUpdate({
  315. users: [null],
  316. startTs: 70,
  317. v: 6,
  318. }),
  319. ],
  320. ])
  321. expectSummaries('should merge the null values', {}, [
  322. makeSummary({
  323. users: [null, 'user1'],
  324. startTs: 10,
  325. endTs: 80,
  326. fromV: 4,
  327. toV: 7,
  328. }),
  329. ])
  330. })
  331. describe('multiple users', function () {
  332. setupChunks([
  333. [
  334. makeUpdate({
  335. users: ['user1'],
  336. startTs: 0,
  337. v: 4,
  338. }),
  339. makeUpdate({
  340. users: ['user2'],
  341. startTs: 20,
  342. v: 5,
  343. }),
  344. ],
  345. ])
  346. expectSummaries('should merge the users', {}, [
  347. makeSummary({
  348. users: ['user1', 'user2'],
  349. startTs: 0,
  350. endTs: 30,
  351. fromV: 4,
  352. toV: 6,
  353. }),
  354. ])
  355. })
  356. describe('duplicate updates with the same v1 user', function () {
  357. setupChunks([
  358. [
  359. makeUpdate({
  360. users: [{ id: 'user1' }],
  361. startTs: 0,
  362. v: 4,
  363. }),
  364. makeUpdate({
  365. users: [{ id: 'user1' }],
  366. startTs: 20,
  367. v: 5,
  368. }),
  369. ],
  370. ])
  371. expectSummaries('should deduplicate the users', {}, [
  372. makeSummary({
  373. users: [{ id: 'user1' }],
  374. startTs: 0,
  375. endTs: 30,
  376. fromV: 4,
  377. toV: 6,
  378. }),
  379. ])
  380. })
  381. describe('duplicate updates with the same v2 user', function () {
  382. setupChunks([
  383. [
  384. makeUpdate({
  385. users: ['user1'],
  386. startTs: 0,
  387. v: 4,
  388. }),
  389. makeUpdate({
  390. users: ['user1'],
  391. startTs: 20,
  392. v: 5,
  393. }),
  394. ],
  395. ])
  396. expectSummaries('should deduplicate the users', {}, [
  397. makeSummary({
  398. users: ['user1'],
  399. startTs: 0,
  400. endTs: 30,
  401. fromV: 4,
  402. toV: 6,
  403. }),
  404. ])
  405. })
  406. describe('mixed v1 and v2 users with the same id', function () {
  407. setupChunks([
  408. [
  409. makeUpdate({
  410. users: ['user1'],
  411. startTs: 0,
  412. v: 4,
  413. }),
  414. makeUpdate({
  415. users: [{ id: 'user1' }],
  416. startTs: 20,
  417. v: 5,
  418. }),
  419. ],
  420. ])
  421. expectSummaries('should deduplicate the users', {}, [
  422. makeSummary({
  423. users: [{ id: 'user1' }],
  424. startTs: 0,
  425. endTs: 30,
  426. fromV: 4,
  427. toV: 6,
  428. }),
  429. ])
  430. })
  431. describe('project ops in mergeable updates', function () {
  432. setupChunks([
  433. [
  434. makeUpdate({
  435. pathnames: [],
  436. projectOps: [
  437. { rename: { pathname: 'C.tex', newPathname: 'D.tex' } },
  438. ],
  439. users: ['user2'],
  440. startTs: 0,
  441. v: 4,
  442. }),
  443. makeUpdate({
  444. pathnames: [],
  445. projectOps: [
  446. { rename: { pathname: 'A.tex', newPathname: 'B.tex' } },
  447. ],
  448. users: ['user1'],
  449. startTs: 20,
  450. v: 5,
  451. }),
  452. ],
  453. ])
  454. expectSummaries('should merge project ops', {}, [
  455. makeSummary({
  456. pathnames: [],
  457. projectOps: [
  458. {
  459. atV: 5,
  460. rename: {
  461. pathname: 'A.tex',
  462. newPathname: 'B.tex',
  463. },
  464. },
  465. {
  466. atV: 4,
  467. rename: {
  468. pathname: 'C.tex',
  469. newPathname: 'D.tex',
  470. },
  471. },
  472. ],
  473. users: ['user1', 'user2'],
  474. startTs: 0,
  475. endTs: 30,
  476. fromV: 4,
  477. toV: 6,
  478. }),
  479. ])
  480. })
  481. describe('mergable updates with a mix of project ops and doc ops', function () {
  482. setupChunks([
  483. [
  484. makeUpdate({
  485. pathnames: ['main.tex'],
  486. users: ['user1'],
  487. startTs: 0,
  488. v: 4,
  489. }),
  490. makeUpdate({
  491. pathnames: [],
  492. users: ['user2'],
  493. projectOps: [
  494. { rename: { pathname: 'A.tex', newPathname: 'B.tex' } },
  495. ],
  496. startTs: 20,
  497. v: 5,
  498. }),
  499. makeUpdate({
  500. pathnames: ['chapter.tex'],
  501. users: ['user2'],
  502. startTs: 40,
  503. v: 6,
  504. }),
  505. ],
  506. ])
  507. expectSummaries('should keep updates separate', {}, [
  508. makeSummary({
  509. pathnames: ['chapter.tex'],
  510. users: ['user2'],
  511. startTs: 40,
  512. fromV: 6,
  513. }),
  514. makeSummary({
  515. pathnames: [],
  516. users: ['user2'],
  517. projectOps: [
  518. { atV: 5, rename: { pathname: 'A.tex', newPathname: 'B.tex' } },
  519. ],
  520. startTs: 20,
  521. fromV: 5,
  522. }),
  523. makeSummary({
  524. pathnames: ['main.tex'],
  525. users: ['user1'],
  526. startTs: 0,
  527. fromV: 4,
  528. }),
  529. ])
  530. })
  531. describe('label on an update', function () {
  532. const label = {
  533. id: 'mock-id',
  534. comment: 'an example comment',
  535. version: 5,
  536. }
  537. setupChunks([
  538. [
  539. makeUpdate({ startTs: 0, v: 3 }),
  540. makeUpdate({ startTs: 20, v: 4 }),
  541. makeUpdate({ startTs: 40, v: 5 }),
  542. makeUpdate({ startTs: 60, v: 6 }),
  543. ],
  544. ])
  545. setupLabels([label])
  546. expectSummaries('should split the updates at the label', {}, [
  547. makeSummary({ startTs: 40, endTs: 70, fromV: 5, toV: 7 }),
  548. makeSummary({
  549. startTs: 0,
  550. endTs: 30,
  551. fromV: 3,
  552. toV: 5,
  553. labels: [label],
  554. }),
  555. ])
  556. })
  557. describe('updates with origin', function () {
  558. setupChunks([
  559. [
  560. makeUpdate({ startTs: 0, v: 1 }),
  561. makeUpdate({ startTs: 10, v: 2 }),
  562. makeUpdate({
  563. startTs: 20,
  564. v: 3,
  565. origin: { kind: 'history-resync' },
  566. }),
  567. makeUpdate({
  568. startTs: 30,
  569. v: 4,
  570. origin: { kind: 'history-resync' },
  571. }),
  572. makeUpdate({ startTs: 40, v: 5 }),
  573. makeUpdate({ startTs: 50, v: 6 }),
  574. ],
  575. ])
  576. expectSummaries(
  577. 'should split the updates where the origin appears or disappears',
  578. {},
  579. [
  580. makeSummary({ startTs: 40, endTs: 60, fromV: 5, toV: 7 }),
  581. makeSummary({
  582. startTs: 20,
  583. endTs: 40,
  584. fromV: 3,
  585. toV: 5,
  586. origin: { kind: 'history-resync' },
  587. }),
  588. makeSummary({ startTs: 0, endTs: 20, fromV: 1, toV: 3 }),
  589. ]
  590. )
  591. })
  592. describe('updates with different origins', function () {
  593. setupChunks([
  594. [
  595. makeUpdate({ startTs: 0, v: 1, origin: { kind: 'origin-a' } }),
  596. makeUpdate({ startTs: 10, v: 2, origin: { kind: 'origin-a' } }),
  597. makeUpdate({ startTs: 20, v: 3, origin: { kind: 'origin-b' } }),
  598. makeUpdate({ startTs: 30, v: 4, origin: { kind: 'origin-b' } }),
  599. ],
  600. ])
  601. expectSummaries(
  602. 'should split the updates when the origin kind changes',
  603. {},
  604. [
  605. makeSummary({
  606. startTs: 20,
  607. endTs: 40,
  608. fromV: 3,
  609. toV: 5,
  610. origin: { kind: 'origin-b' },
  611. }),
  612. makeSummary({
  613. startTs: 0,
  614. endTs: 20,
  615. fromV: 1,
  616. toV: 3,
  617. origin: { kind: 'origin-a' },
  618. }),
  619. ]
  620. )
  621. })
  622. describe('empty updates', function () {
  623. setupChunks([
  624. [
  625. makeUpdate({ startTs: 0, v: 1, pathnames: ['main.tex'] }),
  626. makeUpdate({ startTs: 10, v: 2, pathnames: [] }),
  627. makeUpdate({ startTs: 20, v: 3, pathnames: ['main.tex'] }),
  628. makeUpdate({ startTs: 30, v: 4, pathnames: [] }),
  629. makeUpdate({ startTs: 40, v: 5, pathnames: [] }),
  630. ],
  631. [
  632. makeUpdate({ startTs: 50, v: 6, pathnames: [] }),
  633. makeUpdate({ startTs: LATER, v: 7, pathnames: [] }),
  634. makeUpdate({ startTs: LATER + 10, v: 8, pathnames: ['main.tex'] }),
  635. makeUpdate({ startTs: LATER + 20, v: 9, pathnames: ['main.tex'] }),
  636. makeUpdate({ startTs: LATER + 30, v: 10, pathnames: [] }),
  637. ],
  638. ])
  639. expectSummaries('should skip empty updates', {}, [
  640. makeSummary({
  641. startTs: LATER + 10,
  642. endTs: LATER + 30,
  643. fromV: 8,
  644. toV: 11,
  645. }),
  646. makeSummary({ startTs: 0, endTs: 30, fromV: 1, toV: 8 }),
  647. ])
  648. })
  649. describe('history resync updates', function () {
  650. setupChunks([
  651. [
  652. makeUpdate({
  653. startTs: 0,
  654. v: 1,
  655. origin: { kind: 'history-resync' },
  656. projectOps: [{ add: { pathname: 'file1.tex' } }],
  657. pathnames: [],
  658. }),
  659. makeUpdate({
  660. startTs: 20,
  661. v: 2,
  662. origin: { kind: 'history-resync' },
  663. projectOps: [
  664. { add: { pathname: 'file2.tex' } },
  665. { add: { pathname: 'file3.tex' } },
  666. ],
  667. pathnames: [],
  668. }),
  669. makeUpdate({
  670. startTs: 40,
  671. v: 3,
  672. origin: { kind: 'history-resync' },
  673. projectOps: [{ add: { pathname: 'file4.tex' } }],
  674. pathnames: [],
  675. }),
  676. makeUpdate({
  677. startTs: 60,
  678. v: 4,
  679. origin: { kind: 'history-resync' },
  680. projectOps: [],
  681. pathnames: ['file1.tex', 'file2.tex', 'file5.tex'],
  682. }),
  683. makeUpdate({
  684. startTs: 80,
  685. v: 5,
  686. origin: { kind: 'history-resync' },
  687. projectOps: [],
  688. pathnames: ['file4.tex'],
  689. }),
  690. makeUpdate({ startTs: 100, v: 6, pathnames: ['file1.tex'] }),
  691. ],
  692. ])
  693. expectSummaries('should merge creates and edits', {}, [
  694. makeSummary({
  695. startTs: 100,
  696. endTs: 110,
  697. fromV: 6,
  698. toV: 7,
  699. pathnames: ['file1.tex'],
  700. }),
  701. makeSummary({
  702. startTs: 0,
  703. endTs: 90,
  704. fromV: 1,
  705. toV: 6,
  706. origin: { kind: 'history-resync' },
  707. pathnames: ['file5.tex'],
  708. projectOps: [
  709. { add: { pathname: 'file4.tex' }, atV: 3 },
  710. { add: { pathname: 'file2.tex' }, atV: 2 },
  711. { add: { pathname: 'file3.tex' }, atV: 2 },
  712. { add: { pathname: 'file1.tex' }, atV: 1 },
  713. ],
  714. }),
  715. ])
  716. })
  717. })
  718. })
  719. })
  720. /**
  721. * Set up mocks as if the project had a number of chunks.
  722. *
  723. * Each parameter represents a chunk and the value of the parameter is the list
  724. * of updates in that chunk.
  725. */
  726. function setupChunks(updatesByChunk) {
  727. beforeEach('set up chunks', function () {
  728. let startVersion = 0
  729. for (let i = 0; i < updatesByChunk.length; i++) {
  730. const updates = updatesByChunk[i]
  731. const chunk = { chunk: { startVersion } }
  732. // Find the chunk by any update version
  733. for (const update of updates) {
  734. this.HistoryStoreManager.getChunkAtVersion
  735. .withArgs(this.projectId, this.historyId, update.v)
  736. .yields(null, chunk)
  737. startVersion = update.v
  738. }
  739. if (i === updatesByChunk.length - 1) {
  740. this.HistoryStoreManager.getMostRecentChunk
  741. .withArgs(this.projectId, this.historyId)
  742. .yields(null, chunk)
  743. }
  744. this.ChunkTranslator.convertToSummarizedUpdates
  745. .withArgs(chunk)
  746. .yields(null, updates)
  747. }
  748. })
  749. }
  750. function setupLabels(labels) {
  751. beforeEach('set up labels', function () {
  752. this.LabelsManager.getLabels.withArgs(this.projectId).yields(null, labels)
  753. })
  754. }
  755. function expectSummaries(description, options, expectedSummaries) {
  756. it(`${description}`, function (done) {
  757. this.SummarizedUpdatesManager.getSummarizedProjectUpdates(
  758. this.projectId,
  759. options,
  760. (err, summaries) => {
  761. if (err) {
  762. return done(err)
  763. }
  764. // The order of the users array is not significant
  765. for (const summary of summaries) {
  766. summary.meta.users.sort()
  767. }
  768. for (const summary of expectedSummaries) {
  769. summary.meta.users.sort()
  770. }
  771. expect(summaries).to.deep.equal(expectedSummaries)
  772. done()
  773. }
  774. )
  775. })
  776. }
  777. function makeUpdate(options = {}) {
  778. const {
  779. pathnames = ['main.tex'],
  780. users = ['user1'],
  781. projectOps = [],
  782. startTs = 0,
  783. endTs = startTs + 10,
  784. v = 1,
  785. origin,
  786. } = options
  787. const update = {
  788. pathnames,
  789. project_ops: projectOps,
  790. meta: { users, start_ts: startTs, end_ts: endTs },
  791. v,
  792. }
  793. if (origin) {
  794. update.meta.origin = origin
  795. }
  796. return update
  797. }
  798. function makeSummary(options = {}) {
  799. const {
  800. pathnames = ['main.tex'],
  801. users = ['user1'],
  802. startTs = 0,
  803. endTs = startTs + 10,
  804. fromV = 1,
  805. toV = fromV + 1,
  806. labels = [],
  807. projectOps = [],
  808. origin,
  809. } = options
  810. const summary = {
  811. pathnames: new Set(pathnames),
  812. meta: {
  813. users,
  814. start_ts: startTs,
  815. end_ts: endTs,
  816. },
  817. fromV,
  818. toV,
  819. labels,
  820. project_ops: projectOps,
  821. }
  822. if (origin) {
  823. summary.meta.origin = origin
  824. }
  825. return summary
  826. }