aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorKevin J Hoerr <kjhoerr@protonmail.com>2021-09-16 15:45:34 -0400
committerKevin J Hoerr <kjhoerr@protonmail.com>2021-09-16 15:45:34 -0400
commit6bbd3f03104e6dcd9da89a8ec5dcb5d992ee3ed5 (patch)
tree6f44b79750ad00267c8840b3aa472ee64aefc66b /src
parent77575aab559f058d886a691eefe262cf0f306710 (diff)
downloadao-coverage-6bbd3f03104e6dcd9da89a8ec5dcb5d992ee3ed5.tar.gz
ao-coverage-6bbd3f03104e6dcd9da89a8ec5dcb5d992ee3ed5.tar.bz2
ao-coverage-6bbd3f03104e6dcd9da89a8ec5dcb5d992ee3ed5.zip
#7 Add Cobertura format
Diffstat (limited to 'src')
-rw-r--r--src/formats.test.ts79
-rw-r--r--src/formats.ts28
-rw-r--r--src/routes.test.ts175
-rw-r--r--src/routes.ts109
4 files changed, 371 insertions, 20 deletions
diff --git a/src/formats.test.ts b/src/formats.test.ts
index 1536b5d..02f9a3e 100644
--- a/src/formats.test.ts
+++ b/src/formats.test.ts
@@ -74,7 +74,7 @@ describe("Formats object", () => {
const result = Formats.listFormats();
// Assert
- expect(result).toEqual(["tarpaulin"]);
+ expect(result).toEqual(["tarpaulin", "cobertura"]);
});
it("should return the requested format", () => {
@@ -105,14 +105,14 @@ describe("Tarpaulin format", () => {
expect(matcher).toEqual(defaultColorMatches);
});
- it("should parse coverage from a normal tarpaulin file", () => {
+ it("should parse coverage from a normal tarpaulin file", async () => {
// Arrange
const file = fs.readFileSync(reportPath("tarpaulin-report.html"), "utf-8");
const format = Formats.getFormat("tarpaulin");
// Act
- const result = format.parseCoverage(file);
+ const result = await format.parseCoverage(file);
// Assert
expect(typeof result).toEqual("number");
@@ -122,14 +122,14 @@ describe("Tarpaulin format", () => {
}
});
- it("should parse coverage from an empty tarpaulin file", () => {
+ it("should parse coverage from an empty tarpaulin file", async () => {
// Arrange
const file = fs.readFileSync(reportPath("tarpaulin-empty.html"), "utf-8");
const format = Formats.getFormat("tarpaulin");
// Act
- const result = format.parseCoverage(file);
+ const result = await format.parseCoverage(file);
// Assert
expect(typeof result).toEqual("number");
@@ -138,14 +138,79 @@ describe("Tarpaulin format", () => {
}
});
- it("should return error when parsing coverage from invalid file", () => {
+ it("should return error when parsing coverage from invalid file", async () => {
// Arrange
const file = fs.readFileSync(reportPath("tarpaulin-invalid.html"), "utf-8");
const format = Formats.getFormat("tarpaulin");
// Act
- const result = format.parseCoverage(file);
+ const result = await format.parseCoverage(file);
+
+ // Assert
+ expect(typeof result).not.toEqual("number");
+ if (typeof result !== "number") {
+ expect(result.message).toEqual("Invalid report document");
+ }
+ });
+});
+
+describe("Cobertura format", () => {
+ const reportPath = (file: string): string =>
+ path.join(__dirname, "..", "example_reports", file);
+
+ it("should use the default color matcher", () => {
+ // Arrange
+ const format = Formats.getFormat("cobertura");
+
+ // Act
+ const matcher = format.matchColor;
+
+ // Assert
+ expect(matcher).toEqual(defaultColorMatches);
+ });
+
+ it("should parse coverage from a normal cobertura file", async () => {
+ // Arrange
+ const file = fs.readFileSync(reportPath("cobertura-report.xml"), "utf-8");
+
+ const format = Formats.getFormat("cobertura");
+
+ // Act
+ const result = await format.parseCoverage(file);
+
+ // Assert
+ expect(typeof result).toEqual("number");
+ if (typeof result === "number") {
+ // 96.17% is the result given in the document itself
+ expect(result.toFixed(2)).toEqual("96.04");
+ }
+ });
+
+ it("should parse coverage from an empty cobertura file", async () => {
+ // Arrange
+ const file = fs.readFileSync(reportPath("cobertura-empty.xml"), "utf-8");
+
+ const format = Formats.getFormat("cobertura");
+
+ // Act
+ const result = await format.parseCoverage(file);
+
+ // Assert
+ expect(typeof result).toEqual("number");
+ if (typeof result === "number") {
+ expect(result.toFixed(2)).toEqual("0.00");
+ }
+ });
+
+ it("should return error when parsing coverage from invalid file", async () => {
+ // Arrange
+ const file = fs.readFileSync(reportPath("cobertura-invalid.xml"), "utf-8");
+
+ const format = Formats.getFormat("cobertura");
+
+ // Act
+ const result = await format.parseCoverage(file);
// Assert
expect(typeof result).not.toEqual("number");
diff --git a/src/formats.ts b/src/formats.ts
index 11113f0..db13fa6 100644
--- a/src/formats.ts
+++ b/src/formats.ts
@@ -1,11 +1,12 @@
import { JSDOM } from "jsdom";
+import { Parser } from "xml2js";
import { InvalidReportDocumentError } from "./errors";
type CoverageResult = number | InvalidReportDocumentError;
export interface Format {
// returns the coverage value as %: Number(90.0), Number(100.0), Number(89.5)
- parseCoverage: (contents: string) => CoverageResult;
+ parseCoverage: (contents: string) => Promise<CoverageResult>;
matchColor: (coverage: number, style: GradientStyle) => string;
fileName: string;
}
@@ -46,7 +47,7 @@ export const defaultColorMatches = (
const FormatsObj: FormatObj = {
formats: {
tarpaulin: {
- parseCoverage: (contents: string): CoverageResult => {
+ parseCoverage: async (contents: string): Promise<CoverageResult> => {
const file = new JSDOM(contents).window.document;
const scripts = file.getElementsByTagName("script");
if (scripts.length === 0) {
@@ -75,6 +76,29 @@ const FormatsObj: FormatObj = {
},
matchColor: defaultColorMatches,
fileName: "index.html"
+ },
+ cobertura: {
+ parseCoverage: async (contents: string): Promise<CoverageResult> => {
+ try {
+ const document = await new Parser().parseStringPromise(contents);
+
+ if (document.coverage === undefined) {
+ return new InvalidReportDocumentError();
+ } else {
+ const validLines = Number(document.coverage.$["lines-valid"]);
+ const coveredLines = Number(document.coverage.$["lines-covered"]);
+ // do not error if LOC is 0
+ if (validLines === 0) {
+ return 0.0;
+ }
+ return (100 * coveredLines) / validLines;
+ }
+ } catch (err) {
+ return new InvalidReportDocumentError();
+ }
+ },
+ matchColor: defaultColorMatches,
+ fileName: "index.xml"
}
},
diff --git a/src/routes.test.ts b/src/routes.test.ts
index e1b8f74..f8bcfcd 100644
--- a/src/routes.test.ts
+++ b/src/routes.test.ts
@@ -174,12 +174,18 @@ describe("Badges and reports", () => {
"testbranch",
"testcommit"
);
- const actualReport = path.join(
+ const tarpaulinReport = path.join(
__dirname,
"..",
"example_reports",
"tarpaulin-report.html"
);
+ const coberturaReport = path.join(
+ __dirname,
+ "..",
+ "example_reports",
+ "cobertura-report.xml"
+ );
const fakeBadge = badgen({
label: "coverage",
status: "120%",
@@ -190,9 +196,13 @@ describe("Badges and reports", () => {
// place test files on HOST_DIR
await fs.promises.mkdir(reportPath, { recursive: true });
await fs.promises.copyFile(
- actualReport,
+ tarpaulinReport,
path.join(reportPath, "index.html")
);
+ await fs.promises.copyFile(
+ coberturaReport,
+ path.join(reportPath, "index.xml")
+ );
await fs.promises.writeFile(path.join(reportPath, "badge.svg"), fakeBadge);
});
@@ -202,7 +212,7 @@ describe("Badges and reports", () => {
.get("/v1/testorg/testrepo/testbranch/testcommit.html")
.expect("Content-Type", /html/)
.expect(200);
- const buffer = await fs.promises.readFile(actualReport);
+ const buffer = await fs.promises.readFile(tarpaulinReport);
expect(res.text).toEqual(buffer.toString("utf-8"));
});
@@ -214,6 +224,24 @@ describe("Badges and reports", () => {
});
});
+ describe("GET /v1/:org/:repo/:branch/:commit.xml", () => {
+ it("should retrieve the stored report file", async () => {
+ const res = await (await request())
+ .get("/v1/testorg/testrepo/testbranch/testcommit.xml")
+ .expect("Content-Type", /xml/)
+ .expect(200);
+ const buffer = await fs.promises.readFile(coberturaReport);
+
+ expect(res.text).toEqual(buffer.toString("utf-8"));
+ });
+
+ it("should return 404 if file does not exist", async () => {
+ await (await request())
+ .get("/v1/neorg/nerepo/nebranch/necommit.xml")
+ .expect(404);
+ });
+ });
+
describe("GET /v1/:org/:repo/:branch.html", () => {
it("should retrieve the stored report file with the associated head commit", async () => {
const mockMeta = mock();
@@ -221,7 +249,7 @@ describe("Badges and reports", () => {
.get("/v1/testorg/testrepo/testbranch.html")
.expect("Content-Type", /html/)
.expect(200);
- const buffer = await fs.promises.readFile(actualReport);
+ const buffer = await fs.promises.readFile(tarpaulinReport);
expect(mockMeta.getHeadCommit).toHaveBeenCalledTimes(1);
expect(res.text).toEqual(buffer.toString("utf-8"));
@@ -248,6 +276,40 @@ describe("Badges and reports", () => {
});
});
+ describe("GET /v1/:org/:repo/:branch.xml", () => {
+ it("should retrieve the stored report file with the associated head commit", async () => {
+ const mockMeta = mock();
+ const res = await (await request(mockMeta))
+ .get("/v1/testorg/testrepo/testbranch.xml")
+ .expect("Content-Type", /xml/)
+ .expect(200);
+ const buffer = await fs.promises.readFile(coberturaReport);
+
+ expect(mockMeta.getHeadCommit).toHaveBeenCalledTimes(1);
+ expect(res.text).toEqual(buffer.toString("utf-8"));
+ });
+
+ it("should return 404 if file does not exist", async () => {
+ await (await request()).get("/v1/neorg/nerepo/nebranch.xml").expect(404);
+ });
+
+ it("should return 404 if head commit not found", async () => {
+ const head = jest.fn(
+ () => new Promise(solv => solv(new BranchNotFoundError()))
+ );
+ await (await request(mock(head)))
+ .get("/v1/testorg/testrepo/testbranch.xml")
+ .expect(404);
+ });
+
+ it("should return 500 if promise is rejected", async () => {
+ const head = jest.fn(() => new Promise((_, rej) => rej("fooey")));
+ await (await request(mock(head)))
+ .get("/v1/testorg/testrepo/testbranch.xml")
+ .expect(500);
+ });
+ });
+
describe("GET /v1/:org/:repo/:branch/:commit.svg", () => {
it("should retrieve the stored report badge", async () => {
const res = await (await request())
@@ -307,9 +369,6 @@ describe("Uploads", () => {
"newthis",
"newthat"
);
- const data = fs.promises.readFile(
- path.join(__dirname, "..", "example_reports", "tarpaulin-report.html")
- );
beforeEach(async () => {
try {
@@ -320,6 +379,10 @@ describe("Uploads", () => {
});
describe("POST /v1/:org/:repo/:branch/:commit.html", () => {
+ const data = fs.promises.readFile(
+ path.join(__dirname, "..", "example_reports", "tarpaulin-report.html")
+ );
+
it("should upload the report and generate a badge", async () => {
const mockMeta = mock();
await (await request(mockMeta))
@@ -375,7 +438,10 @@ describe("Uploads", () => {
it("should return 413 when request body is not the appropriate format", async () => {
const file = await data;
- const bigData = Buffer.concat([file, file]);
+ let bigData = file;
+ while (bigData.length <= config.uploadLimit) {
+ bigData = Buffer.concat([bigData, file]);
+ }
await (await request())
.post(
`/v1/testorg/testrepo/newthis/newthat.html?token=${config.token}&format=tarpaulin`
@@ -404,4 +470,97 @@ describe("Uploads", () => {
.expect(500);
});
});
+
+ describe("POST /v1/:org/:repo/:branch/:commit.xml", () => {
+ const data = fs.promises.readFile(
+ path.join(__dirname, "..", "example_reports", "cobertura-report.xml")
+ );
+
+ it("should upload the report and generate a badge", async () => {
+ const mockMeta = mock();
+ await (await request(mockMeta))
+ .post(
+ `/v1/testorg/testrepo/newthis/newthat.xml?token=${config.token}&format=cobertura`
+ )
+ .send(await data)
+ .expect(200);
+
+ expect(mockMeta.updateBranch).toBeCalledWith({
+ organization: "testorg",
+ repository: "testrepo",
+ branch: "newthis",
+ head: "newthat"
+ });
+ expect(mockMeta.updateBranch).toHaveBeenCalledTimes(1);
+ await fs.promises.access(
+ path.join(reportPath, "index.xml"),
+ fs.constants.R_OK
+ );
+ await fs.promises.access(
+ path.join(reportPath, "badge.svg"),
+ fs.constants.R_OK
+ );
+ });
+
+ it("should return 401 when token is not correct", async () => {
+ await (await request())
+ .post(
+ `/v1/testorg/testrepo/newthis/newthat.xml?token=wrong&format=cobertura`
+ )
+ .send(await data)
+ .expect(401);
+ });
+
+ it("should return 406 with an invalid format", async () => {
+ await (await request())
+ .post(
+ `/v1/testorg/testrepo/newthis/newthat.xml?token=${config.token}&format=pepperoni`
+ )
+ .send(await data)
+ .expect(406);
+ });
+
+ it("should return 400 when request body is not the appropriate format", async () => {
+ await (await request())
+ .post(
+ `/v1/testorg/testrepo/newthis/newthat.xml?token=${config.token}&format=cobertura`
+ )
+ .send("This is not a file")
+ .expect(400);
+ });
+
+ it("should return 413 when request body is not the appropriate format", async () => {
+ const file = await data;
+ let bigData = file;
+ while (bigData.length <= config.uploadLimit) {
+ bigData = Buffer.concat([bigData, file]);
+ }
+ await (await request())
+ .post(
+ `/v1/testorg/testrepo/newthis/newthat.xml?token=${config.token}&format=cobertura`
+ )
+ .send(bigData)
+ .expect(413);
+ });
+
+ it("should return 500 when Metadata does not create branch", async () => {
+ const update = jest.fn(() => new Promise(solv => solv(false)));
+ await (await request(mock(jest.fn(), update)))
+ .post(
+ `/v1/testorg/testrepo/newthis/newthat.xml?token=${config.token}&format=cobertura`
+ )
+ .send(await data)
+ .expect(500);
+ });
+
+ it("should return 500 when promise chain is rejected", async () => {
+ const update = jest.fn(() => new Promise((_, rej) => rej("fooey 2")));
+ await (await request(mock(jest.fn(), update)))
+ .post(
+ `/v1/testorg/testrepo/newthis/newthat.xml?token=${config.token}&format=cobertura`
+ )
+ .send(await data)
+ .expect(500);
+ });
+ });
});
diff --git a/src/routes.ts b/src/routes.ts
index 634af7d..df23a54 100644
--- a/src/routes.ts
+++ b/src/routes.ts
@@ -26,7 +26,7 @@ export default (metadata: Metadata): Router => {
identity.branch,
identity.head
);
- const coverage = formatter.parseCoverage(contents);
+ const coverage = await formatter.parseCoverage(contents);
if (typeof coverage !== "number") {
return coverage;
}
@@ -138,6 +138,68 @@ export default (metadata: Metadata): Router => {
});
});
+ // Upload XML file
+ router.post("/v1/:org/:repo/:branch/:commit.xml", (req, res) => {
+ const { org, repo, branch, commit } = req.params;
+
+ const { token, format } = req.query;
+ if (token != metadata.getToken()) {
+ return res.status(401).send(Messages.InvalidToken);
+ }
+
+ if (typeof format !== "string" || !formats.listFormats().includes(format)) {
+ return res.status(406).send(Messages.InvalidFormat);
+ }
+
+ const limit = metadata.getUploadLimit();
+ if (Number(req.headers["content-length"] ?? 0) > limit) {
+ return res.status(413).send(Messages.FileTooLarge);
+ }
+
+ let contents = "";
+ req.on("data", raw => {
+ if (contents.length <= limit) {
+ contents += raw;
+ }
+ });
+ req.on("end", async () => {
+ // Ignore large requests
+ if (contents.length > limit) {
+ return res.status(413).send(Messages.FileTooLarge);
+ }
+
+ const formatter = formats.getFormat(format);
+ const identity = {
+ organization: org,
+ repository: repo,
+ branch,
+ head: commit
+ };
+
+ try {
+ const result = await commitFormatDocs(contents, identity, formatter);
+
+ if (typeof result === "boolean") {
+ if (result) {
+ return res.status(200).send();
+ } else {
+ logger.error(
+ "Unknown error while attempting to commit branch update"
+ );
+ return res.status(500).send(Messages.UnknownError);
+ }
+ } else {
+ return res.status(400).send(Messages.InvalidFormat);
+ }
+ } catch (err) {
+ logger.error(
+ err ?? "Unknown error occurred while processing POST request"
+ );
+ return res.status(500).send(Messages.UnknownError);
+ }
+ });
+ });
+
const retrieveFile = (
res: express.Response,
identity: HeadIdentity,
@@ -188,6 +250,34 @@ export default (metadata: Metadata): Router => {
router.get("/v1/:org/:repo/:branch.html", (req, res) => {
const { org, repo, branch } = req.params;
+ const format = formats.formats.tarpaulin;
+
+ metadata.getHeadCommit(org, repo, branch).then(
+ result => {
+ if (typeof result === "string") {
+ const identity = {
+ organization: org,
+ repository: repo,
+ branch,
+ head: result.toString()
+ };
+ retrieveFile(res, identity, format.fileName);
+ } else {
+ res.status(404).send(result.message);
+ }
+ },
+ err => {
+ logger.error(
+ err ?? "Error occurred while fetching commit for GET request"
+ );
+ res.status(500).send(Messages.UnknownError);
+ }
+ );
+ });
+
+ router.get("/v1/:org/:repo/:branch.xml", (req, res) => {
+ const { org, repo, branch } = req.params;
+ const format = formats.formats.cobertura;
metadata.getHeadCommit(org, repo, branch).then(
result => {
@@ -198,7 +288,7 @@ export default (metadata: Metadata): Router => {
branch,
head: result.toString()
};
- retrieveFile(res, identity, "index.html");
+ retrieveFile(res, identity, format.fileName);
} else {
res.status(404).send(result.message);
}
@@ -227,13 +317,26 @@ export default (metadata: Metadata): Router => {
// provide hard link for commit
router.get("/v1/:org/:repo/:branch/:commit.html", (req, res) => {
const { org, repo, branch, commit } = req.params;
+ const format = formats.formats.tarpaulin;
+ const identity = {
+ organization: org,
+ repository: repo,
+ branch,
+ head: commit
+ };
+ retrieveFile(res, identity, format.fileName);
+ });
+
+ router.get("/v1/:org/:repo/:branch/:commit.xml", (req, res) => {
+ const { org, repo, branch, commit } = req.params;
+ const format = formats.formats.cobertura;
const identity = {
organization: org,
repository: repo,
branch,
head: commit
};
- retrieveFile(res, identity, "index.html");
+ retrieveFile(res, identity, format.fileName);
});
router.use((_, res) => {