diff --git a/src/chaos-experiments/models/chaosExperimentsManager.js b/src/chaos-experiments/models/chaosExperimentsManager.js index 6ef808bd4..a619f743c 100644 --- a/src/chaos-experiments/models/chaosExperimentsManager.js +++ b/src/chaos-experiments/models/chaosExperimentsManager.js @@ -92,5 +92,9 @@ module.exports.runChaosExperiment = async (kubernetesChaosConfig, jobExperimentI }; module.exports.getFutureJobExperiments = async function (timestamp, contextId) { - return databaseConnector.getFutureJobExperiments(contextId); + return databaseConnector.getFutureJobExperiments(timestamp, contextId); +}; + +module.exports.getChaosJobExperimentsByJobId = async function (jobId, contextId) { + return databaseConnector.getChaosJobExperimentsByJobId(jobId, contextId); }; diff --git a/src/chaos-experiments/models/database/databaseConnector.js b/src/chaos-experiments/models/database/databaseConnector.js index 04783f22f..3702c2862 100644 --- a/src/chaos-experiments/models/database/databaseConnector.js +++ b/src/chaos-experiments/models/database/databaseConnector.js @@ -9,7 +9,7 @@ module.exports = { getChaosExperimentsByIds, deleteChaosExperiment, insertChaosJobExperiment, - getChaosJobExperimentById, + getChaosJobExperimentsByJobId, getChaosJobExperimentByJobId, getFutureJobExperiments, setChaosJobExperimentTriggered, @@ -57,16 +57,16 @@ async function insertChaosJobExperiment(jobExperimentId, jobId, experimentId, st return databaseConnector.insertChaosJobExperiment(jobExperimentId, jobId, experimentId, startTime, endTime, contextId); } -async function getChaosJobExperimentById(jobExperimentId, contextId) { - return databaseConnector.getChaosJobExperimentById(jobExperimentId, contextId); +async function getChaosJobExperimentsByJobId(jobExperimentId, contextId) { + return databaseConnector.getChaosJobExperimentsByJobId(jobExperimentId, contextId); } async function getChaosJobExperimentByJobId(jobId, contextId) { return databaseConnector.getChaosJobExperimentById(jobId, contextId); } -async function getFutureJobExperiments(contextId) { - return databaseConnector.getFutureJobExperiments(contextId); +async function getFutureJobExperiments(timestamp, contextId) { + return databaseConnector.getFutureJobExperiments(timestamp, contextId); } async function setChaosJobExperimentTriggered(jobExperimentId, isTriggered, contextId) { diff --git a/src/chaos-experiments/models/database/sequelize/sequelizeConnector.js b/src/chaos-experiments/models/database/sequelize/sequelizeConnector.js index b556c01c5..ae11fa729 100644 --- a/src/chaos-experiments/models/database/sequelize/sequelizeConnector.js +++ b/src/chaos-experiments/models/database/sequelize/sequelizeConnector.js @@ -17,7 +17,7 @@ module.exports = { updateChaosExperiment, insertChaosJobExperiment, getChaosJobExperimentById, - getChaosJobExperimentByJobId, + getChaosJobExperimentsByJobId, getFutureJobExperiments, setChaosJobExperimentTriggered }; @@ -70,11 +70,10 @@ async function getChaosExperimentById(experimentId, contextId) { options.where.context_id = contextId; } - let chaosExperiment = await _getChaosExperiment(options); + const chaosExperiment = await _getChaosExperiment(options); if (chaosExperiment) { - chaosExperiment = chaosExperiment.get(); + return chaosExperiment.get(); } - return chaosExperiment; } async function getChaosExperimentsByIds(experimentIds, exclude, contextId) { @@ -152,14 +151,13 @@ async function getChaosJobExperimentById(jobExperimentId, contextId) { options.where.context_id = contextId; } - let chaosExperiment = await _getChaosJobExperiment(options); - if (chaosExperiment) { - chaosExperiment = chaosExperiment.get(); + const chaosJobExperiment = await _getChaosJobExperiment(options); + if (chaosJobExperiment) { + return chaosJobExperiment.get(); } - return chaosExperiment; } -async function getChaosJobExperimentByJobId(jobId, contextId) { +async function getChaosJobExperimentsByJobId(jobId, contextId) { const options = { where: { job_id: jobId } }; @@ -168,11 +166,9 @@ async function getChaosJobExperimentByJobId(jobId, contextId) { options.where.context_id = contextId; } - let chaosExperiment = await _getChaosJobExperiment(options); - if (chaosExperiment) { - chaosExperiment = chaosExperiment.get(); - } - return chaosExperiment; + const chaosJobExperimentModel = client.model(CHAOS_JOB_EXPERIMENTS_TABLE_NAME); + const allChaosJobExperiments = await chaosJobExperimentModel.findAll(options); + return allChaosJobExperiments; } async function getFutureJobExperiments(timestamp, contextId) { diff --git a/src/reports/models/aggregateReportManager.js b/src/reports/models/aggregateReportManager.js index ccc7185f9..71030fce3 100644 --- a/src/reports/models/aggregateReportManager.js +++ b/src/reports/models/aggregateReportManager.js @@ -5,6 +5,7 @@ const math = require('mathjs'); const logger = require('../../common/logger'); const databaseConnector = require('./databaseConnector'); +const chaosExperimentsManager = require('../../chaos-experiments/models/chaosExperimentsManager'); const constants = require('../utils/constants'); const STATS_INTERVAL = 30; @@ -15,6 +16,7 @@ module.exports = { async function aggregateReport(report) { let stats = await databaseConnector.getStats(report.test_id, report.report_id); + const experiments = await getChaosExperimentsByJobId(report.job_id); if (stats.length === 0) { const errorMessage = `Can not generate aggregate report as there are no statistics yet for testId: ${report.test_id} and reportId: ${report.report_id}`; @@ -35,6 +37,7 @@ async function aggregateReport(report) { reportInput.revision_id = report.revision_id; reportInput.score = report.score; reportInput.benchmark_weights_data = report.benchmark_weights_data; + reportInput.experiments = experiments; reportInput.notes = report.notes; reportInput.status = mapReportStatus(report.status); @@ -63,6 +66,28 @@ async function aggregateReport(report) { return reportInput; } +async function getChaosExperimentsByJobId(jobId) { + const chaosJobExperiments = await chaosExperimentsManager.getChaosJobExperimentsByJobId(jobId); + if (!chaosJobExperiments) { + return; + } + const uniqueExperimentIds = [...new Set(chaosJobExperiments.map(jobExperiment => jobExperiment.experiment_id))]; + const chaosExperiments = await chaosExperimentsManager.getChaosExperimentsByIds(uniqueExperimentIds); + const mappedChaosJobExperiments = chaosJobExperiments.map((jobExperiment) => { + const chaosExperiment = chaosExperiments.find((experiment) => experiment.id === jobExperiment.experiment_id && jobExperiment.is_triggered); + if (chaosExperiment) { + return { + kind: chaosExperiment.kubeObject.kind, + name: chaosExperiment.name, + id: chaosExperiment.id, + start_time: jobExperiment.start_time, + end_time: jobExperiment.end_time + }; + } + }); + return mappedChaosJobExperiments; +} + function createAggregateManually(listOfStats) { const requestMedians = [], requestMaxs = [], requestMins = [], scenario95 = [], scenario99 = [], request95 = [], request99 = [], scenarioMins = [], scenarioMaxs = [], scenarioMedians = []; diff --git a/tests/unit-tests/chaos-experiments/sequelize/sequelizeConnector-test.js b/tests/unit-tests/chaos-experiments/sequelize/sequelizeConnector-test.js index c1c356eaa..c8bedec8b 100644 --- a/tests/unit-tests/chaos-experiments/sequelize/sequelizeConnector-test.js +++ b/tests/unit-tests/chaos-experiments/sequelize/sequelizeConnector-test.js @@ -140,7 +140,7 @@ describe('Sequelize client tests', function () { should(sequelizeGetStub.args[0][0]).containDeep({ where: { name: experimentName } }); }); }); - describe('Get ChaosExperimentsById', () => { + describe('Get ChaosExperimentsByIds', () => { it('Validate sequelize passed arguments', async () => { sequelizeGetStub.returns([experiment]); const experimentIds = ['1234', '4321']; @@ -226,15 +226,30 @@ describe('Sequelize client tests', function () { should(sequelizeGetStub.args[0][0]).containDeep({ where: { id: jobExperimentId } }); }); }); - describe('getChaosJobExperimentByJobId', function() { + describe('getChaosJobExperimentsByJobId', function() { it('Validate sequelize passed arguments', async () => { sequelizeGetStub.returns([jobExperiment]); const experimentJobId = experiment.job_id; - await sequelizeConnector.getChaosJobExperimentByJobId(experimentJobId); + await sequelizeConnector.getChaosJobExperimentsByJobId(experimentJobId); should(sequelizeGetStub.calledOnce).eql(true); should(sequelizeGetStub.args[0][0]).containDeep({ where: { job_id: experimentJobId } }); }); }); + describe('getFutureJobExperiments', function() { + it('Validate sequelize passed arguments', async () => { + sequelizeGetStub.returns([jobExperiment]); + const timestamp = Date.now(); + await sequelizeConnector.getFutureJobExperiments(timestamp, 'contextId'); + should(sequelizeGetStub.calledOnce).eql(true); + should(sequelizeGetStub.args[0][0]).deepEqual({ + where: { + is_triggered: false, + start_time: {}, + context_id: 'contextId' + } + }); + }); + }); }); describe('Set job experiment is triggered', () => { diff --git a/tests/unit-tests/reporter/models/finalReportGenerator-test.js b/tests/unit-tests/reporter/models/finalReportGenerator-test.js index 6114c9e9e..dea810cda 100644 --- a/tests/unit-tests/reporter/models/finalReportGenerator-test.js +++ b/tests/unit-tests/reporter/models/finalReportGenerator-test.js @@ -6,8 +6,11 @@ const rewire = require('rewire'); const logger = require('../../../../src/common/logger'); const aggregateReportGenerator = rewire('../../../../src/reports/models/aggregateReportGenerator'); const aggregateReportManager = rewire('../../../../src/reports/models/aggregateReportManager'); +const chaosExperimentsManager = require('../../../../src/chaos-experiments/models/chaosExperimentsManager'); const databaseConnector = require('../../../../src/reports/models/databaseConnector'); const reportsManager = require('../../../../src/reports/models/reportsManager'); +const configHandler = require('../../../../src/configManager/models/configHandler'); +const consts = require('../../../../src/common/consts'); const REPORT = { test_id: 'test_id', @@ -17,17 +20,28 @@ const REPORT = { test_name: 'some_test_name', webhooks: ['http://www.zooz.com'], arrival_rate: 100, + job_id: 'job_id', duration: 10, environment: 'test' }; describe('Artillery report generator test', () => { - let sandbox, databaseConnectorGetStatsStub, loggerErrorStub, loggerWarnStub, reportsManagerGetReportStub; + let sandbox, + configHandlerStub, + databaseConnectorGetStatsStub, + getJobExperimentsByJobIdStub, + getChaosExperimentsByIdsStub, + loggerErrorStub, + loggerWarnStub, + reportsManagerGetReportStub; before(() => { sandbox = sinon.sandbox.create(); + configHandlerStub = sandbox.stub(configHandler, 'getConfigValue'); databaseConnectorGetStatsStub = sandbox.stub(databaseConnector, 'getStats'); reportsManagerGetReportStub = sandbox.stub(reportsManager, 'getReport'); + getJobExperimentsByJobIdStub = sandbox.stub(chaosExperimentsManager, 'getChaosJobExperimentsByJobId'); + getChaosExperimentsByIdsStub = sandbox.stub(chaosExperimentsManager, 'getChaosExperimentsByIds'); loggerErrorStub = sandbox.stub(logger, 'error'); loggerWarnStub = sandbox.stub(logger, 'warn'); }); @@ -48,6 +62,7 @@ describe('Artillery report generator test', () => { it('create aggregate report when there is only intermediate rows', async () => { databaseConnectorGetStatsStub.resolves(SINGLE_RUNNER_INTERMEDIATE_ROWS); + getJobExperimentsByJobIdStub.resolves([]); const reportOutput = await aggregateReportGenerator.createAggregateReport(REPORT.test_id, REPORT.report_id); should(reportOutput.parallelism).eql(1); @@ -57,6 +72,7 @@ describe('Artillery report generator test', () => { const statsWithUnknownData = JSON.parse(JSON.stringify(SINGLE_RUNNER_INTERMEDIATE_ROWS)); statsWithUnknownData.push({ phase_status: 'some_unknown_phase', data: JSON.stringify({}) }); databaseConnectorGetStatsStub.resolves(statsWithUnknownData); + getJobExperimentsByJobIdStub.resolves([]); const reportOutput = await aggregateReportGenerator.createAggregateReport(REPORT.test_id, REPORT.report_id); should(reportOutput.parallelism).eql(1); @@ -72,6 +88,39 @@ describe('Artillery report generator test', () => { loggerWarnStub.callCount.should.eql(1); }); + + it('create final report successfully with chaos experiments', async function() { + configHandlerStub.withArgs(consts.CONFIG.JOB_PLATFORM).resolves('KUBERNETES'); + const statsWithUnknownData = JSON.parse(JSON.stringify(SINGLE_RUNNER_INTERMEDIATE_ROWS)); + statsWithUnknownData.push({ phase_status: 'intermediate', data: 'unsupported data type' }); + databaseConnectorGetStatsStub.resolves(statsWithUnknownData); + getJobExperimentsByJobIdStub.resolves(JOB_EXPERIMENTS_ROWS); + getChaosExperimentsByIdsStub.resolves(CHAOS_EXPERIMENTS_ROWS); + const reportOutput = await aggregateReportGenerator.createAggregateReport(REPORT.test_id, REPORT.report_id); + should(reportOutput.experiments).deepEqual([ + { + kind: CHAOS_EXPERIMENTS_ROWS[0].kubeObject.kind, + name: CHAOS_EXPERIMENTS_ROWS[0].name, + id: JOB_EXPERIMENTS_ROWS[0].experiment_id, + start_time: JOB_EXPERIMENTS_ROWS[0].start_time, + end_time: JOB_EXPERIMENTS_ROWS[0].end_time + }, + { + kind: CHAOS_EXPERIMENTS_ROWS[1].kubeObject.kind, + name: CHAOS_EXPERIMENTS_ROWS[1].name, + id: JOB_EXPERIMENTS_ROWS[1].experiment_id, + start_time: JOB_EXPERIMENTS_ROWS[1].start_time, + end_time: JOB_EXPERIMENTS_ROWS[1].end_time + }, + { + kind: CHAOS_EXPERIMENTS_ROWS[2].kubeObject.kind, + name: CHAOS_EXPERIMENTS_ROWS[2].name, + id: JOB_EXPERIMENTS_ROWS[2].experiment_id, + start_time: JOB_EXPERIMENTS_ROWS[2].start_time, + end_time: JOB_EXPERIMENTS_ROWS[2].end_time + } + ]); + }); }); describe('Happy flows - With parallelism', function () { @@ -86,6 +135,7 @@ describe('Artillery report generator test', () => { const firstStatsTimestamp = JSON.parse(PARALLEL_INTERMEDIATE_ROWS[0].data).timestamp; reportsManagerGetReportStub.resolves(REPORT); + getJobExperimentsByJobIdStub.resolves([]); REPORT.start_time = new Date(new Date(firstStatsTimestamp).getTime() - (STATS_INTERVAL * 1000)); databaseConnectorGetStatsStub.resolves(PARALLEL_INTERMEDIATE_ROWS); const reportOutput = await aggregateReportGenerator.createAggregateReport(REPORT.test_id, REPORT.report_id); @@ -233,9 +283,42 @@ describe('Artillery report generator test', () => { testShouldFail.should.eql(false, 'Test action was supposed to get exception'); }); + + it('create final report fails when get experiments returns error on get job experiments', async () => { + databaseConnectorGetStatsStub.resolves(SINGLE_RUNNER_INTERMEDIATE_ROWS); + reportsManagerGetReportStub.rejects(new Error('Database failure')); + + let testShouldFail = true; + try { + await aggregateReportGenerator.createAggregateReport('testId', 'reportId'); + } catch (error) { + testShouldFail = false; + error.message.should.eql('Database failure'); + } + + testShouldFail.should.eql(false, 'Test action was supposed to get exception'); + }); + + it('create final report fails when get experiments returns error on get chaos experiments', async () => { + databaseConnectorGetStatsStub.resolves(SINGLE_RUNNER_INTERMEDIATE_ROWS); + getJobExperimentsByJobIdStub.resolves(JOB_EXPERIMENTS_ROWS); + getChaosExperimentsByIdsStub.rejects(new Error('Database failure')); + + let testShouldFail = true; + try { + await aggregateReportGenerator.createAggregateReport('testId', 'reportId'); + } catch (error) { + testShouldFail = false; + error.message.should.eql('Database failure'); + } + + testShouldFail.should.eql(false, 'Test action was supposed to get exception'); + }); }); }); +const timestamp = Date.now(); + const SINGLE_RUNNER_INTERMEDIATE_ROWS = [{ test_id: 'cb7d7862-55c2-4a9b-bcec-d41d54101836', report_id: 'b6489011-2073-4998-91cc-fd62f8b927f7', @@ -337,3 +420,41 @@ const PARALLEL_INTERMEDIATE_ROWS = [ data: '{"timestamp":"2019-03-10T17:24:33.043Z","scenariosCreated":300,"scenariosCompleted":300,"requestsCompleted":300,"latency":{"min":59.5,"max":98.3,"median":61.3,"p95":72.9,"p99":84},"rps":{"count":300,"mean":20},"scenarioDuration":{"min":60,"max":98.9,"median":61.9,"p95":73.5,"p99":84.5},"scenarioCounts":{"Get response code 200":300},"errors":{},"codes":{"200":300},"matches":0,"customStats":{},"counters":{},"concurrency":1,"pendingRequests":1,"scenariosAvoided":0}' } ]; + +const JOB_EXPERIMENTS_ROWS = [{ + job_id: REPORT.job_id, + experiment_id: '1234-abc-5678', + start_time: timestamp, + end_time: timestamp + 100, + is_triggered: true +}, +{ + job_id: REPORT.job_id, + experiment_id: 'abcd-1234-efgh', + start_time: timestamp, + end_time: timestamp + 200, + is_triggered: true +}, +{ + job_id: REPORT.job_id, + experiment_id: '4321-abc-5678', + start_time: timestamp, + end_time: timestamp + 300, + is_triggered: true +}]; + +const CHAOS_EXPERIMENTS_ROWS = [{ + id: '1234-abc-5678', + name: 'first-experiment', + kubeObject: { kind: 'PodChaos' } +}, +{ + id: 'abcd-1234-efgh', + name: 'second-experiment', + kubeObject: { kind: 'DNSChaos' } +}, +{ + id: '4321-abc-5678', + name: 'third-experiment', + kubeObject: { kind: 'IOChaos' } +}]; diff --git a/ui/src/features/components/JobForm/index.js b/ui/src/features/components/JobForm/index.js index b048de530..ae8903b40 100644 --- a/ui/src/features/components/JobForm/index.js +++ b/ui/src/features/components/JobForm/index.js @@ -212,7 +212,7 @@ class Form extends React.Component { experiment name: {experiment.experiment_name} start after: - {experiment.start_after / ONE_MIN_MS} seconds + {experiment.start_after / ONE_MIN_MS} minutes