Skip to content

Commit

Permalink
chore: harden code
Browse files Browse the repository at this point in the history
  • Loading branch information
rorlic committed Mar 19, 2024
1 parent 4986643 commit b9de95f
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 57 deletions.
76 changes: 46 additions & 30 deletions src/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const statusTemplate = '<!DOCTYPE html><html>\
const noTestsFoundTemplate = '<!DOCTYPE html><html>\
<head><title>Tests Overview</title><meta http-equiv="refresh" content="{{refresh}}"></head>\
<body>No tests found.</body></html>';

const overviewTemplate = '<!DOCTYPE html><html>\
<head><title>Test Runs Overview</title><meta http-equiv="refresh" content="{{refresh}}"></head>\
<body><h1>Test Runs</h1>\
Expand All @@ -39,7 +39,7 @@ const overviewTemplate = '<!DOCTYPE html><html>\
export class Controller {

private _testRunsById: TestRunDatabase = {};
private _testParser = new XMLParser({ stopNodes: ['jmeterTestPlan.hashTree.hashTree'], ignoreAttributes: false, attributeNamePrefix: '_'});
private _testParser = new XMLParser({ stopNodes: ['jmeterTestPlan.hashTree.hashTree'], ignoreAttributes: false, attributeNamePrefix: '_' });

private get _testRuns() {
return Object.values(this._testRunsById);
Expand Down Expand Up @@ -69,23 +69,24 @@ export class Controller {
run.status = TestRunStatus.cancelled;
}
});
}
}

private _writeMetadata(run: TestRun) {
const metadata = path.join(this._baseFolder, run.id, metadataName);
const fd = fs.openSync(metadata, 'w');
try {
const metadata = path.join(this._baseFolder, run.id, metadataName);
fs.writeFileSync(metadata, JSON.stringify(run), { encoding: 'utf8', flag: 'w', flush: true });
} catch (error) {
console.error('Failed to write metadata because: ', error);
fs.writeFileSync(fd, JSON.stringify(run), { encoding: 'utf8', flush: true });
} finally {
fs.closeSync(fd);
}
}

private _purgeTestRun(run: TestRun) {
const id = run.id;
if (run.status === TestRunStatus.running) {
return `Test ${id} is still running.`
}

const folder = path.join(this._baseFolder, id);
fs.rmSync(folder, { recursive: true, force: true });
this._deleteTestRun(run);
Expand All @@ -103,9 +104,14 @@ export class Controller {
folders.forEach(id => {
const metadata = path.join(this._baseFolder, id, metadataName);
if (fs.existsSync(metadata)) {
const content = fs.readFileSync(metadata, {encoding: 'utf8', flag: 'r'});
const run = JSON.parse(content);
this._upsertTestRun(run);
const fd = fs.openSync(metadata, 'r');
try {
const content = fs.readFileSync(fd, { encoding: 'utf8' });
const run = JSON.parse(content);
this._upsertTestRun(run);
} finally {
fs.closeSync(fd);
}
}
})
this._cancelRunningTests();
Expand All @@ -117,7 +123,7 @@ export class Controller {
}

public testRunExists(id: string): boolean {
return this._getTestRun(id) != undefined;
return this._getTestRun(id) != undefined;
}

public deleteTestRun(id: string): string {
Expand All @@ -130,16 +136,22 @@ export class Controller {
return this._testRuns.map(x => this._purgeTestRun(x));
}

private _read(fullPathName: string) {
const fd = fs.openSync(fullPathName, 'r');
try {
fs.readFileSync(fd, { encoding: 'utf8' });
} finally {
fs.closeSync(fd);
}
}

public async getTestRunStatus(id: string, limit: number = 1000) {
const run = this._getTestRun(id);
if (!run) throw new Error(`Test ${id} does not exist.`);

const output = limit
? await read(run.stdout, limit)
: fs.readFileSync(run.stdout, {encoding: 'utf8', flag: 'r'});

const output = limit ? await read(run.stdout, limit) : this._read(run.stdout);
const data = {
... run,
...run,
refresh: run.status === TestRunStatus.running ? this._refreshTimeInSeconds : false,
output: output,
};
Expand All @@ -152,29 +164,29 @@ export class Controller {
if (!testRuns.length) {
return Mustache.render(noTestsFoundTemplate, { refresh: this._refreshTimeInSeconds });
}

const runs = testRuns
.sort((f, s) => Date.parse(f.timestamp) - Date.parse(s.timestamp))
.map(test => {
switch (test.status) {
case TestRunStatus.done:
return { ... test, link: `${this._baseUrl}/${test.id}/results/`, text: 'results' };
return { ...test, link: `${this._baseUrl}/${test.id}/results/`, text: 'results' };
case TestRunStatus.cancelled:
return { ... test, link: `${this._baseUrl}/${test.id}`, text: 'output' };
return { ...test, link: `${this._baseUrl}/${test.id}`, text: 'output' };
case TestRunStatus.running:
return { ... test, link: `${this._baseUrl}/${test.id}`, text: 'status' };
return { ...test, link: `${this._baseUrl}/${test.id}`, text: 'status' };
default:
throw new Error(`Unknown test status: `, test.status);
}
});
const runsGroupedByCategory = _.groupBy(runs, (run: {category?: string}) => run.category);

const runsGroupedByCategory = _.groupBy(runs, (run: { category?: string }) => run.category);
const runsByCategoryAndName = _.keys(runsGroupedByCategory).map(x => {
const categoryGroupedByName = _.groupBy(runsGroupedByCategory[x] || [], (run: {name: string}) => run.name);
const categoryByName = _.keys(categoryGroupedByName).map(x => ({name: x, group: categoryGroupedByName[x] || []}));
return {category: x, group: categoryByName};
const categoryGroupedByName = _.groupBy(runsGroupedByCategory[x] || [], (run: { name: string }) => run.name);
const categoryByName = _.keys(categoryGroupedByName).map(x => ({ name: x, group: categoryGroupedByName[x] || [] }));
return { category: x, group: categoryByName };
});

const data = {
refresh: this._refreshTimeInSeconds,
tests: runsByCategoryAndName,
Expand All @@ -188,7 +200,7 @@ export class Controller {
fs.mkdirSync(folder);

await fsp.writeFile(path.join(folder, testName), body);

const parsed = this._testParser.parse(body) as JMeterTest;
const stdout = path.join(folder, stdoutName);
const timestamp = new Date().toISOString();
Expand All @@ -206,7 +218,11 @@ export class Controller {
const jmeter = cp.spawn('jmeter', ['-n', '-t', `${testName}`, '-l', `${reportName}`, '-e', '-o', `${resultsFolder}`], { cwd: folder });

jmeter.on('close', (code) => {
this._writeMetadata(this._upsertTestRun({ ...run, status: TestRunStatus.done, code: code }));
try {
this._writeMetadata(this._upsertTestRun({ ...run, status: TestRunStatus.done, code: code }));
} catch (error) {
console.error('Failed to write metadata because: ', error);
}
});

jmeter.stdout.pipe(fs.createWriteStream(stdout, { encoding: 'utf8', flags: 'w', flush: true, autoClose: true, emitClose: false }));
Expand Down
79 changes: 52 additions & 27 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,19 @@ server.register(fastifyStatic, {
});

server.addHook('onReady', async () => {
await controller.importTestRuns();
try {
await controller.importTestRuns();
} catch (error) {
console.error('Failed to import metadata because: ', error);
}
});

server.addHook('onClose', async () => {
await controller.exportTestRuns();
try {
await controller.exportTestRuns();
} catch (error) {
console.error('Failed to export metadata because: ', error);
}
})

server.addHook('onResponse', (request, reply, done) => {
Expand All @@ -66,11 +74,11 @@ server.addContentTypeParser(['application/xml'], { parseAs: 'string' }, function
})

server.post('/', { schema: { querystring: { category: { type: 'string' } } } }, async (request, reply) => {
try {
if (!checkApiKey(request, apiKeyRunTest)) {
return reply.status(401).send('');
}
if (!checkApiKey(request, apiKeyRunTest)) {
return reply.status(401).send('');
}

try {
if (controller.runningCount >= maxRunning) {
return reply.status(503)
.header('content-type', 'text/plain')
Expand All @@ -92,9 +100,13 @@ server.get('/', async (request, reply) => {
if (!checkApiKey(request, apiKeyCheckTest)) {
return reply.status(401).send('');
}

const body = controller.getTestRunsOverview();
reply.header('content-type', 'text/html').send(body);

try {
const body = controller.getTestRunsOverview();
reply.header('content-type', 'text/html').send(body);
} catch (error) {
reply.send({ msg: 'Cannot display test runs overview', error: error });
}
});

server.get('/:id', { schema: { querystring: { limit: { type: 'number' } } } }, async (request, reply) => {
Expand All @@ -104,11 +116,16 @@ server.get('/:id', { schema: { querystring: { limit: { type: 'number' } } } }, a

const parameters = request.query as { limit?: number };
const { id } = request.params as { id: string };
if(controller.testRunExists(id)) {
const body = await controller.getTestRunStatus(id, parameters.limit);
reply.header('content-type', 'text/html').send(body);
} else {
reply.status(404).send('');

try {
if (controller.testRunExists(id)) {
const body = await controller.getTestRunStatus(id, parameters.limit);
reply.header('content-type', 'text/html').send(body);
} else {
reply.status(404).send('');
}
} catch (error) {
reply.send({ msg: `Cannot display status for test run ${id}`, error: error });
}
});

Expand All @@ -117,12 +134,16 @@ server.delete('/', async (request, reply) => {
return reply.status(401).send('');
}

const responses = controller.deleteAllTestRuns().filter(x => !!x);
if (responses.length > 0) {
const response = responses.join('\n');
reply.status(405).send(response);
} else {
reply.send('All tests deleted');
try {
const responses = controller.deleteAllTestRuns().filter(x => !!x);
if (responses.length > 0) {
const response = responses.join('\n');
reply.status(405).send(response);
} else {
reply.send('All tests deleted');
}
} catch (error) {
reply.send({ msg: 'Cannot delete all tests', error: error });
}
});

Expand All @@ -133,15 +154,19 @@ server.delete('/:id', async (request, reply) => {

const { id } = request.params as { id: string };

if (controller.testRunExists(id)) {
const response = controller.deleteTestRun(id);
if (response) {
reply.status(405).send(response);
try {
if (controller.testRunExists(id)) {
const response = controller.deleteTestRun(id);
if (response) {
reply.status(405).send(response);
} else {
reply.send(`Test ${id} deleted`);
}
} else {
reply.send(`Test ${id} deleted`);
reply.status(404).send('');
}
} else {
reply.status(404).send('');
} catch (error) {
reply.send({ msg: `Cannot delete test ${id}`, error: error });
}
});

Expand Down

0 comments on commit b9de95f

Please sign in to comment.