forked from extern/bruno
Merge pull request #1187 from awinder/feature/junit-reporter
Feature/junit reporter
This commit is contained in:
commit
e3865d4710
8
package-lock.json
generated
8
package-lock.json
generated
@ -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"
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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}`)));
|
||||
}
|
||||
|
||||
|
85
packages/bruno-cli/src/reporters/junit.js
Normal file
85
packages/bruno-cli/src/reporters/junit.js
Normal 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;
|
135
packages/bruno-cli/tests/reporters/junit.spec.js
Normal file
135
packages/bruno-cli/tests/reporters/junit.spec.js
Normal 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');
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user