diff --git a/.changeset/green-moose-leave.md b/.changeset/green-moose-leave.md new file mode 100644 index 0000000000..df6ff04d82 --- /dev/null +++ b/.changeset/green-moose-leave.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Backends started with `startTestBackend` are now automatically stopped after all tests have run. diff --git a/.changeset/nasty-colts-cough.md b/.changeset/nasty-colts-cough.md new file mode 100644 index 0000000000..5dd098ec27 --- /dev/null +++ b/.changeset/nasty-colts-cough.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +Added `.stop()` method to `Backend`. diff --git a/packages/backend-app-api/api-report.md b/packages/backend-app-api/api-report.md index eba4517a1f..33e732cf05 100644 --- a/packages/backend-app-api/api-report.md +++ b/packages/backend-app-api/api-report.md @@ -25,6 +25,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 8311ae10bb..478cb30ff8 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, + coreServices, ServiceRef, } from '@backstage/backend-plugin-api'; +import { BackendLifecycleImpl } from '../services/implementations/lifecycleService'; import { BackendRegisterInit, EnumerableServiceHolder, @@ -173,4 +175,23 @@ export class BackendInitializer { return orderedRegisterInits; } + + async stop(): Promise { + if (!this.#started) { + return; + } + + const lifecycleService = await this.#serviceHolder.get( + coreServices.lifecycle, + 'root', + ); + + // TODO(Rugvip): Find a better way to do this + 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 76bcd5ab6d..46ac690702 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..3cc8507df3 100644 --- a/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts +++ b/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts @@ -19,9 +19,42 @@ import { createExtensionPoint, createServiceFactory, createServiceRef, + coreServices, } from '@backstage/backend-plugin-api'; import { startTestBackend } from './TestBackend'; +// This bit makes sure that test backends are cleaned up properly +let globalTestBackendHasBeenStopped = false; +beforeAll(async () => { + await startTestBackend({ + services: [], + features: [ + createBackendModule({ + moduleId: 'test.module', + pluginId: 'test', + register(env) { + env.registerInit({ + deps: { lifecycle: coreServices.lifecycle }, + async init({ lifecycle }) { + lifecycle.addShutdownHook({ + fn() { + globalTestBackendHasBeenStopped = true; + }, + }); + }, + }); + }, + })(), + ], + }); +}); + +afterAll(() => { + if (!globalTestBackendHasBeenStopped) { + throw new Error('Expected backend to have been stopped'); + } +}); + describe('TestBackend', () => { it('should get a type error if service implementation does not match', async () => { type Obj = { a: string; b: string }; @@ -94,4 +127,32 @@ describe('TestBackend', () => { expect(testFn).toHaveBeenCalledWith('winning'); }); + + 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: coreServices.lifecycle, + }, + 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..1d401bbf6f 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,19 @@ export interface TestBackendOptions< features?: BackendFeature[]; } +const defaultServiceFactories = [ + rootLoggerFactory(), + loggerFactory(), + lifecycleFactory(), +]; + +const backendInstancesToCleanUp = new Array(); + /** @alpha */ export async function startTestBackend< TServices extends any[], TExtensionPoints extends any[], ->(options: TestBackendOptions): Promise { +>(options: TestBackendOptions): Promise { const { services = [], extensionPoints = [], @@ -69,22 +83,30 @@ 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, }); + backendInstancesToCleanUp.push(backend); + backend.add({ id: `---test-extension-point-registrar`, register(reg) { @@ -101,4 +123,32 @@ export async function startTestBackend< } await backend.start(); + + return backend; } + +let registered = false; +function registerTestHooks() { + if (typeof afterAll !== 'function') { + return; + } + if (registered) { + return; + } + registered = true; + + afterAll(async () => { + await Promise.all( + backendInstancesToCleanUp.map(async backend => { + try { + await backend.stop(); + } catch (error) { + console.error(`Failed to stop backend after tests, ${error}`); + } + }), + ); + backendInstancesToCleanUp.length = 0; + }); +} + +registerTestHooks();