diff --git a/projects/yeoman-ui/packages/backend/src/utils/env.ts b/projects/yeoman-ui/packages/backend/src/utils/env.ts index 20d3f6f2..ebf23814 100644 --- a/projects/yeoman-ui/packages/backend/src/utils/env.ts +++ b/projects/yeoman-ui/packages/backend/src/utils/env.ts @@ -24,6 +24,11 @@ export type EnvGen = { gen: any; }; +export type PrepareEnvGen = ( + env: Environment, + gen: any +) => void | Promise; + export type GeneratorData = { generatorMeta: LookupGeneratorMeta; generatorPackageJson: any; @@ -57,12 +62,20 @@ class EnvUtil { public isEnvIncompatibilityError(error: unknown): boolean { return ( - (error as Error)?.message?.startsWith( + (error as Error)?.message?.includes( Constants.ENV_INCOMPATIBILITY_MESSAGE_PREFIX ) ?? false ); } + private isV3RuntimeIncompatibilityError(error: unknown): boolean { + const message = (error as Error)?.message ?? ""; + return ( + message.includes("requires yeoman-environment") || + message.includes("object is not extensible") + ); + } + public loadNpmPath(_force = false) { return this; } @@ -180,19 +193,7 @@ class EnvUtil { ); try { - const v6Env: Environment = this.createEnvInstance( - { sharedOptions: { forwardErrorToEnvironment: true } as any }, - adapter - ); - - v6Env.register(meta.resolved!, { - namespace: genNamespace, - packagePath: meta.packagePath, - }); - - const gen: any = await v6Env.create(genNamespace, { options } as any); - - return { env: v6Env, gen }; + return await this.createV6EnvAndGen(genNamespace, meta, options, adapter); } catch (v6Error) { const shouldFallbackToV3 = this.isEnvIncompatibilityError(v6Error); if (!shouldFallbackToV3) { @@ -229,6 +230,91 @@ class EnvUtil { } } + public async createRunGen( + genNamespace: string, + options: any, + adapter: any, + prepare: PrepareEnvGen + ): Promise { + const meta: LookupGeneratorMeta = await this.getGenMetadata(genNamespace); + + this.unloadGeneratorModules(genNamespace); + let v3EnvGen: EnvGen | undefined; + try { + v3EnvGen = this.createLegacyV3EnvAndGen( + genNamespace, + meta, + options, + adapter + ); + } catch (v3CreateError) { + if (this.isV3RuntimeIncompatibilityError(v3CreateError)) { + this.logger?.info( + `generator ${genNamespace} needs yeoman-environment v6; instantiation on v3 was rejected`, + { error: (v3CreateError as Error)?.message } + ); + } else { + this.logger?.debug( + `generator ${genNamespace} failed to instantiate on yeoman-environment v3; surfacing the error (not a v6-runtime signal)`, + { error: (v3CreateError as Error)?.message } + ); + throw v3CreateError; + } + } + + if (v3EnvGen) { + this.logger?.debug( + `routing generator ${genNamespace} to yeoman-environment v3` + ); + await this.prepareAndRun(v3EnvGen.env, v3EnvGen.gen, adapter, prepare); + return; + } + + this.logger?.debug( + `routing generator ${genNamespace} to yeoman-environment v6` + ); + this.unloadGeneratorModules(genNamespace); + const { env, gen } = await this.createV6EnvAndGen( + genNamespace, + meta, + options, + adapter + ); + await this.prepareAndRun(env, gen, adapter, prepare); + } + + private async prepareAndRun( + env: Environment, + gen: any, + adapter: any, + prepare: PrepareEnvGen + ): Promise { + adapter?.resetSignal?.(); + await prepare(env, gen); + await Promise.resolve(env.runGenerator(gen)); + } + + private async createV6EnvAndGen( + genNamespace: string, + meta: LookupGeneratorMeta, + options: any, + adapter: any + ): Promise { + const v6Env: Environment = this.createEnvInstance( + { sharedOptions: { forwardErrorToEnvironment: true } as any }, + adapter + ); + + v6Env.register(meta.resolved!, { + namespace: genNamespace, + packagePath: meta.packagePath, + }); + + const gen: any = await v6Env.create(genNamespace, { options } as any); + + return { env: v6Env, gen }; + } + private createLegacyV3EnvAndGen( genNamespace: string, meta: LookupGeneratorMeta, diff --git a/projects/yeoman-ui/packages/backend/src/yeomanui.ts b/projects/yeoman-ui/packages/backend/src/yeomanui.ts index ec21c8fd..f7b82f59 100644 --- a/projects/yeoman-ui/packages/backend/src/yeomanui.ts +++ b/projects/yeoman-ui/packages/backend/src/yeomanui.ts @@ -16,12 +16,7 @@ import { IPrompt, MessageType } from "@sap-devx/yeoman-ui-types"; import { AnalyticsWrapper } from "./usage-report/usage-analytics-wrapper.js"; import { Output } from "./output.js"; import { resolve } from "path"; -import { - Env, - EnvGen, - GeneratorData, - GeneratorNotFoundError, -} from "./utils/env.js"; +import { Env, GeneratorData, GeneratorNotFoundError } from "./utils/env.js"; import { namespaceToName } from "./utils/namespace.js"; import { vscode, getVscode } from "./utils/vscodeProxy.js"; import Generator from "yeoman-generator"; @@ -238,32 +233,35 @@ export class YeomanUI { appWizard: this.youiEvents.getAppWizard(), }; - const envGen: EnvGen = await Env.createEnvAndGen( + const prepare = (env: Environment, gen: any) => { + this.errorThrown = false; + + // check if generator defined a helper function called setPromptsCallback() + const setPromptsCallback = _.get(gen, "setPromptsCallback"); + if (setPromptsCallback) { + setPromptsCallback(this.setPromptList.bind(this)); + } + + this.promptCount = 0; + this.gen = gen as Generator; + // do not add second parameter with value true + // some generators rely on fact that this.env.cwd and + // the current working directory is changed. + this.gen.destinationRoot(targetFolder); + // notifies ui wether generator is in writing state + this.setGenInWriting(this.gen); + // handles generator install step if exists + this.onGenInstall(this.gen); + // handles generator errors + this.handleErrors(env, this.gen, generatorNamespace); + }; + + await Env.createRunGen( generatorNamespace, options, - this.youiAdapter + this.youiAdapter, + prepare ); - - // check if generator defined a helper function called setPromptsCallback() - const setPromptsCallback = _.get(envGen.gen, "setPromptsCallback"); - if (setPromptsCallback) { - setPromptsCallback(this.setPromptList.bind(this)); - } - - this.promptCount = 0; - this.gen = envGen.gen as Generator; - // do not add second parameter with value true - // some generators rely on fact that this.env.cwd and - // the current working directory is changed. - this.gen.destinationRoot(targetFolder); - // notifies ui wether generator is in writing state - this.setGenInWriting(this.gen); - // handles generator install step if exists - this.onGenInstall(this.gen); - // handles generator errors - this.handleErrors(envGen.env, this.gen, generatorNamespace); - - await envGen.env.runGenerator(envGen.gen); if (!this.errorThrown) { // Without resolve this code worked only for absolute paths without / at the end. // Generator can put a relative path, path including . and .. and / at the end. @@ -316,7 +314,6 @@ export class YeomanUI { generatorName, this.getErrorWithAdditionalInfo(error, `env.on(${errorEventName})`) ); - env.emit(errorEventName, error); }); gen.on(errorEventName, (error: any) => diff --git a/projects/yeoman-ui/packages/backend/src/youi-adapter.ts b/projects/yeoman-ui/packages/backend/src/youi-adapter.ts index be1cf30f..5c73f2f9 100644 --- a/projects/yeoman-ui/packages/backend/src/youi-adapter.ts +++ b/projects/yeoman-ui/packages/backend/src/youi-adapter.ts @@ -11,6 +11,7 @@ const { get, isFunction } = lodash; export class YouiAdapter { private yeomanui: YeomanUI; + private abortController = new AbortController(); constructor( private readonly youiEvents: YouiEvents, @@ -26,6 +27,32 @@ export class YouiAdapter { this.log = yoUiLog(this.output, this.yeomanui); } + // --- yeoman-environment v6 adapter contract --- + + get signal(): AbortSignal { + return this.abortController.signal; + } + + public resetSignal(): void { + this.abortController = new AbortController(); + } + + public abort(reason?: unknown): void { + if (!this.abortController.signal.aborted) { + this.abortController.abort(reason); + } + } + + public onIdle(): Promise { + return Promise.resolve(); + } + + public async progress( + fn: (progress: { step: (...args: any[]) => void }) => T | Promise + ): Promise { + return fn({ step: () => undefined }); + } + get colorDiffAdded() { return chalk.black.bgGreen; } diff --git a/projects/yeoman-ui/packages/backend/test/fixtures/generator-compose-sub/generators/app/index.js b/projects/yeoman-ui/packages/backend/test/fixtures/generator-compose-sub/generators/app/index.js new file mode 100644 index 00000000..350ddb3c --- /dev/null +++ b/projects/yeoman-ui/packages/backend/test/fixtures/generator-compose-sub/generators/app/index.js @@ -0,0 +1,13 @@ +"use strict"; + +const Generator = require("yeoman-generator-v5"); +const Base = Generator.default || Generator; + +module.exports = class ComposeSubGenerator extends Base { + writing() { + // Signal on the shared env options so the test can observe the sub ran + if (this.options && this.options.composeMarker) { + this.options.composeMarker.subRan = true; + } + } +}; diff --git a/projects/yeoman-ui/packages/backend/test/fixtures/generator-compose-sub/package.json b/projects/yeoman-ui/packages/backend/test/fixtures/generator-compose-sub/package.json new file mode 100644 index 00000000..bd2750cf --- /dev/null +++ b/projects/yeoman-ui/packages/backend/test/fixtures/generator-compose-sub/package.json @@ -0,0 +1,4 @@ +{ + "name": "generator-compose-sub", + "type": "commonjs" +} diff --git a/projects/yeoman-ui/packages/backend/test/fixtures/generator-compose-top/generators/app/index.js b/projects/yeoman-ui/packages/backend/test/fixtures/generator-compose-top/generators/app/index.js new file mode 100644 index 00000000..c8c48370 --- /dev/null +++ b/projects/yeoman-ui/packages/backend/test/fixtures/generator-compose-top/generators/app/index.js @@ -0,0 +1,18 @@ +"use strict"; + +const Generator = require("yeoman-generator-v5"); +const Base = Generator.default || Generator; + +module.exports = class ComposeTopGenerator extends Base { + writing() { + this.composeWith( + { + Generator: require("../../../generator-compose-sub/generators/app/index.js"), + path: require.resolve( + "../../../generator-compose-sub/generators/app/index.js" + ), + }, + this.options + ); + } +}; diff --git a/projects/yeoman-ui/packages/backend/test/fixtures/generator-compose-top/package.json b/projects/yeoman-ui/packages/backend/test/fixtures/generator-compose-top/package.json new file mode 100644 index 00000000..a0ca6f43 --- /dev/null +++ b/projects/yeoman-ui/packages/backend/test/fixtures/generator-compose-top/package.json @@ -0,0 +1,4 @@ +{ + "name": "generator-compose-top", + "type": "commonjs" +} diff --git a/projects/yeoman-ui/packages/backend/test/utils/env.spec.ts b/projects/yeoman-ui/packages/backend/test/utils/env.spec.ts index 23df4efd..1e67943f 100644 --- a/projects/yeoman-ui/packages/backend/test/utils/env.spec.ts +++ b/projects/yeoman-ui/packages/backend/test/utils/env.spec.ts @@ -4,6 +4,7 @@ import { resolve } from "path"; import { fileURLToPath } from "url"; import { dirname } from "path"; import { createRequire } from "module"; +import { EventEmitter } from "events"; import { Env } from "../../src/utils/env.js"; import { Constants } from "../../src/utils/constants.js"; import type { LookupGeneratorMeta } from "@yeoman/types"; @@ -40,6 +41,12 @@ const envV6InitErrorFixture = { ), }; +const composeTopFixture = { + namespace: "compose-top:app", + packagePath: resolve(FIXTURES, "generator-compose-top"), + resolved: resolve(FIXTURES, "generator-compose-top/generators/app/index.js"), +}; + /** Build a LookupGeneratorMeta the way env.ts consumes it. */ function metaFor(fixture: { namespace: string; @@ -197,4 +204,283 @@ describe("Env.createEnvAndGen()", () => { "the original v6 incompatibility error is attached for diagnostics" ).to.contain(`${Constants.ENV_INCOMPATIBILITY_MESSAGE_PREFIX}.`); }); + + it("isEnvIncompatibilityError matches the incompatibility text even when wrapped", () => { + const wrapped = new Error( + `Could not call '@bas-dev/generator-extensibility-sub' sub-generator: ${Constants.ENV_INCOMPATIBILITY_MESSAGE_PREFIX}.` + ); + expect((Env as any).isEnvIncompatibilityError(wrapped)).to.equal(true); + expect( + (Env as any).isEnvIncompatibilityError(new Error("some other failure")), + "an unrelated error is not treated as an env-incompatibility" + ).to.equal(false); + }); +}); + +describe("Env.createRunGen()", () => { + let sandbox: SinonSandbox; + + beforeEach(() => { + sandbox = createSandbox(); + // createRunGen resolves metadata + reloads modules; keep those inert + sandbox.stub(Env as any, "getGenMetadata").resolves(metaFor(envV6Fixture)); + sandbox.stub(Env as any, "unloadGeneratorModules"); + }); + + afterEach(() => { + sandbox.restore(); + }); + + function fakeEnv(behavior: { reject?: Error }): any { + const emitter = new EventEmitter(); + return Object.assign(emitter, { + runGenerator(): Promise { + return behavior.reject + ? Promise.reject(behavior.reject) + : Promise.resolve(); + }, + }); + } + + function fakeAdapter(): any { + return { + resetSignal(): void { + return undefined; + }, + }; + } + + it("runs on v3 when the generator can be instantiated on v3", async () => { + const v3Create = sandbox + .stub(Env as any, "createLegacyV3EnvAndGen") + .returns({ env: fakeEnv({}), gen: { id: "v3" } }); + const v6Create = sandbox.stub(Env as any, "createV6EnvAndGen"); + + const prepare = sandbox.stub(); + await Env.createRunGen( + envV6Fixture.namespace, + { silent: true }, + fakeAdapter(), + prepare + ); + + expect(v3Create.calledOnce, "the generator was created on v3").to.equal( + true + ); + expect( + v6Create.called, + "v6 is not attempted when v3 create succeeds" + ).to.equal(false); + expect(prepare.calledOnce, "prepare wired the v3 env/gen").to.equal(true); + expect(prepare.firstCall.args[1]).to.deep.equal({ id: "v3" }); + }); + + it("resets the adapter signal before the run", async () => { + sandbox + .stub(Env as any, "createLegacyV3EnvAndGen") + .returns({ env: fakeEnv({}), gen: { id: "v3" } }); + + const resetSignal = sandbox.stub(); + await Env.createRunGen( + envV6Fixture.namespace, + { silent: true }, + { resetSignal }, + sandbox.stub() + ); + + expect(resetSignal.calledOnce, "a fresh signal is prepared").to.equal(true); + }); + + it("runs on v6 when the generator cannot be instantiated on v3", async () => { + const v3Create = sandbox + .stub(Env as any, "createLegacyV3EnvAndGen") + .throws( + new Error( + "This generator requires yeoman-environment at least 4.0.0-rc.0" + ) + ); + const v6Create = sandbox + .stub(Env as any, "createV6EnvAndGen") + .resolves({ env: fakeEnv({}), gen: { id: "v6" } }); + + const prepare = sandbox.stub(); + await Env.createRunGen( + envV6Fixture.namespace, + { silent: true }, + fakeAdapter(), + prepare + ); + + expect(v3Create.calledOnce, "v3 create was probed first").to.equal(true); + expect(v6Create.calledOnce, "the generator ran on v6").to.equal(true); + expect(prepare.calledOnce, "prepare wired the v6 env/gen").to.equal(true); + expect(prepare.firstCall.args[1]).to.deep.equal({ id: "v6" }); + }); + + it("surfaces the v6 error when the generator fails on v3 create AND on v6", async () => { + sandbox + .stub(Env as any, "createLegacyV3EnvAndGen") + .throws(new Error("requires yeoman-environment at least 4.0.0-rc.0")); + const V6_ERROR = "v6 run blew up for its own reason"; + sandbox.stub(Env as any, "createV6EnvAndGen").rejects(new Error(V6_ERROR)); + + let thrown: any; + try { + await Env.createRunGen( + envV6Fixture.namespace, + { silent: true }, + fakeAdapter(), + sandbox.stub() + ); + } catch (error) { + thrown = error; + } + + expect(thrown?.message, "the v6 error is surfaced").to.contain(V6_ERROR); + }); + + it("surfaces a v3 run error", async () => { + const V3_RUN_ERROR = "v3 generator writing() blew up"; + sandbox + .stub(Env as any, "createLegacyV3EnvAndGen") + .returns({ env: fakeEnv({ reject: new Error(V3_RUN_ERROR) }), gen: {} }); + const v6Create = sandbox.stub(Env as any, "createV6EnvAndGen"); + + let thrown: any; + try { + await Env.createRunGen( + envV6Fixture.namespace, + { silent: true }, + fakeAdapter(), + sandbox.stub() + ); + } catch (error) { + thrown = error; + } + + expect(thrown?.message, "the v3 run error is surfaced").to.contain( + V3_RUN_ERROR + ); + expect( + v6Create.called, + "v6 is not tried after a successful v3 create" + ).to.equal(false); + }); + + it("surfaces a v3 CREATE error as-is (does NOT route to v6) when it is not a runtime-incompatibility signal", async () => { + // A legacy generator whose constructor throws for its own reason (e.g. + // @bas-dev/extensibility-sub run standalone -> `"undefined" is not valid + // JSON`). This is a real v3 error, not a "needs v6" signal, so it must + // surface directly - routing to v6 would produce a misleading + // env-incompatibility error instead. + const V3_BUG = '"undefined" is not valid JSON'; + sandbox + .stub(Env as any, "createLegacyV3EnvAndGen") + .throws(new Error(V3_BUG)); + const v6Create = sandbox.stub(Env as any, "createV6EnvAndGen"); + + let thrown: any; + try { + await Env.createRunGen( + envV6Fixture.namespace, + { silent: true }, + fakeAdapter(), + sandbox.stub() + ); + } catch (error) { + thrown = error; + } + + expect(thrown?.message, "the original v3 error is surfaced").to.contain( + V3_BUG + ); + expect( + v6Create.called, + "a non-incompatibility v3 error must NOT trigger a v6 attempt" + ).to.equal(false); + }); + + it("routes to v6 only when the v3 create error IS a runtime-incompatibility signal", async () => { + sandbox + .stub(Env as any, "createLegacyV3EnvAndGen") + .throws(new Error("requires yeoman-environment at least 4.0.0-rc.0")); + const v6Create = sandbox + .stub(Env as any, "createV6EnvAndGen") + .resolves({ env: fakeEnv({}), gen: { id: "v6" } }); + + const prepare = sandbox.stub(); + await Env.createRunGen( + envV6Fixture.namespace, + { silent: true }, + fakeAdapter(), + prepare + ); + + expect(v6Create.calledOnce, "the generator ran on v6").to.equal(true); + expect(prepare.firstCall.args[1]).to.deep.equal({ id: "v6" }); + }); +}); + +describe("Env.createRunGen() - real compose regression", () => { + let sandbox: SinonSandbox; + + beforeEach(() => { + sandbox = createSandbox(); + sandbox + .stub(Env as any, "getGenMetadata") + .resolves(metaFor(composeTopFixture)); + // Use the bundled v3 runtime for the real v3 path. + sandbox + .stub(Env as any, "loadLegacyV3Compat") + .returns(require("yeoman-env-v3")); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("routes a legacy top generator to v3 and runs its composed sub-generator to completion", async function () { + this.timeout(15000); + + const marker = { subRan: false }; + const options = { silent: true, composeMarker: marker }; + // Minimal adapter; v3 ignores the v6 hooks. + const log: any = (): void => undefined; + log.info = (): void => undefined; + log.error = (): void => undefined; + log.writeln = (): void => undefined; + const adapter: any = { + log, + prompt: (): Promise => Promise.resolve({}), + diff: (): string => "", + colorDiffAdded: (s: string): string => s, + colorDiffRemoved: (s: string): string => s, + resetSignal: (): void => undefined, + }; + + const v6Create = sandbox.spy(Env as any, "createV6EnvAndGen"); + + let capturedEnv: any; + await Env.createRunGen( + composeTopFixture.namespace, + options, + adapter, + (env: any): void => { + capturedEnv = env; + } + ); + + expect( + (capturedEnv as any)?.getVersion?.(), + "the top generator ran on the yeoman-environment v3 runtime" + ).to.match(/^3\./); + expect( + marker.subRan, + "the composed sub-generator's writing phase actually ran" + ).to.equal(true); + expect( + v6Create.called, + "a legacy generator does not touch the v6 runtime" + ).to.equal(false); + }); }); diff --git a/projects/yeoman-ui/packages/backend/test/yeomanui.spec.ts b/projects/yeoman-ui/packages/backend/test/yeomanui.spec.ts index 1f8c9252..780ea334 100644 --- a/projects/yeoman-ui/packages/backend/test/yeomanui.spec.ts +++ b/projects/yeoman-ui/packages/backend/test/yeomanui.spec.ts @@ -240,18 +240,16 @@ describe("yeomanui unit test", () => { it("flow is successfull", async () => { envUtilsMock.expects("getGeneratorsData").resolves(gensMeta); envUtilsMock - .expects("createEnvAndGen") + .expects("createRunGen") .withArgs("test1-project:app") - .resolves({ - env: { - on: () => "", - runGenerator: () => "", - }, - gen: { - destinationRoot: () => "", - on: () => "", - }, - }); + .callsFake( + async (_ns: string, _options: any, _adapter: any, prepare: any) => { + await prepare( + { on: () => "", removeListener: () => "" }, + { destinationRoot: () => "", on: () => "" } + ); + } + ); youiEventsMock.expects("setAppWizardHeaderTitle").withArgs(undefined); wsConfigMock .expects("get") @@ -289,18 +287,16 @@ describe("yeomanui unit test", () => { it("single generator", async () => { yeomanUi["uiOptions"].generator = "test4:app"; envUtilsMock - .expects("createEnvAndGen") + .expects("createRunGen") .withArgs("test4:app") - .resolves({ - env: { - on: () => "", - runGenerator: () => "", - }, - gen: { - destinationRoot: () => "", - on: () => "", - }, - }); + .callsFake( + async (_ns: string, _options: any, _adapter: any, prepare: any) => { + await prepare( + { on: () => "", removeListener: () => "" }, + { destinationRoot: () => "", on: () => "" } + ); + } + ); wsConfigMock .expects("get") .withExactArgs(yeomanUi["TARGET_FOLDER_CONFIG_PROP"]) diff --git a/projects/yeoman-ui/packages/backend/test/youi-adapter.spec.ts b/projects/yeoman-ui/packages/backend/test/youi-adapter.spec.ts index 1475c1c0..485bf51b 100644 --- a/projects/yeoman-ui/packages/backend/test/youi-adapter.spec.ts +++ b/projects/yeoman-ui/packages/backend/test/youi-adapter.spec.ts @@ -128,4 +128,41 @@ describe("YouiAdapter", () => { expect(response.lastName).to.equal(lastName); }); }); + + describe("#v6 adapter contract", () => { + it("aborts the signal with the given reason", () => { + const adapter: any = new YouiAdapter(youiEvents, outputChannel); + const reason = new Error("boom"); + expect(adapter.signal.aborted).to.equal(false); + adapter.abort(reason); + expect(adapter.signal.aborted).to.equal(true); + expect(adapter.signal.reason).to.equal(reason); + }); + + it("abort() is idempotent (v6 may call it more than once per failure)", () => { + const adapter: any = new YouiAdapter(youiEvents, outputChannel); + adapter.abort(new Error("first")); + // A second abort on an already-aborted controller would otherwise throw. + expect(() => adapter.abort(new Error("second"))).to.not.throw(); + }); + + it("resetSignal() yields a fresh, un-aborted signal per run", () => { + const adapter: any = new YouiAdapter(youiEvents, outputChannel); + adapter.abort(new Error("boom")); + expect(adapter.signal.aborted).to.equal(true); + adapter.resetSignal(); + expect(adapter.signal.aborted).to.equal(false); + }); + + it("progress() runs the provided step function", async () => { + const adapter: any = new YouiAdapter(youiEvents, outputChannel); + const result = await adapter.progress(() => "done"); + expect(result).to.equal("done"); + }); + + it("onIdle() resolves", async () => { + const adapter: any = new YouiAdapter(youiEvents, outputChannel); + await adapter.onIdle(); + }); + }); });