aboutsummaryrefslogtreecommitdiff
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
parent77575aab559f058d886a691eefe262cf0f306710 (diff)
downloadao-coverage-6bbd3f03104e6dcd9da89a8ec5dcb5d992ee3ed5.tar.gz
ao-coverage-6bbd3f03104e6dcd9da89a8ec5dcb5d992ee3ed5.tar.bz2
ao-coverage-6bbd3f03104e6dcd9da89a8ec5dcb5d992ee3ed5.zip
#7 Add Cobertura format
-rw-r--r--CHANGELOG.md4
-rw-r--r--example_reports/cobertura-empty.xml1
-rw-r--r--example_reports/cobertura-invalid.xml1
-rw-r--r--example_reports/cobertura-report.xml1
-rw-r--r--package-lock.json64
-rw-r--r--package.json4
-rw-r--r--public/templates/bash.template14
-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
11 files changed, 454 insertions, 26 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c1b1a66..0736b0a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Added
+
+- Cobertura XML format
+
### Changed
- Moved stage values for gradient to environment and made accessible via metadata
diff --git a/example_reports/cobertura-empty.xml b/example_reports/cobertura-empty.xml
new file mode 100644
index 0000000..7459b91
--- /dev/null
+++ b/example_reports/cobertura-empty.xml
@@ -0,0 +1 @@
+<?xml version="1.0"?><coverage lines-covered="0" lines-valid="0" line-rate="0" branches-covered="0" branches-valid="0" branch-rate="0" complexity="0" version="1.9" timestamp="1631817281"></coverage> \ No newline at end of file
diff --git a/example_reports/cobertura-invalid.xml b/example_reports/cobertura-invalid.xml
new file mode 100644
index 0000000..236f9c4
--- /dev/null
+++ b/example_reports/cobertura-invalid.xml
@@ -0,0 +1 @@
+<?xml version="1.0"?><b>This isn't a report. 50.64%</b> \ No newline at end of file
diff --git a/example_reports/cobertura-report.xml b/example_reports/cobertura-report.xml
new file mode 100644
index 0000000..65c4036
--- /dev/null
+++ b/example_reports/cobertura-report.xml
@@ -0,0 +1 @@
+<?xml version="1.0"?><coverage lines-covered="194" lines-valid="202" line-rate="0.9847282864932456" branches-covered="0" branches-valid="0" branch-rate="0" complexity="0" version="1.9" timestamp="1631817281"><sources><source>/home/professor/Documents/prj/august-offensive</source></sources><packages><package name="src/messages" line-rate="1" branch-rate="0" complexity="0"><classes><class name="callback" filename="src/messages/callback.rs" line-rate="1" branch-rate="0" complexity="0"><methods/><lines><line number="13" hits="1"/><line number="19" hits="1"/><line number="20" hits="1"/><line number="29" hits="3"/><line number="31" hits="1"/><line number="33" hits="1"/><line number="34" hits="1"/><line number="39" hits="1"/><line number="42" hits="2"/><line number="46" hits="3"/><line number="49" hits="1"/><line number="50" hits="1"/><line number="51" hits="1"/><line number="53" hits="1"/><line number="56" hits="1"/><line number="59" hits="2"/><line number="60" hits="2"/></lines></class><class name="mod" filename="src/messages/mod.rs" line-rate="1" branch-rate="0" complexity="0"><methods/><lines><line number="18" hits="3"/><line number="22" hits="3"/><line number="23" hits="3"/><line number="24" hits="3"/></lines></class><class name="not_understood" filename="src/messages/not_understood.rs" line-rate="1" branch-rate="0" complexity="0"><methods/><lines><line number="10" hits="1"/><line number="16" hits="1"/><line number="17" hits="1"/><line number="26" hits="3"/><line number="28" hits="1"/><line number="31" hits="1"/><line number="34" hits="2"/><line number="38" hits="3"/><line number="40" hits="1"/><line number="41" hits="1"/><line number="44" hits="1"/><line number="47" hits="2"/><line number="48" hits="2"/></lines></class></classes></package><package name="src/routes" line-rate="0.9937888198757764" branch-rate="0" complexity="0"><classes><class name="callback" filename="src/routes/callback.rs" line-rate="1" branch-rate="0" complexity="0"><methods/><lines><line number="4" hits="1"/><line number="5" hits="1"/><line number="6" hits="2"/><line number="9" hits="1"/><line number="10" hits="1"/><line number="11" hits="1"/><line number="14" hits="2"/><line number="24" hits="3"/><line number="26" hits="1"/><line number="27" hits="1"/><line number="29" hits="1"/><line number="30" hits="3"/><line number="31" hits="1"/><line number="32" hits="1"/><line number="35" hits="1"/><line number="38" hits="1"/><line number="40" hits="2"/><line number="41" hits="1"/><line number="42" hits="2"/><line number="43" hits="1"/><line number="44" hits="2"/><line number="48" hits="3"/><line number="50" hits="1"/><line number="51" hits="1"/><line number="53" hits="1"/><line number="54" hits="3"/><line number="55" hits="1"/><line number="56" hits="1"/><line number="59" hits="1"/><line number="62" hits="1"/><line number="64" hits="2"/><line number="65" hits="1"/><line number="66" hits="2"/><line number="67" hits="1"/><line number="68" hits="2"/><line number="72" hits="3"/><line number="74" hits="1"/><line number="75" hits="1"/><line number="76" hits="1"/><line number="79" hits="1"/><line number="82" hits="1"/><line number="84" hits="2"/><line number="85" hits="1"/><line number="86" hits="2"/><line number="87" hits="2"/><line number="88" hits="2"/></lines></class><class name="format_msg" filename="src/routes/format_msg.rs" line-rate="0.9736842105263158" branch-rate="0" complexity="0"><methods/><lines><line number="11" hits="2"/><line number="12" hits="2"/><line number="15" hits="2"/><line number="27" hits="4"/><line number="28" hits="4"/><line number="29" hits="7"/><line number="30" hits="1"/><line number="33" hits="12"/><line number="34" hits="0"/><line number="35" hits="3"/><line number="47" hits="3"/><line number="49" hits="1"/><line number="50" hits="1"/><line number="52" hits="1"/><line number="57" hits="1"/><line number="60" hits="1"/><line number="61" hits="2"/><line number="65" hits="3"/><line number="67" hits="1"/><line number="68" hits="1"/><line number="71" hits="1"/><line number="74" hits="2"/><line number="75" hits="2"/><line number="79" hits="3"/><line number="81" hits="1"/><line number="82" hits="1"/><line number="87" hits="2"/><line number="90" hits="3"/><line number="93" hits="2"/><line number="94" hits="2"/><line number="96" hits="1"/><line number="97" hits="1"/><line number="103" hits="1"/><line number="104" hits="2"/><line number="109" hits="3"/><line number="116" hits="1"/><line number="119" hits="1"/><line number="122" hits="2"/></lines></class><class name="mod" filename="src/routes/mod.rs" line-rate="1" branch-rate="0" complexity="0"><methods/><lines><line number="17" hits="2"/><line number="19" hits="2"/><line number="20" hits="6"/><line number="24" hits="3"/><line number="25" hits="3"/><line number="42" hits="3"/><line number="44" hits="1"/><line number="45" hits="1"/><line number="46" hits="3"/><line number="49" hits="2"/><line number="52" hits="2"/><line number="54" hits="2"/><line number="55" hits="1"/><line number="56" hits="2"/><line number="60" hits="3"/><line number="62" hits="1"/><line number="63" hits="1"/><line number="64" hits="3"/><line number="67" hits="2"/><line number="70" hits="2"/><line number="72" hits="2"/><line number="73" hits="1"/><line number="74" hits="2"/><line number="78" hits="3"/><line number="80" hits="1"/><line number="81" hits="1"/><line number="82" hits="3"/><line number="85" hits="2"/><line number="88" hits="2"/><line number="92" hits="3"/><line number="94" hits="1"/><line number="97" hits="1"/><line number="100" hits="2"/><line number="104" hits="3"/><line number="106" hits="1"/><line number="109" hits="1"/><line number="112" hits="1"/><line number="116" hits="3"/><line number="118" hits="1"/><line number="121" hits="1"/><line number="124" hits="1"/><line number="127" hits="1"/><line number="128" hits="20"/><line number="129" hits="7"/><line number="133" hits="2"/><line number="134" hits="2"/><line number="135" hits="8"/><line number="136" hits="2"/><line number="139" hits="4"/><line number="142" hits="3"/><line number="143" hits="3"/><line number="144" hits="3"/><line number="145" hits="3"/><line number="146" hits="3"/><line number="147" hits="6"/><line number="152" hits="6"/><line number="153" hits="3"/></lines></class><class name="not_understood" filename="src/routes/not_understood.rs" line-rate="1" branch-rate="0" complexity="0"><methods/><lines><line number="4" hits="2"/><line number="6" hits="2"/><line number="9" hits="2"/><line number="10" hits="2"/><line number="21" hits="3"/><line number="23" hits="1"/><line number="24" hits="1"/><line number="27" hits="1"/><line number="30" hits="1"/><line number="32" hits="2"/><line number="33" hits="1"/><line number="34" hits="2"/><line number="38" hits="3"/><line number="40" hits="1"/><line number="41" hits="1"/><line number="44" hits="1"/><line number="47" hits="1"/><line number="49" hits="2"/><line number="50" hits="1"/><line number="51" hits="2"/></lines></class></classes></package><package name="src" line-rate="0.9603960396039604" branch-rate="0" complexity="0"><classes><class name="schema" filename="src/schema.rs" line-rate="0" branch-rate="0" complexity="0"><methods/><lines><line number="75" hits="0"/><line number="76" hits="0"/><line number="77" hits="0"/><line number="78" hits="0"/><line number="79" hits="0"/><line number="80" hits="0"/><line number="81" hits="0"/></lines></class></classes></package></packages></coverage> \ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 13ce4c2..d9b5016 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
"@types/express": "^4.17.6",
"@types/jsdom": "^12.2.4",
"@types/mongodb": "^3.5.16",
+ "@types/xml2js": "^0.4.9",
"badgen": "^3.0.1",
"dotenv": "^8.2.0",
"express": "^4.17.1",
@@ -19,7 +20,8 @@
"jsdom": "^15.2.1",
"mongodb": "^3.5.7",
"typescript": "^3.8.3",
- "winston": "^3.2.1"
+ "winston": "^3.2.1",
+ "xml2js": "^0.4.23"
},
"devDependencies": {
"@microsoft/tsdoc": "^0.12.19",
@@ -1478,6 +1480,14 @@
"integrity": "sha512-txGIh+0eDFzKGC25zORnswy+br1Ha7hj5cMVwKIU7+s0U2AxxJru/jZSMU6OC9MJWP6+pc/hc6ZjyZShpsyY2g==",
"dev": true
},
+ "node_modules/@types/xml2js": {
+ "version": "0.4.9",
+ "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.9.tgz",
+ "integrity": "sha512-CHiCKIihl1pychwR2RNX5mAYmJDACgFVCMT5OArMaO3erzwXVcBqPcusr+Vl8yeeXukxZqtF8mZioqX+mpjjdw==",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/yargs": {
"version": "15.0.14",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz",
@@ -8252,6 +8262,11 @@
"node": ">=6"
}
},
+ "node_modules/sax": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
+ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
+ },
"node_modules/saxes": {
"version": "3.1.11",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.11.tgz",
@@ -10098,6 +10113,26 @@
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz",
"integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw=="
},
+ "node_modules/xml2js": {
+ "version": "0.4.23",
+ "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
+ "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
+ "dependencies": {
+ "sax": ">=0.6.0",
+ "xmlbuilder": "~11.0.0"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/xmlbuilder": {
+ "version": "11.0.1",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
+ "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
"node_modules/xmlchars": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
@@ -11331,6 +11366,14 @@
"integrity": "sha512-txGIh+0eDFzKGC25zORnswy+br1Ha7hj5cMVwKIU7+s0U2AxxJru/jZSMU6OC9MJWP6+pc/hc6ZjyZShpsyY2g==",
"dev": true
},
+ "@types/xml2js": {
+ "version": "0.4.9",
+ "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.9.tgz",
+ "integrity": "sha512-CHiCKIihl1pychwR2RNX5mAYmJDACgFVCMT5OArMaO3erzwXVcBqPcusr+Vl8yeeXukxZqtF8mZioqX+mpjjdw==",
+ "requires": {
+ "@types/node": "*"
+ }
+ },
"@types/yargs": {
"version": "15.0.14",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz",
@@ -16557,6 +16600,11 @@
"sparse-bitfield": "^3.0.3"
}
},
+ "sax": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
+ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
+ },
"saxes": {
"version": "3.1.11",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.11.tgz",
@@ -18044,6 +18092,20 @@
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz",
"integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw=="
},
+ "xml2js": {
+ "version": "0.4.23",
+ "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
+ "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
+ "requires": {
+ "sax": ">=0.6.0",
+ "xmlbuilder": "~11.0.0"
+ }
+ },
+ "xmlbuilder": {
+ "version": "11.0.1",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
+ "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="
+ },
"xmlchars": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
diff --git a/package.json b/package.json
index 58edad6..15aeb26 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,7 @@
"@types/express": "^4.17.6",
"@types/jsdom": "^12.2.4",
"@types/mongodb": "^3.5.16",
+ "@types/xml2js": "^0.4.9",
"badgen": "^3.0.1",
"dotenv": "^8.2.0",
"express": "^4.17.1",
@@ -33,7 +34,8 @@
"jsdom": "^15.2.1",
"mongodb": "^3.5.7",
"typescript": "^3.8.3",
- "winston": "^3.2.1"
+ "winston": "^3.2.1",
+ "xml2js": "^0.4.23"
},
"devDependencies": {
"@microsoft/tsdoc": "^0.12.19",
diff --git a/public/templates/bash.template b/public/templates/bash.template
index 4eb3a47..375143e 100644
--- a/public/templates/bash.template
+++ b/public/templates/bash.template
@@ -12,6 +12,8 @@ format="tarpaulin"
report=""
token="$COV_TOKEN"
curl_verbosity=""
+content_type="text/html"
+extension="html"
function verbose_say() {
if [ -z "$SILENT" ] && [ "$VERBOSE" == "true" ]; then
@@ -51,6 +53,10 @@ verbose_say
if [ "$format" == "tarpaulin" ]; then
report="${REPORT_FILE:-tarpaulin-report.html}"
+elif [ "$format" == "cobertura" ]; then
+ report="${REPORT_FILE:-cobertura.xml}"
+ content_type="application/xml"
+ extension="xml"
fi
if [[ ! -f "$report" ]]; then
@@ -65,9 +71,9 @@ fi
say "Uploading $report . . ."
response=$(curl -X POST --data-binary "@$report" \
- -H 'Content-Type: text/html' \
+ -H "Content-Type: $content_type" \
$curl_verbosity \
- "$url/v1/$repo/$branch/$commit.html?token=$token&format=$format")
+ "$url/v1/$repo/$branch/$commit.$extension?token=$token&format=$format")
if [ ! -z "$response" ]; then
say "Error uploading report: $response"
@@ -75,10 +81,10 @@ if [ ! -z "$response" ]; then
else
say "Successfully uploaded report!"
say
- say "View uploaded report at: $url/v1/$repo/$branch/$commit.html"
+ say "View uploaded report at: $url/v1/$repo/$branch/$commit.$extension"
say "View coverage badge at: $url/v1/$repo/$branch/$commit.svg"
say
say "Shorthand links are also available, as the latest commit of this branch."
- say "View latest report for branch $branch: $url/v1/$repo/$branch.html"
+ say "View latest report for branch $branch: $url/v1/$repo/$branch.$extension"
say "View latest badge for branch $branch: $url/v1/$repo/$branch.svg"
fi \ No newline at end of file
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()