diff --git a/server-ce/test/helpers/compile.ts b/server-ce/test/helpers/compile.ts index 9f0c9e4150..d41e43221f 100644 --- a/server-ce/test/helpers/compile.ts +++ b/server-ce/test/helpers/compile.ts @@ -9,6 +9,14 @@ export function throttledRecompile() { return recompile } +export function stopCompile(options: { delay?: number } = {}) { + const { delay = 0 } = options + cy.wait(delay) + cy.log('Stop compile') + cy.findByRole('button', { name: 'Toggle compile options menu' }).click() + cy.findByRole('menuitem', { name: 'Stop compilation' }).click() +} + export function prepareWaitForNextCompileSlot() { let lastCompile = 0 function queueReset() { diff --git a/server-ce/test/sandboxed-compiles.spec.ts b/server-ce/test/sandboxed-compiles.spec.ts index c84af1897b..62c87384d6 100644 --- a/server-ce/test/sandboxed-compiles.spec.ts +++ b/server-ce/test/sandboxed-compiles.spec.ts @@ -1,7 +1,7 @@ import { ensureUserExists, login } from './helpers/login' import { createProject } from './helpers/project' import { isExcludedBySharding, startWith } from './helpers/config' -import { throttledRecompile } from './helpers/compile' +import { throttledRecompile, stopCompile } from './helpers/compile' import { v4 as uuid } from 'uuid' import { waitUntilScrollingFinished } from './helpers/waitUntilScrollingFinished' import { beforeWithReRunOnTestRetry } from './helpers/beforeWithReRunOnTestRetry' @@ -56,8 +56,40 @@ describe('SandboxedCompiles', function () { checkSyncTeX() checkXeTeX() checkRecompilesAfterErrors() + checkStopCompile() }) + function checkStopCompile() { + it('users can stop a running compile', function () { + login('user@example.com') + createProject('test-project') + // create an infinite loop in the main document + // this will cause the compile to run indefinitely + cy.findByText('\\maketitle').parent().click() + cy.findByText('\\maketitle') + .parent() + .type('\n\\def\\x{{}Hello!\\par\\x}\\x') + cy.log('Start compile') + // We need to start the compile manually because we do not want to wait for it to finish + cy.findByText('Recompile').click() + // Now stop the compile and kill the latex process + stopCompile({ delay: 1000 }) + cy.get('.logs-pane') + .invoke('text') + .should('match', /PDF Rendering Error|Compilation cancelled/) + // Check that the previous compile is not running in the background by + // disabling the infinite loop and recompiling + cy.findByText('\\def').parent().click() + cy.findByText('\\def').parent().type('{home}disabled loop% ') + cy.findByText('Recompile').click() + cy.get('.pdf-viewer').should('contain.text', 'disabled loop') + cy.get('.logs-pane').should( + 'not.contain.text', + 'A previous compile is still running' + ) + }) + } + function checkSyncTeX() { // TODO(25342): re-enable // eslint-disable-next-line mocha/no-skipped-tests @@ -227,6 +259,7 @@ describe('SandboxedCompiles', function () { checkSyncTeX() checkXeTeX() checkRecompilesAfterErrors() + checkStopCompile() }) describe.skip('unavailable in CE', function () { @@ -241,5 +274,6 @@ describe('SandboxedCompiles', function () { checkSyncTeX() checkXeTeX() checkRecompilesAfterErrors() + checkStopCompile() }) }) diff --git a/services/clsi/app/js/LocalCommandRunner.js b/services/clsi/app/js/LocalCommandRunner.js index ce27473358..aa62825443 100644 --- a/services/clsi/app/js/LocalCommandRunner.js +++ b/services/clsi/app/js/LocalCommandRunner.js @@ -54,6 +54,7 @@ module.exports = CommandRunner = { cwd: directory, env, stdio: ['pipe', 'pipe', 'ignore'], + detached: true, }) let stdout = '' diff --git a/services/clsi/test/acceptance/js/StopCompile.js b/services/clsi/test/acceptance/js/StopCompile.js new file mode 100644 index 0000000000..103a70f37d --- /dev/null +++ b/services/clsi/test/acceptance/js/StopCompile.js @@ -0,0 +1,47 @@ +const Client = require('./helpers/Client') +const ClsiApp = require('./helpers/ClsiApp') +const { expect } = require('chai') + +describe('Stop compile', function () { + before(function (done) { + this.request = { + options: { + timeout: 100, + }, // seconds + resources: [ + { + path: 'main.tex', + content: `\ +\\documentclass{article} +\\begin{document} +\\def\\x{Hello!\\par\\x} +\\x +\\end{document}\ +`, + }, + ], + } + this.project_id = Client.randomId() + ClsiApp.ensureRunning(() => { + // start the compile in the background + Client.compile(this.project_id, this.request, (error, res, body) => { + this.compileResult = { error, res, body } + }) + // wait for 1 second before stopping the compile + setTimeout(() => { + Client.stopCompile(this.project_id, (error, res, body) => { + this.stopResult = { error, res, body } + setTimeout(done, 1000) // allow time for the compile request to terminate + }) + }, 1000) + }) + }) + + it('should force a compile response with an error status', function () { + expect(this.stopResult.error).to.be.null + expect(this.stopResult.res.statusCode).to.equal(204) + expect(this.compileResult.res.statusCode).to.equal(200) + expect(this.compileResult.body.compile.status).to.equal('terminated') + expect(this.compileResult.body.compile.error).to.equal('terminated') + }) +}) diff --git a/services/clsi/test/acceptance/js/helpers/Client.js b/services/clsi/test/acceptance/js/helpers/Client.js index a0bdce734f..49bf7390c6 100644 --- a/services/clsi/test/acceptance/js/helpers/Client.js +++ b/services/clsi/test/acceptance/js/helpers/Client.js @@ -42,6 +42,16 @@ module.exports = Client = { ) }, + stopCompile(projectId, callback) { + if (callback == null) { + callback = function () {} + } + return request.post( + { url: `${this.host}/project/${projectId}/compile/stop` }, + callback + ) + }, + clearCache(projectId, callback) { if (callback == null) { callback = function () {}