@theia/core 1.72.0-next.36 → 1.72.0-next.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,310 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 EclipseSource and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { expect } from 'chai';
18
+ import * as sinon from 'sinon';
19
+ import { Container, ContainerModule, injectable, preDestroy } from 'inversify';
20
+ import { bindContributionProvider, ILogger, Stopwatch } from '../common';
21
+ import { Deferred } from '../common/promise-util';
22
+ import { MockLogger } from '../common/test/mock-logger';
23
+ import { NodeStopwatch } from './performance/node-stopwatch';
24
+ import { ProcessUtils } from './process-utils';
25
+ import {
26
+ BackendApplication,
27
+ BackendApplicationCliContribution,
28
+ BackendApplicationContribution,
29
+ RootContainer
30
+ } from './backend-application';
31
+ import { CliContribution } from './cli';
32
+
33
+ /**
34
+ * Test subclass that exposes the protected `gracefulShutdown` for direct testing.
35
+ */
36
+ @injectable()
37
+ class TestBackendApplication extends BackendApplication {
38
+ public invokeGracefulShutdown(): Promise<void> {
39
+ return this.gracefulShutdown();
40
+ }
41
+ }
42
+
43
+ // All process events on which `BackendApplication` installs listeners in its
44
+ // constructor. We snapshot and restore them around each test to avoid leaking
45
+ // listeners across tests (and triggering `MaxListenersExceededWarning`).
46
+ const PROCESS_EVENTS = ['SIGINT', 'SIGTERM', 'SIGPIPE', 'exit', 'uncaughtException'] as const;
47
+ type ProcessEventName = typeof PROCESS_EVENTS[number];
48
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
49
+ type AnyListener = (...args: any[]) => void;
50
+
51
+ describe('BackendApplication', () => {
52
+ let sandbox: sinon.SinonSandbox;
53
+ let exitStub: sinon.SinonStub;
54
+ let savedListeners: Partial<Record<ProcessEventName, AnyListener[]>>;
55
+
56
+ beforeEach(() => {
57
+ sandbox = sinon.createSandbox();
58
+
59
+ // Snapshot any existing listeners so we can restore after each test
60
+ // (BackendApplication installs its own as part of construction).
61
+ savedListeners = {};
62
+ for (const evt of PROCESS_EVENTS) {
63
+ savedListeners[evt] = [...process.listeners(evt as NodeJS.Signals)] as AnyListener[];
64
+ process.removeAllListeners(evt);
65
+ }
66
+
67
+ exitStub = sandbox.stub(process, 'exit');
68
+ });
69
+
70
+ afterEach(() => {
71
+ for (const evt of PROCESS_EVENTS) {
72
+ process.removeAllListeners(evt);
73
+ for (const listener of savedListeners[evt] ?? []) {
74
+ process.on(evt, listener);
75
+ }
76
+ }
77
+
78
+ sandbox.restore();
79
+ });
80
+
81
+ function createTestContainer(): Container {
82
+ const container = new Container();
83
+
84
+ container.bind(RootContainer).toConstantValue(container);
85
+
86
+ container.bind(ILogger).to(MockLogger).inSingletonScope();
87
+ container.bind(Stopwatch).to(NodeStopwatch).inSingletonScope();
88
+ container.bind(ProcessUtils).toSelf().inSingletonScope();
89
+ container.bind(BackendApplicationCliContribution).toSelf().inSingletonScope();
90
+ container.bind(CliContribution).toService(BackendApplicationCliContribution);
91
+ bindContributionProvider(container, BackendApplicationContribution);
92
+
93
+ container.bind(TestBackendApplication).toSelf().inSingletonScope();
94
+ container.bind(BackendApplication).toService(TestBackendApplication);
95
+
96
+ return container;
97
+ }
98
+
99
+ describe('graceful shutdown', () => {
100
+
101
+ it('runs @preDestroy on root-scoped singletons before exiting with code 1', async () => {
102
+ let canaryDisposed = false;
103
+
104
+ @injectable()
105
+ class Canary {
106
+ @preDestroy()
107
+ protected onPreDestroy(): void {
108
+ canaryDisposed = true;
109
+ }
110
+ }
111
+
112
+ const container = createTestContainer();
113
+ const canaryModule = new ContainerModule(bind => {
114
+ bind(Canary).toSelf().inSingletonScope();
115
+ });
116
+ container.load(canaryModule);
117
+ container.get(Canary);
118
+
119
+ const app = container.get(TestBackendApplication);
120
+
121
+ await app.invokeGracefulShutdown();
122
+
123
+ expect(canaryDisposed, '@preDestroy was not invoked on root-scoped singleton').to.be.true;
124
+ expect(exitStub.calledOnceWith(1), 'process.exit(1) was not called exactly once').to.be.true;
125
+ });
126
+
127
+ it('is idempotent: a second invocation does not unbind the container twice', async () => {
128
+ const container = createTestContainer();
129
+ const unbindSpy = sandbox.spy(container, 'unbindAllAsync');
130
+
131
+ const app = container.get(TestBackendApplication);
132
+
133
+ await app.invokeGracefulShutdown();
134
+ await app.invokeGracefulShutdown();
135
+
136
+ expect(unbindSpy.callCount, 'unbindAllAsync should be called only once').to.equal(1);
137
+ expect(exitStub.callCount, 'process.exit should be called only once').to.equal(1);
138
+ });
139
+
140
+ it('still exits if container cleanup rejects', async () => {
141
+ const container = createTestContainer();
142
+ const cleanupError = new Error('cleanup boom');
143
+ sandbox.stub(container, 'unbindAllAsync').rejects(cleanupError);
144
+ const warnStub = sandbox.stub(console, 'warn');
145
+
146
+ const app = container.get(TestBackendApplication);
147
+
148
+ await app.invokeGracefulShutdown();
149
+
150
+ expect(exitStub.calledOnceWith(1), 'process.exit(1) was not called').to.be.true;
151
+ expect(warnStub.calledOnce, 'a warning should be logged when cleanup rejects').to.be.true;
152
+ expect(warnStub.firstCall.args[0]).to.match(/cleanup boom/);
153
+ });
154
+
155
+ it('exits even when container cleanup hangs past the timeout', async () => {
156
+ const clock = sandbox.useFakeTimers();
157
+ const container = createTestContainer();
158
+ sandbox.stub(container, 'unbindAllAsync').returns(new Promise<void>(() => { /* never */ }));
159
+ const warnStub = sandbox.stub(console, 'warn');
160
+
161
+ const app = container.get(TestBackendApplication);
162
+
163
+ const shutdownPromise = app.invokeGracefulShutdown();
164
+
165
+ await clock.tickAsync(5001);
166
+ await shutdownPromise;
167
+
168
+ expect(exitStub.calledOnceWith(1), 'process.exit(1) was not called after timeout').to.be.true;
169
+ expect(warnStub.calledOnce, 'a warning should be logged on timeout').to.be.true;
170
+ expect(warnStub.firstCall.args[0]).to.match(/timed out/);
171
+ });
172
+
173
+ it('awaits async onStop contributions before unbinding the container', async () => {
174
+ const container = createTestContainer();
175
+ const onStopDeferred = new Deferred<void>();
176
+ container.bind(BackendApplicationContribution).toConstantValue({
177
+ onStop: () => onStopDeferred.promise
178
+ });
179
+ const unbindSpy = sandbox.spy(container, 'unbindAllAsync');
180
+
181
+ const app = container.get(TestBackendApplication);
182
+ const shutdownPromise = app.invokeGracefulShutdown();
183
+
184
+ expect(unbindSpy.called, 'unbindAllAsync should not run before onStop resolves').to.be.false;
185
+
186
+ onStopDeferred.resolve();
187
+ await shutdownPromise;
188
+
189
+ expect(unbindSpy.calledOnce, 'unbindAllAsync should run once onStop completes').to.be.true;
190
+ expect(exitStub.calledOnceWith(1)).to.be.true;
191
+ });
192
+
193
+ it('invokes onStop hooks while injected services are still resolvable', async () => {
194
+ @injectable()
195
+ class Helper {
196
+ readonly value = 'still-bound';
197
+ }
198
+
199
+ const container = createTestContainer();
200
+ container.bind(Helper).toSelf().inSingletonScope();
201
+
202
+ let observed: string | undefined;
203
+ container.bind(BackendApplicationContribution).toConstantValue({
204
+ onStop: () => {
205
+ observed = container.get(Helper).value;
206
+ }
207
+ });
208
+
209
+ const app = container.get(TestBackendApplication);
210
+ await app.invokeGracefulShutdown();
211
+
212
+ expect(observed, 'onStop should observe injected services that are still bound').to.equal('still-bound');
213
+ });
214
+
215
+ it('proceeds with shutdown when onStop hooks exceed the timeout', async () => {
216
+ const clock = sandbox.useFakeTimers();
217
+ const container = createTestContainer();
218
+ container.bind(BackendApplicationContribution).toConstantValue({
219
+ onStop: () => new Promise<void>(() => { /* never */ })
220
+ });
221
+ const unbindSpy = sandbox.spy(container, 'unbindAllAsync');
222
+ const warnStub = sandbox.stub(console, 'warn');
223
+
224
+ const app = container.get(TestBackendApplication);
225
+ const shutdownPromise = app.invokeGracefulShutdown();
226
+
227
+ await clock.tickAsync(5001);
228
+ await shutdownPromise;
229
+
230
+ expect(warnStub.calledOnce, 'a warning should be logged on onStop timeout').to.be.true;
231
+ expect(warnStub.firstCall.args[0]).to.match(/Stopping backend contributions/);
232
+ expect(unbindSpy.calledOnce, 'unbind should still run after onStop times out').to.be.true;
233
+ expect(exitStub.calledOnceWith(1)).to.be.true;
234
+ });
235
+
236
+ it('runs all contributions even when one onStop rejects', async () => {
237
+ const container = createTestContainer();
238
+ let secondRan = false;
239
+ container.bind(BackendApplicationContribution).toConstantValue({
240
+ onStop: async () => { throw new Error('boom'); }
241
+ });
242
+ container.bind(BackendApplicationContribution).toConstantValue({
243
+ onStop: () => { secondRan = true; }
244
+ });
245
+ const errorStub = sandbox.stub(console, 'error');
246
+
247
+ const app = container.get(TestBackendApplication);
248
+ await app.invokeGracefulShutdown();
249
+
250
+ expect(secondRan, 'second contribution should still be stopped after first rejects').to.be.true;
251
+ expect(errorStub.calledWithMatch('Could not stop contribution')).to.be.true;
252
+ });
253
+
254
+ it('is idempotent when a contribution re-enters graceful shutdown', async () => {
255
+ const container = createTestContainer();
256
+ // Indirected through a holder so the contribution closure can refer to the
257
+ // application instance that is constructed after the binding is recorded.
258
+ const appHolder: { current?: TestBackendApplication } = {};
259
+ container.bind(BackendApplicationContribution).toConstantValue({
260
+ onStop: () => appHolder.current!.invokeGracefulShutdown()
261
+ });
262
+ const unbindSpy = sandbox.spy(container, 'unbindAllAsync');
263
+
264
+ appHolder.current = container.get(TestBackendApplication);
265
+ await appHolder.current.invokeGracefulShutdown();
266
+
267
+ expect(unbindSpy.callCount, 'unbindAllAsync should be called only once').to.equal(1);
268
+ expect(exitStub.callCount, 'process.exit should be called only once').to.equal(1);
269
+ });
270
+
271
+ });
272
+
273
+ describe('process exit handler', () => {
274
+
275
+ it('does not re-invoke contributions after graceful shutdown ran them', async () => {
276
+ const container = createTestContainer();
277
+ const onStopSpy = sandbox.spy();
278
+ container.bind(BackendApplicationContribution).toConstantValue({ onStop: onStopSpy });
279
+ const terminateStub = sandbox.stub(ProcessUtils.prototype, 'terminateProcessTree');
280
+
281
+ const app = container.get(TestBackendApplication);
282
+ const exitListener = process.listeners('exit')[0] as () => void;
283
+
284
+ await app.invokeGracefulShutdown();
285
+ expect(onStopSpy.callCount, 'contribution onStop should fire from gracefulShutdown').to.equal(1);
286
+
287
+ exitListener();
288
+
289
+ expect(onStopSpy.callCount, 'contribution onStop should not be invoked a second time').to.equal(1);
290
+ expect(terminateStub.called, 'terminateProcessTree should be invoked by the exit handler').to.be.true;
291
+ });
292
+
293
+ it('invokes contributions synchronously when graceful shutdown was bypassed', () => {
294
+ const container = createTestContainer();
295
+ const onStopSpy = sandbox.spy();
296
+ container.bind(BackendApplicationContribution).toConstantValue({ onStop: onStopSpy });
297
+ const terminateStub = sandbox.stub(ProcessUtils.prototype, 'terminateProcessTree');
298
+
299
+ container.get(TestBackendApplication);
300
+ const exitListener = process.listeners('exit')[0] as () => void;
301
+
302
+ exitListener();
303
+
304
+ expect(onStopSpy.calledOnce, 'sync exit path should still invoke contributions').to.be.true;
305
+ expect(terminateStub.called, 'terminateProcessTree should be invoked').to.be.true;
306
+ });
307
+
308
+ });
309
+
310
+ });
@@ -21,10 +21,10 @@ import * as https from 'https';
21
21
  import * as express from 'express';
22
22
  import * as yargs from 'yargs';
23
23
  import * as fs from 'fs-extra';
24
- import { inject, named, injectable, postConstruct } from 'inversify';
24
+ import { inject, named, injectable, type interfaces, postConstruct } from 'inversify';
25
25
  import { ContributionProvider, LogLevel, MaybePromise, MeasurementContext, Stopwatch } from '../common';
26
26
  import { CliContribution } from './cli';
27
- import { Deferred } from '../common/promise-util';
27
+ import { Deferred, timeoutReject } from '../common/promise-util';
28
28
  import { environment } from '../common/index';
29
29
  import { AddressInfo } from 'net';
30
30
  import { ProcessUtils } from './process-utils';
@@ -35,12 +35,19 @@ import { ProcessUtils } from './process-utils';
35
35
  */
36
36
  export const BackendApplicationPath = process.env.THEIA_APP_PROJECT_PATH || process.cwd();
37
37
 
38
+ /**
39
+ * Private injection token for the backend's root Inversify {@link Container}.
40
+ */
41
+ export const RootContainer = Symbol('RootContainer');
42
+
38
43
  export type DnsResultOrder = 'ipv4first' | 'verbatim' | 'nodeDefault';
39
44
 
40
45
  const APP_PROJECT_PATH = 'app-project-path';
41
46
 
42
47
  const TIMER_WARNING_THRESHOLD = 50;
43
48
 
49
+ const SHUTDOWN_TIMEOUT_MS = 5000;
50
+
44
51
  const DEFAULT_PORT = environment.electron.is() ? 0 : 3000;
45
52
  const DEFAULT_HOST = 'localhost';
46
53
  const DEFAULT_SSL = false;
@@ -103,12 +110,23 @@ export interface BackendApplicationContribution {
103
110
  onStart?(server: http.Server | https.Server): MaybePromise<void>;
104
111
 
105
112
  /**
106
- * Called when the backend application shuts down. Contributions must perform only synchronous operations.
107
- * Any kind of additional asynchronous work queued in the event loop will be ignored and abandoned.
113
+ * Called when the backend application shuts down.
114
+ *
115
+ * When shutdown is initiated via `SIGINT`/`SIGTERM`, contributions are dispatched
116
+ * in parallel and any returned promise is awaited up to `SHUTDOWN_TIMEOUT_MS`
117
+ * milliseconds while injected services from the root container are still
118
+ * resolvable.
119
+ *
120
+ * On synchronous-exit fallback paths (uncaught exceptions, server bind failures,
121
+ * or normal process exit), the hook is invoked synchronously and any returned
122
+ * promise is discarded. Implementations should be resilient to either path.
123
+ *
124
+ * Contributions must be independent of one another during stop because they are
125
+ * dispatched in parallel.
108
126
  *
109
127
  * @param app the express application.
110
128
  */
111
- onStop?(app?: express.Application): void;
129
+ onStop?(app?: express.Application): MaybePromise<void>;
112
130
  }
113
131
 
114
132
  @injectable()
@@ -162,8 +180,14 @@ export class BackendApplication {
162
180
  @inject(Stopwatch)
163
181
  protected readonly stopwatch: Stopwatch;
164
182
 
183
+ @inject(RootContainer)
184
+ protected readonly rootContainer: interfaces.Container;
185
+
165
186
  private _configured: Promise<void>;
166
187
 
188
+ private stoppedContributions = false;
189
+ private shuttingDown = false;
190
+
167
191
  private settlementContext?: MeasurementContext<BackendApplicationContribution>;
168
192
 
169
193
  constructor(
@@ -179,18 +203,15 @@ export class BackendApplication {
179
203
  process.on('SIGPIPE', () => {
180
204
  console.error(new Error('Unexpected SIGPIPE'));
181
205
  });
182
- /**
183
- * Kill the current process tree on exit.
184
- */
185
- function signalHandler(signal: NodeJS.Signals): never {
186
- process.exit(1);
187
- }
206
+
188
207
  // Handles normal process termination.
189
208
  process.on('exit', () => this.onStop());
190
- // Handles `Ctrl+C`.
191
- process.on('SIGINT', signalHandler);
192
- // Handles `kill pid`.
193
- process.on('SIGTERM', signalHandler);
209
+
210
+ // Handles `Ctrl+C` and `kill pid`. Delegates to gracefulShutdown so that
211
+ // root-scoped singletons get their @preDestroy hooks invoked before exit.
212
+ const onSignal = () => { this.gracefulShutdown().catch(err => console.error(err)); };
213
+ process.on('SIGINT', onSignal);
214
+ process.on('SIGTERM', onSignal);
194
215
  }
195
216
 
196
217
  protected async initialize(): Promise<void> {
@@ -332,18 +353,82 @@ export class BackendApplication {
332
353
  : `${scheme}://${address}:${port}`;
333
354
  }
334
355
 
335
- protected onStop(): void {
356
+ /**
357
+ * Performs an asynchronous shutdown of the backend in two phases:
358
+ *
359
+ * 1. Contributions' {@link BackendApplicationContribution.onStop onStop} hooks are
360
+ * dispatched in parallel and awaited so that they can still resolve services
361
+ * from the root Inversify container while it is bound.
362
+ * 2. All services in the root container are unbound, running their `@preDestroy`
363
+ * hooks.
364
+ *
365
+ * Each phase has its own {@link SHUTDOWN_TIMEOUT_MS} budget to avoid hanging on a
366
+ * misbehaving hook. Late-resolving promises from a timed-out phase may still
367
+ * settle in the background and could log noisily or interact with a partly
368
+ * unbound container; this is accepted because the process is exiting.
369
+ *
370
+ * Idempotent: a second invocation is a no-op. Exits the process with code 1 so
371
+ * that the `process.on('exit')` handler runs for fallback cleanup such as
372
+ * {@link ProcessUtils.terminateProcessTree}; the exit handler does not re-invoke
373
+ * contribution `onStop()` hooks that this method already dispatched.
374
+ */
375
+ protected async gracefulShutdown(): Promise<void> {
376
+ if (this.shuttingDown) {
377
+ return;
378
+ }
379
+ this.shuttingDown = true;
380
+
381
+ try {
382
+ await Promise.race([
383
+ this.stopContributions(),
384
+ timeoutReject<void>(SHUTDOWN_TIMEOUT_MS, `Stopping backend contributions timed out after ${SHUTDOWN_TIMEOUT_MS}ms`)
385
+ ]);
386
+ } catch (err) {
387
+ const message = err instanceof Error ? err.message : String(err);
388
+ console.warn(`Backend contributions cleanup failed: ${message}`);
389
+ }
390
+
391
+ try {
392
+ await Promise.race([
393
+ this.rootContainer.unbindAllAsync(),
394
+ timeoutReject<void>(SHUTDOWN_TIMEOUT_MS, `Container unbind timed out after ${SHUTDOWN_TIMEOUT_MS}ms`)
395
+ ]);
396
+ } catch (err) {
397
+ const message = err instanceof Error ? err.message : String(err);
398
+ console.warn(`Backend root container cleanup failed: ${message}`);
399
+ }
400
+
401
+ process.exit(1);
402
+ }
403
+
404
+ protected async stopContributions(): Promise<void> {
405
+ if (this.stoppedContributions) {
406
+ return;
407
+ }
408
+ this.stoppedContributions = true;
336
409
  console.info('>>> Stopping backend contributions...');
337
- for (const contrib of this.contributionsProvider.getContributions()) {
410
+ // The `async` wrapper converts a synchronous throw inside a non-async
411
+ // contribution's `onStop` into a rejected promise so the per-contribution
412
+ // try/catch can handle it; otherwise `Promise.all` would abort.
413
+ await Promise.all(this.contributionsProvider.getContributions().map(async contrib => {
338
414
  if (contrib.onStop) {
339
415
  try {
340
- contrib.onStop(this.app);
416
+ await contrib.onStop(this.app);
341
417
  } catch (error) {
342
418
  console.error('Could not stop contribution', error);
343
419
  }
344
420
  }
345
- }
421
+ }));
346
422
  console.info('<<< All backend contributions have been stopped.');
423
+ }
424
+
425
+ protected onStop(): void {
426
+ // Deliberate fire-and-forget of an async `stopContributions`()` call.
427
+ // It invokes each contribution's `onStop` synchronously up to its
428
+ // first `await`, so any synchronous cleanup runs before
429
+ // `terminateProcessTree`. Any returned promises are abandoned because
430
+ // the `'exit'` event does not yield back to the event loop.
431
+ this.stopContributions();
347
432
  this.processUtils.terminateProcessTree(process.pid);
348
433
  }
349
434
 
@@ -67,7 +67,7 @@ describe('log-level-cli-contribution', () => {
67
67
  });
68
68
 
69
69
  it('should use --log-level flag', async () => {
70
- const args: yargs.Arguments = yargs.parse(['--log-level=debug']);
70
+ const args: yargs.Arguments = await yargs.parse(['--log-level=debug']);
71
71
  await cli.setArguments(args);
72
72
 
73
73
  expect(cli.defaultLogLevel).eq(LogLevel.DEBUG);
@@ -85,7 +85,7 @@ describe('log-level-cli-contribution', () => {
85
85
  fs.fsyncSync(file.fd);
86
86
  fs.closeSync(file.fd);
87
87
 
88
- const args: yargs.Arguments = yargs.parse(['--log-config', file.path]);
88
+ const args: yargs.Arguments = await yargs.parse(['--log-config', file.path]);
89
89
  await cli.setArguments(args);
90
90
 
91
91
  expect(cli.defaultLogLevel).eq(LogLevel.INFO);
@@ -96,7 +96,7 @@ describe('log-level-cli-contribution', () => {
96
96
  });
97
97
 
98
98
  it('should use info as default log level', async () => {
99
- const args: yargs.Arguments = yargs.parse([]);
99
+ const args: yargs.Arguments = await yargs.parse([]);
100
100
  await cli.setArguments(args);
101
101
 
102
102
  expect(cli.defaultLogLevel).eq(LogLevel.INFO);
@@ -113,7 +113,7 @@ describe('log-level-cli-contribution', () => {
113
113
  }
114
114
  }));
115
115
 
116
- const args: yargs.Arguments = yargs.parse(['--log-config', file.path]);
116
+ const args: yargs.Arguments = await yargs.parse(['--log-config', file.path]);
117
117
  await cli.setArguments(args);
118
118
  sinon.assert.calledWithMatch(consoleErrorSpy, 'Unknown default log level in');
119
119
  });
@@ -128,13 +128,13 @@ describe('log-level-cli-contribution', () => {
128
128
  }
129
129
  }));
130
130
 
131
- const args: yargs.Arguments = yargs.parse(['--log-config', file.path]);
131
+ const args: yargs.Arguments = await yargs.parse(['--log-config', file.path]);
132
132
  await cli.setArguments(args);
133
133
  sinon.assert.calledWithMatch(consoleErrorSpy, 'Unknown log level for logger hello in');
134
134
  });
135
135
 
136
136
  it('should reject nonexistent config files', async () => {
137
- const args: yargs.Arguments = yargs.parse(['--log-config', '/tmp/cacaca']);
137
+ const args: yargs.Arguments = await yargs.parse(['--log-config', '/tmp/cacaca']);
138
138
  await cli.setArguments(args);
139
139
  sinon.assert.calledWithMatch(consoleErrorSpy, 'no such file or directory');
140
140
  });
@@ -150,7 +150,7 @@ describe('log-level-cli-contribution', () => {
150
150
  });
151
151
  fs.writeFileSync(file.fd, '{' + text);
152
152
 
153
- const args: yargs.Arguments = yargs.parse(['--log-config', file.path]);
153
+ const args: yargs.Arguments = await yargs.parse(['--log-config', file.path]);
154
154
  await cli.setArguments(args);
155
155
  sinon.assert.calledWithMatch(consoleErrorSpy, 'Error reading log config file');
156
156
  });
@@ -175,7 +175,7 @@ describe('log-level-cli-contribution', () => {
175
175
  fs.fsyncSync(file.fd);
176
176
  fs.closeSync(file.fd);
177
177
 
178
- const args: yargs.Arguments = yargs.parse(['--log-config', file.path]);
178
+ const args: yargs.Arguments = await yargs.parse(['--log-config', file.path]);
179
179
  await cli.setArguments(args);
180
180
  }
181
181
 
@@ -224,7 +224,7 @@ describe('log-level-cli-contribution', () => {
224
224
  }));
225
225
  fs.fsyncSync(file.fd);
226
226
 
227
- const args: yargs.Arguments = yargs.parse(['--log-config', file.path]);
227
+ const args: yargs.Arguments = await yargs.parse(['--log-config', file.path]);
228
228
  await cli.setArguments(args);
229
229
 
230
230
  expect(cli.defaultLogLevel).eq(LogLevel.INFO);