From bf7d4c88a8cab21367b033cd3144a144164583a5 Mon Sep 17 00:00:00 2001 From: Shubh Bapna Date: Tue, 30 Jan 2024 14:40:23 -0500 Subject: [PATCH 1/4] added ability to mock step using index of that step and added ability to insert steps instead of replacing them --- src/step-mocker/step-mocker.ts | 64 ++++++++++++++++++++++++---- src/step-mocker/step-mocker.types.ts | 35 ++++++++++++++- 2 files changed, 88 insertions(+), 11 deletions(-) diff --git a/src/step-mocker/step-mocker.ts b/src/step-mocker/step-mocker.ts index 851245a..8c32f5e 100644 --- a/src/step-mocker/step-mocker.ts +++ b/src/step-mocker/step-mocker.ts @@ -1,12 +1,18 @@ import { GithubWorkflow, GithubWorkflowStep, + isStepIdentifierUsingAfter, + isStepIdentifierUsingBefore, + isStepIdentifierUsingBeforeOrAfter, isStepIdentifierUsingId, + isStepIdentifierUsingIndex, isStepIdentifierUsingName, isStepIdentifierUsingRun, isStepIdentifierUsingUses, MockStep, StepIdentifier, + StepIdentifierUsingAfter, + StepIdentifierUsingBefore, } from "@aj/step-mocker/step-mocker.types"; import { existsSync, readFileSync, writeFileSync } from "fs"; import path from "path"; @@ -27,14 +33,10 @@ export class StepMocker { for (const mockStep of mockSteps[job]) { const { step, stepIndex } = this.locateStep(workflow, job, mockStep); if (step) { - if (typeof mockStep.mockWith === "string") { - this.updateStep(workflow, job, stepIndex, { - ...step, - run: mockStep.mockWith, - uses: undefined, - }); + if (isStepIdentifierUsingBeforeOrAfter(mockStep)) { + this.addStep(workflow, job, stepIndex, mockStep); } else { - this.updateStep(workflow, job, stepIndex, mockStep.mockWith); + this.updateStep(workflow, job, stepIndex, mockStep); } } else { throw new Error("Could not find step"); @@ -44,14 +46,42 @@ export class StepMocker { return this.writeWorkflowFile(filePath, workflow); } + private addStep( + workflow: GithubWorkflow, + jobId: string, + stepIndex: number, + mockStep: StepIdentifierUsingAfter | StepIdentifierUsingBefore + ) { + if (workflow.jobs[jobId]) { + let indexToInsertAt = stepIndex; + if (isStepIdentifierUsingBefore(mockStep)) { + indexToInsertAt = stepIndex <= 0 ? 0 : indexToInsertAt - 1; + } else { + indexToInsertAt = + stepIndex >= workflow.jobs[jobId].steps.length - 1 + ? workflow.jobs[jobId].steps.length + : indexToInsertAt + 1; + } + workflow.jobs[jobId].steps.splice(indexToInsertAt, 0, mockStep.mockWith); + } + } + private updateStep( workflow: GithubWorkflow, jobId: string, stepIndex: number, - newStep: GithubWorkflowStep + mockStep: StepIdentifier ) { if (workflow.jobs[jobId]) { const oldStep = workflow.jobs[jobId].steps[stepIndex]; + const newStep = + typeof mockStep.mockWith === "string" + ? { + ...oldStep, + run: mockStep.mockWith, + uses: undefined, + } + : mockStep.mockWith; const updatedStep = { ...oldStep, ...newStep }; for (const key of Object.keys(oldStep)) { @@ -72,7 +102,7 @@ export class StepMocker { jobId: string, step: StepIdentifier ): { stepIndex: number; step: GithubWorkflowStep | undefined } { - const index = workflow.jobs[jobId]?.steps.findIndex(s => { + const index = workflow.jobs[jobId]?.steps.findIndex((s, index) => { if (isStepIdentifierUsingId(step)) { return step.id === s.id; } @@ -88,6 +118,22 @@ export class StepMocker { if (isStepIdentifierUsingRun(step)) { return step.run === s.run; } + + if (isStepIdentifierUsingIndex(step)) { + return step.index === index; + } + + if (isStepIdentifierUsingBefore(step)) { + return typeof step.before === "string" + ? [s.id, s.name, s.uses, s.run].includes(step.before) + : step.before === index; + } + + if (isStepIdentifierUsingAfter(step)) { + return typeof step.after === "string" + ? [s.id, s.name, s.uses, s.run].includes(step.after) + : step.after === index; + } return false; }); diff --git a/src/step-mocker/step-mocker.types.ts b/src/step-mocker/step-mocker.types.ts index 15d9d1e..55d1321 100644 --- a/src/step-mocker/step-mocker.types.ts +++ b/src/step-mocker/step-mocker.types.ts @@ -92,11 +92,18 @@ export type StepIdentifierUsingName = { name: string; mockWith: GithubWorkflowSt export type StepIdentifierUsingId = { id: string; mockWith: GithubWorkflowStep | string }; export type StepIdentifierUsingUses = { uses: string; mockWith: GithubWorkflowStep | string }; export type StepIdentifierUsingRun = { run: string; mockWith: GithubWorkflowStep | string }; +export type StepIdentifierUsingIndex = { index: number; mockWith: GithubWorkflowStep | string }; +export type StepIdentifierUsingBefore = { before: number | string; mockWith: GithubWorkflowStep }; +export type StepIdentifierUsingAfter = { after: number | string; mockWith: GithubWorkflowStep }; + export type StepIdentifier = | StepIdentifierUsingName | StepIdentifierUsingId | StepIdentifierUsingUses - | StepIdentifierUsingRun; + | StepIdentifierUsingRun + | StepIdentifierUsingIndex + | StepIdentifierUsingBefore + | StepIdentifierUsingAfter; export function isStepIdentifierUsingName( step: StepIdentifier @@ -118,6 +125,30 @@ export function isStepIdentifierUsingUses( export function isStepIdentifierUsingRun( step: StepIdentifier -): step is StepIdentifierUsingUses { +): step is StepIdentifierUsingRun { return Object.prototype.hasOwnProperty.call(step, "run"); } + +export function isStepIdentifierUsingIndex( + step: StepIdentifier +): step is StepIdentifierUsingIndex { + return Object.prototype.hasOwnProperty.call(step, "index"); +} + +export function isStepIdentifierUsingBefore( + step: StepIdentifier +): step is StepIdentifierUsingBefore { + return Object.prototype.hasOwnProperty.call(step, "before"); +} + +export function isStepIdentifierUsingAfter( + step: StepIdentifier +): step is StepIdentifierUsingAfter { + return Object.prototype.hasOwnProperty.call(step, "after"); +} + +export function isStepIdentifierUsingBeforeOrAfter( + step: StepIdentifier +): step is StepIdentifierUsingBefore | StepIdentifierUsingAfter { + return isStepIdentifierUsingBefore(step) || isStepIdentifierUsingAfter(step); +} From 273e5f3916dfc62b582132c33af12d2838fdb8a5 Mon Sep 17 00:00:00 2001 From: Shubh Bapna Date: Tue, 30 Jan 2024 14:40:39 -0500 Subject: [PATCH 2/4] updated test cases --- test/unit/step-mocker/step-mocker.test.ts | 82 ++++++++++++++++++++++- 1 file changed, 80 insertions(+), 2 deletions(-) diff --git a/test/unit/step-mocker/step-mocker.test.ts b/test/unit/step-mocker/step-mocker.test.ts index 2c089ea..5137a27 100644 --- a/test/unit/step-mocker/step-mocker.test.ts +++ b/test/unit/step-mocker/step-mocker.test.ts @@ -1,4 +1,5 @@ import { StepMocker } from "@aj/step-mocker/step-mocker"; +import { GithubWorkflow } from "@aj/step-mocker/step-mocker.types"; import { readFileSync, existsSync, writeFileSync } from "fs"; import { readFile } from "fs/promises"; import path from "path"; @@ -202,9 +203,86 @@ describe("locateSteps", () => { "utf8" ); }); + + test("step found using index", async () => { + const data = await readFile(path.join(resources, "steps.yaml"), "utf8"); + readFileSyncMock.mockReturnValueOnce(data); + const stepMocker = new StepMocker("workflow.yaml", __dirname); + await stepMocker.mock({ + name: [ + { + index: 1, + mockWith: "echo step", + }, + ], + }); + const workflow = stringify(parse(data.replace("echo $TEST1", "echo step"))); + expect(writeFileSyncMock).toHaveBeenLastCalledWith( + path.join(__dirname, "workflow.yaml"), + workflow, + "utf8" + ); + }); + + test.each([ + ["index", 0, 0], + ["name", "secrets", 0] + ])("step found using before: %p", async (_title, before, index) => { + const data = await readFile(path.join(resources, "steps.yaml"), "utf8"); + readFileSyncMock.mockReturnValueOnce(data); + const stepMocker = new StepMocker("workflow.yaml", __dirname); + const mockWith = { + name: "added new step", + run: "echo new step" + }; + await stepMocker.mock({ + name: [ + { + before, + mockWith, + }, + ], + }); + const outputWorkflow = parse(data) as GithubWorkflow; + outputWorkflow.jobs["name"].steps.splice(index, 0, mockWith); + expect(writeFileSyncMock).toHaveBeenLastCalledWith( + path.join(__dirname, "workflow.yaml"), + stringify(outputWorkflow), + "utf8" + ); + }); + + test.each([ + ["index", 3, 4], + ["name", "secrets", 2] + ])("step found using after: %p", async (_title, after, index) => { + const data = await readFile(path.join(resources, "steps.yaml"), "utf8"); + readFileSyncMock.mockReturnValueOnce(data); + const stepMocker = new StepMocker("workflow.yaml", __dirname); + const mockWith = { + name: "added new step", + run: "echo new step" + }; + await stepMocker.mock({ + name: [ + { + after, + mockWith, + }, + ], + }); + const outputWorkflow = parse(data) as GithubWorkflow; + outputWorkflow.jobs["name"].steps.splice(index, 0, mockWith); + expect(writeFileSyncMock).toHaveBeenLastCalledWith( + path.join(__dirname, "workflow.yaml"), + stringify(outputWorkflow), + "utf8" + ); + }); + }); -describe("mock", () => { +describe("update step", () => { beforeEach(async () => { existsSyncMock.mockReturnValueOnce(true); writeFileSyncMock.mockReturnValueOnce(undefined); @@ -289,4 +367,4 @@ describe("mock", () => { }, }); }); -}); +}); \ No newline at end of file From 5ede73b177caa491fa4b32236c0144f7b747a0e1 Mon Sep 17 00:00:00 2001 From: Shubh Bapna Date: Tue, 30 Jan 2024 14:46:23 -0500 Subject: [PATCH 3/4] updated docs --- README.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index dcdbbfb..da47449 100644 --- a/README.md +++ b/README.md @@ -404,12 +404,26 @@ Schema for `mockSteps` { run: "locates the step using the run field" mockWith: "command or a new step as JSON to replace the given step with" + } | + { + index: "locates the step using the index (0 indexed) in the steps array of the workflow" + mockWith: "command or a new step as JSON to replace the given step with" + } | + { + before: "index of the step or the id/name/run/uses of the step before which you want to insert a step" + mockWith: "a new step as JSON to be added before the given step" + } | + { + after: "index of the step or the id/name/run/uses of the step after which you want to insert a step" + mockWith: "a new step as JSON to be added after the given step" } )[] } ``` -NOTE: Please use `MockGithub` to run the workflow in a clean safe github repository so that any changes made to the Workflow file are done in the test environment and not to the actual file. +**Important Notes**: +- Please use `MockGithub` to run the workflow in a clean safe github repository so that any changes made to the Workflow file are done in the test environment and not to the actual file. +- Using `before` or `after` will cause changes in the indexing of the steps which will impact the subsequent mock steps that use `before`, `after` or `index`. #### Run result From ce71d84dd80b794b0622f05a037425cfeaf72d33 Mon Sep 17 00:00:00 2001 From: Shubh Bapna Date: Thu, 1 Feb 2024 13:36:44 -0500 Subject: [PATCH 4/4] fix indexing problem --- README.md | 1 - src/step-mocker/step-mocker.ts | 17 ++-- test/unit/step-mocker/step-mocker.test.ts | 102 ++++++++++++++++++++++ 3 files changed, 113 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index da47449..2a413f7 100644 --- a/README.md +++ b/README.md @@ -423,7 +423,6 @@ Schema for `mockSteps` **Important Notes**: - Please use `MockGithub` to run the workflow in a clean safe github repository so that any changes made to the Workflow file are done in the test environment and not to the actual file. -- Using `before` or `after` will cause changes in the indexing of the steps which will impact the subsequent mock steps that use `before`, `after` or `index`. #### Run result diff --git a/src/step-mocker/step-mocker.ts b/src/step-mocker/step-mocker.ts index 8c32f5e..9ef47b5 100644 --- a/src/step-mocker/step-mocker.ts +++ b/src/step-mocker/step-mocker.ts @@ -29,19 +29,24 @@ export class StepMocker { async mock(mockSteps: MockStep) { const filePath = this.getWorkflowPath(); const workflow = await this.readWorkflowFile(filePath); - for (const job of Object.keys(mockSteps)) { - for (const mockStep of mockSteps[job]) { - const { step, stepIndex } = this.locateStep(workflow, job, mockStep); + for (const jobId of Object.keys(mockSteps)) { + const stepsToAdd = []; + for (const mockStep of mockSteps[jobId]) { + const { step, stepIndex } = this.locateStep(workflow, jobId, mockStep); if (step) { if (isStepIdentifierUsingBeforeOrAfter(mockStep)) { - this.addStep(workflow, job, stepIndex, mockStep); + // need to adjust the step index if there were elements added previously + const adjustIndex: number = stepsToAdd.filter(s => s.stepIndex < stepIndex).length; + // we will only actually add the steps at the end so as to avoid indexing errors in subsequent add steps + stepsToAdd.push({jobId, stepIndex: stepIndex + adjustIndex, mockStep}); } else { - this.updateStep(workflow, job, stepIndex, mockStep); + this.updateStep(workflow, jobId, stepIndex, mockStep); } } else { throw new Error("Could not find step"); } } + stepsToAdd.forEach(s => this.addStep(workflow, s.jobId, s.stepIndex, s.mockStep)); } return this.writeWorkflowFile(filePath, workflow); } @@ -62,7 +67,7 @@ export class StepMocker { ? workflow.jobs[jobId].steps.length : indexToInsertAt + 1; } - workflow.jobs[jobId].steps.splice(indexToInsertAt, 0, mockStep.mockWith); + workflow.jobs[jobId].steps.splice(indexToInsertAt, 0, {...mockStep.mockWith}); } } diff --git a/test/unit/step-mocker/step-mocker.test.ts b/test/unit/step-mocker/step-mocker.test.ts index 5137a27..5f1374a 100644 --- a/test/unit/step-mocker/step-mocker.test.ts +++ b/test/unit/step-mocker/step-mocker.test.ts @@ -367,4 +367,106 @@ describe("update step", () => { }, }); }); +}); + +describe("add step", () => { + beforeEach(() => { + existsSyncMock.mockReturnValueOnce(true); + writeFileSyncMock.mockReturnValueOnce(undefined); + }); + test.each([ + ["index", 0, 0], + ["name", "secrets", 0] + ])("step found using before: %p", async (_title, before, index) => { + const data = await readFile(path.join(resources, "steps.yaml"), "utf8"); + readFileSyncMock.mockReturnValueOnce(data); + const stepMocker = new StepMocker("workflow.yaml", __dirname); + const mockWith = { + name: "added new step", + run: "echo new step" + }; + await stepMocker.mock({ + name: [ + { + before, + mockWith, + }, + ], + }); + const outputWorkflow = parse(data) as GithubWorkflow; + outputWorkflow.jobs["name"].steps.splice(index, 0, mockWith); + expect(writeFileSyncMock).toHaveBeenLastCalledWith( + path.join(__dirname, "workflow.yaml"), + stringify(outputWorkflow), + "utf8" + ); + }); + + test.each([ + ["index", 3, 4], + ["name", "secrets", 2] + ])("step found using after: %p", async (_title, after, index) => { + const data = await readFile(path.join(resources, "steps.yaml"), "utf8"); + readFileSyncMock.mockReturnValueOnce(data); + const stepMocker = new StepMocker("workflow.yaml", __dirname); + const mockWith = { + name: "added new step", + run: "echo new step" + }; + await stepMocker.mock({ + name: [ + { + after, + mockWith, + }, + ], + }); + const outputWorkflow = parse(data) as GithubWorkflow; + outputWorkflow.jobs["name"].steps.splice(index, 0, mockWith); + expect(writeFileSyncMock).toHaveBeenLastCalledWith( + path.join(__dirname, "workflow.yaml"), + stringify(outputWorkflow), + "utf8" + ); + }); + + test("multiple add steps with indexing", async () => { + const data = await readFile(path.join(resources, "steps.yaml"), "utf8"); + readFileSyncMock.mockReturnValueOnce(data); + const stepMocker = new StepMocker("workflow.yaml", __dirname); + const mockWith = { + name: "added new step", + run: "echo new step" + }; + await stepMocker.mock({ + name: [ + { + after: 1, + mockWith + }, + { + before: 0, + mockWith, + }, + { + before: 2, + mockWith + }, + { + index: 3, + mockWith: "echo updated" + } + ], + }); + const outputWorkflow = parse(data) as GithubWorkflow; + outputWorkflow.jobs["name"].steps[ outputWorkflow.jobs["name"].steps.length - 1].run = "echo updated"; + outputWorkflow.jobs["name"].steps.splice(0, 0, {...mockWith}); + outputWorkflow.jobs["name"].steps.splice(3, 0, {...mockWith}); + outputWorkflow.jobs["name"].steps.splice(3, 0, {...mockWith}); + expect(writeFileSyncMock).toHaveBeenLastCalledWith( + path.join(__dirname, "workflow.yaml"), + stringify(outputWorkflow), + "utf8" + ); + }); }); \ No newline at end of file