diff --git a/src/compile/queue.ts b/src/compile/queue.ts index 96649a601..c028dd01f 100644 --- a/src/compile/queue.ts +++ b/src/compile/queue.ts @@ -135,8 +135,5 @@ export const queue = { clear, isLastStep, getStep, - getStepString, - _test: { - getQueue: () => stepQueue - } + getStepString } diff --git a/src/compile/recipe.ts b/src/compile/recipe.ts index b6f9e1a2c..bf97a5bc9 100644 --- a/src/compile/recipe.ts +++ b/src/compile/recipe.ts @@ -8,14 +8,19 @@ import { queue } from './queue' const logger = lw.log('Build', 'Recipe') -const state: { +let state: { prevRecipe: Recipe | undefined, prevLangId: string, isMikTeX: boolean | undefined -} = { - prevRecipe: undefined, - prevLangId: '', - isMikTeX: undefined +} + +initialize() +export function initialize() { + state = { + prevRecipe: undefined, + prevLangId: '', + isMikTeX: undefined + } } setDockerImage() @@ -306,8 +311,8 @@ function findRecipe(rootFile: string, langId: string, recipeName?: string): Reci candidates = recipes.filter(candidate => candidate.name.toLowerCase().match('pnw|pweave')) } if (candidates.length < 1) { - logger.log(`Failed to resolve build recipe: ${recipeName}.`) - void logger.showErrorMessage(`Failed to resolve build recipe: ${recipeName}.`) + logger.log(`Cannot find any recipe for langID \`${langId}\`.`) + void logger.showErrorMessage(`[Builder] Cannot find any recipe for langID \`${langId}\`: ${recipeName}.`) } recipe = candidates[0] } @@ -360,7 +365,7 @@ function populateTools(rootFile: string, buildTools: Tool[]): Tool[] { tool.args.includes('--lualatex') || tool.args.includes('--pdflua') || tool.args.includes('--pdflualatex') - if (isMikTeX() && ((tool.command === 'latexmk' && !isLuaLatex) || tool.command === 'pdflatex')) { + if (((tool.command === 'latexmk' && !isLuaLatex) || tool.command === 'pdflatex') && isMikTeX()) { tool.args.unshift('--max-print-line=' + lw.constant.MAX_PRINT_LINE) } } @@ -390,17 +395,3 @@ function isMikTeX(): boolean { } return state.isMikTeX } - -export const _test = { - setDockerImage, - setDockerPath, - createOutputSubFolders, - findMagicComments, - createBuildMagic, - findRecipe, - state, - populateTools, - isMikTeX, - createBuildTools, - build -} diff --git a/src/core/cache.ts b/src/core/cache.ts index 1f8527b94..0abb59de8 100644 --- a/src/core/cache.ts +++ b/src/core/cache.ts @@ -41,13 +41,7 @@ export const cache = { reset, refreshCache, refreshCacheAggressive, - loadFlsFile, - _test: { - caches, - canCache, - isExcluded, - updateAST - } + loadFlsFile } // Listener for file changes: refreshes the cache if the file can be cached. @@ -59,7 +53,7 @@ lw.watcher.src.onChange((filePath: string) => { // Listener for file deletions: removes the file from the cache if it exists. lw.watcher.src.onDelete((filePath: string) => { - if (get(filePath) === undefined) { + if (get(filePath) !== undefined) { caches.delete(filePath) logger.log(`Removed ${filePath} .`) } @@ -236,10 +230,11 @@ let cachingFilesCount: number = 0 */ async function refreshCache(filePath: string, rootPath?: string): Promise | undefined> { if (isExcluded(filePath)) { - logger.log(`Ignored ${filePath} .`) + logger.log(`File is excluded from caching: ${filePath} .`) return } if (!canCache(filePath)) { + logger.log(`File cannot be cached: ${filePath} .`) return } logger.log(`Caching ${filePath} .`) diff --git a/src/core/file.ts b/src/core/file.ts index a5f01cf17..5ff626a44 100644 --- a/src/core/file.ts +++ b/src/core/file.ts @@ -8,7 +8,7 @@ import { lw } from '../lw' const logger = lw.log('File') export const file = { - tmpDirPath: createTmpDir(), + tmpDirPath: '', getOutDir, getLangId, getJobname, @@ -23,10 +23,12 @@ export const file = { setTeXDirs, exists, read, - kpsewhich, - _test: { - createTmpDir - } + kpsewhich +} + +initialize() +export function initialize() { + file.tmpDirPath = createTmpDir() } /** diff --git a/src/core/root.ts b/src/core/root.ts index c8906aa84..d13da824a 100644 --- a/src/core/root.ts +++ b/src/core/root.ts @@ -20,15 +20,7 @@ export const root = { langId: undefined as string | undefined, }, find, - getWorkspace, - _test: { - getIndicator, - getWorkspace, - findFromMagic, - findFromActive, - findFromRoot, - findInWorkspace - } + getWorkspace } lw.watcher.src.onDelete(filePath => { @@ -163,6 +155,7 @@ async function findFromMagic(): Promise { return } + logger.log('Try finding root from magic comment.') const regex = /^(?:%\s*!\s*T[Ee]X\sroot\s*=\s*(.*\.(?:tex|[jrsRS]nw|[rR]tex|jtexw))$)/m const fileStack: string[] = [] let content: string | undefined = vscode.window.activeTextEditor.document.getText() @@ -212,6 +205,7 @@ function findFromRoot(): string | undefined { logger.log(`The active document cannot be used as the root file: ${vscode.window.activeTextEditor.document.uri.toString(true)}`) return } + logger.log('Try finding root from current root.') if (lw.cache.getIncludedTeX().includes(vscode.window.activeTextEditor.document.fileName)) { return root.file.path } @@ -235,6 +229,7 @@ function findFromActive(): string | undefined { logger.log(`The active document cannot be used as the root file: ${vscode.window.activeTextEditor.document.uri.toString(true)}`) return } + logger.log('Try finding root from active editor.') const content = utils.stripCommentsAndVerbatim(vscode.window.activeTextEditor.document.getText()) const result = content.match(getIndicator()) if (result) { @@ -288,7 +283,7 @@ function findSubfiles(content: string): string | undefined { */ async function findInWorkspace(): Promise { const workspace = getWorkspace() - logger.log(`Current workspaceRootDir: ${workspace ? workspace.toString(true) : ''} .`) + logger.log(`Try finding root from current workspaceRootDir: ${workspace ? workspace.toString(true) : ''} .`) if (!workspace) { return diff --git a/src/core/watcher.ts b/src/core/watcher.ts index c20161e03..1006a8cfb 100644 --- a/src/core/watcher.ts +++ b/src/core/watcher.ts @@ -9,20 +9,32 @@ class Watcher { * Map of folder paths to watcher information. Each folder has its own * watcher to save resources. */ - private readonly watchers: {[folder: string]: {watcher: vscode.FileSystemWatcher, files: Set}} = {} + private get watchers() { + return this._watchers + } + private readonly _watchers: {[folder: string]: {watcher: vscode.FileSystemWatcher, files: Set}} = {} /** * Set of handlers to be called when a file is created. */ - private readonly onCreateHandlers: Set<(filePath: string) => void> = new Set() + private get onCreateHandlers() { + return this._onCreateHandlers + } + private readonly _onCreateHandlers: Set<(filePath: string) => void> = new Set() /** * Set of handlers to be called when a file is changed. */ - private readonly onChangeHandlers: Set<(filePath: string) => void> = new Set() + private get onChangeHandlers() { + return this._onChangeHandlers + } + private readonly _onChangeHandlers: Set<(filePath: string) => void> = new Set() /** * Set of handlers to be called when a file is deleted. */ - private readonly onDeleteHandlers: Set<(filePath: string) => void> = new Set() + private get onDeleteHandlers() { + return this._onDeleteHandlers + } + private readonly _onDeleteHandlers: Set<(filePath: string) => void> = new Set() /** * Map of file paths to polling information. This may be of particular use * when large binary files are progressively write to disk, and multiple @@ -30,17 +42,6 @@ class Watcher { */ private readonly polling: {[filePath: string]: {time: number, size: number}} = {} - readonly _test = { - handlers: { - onCreateHandlers: this.onCreateHandlers, - onChangeHandlers: this.onChangeHandlers, - onDeleteHandlers: this.onDeleteHandlers, - }, - getWatchers: () => this.watchers, - onDidChange: (...args: Parameters) => this.onDidChange(...args), - onDidDelete: (...args: Parameters) => this.onDidDelete(...args) - } - /** * Creates a new Watcher instance. * diff --git a/src/main.ts b/src/main.ts index b82de3154..3391ec0dd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -17,8 +17,8 @@ lw.cache = cache import { root } from './core/root' lw.root = root import { parser } from './parse' +void parser.parse.reset() lw.parser = parser -void lw.parser.parse.reset() import { compile } from './compile' lw.compile = compile import { preview, server, viewer } from './preview' diff --git a/test/units/01_core_file.test.ts b/test/units/01_core_file.test.ts index 3c92b9af1..d80a29b38 100644 --- a/test/units/01_core_file.test.ts +++ b/test/units/01_core_file.test.ts @@ -4,6 +4,7 @@ import * as path from 'path' import * as sinon from 'sinon' import { assert, get, mock, set } from './utils' import { lw } from '../../src/lw' +import { initialize } from '../../src/core/file' describe(path.basename(__filename).split('.')[0] + ':', () => { const fixture = path.basename(__filename).split('.')[0] @@ -18,11 +19,14 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { describe('lw.file.createTmpDir', () => { it('should create temporary directories', () => { - assert.ok(lw.file._test.createTmpDir()) + assert.ok(path.isAbsolute(lw.file.tmpDirPath), lw.file.tmpDirPath) }) it('should create different temporary directories', () => { - assert.notStrictEqual(lw.file._test.createTmpDir(), lw.file._test.createTmpDir()) + const tmpDir1 = lw.file.tmpDirPath + initialize() + + assert.notStrictEqual(tmpDir1, lw.file.tmpDirPath) }) function forbiddenTemp(chars: string[ ]) { @@ -31,7 +35,7 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { chars.forEach(char => { tmpNames.forEach(envvar => process.env[envvar] = (process.env[envvar] === undefined ? undefined : ('\\Test ' + char))) try { - lw.file._test.createTmpDir() + initialize() assert.fail('Expected an error to be thrown') } catch { assert.ok(true) @@ -487,10 +491,9 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { }) it('should handle non-file URIs', async () => { - const oldStat = lw.external.stat - lw.external.stat = () => { return Promise.resolve({type: 0, ctime: 0, mtime: 0, size: 0}) } + const stub = sinon.stub(lw.external, 'stat').resolves({type: 0, ctime: 0, mtime: 0, size: 0}) const result = await lw.file.exists(vscode.Uri.parse('https://code.visualstudio.com/')) - lw.external.stat = oldStat + stub.restore() assert.ok(result) }) diff --git a/test/units/02_core_watcher.test.ts b/test/units/02_core_watcher.test.ts index be86fe42c..f85704bed 100644 --- a/test/units/02_core_watcher.test.ts +++ b/test/units/02_core_watcher.test.ts @@ -6,9 +6,24 @@ import { lw } from '../../src/lw' describe(path.basename(__filename).split('.')[0] + ':', () => { const fixture = path.basename(__filename).split('.')[0] + let _onDidChangeSpy: sinon.SinonSpy + const callOnDidChange = async (event: 'create' | 'change', uri: vscode.Uri) => { await _onDidChangeSpy.call(lw.watcher.src, event, uri) } + let _onDidDeleteSpy: sinon.SinonSpy + const callOnDidDelete = async (uri: vscode.Uri) => { await _onDidDeleteSpy.call(lw.watcher.src, uri) } + let _watchersSpy: sinon.SinonSpy + const getWatchers = () => _watchersSpy.call(lw.watcher.src) as {[folder: string]: {watcher: vscode.FileSystemWatcher, files: Set}} + let _onChangeHandlersSpy: sinon.SinonSpy + const getOnChangeHandlers = () => _onChangeHandlersSpy.call(lw.watcher.src) as Set<(filePath: string) => void> + let _onDeleteHandlersSpy: sinon.SinonSpy + const getOnDeleteHandlers = () => _onDeleteHandlersSpy.call(lw.watcher.src) as Set<(filePath: string) => void> before(() => { mock.object(lw, 'file', 'watcher') + _onDidChangeSpy = sinon.spy(lw.watcher.src as any, 'onDidChange') + _onDidDeleteSpy = sinon.spy(lw.watcher.src as any, 'onDidDelete') + _watchersSpy = sinon.spy(lw.watcher.src as any, 'watchers', ['get']).get + _onChangeHandlersSpy = sinon.spy(lw.watcher.src as any, 'onChangeHandlers', ['get']).get + _onDeleteHandlersSpy = sinon.spy(lw.watcher.src as any, 'onDeleteHandlers', ['get']).get }) after(() => { @@ -41,8 +56,8 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { lw.watcher.src.add(texPath) assert.ok(spy.called) - assert.ok(Object.keys(lw.watcher.src._test.getWatchers()).includes(rootDir)) - assert.ok(lw.watcher.src._test.getWatchers()[rootDir].files.has('main.tex')) + assert.ok(Object.keys(getWatchers()).includes(rootDir)) + assert.ok(getWatchers()[rootDir].files.has('main.tex')) }) it('should add a file to the existing watcher if a watcher already exists for the folder', () => { @@ -51,8 +66,8 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { lw.watcher.src.add(texPath) lw.watcher.src.add(get.path(fixture, 'another.tex')) - assert.listStrictEqual(Object.keys(lw.watcher.src._test.getWatchers()), [ rootDir ]) - assert.ok(lw.watcher.src._test.getWatchers()[rootDir].files.has('another.tex')) + assert.listStrictEqual(Object.keys(getWatchers()), [ rootDir ]) + assert.ok(getWatchers()[rootDir].files.has('another.tex')) }) }) @@ -66,9 +81,9 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { const texPath = get.path(fixture, 'main.tex') lw.watcher.src.add(texPath) - assert.ok(lw.watcher.src._test.getWatchers()[rootDir].files.has('main.tex')) + assert.ok(getWatchers()[rootDir].files.has('main.tex')) lw.watcher.src.remove(texPath) - assert.ok(!lw.watcher.src._test.getWatchers()[rootDir].files.has('main.tex')) + assert.ok(!getWatchers()[rootDir].files.has('main.tex')) }) it('should not throw an error if the file is not being watched', () => { @@ -89,7 +104,7 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { const texPath = get.path(fixture, 'main.tex') lw.watcher.src.add(texPath) - assert.ok(lw.watcher.src._test.getWatchers()[rootDir].files.has('main.tex')) + assert.ok(getWatchers()[rootDir].files.has('main.tex')) }) it('should return false if a file is not being watched', () => { @@ -97,7 +112,7 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { const texPath = get.path(fixture, 'main.tex') lw.watcher.src.add(texPath) - assert.ok(!lw.watcher.src._test.getWatchers()[rootDir].files.has('another.tex')) + assert.ok(!getWatchers()[rootDir].files.has('another.tex')) }) }) @@ -111,11 +126,11 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { const texPath = get.path(fixture, 'main.tex') lw.watcher.src.add(texPath) - const spy = sinon.spy(lw.watcher.src._test.getWatchers()[rootDir].watcher, 'dispose') + const spy = sinon.spy(getWatchers()[rootDir].watcher, 'dispose') lw.watcher.src.reset() spy.restore() assert.ok(spy.called) - assert.listStrictEqual(Object.keys(lw.watcher.src._test.getWatchers()), [ ]) + assert.listStrictEqual(Object.keys(getWatchers()), [ ]) }) }) @@ -130,14 +145,14 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { afterEach(() => { lw.watcher.src.reset() - lw.watcher.src._test.handlers.onChangeHandlers.delete(handler) + getOnChangeHandlers().delete(handler) }) it('should call onChangeHandlers when creating watched file', async () => { const texPath = get.path(fixture, 'main.tex') lw.watcher.src.add(texPath) - await lw.watcher.src._test.onDidChange('create', vscode.Uri.file(texPath)) + await callOnDidChange('create', vscode.Uri.file(texPath)) assert.strictEqual(stub.callCount, 1) assert.listStrictEqual(stub.getCall(0).args, [ texPath ]) }) @@ -146,7 +161,7 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { const texPath = get.path(fixture, 'main.tex') lw.watcher.src.add(texPath) - await lw.watcher.src._test.onDidChange('change', vscode.Uri.file(texPath)) + await callOnDidChange('change', vscode.Uri.file(texPath)) assert.strictEqual(stub.callCount, 1) assert.listStrictEqual(stub.getCall(0).args, [ texPath ]) }) @@ -155,7 +170,7 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { const texPath = get.path(fixture, 'main.tex') lw.watcher.src.add(texPath) - await lw.watcher.src._test.onDidChange('create', vscode.Uri.file(get.path(fixture, 'another.tex'))) + await callOnDidChange('create', vscode.Uri.file(get.path(fixture, 'another.tex'))) assert.strictEqual(stub.callCount, 0) }) @@ -163,7 +178,7 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { const texPath = get.path(fixture, 'main.tex') lw.watcher.src.add(texPath) - await lw.watcher.src._test.onDidChange('change', vscode.Uri.file(get.path(fixture, 'another.tex'))) + await callOnDidChange('change', vscode.Uri.file(get.path(fixture, 'another.tex'))) assert.strictEqual(stub.callCount, 0) }) @@ -172,8 +187,8 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { const binPath = get.path(fixture, 'main.bin') lw.watcher.src.add(binPath) - await lw.watcher.src._test.onDidChange('change', vscode.Uri.file(binPath)) - await lw.watcher.src._test.onDidChange('change', vscode.Uri.file(binPath)) + await callOnDidChange('change', vscode.Uri.file(binPath)) + await callOnDidChange('change', vscode.Uri.file(binPath)) await sleep(500) assert.strictEqual(stub.callCount, 1) }) @@ -183,9 +198,9 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { const binPath = get.path(fixture, 'main.bin') lw.watcher.src.add(binPath) - await lw.watcher.src._test.onDidChange('change', vscode.Uri.file(binPath)) + await callOnDidChange('change', vscode.Uri.file(binPath)) await sleep(500) - await lw.watcher.src._test.onDidChange('change', vscode.Uri.file(binPath)) + await callOnDidChange('change', vscode.Uri.file(binPath)) await sleep(500) assert.strictEqual(stub.callCount, 2) }) @@ -203,7 +218,7 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { afterEach(() => { lw.watcher.src.reset() - lw.watcher.src._test.handlers.onDeleteHandlers.delete(handler) + getOnDeleteHandlers().delete(handler) }) it('should call onDeleteHandlers when deleting watched file', async function (this: Mocha.Context) { @@ -211,7 +226,7 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { const texPath = get.path(fixture, 'main.tex') lw.watcher.src.add(texPath) - await lw.watcher.src._test.onDidDelete(vscode.Uri.file(texPath)) + await callOnDidDelete(vscode.Uri.file(texPath)) assert.strictEqual(stub.callCount, 1) assert.listStrictEqual(stub.getCall(0).args, [ texPath ]) }) @@ -221,7 +236,7 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { const binPath = get.path(fixture, 'main.bin') lw.watcher.src.add(binPath) - await lw.watcher.src._test.onDidDelete(vscode.Uri.file(binPath)) + await callOnDidDelete(vscode.Uri.file(binPath)) assert.strictEqual(stub.callCount, 0) }) }) diff --git a/test/units/03_core_cache.test.ts b/test/units/03_core_cache.test.ts index c630424db..2ccfeae8d 100644 --- a/test/units/03_core_cache.test.ts +++ b/test/units/03_core_cache.test.ts @@ -1,7 +1,8 @@ +import * as vscode from 'vscode' import * as Mocha from 'mocha' import * as path from 'path' import * as sinon from 'sinon' -import { assert, get, has, mock, set, sleep } from './utils' +import { assert, get, log, mock, set, sleep } from './utils' import { lw } from '../../src/lw' describe(path.basename(__filename).split('.')[0] + ':', () => { @@ -15,43 +16,91 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { sinon.restore() }) - describe('lw.cache.canCache', () => { - it('should return true for supported TeX files', () => { - const texPath = get.path(fixture, 'main.tex') + describe('lw.cache.isExcluded', () => { + const texPath = get.path(fixture, 'main.tex') + const bblPath = get.path(fixture, 'main.bbl') - assert.ok(lw.cache._test.canCache(texPath)) - assert.ok(lw.cache._test.canCache(get.path(fixture, 'main.rnw'))) - assert.ok(lw.cache._test.canCache(get.path(fixture, 'main.jnw'))) - assert.ok(lw.cache._test.canCache(get.path(fixture, 'main.pnw'))) + it('should excluded files', async () => { + log.start() + await lw.cache.refreshCache(bblPath) + log.stop() + assert.hasLog(`File is excluded from caching: ${bblPath} .`) + + log.start() + await lw.cache.refreshCache('/dev/null') + log.stop() + assert.hasLog('File is excluded from caching: /dev/null .') }) - it('should return false for unsupported files', () => { - assert.ok(!lw.cache._test.canCache(get.path(fixture, 'main.cls'))) - assert.ok(!lw.cache._test.canCache(get.path(fixture, 'main.sty'))) - assert.ok(!lw.cache._test.canCache(get.path(fixture, 'main.txt'))) + it('should not exclude non-excluded files', async () => { + await lw.cache.refreshCache(texPath) + assert.notHasLog(`File is excluded from caching: ${texPath} .`) }) - it('should return false for expl3-code.tex', () => { - assert.ok(!lw.cache._test.canCache(get.path(fixture, 'expl3-code.tex'))) + it('should excluded files with config set ', async () => { + await set.config('latex.watch.files.ignore', ['**/*.bbl']) + + log.start() + await lw.cache.refreshCache(bblPath) + log.stop() + assert.hasLog(`File is excluded from caching: ${bblPath} .`) + + log.start() + await lw.cache.refreshCache('/dev/null') + log.stop() + assert.notHasLog('File is excluded from caching: /dev/null .') }) }) - describe('lw.cache.isExcluded', () => { - const texPath = get.path(fixture, 'main.tex') - const bblPath = get.path(fixture, 'main.bbl') + describe('lw.cache.canCache', () => { + beforeEach(async () => { + await set.config('latex.watch.files.ignore', []) + }) + + it('should cache supported TeX files', async () => { + const texPath = get.path(fixture, 'main.tex') + + log.start() + await lw.cache.refreshCache(texPath) + log.stop() + assert.notHasLog(`File cannot be cached: ${texPath} .`) + + log.start() + await lw.cache.refreshCache(get.path(fixture, 'main.rnw')) + log.stop() + assert.notHasLog(`File cannot be cached: ${get.path(fixture, 'main.rnw')} .`) + + log.start() + await lw.cache.refreshCache(get.path(fixture, 'main.jnw')) + log.stop() + assert.notHasLog(`File cannot be cached: ${get.path(fixture, 'main.jnw')} .`) - it('should return true for excluded files', () => { - assert.ok(lw.cache._test.isExcluded(bblPath)) - assert.ok(lw.cache._test.isExcluded('/dev/null')) + log.start() + await lw.cache.refreshCache(get.path(fixture, 'main.pnw')) + log.stop() + assert.notHasLog(`File cannot be cached: ${get.path(fixture, 'main.pnw')} .`) }) - it('should return false for non-excluded files', () => { - assert.ok(!lw.cache._test.isExcluded(texPath)) + it('should return false for unsupported files', async () => { + log.start() + await lw.cache.refreshCache(get.path(fixture, 'main.cls')) + log.stop() + assert.hasLog(`File cannot be cached: ${get.path(fixture, 'main.cls')} .`) + + log.start() + await lw.cache.refreshCache(get.path(fixture, 'main.sty')) + log.stop() + assert.hasLog(`File cannot be cached: ${get.path(fixture, 'main.sty')} .`) + + log.start() + await lw.cache.refreshCache(get.path(fixture, 'main.txt')) + log.stop() + assert.hasLog(`File cannot be cached: ${get.path(fixture, 'main.txt')} .`) }) - it('should return true for excluded files with config set ', async () => { - await set.config('latex.watch.files.ignore', ['**/*.bbl']) - assert.ok(lw.cache._test.isExcluded(bblPath)) - assert.ok(!lw.cache._test.isExcluded('/dev/null')) + + it('should return false for expl3-code.tex', async () => { + await lw.cache.refreshCache(get.path(fixture, 'expl3-code.tex')) + assert.hasLog(`File cannot be cached: ${get.path(fixture, 'expl3-code.tex')} .`) }) }) @@ -188,21 +237,21 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { const texPath = get.path(fixture, 'main.tex') await lw.cache.refreshCache(texPath) - assert.ok(has.log('Updated inputs of ')) + assert.hasLog('Updated inputs of ') }) it('should update AST during caching', async () => { const texPath = get.path(fixture, 'main.tex') await lw.cache.refreshCache(texPath) - assert.ok(has.log('Parsed LaTeX AST in ')) + assert.hasLog('Parsed LaTeX AST in ') }) it('should update document elements during caching', async () => { const texPath = get.path(fixture, 'main.tex') await lw.cache.refreshCache(texPath) - assert.ok(has.log('Updated elements in ')) + assert.hasLog('Updated elements in ') }) it('should cache provided dirty TeX source', async () => { @@ -279,7 +328,7 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { lw.cache.refreshCacheAggressive(texPath) await sleep(150) stub.restore() - assert.ok(has.log('Parsing .fls ')) + assert.hasLog('Parsing .fls ') }) it('should not aggressively cache cached files without `intellisense.update.aggressive.enabled`', async function (this: Mocha.Context) { @@ -346,12 +395,9 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { it('should call lw.parser.parse.tex to parse AST', async () => { const texPath = get.path(fixture, 'main.tex') - lw.cache.add(texPath) - await lw.cache.refreshCache(texPath) - const texCache = lw.cache.get(texPath) - assert.ok(texCache) ;(lw.parser.parse.tex as sinon.SinonStub).reset() - await lw.cache._test.updateAST(texCache) + await lw.cache.refreshCache(texPath) + assert.hasLog(`Parse LaTeX AST: ${texPath} .`) assert.strictEqual((lw.parser.parse.tex as sinon.SinonStub).callCount, 1) }) }) @@ -628,7 +674,7 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { const texPathAnother = get.path(fixture, 'another.tex') await lw.cache.loadFlsFile(texPathAnother) - assert.ok(!has.log('Parsing .fls ')) + assert.notHasLog('Parsing .fls ') }) it('should not consider files that are both INPUT and OUTPUT', async () => { @@ -810,12 +856,18 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { it('should return a list of included .tex files even non-cached with `cachedOnly` set to `false`', async () => { const toParse = get.path(fixture, 'included_tex', 'main.tex') + const texPathAnother = get.path(fixture, 'included_tex', 'another.tex') + await lw.cache.refreshCache(toParse) - lw.cache._test.caches.delete(get.path(fixture, 'included_tex', 'another.tex')) - assert.listStrictEqual(lw.cache.getIncludedTeX(toParse, false), [ - get.path(fixture, 'included_tex', 'main.tex'), - get.path(fixture, 'included_tex', 'another.tex') - ]) + + const onDidDeleteSpy = sinon.spy(lw.watcher.src as any, 'onDidDelete') + const existsStub = sinon.stub(lw.file, 'exists').resolves(false) + await onDidDeleteSpy.call(lw.watcher.src, vscode.Uri.file(texPathAnother)) + onDidDeleteSpy.restore() + existsStub.restore() + + assert.strictEqual(lw.cache.get(texPathAnother), undefined) + assert.listStrictEqual(lw.cache.getIncludedTeX(toParse, false), [ toParse, texPathAnother ]) }) it('should return a list of included .bib files with circular inclusions', async () => { diff --git a/test/units/04_core_root.test.ts b/test/units/04_core_root.test.ts index 110434016..2542632db 100644 --- a/test/units/04_core_root.test.ts +++ b/test/units/04_core_root.test.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode' import * as path from 'path' import * as sinon from 'sinon' -import { assert, get, has, mock, set, sleep } from './utils' +import { assert, get, mock, set, sleep } from './utils' import { lw } from '../../src/lw' describe(path.basename(__filename).split('.')[0] + ':', () => { @@ -30,35 +30,12 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { lw.root.file.path = texPath lw.watcher.src.add(texPath) - assert.ok(!has.log('Current workspace folders: ')) - await lw.watcher.src._test.onDidDelete(vscode.Uri.file(texPath)) - assert.ok(has.log('Current workspace folders: ')) - }) - }) - - describe('lw.root.getIndicator', () => { - it('should return \\documentclass indicator on selecting `\\documentclass[]{}`', async () => { - await set.config('latex.rootFile.indicator', '\\documentclass[]{}') - const indicator = lw.root._test.getIndicator() - - assert.ok(indicator.exec('\\documentclass{article}\n')) - assert.ok(!indicator.exec('\\begin{document}\n\\end{document}\n')) - }) - - it('should return \\begin{document} indicator on selecting `\\begin{document}`', async () => { - await set.config('latex.rootFile.indicator', '\\begin{document}') - const indicator = lw.root._test.getIndicator() + assert.notHasLog('Current workspace folders: ') - assert.ok(!indicator.exec('\\documentclass{article}\n')) - assert.ok(indicator.exec('\\begin{document}\n\\end{document}\n')) - }) - - it('should return \\documentclass indicator on other values', async () => { - await set.config('latex.rootFile.indicator', 'invalid value') - const indicator = lw.root._test.getIndicator() - - assert.ok(indicator.exec('\\documentclass{article}\n')) - assert.ok(!indicator.exec('\\begin{document}\n\\end{document}\n')) + const onDidDeleteSpy = sinon.spy(lw.watcher.src as any, 'onDidDelete') + await onDidDeleteSpy.call(lw.watcher.src, vscode.Uri.file(texPath)) + onDidDeleteSpy.restore() + assert.hasLog('Current workspace folders: ') }) }) @@ -66,8 +43,8 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { it('should return undefined if no workspace is opened', () => { const texPath = get.path(fixture, 'main.tex') const stub = sinon.stub(vscode.workspace, 'workspaceFolders').value([]) - const workspace1 = lw.root._test.getWorkspace() - const workspace2 = lw.root._test.getWorkspace(texPath) + const workspace1 = lw.root.getWorkspace() + const workspace2 = lw.root.getWorkspace(texPath) stub.restore() assert.strictEqual(workspace1, undefined) @@ -76,7 +53,7 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { it('should return the workspace of provided file', () => { const texPath = get.path(fixture, 'main.tex') - const workspace = lw.root._test.getWorkspace(texPath) + const workspace = lw.root.getWorkspace(texPath) assert.strictEqual( workspace, @@ -85,7 +62,7 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { }) it('should return the first workspace if no file is provided or opened', () => { - const workspace = lw.root._test.getWorkspace() + const workspace = lw.root.getWorkspace() assert.strictEqual( workspace, @@ -96,7 +73,7 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { it('should return the workspace of active editor if no file is provided', () => { const texPath = get.path(fixture, 'main.tex') const stub = mock.activeTextEditor(texPath, '') - const workspace = lw.root._test.getWorkspace() + const workspace = lw.root.getWorkspace() stub.restore() assert.strictEqual( @@ -107,157 +84,159 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { }) describe('lw.root.findFromMagic', () => { - it('should return undefined if there is no active editor', async () => { + it('should do nothing if there is no active editor', async () => { const stub = sinon.stub(vscode.window, 'activeTextEditor').value(undefined) - const root = await lw.root._test.findFromMagic() + await lw.root.find() stub.restore() - assert.strictEqual(root, undefined) + assert.notHasLog('Try finding root from magic comment.') }) it('should find root from magic comment', async () => { const texPath = get.path(fixture, 'main.tex') const stub = mock.activeTextEditor(texPath, '%!TeX root=main.tex') - const root = await lw.root._test.findFromMagic() + await lw.root.find() stub.restore() - assert.strictEqual(root, texPath) + assert.strictEqual(lw.root.file.path, texPath) }) it('should find root from magic comment with relative path', async () => { const texPath = get.path(fixture, 'find_magic', 'main.tex') const stub = mock.activeTextEditor(texPath, '%!TeX root=../main.tex') - const root = await lw.root._test.findFromMagic() + await lw.root.find() stub.restore() - assert.strictEqual(root, get.path(fixture, 'main.tex')) + assert.strictEqual(lw.root.file.path, get.path(fixture, 'main.tex')) }) it('should return undefined if the magic root does not exist', async () => { const texPath = get.path(fixture, 'main.tex') const stub = mock.activeTextEditor(texPath, '%!TeX root=non-existing.tex') - const root = await lw.root._test.findFromMagic() + await lw.root.find() stub.restore() - assert.strictEqual(root, undefined) + assert.hasLog(`Non-existent magic root ${get.path(fixture, 'non-existing.tex')} .`) }) it('should find root from chained magic comment `a->b->c`', async () => { const texPath = get.path(fixture, 'main.tex') const stub = mock.activeTextEditor(texPath, '%!TeX root=find_magic/chain.tex') - const root = await lw.root._test.findFromMagic() + await lw.root.find() stub.restore() - assert.strictEqual(root, get.path(fixture, 'find_magic', 'main.tex')) + assert.strictEqual(lw.root.file.path, get.path(fixture, 'find_magic', 'main.tex')) }) it('should find root from deeply chained magic comment `a->b->c->d`', async () => { const texPath = get.path(fixture, 'main.tex') const stub = mock.activeTextEditor(texPath, '%!TeX root=find_magic/more_chain.tex') - const root = await lw.root._test.findFromMagic() + await lw.root.find() stub.restore() - assert.strictEqual(root, get.path(fixture, 'find_magic', 'main.tex')) + assert.strictEqual(lw.root.file.path, get.path(fixture, 'find_magic', 'main.tex')) }) it('should return undefined if the chained magic root does not exist', async () => { const texPath = get.path(fixture, 'main.tex') const stub = mock.activeTextEditor(texPath, '%!TeX root=find_magic/chain_file_not_exist.tex') - const root = await lw.root._test.findFromMagic() + await lw.root.find() stub.restore() - assert.strictEqual(root, undefined) + assert.hasLog(`Non-existent magic root ${get.path(fixture, 'find_magic', 'non-existent.tex')} .`) }) it('should return the looped root if the chain forms a loop `a->b->c->a`', async () => { const texPath = get.path(fixture, 'main.tex') const stub = mock.activeTextEditor(texPath, '%!TeX root=find_magic/loop_1.tex') - const root = await lw.root._test.findFromMagic() + await lw.root.find() stub.restore() - assert.strictEqual(root, texPath) + assert.strictEqual(lw.root.file.path, texPath) }) it('should find root from magic comment with different syntax', async () => { const texPath = get.path(fixture, 'main.tex') let stub = mock.activeTextEditor(texPath, '% !TeX root=main.tex') - let root = await lw.root._test.findFromMagic() + await lw.root.find() stub.restore() - assert.strictEqual(root, texPath) + assert.strictEqual(lw.root.file.path, texPath) + lw.root.file.path = undefined stub = mock.activeTextEditor(texPath, '% ! TeX root=main.tex') - root = await lw.root._test.findFromMagic() + await lw.root.find() stub.restore() - assert.strictEqual(root, texPath) + assert.strictEqual(lw.root.file.path, texPath) + lw.root.file.path = undefined stub = mock.activeTextEditor(texPath, '%!TEX root=main.tex') - root = await lw.root._test.findFromMagic() + await lw.root.find() stub.restore() - assert.strictEqual(root, texPath) + assert.strictEqual(lw.root.file.path, texPath) }) it('should find root from magic comment with different file name extension', async () => { let rootPath = get.path(fixture, 'find_magic', 'main.jnw') let stub = mock.activeTextEditor(rootPath, '%!TeX root=main.jnw') - let root = await lw.root._test.findFromMagic() + await lw.root.find() stub.restore() - assert.strictEqual(root, rootPath) + assert.strictEqual(lw.root.file.path, rootPath) rootPath = get.path(fixture, 'find_magic', 'main.rnw') stub = mock.activeTextEditor(rootPath, '%!TeX root=main.rnw') - root = await lw.root._test.findFromMagic() + await lw.root.find() stub.restore() - assert.strictEqual(root, rootPath) + assert.strictEqual(lw.root.file.path, rootPath) rootPath = get.path(fixture, 'find_magic', 'main.snw') stub = mock.activeTextEditor(rootPath, '%!TeX root=main.snw') - root = await lw.root._test.findFromMagic() + await lw.root.find() stub.restore() - assert.strictEqual(root, rootPath) + assert.strictEqual(lw.root.file.path, rootPath) rootPath = get.path(fixture, 'find_magic', 'main.rtex') stub = mock.activeTextEditor(rootPath, '%!TeX root=main.rtex') - root = await lw.root._test.findFromMagic() + await lw.root.find() stub.restore() - assert.strictEqual(root, rootPath) + assert.strictEqual(lw.root.file.path, rootPath) rootPath = get.path(fixture, 'find_magic', 'main.jtexw') stub = mock.activeTextEditor(rootPath, '%!TeX root=main.jtexw') - root = await lw.root._test.findFromMagic() + await lw.root.find() stub.restore() - assert.strictEqual(root, rootPath) + assert.strictEqual(lw.root.file.path, rootPath) }) }) describe('lw.root.findFromRoot', () => { - it('should return undefined if there is no active editor', () => { + it('should return undefined if there is no active editor', async () => { const stub = sinon.stub(vscode.window, 'activeTextEditor').value(undefined) - const root = lw.root._test.findFromRoot() + await lw.root.find() stub.restore() - assert.strictEqual(root, undefined) + assert.notHasLog('Try finding root from current root.') }) - it('should return undefined if there is no root', () => { + it('should return undefined if there is no root', async () => { const texPath = get.path(fixture, 'main.tex') const stub = mock.activeTextEditor(texPath, '') - const root = lw.root._test.findFromRoot() + await lw.root.find() stub.restore() - assert.strictEqual(root, undefined) + assert.notHasLog('Try finding root from current root.') }) - it('should return undefined if active editor is not a file', () => { + it('should return undefined if active editor is not a file', async () => { const texPath = get.path(fixture, 'main.tex') set.root(texPath) const stub = mock.activeTextEditor('https://google.com', '', { scheme: 'https' }) - const root = lw.root._test.findFromRoot() + await lw.root.find() stub.restore() - assert.strictEqual(root, undefined) + assert.hasLog('The active document cannot be used as the root file: ') }) it('should find root if active file is in the root tex tree', async () => { @@ -265,12 +244,14 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { const toParse = get.path(fixture, 'find_root', 'root.tex') set.root(toParse) + await lw.cache.refreshCache(texPath) await lw.cache.refreshCache(toParse) const stub = mock.activeTextEditor(texPath, '') - const root = lw.root._test.findFromRoot() + await lw.root.find() stub.restore() - assert.strictEqual(root, toParse) + assert.hasLog('Try finding root from current root.') + assert.pathStrictEqual(lw.root.file.path, toParse) }) it('should return undefined if active file is not in the root tex tree', async () => { @@ -278,12 +259,14 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { const toParse = get.path(fixture, 'find_root', 'root_no_input.tex') set.root(toParse) + await lw.cache.refreshCache(texPath) await lw.cache.refreshCache(toParse) const stub = mock.activeTextEditor(texPath, '') - const root = lw.root._test.findFromRoot() + await lw.root.find() stub.restore() - assert.strictEqual(root, undefined) + assert.hasLog('Try finding root from current root.') + assert.pathNotStrictEqual(lw.root.file.path, toParse) }) }) @@ -292,84 +275,115 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { await set.config('latex.rootFile.indicator', '\\documentclass[]{}') }) - it('should return undefined if there is no active editor', () => { + it('should do nothing if there is no active editor', async () => { const stub = sinon.stub(vscode.window, 'activeTextEditor').value(undefined) - const root = lw.root._test.findFromActive() + await lw.root.find() stub.restore() - assert.strictEqual(root, undefined) + assert.notHasLog('Try finding root from active editor.') }) - it('should return undefined if active editor is not a file', () => { + it('should do nothing if active editor is not a file', async () => { const texPath = get.path(fixture, 'main.tex') set.root(texPath) const stub = mock.activeTextEditor('https://google.com', '', { scheme: 'https' }) - const root = lw.root._test.findFromActive() + await lw.root.find() stub.restore() - assert.strictEqual(root, undefined) + assert.hasLog('The active document cannot be used as the root file:') }) - it('should find root if active file has root file indicator', () => { + it('should find root if active file has root file indicator', async () => { const texPath = get.path(fixture, 'main.tex') const stub = mock.activeTextEditor(texPath, '\\documentclass{article}\n') - const root = lw.root._test.findFromActive() + await lw.root.find() stub.restore() - assert.strictEqual(root, texPath) + assert.hasLog(`Found root file from active editor: ${texPath}`) + assert.pathStrictEqual(lw.root.file.path, texPath) }) - it('should ignore root file indicators in comments', () => { + it('should ignore root file indicators in comments', async () => { const texPath = get.path(fixture, 'main.tex') const stub = mock.activeTextEditor(texPath, '% \\documentclass{article}\n') - const root = lw.root._test.findFromActive() + await lw.root.find() stub.restore() - assert.strictEqual(root, undefined) + assert.notHasLog(`Found root file from active editor: ${texPath}`) }) - it('should find subfile root if active file is a subfile', () => { + it('should find subfile root if active file is a subfile', async () => { const texPath = get.path(fixture, 'main.tex') const stub = mock.activeTextEditor(texPath, '\\documentclass[find_active/main.tex]{subfiles}\n') - const root = lw.root._test.findFromActive() + await lw.root.find() stub.restore() - assert.strictEqual(root, get.path(fixture, 'find_active', 'main.tex')) - assert.strictEqual(lw.root.subfiles.path, texPath) + assert.hasLog('Try finding root from active editor.') + assert.pathStrictEqual(lw.root.file.path, get.path(fixture, 'find_active', 'main.tex')) + assert.pathStrictEqual(lw.root.subfiles.path, texPath) }) - it('should find root if active file is a subfile but points to non-existing file', () => { + it('should find root if active file is a subfile but points to non-existing file', async () => { const texPath = get.path(fixture, 'main.tex') const stub = mock.activeTextEditor(texPath, '\\documentclass[find_active/nothing.tex]{subfiles}\n') - const root = lw.root._test.findFromActive() + await lw.root.find() + stub.restore() + + assert.hasLog('Try finding root from active editor.') + assert.pathStrictEqual(lw.root.file.path, texPath) + }) + }) + + describe('lw.root.getIndicator', () => { + it('should use \\begin{document} indicator on selecting `\\begin{document}`', async () => { + await set.config('latex.rootFile.indicator', '\\begin{document}') + + const texPath = get.path(fixture, 'main.tex') + + const stub = mock.activeTextEditor(texPath, '\\begin{document}\n\\end{document}\n') + await lw.root.find() + stub.restore() + + assert.hasLog(`Found root file from active editor: ${texPath}`) + assert.pathStrictEqual(lw.root.file.path, texPath) + }) + + it('should return \\documentclass indicator on other values', async () => { + await set.config('latex.rootFile.indicator', 'invalid value') + + const texPath = get.path(fixture, 'main.tex') + + const stub = mock.activeTextEditor(texPath, '\\documentclass{article}\n') + await lw.root.find() stub.restore() - assert.strictEqual(root, texPath) + assert.hasLog(`Found root file from active editor: ${texPath}`) + assert.pathStrictEqual(lw.root.file.path, texPath) }) }) describe('lw.root.findInWorkspace', () => { beforeEach(async () => { - await set.config('latex.rootFile.indicator', '\\documentclass[]{}') + await set.config('latex.rootFile.indicator', '\\begin{document}') // avoid active editor check }) it('should follow `latex.search.rootFiles.include` config', async () => { await set.config('latex.search.rootFiles.include', [ 'absolutely-nothing.tex' ]) - const root = await lw.root._test.findInWorkspace() + await lw.root.find() - assert.strictEqual(root, undefined) + assert.strictEqual(lw.root.file.path, undefined) }) it('should follow `latex.search.rootFiles.exclude` config', async () => { await set.config('latex.search.rootFiles.exclude', [ '**/*' ]) - const root = await lw.root._test.findInWorkspace() + await lw.root.find() - assert.strictEqual(root, undefined) + assert.strictEqual(lw.root.file.path, undefined) }) it('should find the correct root from workspace', async () => { @@ -377,16 +391,17 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { await set.config('latex.search.rootFiles.include', [ `${fixture}/find_workspace/**/*.tex` ]) await set.config('latex.search.rootFiles.exclude', [ `${fixture}/find_workspace/**/parent.tex` ]) - const root = await lw.root._test.findInWorkspace() + await lw.root.find() - assert.strictEqual(root, texPath) + assert.hasLog('Try finding root from current workspaceRootDir:') + assert.strictEqual(lw.root.file.path, texPath) }) it('should ignore root file indicators in comments', async () => { await set.config('latex.search.rootFiles.include', [ `${fixture}/find_workspace/**/comment.tex` ]) - const root = await lw.root._test.findInWorkspace() + await lw.root.find() - assert.strictEqual(root, undefined) + assert.strictEqual(lw.root.file.path, undefined) }) it('should find the correct root if the .fls of root includes active editor', async () => { @@ -395,10 +410,11 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { await set.config('latex.search.rootFiles.include', [ `${fixture}/find_workspace/**/*.tex` ]) const stub = mock.activeTextEditor(texPathAnother, '\\documentclass{article}\n') - const root = await lw.root._test.findInWorkspace() + await lw.root.find() stub.restore() - assert.strictEqual(root, texPath) + assert.hasLog('Try finding root from current workspaceRootDir:') + assert.strictEqual(lw.root.file.path, texPath) }) it('should find the correct root if the children of root includes active editor', async () => { @@ -409,10 +425,11 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { await set.config('latex.search.rootFiles.exclude', [ `${fixture}/find_workspace/main.tex` ]) await lw.cache.refreshCache(texPath) const stub = mock.activeTextEditor(texPathAnother, '\\documentclass{article}\n') - const root = await lw.root._test.findInWorkspace() + await lw.root.find() stub.restore() - assert.strictEqual(root, texPath) + assert.hasLog('Try finding root from current workspaceRootDir:') + assert.strictEqual(lw.root.file.path, texPath) }) it('should find the correct root if there is a fls file, and the children of root includes active editor', async () => { @@ -422,10 +439,11 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { await set.config('latex.search.rootFiles.include', [ `${fixture}/find_workspace/**/*.tex` ]) await lw.cache.refreshCache(texPath) const stub = mock.activeTextEditor(texPathAnother, '\\documentclass{article}\n') - const root = await lw.root._test.findInWorkspace() + await lw.root.find() stub.restore() - assert.strictEqual(root, get.path(fixture, 'find_workspace', 'main.tex')) + assert.hasLog('Try finding root from current workspaceRootDir:') + assert.strictEqual(lw.root.file.path, get.path(fixture, 'find_workspace', 'main.tex')) }) it('should find the correct root if current root is in the candidates', async () => { @@ -434,10 +452,11 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { await set.config('latex.search.rootFiles.include', [ `${fixture}/find_workspace/**/*.tex` ]) set.root(fixture, 'find_workspace', 'main.tex') const stub = mock.activeTextEditor(texPath, '\\documentclass{article}\n') - const root = await lw.root._test.findInWorkspace() + await lw.root.find() stub.restore() - assert.strictEqual(root, texPath) + assert.hasLog('Try finding root from current workspaceRootDir:') + assert.strictEqual(lw.root.file.path, texPath) }) }) diff --git a/test/units/05_compile_queue.test.ts b/test/units/05_compile_queue.test.ts index 7b385d205..7548f962f 100644 --- a/test/units/05_compile_queue.test.ts +++ b/test/units/05_compile_queue.test.ts @@ -20,10 +20,11 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { it('should clear the queue', () => { queue.add({ name: 'latex', command: 'pdflatex' }, 'main.tex', 'Recipe1', Date.now(), false) - assert.strictEqual(queue._test.getQueue().steps.length, 1) + queue.add({ name: 'latex', command: 'pdflatex' }, 'main.tex', 'Recipe1', Date.now(), false) + assert.ok(queue.getStep()) queue.clear() - assert.strictEqual(queue._test.getQueue().steps.length, 0) + assert.strictEqual(queue.getStep(), undefined) }) it('should get the next step from the queue', () => { diff --git a/test/units/06_compile_recipe.test.ts b/test/units/06_compile_recipe.test.ts index f21185b11..c8255d552 100644 --- a/test/units/06_compile_recipe.test.ts +++ b/test/units/06_compile_recipe.test.ts @@ -2,211 +2,219 @@ import * as vscode from 'vscode' import * as path from 'path' import * as fs from 'fs' import Sinon, * as sinon from 'sinon' -import { assert, get, has, mock, set } from './utils' +import { assert, get, log, mock, set, sleep } from './utils' import { lw } from '../../src/lw' +import { build, initialize } from '../../src/compile/recipe' import { queue } from '../../src/compile/queue' -import { _test as recipe } from '../../src/compile/recipe' -import type { Tool } from '../../src/types' describe(path.basename(__filename).split('.')[0] + ':', () => { const fixture = path.basename(__filename).split('.')[0] + let getOutDirStub: sinon.SinonStub + let getIncludedTeXStub: sinon.SinonStub before(() => { mock.object(lw, 'file', 'root') + getOutDirStub = sinon.stub(lw.file, 'getOutDir').returns('.') + getIncludedTeXStub = lw.cache.getIncludedTeX as sinon.SinonStub + }) + + beforeEach(async () => { + initialize() + getIncludedTeXStub.returns([]) + await set.config('latex.recipe.default', 'first') + }) + + afterEach(() => { + getOutDirStub.resetHistory() + getIncludedTeXStub.resetHistory() + lw.root.subfiles.path = undefined + lw.compile.compiledPDFPath = '' }) after(() => { sinon.restore() }) - describe('lw.compile->queue', () => { - beforeEach(() => { - queue.clear() - }) + describe('lw.compile->recipe', () => { + it('should set the LATEXWORKSHOP_DOCKER_LATEX environment variable based on the configuration', async function (this: Mocha.Context) { + this.slow(500) + const expectedImageName = 'your-docker-image' - it('should clear the queue', () => { - queue.add({ name: 'latex', command: 'pdflatex' }, 'main.tex', 'Recipe1', Date.now(), false) - assert.strictEqual(queue._test.getQueue().steps.length, 1) + await set.config('docker.image.latex', expectedImageName) + await sleep(150) - queue.clear() - assert.strictEqual(queue._test.getQueue().steps.length, 0) + assert.strictEqual(process.env['LATEXWORKSHOP_DOCKER_LATEX'], expectedImageName) }) - it('should get the next step from the queue', () => { - queue.add({ name: 'latex', command: 'pdflatex' }, 'main.tex', 'Recipe1', Date.now(), false) - queue.add({ name: 'bibtex', command: 'bibtex' }, 'main.tex', 'Recipe1', Date.now(), false) + it('should set the LATEXWORKSHOP_DOCKER_PATH environment variable based on the configuration', async function (this: Mocha.Context) { + this.slow(500) + const expectedDockerPath = '/usr/local/bin/docker' - const step1 = queue.getStep() - const step2 = queue.getStep() + await set.config('docker.path', expectedDockerPath) + await sleep(150) - assert.strictEqual(step1?.name, 'latex') - assert.strictEqual(step2?.name, 'bibtex') + assert.strictEqual(process.env['LATEXWORKSHOP_DOCKER_PATH'], expectedDockerPath) }) + }) - it('should add a Tool as a RecipeStep to the queue', () => { - queue.add({ name: 'latex', command: 'pdflatex' }, 'main.tex', 'Recipe1', Date.now()) + describe('lw.compile->recipe.build', () => { + it('should save all open files in the workspace', async () => { + const stub = sinon.stub(vscode.workspace, 'saveAll') as sinon.SinonStub + const rootFile = set.root(fixture, 'main.tex') - const step = queue.getStep() - assert.ok(step) - assert.strictEqual(step.rootFile, 'main.tex') - assert.strictEqual(step.recipeName, 'Recipe1') - assert.strictEqual(step.isExternal, false) + await build(rootFile, 'latex', async () => {}) + stub.restore() + + assert.ok(stub.calledOnce) }) - it('should add a Tool as an ExternalStep to the queue', () => { - queue.add({ name: 'latex', command: 'pdflatex' }, 'main.tex', 'Recipe1', Date.now(), true, '/usr/bin') + it('should call `createOutputSubFolders` with correct args', async () => { + const rootFile = set.root(fixture, 'main.tex') + const subPath = get.path(fixture, '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 + getIncludedTeXStub.returns([rootFile, subPath]) - const step = queue.getStep() - assert.ok(step) - assert.strictEqual(step.recipeName, 'External') - assert.strictEqual(step.isExternal, true) - assert.strictEqual(step.cwd, '/usr/bin') + await build(rootFile, 'latex', async () => {}) + assert.hasLog(`outDir: ${path.dirname(rootFile)} .`) }) - it('should prepend a Tool as a RecipeStep to the queue', () => { - queue.add({ name: 'latex', command: 'pdflatex' }, 'main.tex', 'Recipe1', Date.now()) + 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') + await set.config('latex.tools', [{ name: 'latexmk', command: 'latexmk' }]) + await set.config('latex.recipes', [{ name: 'Recipe1', tools: ['latexmk'] }]) + lw.root.subfiles.path = subPath + getIncludedTeXStub.returns([rootFile, subPath]) - let step = queue.getStep() - queue.clear() - queue.add({ name: 'latex', command: 'pdflatex' }, 'alt.tex', 'Recipe1', Date.now()) + await build(subPath, 'latex', async () => {}) + assert.hasLog(`outDir: ${path.dirname(rootFile)} .`) + }) - assert.ok(step) - queue.prepend(step) + it('should not call buildLoop if no tool is created', async () => { + const rootFile = set.root(fixture, 'main.tex') + await set.config('latex.tools', []) + await set.config('latex.recipes', [{ name: 'Recipe1', tools: ['nonexistentTool'] }]) - step = queue.getStep() - assert.ok(step) - assert.strictEqual(step.rootFile, 'main.tex') - assert.strictEqual(step.recipeName, 'Recipe1') - assert.strictEqual(step.isExternal, false) - }) + const stub = sinon.stub() + await build(rootFile, 'latex', stub) - it('should check if the last step in the queue', () => { - queue.add({ name: 'latex', command: 'pdflatex' }, 'main.tex', 'Recipe1', Date.now(), false) + assert.strictEqual(stub.callCount, 0) + }) - let step = queue.getStep() - assert.ok(step) - assert.ok(queue.isLastStep(step)) + it('should set lw.compile.compiledPDFPath', async () => { + const rootFile = set.root(fixture, 'main.tex') + await set.config('latex.tools', [{ name: 'latexmk', command: 'latexmk' }]) + await set.config('latex.recipes', [{ name: 'Recipe1', tools: ['latexmk'] }]) - queue.add({ name: 'latex', command: 'pdflatex' }, 'main.tex', 'Recipe1', Date.now(), false) - queue.add({ name: 'latex', command: 'pdflatex' }, 'main.tex', 'Recipe1', Date.now() + 1, false) + await build(rootFile, 'latex', async () => {}) - step = queue.getStep() - assert.ok(step) - assert.ok(queue.isLastStep(step)) + assert.pathStrictEqual(lw.compile.compiledPDFPath, rootFile.replace('.tex', '.pdf')) }) + }) - it('should get the formatted string representation of a step', () => { - queue.add({ name: 'latex', command: 'pdflatex' }, 'main.tex', 'Recipe1', Date.now(), false) + describe('lw.compile->recipe.createBuildTools', () => { + it('should return undefined if no recipe is found', async () => { + const rootFile = set.root(fixture, 'main.tex') + await set.config('latex.recipes', []) - const step = queue.getStep() - assert.ok(step) - const stepString = queue.getStepString(step) - assert.strictEqual(stepString, 'Recipe1') + await build(rootFile, 'latex', async () => {}) + + assert.hasLog('Invalid toolchain.') }) - it('should get correct step repr with multiple recipes', () => { - queue.add({ name: 'latex', command: 'pdflatex' }, 'main.tex', 'Recipe1', Date.now(), false) - queue.add({ name: 'latex', command: 'pdflatex' }, 'main.tex', 'Recipe1', Date.now() + 1, false) + it('should create build tools based on magic comments when enabled', async () => { + const rootFile = set.root(fixture, 'magic.tex') + await set.config('latex.recipes', []) + await set.config('latex.build.forceRecipeUsage', false) + await set.config('latex.magic.args', ['--shell-escape']) + + await build(rootFile, 'latex', async () => {}) + const step = queue.getStep() assert.ok(step) - const stepString = queue.getStepString(step) - assert.strictEqual(stepString, 'Recipe1') + assert.strictEqual(step.name, lw.constant.TEX_MAGIC_PROGRAM_NAME + lw.constant.MAGIC_PROGRAM_ARGS_SUFFIX) + assert.strictEqual(step.command, 'pdflatex') + assert.listStrictEqual(step.args, ['--shell-escape']) }) - it('should get correct step repr with multiple tools in one recipe', () => { - const recipeTime = Date.now() - queue.add({ name: 'latex1', command: 'pdflatex' }, 'main.tex', 'Recipe1', recipeTime, false) - queue.add({ name: 'latex2', command: 'pdflatex' }, 'main.tex', 'Recipe1', recipeTime, false) + it('should return undefined with magic comments but disabled', async () => { + const rootFile = set.root(fixture, 'magic.tex') + await set.config('latex.recipes', []) + await set.config('latex.build.forceRecipeUsage', true) - let step = queue.getStep() - assert.ok(step) - let stepString = queue.getStepString(step) - assert.strictEqual(stepString, 'Recipe1: 1/2 (latex1)') + await build(rootFile, 'latex', async () => {}) - step = queue.getStep() - assert.ok(step) - stepString = queue.getStepString(step) - assert.strictEqual(stepString, 'Recipe1: 2/2 (latex2)') + assert.hasLog('Invalid toolchain.') }) - }) - describe('lw.compile->recipe', () => { - it('should set the LATEXWORKSHOP_DOCKER_LATEX environment variable based on the configuration', async () => { - const expectedImageName = 'your-docker-image' + it('should skip undefined tools in the recipe and log an error', async () => { + const rootFile = set.root(fixture, 'main.tex') + await set.config('latex.tools', [{ name: 'existingTool', command: 'pdflatex' }]) + await set.config('latex.recipes', [{ name: 'Recipe1', tools: ['nonexistentTool', 'existingTool'] }]) - await set.config('docker.image.latex', expectedImageName) - recipe.setDockerImage() + await build(rootFile, 'latex', async () => {}) - assert.strictEqual(process.env['LATEXWORKSHOP_DOCKER_LATEX'], expectedImageName) + assert.hasLog('Skipping undefined tool nonexistentTool in recipe Recipe1.') + const step = queue.getStep() + assert.ok(step) + assert.strictEqual(step.name, 'existingTool') + assert.strictEqual(step.command, 'pdflatex') + assert.listStrictEqual(step.args, []) }) - it('should set the LATEXWORKSHOP_DOCKER_PATH environment variable based on the configuration', async () => { - const expectedDockerPath = '/usr/local/bin/docker' + it('should return undefined if no tools are prepared', async () => { + const rootFile = set.root(fixture, 'main.tex') + await set.config('latex.tools', []) + await set.config('latex.recipes', [{ name: 'Recipe1', tools: ['nonexistentTool'] }]) - await set.config('docker.path', expectedDockerPath) - recipe.setDockerPath() + await build(rootFile, 'latex', async () => {}) - assert.strictEqual(process.env['LATEXWORKSHOP_DOCKER_PATH'], expectedDockerPath) + assert.hasLog('Invalid toolchain.') }) }) describe('lw.compile->recipe.createOutputSubFolders', () => { - let getOutDirStub: sinon.SinonStub - let getIncludedTeXStub: sinon.SinonStub - - before(() => { - getOutDirStub = sinon.stub(lw.file, 'getOutDir') - getIncludedTeXStub = lw.cache.getIncludedTeX as sinon.SinonStub + beforeEach(() => { + getIncludedTeXStub.returns([ set.root(fixture, 'main.tex') ]) }) afterEach(() => { - getOutDirStub.reset() - getIncludedTeXStub.reset() - }) - - after(() => { - getOutDirStub.restore() + getOutDirStub.returns('.') }) - it('should resolve the output directory relative to the root directory if not absolute', () => { + it('should resolve the output directory relative to the root directory if not absolute', async () => { const rootFile = set.root(fixture, 'main.tex') const relativeOutDir = 'output' const expectedOutDir = path.resolve(path.dirname(rootFile), relativeOutDir) - getOutDirStub.returns(relativeOutDir) - getIncludedTeXStub.returns([rootFile]) - recipe.createOutputSubFolders(rootFile) + await build(rootFile, 'latex', async () => {}) - assert.strictEqual(getOutDirStub.getCall(0).args[0], rootFile) - assert.ok(has.log(`outDir: ${expectedOutDir} .`)) + assert.hasLog(`outDir: ${expectedOutDir} .`) }) - it('should use the absolute output directory as is', () => { + it('should use the absolute output directory as is', async () => { const rootFile = set.root(fixture, 'main.tex') const absoluteOutDir = '/absolute/output' - getOutDirStub.returns(absoluteOutDir) - getIncludedTeXStub.returns([rootFile]) - recipe.createOutputSubFolders(rootFile) + await build(rootFile, 'latex', async () => {}) - assert.strictEqual(getOutDirStub.getCall(0).args[0], rootFile) - assert.ok(has.log(`outDir: ${absoluteOutDir} .`)) + assert.hasLog(`outDir: ${absoluteOutDir} .`) }) - it('should create the output directory if it does not exist', () => { + it('should create the output directory if it does not exist', async () => { const rootFile = set.root(fixture, 'main.tex') const relativeOutDir = 'output' const expectedOutDir = path.resolve(path.dirname(rootFile), relativeOutDir) - - getOutDirStub.returns(relativeOutDir) - getIncludedTeXStub.returns([rootFile]) const existsStub = sinon.stub(lw.external, 'existsSync').returns(false) const statStub = sinon.stub(lw.external, 'statSync').returns(fs.statSync(get.path(fixture))) const mkdirStub = sinon.stub(lw.external, 'mkdirSync').returns(undefined) + getOutDirStub.returns(relativeOutDir) - recipe.createOutputSubFolders(rootFile) + await build(rootFile, 'latex', async () => {}) existsStub.restore() statStub.restore() mkdirStub.restore() @@ -215,22 +223,18 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { assert.deepStrictEqual(mkdirStub.getCalls()[0].args[1], { recursive: true }) }) - it('should not create the output directory if it already exists', () => { + it('should not create the output directory if it already exists', async () => { const rootFile = set.root(fixture, 'main.tex') const relativeOutDir = 'output' - - getOutDirStub.returns(relativeOutDir) - getIncludedTeXStub.returns([rootFile]) const existsStub = sinon.stub(lw.external, 'existsSync').returns(true) const statStub = sinon.stub(lw.external, 'statSync').returns(fs.statSync(get.path(fixture))) const mkdirStub = sinon.stub(lw.external, 'mkdirSync').returns(undefined) + getOutDirStub.returns(relativeOutDir) - recipe.createOutputSubFolders(rootFile) - existsStub.restore() - statStub.restore() - mkdirStub.restore() + await build(rootFile, 'latex', async () => {}) + mkdirStub.resetHistory() - recipe.createOutputSubFolders(rootFile) + await build(rootFile, 'latex', async () => {}) existsStub.restore() statStub.restore() mkdirStub.restore() @@ -244,10 +248,16 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { before(() => { readStub = sinon.stub(lw.file, 'read') + queue.clear() + }) + + beforeEach(async () => { + await set.config('latex.build.forceRecipeUsage', false) }) afterEach(() => { readStub.reset() + queue.clear() }) after(() => { @@ -257,69 +267,79 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { it('should return an empty object if there are no magic comments', async () => { readStub.resolves('Some regular content\nwith no magic comments') - const result = await recipe.findMagicComments('dummy.tex') + await build('dummy.tex', 'latex', async () => {}) + + const step = queue.getStep() - assert.deepStrictEqual(result, { tex: undefined, bib: undefined, recipe: undefined }) + assert.ok(step) + assert.notStrictEqual(step.name, lw.constant.TEX_MAGIC_PROGRAM_NAME) + assert.notStrictEqual(step.name, lw.constant.TEX_MAGIC_PROGRAM_NAME + lw.constant.MAGIC_PROGRAM_ARGS_SUFFIX) }) it('should detect only TeX magic comment', async () => { readStub.resolves('% !TEX program = pdflatex\n') - const result = await recipe.findMagicComments('dummy.tex') + await build('dummy.tex', 'latex', async () => {}) + + const step = queue.getStep() - assert.deepStrictEqual(result, { - tex: { name: lw.constant.TEX_MAGIC_PROGRAM_NAME, command: 'pdflatex' }, - bib: undefined, - recipe: undefined, - }) + assert.ok(step) + assert.strictEqual(step.name, lw.constant.TEX_MAGIC_PROGRAM_NAME + lw.constant.MAGIC_PROGRAM_ARGS_SUFFIX) + assert.strictEqual(step.command, 'pdflatex') }) it('should detect TeX magic comment with options', async () => { readStub.resolves('% !TEX program = pdflatex\n% !TEX options = --shell-escape\n') - const result = await recipe.findMagicComments('dummy.tex') + await build('dummy.tex', 'latex', async () => {}) + + const step = queue.getStep() - assert.deepStrictEqual(result, { - tex: { name: lw.constant.TEX_MAGIC_PROGRAM_NAME, command: 'pdflatex', args: ['--shell-escape'] }, - bib: undefined, - recipe: undefined, - }) + assert.ok(step) + assert.strictEqual(step.name, lw.constant.TEX_MAGIC_PROGRAM_NAME) + assert.strictEqual(step.command, 'pdflatex') + assert.listStrictEqual(step.args, ['--shell-escape']) }) - it('should detect only BIB magic comment', async () => { - readStub.resolves('% !BIB program = bibtex\n') + it('should detect BIB magic comment', async () => { + readStub.resolves('% !TEX program = pdflatex\n% !BIB program = bibtex\n') + + await build('dummy.tex', 'latex', async () => {}) - const result = await recipe.findMagicComments('dummy.tex') + queue.getStep() // pdflatex + const step = queue.getStep() // bibtex - assert.deepStrictEqual(result, { - tex: undefined, - bib: { name: lw.constant.BIB_MAGIC_PROGRAM_NAME, command: 'bibtex' }, - recipe: undefined, - }) + assert.ok(step) + assert.strictEqual(step.name, lw.constant.BIB_MAGIC_PROGRAM_NAME + lw.constant.MAGIC_PROGRAM_ARGS_SUFFIX) + assert.strictEqual(step.command, 'bibtex') }) it('should detect BIB magic comment with options', async () => { - readStub.resolves('% !BIB program = bibtex\n% !BIB options = --min-crossrefs=100\n') + readStub.resolves('% !TEX program = pdflatex\n% !BIB program = bibtex\n% !BIB options = --min-crossrefs=100\n') - const result = await recipe.findMagicComments('dummy.tex') + await build('dummy.tex', 'latex', async () => {}) - assert.deepStrictEqual(result, { - tex: undefined, - bib: { name: lw.constant.BIB_MAGIC_PROGRAM_NAME, command: 'bibtex', args: ['--min-crossrefs=100'] }, - recipe: undefined, - }) + queue.getStep() // pdflatex + const step = queue.getStep() // bibtex + + assert.ok(step) + assert.strictEqual(step.name, lw.constant.BIB_MAGIC_PROGRAM_NAME) + assert.strictEqual(step.command, 'bibtex') + assert.listStrictEqual(step.args, ['--min-crossrefs=100']) }) it('should detect only LW recipe comment', async () => { - readStub.resolves('% !LW recipe = default\n') + await set.config('latex.tools', [{ name: 'Tool1', command: 'pdflatex' }, { name: 'Tool2', command: 'xelatex' }]) + await set.config('latex.recipes', [{ name: 'Recipe1', tools: ['Tool1'] }, { name: 'Recipe2', tools: ['Tool2'] }]) + + readStub.resolves('% !LW recipe = Recipe2\n') - const result = await recipe.findMagicComments('dummy.tex') + await build('dummy.tex', 'latex', async () => {}) - assert.deepStrictEqual(result, { - tex: undefined, - bib: undefined, - recipe: 'default', - }) + const step = queue.getStep() + assert.ok(step) + assert.strictEqual(step.name, 'Tool2') + assert.strictEqual(step.command, 'xelatex') }) it('should detect all magic comments', async () => { @@ -327,37 +347,32 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { '% !TEX program = xelatex\n' + '% !TEX options = -interaction=nonstopmode\n' + '% !BIB program = biber\n' + - '% !BIB options = --debug\n' + - '% !LW recipe = customRecipe' + '% !BIB options = --debug' ) - const result = await recipe.findMagicComments('dummy.tex') + await build('dummy.tex', 'latex', async () => {}) - assert.deepStrictEqual(result, { - tex: { - name: lw.constant.TEX_MAGIC_PROGRAM_NAME, - command: 'xelatex', - args: ['-interaction=nonstopmode'], - }, - bib: { - name: lw.constant.BIB_MAGIC_PROGRAM_NAME, - command: 'biber', - args: ['--debug'], - }, - recipe: 'customRecipe', - }) + let step = queue.getStep() + assert.ok(step) + assert.strictEqual(step.name, lw.constant.TEX_MAGIC_PROGRAM_NAME) + assert.strictEqual(step.command, 'xelatex') + assert.listStrictEqual(step.args, ['-interaction=nonstopmode']) + + step = queue.getStep() + assert.ok(step) + assert.strictEqual(step.name, lw.constant.BIB_MAGIC_PROGRAM_NAME) + assert.strictEqual(step.command, 'biber') + assert.listStrictEqual(step.args, ['--debug']) }) it('should ignore non-magic comments', async () => { - readStub.resolves('This is a regular line\n% !TEX program = lualatex') + readStub.resolves('This is a regular line\n% !TEX program = texprogram') - const result = await recipe.findMagicComments('dummy.tex') + await build('dummy.tex', 'latex', async () => {}) - assert.deepStrictEqual(result, { - tex: undefined, - bib: undefined, - recipe: undefined, - }) + const step = queue.getStep() + assert.ok(step) + assert.notStrictEqual(step.command, 'texprogram') }) it('should stop reading after encountering non-comment lines', async () => { @@ -367,211 +382,204 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { '% !BIB program = bibtex' ) - const result = await recipe.findMagicComments('dummy.tex') + await build('dummy.tex', 'latex', async () => {}) + + queue.getStep() // pdflatex + const step = queue.getStep() // bibtex - assert.deepStrictEqual(result, { - tex: { name: lw.constant.TEX_MAGIC_PROGRAM_NAME, command: 'pdflatex' }, - bib: undefined, - recipe: undefined, - }) + assert.strictEqual(step, undefined) }) }) describe('lw.compile->recipe.createBuildMagic', () => { - it('should set magicTex.args and magicTex.name if magicTex.args is undefined and magicBib is undefined', async () => { - const rootFile = set.root(fixture, 'main.tex') + let readStub: sinon.SinonStub + + before(() => { + readStub = sinon.stub(lw.file, 'read') + queue.clear() + }) + + beforeEach(async () => { + await set.config('latex.build.forceRecipeUsage', false) await set.config('latex.magic.args', ['--shell-escape']) + await set.config('latex.magic.bib.args', ['--min-crossrefs=1000']) + }) - let tool: Tool = { - command: 'pdflatex', - args: undefined, - name: 'TeXTool', - } - tool = recipe.createBuildMagic(rootFile, tool)[0] + afterEach(() => { + readStub.reset() + queue.clear() + }) + + after(() => { + readStub.restore() + }) + + it('should set magicTex.args and magicTex.name if magicTex.args is undefined and magicBib is undefined', async () => { + readStub.resolves('% !TEX program = pdflatex\n\n') + + await build('dummy.tex', 'latex', async () => {}) - assert.strictEqual(tool.name, lw.constant.TEX_MAGIC_PROGRAM_NAME + lw.constant.MAGIC_PROGRAM_ARGS_SUFFIX) - assert.listStrictEqual(tool.args, ['--shell-escape']) + const step = queue.getStep() + assert.ok(step) + assert.strictEqual(step.name, lw.constant.TEX_MAGIC_PROGRAM_NAME + lw.constant.MAGIC_PROGRAM_ARGS_SUFFIX) + assert.strictEqual(step.command, 'pdflatex') + assert.listStrictEqual(step.args, ['--shell-escape']) }) it('should set magicTex.args, magicTex.name, magicBib.args, and magicBib.name when magicBib is provided and both args are undefined', async () => { - const rootFile = set.root(fixture, 'main.tex') - await set.config('latex.magic.args', ['--shell-escape']) - await set.config('latex.magic.bib.args', ['--min-crossrefs=1000']) + readStub.resolves('% !TEX program = xelatex\n% !BIB program = biber\n') - let texTool: Tool = { - command: 'pdflatex', - args: undefined, - name: 'TeXTool', - } - let bibTool: Tool = { - command: 'bibtex', - args: undefined, - name: 'BibTool', - } + await build('dummy.tex', 'latex', async () => {}) - const result = recipe.createBuildMagic(rootFile, texTool, bibTool) - texTool = result[0] - bibTool = result[1] + let step = queue.getStep() + assert.ok(step) + assert.strictEqual(step.name, lw.constant.TEX_MAGIC_PROGRAM_NAME + lw.constant.MAGIC_PROGRAM_ARGS_SUFFIX) + assert.strictEqual(step.command, 'xelatex') + assert.listStrictEqual(step.args, ['--shell-escape']) - assert.strictEqual(texTool.name, lw.constant.TEX_MAGIC_PROGRAM_NAME + lw.constant.MAGIC_PROGRAM_ARGS_SUFFIX) - assert.listStrictEqual(texTool.args, ['--shell-escape']) - assert.strictEqual(bibTool.name, lw.constant.BIB_MAGIC_PROGRAM_NAME + lw.constant.MAGIC_PROGRAM_ARGS_SUFFIX) - assert.listStrictEqual(bibTool.args, ['--min-crossrefs=1000']) + step = queue.getStep() + assert.ok(step) + assert.strictEqual(step.name, lw.constant.BIB_MAGIC_PROGRAM_NAME + lw.constant.MAGIC_PROGRAM_ARGS_SUFFIX) + assert.strictEqual(step.command, 'biber') + assert.listStrictEqual(step.args, ['--min-crossrefs=1000']) }) it('should not overwrite magicTex.args if it is already defined, even when magicBib is provided and magicBib.args is undefined', async () => { - const rootFile = set.root(fixture, 'main.tex') - await set.config('latex.magic.args', ['--shell-escape']) - await set.config('latex.magic.bib.args', ['--min-crossrefs=1000']) + readStub.resolves('% !TEX program = xelatex\n% !TEX options = -interaction=nonstopmode\n% !BIB program = biber\n') - let texTool: Tool = { - command: 'pdflatex', - args: ['-interaction=nonstopmode'], - name: 'TeXTool', - } - let bibTool: Tool = { - command: 'bibtex', - args: undefined, - name: 'BibTool', - } - - const result = recipe.createBuildMagic(rootFile, texTool, bibTool) - texTool = result[0] - bibTool = result[1] + await build('dummy.tex', 'latex', async () => {}) - assert.strictEqual(texTool.name, 'TeXTool') - assert.listStrictEqual(texTool.args, ['-interaction=nonstopmode']) - assert.strictEqual(bibTool.name, lw.constant.BIB_MAGIC_PROGRAM_NAME + lw.constant.MAGIC_PROGRAM_ARGS_SUFFIX) - assert.listStrictEqual(bibTool.args, ['--min-crossrefs=1000']) + const step = queue.getStep() + assert.ok(step) + assert.strictEqual(step.name, lw.constant.TEX_MAGIC_PROGRAM_NAME) + assert.strictEqual(step.command, 'xelatex') + assert.listStrictEqual(step.args, ['-interaction=nonstopmode']) }) it('should not overwrite magicTex.args and magicBib.args if both are already defined', async () => { - const rootFile = set.root(fixture, 'main.tex') - await set.config('latex.magic.args', ['--shell-escape']) - await set.config('latex.magic.bib.args', ['--min-crossrefs=1000']) + readStub.resolves('% !TEX program = xelatex\n% !TEX options = -interaction=nonstopmode\n% !BIB program = biber\n% !BIB options = --debug\n') - let texTool: Tool = { - command: 'pdflatex', - args: ['-interaction=nonstopmode'], - name: 'TeXTool', - } - let bibTool: Tool = { - command: 'bibtex', - args: ['--min-crossrefs=2'], - name: 'BibTool', - } + await build('dummy.tex', 'latex', async () => {}) - const result = recipe.createBuildMagic(rootFile, texTool, bibTool) - texTool = result[0] - bibTool = result[1] + let step = queue.getStep() + assert.ok(step) + assert.strictEqual(step.name, lw.constant.TEX_MAGIC_PROGRAM_NAME) + assert.strictEqual(step.command, 'xelatex') + assert.listStrictEqual(step.args, ['-interaction=nonstopmode']) - assert.strictEqual(texTool.name, 'TeXTool') - assert.listStrictEqual(texTool.args, ['-interaction=nonstopmode']) - assert.strictEqual(bibTool.name, 'BibTool') - assert.listStrictEqual(bibTool.args, ['--min-crossrefs=2']) + step = queue.getStep() + assert.ok(step) + assert.strictEqual(step.name, lw.constant.BIB_MAGIC_PROGRAM_NAME) + assert.strictEqual(step.command, 'biber') + assert.listStrictEqual(step.args, ['--debug']) }) }) describe('lw.compile->recipe.findRecipe', () => { - beforeEach(() => { - recipe.state.prevLangId = '' - recipe.state.prevRecipe = undefined + beforeEach(async () => { + await set.config('latex.tools', [{ name: 'Tool1', command: 'pdflatex' }, { name: 'Tool2', command: 'xelatex' }, { name: 'Tool3', command: 'lualatex' }]) + await set.config('latex.recipes', [{ name: 'Recipe1', tools: ['Tool1'] }, { name: 'Recipe2', tools: ['Tool2'] }, { name: 'Recipe3', tools: ['Tool3'] }]) }) it('should return undefined and log an error if no recipes are defined', async () => { - const rootFile = set.root(fixture, 'main.tex') await set.config('latex.recipes', []) - const result = recipe.findRecipe(rootFile, 'latex') + await build('dummy.tex', 'latex', async () => {}) - assert.strictEqual(result, undefined) - assert.ok(has.log('No recipes defined.')) + assert.hasLog('No recipes defined.') }) - it('should reset prevRecipe if the language ID changes', async () => { - recipe.state.prevLangId = 'oldLangId' - await set.config('latex.recipes', [{ name: 'recipe1' }]) - await set.config('latex.recipe.default', 'recipe1') + it('should use the default recipe name if recipeName is undefined', async () => { + await set.config('latex.recipe.default', 'Recipe2') - recipe.findRecipe('root.tex', 'newLangId') + await build('dummy.tex', 'latex', async () => {}) - assert.strictEqual(recipe.state.prevRecipe, undefined) + const step = queue.getStep() + assert.ok(step) + assert.strictEqual(step.name, 'Tool2') }) - it('should use the default recipe name if recipeName is undefined', async () => { - const rootFile = set.root(fixture, 'main.tex') - await set.config('latex.recipes', [{ name: 'recipe1' }]) - await set.config('latex.recipe.default', 'recipe1') - - const result = recipe.findRecipe(rootFile, 'latex') + it('should log an error and return undefined if the specified recipe is not found', async () => { + await build('dummy.tex', 'latex', async () => {}, 'nonExistentRecipe') - assert.deepStrictEqual(result, { name: 'recipe1' }) + assert.hasLog('Failed to resolve build recipe') }) - it('should log an error and return undefined if the specified recipe is not found', async () => { - const rootFile = set.root(fixture, 'main.tex') - await set.config('latex.recipes', [{ name: 'recipe1' }]) + it('should return the last used recipe if defaultRecipeName is lastUsed', async () => { + await set.config('latex.recipe.default', 'lastUsed') - recipe.findRecipe(rootFile, 'latex', 'nonExistentRecipe') + await build('dummy.tex', 'latex', async () => {}, 'Recipe2') + queue.clear() + await build('dummy.tex', 'latex', async () => {}) - assert.ok(has.log('Failed to resolve build recipe')) + const step = queue.getStep() + assert.ok(step) + assert.strictEqual(step.name, 'Tool2') }) - it('should return the last used recipe if defaultRecipeName is lastUsed', async () => { - const rootFile = set.root(fixture, 'main.tex') - await set.config('latex.recipes', [{ name: 'recipe1' }, { name: 'lastUsedRecipe' }]) + it('should reset prevRecipe if the language ID changes', async () => { await set.config('latex.recipe.default', 'lastUsed') - recipe.state.prevLangId = 'latex' - recipe.state.prevRecipe = { name: 'lastUsedRecipe', tools: [] } - const result = recipe.findRecipe(rootFile, 'latex') + await build('dummy.tex', 'latex', async () => {}, 'Recipe2') + queue.clear() + await build('dummy.tex', 'latex-expl3', async () => {}) - assert.deepStrictEqual(result, { name: 'lastUsedRecipe', tools: [] }) + const step = queue.getStep() + assert.ok(step) + assert.notStrictEqual(step.name, 'Tool2') }) it('should return the first matching recipe based on langId if no recipe is found', async () => { - const rootFile = set.root(fixture, 'main.tex') await set.config('latex.recipes', [ - { name: 'recipe1' }, - { name: 'rsweave Recipe' }, - { name: 'weave.jl Recipe' }, - { name: 'pweave Recipe' }, + { name: 'recipe1', tools: [] }, + { name: 'rsweave Recipe', tools: [] }, + { name: 'weave.jl Recipe', tools: [] }, + { name: 'pweave Recipe', tools: [] }, ]) - await set.config('latex.recipe.default', 'first') - let result = recipe.findRecipe(rootFile, 'rsweave') - assert.deepStrictEqual(result, { name: 'rsweave Recipe' }) + log.start() + await build('dummy.tex', 'rsweave', async () => {}) + log.stop() + + assert.hasLog('Preparing to run recipe: rsweave Recipe.') + assert.notHasLog('Preparing to run recipe: weave.jl Recipe.') + assert.notHasLog('Preparing to run recipe: pweave Recipe.') + + log.start() + await build('dummy.tex', 'jlweave', async () => {}) + log.stop() + + assert.notHasLog('Preparing to run recipe: rsweave Recipe.') + assert.hasLog('Preparing to run recipe: weave.jl Recipe.') + assert.notHasLog('Preparing to run recipe: pweave Recipe.') - result = recipe.findRecipe(rootFile, 'jlweave') - assert.deepStrictEqual(result, { name: 'weave.jl Recipe' }) + log.start() + await build('dummy.tex', 'pweave', async () => {}) + log.stop() - result = recipe.findRecipe(rootFile, 'pweave') - assert.deepStrictEqual(result, { name: 'pweave Recipe' }) + assert.notHasLog('Preparing to run recipe: rsweave Recipe.') + assert.notHasLog('Preparing to run recipe: weave.jl Recipe.') + assert.hasLog('Preparing to run recipe: pweave Recipe.') }) it('should log an error and return undefined if no matching recipe is found for the specific langId', async () => { - const rootFile = set.root(fixture, 'main.tex') - await set.config('latex.recipes', [{ name: 'recipe1' }]) - - const result = recipe.findRecipe(rootFile, 'rsweave') + await build('dummy.tex', 'rsweave', async () => {}) - assert.strictEqual(result, undefined) - assert.ok(has.log('Failed to resolve build recipe: undefined.')) + assert.hasLog('Cannot find any recipe for langID `rsweave`.') }) it('should return the first recipe if no other recipe matches', async () => { - const rootFile = set.root(fixture, 'main.tex') - await set.config('latex.recipes', [{ name: 'recipe1' }, { name: 'recipe2' }]) - await set.config('latex.recipe.default', 'first') + await build('dummy.tex', 'latex', async () => {}) - const result = recipe.findRecipe(rootFile, 'latex') - - sinon.assert.match(result, { name: 'recipe1' }) + const step = queue.getStep() + assert.ok(step) + assert.strictEqual(step.name, 'Tool1') }) }) describe('lw.compile->recipe.populateTools', () => { let readStub: sinon.SinonStub + let syncStub: sinon.SinonStub let platform: PropertyDescriptor | undefined let extRoot: string @@ -581,121 +589,144 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { before(() => { readStub = sinon.stub(lw.file, 'read') + syncStub = sinon.stub(lw.external, 'sync') platform = Object.getOwnPropertyDescriptor(process, 'platform') extRoot = lw.extensionRoot }) + beforeEach(async () => { + await set.config('latex.tools', [{ name: 'latexmk', command: 'latexmk' }]) + await set.config('latex.recipes', [{ name: 'Recipe1', tools: ['latexmk'] }]) + }) + afterEach(() => { readStub.reset() + syncStub.reset() if (platform !== undefined) { Object.defineProperty(process, 'platform', platform) } lw.extensionRoot = extRoot - recipe.state.isMikTeX = undefined + queue.clear() }) after(() => { readStub.restore() + syncStub.restore() }) it('should modify command when Docker is enabled on Windows', async () => { - const tools: Tool[] = [{ name: 'latexmk', command: 'latexmk', args: [], env: {} }] - const rootFile = set.root(fixture, 'main.tex') await set.config('docker.enabled', true) - setPlatform('win32') lw.extensionRoot = '/path/to/extension' - const result = recipe.populateTools(rootFile, tools) + await build('dummy.tex', 'latex', async () => {}) - assert.strictEqual(result[0].command, path.resolve('/path/to/extension', './scripts/latexmk.bat')) + const step = queue.getStep() + assert.ok(step) + assert.pathStrictEqual(step.command, path.resolve('/path/to/extension', './scripts/latexmk.bat')) }) it('should modify command and chmod when Docker is enabled on non-Windows', async () => { - const tools: Tool[] = [{ name: 'latexmk', command: 'latexmk', args: [], env: {} }] - const rootFile = set.root(fixture, 'main.tex') await set.config('docker.enabled', true) - setPlatform('linux') lw.extensionRoot = '/path/to/extension' + const stub = sinon.stub(lw.external, 'chmodSync') - const result = recipe.populateTools(rootFile, tools) + await build('dummy.tex', 'latex', async () => {}) stub.restore() - assert.strictEqual(result[0].command, path.resolve('/path/to/extension', './scripts/latexmk')) - assert.listStrictEqual(stub.getCall(0).args, [result[0].command, 0o755]) + const step = queue.getStep() + assert.ok(step) + assert.pathStrictEqual(step.command, path.resolve('/path/to/extension', './scripts/latexmk')) + assert.strictEqual(stub.getCall(0).args?.[1], 0o755) }) it('should not modify command when Docker is disabled', async () => { - const tools: Tool[] = [{ name: 'latexmk', command: 'latexmk', args: [], env: {} }] - const rootFile = set.root(fixture, 'main.tex') await set.config('docker.enabled', false) - const result = recipe.populateTools(rootFile, tools) + await build('dummy.tex', 'latex', async () => {}) - assert.strictEqual(result[0].command, 'latexmk') + const step = queue.getStep() + assert.ok(step) + assert.strictEqual(step.command, 'latexmk') }) - it('should replace argument placeholders', () => { - const tools: Tool[] = [{ name: 'latexmk', command: 'latexmk', args: ['%DOC%', '%DOC%', '%DIR%'], env: {} }] + 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 result = recipe.populateTools(rootFile, tools) + await build(rootFile, 'latex', async () => {}) - assert.pathStrictEqual(result[0].args?.[0], rootFile.replace('.tex', '')) - assert.pathStrictEqual(result[0].args?.[1], rootFile.replace('.tex', '')) - assert.pathStrictEqual(result[0].args?.[2], get.path(fixture)) + const step = queue.getStep() + 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)) }) - it('should set TeX directories correctly', () => { - const tools: Tool[] = [ + it('should set TeX directories correctly', async () => { + await set.config('latex.tools', [ { name: 'latexmk', command: 'latexmk', args: ['-out-directory=out', '-aux-directory=aux'], env: {}, }, - ] + ]) const rootFile = set.root(fixture, 'main.tex') const stub = sinon.stub(lw.file, 'setTeXDirs') - recipe.populateTools(rootFile, tools) + await build(rootFile, 'latex', async () => {}) stub.restore() assert.listStrictEqual(stub.getCall(0).args, [rootFile, 'out', 'aux']) }) - it('should process environment variables correctly', () => { - const tools: Tool[] = [ + it('should process environment variables correctly', async () => { + await set.config('latex.tools', [ { name: 'latexmk', command: 'latexmk', args: [], env: { DOC: '%DOC%' }, }, - ] + ]) const rootFile = set.root(fixture, 'main.tex') - const result = recipe.populateTools(rootFile, tools) + await build(rootFile, 'latex', async () => {}) - assert.pathStrictEqual(result[0].env?.['DOC'], rootFile.replace('.tex', '')) + const step = queue.getStep() + assert.ok(step) + assert.pathStrictEqual(step.env?.['DOC'], rootFile.replace('.tex', '')) }) it('should append max print line arguments when enabled', async () => { - const rootFile = set.root(fixture, 'main.tex') await set.config('latex.option.maxPrintLine.enabled', true) - recipe.state.isMikTeX = true + await set.config('latex.tools', [{ name: 'latexmk', command: 'latexmk' }]) + syncStub.returns('pdfTeX 3.14159265-2.6-1.40.21 (MiKTeX 2.9.7350 64-bit)') + const rootFile = set.root(fixture, 'main.tex') + + await build(rootFile, 'latex', async () => {}) - let result = recipe.populateTools(rootFile, [{ name: 'latexmk', command: 'latexmk', args: [], env: {} }]) - assert.ok(result[0].args?.includes('--max-print-line=' + lw.constant.MAX_PRINT_LINE)) + let step = queue.getStep() + assert.ok(step) + assert.ok(step.args?.includes('--max-print-line=' + lw.constant.MAX_PRINT_LINE), step.args?.join(' ')) - result = recipe.populateTools(rootFile, [{ name: 'pdflatex', command: 'pdflatex', args: [], env: {} }]) - assert.ok(result[0].args?.includes('--max-print-line=' + lw.constant.MAX_PRINT_LINE)) + await set.config('latex.tools', [{ name: 'latexmk', command: 'pdflatex' }]) + initialize() + await build(rootFile, 'latex', async () => {}) - result = recipe.populateTools(rootFile, [ - { name: 'latexmk', command: 'latexmk', args: ['-lualatex'], env: {} }, - ]) - assert.listStrictEqual(result[0].args, ['-lualatex']) + step = queue.getStep() + assert.ok(step) + assert.ok(step.args?.includes('--max-print-line=' + lw.constant.MAX_PRINT_LINE), step.args?.join(' ')) + + await set.config('latex.tools', [{ name: 'latexmk', command: 'latexmk', args: ['--lualatex'] }]) + initialize() + await build(rootFile, 'latex', async () => {}) + + step = queue.getStep() + assert.ok(step) + assert.ok(!step.args?.includes('--max-print-line=' + lw.constant.MAX_PRINT_LINE), step.args?.join(' ')) }) }) @@ -706,194 +737,50 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { syncStub = sinon.stub(lw.external, 'sync') }) + beforeEach(async () => { + await set.config('latex.option.maxPrintLine.enabled', true) + await set.config('latex.tools', [{ name: 'latexmk', command: 'latexmk' }]) + }) + afterEach(() => { syncStub.reset() - recipe.state.isMikTeX = undefined }) after(() => { syncStub.restore() }) - it('should return true when pdflatex is provided by MiKTeX', () => { - syncStub.returns('pdfTeX 3.14159265-2.6-1.40.21 (MiKTeX 2.9.7350 64-bit)') - - const result = recipe.isMikTeX() - - assert.ok(result) - assert.strictEqual(syncStub.callCount, 1) - }) - - it('should return false when pdflatex is not provided by MiKTeX', () => { - syncStub.returns('pdfTeX 3.14159265-2.6-1.40.21 (TeX Live 2020)') - - const result = recipe.isMikTeX() - - assert.ok(!result) - assert.strictEqual(syncStub.callCount, 1) - }) - - it('should return false when pdflatex command fails', () => { + it('should return false when pdflatex command fails', async () => { syncStub.throws(new Error('Command failed')) - - const result = recipe.isMikTeX() - - assert.ok(!result) - assert.strictEqual(syncStub.callCount, 1) - assert.ok(has.log('Cannot run `pdflatex` to determine if we are using MiKTeX.')) - }) - - it('should return cached value if state.isMikTeX is already defined', () => { - recipe.state.isMikTeX = true - - const result = recipe.isMikTeX() - - assert.ok(result) - assert.strictEqual(syncStub.callCount, 0) - }) - }) - - describe('lw.compile->recipe.createBuildTools', () => { - it('should return undefined if no recipe is found', async () => { const rootFile = set.root(fixture, 'main.tex') - await set.config('latex.recipes', []) - - const result = await recipe.createBuildTools(rootFile, 'latex') - - assert.strictEqual(result, undefined) - }) - - it('should create build tools based on magic comments when enabled', async () => { - const rootFile = set.root(fixture, 'magic.tex') - await set.config('latex.recipes', []) - await set.config('latex.build.forceRecipeUsage', false) - await set.config('latex.magic.args', ['--shell-escape']) - - const result = await recipe.createBuildTools(rootFile, 'latex') - - assert.deepStrictEqual(result, [ - { - name: lw.constant.TEX_MAGIC_PROGRAM_NAME + lw.constant.MAGIC_PROGRAM_ARGS_SUFFIX, - command: 'pdflatex', - args: ['--shell-escape'], - }, - ]) - }) - - it('should return undefined with magic comments but disabled', async () => { - const rootFile = set.root(fixture, 'magic.tex') - await set.config('latex.recipes', []) - await set.config('latex.build.forceRecipeUsage', true) - - const result = await recipe.createBuildTools(rootFile, 'latex') - - assert.strictEqual(result, undefined) - }) - - it('should skip undefined tools in the recipe and log an error', async () => { - const rootFile = set.root(fixture, 'main.tex') - await set.config('latex.tools', [{ name: 'existingTool', command: 'pdflatex' }]) - await set.config('latex.recipes', [{ name: 'Recipe1', tools: ['nonexistentTool', 'existingTool'] }]) - - const result = await recipe.createBuildTools(rootFile, 'latex') - - assert.deepStrictEqual(result, [{ name: 'existingTool', command: 'pdflatex', args: [] }]) - assert.ok(has.log('Skipping undefined tool nonexistentTool in recipe Recipe1.')) - }) - - it('should return undefined if no tools are prepared', async () => { - const rootFile = set.root(fixture, 'main.tex') - await set.config('latex.tools', []) - await set.config('latex.recipes', [{ name: 'Recipe1', tools: ['nonexistentTool'] }]) - - const result = await recipe.createBuildTools(rootFile, 'latex') - - assert.strictEqual(result, undefined) - }) - }) - - describe('lw.compile->recipe.build', () => { - let getOutDirStub: sinon.SinonStub - let getIncludedTeXStub: sinon.SinonStub - let saveAllStub: sinon.SinonStub - - before(() => { - getOutDirStub = sinon.stub(lw.file, 'getOutDir') - getIncludedTeXStub = lw.cache.getIncludedTeX as sinon.SinonStub - saveAllStub = sinon.stub(vscode.workspace, 'saveAll') as sinon.SinonStub - }) - - afterEach(() => { - getOutDirStub.reset() - getIncludedTeXStub.reset() - saveAllStub.reset() - lw.root.subfiles.path = undefined - lw.compile.compiledPDFPath = '' - }) - - after(() => { - getOutDirStub.restore() - }) - - it('should save all open files in the workspace', async () => { - const rootFile = set.root(fixture, 'main.tex') - getIncludedTeXStub.returns([rootFile]) - getOutDirStub.returns('.') - - await recipe.build(rootFile, 'latex', async () => {}) - - assert.ok(saveAllStub.calledOnce) - }) - - it('should call `createOutputSubFolders` with correct args', async () => { - const rootFile = set.root(fixture, 'main.tex') - const subPath = get.path(fixture, '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 - getIncludedTeXStub.returns([rootFile, subPath]) - getOutDirStub.returns('.') - - await recipe.build(rootFile, 'latex', async () => {}) - assert.ok(has.log(`outDir: ${path.dirname(rootFile)} .`)) - }) - - 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') - await set.config('latex.tools', [{ name: 'latexmk', command: 'latexmk' }]) - await set.config('latex.recipes', [{ name: 'Recipe1', tools: ['latexmk'] }]) - lw.root.subfiles.path = subPath - getIncludedTeXStub.returns([rootFile, subPath]) - getOutDirStub.returns('.') - await recipe.build(subPath, 'latex', async () => {}) - assert.ok(has.log(`outDir: ${path.dirname(rootFile)} .`)) - }) - - it('should not call buildLoop if no tool is created', async () => { - const rootFile = set.root(fixture, 'main.tex') - await set.config('latex.tools', []) - await set.config('latex.recipes', [{ name: 'Recipe1', tools: ['nonexistentTool'] }]) - getIncludedTeXStub.returns([rootFile]) - getOutDirStub.returns('.') - - const stub = sinon.stub() - await recipe.build(rootFile, 'latex', stub) + await build(rootFile, 'latex', async () => {}) - assert.strictEqual(stub.callCount, 0) + const step = queue.getStep() + assert.ok(step) + assert.ok(!step.args?.includes('--max-print-line=' + lw.constant.MAX_PRINT_LINE), step.args?.join(' ')) + assert.hasLog('Cannot run `pdflatex` to determine if we are using MiKTeX.') }) - it('should set lw.compile.compiledPDFPath', async () => { + it('should return cached value if state.isMikTeX is already defined', async () => { const rootFile = set.root(fixture, 'main.tex') - await set.config('latex.tools', [{ name: 'latexmk', command: 'latexmk' }]) - await set.config('latex.recipes', [{ name: 'Recipe1', tools: ['latexmk'] }]) - getIncludedTeXStub.returns([rootFile]) - getOutDirStub.returns('.') + syncStub.returns('pdfTeX 3.14159265-2.6-1.40.21 (MiKTeX 2.9.7350 64-bit)') - await recipe.build(rootFile, 'latex', async () => {}) + await build(rootFile, 'latex', async () => {}) + queue.clear() + syncStub.resetHistory() + const testEnv = process.env['LATEXWORKSHOP_TEST'] + const ciEnv = process.env['LATEXWORKSHOP_CITEST'] + process.env['LATEXWORKSHOP_TEST'] = '' + process.env['LATEXWORKSHOP_CITEST'] = '' + await build(rootFile, 'latex', async () => {}) + process.env['LATEXWORKSHOP_TEST'] = testEnv + process.env['LATEXWORKSHOP_CITEST'] = ciEnv - assert.strictEqual(lw.compile.compiledPDFPath, rootFile.replace('.tex', '.pdf')) + const step = queue.getStep() + assert.ok(step) + assert.ok(step.args?.includes('--max-print-line=' + lw.constant.MAX_PRINT_LINE), step.args?.join(' ')) + assert.strictEqual(syncStub.callCount, 0) }) }) }) diff --git a/test/units/07_compile_external.test.ts b/test/units/07_compile_external.test.ts index a69eef550..5b1fc9380 100644 --- a/test/units/07_compile_external.test.ts +++ b/test/units/07_compile_external.test.ts @@ -37,9 +37,8 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { await build('command', ['arg1', 'arg2'], '/cwd', sinon.stub(), rootFile) - assert.strictEqual(queue._test.getQueue().steps.length, 1) - - const step = queue._test.getQueue().steps[0] as ExternalStep + const step = queue.getStep() as ExternalStep | undefined + assert.ok(step) assert.strictEqual(step.name, 'command') assert.strictEqual(step.command, 'command') assert.strictEqual(step.isExternal, true) @@ -49,7 +48,8 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { it('should determine the current working directory for the build', async () => { await build('command', ['arg1', 'arg2'], '/cwd', sinon.stub()) - const step = queue._test.getQueue().steps[0] as ExternalStep + const step = queue.getStep() as ExternalStep | undefined + assert.ok(step) assert.pathStrictEqual(step.cwd, get.path()) }) @@ -58,7 +58,8 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { await build('command', ['arg1', 'arg2'], '/cwd', sinon.stub()) stub.restore() - const step = queue._test.getQueue().steps[0] as ExternalStep + const step = queue.getStep() as ExternalStep | undefined + assert.ok(step) assert.pathStrictEqual(step.cwd, '/cwd') }) @@ -117,7 +118,8 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { await build('command', ['arg1', 'arg2'], '/cwd', sinon.stub(), rootFile) - assert.strictEqual(queue._test.getQueue().steps.length, 1) + assert.ok(queue.getStep()) + assert.strictEqual(queue.getStep(), undefined) }) }) }) diff --git a/test/units/utils.ts b/test/units/utils.ts index 0b149c5f8..207e750fe 100644 --- a/test/units/utils.ts +++ b/test/units/utils.ts @@ -5,28 +5,48 @@ import * as path from 'path' import * as nodeAssert from 'assert' import * as sinon from 'sinon' import { lw } from '../../src/lw' -import { log } from '../../src/utils/logger' +import { log as lwLog } from '../../src/utils/logger' type ExtendedAssert = typeof nodeAssert & { listStrictEqual: (actual: T[] | undefined, expected: T[] | undefined, message?: string | Error) => void, - pathStrictEqual: (actual: string | undefined, expected: string | undefined, message?: string | Error) => void + 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 } export const assert: ExtendedAssert = nodeAssert as ExtendedAssert assert.listStrictEqual = (actual: T[] | undefined, expected: T[] | undefined, message?: string | Error) => { if (actual === undefined || expected === undefined) { - nodeAssert.strictEqual(actual, expected) + assert.strictEqual(actual, expected) } else { - nodeAssert.deepStrictEqual(actual.sort(), expected.sort(), message) + assert.deepStrictEqual(actual.sort(), expected.sort(), message) } } -assert.pathStrictEqual = (actual: string | undefined, expected: string | undefined, message?: string | Error) => { +function getPaths(actual: string | undefined, expected: string | undefined): [string, string] { actual = path.normalize(actual ?? '.') expected = path.normalize(expected ?? '.') if (os.platform() === 'win32') { actual = actual.replace(/^([a-zA-Z]):/, (_, p1: string) => p1.toLowerCase() + ':') expected = expected.replace(/^([a-zA-Z]):/, (_, p1: string) => p1.toLowerCase() + ':') } - nodeAssert.strictEqual(path.relative(actual, expected), '', message) + return [actual, expected] +} +assert.pathStrictEqual = (actual: string | undefined, expected: string | undefined, message?: string | Error) => { + assert.strictEqual(path.relative(...getPaths(actual, expected)), '', message) +} +assert.pathNotStrictEqual = (actual: string | undefined, expected: string | undefined, message?: string | Error) => { + assert.notStrictEqual(path.relative(...getPaths(actual, expected)), '', message) +} +function hasLog(message: string | RegExp) { + return typeof message === 'string' + ? log.all().some(logMessage => logMessage.includes(lwLog.applyPlaceholders(message))) + : log.all().some(logMessage => message.exec(logMessage)) +} +assert.hasLog = (message: string | RegExp) => { + assert.ok(hasLog(message), log.all().join('\n')) +} +assert.notHasLog = (message: string | RegExp) => { + assert.ok(!hasLog(message), log.all().join('\n')) } export const get = { @@ -72,18 +92,23 @@ export const reset = { changedConfigs.clear() }, log: () => { - log.resetCachedLog() + lwLog.resetCachedLog() + _logStartIdx = 0 + _logStopIdx = 0 } } -export const has = { - log: (message: string | RegExp): boolean => { - const logs = log.getCachedLog().CACHED_EXTLOG - if (typeof message === 'string') { - return logs.some(logMessage => logMessage.includes(log.applyPlaceholders(message))) - } else { - return logs.some(logMessage => message.exec(logMessage)) - } +let _logStartIdx = 0 +let _logStopIdx = 0 +export const log = { + all: () => { + return lwLog.getCachedLog().CACHED_EXTLOG.slice(_logStartIdx, _logStopIdx ? _logStopIdx : undefined) + }, + start: () => { + _logStartIdx = lwLog.getCachedLog().CACHED_EXTLOG.length + }, + stop: () => { + _logStopIdx = lwLog.getCachedLog().CACHED_EXTLOG.length } } @@ -118,7 +143,7 @@ export const mock = { export const hooks = { beforeEach: () => { - log.resetCachedLog() + reset.log() }, async afterEach(this: Mocha.Context) { cacheLog(this) @@ -134,7 +159,7 @@ function cacheLog(context: Mocha.Context) { } const name = sanitize(context.currentTest?.title ?? '') - const cachedLog = log.getCachedLog() + const cachedLog = lwLog.getCachedLog() const folders = [] let parent = context.currentTest?.parent while(parent && parent.title !== '') {