diff --git a/messages/sandboxbase.md b/messages/sandboxbase.md index c18581da..4e39a40b 100644 --- a/messages/sandboxbase.md +++ b/messages/sandboxbase.md @@ -1,11 +1,10 @@ # sandboxSuccess -The sandbox org %s was successful. +Your sandbox is ready. # sandboxSuccess.actions -The username for the sandbox is %s. -You can open the org by running "%s org open -o %s" +You can open it by running "%s org open -o %s" # checkSandboxStatus diff --git a/src/commands/org/create/sandbox.ts b/src/commands/org/create/sandbox.ts index daa6dd29..b00fd885 100644 --- a/src/commands/org/create/sandbox.ts +++ b/src/commands/org/create/sandbox.ts @@ -124,6 +124,7 @@ export default class CreateSandbox extends SandboxCommandBase { const lifecycle = Lifecycle.getInstance(); - this.prodOrg = this.flags['target-org']; - this.registerLifecycleListeners(lifecycle, { - isAsync: this.flags.async, - setDefault: this.flags['set-default'], - alias: this.flags.alias, - prodOrg: this.prodOrg, - tracksSource: this.flags['no-track-source'] === true ? false : undefined, - }); const sandboxReq = await this.createSandboxRequest(); await this.confirmSandboxReq({ ...sandboxReq, }); this.initSandboxProcessData(sandboxReq); - if (!this.flags.async) { - this.spinner.start('Sandbox Create'); - } + this.registerLifecycleListenersAndMSO(lifecycle, { + mso: { + title: 'Sandbox Create', + }, + isAsync: this.flags.async, + setDefault: this.flags['set-default'], + alias: this.flags.alias, + prodOrg: this.prodOrg, + tracksSource: this.flags['no-track-source'] === true ? false : undefined, + }); this.debug('Calling create with SandboxRequest: %s ', sandboxReq); @@ -217,12 +217,12 @@ export default class CreateSandbox extends SandboxCommandBase 0) { - this.spinner.start(`Resume ${this.sandboxRequestData.action ?? 'Create/Refresh'}`); - } - this.debug('Calling resume with ResumeSandboxRequest: %s ', sandboxReq); try { @@ -152,7 +153,6 @@ export default class ResumeSandbox extends SandboxCommandBase; const sce = entries.find(([, e]) => e?.sandboxProcessObject?.Id === this.flags['job-id'])?.[1]; sandboxRequestCacheEntry = sce; + if (sandboxRequestCacheEntry === undefined) { + this.warn( + `Could not find a cache entry for ${this.flags['job-id']}.${EOL}If you are resuming a sandbox operation from a different machine note that we cannot set the alias/set-default flag values as those are saved locally.` + ); + } } // If the action is in the cache entry, use it. if (sandboxRequestCacheEntry?.action) { this.action = sandboxRequestCacheEntry?.action; - this.sandboxProgress.action = sandboxRequestCacheEntry?.action; } return { diff --git a/src/shared/sandboxCommandBase.ts b/src/shared/sandboxCommandBase.ts index 84b3de53..b68adc50 100644 --- a/src/shared/sandboxCommandBase.ts +++ b/src/shared/sandboxCommandBase.ts @@ -5,7 +5,6 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import os from 'node:os'; - import { SfCommand } from '@salesforce/sf-plugins-core'; import { Config } from '@oclif/core'; import { @@ -21,8 +20,7 @@ import { SandboxUserAuthResponse, StatusEvent, } from '@salesforce/core'; -import { SandboxProgress } from './sandboxProgress.js'; -import { State } from './stagedProgress.js'; +import { SandboxStages } from './sandboxStages.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-org', 'sandboxbase'); @@ -32,7 +30,7 @@ export type SandboxCommandResponse = SandboxProcessObject & { }; export abstract class SandboxCommandBase extends SfCommand { - protected sandboxProgress: SandboxProgress; + protected stages!: SandboxStages; protected latestSandboxProgressObj?: SandboxProcessObject; protected sandboxAuth?: SandboxUserAuthResponse; protected prodOrg?: Org; @@ -47,10 +45,9 @@ export abstract class SandboxCommandBase extends SfCommand { this.action = this.constructor.name === 'RefreshSandbox' ? 'Refresh' - : this.constructor.name === 'CreateSandbox' + : ['CreateSandbox', 'ResumeSandbox'].includes(this.constructor.name) ? 'Create' : 'Create/Refresh'; - this.sandboxProgress = new SandboxProgress({ action: this.action }); } protected async getSandboxRequestConfig(): Promise { if (!this.sandboxRequestConfig) { @@ -85,72 +82,81 @@ export abstract class SandboxCommandBase extends SfCommand { return true; } - protected registerLifecycleListeners( + protected registerLifecycleListenersAndMSO( lifecycle: Lifecycle, - options: { isAsync: boolean; alias?: string; setDefault?: boolean; prodOrg?: Org; tracksSource?: boolean } + options: { + mso: { title: string; refresh?: boolean }; + isAsync: boolean; + alias?: string; + setDefault?: boolean; + prodOrg?: Org; + tracksSource?: boolean; + } ): void { + this.stages = new SandboxStages({ + refresh: options.mso.refresh ?? false, + jsonEnabled: this.jsonEnabled(), + title: options.isAsync ? `${options.mso.title} (async)` : options.mso.title, + }); + + this.stages.start(); + lifecycle.on('POLLING_TIME_OUT', async () => { this.pollingTimeOut = true; + this.stages.stop(); return Promise.resolve(this.updateSandboxRequestData()); }); lifecycle.on(SandboxEvents.EVENT_RESUME, async (results: SandboxProcessObject) => { + this.stages.start(); this.latestSandboxProgressObj = results; - this.sandboxProgress.markPreviousStagesAsCompleted( - results.Status !== 'Completed' ? results.Status : 'Authenticating' - ); + this.stages.update(this.latestSandboxProgressObj); + return Promise.resolve(this.updateSandboxRequestData()); }); - lifecycle.on(SandboxEvents.EVENT_ASYNC_RESULT, async (results?: SandboxProcessObject) => { - this.latestSandboxProgressObj = results ?? this.latestSandboxProgressObj; - this.updateSandboxRequestData(); - if (!options.isAsync) { - this.spinner.stop(); - } - // things that require data on latestSandboxProgressObj - if (this.latestSandboxProgressObj) { - const progress = this.sandboxProgress.getSandboxProgress({ - sandboxProcessObj: this.latestSandboxProgressObj, - sandboxRes: undefined, - }); - const currentStage = progress.status; - this.sandboxProgress.markPreviousStagesAsCompleted(currentStage); - this.updateStage(currentStage, 'inProgress'); - this.updateProgress( - { sandboxProcessObj: this.latestSandboxProgressObj, sandboxRes: undefined }, - options.isAsync - ); + lifecycle.on(SandboxEvents.EVENT_ASYNC_RESULT, async (results: SandboxProcessObject | undefined) => { + // this event is fired by commands on poll timeout without any payload, + // we want to make sure to only update state if there's payload (event from sfdx-core). + if (results) { + this.latestSandboxProgressObj = results; + this.stages.update(this.latestSandboxProgressObj); + this.updateSandboxRequestData(); } + + this.stages.stop('async'); if (this.pollingTimeOut) { this.warn(messages.getMessage('warning.ClientTimeoutWaitingForSandboxProcess', [this.action.toLowerCase()])); } - this.log(this.sandboxProgress.formatProgressStatus(false)); return Promise.resolve(this.info(messages.getMessage('checkSandboxStatus', this.getCheckSandboxStatusParams()))); }); lifecycle.on(SandboxEvents.EVENT_STATUS, async (results: StatusEvent) => { + // this starts MSO for: + // * org create/create sandbox + this.stages.start(); this.latestSandboxProgressObj = results.sandboxProcessObj; this.updateSandboxRequestData(); - const progress = this.sandboxProgress.getSandboxProgress(results); - const currentStage = progress.status; - this.updateStage(currentStage, 'inProgress'); - return Promise.resolve(this.updateProgress(results, options.isAsync)); + + this.stages.update(this.latestSandboxProgressObj); + + return Promise.resolve(); }); lifecycle.on(SandboxEvents.EVENT_AUTH, async (results: SandboxUserAuthResponse) => { + this.sandboxUsername = results.authUserName; + this.stages.auth(); this.sandboxAuth = results; return Promise.resolve(); }); lifecycle.on(SandboxEvents.EVENT_RESULT, async (results: ResultEvent) => { this.latestSandboxProgressObj = results.sandboxProcessObj; + this.sandboxUsername = results.sandboxRes.authUserName; this.updateSandboxRequestData(); - this.sandboxProgress.markPreviousStagesAsCompleted(); - this.updateProgress(results, options.isAsync); - if (!options.isAsync) { - this.progress.stop(); - } + + this.stages.update(results.sandboxProcessObj); + if (results.sandboxRes?.authUserName) { const authInfo = await AuthInfo.create({ username: results.sandboxRes?.authUserName }); await authInfo.handleAliasAndDefaultSettings({ @@ -160,8 +166,9 @@ export abstract class SandboxCommandBase extends SfCommand { setTracksSource: await this.calculateTrackingSetting(options.tracksSource), }); } + this.stages.stop(); + this.removeSandboxProgressConfig(); - this.updateProgress(results, options.isAsync); this.reportResults(results); }); @@ -185,43 +192,14 @@ export abstract class SandboxCommandBase extends SfCommand { } protected reportResults(results: ResultEvent): void { - this.log(); - this.styledHeader(`Sandbox Org ${this.action} Status`); - this.log(this.sandboxProgress.formatProgressStatus(false)); this.logSuccess( [ - messages.getMessage('sandboxSuccess', [this.action.toLowerCase()]), - messages.getMessages('sandboxSuccess.actions', [ - results.sandboxRes?.authUserName, - this.config.bin, - results.sandboxRes?.authUserName, - ]), + messages.getMessage('sandboxSuccess'), + messages.getMessages('sandboxSuccess.actions', [this.config.bin, results.sandboxRes?.authUserName]), ].join(os.EOL) ); } - protected updateProgress( - event: StatusEvent | (Omit & { sandboxRes?: ResultEvent['sandboxRes'] }), - isAsync: boolean - ): void { - const sandboxProgress = this.sandboxProgress.getSandboxProgress(event); - this.sandboxUsername = (event as ResultEvent).sandboxRes?.authUserName; - this.sandboxProgress.statusData = { - sandboxUsername: this.sandboxUsername, - sandboxProgress, - sandboxProcessObj: event.sandboxProcessObj, - }; - if (!isAsync) { - this.spinner.status = this.sandboxProgress.formatProgressStatus(); - } - } - - protected updateStage(stage: string | undefined, state: State): void { - if (stage) { - this.sandboxProgress.transitionStages(stage, state); - } - } - protected updateSandboxRequestData(): void { if (this.sandboxRequestData && this.latestSandboxProgressObj) { this.sandboxRequestData.sandboxProcessObject = this.latestSandboxProgressObj; @@ -262,6 +240,13 @@ export abstract class SandboxCommandBase extends SfCommand { return { ...(this.latestSandboxProgressObj as SandboxProcessObject), SandboxUsername: sbxUsername }; } + protected catch(error: Error): Promise { + if (this.stages) { + this.stages.stop('failed'); + } + + return super.catch(error); + } // eslint-disable-next-line @typescript-eslint/no-explicit-any protected async finally(_: Error | undefined): Promise { const lifecycle = Lifecycle.getInstance(); diff --git a/src/shared/sandboxProgress.ts b/src/shared/sandboxProgress.ts deleted file mode 100644 index 6b6a97c2..00000000 --- a/src/shared/sandboxProgress.ts +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright (c) 2020, salesforce.com, inc. - * All rights reserved. - * Licensed under the BSD 3-Clause license. - * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause - */ -import os from 'node:os'; -import { colorize } from '@oclif/core/ux'; -import { StatusEvent, ResultEvent, SandboxProcessObject } from '@salesforce/core'; -import { getClockForSeconds } from '../shared/timeUtils.js'; -import { StagedProgress } from './stagedProgress.js'; -import { isDefined } from './utils.js'; - -export type SandboxProgressData = { - id: string; - status: string; - percentComplete?: number; - remainingWaitTime: number; - remainingWaitTimeHuman: string; -}; - -export type SandboxStatusData = { - sandboxUsername: string; - sandboxProgress: SandboxProgressData; - sandboxProcessObj?: SandboxProcessObject | undefined; -}; - -export type SandboxProgressConfig = { - stageNames?: string[]; - action?: 'Create' | 'Refresh' | 'Create/Refresh'; -}; - -export class SandboxProgress extends StagedProgress { - public action: SandboxProgressConfig['action']; - - public constructor(config?: SandboxProgressConfig) { - const stageNames = config?.stageNames ?? ['Pending', 'Processing', 'Activating', 'Authenticating']; - super(stageNames); - this.action = config?.action ?? 'Create/Refresh'; - } - // eslint-disable-next-line class-methods-use-this - public getLogSandboxProcessResult(result: ResultEvent): string { - const { sandboxProcessObj } = result; - const sandboxReadyForUse = `Sandbox ${sandboxProcessObj.SandboxName}(${sandboxProcessObj.Id}) is ready for use.`; - return sandboxReadyForUse; - } - - // eslint-disable-next-line class-methods-use-this - public getSandboxProgress( - // sometimes an undefined sandboxRes is passed in - event: StatusEvent | (Omit & { sandboxRes?: ResultEvent['sandboxRes'] }) - ): SandboxProgressData { - const waitingOnAuth = 'waitingOnAuth' in event ? event.waitingOnAuth : false; - const { sandboxProcessObj } = event; - const waitTimeInSec = 'remainingWait' in event ? event.remainingWait ?? 0 : 0; - - const sandboxIdentifierMsg = `${sandboxProcessObj.SandboxName}(${sandboxProcessObj.Id})`; - - return { - id: sandboxIdentifierMsg, - status: waitingOnAuth || sandboxProcessObj.Status === 'Completed' ? 'Authenticating' : sandboxProcessObj.Status, - percentComplete: sandboxProcessObj.CopyProgress, - remainingWaitTime: waitTimeInSec, - remainingWaitTimeHuman: waitTimeInSec === 0 ? '' : `${getClockForSeconds(waitTimeInSec)} until timeout.`, - }; - } - - public formatProgressStatus(withClock = true): string { - const table = getSandboxTableAsText(undefined, this.statusData?.sandboxProcessObj); - return [ - withClock && this.statusData - ? `${getClockForSeconds(this.statusData.sandboxProgress.remainingWaitTime)} until timeout. ${ - this.statusData.sandboxProgress.percentComplete ?? 0 - }%` - : undefined, - table, - '---------------------', - `Sandbox ${this.action ?? ''} Stages`, - this.formatStages(), - ] - .filter(isDefined) - .join(os.EOL); - } - // eslint-disable-next-line class-methods-use-this - protected mapCurrentStage(currentStage: string): string { - switch (currentStage) { - case 'Pending Remote Creation': - return 'Pending'; - case 'Remote Sandbox Created': - return 'Pending'; - case 'Completed': - return 'Authenticating'; - default: - return currentStage; - } - } -} - -export const getTableDataFromProcessObj = ( - sandboxProcessObj: SandboxProcessObject, - authUserName?: string | undefined -): Array<{ key: string; value: string | number }> => [ - { key: 'Id', value: sandboxProcessObj.Id }, - { key: 'SandboxName', value: sandboxProcessObj.SandboxName }, - { key: 'Status', value: sandboxProcessObj.Status }, - { key: 'LicenseType', value: sandboxProcessObj.LicenseType }, - { key: 'SandboxInfoId', value: sandboxProcessObj.SandboxInfoId }, - { key: 'Created Date', value: sandboxProcessObj.CreatedDate }, - { key: 'CopyProgress', value: `${sandboxProcessObj.CopyProgress ?? 0}%` }, - ...(sandboxProcessObj.SourceId ? [{ key: 'SourceId', value: sandboxProcessObj.SourceId }] : []), - ...(sandboxProcessObj.SandboxOrganization - ? [{ key: 'SandboxOrg', value: sandboxProcessObj.SandboxOrganization }] - : []), - ...(sandboxProcessObj.ApexClassId ? [{ key: 'ApexClassId', value: sandboxProcessObj.ApexClassId }] : []), - ...(sandboxProcessObj.Description ? [{ key: 'Description', value: sandboxProcessObj.Description }] : []), - ...(authUserName ? [{ key: 'Authorized Sandbox Username', value: authUserName }] : []), -]; - -export const getSandboxTableAsText = (sandboxUsername?: string, sandboxProgress?: SandboxProcessObject): string => { - if (!sandboxProgress) { - return ''; - } - - const data = getTableDataFromProcessObj(sandboxProgress, sandboxUsername); - const longestKey = data.reduce((acc, row) => (row.key.length > acc ? row.key.length : acc), 0); - const longestValue = data.reduce( - (acc, row) => (row.value.toString().length > acc ? row.value.toString().length : acc), - 0 - ); - return [ - colorize('bold', `${'Field'.padEnd(longestKey)} Value`), - `${'-'.repeat(longestKey)} ${'-'.repeat(longestValue)}`, - ...data.map((row) => `${row.key.padEnd(longestKey)} ${row.value}`), - ].join(os.EOL); -}; diff --git a/src/shared/sandboxReporter.ts b/src/shared/sandboxReporter.ts deleted file mode 100644 index 6ca2f606..00000000 --- a/src/shared/sandboxReporter.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2020, salesforce.com, inc. - * All rights reserved. - * Licensed under the BSD 3-Clause license. - * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause - */ -import { StatusEvent, ResultEvent } from '@salesforce/core'; -import { getSecondsToHuman } from './timeUtils.js'; - -export class SandboxReporter { - public static sandboxProgress(update: StatusEvent): string { - const { remainingWait, interval, sandboxProcessObj, waitingOnAuth } = update; - - const waitTime: string = getSecondsToHuman(remainingWait); - const waitTimeMsg = `Sleeping ${interval} seconds. Will wait ${waitTime} more before timing out.`; - const sandboxIdentifierMsg = `${sandboxProcessObj.SandboxName}(${sandboxProcessObj.Id})`; - const waitingOnAuthMessage: string = waitingOnAuth ? ', waiting on JWT auth' : ''; - const completionMessage = sandboxProcessObj.CopyProgress - ? `(${sandboxProcessObj.CopyProgress}% completed${waitingOnAuthMessage})` - : ''; - - return `Sandbox request ${sandboxIdentifierMsg} is ${sandboxProcessObj.Status} ${completionMessage}. ${waitTimeMsg}`; - } - - public static logSandboxProcessResult( - result: ResultEvent - // sandboxProcessObj.CopyProgress is a number - ): { sandboxReadyForUse: string; data: Array<{ key: string; value: string | number | undefined }> } { - const { sandboxProcessObj, sandboxRes } = result; - const sandboxReadyForUse = `Sandbox ${sandboxProcessObj.SandboxName}(${sandboxProcessObj.Id}) is ready for use.`; - - const data = [ - { key: 'Id', value: sandboxProcessObj.Id }, - { key: 'SandboxName', value: sandboxProcessObj.SandboxName }, - { key: 'Status', value: sandboxProcessObj.Status }, - { key: 'CopyProgress', value: sandboxProcessObj.CopyProgress }, - { key: 'Description', value: sandboxProcessObj.Description }, - { key: 'LicenseType', value: sandboxProcessObj.LicenseType }, - { key: 'SandboxInfoId', value: sandboxProcessObj.SandboxInfoId }, - { key: 'SourceId', value: sandboxProcessObj.SourceId }, - { key: 'SandboxOrg', value: sandboxProcessObj.SandboxOrganization }, - { key: 'Created Date', value: sandboxProcessObj.CreatedDate }, - { key: 'ApexClassId', value: sandboxProcessObj.ApexClassId }, - { key: 'Authorized Sandbox Username', value: sandboxRes.authUserName }, - ]; - - return { sandboxReadyForUse, data }; - } -} diff --git a/src/shared/sandboxStages.ts b/src/shared/sandboxStages.ts new file mode 100644 index 00000000..9b4ec1bf --- /dev/null +++ b/src/shared/sandboxStages.ts @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2025, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +import { MultiStageOutput } from '@oclif/multi-stage-output'; +import { SandboxProcessObject } from '@salesforce/core'; +import { StageStatus } from 'node_modules/@oclif/multi-stage-output/lib/stage-tracker.js'; + +type Options = { + title: string; + jsonEnabled: boolean; + refresh: boolean; +}; + +export class SandboxStages { + private mso: MultiStageOutput; + private refresh: boolean; + + public constructor({ title, jsonEnabled, refresh = false }: Options) { + this.refresh = refresh; + this.mso = new MultiStageOutput({ + stages: ['Creating new sandbox', 'Refreshing org', 'Authenticating'], + title, + jsonEnabled, + stageSpecificBlock: [ + { + get: (data): string | undefined => data?.SandboxName, + stage: 'Creating new sandbox', + label: 'Name', + type: 'dynamic-key-value', + }, + { + get: (data): string | undefined => data?.Id, + stage: 'Creating new sandbox', + label: 'ID', + type: 'dynamic-key-value', + }, + ], + postStagesBlock: [ + { + label: 'Status', + get: (data): string | undefined => data?.Status, + type: 'dynamic-key-value', + bold: true, + }, + { + label: 'Copy progress', + get: (data): string | undefined => `${data?.CopyProgress ?? 0}%`, + type: 'dynamic-key-value', + }, + ], + }); + } + + public start(): void { + if (this.refresh) { + this.mso.skipTo('Refreshing org'); + } else { + this.mso.goto('Creating new sandbox'); + } + } + + public auth(): void { + this.mso.goto('Authenticating'); + } + + public update(data: SandboxProcessObject): void { + this.mso.updateData(data); + } + + public stop(finalStatus?: StageStatus): void { + this.mso.stop(finalStatus); + } +} diff --git a/src/shared/stagedProgress.ts b/src/shared/stagedProgress.ts deleted file mode 100644 index 02be37fe..00000000 --- a/src/shared/stagedProgress.ts +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright (c) 2020, salesforce.com, inc. - * All rights reserved. - * Licensed under the BSD 3-Clause license. - * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause - */ -import os from 'node:os'; -import ansis, { type Ansis } from 'ansis'; -import { StandardColors } from '@salesforce/sf-plugins-core'; -import { SfError } from '@salesforce/core'; - -const compareStages = ([, aValue]: [string, StageAttributes], [, bValue]: [string, StageAttributes]): number => - aValue.index - bValue.index; - -export const boldPurple = ansis.rgb(157, 129, 221).bold; - -export type State = 'inProgress' | 'completed' | 'failed' | 'unknown'; - -export type StageAttributes = { - state: State; - char: string; - color: Ansis; - index: number; - visited: boolean; -}; - -export const StateConstants: { [stage: string]: Omit } = { - inProgress: { color: boldPurple, char: '…', visited: false, state: 'inProgress' }, - completed: { color: StandardColors.success, char: '✓', visited: false, state: 'completed' }, - failed: { color: ansis.bold.red, char: '✖', visited: false, state: 'failed' }, - unknown: { color: ansis.dim, char: '…', visited: false, state: 'unknown' }, -}; - -export type Stage = { - [stage: string]: StageAttributes; -}; - -export abstract class StagedProgress { - private dataForTheStatus: T | undefined; - private theStages: Stage; - private currentStage?: string; - private previousStage?: string; - public constructor(stages: string[]) { - this.theStages = stages - .map((stage, index) => ({ - [stage]: { ...StateConstants['unknown'], index: (index + 1) * 10 }, - })) - .reduce((m, b) => Object.assign(m, b), {}); - } - - public get statusData(): T | undefined { - return this.dataForTheStatus; - } - public set statusData(statusData: T | undefined) { - this.dataForTheStatus = statusData; - } - - public formatStages(): string { - return Object.entries(this.theStages) - .sort(compareStages) - .map(([stage, stageState]) => stageState.color(`${stageState.char} - ${stage}`)) - .join(os.EOL); - } - - public transitionStages(currentStage: string, newState: State): void { - currentStage = this.mapCurrentStage(currentStage); - if (this.previousStage && this.previousStage !== currentStage) { - this.updateStages(this.previousStage, 'completed'); - } - - // mark all previous stages as visited and completed - this.markPreviousStagesAsCompleted(currentStage); - - this.previousStage = currentStage; - this.currentStage = currentStage; - this.updateStages(currentStage, newState); - } - - public markPreviousStagesAsCompleted(currentStage?: string): void { - if (currentStage) { - currentStage = this.mapCurrentStage(currentStage); - } - Object.entries(this.theStages).forEach(([stage, stageState]) => { - if (!currentStage || stageState.index < (this.theStages[currentStage]?.index ?? 0)) { - this.updateStages(stage, 'completed'); - } - }); - } - - public updateCurrentStage(newState: State): void { - if (!this.currentStage) { - throw new SfError('transitionStages must be called before updateCurrentStage'); - } - this.updateStages(this.currentStage, newState); - } - - public updateStages(currentStage: string, newState?: State): void { - currentStage = this.mapCurrentStage(currentStage); - if (!this.theStages[currentStage]) { - const sortedEntries = Object.entries(this.theStages).sort(compareStages); - const visitedEntries = sortedEntries.filter(([, stageState]) => stageState.visited); - const [, lastState] = visitedEntries.length - ? visitedEntries[visitedEntries.length - 1] - : ['', { state: StateConstants.unknown.state, index: 0, visited: true }]; - const newEntry = { - [currentStage]: { state: StateConstants.unknown.state, visited: true, index: lastState.index + 1 }, - }; - this.theStages = Object.assign(this.theStages, newEntry); - } - this.theStages[currentStage].visited = true; - this.theStages[currentStage].state = newState ?? 'inProgress'; - this.theStages[currentStage].char = StateConstants[this.theStages[currentStage].state].char; - if (newState) { - this.theStages[currentStage].color = StateConstants[newState.toString()].color; - } - } - - public getStages(): Stage { - return this.theStages; - } - - // eslint-disable-next-line class-methods-use-this - protected mapCurrentStage(currentStage: string): string { - return currentStage; - } - - public abstract formatProgressStatus(withClock: boolean): string; -} diff --git a/src/shared/timeUtils.ts b/src/shared/timeUtils.ts deleted file mode 100644 index 6e6d309c..00000000 --- a/src/shared/timeUtils.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2022, salesforce.com, inc. - * All rights reserved. - * Licensed under the BSD 3-Clause license. - * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause - */ -import { Duration } from '@salesforce/kit'; - -export type TimeComponents = { - days: Duration; - hours: Duration; - minutes: Duration; - seconds: Duration; -}; - -export const getClockForSeconds = (timeInSec: number): string => { - const tc = getTimeComponentsFromSeconds(timeInSec); - - const dDisplay: string = tc.days.days > 0 ? `${tc.days.days.toString()}:` : ''; - const hDisplay: string = tc.hours.hours.toString().padStart(2, '0'); - const mDisplay: string = tc.minutes.minutes.toString().padStart(2, '0'); - const sDisplay: string = tc.seconds.seconds.toString().padStart(2, '0'); - - return `${dDisplay}${hDisplay}:${mDisplay}:${sDisplay}`; -}; -export const getTimeComponentsFromSeconds = (timeInSec: number): TimeComponents => { - const days = Duration.days(Math.floor(timeInSec / 86_400)); - const hours = Duration.hours(Math.floor((timeInSec % 86_400) / 3600)); - const minutes = Duration.minutes(Math.floor((timeInSec % 3600) / 60)); - const seconds = Duration.seconds(Math.floor(timeInSec % 60)); - - return { days, hours, minutes, seconds }; -}; -export const getSecondsToHuman = (timeInSec: number): string => { - const tc = getTimeComponentsFromSeconds(timeInSec); - - const dDisplay: string = tc.days.days > 0 ? tc.days.toString() + ' ' : ''; - const hDisplay: string = tc.hours.hours > 0 ? tc.hours.toString() + ' ' : ''; - const mDisplay: string = tc.minutes.minutes > 0 ? tc.minutes.toString() + ' ' : ''; - const sDisplay: string = tc.seconds.seconds > 0 ? tc.seconds.toString() : ''; - - return (dDisplay + hDisplay + mDisplay + sDisplay).trim(); -}; diff --git a/test/nut/sandbox.sandboxNut.ts b/test/nut/sandbox.sandboxNut.ts index 54db8f85..a11af0f6 100644 --- a/test/nut/sandbox.sandboxNut.ts +++ b/test/nut/sandbox.sandboxNut.ts @@ -29,13 +29,13 @@ describe('Sandbox Orgs', () => { it('will create a sandbox, verify it can be opened, and then attempt to delete it', () => { let result: SandboxProcessObject | undefined; try { - Lifecycle.getInstance().on(SandboxEvents.EVENT_STATUS, async (results: StatusEvent) => + Lifecycle.getInstance().on(SandboxEvents.EVENT_ASYNC_RESULT, async (results: StatusEvent) => // eslint-disable-next-line no-console Promise.resolve(console.log('sandbox copy progress', results.sandboxProcessObj.CopyProgress)) ); let rawResult = execCmd( `env:create:sandbox -a mySandbox -s -l Developer -o ${hubOrgUsername} --no-prompt --json --async`, - { timeout: 3_600_000 } + { timeout: 3_600_000, ensureExitCode: 68 } ); result = rawResult.jsonOutput?.result as SandboxProcessObject; // autogenerated sandbox names start with 'sbx' @@ -48,7 +48,9 @@ describe('Sandbox Orgs', () => { result = rawResult.jsonOutput?.result as SandboxProcessObject; expect(result).to.be.ok; } catch (e) { - expect(false).to.be.true(JSON.stringify(e)); + // eslint-disable-next-line no-console + console.error(e); + expect(false, 'catch verification').to.be.true(JSON.stringify(e)); } assert(result); diff --git a/test/shared/sandboxProgress.test.ts b/test/shared/sandboxProgress.test.ts deleted file mode 100644 index cdb59700..00000000 --- a/test/shared/sandboxProgress.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (c) 2020, salesforce.com, inc. - * All rights reserved. - * Licensed under the BSD 3-Clause license. - * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause - */ -import { expect } from 'chai'; -import { Duration } from '@salesforce/cli-plugins-testkit'; -import { SandboxProcessObject, StatusEvent } from '@salesforce/core'; -import { - SandboxProgress, - getTableDataFromProcessObj, - getSandboxTableAsText, -} from '../../src/shared/sandboxProgress.js'; - -const sandboxProcessObj: SandboxProcessObject = { - Id: '0GR4p000000U8EMXXX', - Status: 'Completed', - SandboxName: 'TestSandbox', - SandboxInfoId: '0GQ4p000000U6sKXXX', - LicenseType: 'DEVELOPER', - CreatedDate: '2021-12-07T16:20:21.000+0000', - CopyProgress: 100, - SandboxOrganization: '00D2f0000008XXX', - SourceId: '123', - Description: 'sandbox description', - ApexClassId: '123', - EndDate: '2021-12-07T16:38:47.000+0000', -}; - -describe('sandbox progress', () => { - let sandboxProgress: SandboxProgress; - beforeEach(() => { - sandboxProgress = new SandboxProgress(); - }); - describe('getSandboxProgress', () => { - it('will calculate the correct human readable message (1h 33min 00seconds seconds left)', () => { - const data: StatusEvent = { - // 186*30 = 5580 = 1 hour, 33 min, 0 seconds. so 186 attempts left, at a 30 second polling interval - sandboxProcessObj, - interval: 30, - remainingWait: Duration.minutes(93).seconds, - waitingOnAuth: false, - }; - const res = sandboxProgress.getSandboxProgress(data); - expect(res).to.have.property('id', 'TestSandbox(0GR4p000000U8EMXXX)'); - expect(res).to.have.property('status', 'Authenticating'); - expect(res).to.have.property('percentComplete', 100); - expect(res).to.have.property('remainingWaitTimeHuman', '01:33:00 until timeout.'); - }); - - it('will calculate the correct human readable message (5 min 30seconds seconds left)', () => { - const data: StatusEvent = { - sandboxProcessObj, - interval: 30, - remainingWait: Duration.minutes(5).seconds + Duration.seconds(30).seconds, - waitingOnAuth: false, - }; - const res = sandboxProgress.getSandboxProgress(data); - expect(res).to.have.property('id', 'TestSandbox(0GR4p000000U8EMXXX)'); - expect(res).to.have.property('status', 'Authenticating'); - expect(res).to.have.property('percentComplete', 100); - expect(res).to.have.property('remainingWaitTimeHuman', '00:05:30 until timeout.'); - }); - }); - - describe('getTableDataFromProcessObj', () => { - it('getTableDataFromProcessObj should work', () => { - const tableData = getTableDataFromProcessObj(sandboxProcessObj, 'admin@prod.org.sandbox'); - expect(tableData.find((r) => r.key === 'Authorized Sandbox Username')).to.have.property( - 'value', - 'admin@prod.org.sandbox' - ); - expect(tableData.find((r) => r.key === 'SandboxInfoId')).to.have.property('value', '0GQ4p000000U6sKXXX'); - }); - }); - describe('getSandboxTableAsText', () => { - it('getSandboxTableAsText should work', () => { - const tableData = getSandboxTableAsText('admin@prod.org.sandbox', sandboxProcessObj); - expect(tableData).to.include('Authorized Sandbox Username'); - expect(tableData).to.include('admin@prod.org.sandbox'); - }); - }); -}); diff --git a/test/shared/sandboxReporter.test.ts b/test/shared/sandboxReporter.test.ts deleted file mode 100644 index d5014819..00000000 --- a/test/shared/sandboxReporter.test.ts +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright (c) 2020, salesforce.com, inc. - * All rights reserved. - * Licensed under the BSD 3-Clause license. - * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause - */ - -import { expect, config } from 'chai'; -import { StatusEvent, SandboxProcessObject, SandboxUserAuthResponse } from '@salesforce/core'; -import { SandboxReporter } from '../../src/shared/sandboxReporter.js'; - -config.truncateThreshold = 0; - -const sandboxProcessObj: SandboxProcessObject = { - Id: '0GR4p000000U8EMXXX', - Status: 'Completed', - SandboxName: 'TestSandbox', - SandboxInfoId: '0GQ4p000000U6sKXXX', - LicenseType: 'DEVELOPER', - CreatedDate: '2021-12-07T16:20:21.000+0000', - CopyProgress: 100, - SandboxOrganization: '00D2f0000008XXX', - SourceId: '123', - Description: 'sandbox description', - ApexClassId: '123', - EndDate: '2021-12-07T16:38:47.000+0000', -}; - -describe('sandboxReporter', () => { - describe('sandboxProgress', () => { - it('will calculate the correct human readable message (1h 33min 00seconds seconds left)', () => { - const data = { - sandboxProcessObj, - interval: 30, - remainingWait: 5580, - waitingOnAuth: false, - }; - const res = SandboxReporter.sandboxProgress(data); - expect(res).to.equal( - 'Sandbox request TestSandbox(0GR4p000000U8EMXXX) is Completed (100% completed). Sleeping 30 seconds. Will wait 1 hour 33 minutes more before timing out.' - ); - }); - - it('will calculate the correct human readable message (5 min 30seconds seconds left)', () => { - const data: StatusEvent = { - sandboxProcessObj, - interval: 30, - remainingWait: 330, - waitingOnAuth: false, - }; - const res = SandboxReporter.sandboxProgress(data); - expect(res).to.equal( - 'Sandbox request TestSandbox(0GR4p000000U8EMXXX) is Completed (100% completed). Sleeping 30 seconds. Will wait 5 minutes 30 seconds more before timing out.' - ); - }); - }); - - describe('logSandboxProcessResult', () => { - it('sandboxCreate EVENT_RESULT', () => { - const sandboxRes: SandboxUserAuthResponse = { - authCode: 'sandboxTestAuthCode', - authUserName: 'newSandboxUsername', - instanceUrl: 'https://login.salesforce.com', - loginUrl: 'https://productionOrg--createdSandbox.salesforce.com/', - }; - - const data = { sandboxProcessObj, sandboxRes }; - expect(SandboxReporter.logSandboxProcessResult(data)).to.deep.equal({ - sandboxReadyForUse: 'Sandbox TestSandbox(0GR4p000000U8EMXXX) is ready for use.', - data: [ - { - key: 'Id', - value: '0GR4p000000U8EMXXX', - }, - { - key: 'SandboxName', - value: 'TestSandbox', - }, - { - key: 'Status', - value: 'Completed', - }, - { - key: 'CopyProgress', - value: 100, - }, - { - key: 'Description', - value: 'sandbox description', - }, - { - key: 'LicenseType', - value: 'DEVELOPER', - }, - { - key: 'SandboxInfoId', - value: '0GQ4p000000U6sKXXX', - }, - { - key: 'SourceId', - value: '123', - }, - { - key: 'SandboxOrg', - value: '00D2f0000008XXX', - }, - { - key: 'Created Date', - value: '2021-12-07T16:20:21.000+0000', - }, - { - key: 'ApexClassId', - value: '123', - }, - { - key: 'Authorized Sandbox Username', - value: 'newSandboxUsername', - }, - ], - }); - }); - }); -}); diff --git a/test/shared/stagedProgress.test.ts b/test/shared/stagedProgress.test.ts deleted file mode 100644 index 4c69f96e..00000000 --- a/test/shared/stagedProgress.test.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (c) 2020, salesforce.com, inc. - * All rights reserved. - * Licensed under the BSD 3-Clause license. - * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause - */ -import { expect } from 'chai'; -import ansis from 'ansis'; -import { StagedProgress, StateConstants } from '../../src/shared/stagedProgress.js'; -import { SandboxStatusData } from '../../src/shared/sandboxProgress.js'; - -class TestStagedProgress extends StagedProgress { - // eslint-disable-next-line class-methods-use-this - public formatProgressStatus(): string { - return ''; - } -} - -describe('stagedProgress', () => { - let stagedProgress: TestStagedProgress; - describe('updateStages', () => { - beforeEach(() => { - stagedProgress = new TestStagedProgress(['Pending', 'Processing', 'Activating', 'Completed', 'Authenticating']); - }); - it('should update existing stage', () => { - stagedProgress.updateStages('Pending', 'failed'); - const pendingStage = stagedProgress.getStages()['Pending']; - expect(pendingStage).to.be.ok; - expect(pendingStage.state).to.equal('failed'); - }); - it('should insert new stage at beginning of stages', () => { - stagedProgress.updateStages('Creating', 'inProgress'); - const creatingStage = stagedProgress.getStages()['Creating']; - const pendingStage = stagedProgress.getStages()['Pending']; - expect(creatingStage).to.be.ok; - expect(creatingStage.state).to.equal('inProgress'); - expect(creatingStage.index).to.be.lessThan(pendingStage.index); - }); - it('should insert new stage at end of stages', () => { - const stages = stagedProgress.getStages(); - Object.keys(stages).forEach((stage) => { - stages[stage].visited = true; - stages[stage].state = 'completed'; - }); - stagedProgress.updateStages('Past the End', 'inProgress'); - const pastTheEnd = stagedProgress.getStages()['Past the End']; - const authenticatingStage = stagedProgress.getStages()['Authenticating']; - expect(pastTheEnd).to.be.ok; - expect(pastTheEnd.state).to.equal('inProgress'); - expect(pastTheEnd.index).to.be.greaterThan(authenticatingStage.index); - }); - it('should insert new stage after Processing', () => { - const stages = stagedProgress.getStages(); - Object.keys(stages).forEach((stage) => { - if (['Pending', 'Processing'].includes(stage)) { - stages[stage].visited = true; - stages[stage].state = 'completed'; - } - }); - stagedProgress.updateStages('After Processing', 'inProgress'); - const afterProcessStage = stagedProgress.getStages()['After Processing']; - const processingStage = stagedProgress.getStages()['Processing']; - expect(afterProcessStage).to.be.ok; - expect(afterProcessStage.state).to.equal('inProgress'); - expect(afterProcessStage.index).to.be.equal(processingStage.index + 1); - }); - }); - describe('getFormattedStages', () => { - beforeEach(() => { - stagedProgress = new TestStagedProgress(['Pending', 'Processing', 'Activating', 'Completed', 'Authenticating']); - }); - it('should get formatted stages - all unknown', () => { - const formattedStages = stagedProgress.formatStages(); - expect(formattedStages).to.be.ok; - expect(formattedStages).to.include(StateConstants.unknown.char); - expect(formattedStages).to.include(ansis.dim('')); - }); - it('should get formatted stages - pending in progress', () => { - stagedProgress.updateStages('Pending', 'inProgress'); - const formattedStages = stagedProgress.formatStages(); - expect(formattedStages).to.be.ok; - expect(formattedStages).to.include(StateConstants.unknown.char); - expect(formattedStages).to.include( - StateConstants.inProgress.color(`${StateConstants.inProgress.char} - Pending`) - ); - }); - it('should get formatted stages - pending successful', () => { - stagedProgress.updateStages('Pending', 'completed'); - stagedProgress.updateStages('Processing', 'inProgress'); - const formattedStages = stagedProgress.formatStages(); - expect(formattedStages).to.be.ok; - expect(formattedStages).to.include(StateConstants.unknown.char); - expect(formattedStages).to.include(StateConstants.completed.color(`${StateConstants.completed.char} - Pending`)); - }); - it('should get formatted stages - processing failed', () => { - stagedProgress.updateStages('Pending', 'completed'); - stagedProgress.updateStages('Processing', 'failed'); - const formattedStages = stagedProgress.formatStages(); - expect(formattedStages).to.be.ok; - expect(formattedStages).to.include(StateConstants.unknown.char); - expect(formattedStages).to.include(StateConstants.failed.color(`${StateConstants.failed.char} - Processing`)); - }); - }); -}); diff --git a/test/shared/timeUtils.test.ts b/test/shared/timeUtils.test.ts deleted file mode 100644 index a007a87d..00000000 --- a/test/shared/timeUtils.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) 2020, salesforce.com, inc. - * All rights reserved. - * Licensed under the BSD 3-Clause license. - * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause - */ -import { expect } from 'chai'; -import { Duration } from '@salesforce/cli-plugins-testkit'; -import { getClockForSeconds, getSecondsToHuman } from '../../src/shared/timeUtils.js'; - -describe('timeUtils', () => { - describe('getSecondsToHuman', () => { - it('should build time string with 10 seconds', () => { - expect(getSecondsToHuman(10)).to.includes('10 seconds'); - }); - it('should build time string 1 minute', () => { - expect(getSecondsToHuman(60)).to.includes('1 minute'); - }); - it('should build time string 1 hour', () => { - expect(getSecondsToHuman(Duration.hours(1).seconds)).to.includes('1 hour'); - }); - it('should build time string 1 day', () => { - expect(getSecondsToHuman(Duration.days(1).seconds)).to.includes('1 day'); - }); - it('should build time string 1 day 12 hours', () => { - expect(getSecondsToHuman(Duration.days(1).seconds + Duration.hours(12).seconds)).to.includes('1 day 12 hour'); - }); - }); - describe('getClockForSeconds', () => { - it('should build time string with 10 seconds', () => { - expect(getClockForSeconds(10)).to.be.equal('00:00:10'); - }); - it('should build time string 1 minute', () => { - expect(getClockForSeconds(60)).to.be.equal('00:01:00'); - }); - it('should build time string 1 hour', () => { - expect(getClockForSeconds(Duration.hours(1).seconds)).to.be.equal('01:00:00'); - }); - it('should build time string 1 day', () => { - expect(getClockForSeconds(Duration.days(1).seconds)).to.be.equal('1:00:00:00'); - }); - it('should build time string 1 day 12 hours', () => { - expect(getClockForSeconds(Duration.days(1).seconds + Duration.hours(12).seconds)).to.be.equal('1:12:00:00'); - }); - }); -});