diff --git a/packages/backend-app-api/api-report.md b/packages/backend-app-api/api-report.md index 5008b1efe1..6cbb992ef8 100644 --- a/packages/backend-app-api/api-report.md +++ b/packages/backend-app-api/api-report.md @@ -26,6 +26,8 @@ export interface Backend { add(feature: BackendFeature): void; // (undocumented) start(): Promise; + // (undocumented) + stop(): Promise; } // @public (undocumented) diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index c86cd8382d..5a5f65a5a9 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -17,8 +17,10 @@ import { BackendFeature, ExtensionPoint, + lifecycleServiceRef, ServiceRef, } from '@backstage/backend-plugin-api'; +import { BackendLifecycleImpl } from '../services/implementations/lifecycleService'; import { BackendRegisterInit, ServiceHolder, @@ -165,4 +167,24 @@ export class BackendInitializer { return orderedRegisterInits; } + + async stop(): Promise { + if (!this.#started) { + throw new Error('Backend has not started'); + } + this.#started = false; + + const lifecycleService = await this.#serviceHolder.get( + lifecycleServiceRef, + 'root', + ); + + // TODO(Rugvip): :D + const lifecycle = (lifecycleService as any)?.lifecycle; + if (lifecycle instanceof BackendLifecycleImpl) { + await lifecycle.shutdown(); + } else { + throw new Error('Unexpected lifecycle service implementation'); + } + } } diff --git a/packages/backend-app-api/src/wiring/BackstageBackend.ts b/packages/backend-app-api/src/wiring/BackstageBackend.ts index 1d76161bfe..c53f47b685 100644 --- a/packages/backend-app-api/src/wiring/BackstageBackend.ts +++ b/packages/backend-app-api/src/wiring/BackstageBackend.ts @@ -36,7 +36,7 @@ export class BackstageBackend implements Backend { await this.#initializer.start(); } - // async stop(): Promise { - // await this.#initializer.stop(); - // } + async stop(): Promise { + await this.#initializer.stop(); + } } diff --git a/packages/backend-app-api/src/wiring/types.ts b/packages/backend-app-api/src/wiring/types.ts index 40ce6aa1ae..3e1acce08e 100644 --- a/packages/backend-app-api/src/wiring/types.ts +++ b/packages/backend-app-api/src/wiring/types.ts @@ -28,6 +28,7 @@ import { BackstageBackend } from './BackstageBackend'; export interface Backend { add(feature: BackendFeature): void; start(): Promise; + stop(): Promise; } export interface BackendRegisterInit { diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index 2fe4ed31d3..3df3f3cb21 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -3,6 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { Backend } from '@backstage/backend-app-api'; import { BackendFeature } from '@backstage/backend-plugin-api'; import { ExtensionPoint } from '@backstage/backend-plugin-api'; import { Knex } from 'knex'; @@ -23,7 +24,7 @@ export function setupRequestMockHandlers(worker: { export function startTestBackend< TServices extends any[], TExtensionPoints extends any[], ->(options: TestBackendOptions): Promise; +>(options: TestBackendOptions): Promise; // @alpha (undocumented) export interface TestBackendOptions< diff --git a/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts b/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts index 164e06d3b5..b87e157477 100644 --- a/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts +++ b/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts @@ -19,6 +19,7 @@ import { createExtensionPoint, createServiceFactory, createServiceRef, + lifecycleServiceRef, } from '@backstage/backend-plugin-api'; import { startTestBackend } from './TestBackend'; @@ -31,7 +32,7 @@ describe('TestBackend', () => { const extensionPoint3 = createExtensionPoint({ id: 'b3' }); const extensionPoint4 = createExtensionPoint({ id: 'b4' }); const extensionPoint5 = createExtensionPoint({ id: 'b5' }); - await startTestBackend({ + const backend = await startTestBackend({ services: [ // @ts-expect-error [extensionPoint1, { a: 'a' }], @@ -58,6 +59,7 @@ describe('TestBackend', () => { ], }); expect(1).toBe(1); + await backend.stop(); }); it('should start the test backend', async () => { @@ -87,11 +89,40 @@ describe('TestBackend', () => { }, }); - await startTestBackend({ + const backend = await startTestBackend({ services: [sf], features: [testModule()], }); expect(testFn).toHaveBeenCalledWith('winning'); + await backend.stop(); + }); + + it('should stop the test backend', async () => { + const shutdownSpy = jest.fn(); + + const testModule = createBackendModule({ + moduleId: 'test.module', + pluginId: 'test', + register(env) { + env.registerInit({ + deps: { + lifecycle: lifecycleServiceRef, + }, + async init({ lifecycle }) { + lifecycle.addShutdownHook({ fn: shutdownSpy }); + }, + }); + }, + }); + + const backend = await startTestBackend({ + services: [], + features: [testModule()], + }); + + expect(shutdownSpy).not.toHaveBeenCalled(); + await backend.stop(); + expect(shutdownSpy).toHaveBeenCalled(); }); }); diff --git a/packages/backend-test-utils/src/next/wiring/TestBackend.ts b/packages/backend-test-utils/src/next/wiring/TestBackend.ts index 677ba930cc..9c9427e332 100644 --- a/packages/backend-test-utils/src/next/wiring/TestBackend.ts +++ b/packages/backend-test-utils/src/next/wiring/TestBackend.ts @@ -14,7 +14,13 @@ * limitations under the License. */ -import { createSpecializedBackend } from '@backstage/backend-app-api'; +import { + Backend, + createSpecializedBackend, + lifecycleFactory, + loggerFactory, + rootLoggerFactory, +} from '@backstage/backend-app-api'; import { ServiceFactory, ServiceRef, @@ -47,11 +53,17 @@ export interface TestBackendOptions< features?: BackendFeature[]; } +const defaultServiceFactories = [ + rootLoggerFactory(), + loggerFactory(), + lifecycleFactory(), +]; + /** @alpha */ export async function startTestBackend< TServices extends any[], TExtensionPoints extends any[], ->(options: TestBackendOptions): Promise { +>(options: TestBackendOptions): Promise { const { services = [], extensionPoints = [], @@ -69,17 +81,23 @@ export async function startTestBackend< service: ref, deps: {}, factory: async () => async () => impl, - }); + })(); } return createServiceFactory({ service: ref, deps: {}, factory: async () => impl, - }); + })(); } return serviceDef as ServiceFactory; }); + for (const factory of defaultServiceFactories) { + if (!factories.some(f => f.service === factory.service)) { + factories.push(factory); + } + } + const backend = createSpecializedBackend({ ...otherOptions, services: factories, @@ -101,4 +119,6 @@ export async function startTestBackend< } await backend.start(); + + return backend; }