Merge pull request #14985 from backstage/mob/pls-stop

backend-app-api, backend-test-utils: added backend.stop() and auto stop in tests
This commit is contained in:
Patrik Oldsberg
2022-12-15 01:06:52 +01:00
committed by GitHub
9 changed files with 154 additions and 8 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-test-utils': patch
---
Backends started with `startTestBackend` are now automatically stopped after all tests have run.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-app-api': patch
---
Added `.stop()` method to `Backend`.
+2
View File
@@ -25,6 +25,8 @@ export interface Backend {
add(feature: BackendFeature): void;
// (undocumented)
start(): Promise<void>;
// (undocumented)
stop(): Promise<void>;
}
// @public (undocumented)
@@ -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<void> {
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');
}
}
}
@@ -36,7 +36,7 @@ export class BackstageBackend implements Backend {
await this.#initializer.start();
}
// async stop(): Promise<void> {
// await this.#initializer.stop();
// }
async stop(): Promise<void> {
await this.#initializer.stop();
}
}
@@ -28,6 +28,7 @@ import { BackstageBackend } from './BackstageBackend';
export interface Backend {
add(feature: BackendFeature): void;
start(): Promise<void>;
stop(): Promise<void>;
}
export interface BackendRegisterInit {
+2 -1
View File
@@ -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<TServices, TExtensionPoints>): Promise<void>;
>(options: TestBackendOptions<TServices, TExtensionPoints>): Promise<Backend>;
// @alpha (undocumented)
export interface TestBackendOptions<
@@ -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();
});
});
@@ -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<Backend>();
/** @alpha */
export async function startTestBackend<
TServices extends any[],
TExtensionPoints extends any[],
>(options: TestBackendOptions<TServices, TExtensionPoints>): Promise<void> {
>(options: TestBackendOptions<TServices, TExtensionPoints>): Promise<Backend> {
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();