Skip to content

Commit

Permalink
Groups (#5)
Browse files Browse the repository at this point in the history
* refactored out extractRunOutput

* implemented a separate class to handle output parsing. added group detection as well

* bump up version and update readme
  • Loading branch information
shubhbapna authored Dec 9, 2022
1 parent 6508570 commit 1eff951
Show file tree
Hide file tree
Showing 8 changed files with 266 additions and 88 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,7 @@ Each run returns an array of `Step` objects that describes what was executed, wh
output: "output of the command",
// 0 implies it succeeded, 1 implies it failed and -1 implies something went wrong with the interface which should be reported to us
status: 0 | 1 | -1,
groups?: {name: string, output: string}[] // output grouped by annotations if there were any
},
];
```
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@kie/act-js",
"version": "1.1.0",
"version": "1.1.1",
"description": "nodejs wrapper for nektos/act",
"main": "build/src/index.js",
"types": "build/src/index.d.ts",
Expand Down
88 changes: 3 additions & 85 deletions src/act/act.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { spawn } from "child_process";
import { RunOpts, Step, Workflow } from "@aj/act/act.type";
import { ACT_BINARY, DEFAULT_JOB } from "@aj/act/act.constants";
import { ACT_BINARY } from "@aj/act/act.constants";
import { existsSync, writeFileSync } from "fs";
import path from "path";
import { homedir } from "os";
Expand All @@ -10,6 +10,7 @@ import { StepMocker } from "@aj/step-mocker/step-mocker";
import { ActionEvent } from "@aj/action-event/action-event";
import { EventJSON } from "@aj/action-event/action-event.types";
import { writeFile } from "fs/promises";
import { OutputParser } from "@aj/output-parser/output-parser";

export class Act {
private secrets: ArgumentMap;
Expand Down Expand Up @@ -241,94 +242,11 @@ export class Act {
promises.push(proxy.stop());
}

const result = this.extractRunOutput(data);
const result = new OutputParser(data).parseOutput();
await Promise.all(promises);
return result;
}

/**
* Parse the output produced by running act successfully. Produces an object
* describing whether the job was successful or not and what was the output of the job
* @param output
* @returns
*/
private extractRunOutput(output: string): Step[] {
// line that has a star followed by Run and job name
const runMatcher = /^\s*(\[.+\])\s*\u2B50\s*Run\s*(.*)/;
// line that has a green tick mark
const successMatcher = /^\s*(\[.+\])\s*\u2705\s*Success\s*-\s*(.*)/;
// line that has a red cross
const failureMatcher = /^\s*(\[.+\])\s*\u274C\s*Failure\s*-\s*(.*)/;
// lines that have no emoji
const runOutputMatcher = /^\s*(\[.+\])\s*\|\s*(.*)/;

// keep track of steps for each job
const matrix: Record<string, Record<string, Step>> = {};

// keep track of the most recent output for a job
const matrixOutput: Record<string, string> = {};

const lines = output.split("\n").map((line) => line.trim());
for (const line of lines) {
const runMatcherResult = runMatcher.exec(line);
const successMatcherResult = successMatcher.exec(line);
const failureMatcherResult = failureMatcher.exec(line);
const runOutputMatcherResult = runOutputMatcher.exec(line);

// if the line indicates the start of a step
if (runMatcherResult !== null) {
// initialize bookkeeping variables
if (!matrix[runMatcherResult[1]]) {
matrix[runMatcherResult[1]] = {};
matrixOutput[runMatcherResult[1]] = "";
}

// create a step object
matrix[runMatcherResult[1]][runMatcherResult[2].trim()] = {
...DEFAULT_JOB,
name: runMatcherResult[2].trim(),
};
}
// if the line indicates that a step was successful
else if (successMatcherResult !== null) {
// store output in step
matrix[successMatcherResult[1]][successMatcherResult[2].trim()] = {
...matrix[successMatcherResult[1]][successMatcherResult[2].trim()],
status: 0,
output: matrixOutput[successMatcherResult[1]].trim(),
};

// reset output
matrixOutput[successMatcherResult[1]] = "";
}
// if the line indicates that a step failed
else if (failureMatcherResult !== null) {
// store output in step
matrix[failureMatcherResult[1]][failureMatcherResult[2].trim()] = {
...matrix[failureMatcherResult[1]][failureMatcherResult[2].trim()],
status: 1,
output: matrixOutput[failureMatcherResult[1]].trim(),
};

// reset output
matrixOutput[failureMatcherResult[1]] = "";
}
// if the line is an output line
else if (runOutputMatcherResult !== null) {
matrixOutput[runOutputMatcherResult[1]] +=
runOutputMatcherResult[2] + "\n";
}
}

let result: Step[] = [];
Object.values(matrix).forEach((jobs) => {
Object.values(jobs).forEach((step) => {
result.push(step);
});
});
return result;
}

/**
* Produce a .actrc file in the home directory of the user if it does not exist
* @param defaultImageSize
Expand Down
6 changes: 6 additions & 0 deletions src/act/act.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ export type Step = {
name: string;
status: number;
output: string;
groups?: Group[];
};

export type Group = {
name: string;
output: string;
};

export type RunOpts = {
Expand Down
198 changes: 198 additions & 0 deletions src/output-parser/output-parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import { DEFAULT_JOB } from "@aj/act/act.constants";
import { Group, Step } from "@aj/act/act.type";

export class OutputParser {
private output: string;

// keep track of steps for each job
private stepMatrix: Record<string, Record<string, Step>>;

// keep track of output for the current step of a job
private outputMatrix: Record<string, string>;

// keep track of groups for the current step of a job
private groupMatrix: Record<string, Group[]>;

private isPartOfGroup: boolean;

constructor(output: string) {
this.output = output;
this.stepMatrix = {};
this.outputMatrix = {};
this.groupMatrix = {};
this.isPartOfGroup = false;
}

/**
* Parse the output produced by running act successfully. Produces an object
* describing whether the job was successful or not and what was the output of the job
* @returns
*/
parseOutput(): Step[] {
const lines = this.output.split("\n").map((line) => line.trim());
for (const line of lines) {
this.parseRun(line);
this.parseSuccess(line);
this.parseFailure(line);
this.parseStartGroup(line);
this.parseEndGroup(line);
this.parseStepOutput(line);
}

let result: Step[] = [];
Object.values(this.stepMatrix).forEach((jobs) => {
Object.values(jobs).forEach((step) => {
result.push(step);
});
});
return result;
}

/**
* Check if the line indicates the start of a step. If it does then accordingly
* update the bookkeeping variables
* @param line
*/
private parseRun(line: string) {
// line that has a star followed by Run and step name
const runMatcher = /^\s*(\[.+\])\s*\u2B50\s*Run\s*(.*)/;
const runMatcherResult = runMatcher.exec(line);
// if the line indicates the start of a step
if (runMatcherResult !== null) {
// initialize bookkeeping variables
if (!this.stepMatrix[runMatcherResult[1]]) {
this.stepMatrix[runMatcherResult[1]] = {};
this.outputMatrix[runMatcherResult[1]] = "";
this.groupMatrix[runMatcherResult[1]] = [];
}

// create a step object
this.stepMatrix[runMatcherResult[1]][runMatcherResult[2].trim()] = {
...DEFAULT_JOB,
name: runMatcherResult[2].trim(),
};
}
}

/**
* Check if the line indicates that a step was successful. If it does then accordingly
* update the bookkeeping variables
* @param line
*/
private parseSuccess(line: string) {
// line that has a green tick mark
const successMatcher = /^\s*(\[.+\])\s*\u2705\s*Success\s*-\s*(.*)/;
const successMatcherResult = successMatcher.exec(line);
// if the line indicates that a step was successful
if (successMatcherResult !== null) {
const groups = this.groupMatrix[successMatcherResult[1]];
// store output in step
this.stepMatrix[successMatcherResult[1]][successMatcherResult[2].trim()] =
{
...this.stepMatrix[successMatcherResult[1]][
successMatcherResult[2].trim()
],
status: 0,
output: this.outputMatrix[successMatcherResult[1]].trim(),
// only add groups attribute if there are any. don't add empty array
...(groups.length > 0 ? { groups } : {}),
};
this.resetOutputAndGroupMatrix(successMatcherResult[1]);
}
}

/**
* Check if the line indicates that a step failed. If it does then accordingly
* update the bookkeeping variables
* @param line
*/
private parseFailure(line: string) {
// line that has a red cross
const failureMatcher = /^\s*(\[.+\])\s*\u274C\s*Failure\s*-\s*(.*)/;
const failureMatcherResult = failureMatcher.exec(line);

// if the line indicates that a step failed
if (failureMatcherResult !== null) {
// store output in step
this.stepMatrix[failureMatcherResult[1]][failureMatcherResult[2].trim()] =
{
...this.stepMatrix[failureMatcherResult[1]][
failureMatcherResult[2].trim()
],
status: 1,
output: this.outputMatrix[failureMatcherResult[1]].trim(),
};

this.resetOutputAndGroupMatrix(failureMatcherResult[1]);
}
}

/**
* Check if the line indicates the start of a group annotation. If it does then accordingly
* update the bookkeeping variables
* @param line
*/
private parseStepOutput(line: string) {
// lines that have no emoji
const stepOutputMatcher = /^\s*(\[.+\])\s*\|\s*(.*)/;
const stepOutputMatcherResult = stepOutputMatcher.exec(line);

// if the line is an output line
if (stepOutputMatcherResult !== null) {
// if output is part of some group then update it
if (this.isPartOfGroup) {
const length = this.groupMatrix[stepOutputMatcherResult[1]].length;
this.groupMatrix[stepOutputMatcherResult[1]][length - 1].output +=
stepOutputMatcherResult[2] + "\n";
}
this.outputMatrix[stepOutputMatcherResult[1]] +=
stepOutputMatcherResult[2] + "\n";
}
}

/**
* Check if the line indicates the end of a group annotation. If it does then accordingly
* update the bookkeeping variables
* @param line
*/
private parseStartGroup(line: string) {
// lines that have a question mark
const startGroupMatcher = /^\s*(\[.+\])\s*\u2753\s*::group::\s*(.*)/;

const startGroupMatcherResult = startGroupMatcher.exec(line);

// if the line indicates start of a group annotation
if (startGroupMatcherResult !== null) {
this.groupMatrix[startGroupMatcherResult[1]].push({
name: startGroupMatcherResult[2],
output: "",
});
this.isPartOfGroup = true;
}
}

/**
* Check if the line is an output line. If it does then accordingly
* update the bookkeeping variables
* @param line
*/
private parseEndGroup(line: string) {
// lines that have a question mark
const endGroupMatcher = /^\s*(\[.+\])\s*\u2753\s*::endgroup::\s*/;

const endGroupMatcherResult = endGroupMatcher.exec(line);

// if the line indicates stop of a group annotation then clean up trailing spaces
if (endGroupMatcherResult !== null) {
const length = this.groupMatrix[endGroupMatcherResult[1]].length;
this.groupMatrix[endGroupMatcherResult[1]][length - 1].output =
this.groupMatrix[endGroupMatcherResult[1]][length - 1].output.trim();
this.isPartOfGroup = false;
}
}

private resetOutputAndGroupMatrix(job: string) {
this.outputMatrix[job] = "";
this.groupMatrix[job] = [];
}
}
34 changes: 34 additions & 0 deletions test/unit/act/act.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,40 @@ describe("run", () => {
},
]);
});

test("run with group annotations", async () => {
const act = new Act();
const output = await act.runJob("groups", { workflowFile: resources });

expect(output).toStrictEqual([
{
name: "Main Group 1 of log lines",
status: 0,
output: "Inside group 1",
groups: [
{
name: "Group 1",
output: "Inside group 1",
},
],
},
{
name: "Main Group 2 of log lines",
status: 0,
output: "Inside group 2 part 1\nInside group 2 part 2",
groups: [
{
name: "Group 2 part 1",
output: "Inside group 2 part 1",
},
{
name: "Group 2 part 2",
output: "Inside group 2 part 2",
},
],
},
]);
});
});

describe("initialization", () => {
Expand Down
Loading

0 comments on commit 1eff951

Please sign in to comment.