Skip to content
Merged
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
14 changes: 11 additions & 3 deletions actions/parse-ci-reports/src/parsers/AstroCheckParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,25 @@ const ANSI_PATTERN = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
* Parser for output generated by `astro check`
*/
export class AstroCheckParser extends BaseParser {
canParse(_filePath, content) {
canParse(filePath, content) {
if (!content) {
return false;
}

const sanitized = this.#stripAnsi(content);
if (LOCATION_PATTERN.test(sanitized)) {
const hasAstroMarkers =
LOCATION_PATTERN.test(sanitized) ||
ASTRO_HEADER_PATTERNS.some((pattern) => pattern.test(sanitized));

if (hasAstroMarkers) {
return true;
}

return ASTRO_HEADER_PATTERNS.some((pattern) => pattern.test(sanitized));
const normalizedPath = filePath.toLowerCase();
const hasSupportedName =
this.matchesAutoPatterns(filePath) || normalizedPath.includes("astro");

return hasSupportedName;
}

getPriority() {
Expand Down
8 changes: 8 additions & 0 deletions actions/parse-ci-reports/src/parsers/AstroCheckParser.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ Result (26 files):
- 0 hints`;

describe("AstroCheckParser", () => {
it("keeps auto-pattern path detection synchronized", () => {
const parser = new AstroCheckParser();
const filePath = "application/humanize-astro-check-report.log";

assert.ok(parser.matchesAutoPatterns(filePath));
assert.ok(parser.canParse(filePath, SAMPLE_ERROR));
});

it("identifies astro check diagnostics", () => {
const parser = new AstroCheckParser();

Expand Down
50 changes: 50 additions & 0 deletions actions/parse-ci-reports/src/parsers/BaseParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ export const ReportCategory = {
* Follows the Strategy pattern for different report formats
*/
export class BaseParser {
constructor() {
this._autoPatternRegexCache = null;
}

/**
* Build glob patterns for file basenames matched anywhere in the workspace.
* @param {string[]} baseNames - File basenames to match
Expand Down Expand Up @@ -66,6 +70,52 @@ export class BaseParser {
return extensions.map((extension) => `**/*.${extension}`);
}

/**
* Check if a file path matches this parser auto-detection patterns.
* @param {string} filePath - Path to evaluate
* @returns {boolean} True if path matches any auto pattern
*/
matchesAutoPatterns(filePath) {
if (!filePath) {
return false;
}

const normalizedPath = this._normalizeFilePath(filePath).toLowerCase();
const matchers = this._getAutoPatternRegexes();

return matchers.some((matcher) => matcher.test(normalizedPath));
}

_getAutoPatternRegexes() {
if (this._autoPatternRegexCache) {
return this._autoPatternRegexCache;
}

const patterns = this.getAutoPatterns();
this._autoPatternRegexCache = patterns.map((pattern) =>
this._globToRegex(pattern),
);

return this._autoPatternRegexCache;
}

_normalizeFilePath(filePath) {
return String(filePath).replace(/\\/g, "/");
}

_globToRegex(pattern) {
const escapedPattern = pattern
.toLowerCase()
.replace(/[-[\]{}()+?.,\\^$|#\s]/g, "\\$&")
.replace(/\*\*\//g, "<<<ANY_DIR_PREFIX>>>")
.replace(/\*\*/g, "<<<ANY_DIR>>>")
.replace(/\*/g, "[^/]*")
.replace(/<<<ANY_DIR_PREFIX>>>/g, "(?:.*/)?")
.replace(/<<<ANY_DIR>>>/g, ".*");

return new RegExp(`^${escapedPattern}$`, "i");
}

/**
* Parse a report file
* @param {string} content - The file content
Expand Down
14 changes: 8 additions & 6 deletions actions/parse-ci-reports/src/parsers/CheckStyleParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ export class CheckStyleParser extends BaseParser {
}

canParse(filePath, content) {
return (
(filePath.toLowerCase().includes("checkstyle") ||
filePath.endsWith(".xml")) &&
content.includes("<checkstyle") &&
content.includes("<file")
);
const normalizedPath = filePath.toLowerCase();
const hasSupportedName =
this.matchesAutoPatterns(filePath) ||
normalizedPath.includes("checkstyle") ||
normalizedPath.endsWith(".xml");
const hasCheckstyleRoot = /<\s*checkstyle\b/i.test(content);

return hasSupportedName && hasCheckstyleRoot;
}

getPriority() {
Expand Down
9 changes: 7 additions & 2 deletions actions/parse-ci-reports/src/parsers/CoberturaParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,14 @@ export class CoberturaParser extends BaseParser {
}

canParse(filePath, content) {
const normalizedPath = filePath.toLowerCase();
const hasSupportedName =
this.matchesAutoPatterns(filePath) ||
normalizedPath.includes("cobertura") ||
normalizedPath.includes("coverage");

return (
(filePath.toLowerCase().includes("cobertura") ||
filePath.toLowerCase().includes("coverage")) &&
hasSupportedName &&
content.includes("<coverage") &&
content.includes("line-rate")
);
Expand Down
10 changes: 9 additions & 1 deletion actions/parse-ci-reports/src/parsers/ESLintParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,15 @@ import { ReportData, LintIssue } from "../models/ReportData.js";
* Standard format for ESLint output
*/
export class ESLintParser extends BaseParser {
canParse(_filePath, content) {
canParse(filePath, content) {
const normalizedPath = filePath.toLowerCase();
const hasSupportedName =
this.matchesAutoPatterns(filePath) || normalizedPath.endsWith(".json");

if (!hasSupportedName) {
return false;
}

try {
const data = JSON.parse(content);
// ESLint format is an array of file results
Expand Down
8 changes: 7 additions & 1 deletion actions/parse-ci-reports/src/parsers/JUnitParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,14 @@ export class JUnitParser extends BaseParser {
}

canParse(filePath, content) {
const normalizedPath = filePath.toLowerCase();
const hasSupportedName =
this.matchesAutoPatterns(filePath) ||
normalizedPath.includes("junit") ||
normalizedPath.endsWith(".xml");

return (
(filePath.toLowerCase().includes("junit") || filePath.endsWith(".xml")) &&
hasSupportedName &&
(content.includes("<testsuite") || content.includes("<testsuites"))
);
}
Expand Down
8 changes: 8 additions & 0 deletions actions/parse-ci-reports/src/parsers/JUnitParser.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ describe("JUnitParser", () => {
assert.strictEqual(parser.canParse("test-results.xml", content), true);
});

it("keeps auto-pattern path detection synchronized", () => {
const filePath = "reports/junit-report.xml";
const content = `<?xml version="1.0"?><testsuite name="suite"><testcase name="ok"/></testsuite>`;

assert.ok(parser.matchesAutoPatterns(filePath));
assert.ok(parser.canParse(filePath, content));
});

it("should parse simple JUnit XML", () => {
const content = `<?xml version="1.0"?>
<testsuite name="Test Suite" tests="3" failures="1" skipped="1">
Expand Down
8 changes: 7 additions & 1 deletion actions/parse-ci-reports/src/parsers/LCOVParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@ import { ReportData, Coverage } from "../models/ReportData.js";
*/
export class LCOVParser extends BaseParser {
canParse(filePath, content) {
const normalizedPath = filePath.toLowerCase();
const hasSupportedName =
this.matchesAutoPatterns(filePath) ||
normalizedPath.includes("lcov") ||
normalizedPath.endsWith(".info");

return (
(filePath.toLowerCase().includes("lcov") || filePath.endsWith(".info")) &&
hasSupportedName &&
content.includes("TN:") &&
(content.includes("SF:") || content.includes("DA:"))
);
Expand Down
10 changes: 9 additions & 1 deletion actions/parse-ci-reports/src/parsers/PrettierParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,15 @@ export class PrettierParser extends BaseParser {
return true;
}

if (filePath?.toLowerCase().includes("prettier")) {
const normalizedPath = filePath.toLowerCase();
const hasSupportedName =
this.matchesAutoPatterns(filePath) || normalizedPath.includes("prettier");

if (!hasSupportedName) {
return false;
}

if (normalizedPath.includes("prettier")) {
return /\[(warn|error)\]\s+.+/i.test(content);
}

Expand Down
8 changes: 8 additions & 0 deletions actions/parse-ci-reports/src/parsers/PrettierParser.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ const SAMPLE_ERROR_LOG = `Checking formatting...
[warn] Code style issues found in the above file(s). Forgot to run Prettier?`;

describe("PrettierParser", () => {
it("keeps auto-pattern path detection synchronized", () => {
const parser = new PrettierParser();
const filePath = "application/humanize-prettier-report.log";

assert.ok(parser.matchesAutoPatterns(filePath));
assert.ok(parser.canParse(filePath, SAMPLE_FAILURE_LOG));
});

it("identifies prettier check logs", () => {
const parser = new PrettierParser();

Expand Down
13 changes: 12 additions & 1 deletion actions/parse-ci-reports/src/parsers/SarifParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,22 @@ import { ReportData, LintIssue } from "../models/ReportData.js";
* Parser for SARIF 2.1.0 static analysis results
*/
export class SarifParser extends BaseParser {
canParse(_filePath, content) {
canParse(filePath, content) {
if (!content) {
return false;
}

const normalizedPath = filePath.toLowerCase();
const hasSupportedName =
this.matchesAutoPatterns(filePath) ||
normalizedPath.endsWith(".sarif") ||
normalizedPath.endsWith(".sarif.json") ||
normalizedPath.endsWith(".json");

if (!hasSupportedName) {
return false;
}

try {
const sarif = JSON.parse(content);
return (
Expand Down
8 changes: 8 additions & 0 deletions actions/parse-ci-reports/src/parsers/SarifParser.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ const SAMPLE_SARIF = JSON.stringify({
});

describe("SarifParser", () => {
it("keeps auto-pattern path detection synchronized", () => {
const parser = new SarifParser();
const filePath = "reports/humanize-sarif-report.json";

assert.ok(parser.matchesAutoPatterns(filePath));
assert.ok(parser.canParse(filePath, SAMPLE_SARIF));
});

it("identifies SARIF reports", () => {
const parser = new SarifParser();

Expand Down
10 changes: 9 additions & 1 deletion actions/parse-ci-reports/src/parsers/TAPParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,16 @@ import { ReportData, TestResult } from "../models/ReportData.js";
* Supports TAP versions 12, 13, and 14
*/
export class TAPParser extends BaseParser {
canParse(_filePath, content) {
canParse(filePath, content) {
const lines = content.split("\n");
const normalizedPath = filePath.toLowerCase();
const hasSupportedName =
this.matchesAutoPatterns(filePath) || normalizedPath.endsWith(".tap");

if (!hasSupportedName) {
return false;
}

// TAP files typically start with TAP version or test plan
return lines.some(
(line) =>
Expand Down
10 changes: 10 additions & 0 deletions actions/parse-ci-reports/src/parsers/TAPParser.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ ok 3 - test passed # SKIP`;
assert.strictEqual(parser.canParse("test.tap", content), true);
});

it("keeps auto-pattern path detection synchronized", () => {
const filePath = "reports/results.tap";
const content = `TAP version 13
1..1
ok 1 - sample`;

assert.ok(parser.matchesAutoPatterns(filePath));
assert.ok(parser.canParse(filePath, content));
});

it("should parse basic TAP output", () => {
const content = `TAP version 13
1..3
Expand Down
Loading