import Client from './helpers/Client.js' import fetch from 'node-fetch' import Stream from 'node:stream' import fs from 'node:fs' import fsPromises from 'node:fs/promises' import ChildProcess from 'node:child_process' import { promisify } from 'node:util' import ClsiApp from './helpers/ClsiApp.js' import Path from 'node:path' import process from 'node:process' import ResourceWriter from '../../../app/js/ResourceWriter.js' import { expect } from 'chai' const fixturePath = path => { if (path.slice(0, 3) === 'tmp') { return '/tmp/clsi_acceptance_tests' + path.slice(3) } return Path.join(import.meta.dirname, '../fixtures/', path) } const pipeline = promisify(Stream.pipeline) console.log( process.pid, process.ppid, process.getuid(), process.getgroups(), 'PID' ) const MOCHA_LATEX_TIMEOUT = 60 * 1000 const convertToPng = function (pdfPath, pngPath) { return new Promise((resolve, reject) => { const command = `convert ${fixturePath(pdfPath)} ${fixturePath(pngPath)}` console.log('COMMAND') console.log(command) const convert = ChildProcess.exec(command) convert.stdout.on('data', chunk => console.log('STDOUT', chunk.toString())) convert.stderr.on('data', chunk => console.log('STDERR', chunk.toString())) convert.on('exit', () => resolve()) convert.on('error', error => reject(error)) }) } const compare = function (originalPath, generatedPath) { return new Promise((resolve, reject) => { const diffFile = `${fixturePath(generatedPath)}-diff.png` const proc = ChildProcess.exec( `compare -metric mae ${fixturePath(originalPath)} ${fixturePath( generatedPath )} ${diffFile}` ) let stderr = '' proc.stderr.on('data', chunk => (stderr += chunk)) proc.on('exit', () => { if (stderr.trim() === '0 (0)') { // remove output diff if test matches expected image fs.unlink(diffFile, err => { if (err) { reject(err) } }) resolve(true) } else { console.log('compare result', stderr) resolve(false) } }) }) } const checkPdfInfo = function (pdfPath) { return new Promise((resolve, reject) => { const proc = ChildProcess.exec(`pdfinfo ${fixturePath(pdfPath)}`) let stdout = '' proc.stdout.on('data', chunk => (stdout += chunk)) proc.stderr.on('data', chunk => console.log('STDERR', chunk.toString())) proc.on('exit', () => { if (stdout.match(/Optimized:\s+yes/)) { resolve(true) } else { resolve(false) } }) proc.on('error', error => reject(error)) }) } const compareMultiplePages = async function (projectId) { async function compareNext(pageNo) { const path = `tmp/${projectId}-source-${pageNo}.png` try { await fsPromises.stat(fixturePath(path)) } catch (error) { return } const same = await compare( `tmp/${projectId}-source-${pageNo}.png`, `tmp/${projectId}-generated-${pageNo}.png` ) same.should.equal(true) await compareNext(pageNo + 1) } await compareNext(0) } const comparePdf = async function (projectId, exampleDir) { console.log('CONVERT') console.log(`tmp/${projectId}.pdf`, `tmp/${projectId}-generated.png`) await convertToPng(`tmp/${projectId}.pdf`, `tmp/${projectId}-generated.png`) await convertToPng( `examples/${exampleDir}/output.pdf`, `tmp/${projectId}-source.png` ) try { await fsPromises.stat(fixturePath(`tmp/${projectId}-source-0.png`)) await compareMultiplePages(projectId) } catch (error) { const same = await compare( `tmp/${projectId}-source.png`, `tmp/${projectId}-generated.png` ) same.should.equal(true) } } const downloadAndComparePdf = async function (projectId, exampleDir, url) { const res = await fetch(url) if (!res.ok) { throw new Error('non success response: ' + res.statusText) } const dest = fs.createWriteStream(fixturePath(`tmp/${projectId}.pdf`)) await pipeline(res.body, dest) const optimised = await checkPdfInfo(`tmp/${projectId}.pdf`) optimised.should.equal(true) await comparePdf(projectId, exampleDir) } describe('Example Documents', function () { Client.runFakeFilestoreService(fixturePath('examples')) before(async function () { await ClsiApp.ensureRunning() }) before(async function () { await fsPromises.rm(fixturePath('tmp'), { force: true, recursive: true }) }) before(async function () { await fsPromises.mkdir(fixturePath('tmp')) }) after(async function () { await fsPromises.rm(fixturePath('tmp'), { force: true, recursive: true }) }) return fs.readdirSync(fixturePath('examples')).map(exampleDir => (exampleDir => describe(exampleDir, function () { before(function () { this.project_id = Client.randomId() this.outputFiles = [] // Allow each test to provide a configuration file const checksJsonPath = fixturePath( Path.join('examples', exampleDir, 'checks.json') ) this.checks = {} const stats = fs.statSync(checksJsonPath, { throwIfNoEntry: false }) if (stats && stats.isFile()) { const rawChecks = fs.readFileSync(checksJsonPath, 'utf8') try { this.checks = JSON.parse(rawChecks) } catch (err) { throw new Error( `Failed to parse checks.json for example "${exampleDir}" at path "${checksJsonPath}": ${err.message}` ) } } }) it('should generate the correct pdf and output files', async function () { this.timeout(MOCHA_LATEX_TIMEOUT) const body = await Client.compileDirectory( this.project_id, fixturePath('examples'), exampleDir ) if (body?.compile?.status === 'failure') { throw new Error('Compile failed') } const pdf = Client.getOutputFile(body, 'pdf') await downloadAndComparePdf(this.project_id, exampleDir, pdf.url) // pass the output files on to subsequent tests this.outputFiles = body.compile.outputFiles if (this.checks.mustNotDeleteRegex) { const mustNotDeleteRegex = new RegExp( this.checks.mustNotDeleteRegex ) // On subsequent compiles the isExtraneousFile method is used // to remove unwanted files - this check ensures that we don't // remove any files that should be kept (e.g. cache files) const filesToBeRemoved = this.outputFiles.filter(file => ResourceWriter.isExtraneousFile(file.path) ) for (const file of filesToBeRemoved) { expect( file.path, 'should not remove any files that must be cached' ).to.not.match(mustNotDeleteRegex) } } }) it('should generate the correct pdf on the second run as well', async function () { this.timeout(MOCHA_LATEX_TIMEOUT) const body = await Client.compileDirectory( this.project_id, fixturePath('examples'), exampleDir ) if (body?.compile?.status === 'failure') { throw new Error('Compile failed') } const pdf = Client.getOutputFile(body, 'pdf') await downloadAndComparePdf(this.project_id, exampleDir, pdf.url) }) }))(exampleDir) ) })