ExampleDocumentTests.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. import Client from './helpers/Client.js'
  2. import fetch from 'node-fetch'
  3. import Stream from 'node:stream'
  4. import fs from 'node:fs'
  5. import fsPromises from 'node:fs/promises'
  6. import ChildProcess from 'node:child_process'
  7. import { promisify } from 'node:util'
  8. import ClsiApp from './helpers/ClsiApp.js'
  9. import Path from 'node:path'
  10. import process from 'node:process'
  11. import ResourceWriter from '../../../app/js/ResourceWriter.js'
  12. import { expect } from 'chai'
  13. const fixturePath = path => {
  14. if (path.slice(0, 3) === 'tmp') {
  15. return '/tmp/clsi_acceptance_tests' + path.slice(3)
  16. }
  17. return Path.join(import.meta.dirname, '../fixtures/', path)
  18. }
  19. const pipeline = promisify(Stream.pipeline)
  20. console.log(
  21. process.pid,
  22. process.ppid,
  23. process.getuid(),
  24. process.getgroups(),
  25. 'PID'
  26. )
  27. const MOCHA_LATEX_TIMEOUT = 60 * 1000
  28. const convertToPng = function (pdfPath, pngPath) {
  29. return new Promise((resolve, reject) => {
  30. const command = `convert ${fixturePath(pdfPath)} ${fixturePath(pngPath)}`
  31. console.log('COMMAND')
  32. console.log(command)
  33. const convert = ChildProcess.exec(command)
  34. convert.stdout.on('data', chunk => console.log('STDOUT', chunk.toString()))
  35. convert.stderr.on('data', chunk => console.log('STDERR', chunk.toString()))
  36. convert.on('exit', () => resolve())
  37. convert.on('error', error => reject(error))
  38. })
  39. }
  40. const compare = function (originalPath, generatedPath) {
  41. return new Promise((resolve, reject) => {
  42. const diffFile = `${fixturePath(generatedPath)}-diff.png`
  43. const proc = ChildProcess.exec(
  44. `compare -metric mae ${fixturePath(originalPath)} ${fixturePath(
  45. generatedPath
  46. )} ${diffFile}`
  47. )
  48. let stderr = ''
  49. proc.stderr.on('data', chunk => (stderr += chunk))
  50. proc.on('exit', () => {
  51. if (stderr.trim() === '0 (0)') {
  52. // remove output diff if test matches expected image
  53. fs.unlink(diffFile, err => {
  54. if (err) {
  55. reject(err)
  56. }
  57. })
  58. resolve(true)
  59. } else {
  60. console.log('compare result', stderr)
  61. resolve(false)
  62. }
  63. })
  64. })
  65. }
  66. const checkPdfInfo = function (pdfPath) {
  67. return new Promise((resolve, reject) => {
  68. const proc = ChildProcess.exec(`pdfinfo ${fixturePath(pdfPath)}`)
  69. let stdout = ''
  70. proc.stdout.on('data', chunk => (stdout += chunk))
  71. proc.stderr.on('data', chunk => console.log('STDERR', chunk.toString()))
  72. proc.on('exit', () => {
  73. if (stdout.match(/Optimized:\s+yes/)) {
  74. resolve(true)
  75. } else {
  76. resolve(false)
  77. }
  78. })
  79. proc.on('error', error => reject(error))
  80. })
  81. }
  82. const compareMultiplePages = async function (projectId) {
  83. async function compareNext(pageNo) {
  84. const path = `tmp/${projectId}-source-${pageNo}.png`
  85. try {
  86. await fsPromises.stat(fixturePath(path))
  87. } catch (error) {
  88. return
  89. }
  90. const same = await compare(
  91. `tmp/${projectId}-source-${pageNo}.png`,
  92. `tmp/${projectId}-generated-${pageNo}.png`
  93. )
  94. same.should.equal(true)
  95. await compareNext(pageNo + 1)
  96. }
  97. await compareNext(0)
  98. }
  99. const comparePdf = async function (projectId, exampleDir) {
  100. console.log('CONVERT')
  101. console.log(`tmp/${projectId}.pdf`, `tmp/${projectId}-generated.png`)
  102. await convertToPng(`tmp/${projectId}.pdf`, `tmp/${projectId}-generated.png`)
  103. await convertToPng(
  104. `examples/${exampleDir}/output.pdf`,
  105. `tmp/${projectId}-source.png`
  106. )
  107. try {
  108. await fsPromises.stat(fixturePath(`tmp/${projectId}-source-0.png`))
  109. await compareMultiplePages(projectId)
  110. } catch (error) {
  111. const same = await compare(
  112. `tmp/${projectId}-source.png`,
  113. `tmp/${projectId}-generated.png`
  114. )
  115. same.should.equal(true)
  116. }
  117. }
  118. const downloadAndComparePdf = async function (projectId, exampleDir, url) {
  119. const res = await fetch(url)
  120. if (!res.ok) {
  121. throw new Error('non success response: ' + res.statusText)
  122. }
  123. const dest = fs.createWriteStream(fixturePath(`tmp/${projectId}.pdf`))
  124. await pipeline(res.body, dest)
  125. const optimised = await checkPdfInfo(`tmp/${projectId}.pdf`)
  126. optimised.should.equal(true)
  127. await comparePdf(projectId, exampleDir)
  128. }
  129. describe('Example Documents', function () {
  130. Client.runFakeFilestoreService(fixturePath('examples'))
  131. before(async function () {
  132. await ClsiApp.ensureRunning()
  133. })
  134. before(async function () {
  135. await fsPromises.rm(fixturePath('tmp'), { force: true, recursive: true })
  136. })
  137. before(async function () {
  138. await fsPromises.mkdir(fixturePath('tmp'))
  139. })
  140. after(async function () {
  141. await fsPromises.rm(fixturePath('tmp'), { force: true, recursive: true })
  142. })
  143. return fs.readdirSync(fixturePath('examples')).map(exampleDir =>
  144. (exampleDir =>
  145. describe(exampleDir, function () {
  146. before(function () {
  147. this.project_id = Client.randomId()
  148. this.outputFiles = []
  149. // Allow each test to provide a configuration file
  150. const checksJsonPath = fixturePath(
  151. Path.join('examples', exampleDir, 'checks.json')
  152. )
  153. this.checks = {}
  154. const stats = fs.statSync(checksJsonPath, { throwIfNoEntry: false })
  155. if (stats && stats.isFile()) {
  156. const rawChecks = fs.readFileSync(checksJsonPath, 'utf8')
  157. try {
  158. this.checks = JSON.parse(rawChecks)
  159. } catch (err) {
  160. throw new Error(
  161. `Failed to parse checks.json for example "${exampleDir}" at path "${checksJsonPath}": ${err.message}`
  162. )
  163. }
  164. }
  165. })
  166. it('should generate the correct pdf and output files', async function () {
  167. this.timeout(MOCHA_LATEX_TIMEOUT)
  168. const body = await Client.compileDirectory(
  169. this.project_id,
  170. fixturePath('examples'),
  171. exampleDir
  172. )
  173. if (body?.compile?.status === 'failure') {
  174. throw new Error('Compile failed')
  175. }
  176. const pdf = Client.getOutputFile(body, 'pdf')
  177. await downloadAndComparePdf(this.project_id, exampleDir, pdf.url)
  178. // pass the output files on to subsequent tests
  179. this.outputFiles = body.compile.outputFiles
  180. if (this.checks.mustNotDeleteRegex) {
  181. const mustNotDeleteRegex = new RegExp(
  182. this.checks.mustNotDeleteRegex
  183. )
  184. // On subsequent compiles the isExtraneousFile method is used
  185. // to remove unwanted files - this check ensures that we don't
  186. // remove any files that should be kept (e.g. cache files)
  187. const filesToBeRemoved = this.outputFiles.filter(file =>
  188. ResourceWriter.isExtraneousFile(file.path)
  189. )
  190. for (const file of filesToBeRemoved) {
  191. expect(
  192. file.path,
  193. 'should not remove any files that must be cached'
  194. ).to.not.match(mustNotDeleteRegex)
  195. }
  196. }
  197. })
  198. it('should generate the correct pdf on the second run as well', async function () {
  199. this.timeout(MOCHA_LATEX_TIMEOUT)
  200. const body = await Client.compileDirectory(
  201. this.project_id,
  202. fixturePath('examples'),
  203. exampleDir
  204. )
  205. if (body?.compile?.status === 'failure') {
  206. throw new Error('Compile failed')
  207. }
  208. const pdf = Client.getOutputFile(body, 'pdf')
  209. await downloadAndComparePdf(this.project_id, exampleDir, pdf.url)
  210. })
  211. }))(exampleDir)
  212. )
  213. })