Skip to content

Commit

Permalink
[FEATURE] Add fileExport capability (#352)
Browse files Browse the repository at this point in the history
- Some tools, such as Support Assistant, provide extra reports directly from tests
using a window._$files variable; this is no longer read anywhere and such reports are
currently unsupported
- See https://github.com/SAP/openui5/blob/master/src/sap.ui.core/src/sap/ui/core/support/RuleEngineOpaExtension.js#L243

Co-authored-by: Lars Kissel <lars.kissel@sap.com>
  • Loading branch information
sap-sebelao and larskissel committed Dec 8, 2021
1 parent 1a19020 commit 56aac75
Show file tree
Hide file tree
Showing 26 changed files with 893 additions and 5 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,4 @@ deploy_key

# Custom directories
dist/
test/integration/*/karma-ui5-reports*
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,38 @@ ui5: {
}
```

### fileExport
Type: `boolean` or `object`
Default: `false`

Configures whether report files provided by tools like UI5 Support Assistant are exported to the file system.
Optionally, an output directory can be set to specify the export path.

Example `boolean`:
```js
ui5: {
fileExport: true
}
```

Example `object`:
```js
ui5: {
fileExport: {
outputDir: "directory/to/export/files"
}
}
```

Projects can also add report files by themselves by setting or enhancing the global `window._$files` array in the executed source code in the following way:
```js
window._$files = window._$files || [];
window._$files.push({
name: "file_name.txt",
content: "file content"
});
```

## API

### helper
Expand Down
56 changes: 55 additions & 1 deletion lib/client/browser.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,53 @@
const istanbulLibCoverage = require("istanbul-lib-coverage");
require("./discovery.js");

const patternTestResources = /(?:^|[^?#]*\/)(?:test-)?(?:resources|webapp)\/(.*)/;
const patternGenericTeststarter = /([^?#]+)\.qunit\.html\?testsuite=test-resources\/((?:[^/]+\/)*testsuite(?:[.a-z0-9]+)?\.qunit)&test=([^&]+)(?:&|$)/;
const patternSpecificTeststarter = /([^?#]+)\.qunit\.html\?test=([^&]+)(?:&|$)/;
const patternAnyTest = /([^?#]*)[?#](?:.*)/;

function getTestPageName(qunitHtmlFile) {
let matches;
let result;

matches = qunitHtmlFile.match(patternTestResources);
if (matches) {
qunitHtmlFile = matches[1];
}

matches = qunitHtmlFile.match(patternGenericTeststarter);
if (matches) {
result = matches[2] + "--" + matches[3];
}

if (!result) {
matches = qunitHtmlFile.match(patternSpecificTeststarter);
if (matches) {
result = matches[1] + "--" + matches[2];
}
}

if (!result) {
matches = qunitHtmlFile.match(patternAnyTest);
if (matches) {
result = matches[1];
}
}

if (!result) {
result = qunitHtmlFile;
}

result = result.replace(/\.qunit\.html/g, "");
result = result.replace(/\.qunit\./g, "--");
result = result.replace(/\.qunit--/g, "--");

return result;
}

(function(window) {
const karma = window.__karma__;
const exportFiles = [];

function reportSetupFailure(description, error) {
karma.info({total: 1});
Expand Down Expand Up @@ -166,6 +211,14 @@ require("./discovery.js");
// Merge test page coverage into global coverage object
if (!accessError) {
mergeCoverage(testWindow.contentWindow.__coverage__);

if (config.fileExport) {
const testPageName = getTestPageName(qunitHtmlFile);
(testWindow.contentWindow._$files || []).forEach((file) => {
file.name = `TEST-${testPageName}-FILE-${file.name}`;
exportFiles.push(file);
});
}
}

// Run next test or trigger completion
Expand All @@ -177,7 +230,8 @@ require("./discovery.js");
// Also merge coverage results from karma window
mergeCoverage(window.__coverage__);
karma.complete({
coverage: coverageMap ? coverageMap.toJSON() : undefined
coverage: coverageMap ? coverageMap.toJSON() : undefined,
exportFiles: config.fileExport ? exportFiles : undefined
});
}
}
Expand Down
13 changes: 12 additions & 1 deletion lib/client/sap-ui-config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
(function(window) {
const config = window.__karma__.config;
const karma = window.__karma__;
const config = karma.config;
const ui5config = (config && config.ui5) || {};
const bootstrapConfig = ui5config.config || {};

window["sap-ui-config"] = bootstrapConfig;

if (ui5config.fileExport) {
const originalKarmaComplete = karma.complete.bind(karma);
karma.complete = function(result) {
if (window._$files) {
result.exportFiles = window._$files;
}
return originalKarmaComplete(result);
};
}
})(window);
29 changes: 29 additions & 0 deletions lib/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,35 @@ module.exports = function(config) {
});
};`,

invalidFileExportReporterUsage: () => `error 21:
The reporter "ui5--fileExport" should not be manually enabled as a karma reporter.
You can enable the FileExportReporter in the karma-ui5 settings.
module.exports = function(config) {
config.set({
ui5: {
fileExport: true
}
});
};
Optionally, an output directory can be set to specify the export path.
module.exports = function(config) {
config.set({
ui5: {
fileExport: {
outputDir: "directory/to/export/files"
}
}
});
};
`,

failure: () => "ui5.framework failed. See error message above"

}
Expand Down
120 changes: 120 additions & 0 deletions lib/fileExportReporter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
const fs = require("fs").promises;
const path = require("path");
const mkdirp = require("mkdirp");

const defaultPath = "./karma-ui5-reports";

function escapeFileName(fileName) {
fileName = fileName.replace(/[:*?"<>|]/g, "");
fileName = fileName.replace(/[\\/]/g, ".");
return fileName;
}

async function getUniqueFileName(exportDir, fileName) {
async function fileExists(_fileName) {
try {
await fs.access(path.join(exportDir, _fileName));
return true;
} catch (err) {
if (err.code === "ENOENT") {
return false;
}
throw err;
}
}

const fileExtension = path.extname(fileName);
const fileNameWithoutExtension = path.basename(fileName, fileExtension);
for (let index = 1; await fileExists(fileName); index++) {
fileName = `${fileNameWithoutExtension}_${index}${fileExtension}`;
}

return fileName;
}

const FileExportReporter = function(baseReporterDecorator, config, logger) {
let reporterInProcess = true;
let exitCode = 0;
let reporterCompleted = function() {};
const log = logger.create("reporter.ui5--fileExport");
const reporterConfig = config.ui5.fileExport;
const multiBrowsers = config.browsers && config.browsers.length > 1;
let outputDir = reporterConfig.outputDir;

if (!outputDir || typeof outputDir !== "string") {
outputDir = defaultPath;
}

outputDir = path.join(config.basePath, outputDir);

log.debug("outputDir is: " + outputDir);

baseReporterDecorator(this);

async function writeSingleFile(fileDir, fileName, content) {
await mkdirp(fileDir);
const uniqueFileName = await getUniqueFileName(fileDir, fileName);
const pathToWrite = path.join(fileDir, uniqueFileName);
if (!pathToWrite.startsWith(fileDir)) {
log.warn(`Invalid export file path: ${pathToWrite}\n\tMake sure the file path is in directory: ${fileDir}`);
return;
}
log.debug(`Writing file: ${pathToWrite}`);
try {
await fs.writeFile(pathToWrite, content);
log.info(`Saved file '${pathToWrite}'`);
} catch (err) {
log.warn("Failed to write file " + pathToWrite + "\n\t" + err.message);
}
}

this.onBrowserComplete = async function(browser, result) {
try {
log.debug("onBrowserComplete triggered.");
if (!result || result.error || result.disconnected) {
log.debug("skipped due to incomplete test run.");
return;
}

if (!result.exportFiles) {
log.debug("No export files provided");
return;
}

if (!Array.isArray(result.exportFiles)) {
log.warn("Export files must be given as an array");
return;
}

let exportPath = outputDir;
if (multiBrowsers) {
exportPath = path.join(exportPath, escapeFileName(browser.name));
}
for (const file of result.exportFiles) {
if (typeof file.name !== "string" || typeof file.content !== "string") {
log.warn("Invalid file object. \"name\" and \"content\" must be strings");
continue;
}

await writeSingleFile(exportPath, escapeFileName(file.name), file.content);
}
} catch (err) {
log.error("An unexpected error occured while exporting files\n\t" + err.message);
exitCode = 1;
}
reporterInProcess = false;
reporterCompleted();
};

this.onExit = function(done) {
if (reporterInProcess) {
reporterCompleted = () => done(exitCode);
} else {
done(exitCode);
}
};
};

FileExportReporter.$inject = ["baseReporterDecorator", "config", "logger"];

module.exports = FileExportReporter;
14 changes: 14 additions & 0 deletions lib/framework.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ class Framework {
this.config.middleware = config.middleware || [];
this.config.files = config.files || [];
this.config.beforeMiddleware = config.beforeMiddleware || [];
this.config.reporters = this.config.reporters || [];

if (!this.config.ui5.mode) {
this.config.ui5.mode = "html";
Expand Down Expand Up @@ -195,6 +196,17 @@ class Framework {
throw new Error(ErrorMessage.failure());
}

if (this.config.reporters.includes("ui5--fileExport")) {
this.logger.log("error", ErrorMessage.invalidFileExportReporterUsage());
throw new Error(ErrorMessage.failure());
}
if (this.config.ui5.fileExport === true || typeof this.config.ui5.fileExport === "object") {
this.config.reporters.push("ui5--fileExport");
if (this.config.ui5.fileExport === true) {
this.config.ui5.fileExport = {};
}
}

this.config.ui5.paths = this.config.ui5.paths || {
webapp: "webapp",
src: "src",
Expand Down Expand Up @@ -247,6 +259,8 @@ class Framework {
this.config.client.ui5.failOnEmptyTestPage = this.config.ui5.failOnEmptyTestPage;
// Pass configured urlParameters to client
this.config.client.ui5.urlParameters = this.config.ui5.urlParameters;
// Pass fileExport parameter to client
this.config.client.ui5.fileExport = this.config.reporters.includes("ui5--fileExport");


if (this.config.ui5.type === "application") {
Expand Down
4 changes: 3 additions & 1 deletion lib/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const {ErrorMessage} = require("./errors");
const Framework = require("./framework");
const FileExportReporter = require("./fileExportReporter");

async function init(config, logger) {
try {
Expand All @@ -24,5 +25,6 @@ getBeforeMiddleware.$inject = getMiddleware.$inject = ["config.ui5"];
module.exports = {
"framework:ui5": ["factory", init],
"middleware:ui5--beforeMiddleware": ["factory", getBeforeMiddleware],
"middleware:ui5--middleware": ["factory", getMiddleware]
"middleware:ui5--middleware": ["factory", getMiddleware],
"reporter:ui5--fileExport": ["type", FileExportReporter]
};
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@
"@ui5/server": "^2.4.0",
"express": "^4.17.1",
"http-proxy": "^1.18.1",
"js-yaml": "^4.1.0"
"js-yaml": "^4.1.0",
"mkdirp": "^1.0.4"
},
"devDependencies": {
"@babel/core": "^7.16.0",
Expand Down

0 comments on commit 56aac75

Please sign in to comment.