Skip to content

Commit eb96739

Browse files
committed
Simplify checking logs
1 parent 78c79bf commit eb96739

2 files changed

Lines changed: 103 additions & 43 deletions

File tree

src/config/file.test.ts

Lines changed: 16 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -34,36 +34,20 @@ test("getConfigFileInput returns input value", async (t) => {
3434

3535
// Even though both an input and repository property are configured,
3636
// we prefer the direct input to the Action.
37-
const targetWithArgs = target
37+
await target
3838
.withActions(actionsEnv)
39-
.withArgs(repositoryProperties);
40-
await targetWithArgs.passes(t.is, testInput);
41-
42-
// Check for the expected log message.
43-
t.true(
44-
targetWithArgs
45-
.getLogger()
46-
.hasMessage("Using configuration file input from workflow"),
47-
);
39+
.withArgs(repositoryProperties)
40+
.logs(t, "Using configuration file input from workflow")
41+
.passes(t.is, testInput);
4842
});
4943

5044
test("getConfigFileInput returns repository property value", async (t) => {
5145
// Since there is no direct input, we should use the repository property.
52-
const target = callee(getConfigFileInput)
46+
await callee(getConfigFileInput)
5347
.withFeatures([Feature.ConfigFileRepositoryProperty])
54-
.withArgs(repositoryProperties);
55-
56-
await target.passes(
57-
t.is,
58-
repositoryProperties[RepositoryPropertyName.CONFIG_FILE],
59-
);
60-
61-
// Check for the expected log message.
62-
t.true(
63-
target
64-
.getLogger()
65-
.hasMessage("Using configuration file input from repository property"),
66-
);
48+
.withArgs(repositoryProperties)
49+
.logs(t, "Using configuration file input from repository property")
50+
.passes(t.is, repositoryProperties[RepositoryPropertyName.CONFIG_FILE]);
6751
});
6852

6953
test("getConfigFileInput ignores empty repository property value", async (t) => {
@@ -76,22 +60,13 @@ test("getConfigFileInput ignores empty repository property value", async (t) =>
7660

7761
test("getConfigFileInput ignores repository property value when FF is off", async (t) => {
7862
// Since the FF is off, we should ignore the repository property value.
79-
const target = callee(getConfigFileInput)
63+
await callee(getConfigFileInput)
8064
.withFeatures([])
81-
.withArgs(repositoryProperties);
82-
83-
await target.passes(t.is, undefined);
84-
85-
t.false(
86-
target
87-
.getLogger()
88-
.hasMessage("Using configuration file input from repository property"),
89-
);
90-
t.true(
91-
target
92-
.getLogger()
93-
.hasMessage(
94-
"Ignoring configuration file input from repository property, because the corresponding feature flag is disabled.",
95-
),
96-
);
65+
.withArgs(repositoryProperties)
66+
.notLogs(t, "Using configuration file input from repository property")
67+
.logs(
68+
t,
69+
"Ignoring configuration file input from repository property, because the corresponding feature flag is disabled.",
70+
)
71+
.passes(t.is, undefined);
9772
});

src/testing-utils.ts

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,12 @@ export function initAllState(
210210
};
211211
}
212212

213+
type DelayedCheck<
214+
Args extends readonly any[],
215+
R,
216+
Fs extends ReadonlyArray<AllState[number]>,
217+
> = (env: Readonly<BaseEnvBuilder<Args, R, Fs>>) => Promise<any>;
218+
213219
/**
214220
* Wraps a function that accepts an `ActionState` for testing in different environments.
215221
*/
@@ -221,6 +227,7 @@ abstract class BaseEnvBuilder<
221227
protected readonly fn: (state: ActionState<Fs>, ...args: Args) => R;
222228
private logger: RecordingLogger;
223229
protected state: ActionState<AllState>;
230+
protected checks: Array<DelayedCheck<Args, R, Fs>>;
224231

225232
constructor(
226233
fn: (state: ActionState<Fs>, ...args: Args) => R,
@@ -232,6 +239,7 @@ abstract class BaseEnvBuilder<
232239
cloneFrom !== undefined
233240
? { ...cloneFrom.state, logger: this.logger }
234241
: initAllState({ logger: this.logger });
242+
this.checks = [...(cloneFrom?.checks ?? [])];
235243
}
236244

237245
/**
@@ -270,6 +278,30 @@ abstract class BaseEnvBuilder<
270278
result.state.actions = actions;
271279
return result;
272280
}
281+
282+
/**
283+
* Adds a delayed check that `messages` are logged. The check will be
284+
* performed after the main assertion passes.
285+
*/
286+
public logs(t: ExecutionContext<unknown>, ...messages: string[]): this {
287+
const result = this.clone();
288+
result.checks.push(async (env) => {
289+
checkExpectedLogMessages(t, env.getLogger().messages, messages);
290+
});
291+
return result;
292+
}
293+
294+
/**
295+
* Adds a delayed check that `messages` are not logged. The check will be
296+
* performed after the main assertion passes.
297+
*/
298+
public notLogs(t: ExecutionContext<unknown>, ...messages: string[]): this {
299+
const result = this.clone();
300+
result.checks.push(async (env) => {
301+
checkUnexpectedLogMessages(t, env.getLogger().messages, messages);
302+
});
303+
return result;
304+
}
273305
}
274306

275307
class EnvBuilder<
@@ -322,8 +354,21 @@ class CallableEnvBuilder<
322354
assertion: (val: Awaited<R>, ...assertionArgs: AArgs) => AResult,
323355
...assertionArgs: AArgs
324356
): Promise<AResult> {
357+
// this.call() may or may not return a promise,
358+
// `Promise.resolve` turns the result into one if it isn't already,
359+
// and we then await it. That ensures that `result` is an `Awaited<R>`.
325360
const result = await Promise.resolve(this.call());
326-
return assertion(result, ...assertionArgs);
361+
362+
// Run the main assertion on the `result`.
363+
const assertionResult = assertion(result, ...assertionArgs);
364+
365+
// Run other delayed checks.
366+
for (const delayedCheck of this.checks) {
367+
await delayedCheck(this);
368+
}
369+
370+
// Return the result of the main assertion.
371+
return assertionResult;
327372
}
328373

329374
/**
@@ -337,7 +382,19 @@ class CallableEnvBuilder<
337382
t: ExecutionContext<unknown>,
338383
expectations?: ThrowsExpectation<ErrorType>,
339384
): Promise<ThrownError<ErrorType>> {
340-
return t.throwsAsync(() => Promise.resolve(this.call()), expectations);
385+
// Run the main assertion.
386+
const error = t.throwsAsync(
387+
() => Promise.resolve(this.call()),
388+
expectations,
389+
);
390+
391+
// Run other delayed checks.
392+
for (const delayedCheck of this.checks) {
393+
await delayedCheck(this);
394+
}
395+
396+
// Return the error.
397+
return error;
341398
}
342399
}
343400

@@ -542,6 +599,34 @@ export function checkExpectedLogMessages(
542599
}
543600
}
544601

602+
/**
603+
* Checks that `messages` contains none of `unexpectedMessages`.
604+
*/
605+
export function checkUnexpectedLogMessages(
606+
t: ExecutionContext<any>,
607+
messages: LoggedMessage[],
608+
unexpectedMessages: string[],
609+
) {
610+
const presentMessages: string[] = [];
611+
612+
for (const unexpectedMessage of unexpectedMessages) {
613+
if (hasLoggedMessage(messages, unexpectedMessage)) {
614+
presentMessages.push(unexpectedMessage);
615+
}
616+
}
617+
618+
if (presentMessages.length > 0) {
619+
const listify = (lines: string[]) =>
620+
lines.map((m) => ` - '${m}'`).join("\n");
621+
622+
t.fail(
623+
`Did not expect\n\n${listify(presentMessages)}\n\nin the logger output, but found them in:\n\n${messages.map((m) => ` - '${m.message}'`).join("\n")}`,
624+
);
625+
} else {
626+
t.pass();
627+
}
628+
}
629+
545630
/**
546631
* Asserts that `message` should not have been logged to `logger`.
547632
*/

0 commit comments

Comments
 (0)