Files
languageservices/actions-languageservice/src/complete.expressions.test.ts
T

690 lines
17 KiB
TypeScript
Raw Normal View History

2022-11-15 15:59:14 -08:00
import {data} from "@github/actions-expressions";
2022-11-15 13:03:44 -08:00
import {complete, getExpressionInput} from "./complete";
import {ContextProviderConfig} from "./context-providers/config";
2022-12-08 10:17:29 -08:00
import {registerLogger} from "./log";
2022-11-15 13:03:44 -08:00
import {getPositionFromCursor} from "./test-utils/cursor-position";
2022-12-08 10:17:29 -08:00
import {TestLogger} from "./test-utils/logger";
2022-11-15 13:03:44 -08:00
const contextProviderConfig: ContextProviderConfig = {
2022-11-29 16:56:20 -05:00
getContext: async (context: string) => {
2022-11-15 15:59:14 -08:00
switch (context) {
case "github":
return new data.Dictionary({
key: "event",
value: new data.StringData("push")
});
2022-11-15 13:03:44 -08:00
}
2022-11-15 15:59:14 -08:00
return undefined;
2022-11-15 13:03:44 -08:00
}
};
2022-12-08 10:17:29 -08:00
registerLogger(new TestLogger());
2022-11-15 13:03:44 -08:00
describe("expressions", () => {
it("input extraction", () => {
const test = (input: string) => {
const [doc, pos] = getPositionFromCursor(input);
return getExpressionInput(doc.getText(), pos.character);
};
expect(test("${{ gh |")).toBe(" gh ");
expect(test("${{ gh |}}")).toBe(" gh ");
expect(test("${{ vars }} ${{ gh |}}")).toBe(" gh ");
});
describe("top-level auto-complete", () => {
it("single region", async () => {
const input = "run-name: ${{ | }}";
2022-12-12 19:06:08 -05:00
const result = await complete(...getPositionFromCursor(input), undefined, undefined);
2022-11-15 13:03:44 -08:00
expect(result.map(x => x.label)).toEqual([
"github",
"inputs",
"vars",
"contains",
"endsWith",
"format",
"fromJson",
"join",
"startsWith",
"toJson"
]);
});
it("single region with existing input", async () => {
const input = "run-name: ${{ g| }}";
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
expect(result.map(x => x.label)).toEqual([
"github",
"inputs",
"vars",
"contains",
"endsWith",
"format",
"fromJson",
"join",
"startsWith",
"toJson"
]);
});
it("multiple regions", async () => {
const input = "run-name: test-${{ github }}-${{ | }}";
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
expect(result.map(x => x.label)).toEqual([
"github",
"inputs",
"vars",
"contains",
"endsWith",
"format",
"fromJson",
"join",
"startsWith",
"toJson"
]);
});
2022-12-08 16:38:36 -08:00
describe("multi-line strings", () => {
it("indented |", async () => {
const input = `on: push
jobs:
build:
steps:
- run: |
first line
test \${{ github.| }}
test2`;
const result = await complete(...getPositionFromCursor(input, 1), undefined, contextProviderConfig);
expect(result.map(x => x.label)).toEqual(["event"]);
});
it("indented |+", async () => {
const input = `on: push
jobs:
build:
steps:
- run: |+
first line
test \${{ github.| }}
test2`;
const result = await complete(...getPositionFromCursor(input, 1), undefined, contextProviderConfig);
expect(result.map(x => x.label)).toEqual(["event"]);
});
it("indented >", async () => {
const input = `on: push
jobs:
build:
steps:
- run: >
first line
test \${{ github.| }}
test2`;
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
expect(result.map(x => x.label)).toEqual(["event"]);
});
it("indented >+", async () => {
const input = `on: push
jobs:
build:
steps:
- run: >+
first line
test \${{ github.| }}
test2`;
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
expect(result.map(x => x.label)).toEqual(["event"]);
});
});
2022-11-15 13:03:44 -08:00
it("nested auto-complete", async () => {
const input = "run-name: ${{ github.| }}";
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
expect(result.map(x => x.label)).toEqual(["event"]);
});
2022-11-15 15:59:14 -08:00
it("using default context provider", async () => {
const input =
"on: push\njobs:\n build:\n runs-on: ubuntu-latest\n environment:\n url: ${{ runner.| }}\n steps:\n - run: echo";
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
expect(result.map(x => x.label)).toEqual(["arch", "name", "os", "temp", "tool_cache"]);
});
2022-11-16 16:19:48 -08:00
it("job if", async () => {
const input = `on: push
jobs:
build:
if: github.|
runs-on: ubuntu-latest
steps:
- run: echo`;
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
expect(result.map(x => x.label)).toEqual(["event"]);
});
it("step if", async () => {
const input = `on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo
if: github.|`;
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
expect(result.map(x => x.label)).toEqual(["event"]);
});
2022-11-15 13:03:44 -08:00
});
2022-11-29 16:56:20 -05:00
it("context inherited from parent", async () => {
// The token definition is just a `string` and
// the parent `step-with` holds the allowed context
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/deploy@v100
with:
deploy-key: \${{ secrets.|
`;
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
2022-12-07 18:10:27 -08:00
expect(result.map(x => x.label)).toEqual(["GITHUB_TOKEN"]);
2022-11-29 16:56:20 -05:00
});
it("needs context only includes referenced jobs", async () => {
const input = `
on: push
jobs:
a:
runs-on: ubuntu-latest
steps:
- run: echo hello a
b:
needs: [a]
runs-on: ubuntu-latest
steps:
- run: echo hello b
c:
needs: [b]
runs-on: ubuntu-latest
steps:
2022-12-02 11:28:03 -08:00
- run: echo "hello \${{ needs.|
`;
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
expect(result.map(x => x.label)).toEqual(["b"]);
});
it("needs.<job_id>", async () => {
const input = `
on: push
jobs:
a:
runs-on: ubuntu-latest
steps:
- run: echo hello a
b:
needs: [a]
runs-on: ubuntu-latest
steps:
2022-12-02 11:28:03 -08:00
- run: echo "hello \${{ needs.a.|
`;
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
expect(result.map(x => x.label)).toEqual(["outputs", "result"]);
});
it("needs.<job_id>.outputs includes outputs", async () => {
const input = `
on: push
jobs:
a:
outputs:
build_id: my-build-id
runs-on: ubuntu-latest
steps:
- run: echo hello a
b:
needs: [a]
runs-on: ubuntu-latest
steps:
- run: echo "hello \${{ needs.a.outputs.|
`;
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
expect(result.map(x => x.label)).toEqual(["build_id"]);
});
2022-12-02 11:28:03 -08:00
it("inputs", async () => {
const input = `
on:
workflow_dispatch:
inputs:
name:
type: string
default: some value
another-name:
type: string
2022-12-13 15:24:54 -05:00
workflow_call:
inputs:
third-name:
type: boolean
2022-12-02 11:28:03 -08:00
jobs:
a:
runs-on: ubuntu-latest
steps:
- run: echo "hello \${{ inputs.|
`;
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
2022-12-13 15:24:54 -05:00
expect(result.map(x => x.label)).toEqual(["another-name", "name", "third-name"]);
2022-12-02 11:28:03 -08:00
});
2022-12-05 15:23:28 -05:00
it("no inputs", async () => {
const input = `
on:
workflow_dispatch:
jobs:
a:
runs-on: ubuntu-latest
steps:
- run: echo "hello \${{ inputs.|
`;
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
expect(result).toEqual([]);
});
2022-12-06 17:46:09 -05:00
2022-12-13 15:24:54 -05:00
describe("github context", () => {
it("includes expected keys", async () => {
const input = `
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: echo \${{ github.| }}
`;
2022-12-12 19:06:08 -05:00
2022-12-13 15:24:54 -05:00
const result = await complete(...getPositionFromCursor(input), undefined, undefined);
expect(result.map(x => x.label)).toContain("actor");
});
it("includes event inputs", async () => {
const input = `
on:
workflow_dispatch:
inputs:
name:
type: string
default: some value
another-name:
type: string
workflow_call:
inputs:
third-name:
type: boolean
jobs:
a:
runs-on: ubuntu-latest
steps:
- run: echo "hello \${{ github.event.inputs.|
2022-12-12 19:06:08 -05:00
`;
2022-12-13 15:24:54 -05:00
const result = await complete(...getPositionFromCursor(input), undefined, undefined);
2022-12-12 19:06:08 -05:00
2022-12-13 15:24:54 -05:00
expect(result.map(x => x.label)).toEqual(["another-name", "name", "third-name"]);
});
2022-12-12 19:06:08 -05:00
2022-12-13 15:24:54 -05:00
it("excludes event inputs and cron when no relevant events", async () => {
const input = `
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: echo \${{ github.event.| }}
`;
const result = await complete(...getPositionFromCursor(input), undefined, undefined);
expect(result.map(x => x.label)).not.toContain("inputs");
expect(result.map(x => x.label)).not.toContain("cron");
});
it("includes cron schedules", async () => {
const input = `
on:
schedule:
- cron: '0 0 * * *'
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: echo \${{ github.event.| }}
`;
const result = await complete(...getPositionFromCursor(input), undefined, undefined);
expect(result.map(x => x.label)).toContain("cron");
});
2022-12-12 19:06:08 -05:00
});
describe("steps context", () => {
it("includes defined step IDs", async () => {
const input = `
on: push
jobs:
one:
runs-on: ubuntu-latest
steps:
- id: a
run: echo hello a
- id: b
run: echo hello b
- id: c
run: echo "hello \${{ steps.|
`;
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
expect(result.map(x => x.label)).toEqual(["a", "b"]);
});
it("step.<step_id>", async () => {
const input = `
on: push
jobs:
one:
runs-on: ubuntu-latest
steps:
- id: a
run: echo hello a
- run: echo "hello \${{ steps.a.|
`;
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
expect(result.map(x => x.label)).toEqual(["conclusion", "outcome", "outputs"]);
});
it("ignores IDs from later steps", async () => {
const input = `
on: push
jobs:
one:
runs-on: ubuntu-latest
steps:
- id: a
run: echo hello a
- id: b
run: echo "hello \${{ steps.|
- id: c
run: echo hello c
`;
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
expect(result.map(x => x.label)).toEqual(["a"]);
});
2022-12-06 17:46:09 -05:00
it("Ignores generated IDs", async () => {
const input = `
on: push
jobs:
one:
runs-on: ubuntu-latest
steps:
- run: echo hello a
- id: b
run: echo hello b
- run: echo "hello \${{ steps.|
`;
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
2022-12-06 17:46:09 -05:00
expect(result.map(x => x.label)).toEqual(["b"]);
});
});
2022-12-06 18:02:54 -05:00
describe("strategy context", () => {
it("strategy is not suggested when outside of a matrix job", async () => {
2022-12-06 18:02:54 -05:00
const input = `
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: npm test > test-job-\${{ | }}.txt
`;
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
expect(result.map(x => x.label)).not.toContain("strategy");
});
it("strategy is suggested within a matrix job", async () => {
const input = `
on: push
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
test-group: [1, 2]
node: [14, 16]
steps:
- uses: actions/checkout@v3
- run: npm test > test-job-\${{ | }}.txt
`;
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
expect(result.map(x => x.label)).toContain("strategy");
});
2022-12-07 19:08:44 -05:00
it("includes expected keys", async () => {
const input = `
2022-12-06 18:02:54 -05:00
on: push
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
test-group: [1, 2]
node: [14, 16]
steps:
- uses: actions/checkout@v3
- run: npm test > test-job-\${{ strategy.| }}.txt
2022-12-07 19:08:44 -05:00
`;
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
expect(result.map(x => x.label)).toEqual(["fail-fast", "job-index", "job-total", "max-parallel"]);
});
});
describe("matrix context", () => {
it("matrix is not suggested when outside of a matrix job", async () => {
const input = `
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: npm test > test-job-\${{ | }}.txt
2022-12-06 18:02:54 -05:00
`;
2022-12-07 19:08:44 -05:00
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
2022-12-06 18:02:54 -05:00
2022-12-07 19:08:44 -05:00
expect(result.map(x => x.label)).not.toContain("strategy");
});
it("matrix is suggested within a matrix job", async () => {
const input = `
on: push
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
test-group: [1, 2]
node: [14, 16]
steps:
- uses: actions/checkout@v3
- run: npm test > test-job-\${{ | }}.txt
`;
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
expect(result.map(x => x.label)).toContain("strategy");
});
it("basic matrix job", async () => {
const input = `
on: push
jobs:
test:
runs-on: \${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
node: [14, 16]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: \${{ matrix.| }}
`;
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
expect(result.map(x => x.label)).toEqual(["node", "os"]);
});
it("matrix with include", async () => {
const input = `
on: push
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
fruit: [apple, pear]
animal: [cat, dog]
include:
- color: green
- color: pink
animal: cat
- fruit: apple
shape: circle
- fruit: banana
- fruit: banana
animal: cat
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: \${{ matrix.| }}
`;
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
expect(result.map(x => x.label)).toEqual(["animal", "color", "fruit", "shape"]);
});
it("matrix from expression", async () => {
const input = `
on: push
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix: \${{ fromJSON('{"color":["green","blue"]}') }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: \${{ matrix.| }}
`;
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
expect(result.map(x => x.label)).toEqual([]);
});
it("matrix with include expression", async () => {
const input = `
on: push
jobs:
test:
runs-on: \${{ matrix.os }}
strategy:
matrix:
fruit: [apple, pear]
animal: [cat, dog]
include: \${{ fromJSON('{"color":"green"}') }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: \${{ matrix.| }}
`;
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
2022-12-08 14:34:11 -05:00
expect(result.map(x => x.label)).toEqual(["animal", "fruit"]);
2022-12-07 19:08:44 -05:00
});
it("matrix with expression in property", async () => {
const input = `
on: push
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
color: \${{ fromJSON('["green","blue"]') }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: \${{ matrix.| }}
`;
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
expect(result.map(x => x.label)).toEqual(["color"]);
});
2022-12-06 18:02:54 -05:00
});
2022-11-15 13:03:44 -08:00
});