diff --git a/packages/amazonq/test/e2e/amazonq/transformByQ.test.ts b/packages/amazonq/test/e2e/amazonq/transformByQ.test.ts index 8d7543211ba..69fc905c985 100644 --- a/packages/amazonq/test/e2e/amazonq/transformByQ.test.ts +++ b/packages/amazonq/test/e2e/amazonq/transformByQ.test.ts @@ -8,7 +8,7 @@ import { qTestingFramework } from './framework/framework' import sinon from 'sinon' import { Messenger } from './framework/messenger' import { JDKVersion, TransformationType, transformByQState } from 'aws-core-vscode/codewhisperer' -import { GumbyController, startTransformByQ, TabsStorage } from 'aws-core-vscode/amazonqGumby' +import { GumbyController, setMaven, startTransformByQ, TabsStorage } from 'aws-core-vscode/amazonqGumby' import { using, registerAuthHook, TestFolder } from 'aws-core-vscode/test' import { loginToIdC } from './utils/setup' import { fs } from 'aws-core-vscode/shared' @@ -338,7 +338,7 @@ describe('Amazon Q Code Transformation', function () { it('WHEN transforming a Java 8 project E2E THEN job is successful', async function () { transformByQState.setTransformationType(TransformationType.LANGUAGE_UPGRADE) - await startTransformByQ.setMaven() + await setMaven() await startTransformByQ.processLanguageUpgradeTransformFormInput(tempDir, JDKVersion.JDK8, JDKVersion.JDK17) await startTransformByQ.startTransformByQ() assert.strictEqual(transformByQState.getPolledJobStatus(), 'COMPLETED') diff --git a/packages/core/src/amazonqGumby/chat/controller/controller.ts b/packages/core/src/amazonqGumby/chat/controller/controller.ts index 3df09af40a4..e79ce883e0a 100644 --- a/packages/core/src/amazonqGumby/chat/controller/controller.ts +++ b/packages/core/src/amazonqGumby/chat/controller/controller.ts @@ -20,18 +20,14 @@ import { compileProject, finishHumanInTheLoop, getValidLanguageUpgradeCandidateProjects, - openBuildLogFile, - openHilPomFile, - parseBuildFile, postTransformationJob, processLanguageUpgradeTransformFormInput, processSQLConversionTransformFormInput, startTransformByQ, stopTransformByQ, validateCanCompileProject, - setMaven, getValidSQLConversionCandidateProjects, - validateSQLMetadataFile, + openHilPomFile, } from '../../../codewhisperer/commands/startTransformByQ' import { JDKVersion, TransformationCandidateProject, transformByQState } from '../../../codewhisperer/models/model' import { @@ -61,6 +57,12 @@ import { getStringHash } from '../../../shared/utilities/textUtilities' import { getVersionData } from '../../../codewhisperer/service/transformByQ/transformMavenHandler' import AdmZip from 'adm-zip' import { AuthError } from '../../../auth/sso/server' +import { + setMaven, + openBuildLogFile, + parseBuildFile, + validateSQLMetadataFile, +} from '../../../codewhisperer/service/transformByQ/transformFileHandler' import { getAuthType } from '../../../auth/utils' // These events can be interactions within the chat, diff --git a/packages/core/src/amazonqGumby/index.ts b/packages/core/src/amazonqGumby/index.ts index 16f64f7734a..8c1109f9997 100644 --- a/packages/core/src/amazonqGumby/index.ts +++ b/packages/core/src/amazonqGumby/index.ts @@ -9,4 +9,5 @@ export { default as MessengerUtils } from './chat/controller/messenger/messenger export { GumbyController } from './chat/controller/controller' export { TabsStorage } from '../amazonq/webview/ui/storages/tabsStorage' export * as startTransformByQ from '../../src/codewhisperer/commands/startTransformByQ' +export { setMaven } from '../../src/codewhisperer/service/transformByQ/transformFileHandler' export * from './errors' diff --git a/packages/core/src/codewhisperer/commands/startTransformByQ.ts b/packages/core/src/codewhisperer/commands/startTransformByQ.ts index e009616ab97..967a9a63218 100644 --- a/packages/core/src/codewhisperer/commands/startTransformByQ.ts +++ b/packages/core/src/codewhisperer/commands/startTransformByQ.ts @@ -5,8 +5,6 @@ import * as vscode from 'vscode' import * as fs from 'fs' // eslint-disable-line no-restricted-imports -import * as os from 'os' -import * as xml2js from 'xml2js' import path from 'path' import { getLogger } from '../../shared/logger' import * as CodeWhispererConstants from '../models/constants' @@ -18,7 +16,6 @@ import { FolderInfo, ZipManifest, TransformByQStatus, - DB, TransformationType, TransformationCandidateProject, } from '../models/model' @@ -56,7 +53,6 @@ import { MetadataResult } from '../../shared/telemetry/telemetryClient' import { submitFeedback } from '../../feedback/vue/submitFeedback' import { placeholder } from '../../shared/vscode/commands2' import { - AbsolutePathDetectedError, AlternateDependencyVersionsNotFoundError, JavaHomeNotSetError, JobStartError, @@ -71,6 +67,7 @@ import { getJsonValuesFromManifestFile, highlightPomIssueInProject, parseVersionsListFromPomFile, + setMaven, writeLogs, } from '../service/transformByQ/transformFileHandler' import { sleep } from '../../shared/utilities/timeoutUtils' @@ -81,7 +78,6 @@ import { setContext } from '../../shared/vscode/setContext' import { makeTemporaryToolkitFolder } from '../../shared' import globals from '../../shared/extensionGlobals' import { convertDateToTimestamp } from '../../shared/datetime' -import { isWin } from '../../shared/vscode/env' import { findStringInDirectory } from '../../shared/utilities/workspaceUtils' function getFeedbackCommentData() { @@ -111,63 +107,6 @@ export async function processSQLConversionTransformFormInput(pathToProject: stri // targetJDKVersion defaults to JDK17, the only supported version, which is fine } -export async function validateSQLMetadataFile(fileContents: string, message: any) { - try { - const sctData = await xml2js.parseStringPromise(fileContents) - const dbEntities = sctData['tree']['instances'][0]['ProjectModel'][0]['entities'][0] - const sourceDB = dbEntities['sources'][0]['DbServer'][0]['$']['vendor'].trim().toUpperCase() - const targetDB = dbEntities['targets'][0]['DbServer'][0]['$']['vendor'].trim().toUpperCase() - const sourceServerName = dbEntities['sources'][0]['DbServer'][0]['$']['name'].trim() - transformByQState.setSourceServerName(sourceServerName) - if (sourceDB !== DB.ORACLE) { - transformByQState.getChatMessenger()?.sendUnrecoverableErrorResponse('unsupported-source-db', message.tabID) - return false - } else if (targetDB !== DB.AURORA_POSTGRESQL && targetDB !== DB.RDS_POSTGRESQL) { - transformByQState.getChatMessenger()?.sendUnrecoverableErrorResponse('unsupported-target-db', message.tabID) - return false - } - transformByQState.setSourceDB(sourceDB) - transformByQState.setTargetDB(targetDB) - - const serverNodeLocations = - sctData['tree']['instances'][0]['ProjectModel'][0]['relations'][0]['server-node-location'] - const schemaNames = new Set() - serverNodeLocations.forEach((serverNodeLocation: any) => { - const schemaNodes = serverNodeLocation['FullNameNodeInfoList'][0]['nameParts'][0][ - 'FullNameNodeInfo' - ].filter((node: any) => node['$']['typeNode'].toLowerCase() === 'schema') - schemaNodes.forEach((node: any) => { - schemaNames.add(node['$']['nameNode'].toUpperCase()) - }) - }) - transformByQState.setSchemaOptions(schemaNames) // user will choose one of these - getLogger().info( - `CodeTransformation: Parsed .sct file with source DB: ${sourceDB}, target DB: ${targetDB}, source host name: ${sourceServerName}, and schema names: ${Array.from(schemaNames)}` - ) - } catch (err: any) { - getLogger().error('CodeTransformation: Error parsing .sct file. %O', err) - transformByQState.getChatMessenger()?.sendUnrecoverableErrorResponse('error-parsing-sct-file', message.tabID) - return false - } - return true -} - -export async function setMaven() { - let mavenWrapperExecutableName = isWin() ? 'mvnw.cmd' : 'mvnw' - const mavenWrapperExecutablePath = path.join(transformByQState.getProjectPath(), mavenWrapperExecutableName) - if (fs.existsSync(mavenWrapperExecutablePath)) { - if (mavenWrapperExecutableName === 'mvnw') { - mavenWrapperExecutableName = './mvnw' // add the './' for non-Windows - } else if (mavenWrapperExecutableName === 'mvnw.cmd') { - mavenWrapperExecutableName = '.\\mvnw.cmd' // add the '.\' for Windows - } - transformByQState.setMavenName(mavenWrapperExecutableName) - } else { - transformByQState.setMavenName('mvn') - } - getLogger().info(`CodeTransformation: using Maven ${transformByQState.getMavenName()}`) -} - async function validateJavaHome(): Promise { const versionData = await getVersionData() let javaVersionUsedByMaven = versionData[1] @@ -290,41 +229,6 @@ export async function finalizeTransformByQ(status: string) { } } -export async function parseBuildFile() { - try { - const absolutePaths = ['users/', 'system/', 'volumes/', 'c:\\', 'd:\\'] - const alias = path.basename(os.homedir()) - absolutePaths.push(alias) - const buildFilePath = path.join(transformByQState.getProjectPath(), 'pom.xml') - if (fs.existsSync(buildFilePath)) { - const buildFileContents = fs.readFileSync(buildFilePath).toString().toLowerCase() - const detectedPaths = [] - for (const absolutePath of absolutePaths) { - if (buildFileContents.includes(absolutePath)) { - detectedPaths.push(absolutePath) - } - } - if (detectedPaths.length > 0) { - const warningMessage = CodeWhispererConstants.absolutePathDetectedMessage( - detectedPaths.length, - path.basename(buildFilePath), - detectedPaths.join(', ') - ) - transformByQState.getChatControllers()?.errorThrown.fire({ - error: new AbsolutePathDetectedError(warningMessage), - tabID: ChatSessionManager.Instance.getSession().tabID, - }) - getLogger().info('CodeTransformation: absolute path potentially in build file') - return warningMessage - } - } - } catch (err: any) { - // swallow error - getLogger().error(`CodeTransformation: error scanning for absolute paths, tranformation continuing: ${err}`) - } - return undefined -} - export async function preTransformationUploadCode() { await vscode.commands.executeCommand('aws.amazonq.transformationHub.focus') @@ -499,12 +403,6 @@ export async function openHilPomFile() { ) } -export async function openBuildLogFile() { - const logFilePath = transformByQState.getPreBuildLogFilePath() - const doc = await vscode.workspace.openTextDocument(logFilePath) - await vscode.window.showTextDocument(doc) -} - export async function terminateHILEarly(jobID: string) { // Call resume with "REJECTED" state which will put our service // back into the normal flow and will not trigger HIL again for this step diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformFileHandler.ts b/packages/core/src/codewhisperer/service/transformByQ/transformFileHandler.ts index f6c5e24bed1..516471fc078 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformFileHandler.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformFileHandler.ts @@ -8,11 +8,15 @@ import * as path from 'path' import * as os from 'os' import xml2js = require('xml2js') import * as CodeWhispererConstants from '../../models/constants' -import { existsSync, writeFileSync } from 'fs' // eslint-disable-line no-restricted-imports -import { BuildSystem, FolderInfo, transformByQState } from '../../models/model' +import { existsSync, readFileSync, writeFileSync } from 'fs' // eslint-disable-line no-restricted-imports +import { BuildSystem, DB, FolderInfo, transformByQState } from '../../models/model' import { IManifestFile } from '../../../amazonqFeatureDev/models' import fs from '../../../shared/fs/fs' import globals from '../../../shared/extensionGlobals' +import { ChatSessionManager } from '../../../amazonqGumby/chat/storages/chatSession' +import { AbsolutePathDetectedError } from '../../../amazonqGumby/errors' +import { getLogger } from '../../../shared/logger' +import { isWin } from '../../../shared/vscode/env' export function getDependenciesFolderInfo(): FolderInfo { const dependencyFolderName = `${CodeWhispererConstants.dependencyFolderName}${globals.clock.Date.now()}` @@ -37,6 +41,104 @@ export async function checkBuildSystem(projectPath: string) { return BuildSystem.Unknown } +export async function parseBuildFile() { + try { + const absolutePaths = ['users/', 'system/', 'volumes/', 'c:\\', 'd:\\'] + const alias = path.basename(os.homedir()) + absolutePaths.push(alias) + const buildFilePath = path.join(transformByQState.getProjectPath(), 'pom.xml') + if (existsSync(buildFilePath)) { + const buildFileContents = readFileSync(buildFilePath).toString().toLowerCase() + const detectedPaths = [] + for (const absolutePath of absolutePaths) { + if (buildFileContents.includes(absolutePath)) { + detectedPaths.push(absolutePath) + } + } + if (detectedPaths.length > 0) { + const warningMessage = CodeWhispererConstants.absolutePathDetectedMessage( + detectedPaths.length, + path.basename(buildFilePath), + detectedPaths.join(', ') + ) + transformByQState.getChatControllers()?.errorThrown.fire({ + error: new AbsolutePathDetectedError(warningMessage), + tabID: ChatSessionManager.Instance.getSession().tabID, + }) + getLogger().info('CodeTransformation: absolute path potentially in build file') + return warningMessage + } + } + } catch (err: any) { + // swallow error + getLogger().error(`CodeTransformation: error scanning for absolute paths, tranformation continuing: ${err}`) + } + return undefined +} + +export async function validateSQLMetadataFile(fileContents: string, message: any) { + try { + const sctData = await xml2js.parseStringPromise(fileContents) + const dbEntities = sctData['tree']['instances'][0]['ProjectModel'][0]['entities'][0] + const sourceDB = dbEntities['sources'][0]['DbServer'][0]['$']['vendor'].trim().toUpperCase() + const targetDB = dbEntities['targets'][0]['DbServer'][0]['$']['vendor'].trim().toUpperCase() + const sourceServerName = dbEntities['sources'][0]['DbServer'][0]['$']['name'].trim() + transformByQState.setSourceServerName(sourceServerName) + if (sourceDB !== DB.ORACLE) { + transformByQState.getChatMessenger()?.sendUnrecoverableErrorResponse('unsupported-source-db', message.tabID) + return false + } else if (targetDB !== DB.AURORA_POSTGRESQL && targetDB !== DB.RDS_POSTGRESQL) { + transformByQState.getChatMessenger()?.sendUnrecoverableErrorResponse('unsupported-target-db', message.tabID) + return false + } + transformByQState.setSourceDB(sourceDB) + transformByQState.setTargetDB(targetDB) + + const serverNodeLocations = + sctData['tree']['instances'][0]['ProjectModel'][0]['relations'][0]['server-node-location'] + const schemaNames = new Set() + serverNodeLocations.forEach((serverNodeLocation: any) => { + const schemaNodes = serverNodeLocation['FullNameNodeInfoList'][0]['nameParts'][0][ + 'FullNameNodeInfo' + ].filter((node: any) => node['$']['typeNode'].toLowerCase() === 'schema') + schemaNodes.forEach((node: any) => { + schemaNames.add(node['$']['nameNode'].toUpperCase()) + }) + }) + transformByQState.setSchemaOptions(schemaNames) // user will choose one of these + getLogger().info( + `CodeTransformation: Parsed .sct file with source DB: ${sourceDB}, target DB: ${targetDB}, source host name: ${sourceServerName}, and schema names: ${Array.from(schemaNames)}` + ) + } catch (err: any) { + getLogger().error('CodeTransformation: Error parsing .sct file. %O', err) + transformByQState.getChatMessenger()?.sendUnrecoverableErrorResponse('error-parsing-sct-file', message.tabID) + return false + } + return true +} + +export async function setMaven() { + let mavenWrapperExecutableName = isWin() ? 'mvnw.cmd' : 'mvnw' + const mavenWrapperExecutablePath = path.join(transformByQState.getProjectPath(), mavenWrapperExecutableName) + if (existsSync(mavenWrapperExecutablePath)) { + if (mavenWrapperExecutableName === 'mvnw') { + mavenWrapperExecutableName = './mvnw' // add the './' for non-Windows + } else if (mavenWrapperExecutableName === 'mvnw.cmd') { + mavenWrapperExecutableName = '.\\mvnw.cmd' // add the '.\' for Windows + } + transformByQState.setMavenName(mavenWrapperExecutableName) + } else { + transformByQState.setMavenName('mvn') + } + getLogger().info(`CodeTransformation: using Maven ${transformByQState.getMavenName()}`) +} + +export async function openBuildLogFile() { + const logFilePath = transformByQState.getPreBuildLogFilePath() + const doc = await vscode.workspace.openTextDocument(logFilePath) + await vscode.window.showTextDocument(doc) +} + export async function createPomCopy( dirname: string, pomFileVirtualFileReference: vscode.Uri, diff --git a/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts b/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts index b6ff86a94cd..68bb795b54c 100644 --- a/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts +++ b/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts @@ -7,13 +7,7 @@ import assert, { fail } from 'assert' import * as vscode from 'vscode' import * as sinon from 'sinon' import { DB, transformByQState, TransformByQStoppedError } from '../../../codewhisperer/models/model' -import { - finalizeTransformationJob, - parseBuildFile, - setMaven, - stopTransformByQ, - validateSQLMetadataFile, -} from '../../../codewhisperer/commands/startTransformByQ' +import { stopTransformByQ, finalizeTransformationJob } from '../../../codewhisperer/commands/startTransformByQ' import { HttpResponse } from 'aws-sdk' import * as codeWhisperer from '../../../codewhisperer/client/codewhisperer' import * as CodeWhispererConstants from '../../../codewhisperer/models/constants' @@ -43,6 +37,11 @@ import { TransformationCandidateProject, ZipManifest } from '../../../codewhispe import globals from '../../../shared/extensionGlobals' import { env, fs } from '../../../shared' import { convertDateToTimestamp, convertToTimeString } from '../../../shared/datetime' +import { + setMaven, + parseBuildFile, + validateSQLMetadataFile, +} from '../../../codewhisperer/service/transformByQ/transformFileHandler' describe('transformByQ', function () { let tempDir: string