Skip to content

Commit f3e5705

Browse files
Stefan Hölzleuniqueck
authored andcommitted
New: Implement JUnitXMLReporter
#1
1 parent 6178b08 commit f3e5705

24 files changed

+1040
-92
lines changed

lib/allChecksRunner.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
const HtmlPage = require('./html/htmlPage')
22
const SinglePageResult = require('./singlePageResult')
3-
const Logger = require('./logging/LoggingFacacde')
43

54
class AllChecksRunner {
6-
constructor (config) {
5+
constructor (config, /* LoggingFacade */ logger) {
76
this.config = config
8-
this.logger = new Logger(config)
7+
this.logger = logger
98
}
109

1110
async performAllChecks () {

lib/cli/config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ exports.loadConfig = filepath => {
8181
* @returns {string|null} Filepath to config, if found
8282
*/
8383
exports.findConfig = (cwd = utils.cwd()) => {
84+
debug(`Searching for config files in path: ${cwd}`)
8485
const filepath = findUp.sync(exports.CONFIG_FILES, { cwd })
8586
if (filepath) {
8687
debug('findConfig: found config file %s', filepath)

lib/cli/options.js

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
'use strict'
22

3+
const debug = require('debug')('htmlSanityCheck:cli:options')
4+
35
/**
46
* Main entry point for handling filesystem-based configuration,
57
* whether that's a config file or `package.json` or whatever.
68
* @module lib/cli/options
79
* @private
810
*/
911

10-
const { loadConfig, findConfig } = require('./config')
12+
const { loadConfig, findConfig, CONFIG_FILES } = require('./config')
1113
const yargsParser = require('yargs-parser')
1214
const ansi = require('ansi-colors')
1315
const htmlsanitycheckrc = require('../htmlsanitycheckrc.json')
@@ -127,10 +129,22 @@ const parse = (args = [], defaultValues = {}, ...configObjects) => {
127129
* @returns {external:yargsParser.Arguments|void} Parsed config, or nothing if `args.config` is `false`
128130
*/
129131
const loadRc = (args = {}) => {
130-
if (args.config !== false) {
131-
const config = args.config || findConfig()
132-
return config ? loadConfig(config) : {}
132+
let configFile
133+
if (args.config != null) {
134+
debug(`Loading config file specified by cli command: ${args.config}`)
135+
configFile = args.config
136+
} else {
137+
debug(`Searching for default config files: ${CONFIG_FILES}`)
138+
configFile = findConfig()
139+
}
140+
let config = {}
141+
if (configFile != null) {
142+
debug(`Loading config file ${configFile}`)
143+
config = loadConfig()
144+
} else {
145+
debug('No config file defined by cli command and no default config files found')
133146
}
147+
return config
134148
}
135149

136150
module.exports.loadRc = loadRc
@@ -159,6 +173,7 @@ const loadOptions = (argv = []) => {
159173
args._ = args._.concat(rcConfig._ || [])
160174
}
161175

176+
debug(`Loading default config: ${JSON.stringify(htmlsanitycheckrc)}`)
162177
args = parse(args._, htmlsanitycheckrc, args, rcConfig || {})
163178

164179
// recombine positional arguments and "spec"

lib/cli/run-option-metadata.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@ const TYPES = (exports.types = {
2424
string: [
2525
'config',
2626
'sourceDir',
27-
'package',
28-
'reporter'
27+
'package'
2928
]
3029
})
3130

lib/cli/run.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99

1010
const HtmlSanityCheck = require('../allChecksRunner')
1111
const { types, aliases } = require('./run-option-metadata')
12+
const Logger = require('../logging/LoggingFacade')
13+
const { createReporters } = require('../utils')
14+
const debug = require('debug')('htmlSanityCheck:cli:run')
1215

1316
/**
1417
* Logical option groups
@@ -62,7 +65,14 @@ exports.builder = yargs =>
6265

6366
exports.handler = async function (argv) {
6467
try {
65-
await new HtmlSanityCheck(argv).performAllChecks()
68+
const config = argv
69+
debug(`config: ${JSON.stringify(config)}`)
70+
const logger = new Logger(config)
71+
const results = await new HtmlSanityCheck(config, logger).performAllChecks()
72+
const reporters = createReporters(config.reporter, logger)
73+
reporters.forEach(/* Reporter */ reporter => {
74+
reporter.reportFindings(results)
75+
})
6676
} catch (err) {
6777
console.error('\n Exception during run:', err)
6878
process.exit(1)

lib/finding.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
'use strict'
22

33
class Finding {
4-
constructor (whatIsTheProblem, nrOfOccurences) {
4+
constructor (/* string */ whatIsTheProblem, /* number */ nrOfOccurrences, /* string[] */ suggestions = []) {
55
this.whatIsTheProblem = whatIsTheProblem
6-
this.nrOfOccurences = nrOfOccurences
6+
this.nrOfOccurences = nrOfOccurrences
7+
this.suggestions = suggestions
78
}
89

910
toString () {

lib/htmlsanitycheckrc.json

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
{
22
"sourceDir": ".",
3-
"extension": ["html"],
3+
"extension": [
4+
"html"
5+
],
46
"recursive": true,
57
"package": "./package.json",
6-
"reporter": "junit",
8+
"reporter": {
9+
"junit": {
10+
"outputPath": "./test-results/htmlSanityCheck",
11+
"enabled": true
12+
}
13+
},
714
"httpSuccessCodes": [200,201,202,203,204,205,206,207,208,226],
815
"httpWarningCodes": [100,101,102,300,301,302,303,304,305,306,307,308],
916
"httpErrorCodes": [400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,500,501,502,503,504,505,506,507,508,509,510,511],
@@ -19,6 +26,5 @@
1926
"ignoreLocalHost": false,
2027
"ignoreIPAddresses": false,
2128
"httpConnectionTimeout": 5000,
22-
"junitResultsDir": "./test-results/htmlSanityCheck",
2329
"checkingResultDir": "./reports/htmlSanityCheck"
2430
}

lib/logging/LoggingFacacde.js renamed to lib/logging/LoggingFacade.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ class LoggingFacade {
88
this.log('trace', msg)
99
}
1010

11+
info (msg) {
12+
this.log('info', msg)
13+
}
14+
1115
log (logLevel, msg) {
1216
if (logLevel === 'trace') {
1317
if (this.traceLogging) {

lib/reporter/JUnitXmlReporter.js

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
const FILE_EXTENSION = 'xml'
2+
const { create } = require('xmlbuilder2')
3+
const fs = require('fs')
4+
const path = require('path')
5+
const { v4: uuidv4 } = require('uuid')
6+
const Reporter = require('./Reporter')
7+
8+
class JUnitXmlReporter extends Reporter {
9+
constructor (/* JUnitXmlReporterConfig */ config, /* LoggingFacade */ logger) {
10+
super(config, logger)
11+
this.config = config
12+
}
13+
14+
reportFindings (/* SinglePageResult[] */ resultsForAllPages) {
15+
this.log.info(`resultsForAllPages ${JSON.stringify(resultsForAllPages)}`)
16+
17+
this.initReport()
18+
19+
this.reportOverallSummary()
20+
21+
this.reportAllPages(resultsForAllPages)
22+
23+
this.closeReport()
24+
}
25+
26+
initReport () {
27+
this.log.info(`Creating JUnit XML report output path: ${this.outputPath}`)
28+
fs.mkdirSync(this.outputPath, { recursive: true })
29+
}
30+
31+
reportOverallSummary () {
32+
// https://github.com/aim42/htmlSanityCheck/blob/main/src/main/groovy/org/aim42/htmlsanitycheck/report/JUnitXmlReporter.groovy#L50
33+
// empty in original impl
34+
}
35+
36+
reportAllPages (/* SinglePageResult[] */ resultsForAllPages) {
37+
// https://github.com/aim42/htmlSanityCheck/blob/main/src/main/groovy/org/aim42/htmlsanitycheck/report/Reporter.groovy#L73
38+
resultsForAllPages.forEach(pageResult => {
39+
this.reportPageSummary(pageResult)
40+
this.reportPageDetails(pageResult)
41+
this.reportPageFooter()
42+
})
43+
}
44+
45+
reportPageSummary (/* SinglePageResult */ pageResult) {
46+
// https://github.com/aim42/htmlSanityCheck/blob/main/src/main/groovy/org/aim42/htmlsanitycheck/report/JUnitXmlReporter.groovy#L54
47+
// Determine the name based on available properties or generate a UUID
48+
const nameParts = []
49+
nameParts.push(pageResult.pageFilePath)
50+
if (pageResult.pageTitle) {
51+
nameParts.push(pageResult.pageTitle)
52+
} else {
53+
nameParts.push(uuidv4())
54+
}
55+
const name = nameParts.join('_')
56+
const sanitizedPath = name.replace(/[^A-Za-z0-9_-]+/g, '_')
57+
const testOutputFileName = `TEST-unit-html-${sanitizedPath}.${FILE_EXTENSION}`
58+
const testOutputFilePath = path.join(this.outputPath, testOutputFileName)
59+
60+
// Initialize the XML document
61+
const doc = create({ version: '1.0', encoding: 'UTF-8' })
62+
.ele('testsuite', {
63+
tests: pageResult.nrOfItemsCheckedOnPage().toString(),
64+
failures: pageResult.nrOfFindingsOnPage().toString(),
65+
errors: '0',
66+
time: '0',
67+
name: `${pageResult.pageFileName}: ${pageResult.pageTitle}`
68+
})
69+
70+
// Add test cases
71+
pageResult.allCheckerResults?.forEach(singleCheckResult => {
72+
const testcase = doc.ele('testcase', {
73+
assertions: singleCheckResult.nrOfItemsChecked.toString(),
74+
time: '0',
75+
name: singleCheckResult.whatIsChecked || ''
76+
})
77+
78+
singleCheckResult.findings.forEach(finding => {
79+
testcase.ele('failure', {
80+
type: singleCheckResult.sourceItemName + ' - ' + singleCheckResult.targetItemName,
81+
message: finding.whatIsTheProblem
82+
}).txt(finding.suggestions?.join(', ') || '')
83+
})
84+
})
85+
86+
// Convert the document to string
87+
const xmlString = doc.end({ prettyPrint: true })
88+
89+
// Write the XML string to a file
90+
this.log.info(`Writing JUnit XML report to ${testOutputFilePath}`)
91+
fs.writeFileSync(testOutputFilePath, xmlString)
92+
}
93+
94+
reportPageDetails (/* SinglePageResult */ pageResult) {
95+
pageResult.allCheckerResults.forEach(resultForOneCheck => {
96+
this.reportSingleCheckSummary(resultForOneCheck)
97+
this.reportSingleCheckDetails(resultForOneCheck)
98+
})
99+
}
100+
101+
reportPageFooter () {
102+
// empty in original impl: https://github.com/aim42/htmlSanityCheck/blob/main/src/main/groovy/org/aim42/htmlsanitycheck/report/JUnitXmlReporter.groovy#L87
103+
}
104+
105+
reportSingleCheckSummary (/* SingleCheckResult */ resultForOneCheck) {
106+
// empty in original impl: https://github.com/aim42/htmlSanityCheck/blob/main/src/main/groovy/org/aim42/htmlsanitycheck/report/JUnitXmlReporter.groovy#L90
107+
}
108+
109+
reportSingleCheckDetails (/* SingleCheckResult */ resultForOneCheck) {
110+
// empty in original impl: https://github.com/aim42/htmlSanityCheck/blob/main/src/main/groovy/org/aim42/htmlsanitycheck/report/JUnitXmlReporter.groovy#L94
111+
}
112+
113+
closeReport () {
114+
// empty in original impl: https://github.com/aim42/htmlSanityCheck/blob/main/src/main/groovy/org/aim42/htmlsanitycheck/report/JUnitXmlReporter.groovy#L98
115+
}
116+
}
117+
118+
module.exports = {
119+
createReporter: (/* JUnitXmlReporterConfig */ config, /* LoggingFacade */ logger) => {
120+
return new JUnitXmlReporter(config, logger)
121+
}
122+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import ReporterConfig from './ReporterConfig'
2+
3+
class JUnitXmlReporterConfig extends ReporterConfig {
4+
}
5+
6+
module.exports = JUnitXmlReporterConfig

0 commit comments

Comments
 (0)