diff options
| author | Kevin J Hoerr <kjhoerr@protonmail.com> | 2021-09-16 15:45:34 -0400 |
|---|---|---|
| committer | Kevin J Hoerr <kjhoerr@protonmail.com> | 2021-09-16 15:45:34 -0400 |
| commit | 6bbd3f03104e6dcd9da89a8ec5dcb5d992ee3ed5 (patch) | |
| tree | 6f44b79750ad00267c8840b3aa472ee64aefc66b /src | |
| parent | 77575aab559f058d886a691eefe262cf0f306710 (diff) | |
| download | ao-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.ts | 79 | ||||
| -rw-r--r-- | src/formats.ts | 28 | ||||
| -rw-r--r-- | src/routes.test.ts | 175 | ||||
| -rw-r--r-- | src/routes.ts | 109 |
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) => { |
