Skip to content

feat: Add support for running functions emulator per codebase #8422

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 141 additions & 21 deletions src/deploy/functions/functionsDeployHelper.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ describe("functionsDeployHelper", () => {
interface Testcase {
desc: string;
selector: string;
strict?: boolean;
expected: EndpointFilter[];
}

Expand All @@ -172,11 +173,11 @@ describe("functionsDeployHelper", () => {
selector: "func",
expected: [
{
codebase: DEFAULT_CODEBASE,
idChunks: ["func"],
codebase: "func",
},
{
codebase: "func",
codebase: DEFAULT_CODEBASE,
idChunks: ["func"],
},
],
},
Expand All @@ -185,30 +186,71 @@ describe("functionsDeployHelper", () => {
selector: "g1.func",
expected: [
{
codebase: DEFAULT_CODEBASE,
idChunks: ["g1", "func"],
codebase: "g1.func",
},
{
codebase: "g1.func",
codebase: DEFAULT_CODEBASE,
idChunks: ["g1", "func"],
},
],
},
{
desc: "parses group selector (with '-') without codebase",
selector: "g1-func",
expected: [
{
codebase: "g1-func",
},
{
codebase: DEFAULT_CODEBASE,
idChunks: ["g1", "func"],
},
],
},
{
desc: "parses group selector (with '-') with codebase",
selector: "node:g1-func",
expected: [
{
codebase: "node",
idChunks: ["g1", "func"],
},
],
},
{
desc: "parses selector without codebase (strict)",
selector: "func",
strict: true,
expected: [
{
codebase: "func",
},
],
},
{
desc: "parses group selector (with '.') without codebase (strict)",
selector: "g1.func",
strict: true,
expected: [
{
codebase: "g1.func",
},
],
},
{
desc: "parses group selector (with '-') without codebase (strict)",
selector: "g1-func",
strict: true,
expected: [
{
codebase: "g1-func",
},
],
},
{
desc: "parses group selector (with '-') with codebase",
desc: "parses group selector (with '-') with codebase (strict)",
selector: "node:g1-func",
strict: true,
expected: [
{
codebase: "node",
Expand All @@ -220,10 +262,9 @@ describe("functionsDeployHelper", () => {

for (const tc of testcases) {
it(tc.desc, () => {
const actual = parseFunctionSelector(tc.selector);

expect(actual.length).to.equal(tc.expected.length);
expect(actual).to.deep.include.members(tc.expected);
const actual = parseFunctionSelector(tc.selector, tc.strict ?? false);
expect(actual).to.have.length(tc.expected.length);
expect(actual).to.have.deep.members(tc.expected);
});
}
});
Expand All @@ -232,46 +273,105 @@ describe("functionsDeployHelper", () => {
interface Testcase {
desc: string;
only: string;
expected: EndpointFilter[];
strict?: boolean;
expected: EndpointFilter[] | undefined;
}

const testcases: Testcase[] = [
{
desc: "should return undefined given no only option",
only: "",
expected: undefined,
},
{
desc: "should return undefined given no functions selector",
only: "hosting:siteA,storage:bucketB",
expected: undefined,
},
{
desc: "should parse multiple selectors",
only: "functions:myFunc,functions:myOtherFunc",
expected: [
{
codebase: "myFunc",
},
{
codebase: DEFAULT_CODEBASE,
idChunks: ["myFunc"],
},
{
codebase: "myFunc",
codebase: "myOtherFunc",
},
{
codebase: DEFAULT_CODEBASE,
idChunks: ["myOtherFunc"],
},
{
codebase: "myOtherFunc",
},
],
},
{
desc: "should parse nested selector",
only: "functions:groupA.myFunc",
expected: [
{
codebase: "groupA.myFunc",
},
{
codebase: DEFAULT_CODEBASE,
idChunks: ["groupA", "myFunc"],
},
],
},
{
desc: "should parse selector with codebase",
only: "functions:my-codebase:myFunc,functions:another-codebase:anotherFunc",
expected: [
{
codebase: "my-codebase",
idChunks: ["myFunc"],
},
{
codebase: "another-codebase",
idChunks: ["anotherFunc"],
},
],
},
{
desc: "should parse nested selector with codebase",
only: "functions:my-codebase:groupA.myFunc",
expected: [
{
codebase: "my-codebase",
idChunks: ["groupA", "myFunc"],
},
],
},
{
desc: "should parse multiple selectors (strict)",
only: "functions:myFunc,functions:myOtherFunc",
strict: true,
expected: [
{
codebase: "myFunc",
},
{
codebase: "myOtherFunc",
},
],
},
{
desc: "should parse nested selector (strict)",
only: "functions:groupA.myFunc",
strict: true,
expected: [
{
codebase: "groupA.myFunc",
},
],
},
{
desc: "should parse selector with codebase",
desc: "should parse selector with codebase (strict)",
only: "functions:my-codebase:myFunc,functions:another-codebase:anotherFunc",
strict: true,
expected: [
{
codebase: "my-codebase",
Expand All @@ -284,15 +384,30 @@ describe("functionsDeployHelper", () => {
],
},
{
desc: "should parse nested selector with codebase",
desc: "should parse nested selector with codebase (strict)",
only: "functions:my-codebase:groupA.myFunc",
strict: true,
expected: [
{
codebase: "my-codebase",
idChunks: ["groupA", "myFunc"],
},
],
},
{
desc: "should parse mixed selectors (strict)",
only: "functions:myFunc,functions:another:anotherFunc",
strict: true,
expected: [
{
codebase: "myFunc",
},
{
codebase: "another",
idChunks: ["anotherFunc"],
},
],
},
];

for (const tc of testcases) {
Expand All @@ -301,10 +416,15 @@ describe("functionsDeployHelper", () => {
only: tc.only,
} as Options;

const actual = helper.getEndpointFilters(options);
const actual = helper.getEndpointFilters(options, !!tc.strict);

expect(actual?.length).to.equal(tc.expected.length);
expect(actual).to.deep.include.members(tc.expected);
if (tc.expected === undefined) {
expect(actual).to.be.undefined;
} else {
expect(actual).to.not.be.undefined;
expect(actual).to.have.length(tc.expected.length);
expect(actual).to.have.deep.members(tc.expected);
}
});
}

Expand Down
24 changes: 16 additions & 8 deletions src/deploy/functions/functionsDeployHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
/**
* Returns list of filters after parsing selector.
*/
export function parseFunctionSelector(selector: string): EndpointFilter[] {
export function parseFunctionSelector(selector: string, strict: boolean): EndpointFilter[] {
const fragments = selector.split(":");
if (fragments.length < 2) {
// This is a plain selector w/o codebase prefix (e.g. "abc" not "abc:efg") .
Expand All @@ -67,10 +67,13 @@
//
// We decide here to create filter for both conditions. This sounds sloppy, but it's only troublesome if there is
// conflict between a codebase name as function id in the default codebase.
return [
{ codebase: fragments[0] },
{ codebase: DEFAULT_CODEBASE, idChunks: fragments[0].split(/[-.]/) },
];
//
// In strict mode, 'default' codebase is not optional and we always assume it is a codebase fragment.
const filters: EndpointFilter[] = [{ codebase: fragments[0] }];
if (!strict) {
filters.push({ codebase: DEFAULT_CODEBASE, idChunks: fragments[0].split(/[-.]/) });
}
return filters;
}
return [
{
Expand Down Expand Up @@ -98,11 +101,16 @@
* 2) Grouped functions w/ "abc" prefix in the default codebase?
* 3) All functions in the "abc" codebase?
*
* Current implementation creates filters that match against all conditions.
* The default implementation creates filters that match against all conditions.
*
* In "strict" mode, we always assume the only filter follows the format functions:[codebase]:[fn].
*
* If no filter exists, we return undefined which the caller should interpret as "match all functions".
*/
export function getEndpointFilters(options: { only?: string }): EndpointFilter[] | undefined {
export function getEndpointFilters(
options: { only?: string },
strict = false,
): EndpointFilter[] | undefined {
if (!options.only) {
return undefined;
}
Expand All @@ -113,7 +121,7 @@
if (selector.startsWith("functions:")) {
selector = selector.replace("functions:", "");
if (selector.length > 0) {
filters.push(...parseFunctionSelector(selector));
filters.push(...parseFunctionSelector(selector, strict));
}
}
}
Expand Down Expand Up @@ -225,6 +233,6 @@
}

/** Checks if a function should be filtered given a list of endpoints. */
export function isEndpointFiltered(endpoint: backend.Endpoint, filters: EndpointFilter[]) {

Check warning on line 236 in src/deploy/functions/functionsDeployHelper.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing return type on function
return filters.some((filter) => endpointMatchesFilter(endpoint, filter));
}
Loading
Loading