Skip to content

Commit 9e721c9

Browse files
committed
polish and fix some of the polling output
1 parent a56d0fe commit 9e721c9

File tree

4 files changed

+85
-48
lines changed

4 files changed

+85
-48
lines changed

src/apptesting/invokeTests.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { Client } from "../apiv2";
22
import { appTestingOrigin } from "../api";
3-
import { InvokeTestCasesRequest, TestCaseInvocation, TestInvocation } from "./types";
3+
import {
4+
InvokedTestCases,
5+
InvokeTestCasesRequest,
6+
TestCaseInvocation,
7+
TestInvocation,
8+
} from "./types";
49
import * as operationPoller from "../operation-poller";
510
import { FirebaseError, getError } from "../error";
611

@@ -15,7 +20,7 @@ export async function invokeTests(appId: string, startUri: string, testDefs: Tes
1520
>(`${appResource}/testInvocations:invokeTestCases`, buildInvokeTestCasesRequest(testDefs));
1621
return invocationResponse.body;
1722
} catch (err: unknown) {
18-
throw new FirebaseError("Test invocation failed", {original: getError(err)});
23+
throw new FirebaseError("Test invocation failed", { original: getError(err) });
1924
}
2025
}
2126

@@ -30,19 +35,23 @@ function buildInvokeTestCasesRequest(
3035
};
3136
}
3237

38+
interface InvocationOperation {
39+
resource: InvokedTestCases;
40+
}
41+
3342
export async function pollInvocationStatus(
3443
operationName: string,
35-
onPoll: (invocation: operationPoller.OperationResult<TestInvocation>) => void,
44+
onPoll: (invocation: operationPoller.OperationResult<InvocationOperation>) => void,
3645
backoff = 30 * 1000,
37-
): Promise<TestInvocation> {
38-
return operationPoller.pollOperation<TestInvocation>({
46+
): Promise<InvocationOperation> {
47+
return operationPoller.pollOperation<InvocationOperation>({
3948
pollerName: "App Testing Invocation Poller",
4049
apiOrigin: appTestingOrigin(),
4150
apiVersion: "v1alpha",
4251
operationResourceName: operationName,
4352
masterTimeout: 30 * 60 * 1000, // 30 minutes
4453
backoff,
45-
maxBackoff: 30 * 1000, // 30 seconds
54+
maxBackoff: 15 * 1000, // 30 seconds
4655
onPoll,
4756
});
4857
}

src/apptesting/parseTestFiles.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { join } from "path";
33
import { logger } from "../logger";
44
import { Browser, TestCaseInvocation } from "./types";
55
import { readFileFromDirectory, wrappedSafeLoad } from "../utils";
6-
import { getErrMsg } from "../error";
6+
import { FirebaseError, getErrMsg, getError } from "../error";
77

88
function createFilter(pattern?: string) {
99
const regex = pattern ? new RegExp(pattern) : undefined;
@@ -16,6 +16,12 @@ export async function parseTestFiles(
1616
filePattern?: string,
1717
namePattern?: string,
1818
): Promise<TestCaseInvocation[]> {
19+
try {
20+
targetUri = new URL(targetUri).toString();
21+
} catch (ex) {
22+
const errMsg = "Invalid URL" + (targetUri.startsWith("http") ? "" : " (must include protocol)");
23+
throw new FirebaseError(errMsg, { original: getError(ex) });
24+
}
1925
const fileFilterFn = createFilter(filePattern);
2026
const nameFilterFn = createFilter(namePattern);
2127

@@ -43,7 +49,7 @@ export async function parseTestFiles(
4349
}
4450
} catch (ex) {
4551
const errMsg = getErrMsg(ex);
46-
const errDetails = errMsg ? `Error details: \n${errMsg}` : '';
52+
const errDetails = errMsg ? `Error details: \n${errMsg}` : "";
4753
logger.info(`Unable to parse test file ${path}. Ignoring.${errDetails}`);
4854
continue;
4955
}

src/apptesting/types.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,17 @@ export interface InvokedTestCases {
1414
testCaseInvocations: TestCaseInvocation[];
1515
}
1616

17-
export interface TestInvocation {
18-
name?: string;
19-
createTime?: string;
17+
export interface ExecutionMetadata {
2018
runningExecutions?: number;
2119
succeededExecutions?: number;
2220
failedExecutions?: number;
21+
totalExecutions?: number;
22+
cancelledExecutions?: number;
23+
}
24+
25+
export interface TestInvocation extends ExecutionMetadata {
26+
name?: string;
27+
createTime?: string;
2328
}
2429

2530
export interface TestCaseInvocation {

src/commands/apptesting-execute.ts

Lines changed: 54 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,19 @@ import * as clc from "colorette";
66
import { parseTestFiles } from "../apptesting/parseTestFiles";
77
import * as ora from "ora";
88
import { invokeTests, pollInvocationStatus } from "../apptesting/invokeTests";
9-
import { TestInvocation } from "../apptesting/types";
9+
import { ExecutionMetadata } from "../apptesting/types";
1010
import { FirebaseError } from "../error";
1111
import { marked } from "marked";
1212
import { needProjectId } from "../projectUtils";
1313
import { consoleUrl } from "../utils";
1414
import { AppPlatform, listFirebaseApps } from "../management/apps";
1515

1616
export const command = new Command("apptesting:execute <target>")
17-
.description(
18-
"upload a release binary and optionally distribute it to testers and run automated tests",
17+
.description("Run automated tests written in natural language driven by AI")
18+
.option(
19+
"--app <app_id>",
20+
"The app id of your Firebase web app. Optional if the project contains exactly one web app.",
1921
)
20-
.option("--app <app_id>", "the app id of your Firebase app")
2122
.option(
2223
"--test-file-pattern <pattern>",
2324
"Test file pattern. Only tests contained in files that match this pattern will be executed.",
@@ -30,15 +31,13 @@ export const command = new Command("apptesting:execute <target>")
3031
.before(requireAuth)
3132
.before(requireConfig)
3233
.action(async (target: string, options: any) => {
33-
if (!options.app) {
34-
throw new FirebaseError("App is required");
35-
}
36-
3734
const projectId = needProjectId(options);
3835
const appList = await listFirebaseApps(projectId, AppPlatform.WEB);
39-
const app = appList.find((a) => a.appId === options.app);
40-
41-
if (!app) {
36+
let app = appList.find((a) => a.appId === options.app);
37+
if (!app && appList.length === 1) {
38+
app = appList[0];
39+
logger.info(`No app specified, defaulting to ${app.appId}`);
40+
} else {
4241
throw new FirebaseError("Invalid app id");
4342
}
4443

@@ -54,13 +53,20 @@ export const command = new Command("apptesting:execute <target>")
5453
throw new FirebaseError("No tests found");
5554
}
5655

57-
logger.info(clc.bold(`\n${clc.white("===")} Running ${tests.length} tests`));
58-
59-
const invokeSpinner = ora("Sending test request");
56+
const invokeSpinner = ora("Requesting test execution");
6057
invokeSpinner.start();
61-
const invocationOperation = await invokeTests(options.app, target, tests);
62-
invokeSpinner.text = "Testing started";
63-
invokeSpinner.succeed();
58+
59+
let invocationOperation;
60+
try {
61+
invocationOperation = await invokeTests(app.appId, target, tests);
62+
invokeSpinner.text = "Test execution requested";
63+
invokeSpinner.succeed();
64+
} catch (ex) {
65+
invokeSpinner.fail("Failed to request test execution");
66+
throw ex;
67+
}
68+
69+
logger.info(clc.bold(`\n${clc.white("===")} Running ${pluralizeTests(tests.length)}`));
6470

6571
const invocationId = invocationOperation.name?.split("/").pop();
6672

@@ -84,29 +90,40 @@ export const command = new Command("apptesting:execute <target>")
8490

8591
const executionSpinner = ora(getOutput(invocationOperation.metadata));
8692
executionSpinner.start();
87-
await pollInvocationStatus(invocationOperation.name, (operation) => {
88-
if (!operation.response) {
89-
logger.info("invocation details unavailable");
90-
return;
93+
const invocationOp = await pollInvocationStatus(invocationOperation.name, (operation) => {
94+
if (!operation.done) {
95+
executionSpinner.text = getOutput(operation.metadata as ExecutionMetadata);
9196
}
92-
executionSpinner.text = getOutput(operation.metadata as TestInvocation);
9397
});
94-
executionSpinner.succeed();
98+
const response = invocationOp.resource.testInvocation;
99+
executionSpinner.text = `Testing complete\n${getOutput(response)}`;
100+
if (response.failedExecutions || response.cancelledExecutions) {
101+
executionSpinner.fail();
102+
throw new FirebaseError("Testing complete with errors");
103+
} else {
104+
executionSpinner.succeed();
105+
}
95106
});
96107

97-
function getOutput(invocation: TestInvocation) {
98-
if (!invocation.failedExecutions && !invocation.runningExecutions) {
99-
return "All tests passed";
108+
function pluralizeTests(numTests: number) {
109+
return `${numTests} test${numTests === 1 ? "" : "s"}`;
110+
}
111+
112+
function getOutput(invocation: ExecutionMetadata) {
113+
const output = [];
114+
if (invocation.runningExecutions) {
115+
output.push(
116+
`${pluralizeTests(invocation.runningExecutions)} running (this may take a while)...`,
117+
);
118+
}
119+
if (invocation.succeededExecutions) {
120+
output.push(`✔ ${pluralizeTests(invocation.succeededExecutions)} passed`);
121+
}
122+
if (invocation.failedExecutions) {
123+
output.push(`✖ ${pluralizeTests(invocation.failedExecutions)} failed`);
124+
}
125+
if (invocation.cancelledExecutions) {
126+
output.push(`⊝ ${pluralizeTests(invocation.cancelledExecutions)} cancelled`);
100127
}
101-
return [
102-
invocation.runningExecutions
103-
? `${invocation.runningExecutions} tests still running (this may take a while)...`
104-
: undefined,
105-
invocation.succeededExecutions
106-
? `✔ ${invocation.succeededExecutions} tests passing`
107-
: undefined,
108-
invocation.failedExecutions ? `✖ ${invocation.failedExecutions} tests failing` : undefined,
109-
]
110-
.filter((a) => a)
111-
.join("\n");
128+
return output.length ? output.join("\n") : "Tests are starting";
112129
}

0 commit comments

Comments
 (0)