feat: export test run result (#220)

This commit is contained in:
kimatata
2025-05-06 20:31:47 +09:00
committed by GitHub
parent c2ef8d4c7b
commit 0faa5f0be2
19 changed files with 488 additions and 55 deletions

View File

@@ -106,11 +106,13 @@ app.use('/attachments', attachmentsDeleteRoute);
app.use('/attachments', attachmentsDownloadRoute);
// "/runs"
const runsDownloadRoute = require('./routes/runs/download')(sequelize);
const runsIndexRoute = require('./routes/runs/index')(sequelize);
const runsShowRoute = require('./routes/runs/show')(sequelize);
const runsNewRoute = require('./routes/runs/new')(sequelize);
const runsEditRoute = require('./routes/runs/edit')(sequelize);
const runDeleteRoute = require('./routes/runs/delete')(sequelize);
app.use('/runs', runsDownloadRoute);
app.use('/runs', runsIndexRoute);
app.use('/runs', runsShowRoute);
app.use('/runs', runsNewRoute);

View File

@@ -18,7 +18,8 @@
"papaparse": "^5.5.2",
"sequelize": "^6.37.5",
"sequelize-cli": "^6.6.2",
"sqlite3": "^5.1.7"
"sqlite3": "^5.1.7",
"xmlbuilder2": "^3.1.1"
}
},
"node_modules/@gar/promisify": {
@@ -219,6 +220,50 @@
"resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz",
"integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw=="
},
"node_modules/@oozcitak/dom": {
"version": "1.15.10",
"resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-1.15.10.tgz",
"integrity": "sha512-0JT29/LaxVgRcGKvHmSrUTEvZ8BXvZhGl2LASRUgHqDTC1M5g1pLmVv56IYNyt3bG2CUjDkc67wnyZC14pbQrQ==",
"dependencies": {
"@oozcitak/infra": "1.0.8",
"@oozcitak/url": "1.0.4",
"@oozcitak/util": "8.3.8"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/@oozcitak/infra": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-1.0.8.tgz",
"integrity": "sha512-JRAUc9VR6IGHOL7OGF+yrvs0LO8SlqGnPAMqyzOuFZPSZSXI7Xf2O9+awQPSMXgIWGtgUf/dA6Hs6X6ySEaWTg==",
"dependencies": {
"@oozcitak/util": "8.3.8"
},
"engines": {
"node": ">=6.0"
}
},
"node_modules/@oozcitak/url": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@oozcitak/url/-/url-1.0.4.tgz",
"integrity": "sha512-kDcD8y+y3FCSOvnBI6HJgl00viO/nGbQoCINmQ0h98OhnGITrWR3bOGfwYCthgcrV8AnTJz8MzslTQbC3SOAmw==",
"dependencies": {
"@oozcitak/infra": "1.0.8",
"@oozcitak/util": "8.3.8"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/@oozcitak/util": {
"version": "8.3.8",
"resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.3.8.tgz",
"integrity": "sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ==",
"engines": {
"node": ">=8.0"
}
},
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@@ -396,6 +441,19 @@
"node": ">= 6"
}
},
"node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/argparse/node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="
},
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
@@ -1087,6 +1145,18 @@
"resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz",
"integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ=="
},
"node_modules/esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
@@ -1772,6 +1842,18 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
"node_modules/js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"dependencies": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/jsbn": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
@@ -3436,6 +3518,20 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"node_modules/xmlbuilder2": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-3.1.1.tgz",
"integrity": "sha512-WCSfbfZnQDdLQLiMdGUQpMxxckeQ4oZNMNhLVkcekTu7xhD4tuUDyAPoY8CwXvBYE6LwBHd6QW2WZXlOWr1vCw==",
"dependencies": {
"@oozcitak/dom": "1.15.10",
"@oozcitak/infra": "1.0.8",
"@oozcitak/util": "8.3.8",
"js-yaml": "3.14.1"
},
"engines": {
"node": ">=12.0"
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
@@ -3637,6 +3733,38 @@
"resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz",
"integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw=="
},
"@oozcitak/dom": {
"version": "1.15.10",
"resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-1.15.10.tgz",
"integrity": "sha512-0JT29/LaxVgRcGKvHmSrUTEvZ8BXvZhGl2LASRUgHqDTC1M5g1pLmVv56IYNyt3bG2CUjDkc67wnyZC14pbQrQ==",
"requires": {
"@oozcitak/infra": "1.0.8",
"@oozcitak/url": "1.0.4",
"@oozcitak/util": "8.3.8"
}
},
"@oozcitak/infra": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-1.0.8.tgz",
"integrity": "sha512-JRAUc9VR6IGHOL7OGF+yrvs0LO8SlqGnPAMqyzOuFZPSZSXI7Xf2O9+awQPSMXgIWGtgUf/dA6Hs6X6ySEaWTg==",
"requires": {
"@oozcitak/util": "8.3.8"
}
},
"@oozcitak/url": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@oozcitak/url/-/url-1.0.4.tgz",
"integrity": "sha512-kDcD8y+y3FCSOvnBI6HJgl00viO/nGbQoCINmQ0h98OhnGITrWR3bOGfwYCthgcrV8AnTJz8MzslTQbC3SOAmw==",
"requires": {
"@oozcitak/infra": "1.0.8",
"@oozcitak/util": "8.3.8"
}
},
"@oozcitak/util": {
"version": "8.3.8",
"resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.3.8.tgz",
"integrity": "sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ=="
},
"@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@@ -3777,6 +3905,21 @@
}
}
},
"argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"requires": {
"sprintf-js": "~1.0.2"
},
"dependencies": {
"sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="
}
}
},
"array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
@@ -4323,6 +4466,11 @@
}
}
},
"esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
},
"etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
@@ -4826,6 +4974,15 @@
}
}
},
"js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"requires": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
}
},
"jsbn": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
@@ -6049,6 +6206,17 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"xmlbuilder2": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-3.1.1.tgz",
"integrity": "sha512-WCSfbfZnQDdLQLiMdGUQpMxxckeQ4oZNMNhLVkcekTu7xhD4tuUDyAPoY8CwXvBYE6LwBHd6QW2WZXlOWr1vCw==",
"requires": {
"@oozcitak/dom": "1.15.10",
"@oozcitak/infra": "1.0.8",
"@oozcitak/util": "8.3.8",
"js-yaml": "3.14.1"
}
},
"xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",

View File

@@ -21,6 +21,7 @@
"papaparse": "^5.5.2",
"sequelize": "^6.37.5",
"sequelize-cli": "^6.6.2",
"sqlite3": "^5.1.7"
"sqlite3": "^5.1.7",
"xmlbuilder2": "^3.1.1"
}
}

View File

@@ -30,14 +30,20 @@ module.exports = function (sequelize) {
return res.status(404).send('No cases found');
}
const csv = Papa.unparse(cases, {
quotes: true,
skipEmptyLines: true,
});
if (type === 'json') {
return res.json(cases);
} else if (type === 'csv') {
const csv = Papa.unparse(cases, {
quotes: true,
skipEmptyLines: true,
});
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
res.setHeader('Content-Disposition', `attachment; filename=cases_folder_${folderId}.csv`);
res.send(csv);
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
res.setHeader('Content-Disposition', `attachment; filename=cases_folder_${folderId}.csv`);
return res.send(csv);
}
return res.status(400).json({ error: 'Unsupported type. Use ?type=json or ?type=csv' });
} catch (error) {
console.error(error);
res.status(500).send('Internal Server Error');

View File

@@ -0,0 +1,126 @@
const express = require('express');
const router = express.Router();
const Papa = require('papaparse');
const { create } = require('xmlbuilder2');
const defineRun = require('../../models/runs');
const defineRunCase = require('../../models/runCases');
const defineCase = require('../../models/cases');
const defineFolder = require('../../models/folders');
module.exports = function (sequelize) {
const { DataTypes } = require('sequelize');
const { verifySignedIn } = require('../../middleware/auth')(sequelize);
const { verifyProjectVisibleFromRunId } = require('../../middleware/verifyVisible')(sequelize);
const Run = defineRun(sequelize, DataTypes);
const RunCase = defineRunCase(sequelize, DataTypes);
const Case = defineCase(sequelize, DataTypes);
const Folder = defineFolder(sequelize, DataTypes);
RunCase.belongsTo(Case, { foreignKey: 'caseId' });
router.get('/download/:runId', verifySignedIn, verifyProjectVisibleFromRunId, async (req, res) => {
const { runId } = req.params;
const { type } = req.query;
if (!runId) {
return res.status(400).json({ error: 'runId is required' });
}
try {
const run = await Run.findByPk(runId);
if (!run) {
return res.status(404).send('Run not found');
}
const runCases = await RunCase.findAll({
where: { runId },
include: [{ model: Case }],
});
if (type === 'xml') {
// JUnit xml valid status
const validStatuses = [1, 2, 4]; // 0: untested, 1: passed, 2 failed, 3: retest, 4: skipped
const filteredRunCases = runCases.filter((rc) => validStatuses.includes(rc.status));
// group cases by folder
const folderMap = new Map();
for (const rc of filteredRunCases) {
const folderId = rc.Case.folderId;
if (!folderMap.has(folderId)) {
folderMap.set(folderId, []);
}
folderMap.get(folderId).push(rc);
}
// Construct JUnit xml
const xml = create({ version: '1.0' });
const root = xml.ele('testsuites');
for (const [folderId, cases] of folderMap.entries()) {
let folderName = '';
const folder = await Folder.findByPk(folderId);
if (folder) {
folderName = folder.name;
}
const suite = root.ele('testsuite', {
name: folderName,
tests: cases.length,
failures: cases.filter((c) => c.status === 2).length,
skipped: cases.filter((c) => c.status === 4).length,
});
for (const rc of cases) {
const testCase = suite.ele('testcase', {
name: rc.Case.title,
classname: folderName,
time: '0',
});
if (rc.status === 2) {
testCase.ele('failure', { message: 'Test failed' }).txt('Test case failed.');
} else if (rc.status === 4) {
testCase.ele('skipped', { message: 'skipped' });
}
}
}
const xmlString = xml.end({ prettyPrint: true });
res.setHeader('Content-Type', 'application/xml');
res.setHeader('Content-Disposition', `attachment; filename=run_${runId}.xml`);
return res.send(xmlString);
} else if (type === 'json') {
return res.json(runCases);
} else if (type === 'csv') {
const records = runCases.map((rc) => ({
id: rc.Case.id,
title: rc.Case.title,
state: rc.Case.state,
priority: rc.Case.priority,
type: rc.Case.type,
automationStatus: rc.Case.automationStatus,
status: rc.status,
}));
const csv = Papa.unparse(records, {
quotes: true,
skipEmptyLines: true,
});
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', `attachment; filename=run_${runId}.csv`);
return res.send(csv);
}
return res.status(400).json({ error: 'Unsupported type. Use ?type=xml or ?type=json or ?type=csv' });
} catch (error) {
console.error(error);
res.status(500).send('Internal Server Error');
}
});
return router;
};