ContentCacheManagerTests.js 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. const Path = require('path')
  2. const crypto = require('crypto')
  3. const { Readable } = require('stream')
  4. const SandboxedModule = require('sandboxed-module')
  5. const sinon = require('sinon')
  6. const { expect } = require('chai')
  7. const MODULE_PATH = '../../../app/js/ContentCacheManager'
  8. class FakeFile {
  9. constructor() {
  10. this.closed = false
  11. this.contents = []
  12. }
  13. async write(blob) {
  14. this.contents.push(blob)
  15. return this
  16. }
  17. async close() {
  18. this.closed = true
  19. return this
  20. }
  21. toJSON() {
  22. return {
  23. contents: Buffer.concat(this.contents).toString(),
  24. closed: this.closed
  25. }
  26. }
  27. }
  28. function hash(blob) {
  29. const hash = crypto.createHash('sha256')
  30. hash.update(blob)
  31. return hash.digest('hex')
  32. }
  33. describe('ContentCacheManager', function () {
  34. let contentDir, pdfPath
  35. let ContentCacheManager, fs, files, Settings
  36. function load() {
  37. ContentCacheManager = SandboxedModule.require(MODULE_PATH, {
  38. requires: {
  39. fs,
  40. 'settings-sharelatex': Settings
  41. }
  42. })
  43. }
  44. let contentRanges, newContentRanges, reclaimed
  45. function run(filePath, done) {
  46. ContentCacheManager.update(contentDir, filePath, (err, ranges) => {
  47. if (err) return done(err)
  48. let newlyReclaimed
  49. ;[contentRanges, newContentRanges, newlyReclaimed] = ranges
  50. reclaimed += newlyReclaimed
  51. done()
  52. })
  53. }
  54. beforeEach(function () {
  55. reclaimed = 0
  56. contentDir =
  57. '/app/output/602cee6f6460fca0ba7921e6/content/1797a7f48f9-5abc1998509dea1f'
  58. pdfPath =
  59. '/app/output/602cee6f6460fca0ba7921e6/generated-files/1797a7f48ea-8ac6805139f43351/output.pdf'
  60. Settings = {
  61. pdfCachingMinChunkSize: 1024,
  62. enablePdfCachingDark: false
  63. }
  64. files = {}
  65. fs = {
  66. createReadStream: sinon.stub().returns(Readable.from([])),
  67. promises: {
  68. async writeFile(name, blob) {
  69. const file = new FakeFile()
  70. await file.write(Buffer.from(blob))
  71. await file.close()
  72. files[name] = file
  73. },
  74. async readFile(name) {
  75. if (!files[name]) {
  76. throw new Error()
  77. }
  78. return files[name].toJSON().contents
  79. },
  80. async open(name) {
  81. files[name] = new FakeFile()
  82. return files[name]
  83. },
  84. async stat(name) {
  85. if (!files[name]) {
  86. throw new Error()
  87. }
  88. },
  89. async rename(oldName, newName) {
  90. if (!files[oldName]) {
  91. throw new Error()
  92. }
  93. files[newName] = files[oldName]
  94. delete files[oldName]
  95. },
  96. async unlink(name) {
  97. if (!files[name]) {
  98. throw new Error()
  99. }
  100. delete files[name]
  101. }
  102. }
  103. }
  104. })
  105. describe('with a small minChunkSize', function () {
  106. beforeEach(function () {
  107. Settings.pdfCachingMinChunkSize = 1
  108. load()
  109. })
  110. describe('when the ranges are split across chunks', function () {
  111. const RANGE_1 = 'stream123endstream'
  112. const RANGE_2 = 'stream(||)endstream'
  113. const RANGE_3 = 'stream!$%/=endstream'
  114. const h1 = hash(RANGE_1)
  115. const h2 = hash(RANGE_2)
  116. const h3 = hash(RANGE_3)
  117. function runWithSplitStream(done) {
  118. fs.createReadStream
  119. .withArgs(pdfPath)
  120. .returns(
  121. Readable.from([
  122. Buffer.from('abcstr'),
  123. Buffer.from('eam123endstreamABC'),
  124. Buffer.from('str'),
  125. Buffer.from('eam(||'),
  126. Buffer.from(')end'),
  127. Buffer.from('stream-_~stream!$%/=endstream')
  128. ])
  129. )
  130. run(pdfPath, done)
  131. }
  132. beforeEach(function (done) {
  133. runWithSplitStream(done)
  134. })
  135. it('should produce three ranges', function () {
  136. expect(contentRanges).to.have.length(3)
  137. })
  138. it('should find the correct offsets', function () {
  139. expect(contentRanges).to.deep.equal([
  140. {
  141. start: 3,
  142. end: 21,
  143. hash: hash(RANGE_1)
  144. },
  145. {
  146. start: 24,
  147. end: 43,
  148. hash: hash(RANGE_2)
  149. },
  150. {
  151. start: 46,
  152. end: 66,
  153. hash: hash(RANGE_3)
  154. }
  155. ])
  156. })
  157. it('should store the contents', function () {
  158. expect(JSON.parse(JSON.stringify(files))).to.deep.equal({
  159. [Path.join(contentDir, h1)]: {
  160. contents: RANGE_1,
  161. closed: true
  162. },
  163. [Path.join(contentDir, h2)]: {
  164. contents: RANGE_2,
  165. closed: true
  166. },
  167. [Path.join(contentDir, h3)]: {
  168. contents: RANGE_3,
  169. closed: true
  170. },
  171. [Path.join(contentDir, '.state.v0.json')]: {
  172. contents: JSON.stringify({
  173. hashAge: [
  174. [h1, 0],
  175. [h2, 0],
  176. [h3, 0]
  177. ],
  178. hashSize: [
  179. [h1, 18],
  180. [h2, 19],
  181. [h3, 20]
  182. ]
  183. }),
  184. closed: true
  185. }
  186. })
  187. })
  188. it('should mark all ranges as new', function () {
  189. expect(contentRanges).to.deep.equal(newContentRanges)
  190. })
  191. describe('when re-running with one stream removed', function () {
  192. function runWithOneSplitStreamRemoved(done) {
  193. fs.createReadStream
  194. .withArgs(pdfPath)
  195. .returns(
  196. Readable.from([
  197. Buffer.from('abcstr'),
  198. Buffer.from('eam123endstreamABC'),
  199. Buffer.from('stream!$%/=endstream')
  200. ])
  201. )
  202. run(pdfPath, done)
  203. }
  204. beforeEach(function (done) {
  205. runWithOneSplitStreamRemoved(done)
  206. })
  207. it('should produce two ranges', function () {
  208. expect(contentRanges).to.have.length(2)
  209. })
  210. it('should find the correct offsets', function () {
  211. expect(contentRanges).to.deep.equal([
  212. {
  213. start: 3,
  214. end: 21,
  215. hash: hash(RANGE_1)
  216. },
  217. {
  218. start: 24,
  219. end: 44,
  220. hash: hash(RANGE_3)
  221. }
  222. ])
  223. })
  224. it('should update the age of the 2nd range', function () {
  225. expect(JSON.parse(JSON.stringify(files))).to.deep.equal({
  226. [Path.join(contentDir, h1)]: {
  227. contents: RANGE_1,
  228. closed: true
  229. },
  230. [Path.join(contentDir, h2)]: {
  231. contents: RANGE_2,
  232. closed: true
  233. },
  234. [Path.join(contentDir, h3)]: {
  235. contents: RANGE_3,
  236. closed: true
  237. },
  238. [Path.join(contentDir, '.state.v0.json')]: {
  239. contents: JSON.stringify({
  240. hashAge: [
  241. [h1, 0],
  242. [h2, 1],
  243. [h3, 0]
  244. ],
  245. hashSize: [
  246. [h1, 18],
  247. [h2, 19],
  248. [h3, 20]
  249. ]
  250. }),
  251. closed: true
  252. }
  253. })
  254. })
  255. it('should find no new ranges', function () {
  256. expect(newContentRanges).to.deep.equal([])
  257. })
  258. describe('when re-running 5 more times', function () {
  259. for (let i = 0; i < 5; i++) {
  260. beforeEach(function (done) {
  261. runWithOneSplitStreamRemoved(done)
  262. })
  263. }
  264. it('should still produce two ranges', function () {
  265. expect(contentRanges).to.have.length(2)
  266. })
  267. it('should still find the correct offsets', function () {
  268. expect(contentRanges).to.deep.equal([
  269. {
  270. start: 3,
  271. end: 21,
  272. hash: hash(RANGE_1)
  273. },
  274. {
  275. start: 24,
  276. end: 44,
  277. hash: hash(RANGE_3)
  278. }
  279. ])
  280. })
  281. it('should delete the 2nd range', function () {
  282. expect(JSON.parse(JSON.stringify(files))).to.deep.equal({
  283. [Path.join(contentDir, h1)]: {
  284. contents: RANGE_1,
  285. closed: true
  286. },
  287. [Path.join(contentDir, h3)]: {
  288. contents: RANGE_3,
  289. closed: true
  290. },
  291. [Path.join(contentDir, '.state.v0.json')]: {
  292. contents: JSON.stringify({
  293. hashAge: [
  294. [h1, 0],
  295. [h3, 0]
  296. ],
  297. hashSize: [
  298. [h1, 18],
  299. [h3, 20]
  300. ]
  301. }),
  302. closed: true
  303. }
  304. })
  305. })
  306. it('should find no new ranges', function () {
  307. expect(newContentRanges).to.deep.equal([])
  308. })
  309. it('should yield the reclaimed space', function () {
  310. expect(reclaimed).to.equal(RANGE_2.length)
  311. })
  312. })
  313. })
  314. })
  315. })
  316. })