From 6c4e54a2c42d0eef740a8704f2bb299006df7e90 Mon Sep 17 00:00:00 2001 From: James Yu Date: Sun, 8 Sep 2024 17:37:16 +0800 Subject: [PATCH] First batch of build unit tests --- src/compile/build.ts | 13 ++- src/compile/index.ts | 4 +- test/units/06_compile_recipe.test.ts | 49 +++++----- test/units/07_compile_external.test.ts | 12 +-- test/units/08_compile_build.test.ts | 124 +++++++++++++++++++++++++ test/units/utils.ts | 16 +++- 6 files changed, 174 insertions(+), 44 deletions(-) create mode 100644 test/units/08_compile_build.test.ts diff --git a/src/compile/build.ts b/src/compile/build.ts index 37a8b513d3..bdb5f84c71 100644 --- a/src/compile/build.ts +++ b/src/compile/build.ts @@ -1,6 +1,5 @@ import * as vscode from 'vscode' import * as path from 'path' -import * as cs from 'cross-spawn' import { pickRootPath } from '../utils/quick-pick' import { lw } from '../lw' import type { ProcessEnv, RecipeStep, Step } from '../types' @@ -208,9 +207,9 @@ function spawnProcess(step: Step): ProcessEnv { const args = step.args if (args && !step.name.endsWith(lw.constant.MAGIC_PROGRAM_ARGS_SUFFIX)) { // All optional arguments are given as a unique string (% !TeX options) if any, so we use {shell: true} - lw.compile.process = cs.spawn(`${step.command} ${args[0]}`, [], {cwd: path.dirname(step.rootFile), env, shell: true}) + lw.compile.process = lw.external.spawn(`${step.command} ${args[0]}`, [], {cwd: path.dirname(step.rootFile), env, shell: true}) } else { - lw.compile.process = cs.spawn(step.command, args, {cwd: path.dirname(step.rootFile), env}) + lw.compile.process = lw.external.spawn(step.command, args ?? [], {cwd: path.dirname(step.rootFile), env}) } } else if (!step.isExternal) { let cwd = path.dirname(step.rootFile) @@ -218,10 +217,10 @@ function spawnProcess(step: Step): ProcessEnv { cwd = lw.root.dir.path } logger.log(`cwd: ${cwd}`) - lw.compile.process = cs.spawn(step.command, step.args, {cwd, env}) + lw.compile.process = lw.external.spawn(step.command, step.args ?? [], {cwd, env}) } else { logger.log(`cwd: ${step.cwd}`) - lw.compile.process = cs.spawn(step.command, step.args, {cwd: step.cwd}) + lw.compile.process = lw.external.spawn(step.command, step.args ?? [], {cwd: step.cwd}) } logger.log(`LaTeX build process spawned with PID ${lw.compile.process.pid}.`) return env @@ -245,13 +244,13 @@ async function monitorProcess(step: Step, env: ProcessEnv): Promise { return false } let stdout = '' - lw.compile.process.stdout.on('data', (msg: Buffer | string) => { + lw.compile.process.stdout?.on('data', (msg: Buffer | string) => { stdout += msg logger.logCompiler(msg.toString()) }) let stderr = '' - lw.compile.process.stderr.on('data', (msg: Buffer | string) => { + lw.compile.process.stderr?.on('data', (msg: Buffer | string) => { stderr += msg logger.logCompiler(msg.toString()) }) diff --git a/src/compile/index.ts b/src/compile/index.ts index 8bbd91fbbc..84b7b40e86 100644 --- a/src/compile/index.ts +++ b/src/compile/index.ts @@ -1,4 +1,4 @@ -import type { ChildProcessWithoutNullStreams } from 'child_process' +import type { ChildProcess } from 'child_process' import type { Step } from '../types' import { build, autoBuild } from './build' import { terminate } from './terminate' @@ -11,5 +11,5 @@ export const compile = { lastAutoBuildTime: 0, compiledPDFPath: '', compiledPDFWriting: 0, - process: undefined as ChildProcessWithoutNullStreams | undefined + process: undefined as ChildProcess | undefined } diff --git a/test/units/06_compile_recipe.test.ts b/test/units/06_compile_recipe.test.ts index 2d8546d9cc..0cd4437cef 100644 --- a/test/units/06_compile_recipe.test.ts +++ b/test/units/06_compile_recipe.test.ts @@ -7,7 +7,6 @@ import { build, initialize } from '../../src/compile/recipe' import { queue } from '../../src/compile/queue' describe(path.basename(__filename).split('.')[0] + ':', () => { - const fixture = path.basename(__filename).split('.')[0] let getOutDirStub: sinon.SinonStub let getIncludedTeXStub: sinon.SinonStub let mkdirStub: sinon.SinonStub @@ -62,7 +61,7 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { describe('lw.compile->recipe.build', () => { it('should call `saveAll` before building', async () => { const stub = sinon.stub(vscode.workspace, 'saveAll') as sinon.SinonStub - const rootFile = set.root(fixture, 'main.tex') + const rootFile = set.root('main.tex') await build(rootFile, 'latex', async () => {}) stub.restore() @@ -71,8 +70,8 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { }) it('should call `createOutputSubFolders` with correct args', async () => { - const rootFile = set.root(fixture, 'main.tex') - const subPath = get.path(fixture, 'sub', 'main.tex') + const rootFile = set.root('main.tex') + const subPath = get.path('sub', 'main.tex') await set.config('latex.tools', [{ name: 'latexmk', command: 'latexmk' }]) await set.config('latex.recipes', [{ name: 'Recipe1', tools: ['latexmk'] }]) lw.root.subfiles.path = subPath @@ -83,8 +82,8 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { }) it('should call `createOutputSubFolders` with correct args with subfiles package', async () => { - const rootFile = set.root(fixture, 'main.tex') - const subPath = get.path(fixture, 'sub', 'main.tex') + const rootFile = set.root('main.tex') + const subPath = get.path('sub', 'main.tex') await set.config('latex.tools', [{ name: 'latexmk', command: 'latexmk' }]) await set.config('latex.recipes', [{ name: 'Recipe1', tools: ['latexmk'] }]) lw.root.subfiles.path = subPath @@ -95,7 +94,7 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { }) it('should not call buildLoop if no tool is created', async () => { - const rootFile = set.root(fixture, 'main.tex') + const rootFile = set.root('main.tex') await set.config('latex.tools', []) await set.config('latex.recipes', [{ name: 'Recipe1', tools: ['nonexistentTool'] }]) @@ -106,7 +105,7 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { }) it('should set lw.compile.compiledPDFPath', async () => { - const rootFile = set.root(fixture, 'main.tex') + const rootFile = set.root('main.tex') await set.config('latex.tools', [{ name: 'latexmk', command: 'latexmk' }]) await set.config('latex.recipes', [{ name: 'Recipe1', tools: ['latexmk'] }]) @@ -128,7 +127,7 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { }) it('should do nothing but log an error if no recipe is found', async () => { - const rootFile = set.root(fixture, 'main.tex') + const rootFile = set.root('main.tex') await set.config('latex.recipes', []) await build(rootFile, 'latex', async () => {}) @@ -137,7 +136,7 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { }) it('should create build tools based on magic comments when enabled', async () => { - const rootFile = set.root(fixture, 'magic.tex') + const rootFile = set.root('magic.tex') readStub.resolves('% !TEX program = pdflatex\n') await set.config('latex.recipes', []) await set.config('latex.build.forceRecipeUsage', false) @@ -153,7 +152,7 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { }) it('should do nothing but log an error with magic comments but disabled', async () => { - const rootFile = set.root(fixture, 'magic.tex') + const rootFile = set.root('magic.tex') await set.config('latex.recipes', []) await set.config('latex.build.forceRecipeUsage', true) @@ -163,7 +162,7 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { }) it('should skip undefined tools in the recipe and log an error', async () => { - const rootFile = set.root(fixture, 'main.tex') + const rootFile = set.root('main.tex') await set.config('latex.tools', [{ name: 'existingTool', command: 'pdflatex' }]) await set.config('latex.recipes', [{ name: 'Recipe1', tools: ['nonexistentTool', 'existingTool'] }]) @@ -178,7 +177,7 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { }) it('should do nothing but log an error if no tools are prepared', async () => { - const rootFile = set.root(fixture, 'main.tex') + const rootFile = set.root('main.tex') await set.config('latex.tools', []) await set.config('latex.recipes', [{ name: 'Recipe1', tools: ['nonexistentTool'] }]) @@ -191,7 +190,7 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { describe('lw.compile->recipe.createOutputSubFolders', () => { beforeEach(() => { - getIncludedTeXStub.returns([ set.root(fixture, 'main.tex') ]) + getIncludedTeXStub.returns([ set.root('main.tex') ]) }) afterEach(() => { @@ -199,7 +198,7 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { }) it('should resolve the output directory relative to the root directory if not absolute', async () => { - const rootFile = set.root(fixture, 'main.tex') + const rootFile = set.root('main.tex') const relativeOutDir = 'output' const expectedOutDir = path.resolve(path.dirname(rootFile), relativeOutDir) getOutDirStub.returns(relativeOutDir) @@ -210,7 +209,7 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { }) it('should use the absolute output directory as is', async () => { - const rootFile = set.root(fixture, 'main.tex') + const rootFile = set.root('main.tex') const absoluteOutDir = '/absolute/output' getOutDirStub.returns(absoluteOutDir) @@ -220,7 +219,7 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { }) it('should create the output directory if it does not exist', async () => { - const rootFile = set.root(fixture, 'main.tex') + const rootFile = set.root('main.tex') const relativeOutDir = 'output' const expectedOutDir = path.resolve(path.dirname(rootFile), relativeOutDir) const stub = sinon.stub(lw.file, 'exists').resolves(false) @@ -234,7 +233,7 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { }) it('should not create the output directory if it already exists', async () => { - const rootFile = set.root(fixture, 'main.tex') + const rootFile = set.root('main.tex') const relativeOutDir = 'output' const stub = sinon.stub(lw.file, 'exists').resolves({ type: vscode.FileType.Directory, ctime: 0, mtime: 0, size: 0 }) getOutDirStub.returns(relativeOutDir) @@ -659,7 +658,7 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { it('should replace argument placeholders', async () => { await set.config('latex.tools', [{ name: 'latexmk', command: 'latexmk', args: ['%DOC%', '%DOC%', '%DIR%'], env: {} }]) - const rootFile = set.root(fixture, 'main.tex') + const rootFile = set.root('main.tex') await build(rootFile, 'latex', async () => {}) @@ -667,7 +666,7 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { assert.ok(step) assert.pathStrictEqual(step.args?.[0], rootFile.replace('.tex', '')) assert.pathStrictEqual(step.args?.[1], rootFile.replace('.tex', '')) - assert.pathStrictEqual(step.args?.[2], get.path(fixture)) + assert.pathStrictEqual(step.args?.[2], get.path('')) }) it('should set TeX directories correctly', async () => { @@ -679,7 +678,7 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { env: {}, }, ]) - const rootFile = set.root(fixture, 'main.tex') + const rootFile = set.root('main.tex') const stub = sinon.stub(lw.file, 'setTeXDirs') await build(rootFile, 'latex', async () => {}) @@ -697,7 +696,7 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { env: { DOC: '%DOC%' }, }, ]) - const rootFile = set.root(fixture, 'main.tex') + const rootFile = set.root('main.tex') await build(rootFile, 'latex', async () => {}) @@ -710,7 +709,7 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { await set.config('latex.option.maxPrintLine.enabled', true) await set.config('latex.tools', [{ name: 'latexmk', command: 'latexmk' }]) syncStub.returns({ stdout: 'pdfTeX 3.14159265-2.6-1.40.21 (MiKTeX 2.9.7350 64-bit)' }) - const rootFile = set.root(fixture, 'main.tex') + const rootFile = set.root('main.tex') await build(rootFile, 'latex', async () => {}) @@ -758,7 +757,7 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { it('should not consider MikTeX logic when pdflatex command fails', async () => { syncStub.throws(new Error('Command failed')) - const rootFile = set.root(fixture, 'main.tex') + const rootFile = set.root('main.tex') await build(rootFile, 'latex', async () => {}) @@ -769,7 +768,7 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { }) it('should not execute compile program again to determine MikTeX if already executed and cached', async () => { - const rootFile = set.root(fixture, 'main.tex') + const rootFile = set.root('main.tex') syncStub.returns({ stdout: 'pdfTeX 3.14159265-2.6-1.40.21 (MiKTeX 2.9.7350 64-bit)' }) await build(rootFile, 'latex', async () => {}) diff --git a/test/units/07_compile_external.test.ts b/test/units/07_compile_external.test.ts index 5b1fc93804..cfe83f971d 100644 --- a/test/units/07_compile_external.test.ts +++ b/test/units/07_compile_external.test.ts @@ -9,8 +9,6 @@ import { build } from '../../src/compile/external' import type { ExternalStep } from '../../src/types' describe(path.basename(__filename).split('.')[0] + ':', () => { - const fixture = path.basename(__filename).split('.')[0] - before(() => { mock.object(lw, 'file', 'root') }) @@ -33,7 +31,7 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { }) it('should create a Tool object representing the build command and arguments', async () => { - const rootFile = set.root(fixture, 'main.tex') + const rootFile = set.root('main.tex') await build('command', ['arg1', 'arg2'], '/cwd', sinon.stub(), rootFile) @@ -67,7 +65,7 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { const stub = sinon.stub().returnsArg(0) const replaceStub = sinon.stub(lwUtils, 'replaceArgumentPlaceholders').returns(stub) const pathStub = sinon.stub(lw.file, 'getPdfPath').returns('main.pdf') - const rootFile = set.root(fixture, 'main.tex') + const rootFile = set.root('main.tex') await build('command', ['arg1', 'arg2'], '/cwd', sinon.stub(), rootFile) replaceStub.restore() @@ -92,11 +90,11 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { }) it('should set the compiledPDFPath if a root file is provided', async () => { - const rootFile = set.root(fixture, 'main.tex') + const rootFile = set.root('main.tex') await build('command', ['arg1', 'arg2'], '/cwd', sinon.stub(), rootFile) - assert.pathStrictEqual(lw.compile.compiledPDFPath, get.path(fixture, 'main.pdf')) + assert.pathStrictEqual(lw.compile.compiledPDFPath, get.path('main.pdf')) }) it('should not set the compiledPDFPath if no root file is provided', async () => { @@ -114,7 +112,7 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { }) it('should add the build tool to the queue for execution', async () => { - const rootFile = set.root(fixture, 'main.tex') + const rootFile = set.root('main.tex') await build('command', ['arg1', 'arg2'], '/cwd', sinon.stub(), rootFile) diff --git a/test/units/08_compile_build.test.ts b/test/units/08_compile_build.test.ts new file mode 100644 index 0000000000..e0c220eadc --- /dev/null +++ b/test/units/08_compile_build.test.ts @@ -0,0 +1,124 @@ +import * as path from 'path' +import * as sinon from 'sinon' +import { assert, get, mock, set } from './utils' +import { lw } from '../../src/lw' +import { build } from '../../src/compile/build' +import * as pick from '../../src/utils/quick-pick' + +describe.only(path.basename(__filename).split('.')[0] + ':', () => { + let activeStub: sinon.SinonStub + let findStub: sinon.SinonStub + + before(() => { + mock.object(lw, 'file', 'root') + ;(lw.cache.getIncludedTeX as sinon.SinonStub).returns([ get.path('main.tex') ]) + findStub = sinon.stub(lw.root, 'find') + }) + + beforeEach(async () => { + activeStub = mock.activeTextEditor(get.path('main.tex'), '', { languageId: 'latex' }) + findStub.callsFake(() => { + set.root('main.tex') + return Promise.resolve(undefined) + }) + await set.config('latex.tools', [{ name: 'tool', command: 'bash', args: ['-c', 'exit 0;'] }]) + await set.config('latex.recipes', [{ name: 'recipe', tools: ['tool'] }]) + }) + + afterEach(() => { + activeStub.restore() + findStub.resetHistory() + }) + + after(() => { + sinon.restore() + }) + + describe('lw.compile->build.build', () => { + it('should do nothing if there is no active text editor', async () => { + activeStub.restore() + + await build() + + assert.hasLog('Cannot start to build because the active editor is undefined.') + }) + + it('should try find root if not given as an argument', async () => { + await build() + + assert.ok(findStub.called) + }) + + it('should skip finding root if given as an argument', async () => { + await build(false, get.path('alt.tex'), 'latex') + + assert.ok(!findStub.called) + }) + + it('should use the correct root file if not given as an argument', async () => { + set.root('main.tex') + + await build() + + assert.hasLog(`Building root file: ${get.path('main.tex')}`) + }) + + it('should use external command to build project if set', async () => { + await set.config('latex.external.build.command', 'bash') + await set.config('latex.external.build.args', ['-c', 'exit 0;#external']) + + await build() + + assert.hasLog('Recipe step 1 The command is bash:["-c","exit 0;#external"].') + }) + + it('should use the current pwd as external command cwd', async () => { + await set.config('latex.external.build.command', 'bash') + await set.config('latex.external.build.args', ['-c', 'echo $PWD']) + + await build() + + assert.hasCompilerLog(path.dirname(get.path('main.tex'))) + }) + + it('should do nothing if cannot find root and not external', async () => { + findStub.callsFake(() => { + lw.root.file.path = undefined + lw.root.file.langId = undefined + return Promise.resolve(undefined) + }) + + await build() + + assert.hasLog('Cannot find LaTeX root file. See') + }) + + it('should let use pick root file when subfile is detected', async () => { + lw.root.subfiles.path = get.path('subfile.tex') + lw.root.file.langId = 'latex' + const stub = sinon.stub(pick, 'pickRootPath').resolves(get.path('subfile.tex')) + + await build() + + lw.root.subfiles.path = undefined + lw.root.file.langId = undefined + stub.restore() + + assert.hasLog(`Building root file: ${get.path('subfile.tex')}`) + }) + + it('should skip picking root file if `skipSelection` is `true`', async () => { + lw.root.subfiles.path = get.path('subfile.tex') + lw.root.file.langId = 'latex' + const stub = sinon.stub(pick, 'pickRootPath').resolves(get.path('subfile.tex')) + + await build(true) + + lw.root.subfiles.path = undefined + lw.root.file.langId = undefined + stub.restore() + + assert.hasLog(`Building root file: ${get.path('main.tex')}`) + }) + }) +}) diff --git a/test/units/utils.ts b/test/units/utils.ts index 207e750fe0..d6557a0eb0 100644 --- a/test/units/utils.ts +++ b/test/units/utils.ts @@ -12,7 +12,8 @@ type ExtendedAssert = typeof nodeAssert & { pathStrictEqual: (actual: string | undefined, expected: string | undefined, message?: string | Error) => void, pathNotStrictEqual: (actual: string | undefined, expected: string | undefined, message?: string | Error) => void, hasLog: (message: string | RegExp) => void, - notHasLog: (message: string | RegExp) => void + notHasLog: (message: string | RegExp) => void, + hasCompilerLog: (message: string | RegExp) => void } export const assert: ExtendedAssert = nodeAssert as ExtendedAssert assert.listStrictEqual = (actual: T[] | undefined, expected: T[] | undefined, message?: string | Error) => { @@ -42,11 +43,19 @@ function hasLog(message: string | RegExp) { ? log.all().some(logMessage => logMessage.includes(lwLog.applyPlaceholders(message))) : log.all().some(logMessage => message.exec(logMessage)) } +function hasCompilerLog(message: string | RegExp) { + return typeof message === 'string' + ? lwLog.getCachedLog().CACHED_COMPILER.some(logMessage => logMessage.includes(message)) + : lwLog.getCachedLog().CACHED_COMPILER.some(logMessage => message.exec(logMessage)) +} assert.hasLog = (message: string | RegExp) => { - assert.ok(hasLog(message), log.all().join('\n')) + assert.ok(hasLog(message), '\n' + log.all().join('\n')) } assert.notHasLog = (message: string | RegExp) => { - assert.ok(!hasLog(message), log.all().join('\n')) + assert.ok(!hasLog(message), '\n' + log.all().join('\n')) +} +assert.hasCompilerLog = (message: string | RegExp) => { + assert.ok(hasCompilerLog(message), '\n' + lwLog.getCachedLog().CACHED_COMPILER.join('\n')) } export const get = { @@ -68,6 +77,7 @@ export const set = { root: (...paths: string[]) => { const rootFile = get.path(...paths) lw.root.file.path = rootFile + lw.root.file.langId = 'latex' lw.root.dir.path = path.dirname(rootFile) return rootFile },