Skip to content

Commit a91c8f0

Browse files
authored
Merge branch 'master' into master
2 parents bd23462 + 5e2c5f8 commit a91c8f0

File tree

18 files changed

+403
-107
lines changed

18 files changed

+403
-107
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "firebase-functions",
3-
"version": "6.3.1",
3+
"version": "6.4.0",
44
"description": "Firebase SDK for Cloud Functions",
55
"keywords": [
66
"firebase",
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
const functions = require("firebase-functions");
2+
3+
// This will cause a syntax error
4+
exports.broken = functions.https.onRequest((request, response) => {
5+
response.send("Hello from Firebase!"
6+
}); // Missing closing parenthesis
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"name": "broken-syntax"
3+
}

scripts/bin-test/test.ts

Lines changed: 139 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import * as subprocess from "child_process";
22
import * as path from "path";
33
import { promisify } from "util";
4+
import * as fs from "fs/promises";
5+
import * as os from "os";
46

57
import { expect } from "chai";
68
import * as yaml from "js-yaml";
@@ -105,7 +107,13 @@ const BASE_STACK = {
105107
interface Testcase {
106108
name: string;
107109
modulePath: string;
108-
expected: Record<string, any>;
110+
expected: Record<string, unknown>;
111+
}
112+
113+
interface DiscoveryResult {
114+
success: boolean;
115+
manifest?: Record<string, unknown>;
116+
error?: string;
109117
}
110118

111119
async function retryUntil(
@@ -134,102 +142,134 @@ async function retryUntil(
134142
await Promise.race([retry, timedOut]);
135143
}
136144

137-
async function startBin(
138-
tc: Testcase,
139-
debug?: boolean
140-
): Promise<{ port: number; cleanup: () => Promise<void> }> {
145+
async function runHttpDiscovery(modulePath: string): Promise<DiscoveryResult> {
141146
const getPort = promisify(portfinder.getPort) as () => Promise<number>;
142147
const port = await getPort();
143148

144149
const proc = subprocess.spawn("npx", ["firebase-functions"], {
145-
cwd: path.resolve(tc.modulePath),
150+
cwd: path.resolve(modulePath),
146151
env: {
147152
PATH: process.env.PATH,
148-
GLCOUD_PROJECT: "test-project",
153+
GCLOUD_PROJECT: "test-project",
149154
PORT: port.toString(),
150155
FUNCTIONS_CONTROL_API: "true",
151156
},
152157
});
153-
if (!proc) {
154-
throw new Error("Failed to start firebase functions");
155-
}
156-
proc.stdout?.on("data", (chunk: Buffer) => {
157-
console.log(chunk.toString("utf8"));
158-
});
159-
proc.stderr?.on("data", (chunk: Buffer) => {
160-
console.log(chunk.toString("utf8"));
161-
});
162158

163-
await retryUntil(async () => {
164-
try {
165-
await fetch(`http://localhost:${port}/__/functions.yaml`);
166-
} catch (e) {
167-
if (e?.code === "ECONNREFUSED") {
168-
return false;
159+
try {
160+
// Wait for server to be ready
161+
await retryUntil(async () => {
162+
try {
163+
await fetch(`http://localhost:${port}/__/functions.yaml`);
164+
return true;
165+
} catch (e: unknown) {
166+
const error = e as { code?: string };
167+
if (error.code === "ECONNREFUSED") {
168+
// This is an expected error during server startup, so we should retry.
169+
return false;
170+
}
171+
// Any other error is unexpected and should fail the test immediately.
172+
throw e;
169173
}
170-
throw e;
174+
}, TIMEOUT_L);
175+
176+
const res = await fetch(`http://localhost:${port}/__/functions.yaml`);
177+
const body = await res.text();
178+
179+
if (res.status === 200) {
180+
const manifest = yaml.load(body) as Record<string, unknown>;
181+
return { success: true, manifest };
182+
} else {
183+
return { success: false, error: body };
171184
}
172-
return true;
173-
}, TIMEOUT_L);
185+
} finally {
186+
if (proc.pid) {
187+
proc.kill(9);
188+
await new Promise<void>((resolve) => proc.on("exit", resolve));
189+
}
190+
}
191+
}
174192

175-
if (debug) {
176-
proc.stdout?.on("data", (data: unknown) => {
177-
console.log(`[${tc.name} stdout] ${data}`);
193+
async function runFileDiscovery(modulePath: string): Promise<DiscoveryResult> {
194+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "firebase-functions-test-"));
195+
const outputPath = path.join(tempDir, "manifest.json");
196+
197+
return new Promise((resolve, reject) => {
198+
const proc = subprocess.spawn("npx", ["firebase-functions"], {
199+
cwd: path.resolve(modulePath),
200+
env: {
201+
PATH: process.env.PATH,
202+
GCLOUD_PROJECT: "test-project",
203+
FUNCTIONS_MANIFEST_OUTPUT_PATH: outputPath,
204+
},
178205
});
179206

180-
proc.stderr?.on("data", (data: unknown) => {
181-
console.log(`[${tc.name} stderr] ${data}`);
207+
let stderr = "";
208+
209+
proc.stderr?.on("data", (chunk: Buffer) => {
210+
stderr += chunk.toString("utf8");
182211
});
183-
}
184212

185-
return {
186-
port,
187-
cleanup: async () => {
188-
process.kill(proc.pid, 9);
189-
await retryUntil(async () => {
213+
const timeoutId = setTimeout(async () => {
214+
if (proc.pid) {
215+
proc.kill(9);
216+
await new Promise<void>((resolve) => proc.on("exit", resolve));
217+
}
218+
resolve({ success: false, error: `File discovery timed out after ${TIMEOUT_M}ms` });
219+
}, TIMEOUT_M);
220+
221+
proc.on("close", async (code) => {
222+
clearTimeout(timeoutId);
223+
224+
if (code === 0) {
190225
try {
191-
process.kill(proc.pid, 0);
192-
} catch {
193-
// process.kill w/ signal 0 will throw an error if the pid no longer exists.
194-
return Promise.resolve(true);
226+
const manifestJson = await fs.readFile(outputPath, "utf8");
227+
const manifest = JSON.parse(manifestJson) as Record<string, unknown>;
228+
await fs.rm(tempDir, { recursive: true }).catch(() => {
229+
// Ignore errors
230+
});
231+
resolve({ success: true, manifest });
232+
} catch (e) {
233+
resolve({ success: false, error: `Failed to read manifest file: ${e}` });
195234
}
196-
return Promise.resolve(false);
197-
}, TIMEOUT_L);
198-
},
199-
};
235+
} else {
236+
const errorLines = stderr.split("\n").filter((line) => line.trim());
237+
const errorMessage = errorLines.join(" ") || "No error message found";
238+
resolve({ success: false, error: errorMessage });
239+
}
240+
});
241+
242+
proc.on("error", (err) => {
243+
clearTimeout(timeoutId);
244+
// Clean up temp directory on error
245+
fs.rm(tempDir, { recursive: true }).catch(() => {
246+
// Ignore errors
247+
});
248+
reject(err);
249+
});
250+
});
200251
}
201252

202253
describe("functions.yaml", function () {
203254
// eslint-disable-next-line @typescript-eslint/no-invalid-this
204255
this.timeout(TIMEOUT_XL);
205256

206-
function runTests(tc: Testcase) {
207-
let port: number;
208-
let cleanup: () => Promise<void>;
209-
210-
before(async () => {
211-
const r = await startBin(tc);
212-
port = r.port;
213-
cleanup = r.cleanup;
214-
});
257+
const discoveryMethods = [
258+
{ name: "http", fn: runHttpDiscovery },
259+
{ name: "file", fn: runFileDiscovery },
260+
];
215261

216-
after(async () => {
217-
await cleanup?.();
218-
});
219-
220-
it("functions.yaml returns expected Manifest", async function () {
262+
function runDiscoveryTests(
263+
tc: Testcase,
264+
discoveryFn: (path: string) => Promise<DiscoveryResult>
265+
) {
266+
it("returns expected manifest", async function () {
221267
// eslint-disable-next-line @typescript-eslint/no-invalid-this
222268
this.timeout(TIMEOUT_M);
223269

224-
const res = await fetch(`http://localhost:${port}/__/functions.yaml`);
225-
const text = await res.text();
226-
let parsed: any;
227-
try {
228-
parsed = yaml.load(text);
229-
} catch (err) {
230-
throw new Error(`Failed to parse functions.yaml: ${err}`);
231-
}
232-
expect(parsed).to.be.deep.equal(tc.expected);
270+
const result = await discoveryFn(tc.modulePath);
271+
expect(result.success).to.be.true;
272+
expect(result.manifest).to.deep.equal(tc.expected);
233273
});
234274
}
235275

@@ -320,7 +360,11 @@ describe("functions.yaml", function () {
320360

321361
for (const tc of testcases) {
322362
describe(tc.name, () => {
323-
runTests(tc);
363+
for (const discovery of discoveryMethods) {
364+
describe(`${discovery.name} discovery`, () => {
365+
runDiscoveryTests(tc, discovery.fn);
366+
});
367+
}
324368
});
325369
}
326370
});
@@ -350,7 +394,33 @@ describe("functions.yaml", function () {
350394

351395
for (const tc of testcases) {
352396
describe(tc.name, () => {
353-
runTests(tc);
397+
for (const discovery of discoveryMethods) {
398+
describe(`${discovery.name} discovery`, () => {
399+
runDiscoveryTests(tc, discovery.fn);
400+
});
401+
}
402+
});
403+
}
404+
});
405+
406+
describe("error handling", () => {
407+
const errorTestcases = [
408+
{
409+
name: "broken syntax",
410+
modulePath: "./scripts/bin-test/sources/broken-syntax",
411+
expectedError: "missing ) after argument list",
412+
},
413+
];
414+
415+
for (const tc of errorTestcases) {
416+
describe(tc.name, () => {
417+
for (const discovery of discoveryMethods) {
418+
it(`${discovery.name} discovery handles error correctly`, async () => {
419+
const result = await discovery.fn(tc.modulePath);
420+
expect(result.success).to.be.false;
421+
expect(result.error).to.include(tc.expectedError);
422+
});
423+
}
354424
});
355425
}
356426
});

spec/helper.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,7 @@ export function runHandler(
7878

7979
const toSend = typeof sendBody === "object" ? JSON.stringify(sendBody) : sendBody;
8080
const body =
81-
typeof this.sentBody === "undefined"
82-
? toSend
83-
: this.sentBody + String(toSend || "");
81+
typeof this.sentBody === "undefined" ? toSend : this.sentBody + String(toSend || "");
8482
this.end(body);
8583
}
8684

spec/logger.spec.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,50 @@ describe("logger", () => {
118118
});
119119
});
120120

121+
it("should not detect duplicate object as circular", () => {
122+
const obj: any = { a: "foo" };
123+
const entry: logger.LogEntry = {
124+
severity: "ERROR",
125+
message: "testing circular",
126+
a: obj,
127+
b: obj,
128+
};
129+
logger.write(entry);
130+
expectStderr({
131+
severity: "ERROR",
132+
message: "testing circular",
133+
a: { a: "foo" },
134+
b: { a: "foo" },
135+
});
136+
});
137+
138+
it("should not detect duplicate object in array as circular", () => {
139+
const obj: any = { a: "foo" };
140+
const arr: any = [
141+
{ a: obj, b: obj },
142+
{ a: obj, b: obj },
143+
];
144+
const entry: logger.LogEntry = {
145+
severity: "ERROR",
146+
message: "testing circular",
147+
a: arr,
148+
b: arr,
149+
};
150+
logger.write(entry);
151+
expectStderr({
152+
severity: "ERROR",
153+
message: "testing circular",
154+
a: [
155+
{ a: { a: "foo" }, b: { a: "foo" } },
156+
{ a: { a: "foo" }, b: { a: "foo" } },
157+
],
158+
b: [
159+
{ a: { a: "foo" }, b: { a: "foo" } },
160+
{ a: { a: "foo" }, b: { a: "foo" } },
161+
],
162+
});
163+
});
164+
121165
it("should not break on objects that override toJSON", () => {
122166
const obj: any = { a: new Date("August 26, 1994 12:24:00Z") };
123167

0 commit comments

Comments
 (0)