Merge pull request #1187 from awinder/feature/junit-reporter

Feature/junit reporter
This commit is contained in:
Anoop M D 2023-12-18 04:05:37 +05:30 committed by GitHub
commit e3865d4710
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 257 additions and 9 deletions

8
package-lock.json generated
View File

@ -17311,7 +17311,6 @@
},
"node_modules/xmlbuilder": {
"version": "15.1.1",
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=8.0"
@ -17601,7 +17600,7 @@
},
"packages/bruno-cli": {
"name": "@usebruno/cli",
"version": "1.2.1",
"version": "1.2.2",
"license": "MIT",
"dependencies": {
"@usebruno/js": "0.9.4",
@ -17620,6 +17619,7 @@
"mustache": "^4.2.0",
"qs": "^6.11.0",
"socks-proxy-agent": "^8.0.2",
"xmlbuilder": "^15.1.1",
"yargs": "^17.6.2"
},
"bin": {
@ -21840,6 +21840,7 @@
"mustache": "^4.2.0",
"qs": "^6.11.0",
"socks-proxy-agent": "^8.0.2",
"xmlbuilder": "^15.1.1",
"yargs": "^17.6.2"
},
"dependencies": {
@ -30144,8 +30145,7 @@
}
},
"xmlbuilder": {
"version": "15.1.1",
"devOptional": true
"version": "15.1.1"
},
"xtend": {
"version": "4.0.2"

View File

@ -1,6 +1,6 @@
{
"name": "@usebruno/cli",
"version": "1.2.1",
"version": "1.2.2",
"license": "MIT",
"main": "src/index.js",
"bin": {
@ -40,6 +40,7 @@
"mustache": "^4.2.0",
"qs": "^6.11.0",
"socks-proxy-agent": "^8.0.2",
"xmlbuilder": "^15.1.1",
"yargs": "^17.6.2"
}
}

View File

@ -5,6 +5,7 @@ const { forOwn } = require('lodash');
const { exists, isFile, isDirectory } = require('../utils/filesystem');
const { runSingleRequest } = require('../runner/run-single-request');
const { bruToEnvJson, getEnvVars } = require('../utils/bru');
const makeJUnitOutput = require('../reporters/junit');
const { rpad } = require('../utils/common');
const { bruToJson, getOptions, collectionBruToJson } = require('../utils/bru');
const { dotenvToJson } = require('@usebruno/lang');
@ -186,7 +187,13 @@ const builder = async (yargs) => {
})
.option('output', {
alias: 'o',
describe: 'Path to write JSON results to',
describe: 'Path to write file results to',
type: 'string'
})
.option('format', {
alias: 'f',
describe: 'Format for the file results',
default: 'json',
type: 'string'
})
.option('insecure', {
@ -204,12 +211,16 @@ const builder = async (yargs) => {
.example(
'$0 run request.bru --output results.json',
'Run a request and write the results to results.json in the current directory'
)
.example(
'$0 run request.bru --output results.xml --format junit',
'Run a request and write the results to results.xml in junit format in the current directory'
);
};
const handler = async function (argv) {
try {
let { filename, cacert, env, envVar, insecure, r: recursive, output: outputPath } = argv;
let { filename, cacert, env, envVar, insecure, r: recursive, output: outputPath, format } = argv;
const collectionPath = process.cwd();
// todo
@ -297,6 +308,11 @@ const handler = async function (argv) {
}
}
if (['json', 'junit'].indexOf(format) === -1) {
console.error(chalk.red(`Format must be one of "json" or "junit"`));
return;
}
// load .env file at root of collection if it exists
const dotEnvPath = path.join(collectionPath, '.env');
const dotEnvExists = await exists(dotEnvPath);
@ -360,6 +376,8 @@ const handler = async function (argv) {
while (currentRequestIndex < bruJsons.length) {
const iter = bruJsons[currentRequestIndex];
const { bruFilepath, bruJson } = iter;
const start = process.hrtime();
const result = await runSingleRequest(
bruFilepath,
bruJson,
@ -371,7 +389,11 @@ const handler = async function (argv) {
collectionRoot
);
results.push(result);
results.push({
...result,
runtime: process.hrtime(start)[0] + process.hrtime(start)[1] / 1e9,
suitename: bruFilepath.replace('.bru', '')
});
// determine next request
const nextRequestName = result?.nextRequestName;
@ -413,7 +435,12 @@ const handler = async function (argv) {
results
};
fs.writeFileSync(outputPath, JSON.stringify(outputJson, null, 2));
if (format === 'json') {
fs.writeFileSync(outputPath, JSON.stringify(outputJson, null, 2));
} else if (format === 'junit') {
makeJUnitOutput(results, outputPath);
}
console.log(chalk.dim(chalk.grey(`Wrote results to ${outputPath}`)));
}

View File

@ -0,0 +1,85 @@
const os = require('os');
const fs = require('fs');
const xmlbuilder = require('xmlbuilder');
const makeJUnitOutput = async (results, outputPath) => {
const output = {
testsuites: {
testsuite: []
}
};
results.forEach((result) => {
const assertionTestCount = result.assertionResults ? result.assertionResults.length : 0;
const testCount = result.testResults ? result.testResults.length : 0;
const totalTests = assertionTestCount + testCount;
const suite = {
'@name': result.suitename,
'@errors': 0,
'@failures': 0,
'@skipped': 0,
'@tests': totalTests,
'@timestamp': new Date().toISOString().split('Z')[0],
'@hostname': os.hostname(),
'@time': result.runtime.toFixed(3),
testcase: []
};
result.assertionResults &&
result.assertionResults.forEach((assertion) => {
const testcase = {
'@name': `${assertion.lhsExpr} ${assertion.rhsExpr}`,
'@status': assertion.status,
'@classname': result.request.url,
'@time': (result.runtime / totalTests).toFixed(3)
};
if (assertion.status === 'fail') {
suite['@failures']++;
testcase.failure = [{ '@type': 'failure', '@message': assertion.error }];
}
suite.testcase.push(testcase);
});
result.testResults &&
result.testResults.forEach((test) => {
const testcase = {
'@name': test.description,
'@status': test.status,
'@classname': result.request.url,
'@time': (result.runtime / totalTests).toFixed(3)
};
if (test.status === 'fail') {
suite['@failures']++;
testcase.failure = [{ '@type': 'failure', '@message': test.error }];
}
suite.testcase.push(testcase);
});
if (result.error) {
suite['@errors'] = 1;
suite['@tests'] = 1;
suite.testcase = [
{
'@name': 'Test suite has no errors',
'@status': 'fail',
'@classname': result.request.url,
'@time': result.runtime.toFixed(3),
error: [{ '@type': 'error', '@message': result.error }]
}
];
}
output.testsuites.testsuite.push(suite);
});
fs.writeFileSync(outputPath, xmlbuilder.create(output).end({ pretty: true }));
};
module.exports = makeJUnitOutput;

View File

@ -0,0 +1,135 @@
const { describe, it, expect } = require('@jest/globals');
const xmlbuilder = require('xmlbuilder');
const fs = require('fs');
const makeJUnitOutput = require('../../src/reporters/junit');
describe('makeJUnitOutput', () => {
let createStub = jest.fn();
beforeEach(() => {
jest.spyOn(xmlbuilder, 'create').mockImplementation(() => {
return { end: createStub };
});
jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {});
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should produce a junit spec object for serialization', () => {
const results = [
{
description: 'description provided',
suitename: 'Tests/Suite A',
request: {
method: 'GET',
url: 'https://ima.test'
},
assertionResults: [
{
lhsExpr: 'res.status',
rhsExpr: 'eq 200',
status: 'pass'
},
{
lhsExpr: 'res.status',
rhsExpr: 'neq 200',
status: 'fail',
error: 'expected 200 to not equal 200'
}
],
runtime: 1.2345678
},
{
request: {
method: 'GET',
url: 'https://imanother.test'
},
suitename: 'Tests/Suite B',
testResults: [
{
lhsExpr: 'res.status',
rhsExpr: 'eq 200',
description: 'A test that passes',
status: 'pass'
},
{
description: 'A test that fails',
status: 'fail',
error: 'expected 200 to not equal 200',
status: 'fail'
}
],
runtime: 2.3456789
}
];
makeJUnitOutput(results, '/tmp/testfile.xml');
expect(createStub).toBeCalled;
const junit = xmlbuilder.create.mock.calls[0][0];
expect(junit.testsuites).toBeDefined;
expect(junit.testsuites.testsuite.length).toBe(2);
expect(junit.testsuites.testsuite[0].testcase.length).toBe(2);
expect(junit.testsuites.testsuite[1].testcase.length).toBe(2);
expect(junit.testsuites.testsuite[0]['@name']).toBe('Tests/Suite A');
expect(junit.testsuites.testsuite[1]['@name']).toBe('Tests/Suite B');
expect(junit.testsuites.testsuite[0]['@tests']).toBe(2);
expect(junit.testsuites.testsuite[1]['@tests']).toBe(2);
const testcase = junit.testsuites.testsuite[0].testcase[0];
expect(testcase['@name']).toBe('res.status eq 200');
expect(testcase['@status']).toBe('pass');
const failcase = junit.testsuites.testsuite[0].testcase[1];
expect(failcase['@name']).toBe('res.status neq 200');
expect(failcase.failure).toBeDefined;
expect(failcase.failure[0]['@type']).toBe('failure');
});
it('should handle request errors', () => {
const results = [
{
description: 'description provided',
suitename: 'Tests/Suite A',
request: {
method: 'GET',
url: 'https://ima.test'
},
assertionResults: [
{
lhsExpr: 'res.status',
rhsExpr: 'eq 200',
status: 'fail'
}
],
runtime: 1.2345678,
error: 'timeout of 2000ms exceeded'
}
];
makeJUnitOutput(results, '/tmp/testfile.xml');
const junit = xmlbuilder.create.mock.calls[0][0];
expect(createStub).toBeCalled;
expect(junit.testsuites).toBeDefined;
expect(junit.testsuites.testsuite.length).toBe(1);
expect(junit.testsuites.testsuite[0].testcase.length).toBe(1);
const failcase = junit.testsuites.testsuite[0].testcase[0];
expect(failcase['@name']).toBe('Test suite has no errors');
expect(failcase.error).toBeDefined;
expect(failcase.error[0]['@type']).toBe('error');
expect(failcase.error[0]['@message']).toBe('timeout of 2000ms exceeded');
});
});