diff --git a/extensions/sql-database-projects/src/common/constants.ts b/extensions/sql-database-projects/src/common/constants.ts index 4b942e781e..04bcfffdcd 100644 --- a/extensions/sql-database-projects/src/common/constants.ts +++ b/extensions/sql-database-projects/src/common/constants.ts @@ -423,9 +423,14 @@ export enum DatabaseProjectItemType { // AutoRest export const autorestPostDeploymentScriptName = 'PostDeploymentScript.sql'; export const nodeButNotAutorestFound = localize('nodeButNotAutorestFound', "Autorest tool not found in system path, but found Node.js. Running via npx. Please execute 'npm install autorest -g' to install permanently."); -export const nodeNotFound = localize('nodeNotFound', "Neither autorest nor Node.js (npx) found in system path. Please install Node.js for autorest generation to work."); +export const nodeNotFound = localize('nodeNotFound', "Neither Autorest nor Node.js (npx) found in system path. Please install Node.js for Autorest generation to work."); +export const nodeButNotAutorestFoundPrompt = localize('nodeButNotAutorestFoundPrompt', "Autorest is not installed. To proceed, choose whether to run Autorest from a temporary location via 'npx' or install Autorest globally then run."); +export const installGlobally = localize('installGlobally', "Install globally"); +export const runViaNpx = localize('runViaNpx', "Run via npx"); + export const selectSpecFile = localize('selectSpecFile', "Select OpenAPI/Swagger spec file"); export function generatingProjectFailed(errorMessage: string) { return localize('generatingProjectFailed', "Generating project via AutoRest failed. Check output pane for more details. Error: {0}", errorMessage); } +export const noSqlFilesGenerated = localize('noSqlFilesGenerated', "No .sql files were generated by Autorest. Please confirm that your spec contains model definitions, or check the output log for details."); export function multipleMostDeploymentScripts(count: number) { return localize('multipleMostDeploymentScripts', "Unexpected number of {0} files: {1}", autorestPostDeploymentScriptName, count); } export const specSelectionText = localize('specSelectionText', "OpenAPI/Swagger spec"); diff --git a/extensions/sql-database-projects/src/controllers/projectController.ts b/extensions/sql-database-projects/src/controllers/projectController.ts index cd536d6518..b75701be26 100644 --- a/extensions/sql-database-projects/src/controllers/projectController.ts +++ b/extensions/sql-database-projects/src/controllers/projectController.ts @@ -913,8 +913,8 @@ export class ProjectsController { return { newProjectFolder, outputFolder, projectName }; } - public async generateAutorestFiles(specPath: string, newProjectFolder: string): Promise { - await this.autorestHelper.generateAutorestFiles(specPath, newProjectFolder); + public async generateAutorestFiles(specPath: string, newProjectFolder: string): Promise { + return this.autorestHelper.generateAutorestFiles(specPath, newProjectFolder); } public async openProjectInWorkspace(projectFilePath: string): Promise { @@ -940,7 +940,18 @@ export class ProjectsController { } // 3. run AutoRest to generate .sql files - await this.generateAutorestFiles(specPath, projectInfo.newProjectFolder); + const result = await this.generateAutorestFiles(specPath, projectInfo.newProjectFolder); + if (!result) { // user canceled operation when choosing how to run autorest + return; + } + + const fileFolderList: vscode.Uri[] | undefined = await this.getSqlFileList(projectInfo.newProjectFolder); + + if (!fileFolderList || fileFolderList.length === 0) { + void vscode.window.showInformationMessage(constants.noSqlFilesGenerated); + this._outputChannel.show(); + return; + } // 4. create new SQL project const newProjFilePath = await this.createNewProject({ @@ -952,7 +963,6 @@ export class ProjectsController { const project = await Project.openProject(newProjFilePath); // 5. add generated files to SQL project - let fileFolderList: vscode.Uri[] = await this.getSqlFileList(project.projectFolderPath); await project.addToProject(fileFolderList.filter(f => !f.fsPath.endsWith(constants.autorestPostDeploymentScriptName))); // Add generated file structure to the project const postDeploymentScript: vscode.Uri | undefined = this.findPostDeploymentScript(fileFolderList); @@ -987,17 +997,20 @@ export class ProjectsController { default: throw new Error(constants.multipleMostDeploymentScripts(results.length)); } - } - private async getSqlFileList(folder: string): Promise { + private async getSqlFileList(folder: string): Promise { + if (!(await utils.exists(folder))) { + return undefined; + } + const entries = await fs.readdir(folder, { withFileTypes: true }); const folders = entries.filter(dir => dir.isDirectory()).map(dir => path.join(folder, dir.name)); - const files = entries.filter(file => !file.isDirectory() && path.extname(file.name) === '.sql').map(file => vscode.Uri.file(path.join(folder, file.name))); + const files = entries.filter(file => !file.isDirectory() && path.extname(file.name) === constants.sqlFileExtension).map(file => vscode.Uri.file(path.join(folder, file.name))); for (const folder of folders) { - files.push(...await this.getSqlFileList(folder)); + files.push(...(await this.getSqlFileList(folder) ?? [])); } return files; diff --git a/extensions/sql-database-projects/src/test/autorestHelper.test.ts b/extensions/sql-database-projects/src/test/autorestHelper.test.ts index a6e378c661..4d9b54b86a 100644 --- a/extensions/sql-database-projects/src/test/autorestHelper.test.ts +++ b/extensions/sql-database-projects/src/test/autorestHelper.test.ts @@ -11,6 +11,8 @@ import * as path from 'path'; import { TestContext, createContext } from './testContext'; import { AutorestHelper } from '../tools/autorestHelper'; import { promises as fs } from 'fs'; +import { window } from 'vscode'; +import { runViaNpx } from '../common/constants'; let testContext: TestContext; @@ -24,12 +26,16 @@ describe('Autorest tests', function (): void { }); it('Should detect autorest', async function (): Promise { + sinon.stub(window, 'showInformationMessage').returns(Promise.resolve(runViaNpx)); // stub a selection in case test runner doesn't have autorest installed + const autorestHelper = new AutorestHelper(testContext.outputChannel); const executable = await autorestHelper.detectInstallation(); should(executable === 'autorest' || executable === 'npx autorest').equal(true, 'autorest command should be found in default path during unit tests'); }); it('Should run an autorest command successfully', async function (): Promise { + sinon.stub(window, 'showInformationMessage').returns(Promise.resolve(runViaNpx)); // stub a selection in case test runner doesn't have autorest installed + const autorestHelper = new AutorestHelper(testContext.outputChannel); const dummyFile = path.join(await testUtils.generateTestFolderPath(), 'testoutput.log'); sinon.stub(autorestHelper, 'constructAutorestCommand').returns(`${await autorestHelper.detectInstallation()} --version > ${dummyFile}`); @@ -47,12 +53,27 @@ describe('Autorest tests', function (): void { }); it('Should construct a correct autorest command for project generation', async function (): Promise { + const autorestHelper = new AutorestHelper(testContext.outputChannel); + sinon.stub(window, 'showInformationMessage').returns(Promise.resolve(runViaNpx)); // stub a selection in case test runner doesn't have autorest installed + sinon.stub(autorestHelper, 'detectInstallation').returns(Promise.resolve('autorest')); + const expectedOutput = 'autorest --use:autorest-sql-testing@latest --input-file="/some/path/test.yaml" --output-folder="/some/output/path" --clear-output-folder --verbose'; - const autorestHelper = new AutorestHelper(testContext.outputChannel); const constructedCommand = autorestHelper.constructAutorestCommand((await autorestHelper.detectInstallation())!, '/some/path/test.yaml', '/some/output/path'); // depending on whether the machine running the test has autorest installed or just node, the expected output may differ by just the prefix, hence matching against two options - should(constructedCommand === expectedOutput || constructedCommand === `npx ${expectedOutput}`).equal(true, `Constructed autorest command not formatting as expected:\nActual: ${constructedCommand}\nExpected: [npx ]${expectedOutput}`); + should(constructedCommand === expectedOutput).equal(true, `Constructed autorest command not formatting as expected:\nActual:\n\t${constructedCommand}\nExpected:\n\t${expectedOutput}`); + }); + + it('Should prompt user for action when autorest not found', async function (): Promise { + const promptStub = sinon.stub(window, 'showInformationMessage').returns(Promise.resolve()); + const detectStub = sinon.stub(utils, 'detectCommandInstallation'); + detectStub.withArgs('autorest').returns(Promise.resolve(false)); + detectStub.withArgs('npx').returns(Promise.resolve(true)); + + const autorestHelper = new AutorestHelper(testContext.outputChannel); + await autorestHelper.detectInstallation(); + + should(promptStub.calledOnce).be.true('User should have been prompted for how to run autorest because it wasn\'t found.'); }); }); diff --git a/extensions/sql-database-projects/src/test/projectController.test.ts b/extensions/sql-database-projects/src/test/projectController.test.ts index 3099b3c46d..1b2de12007 100644 --- a/extensions/sql-database-projects/src/test/projectController.test.ts +++ b/extensions/sql-database-projects/src/test/projectController.test.ts @@ -704,6 +704,7 @@ describe('ProjectsController', function (): void { projController.setup(x => x.generateAutorestFiles(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(async () => { await testUtils.createDummyFileStructure(true, fileList, newProjFolder); await testUtils.createTestFile('SELECT \'This is a post-deployment script\'', constants.autorestPostDeploymentScriptName, newProjFolder); + return 'some dummy console output'; }); projController.setup(x => x.openProjectInWorkspace(TypeMoq.It.isAny())).returns(async () => { }); diff --git a/extensions/sql-database-projects/src/tools/autorestHelper.ts b/extensions/sql-database-projects/src/tools/autorestHelper.ts index 3d5812498f..3ecd9b50ad 100644 --- a/extensions/sql-database-projects/src/tools/autorestHelper.ts +++ b/extensions/sql-database-projects/src/tools/autorestHelper.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { DoNotAskAgain, Install, nodeButNotAutorestFound, nodeNotFound } from '../common/constants'; +import { DoNotAskAgain, Install, nodeButNotAutorestFound, nodeNotFound, nodeButNotAutorestFoundPrompt, runViaNpx, installGlobally } from '../common/constants'; import * as utils from '../common/utils'; import * as semver from 'semver'; import { DBProjectConfigurationKey } from './netcoreTool'; @@ -48,7 +48,15 @@ export class AutorestHelper extends ShellExecutionHelper { if (await utils.detectCommandInstallation(npxCommand)) { this._outputChannel.appendLine(nodeButNotAutorestFound); - return `${npxCommand} ${autorestCommand}`; + + const response = await vscode.window.showInformationMessage(nodeButNotAutorestFoundPrompt, installGlobally, runViaNpx); + + if (response === installGlobally) { + await this.runStreamedCommand('npm install autorest -g', this._outputChannel); + return autorestCommand; + } else if (response === runViaNpx) { + return `${npxCommand} ${autorestCommand}`; + } } return undefined; @@ -63,7 +71,7 @@ export class AutorestHelper extends ShellExecutionHelper { public async generateAutorestFiles(specPath: string, outputFolder: string): Promise { const commandExecutable = await this.detectInstallation(); - if (commandExecutable === undefined) { + if (!commandExecutable) { // unable to find autorest or npx if (vscode.workspace.getConfiguration(DBProjectConfigurationKey)[nodejsDoNotAskAgainKey] !== true) {