simulator/src/testbench.js
/**
* This file contains all functions related the the testbench
* Contains the the testbench engine and UI modules
*/
import { scheduleBackup } from './data/backupCircuit';
import { changeClockEnable } from './sequential';
import { play } from './engine';
import Scope from './circuit';
import { showMessage, escapeHtml } from './utils';
/**
* @typedef {number} RunContext
*/
const CONTEXT = {
CONTEXT_SIMULATOR: 0,
CONTEXT_ASSIGNMENTS: 1,
};
const VALIDATION_ERRORS = {
NOTPRESENT: 0, // Element is not present in the circuit
WRONGBITWIDTH: 1, // Element is present but has incorrect bitwidth
DUPLICATE_ID_DATA: 2, // Duplicate identifiers in test data
DUPLICATE_ID_SCOPE: 3, // Duplicate identifiers in scope
NO_RST: 4, // Sequential circuit but no reset(RST) in scope
};
const TESTBENCH_CREATOR_PATH = '/testbench';
// Do we have any other function to do this?
// Utility function. Converts decimal number to binary string
function dec2bin(dec, bitWidth = undefined) {
if (dec === undefined) return 'X';
const bin = (dec >>> 0).toString(2);
if (!bitWidth) return bin;
return '0'.repeat(bitWidth - bin.length) + bin;
}
/**
* Class to store all data related to the testbench and functions to use it
* @param {Object} data - Javascript object of the test data
* @param {number=} currentGroup - Current group index in the test
* @param {number=} currentCase - Current case index in the group
*/
export class TestbenchData {
constructor(data, currentGroup = 0, currentCase = 0) {
this.currentCase = currentCase;
this.currentGroup = currentGroup;
this.testData = data;
}
/**
* Checks whether given case-group pair exists in the test
*/
isCaseValid() {
if (this.currentGroup >= this.data.groups.length || this.currentGroup < 0) return false;
const caseCount = this.testData.groups[this.currentGroup].inputs[0].values.length;
if (this.currentCase >= caseCount || this.currentCase < 0) return false;
return true;
}
/**
* Validate and set case and group in the test
* @param {number} groupIndex - Group index to set
* @param {number} caseIndex - Case index to set
*/
setCase(groupIndex, caseIndex) {
const newCase = new TestbenchData(this.testData, groupIndex, caseIndex);
if (newCase.isCaseValid()) {
this.currentGroup = groupIndex;
this.currentCase = caseIndex;
return true;
}
return false;
}
/**
* Validate and go to the next group.
* Skips over empty groups
*/
groupNext() {
const newCase = new TestbenchData(this.testData, this.currentGroup, 0);
const groupCount = newCase.testData.groups.length;
let caseCount = newCase.testData.groups[newCase.currentGroup].inputs[0].values.length;
while (caseCount === 0 || this.currentGroup === newCase.currentGroup) {
newCase.currentGroup++;
if (newCase.currentGroup >= groupCount) return false;
caseCount = newCase.testData.groups[newCase.currentGroup].inputs[0].values.length;
}
this.currentGroup = newCase.currentGroup;
this.currentCase = newCase.currentCase;
return true;
}
/**
* Validate and go to the previous group.
* Skips over empty groups
*/
groupPrev() {
const newCase = new TestbenchData(this.testData, this.currentGroup, 0);
const groupCount = newCase.testData.groups.length;
let caseCount = newCase.testData.groups[newCase.currentGroup].inputs[0].values.length;
while (caseCount === 0 || this.currentGroup === newCase.currentGroup) {
newCase.currentGroup--;
if (newCase.currentGroup < 0) return false;
caseCount = newCase.testData.groups[newCase.currentGroup].inputs[0].values.length;
}
this.currentGroup = newCase.currentGroup;
this.currentCase = newCase.currentCase;
return true;
}
/**
* Validate and go to the next case
*/
caseNext() {
const caseCount = this.testData.groups[this.currentGroup].inputs[0].values.length;
if (this.currentCase >= caseCount - 1) return this.groupNext();
this.currentCase++;
return true;
}
/**
* Validate and go to the previous case
*/
casePrev() {
if (this.currentCase <= 0) {
if (!this.groupPrev()) return false;
const caseCount = this.testData.groups[this.currentGroup].inputs[0].values.length;
this.currentCase = caseCount - 1;
return true;
}
this.currentCase--;
return true;
}
/**
* Finds and switches to the first non empty group to start the test from
*/
goToFirstValidGroup() {
const newCase = new TestbenchData(this.testData, 0, 0);
const caseCount = newCase.testData.groups[this.currentGroup].inputs[0].values.length;
// If the first group is not empty, do nothing
if (caseCount > 0) return true;
// Otherwise go next until non empty group
const validExists = newCase.groupNext();
// If all groups empty return false
if (!validExists) return false;
// else set case to the non empty group
this.currentGroup = newCase.currentGroup;
this.currentCase = newCase.currentCase;
return true;
}
}
/**
* UI Function
* Create prompt for the testbench UI when creator is opened
*/
function creatorOpenPrompt(creatorWindow) {
scheduleBackup();
const windowSVG = `
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" fill="white" class="bi bi-window" viewBox="0 0 16 16">
<path d="M2.5 4a.5.5 0 1 0 0-1 .5.5 0 0 0 0 1zm2-.5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0zm1 .5a.5.5 0 1 0 0-1 .5.5 0 0 0 0 1z"/>
<path d="M2 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H2zm13 2v2H1V3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1zM2 14a1 1 0 0 1-1-1V6h14v7a1 1 0 0 1-1 1H2z"/>
</svg>
`;
const s = `
<div style="text-align: center;">
<div style="margin: 20px;">
${windowSVG}
</div>
<p>A browser pop-up is opened to create the test</p>
<p>Please save the test to open it here</p>
</div>
`;
$('#setTestbenchData').dialog({
resizable: false,
width: 'auto',
buttons: [
{
text: 'Close Pop-Up',
click() {
$(this).dialog('close');
creatorWindow.close();
},
},
],
});
$('#setTestbenchData').empty();
$('#setTestbenchData').append(s);
}
/**
* Interface function to run testbench. Called by testbench prompt on simulator or assignments
* @param {Object} data - Object containing Test Data
* @param {RunContext=} runContext - Whether simulator or Assignment called this function
* @param {Scope=} scope - the circuit
*/
export function runTestBench(data, scope = globalScope, runContext = CONTEXT.CONTEXT_SIMULATOR) {
const isValid = validate(data, scope);
if (!isValid.ok) {
showMessage('Testbench: Some elements missing from circuit. Click Validate to know more');
}
if (runContext === CONTEXT.CONTEXT_SIMULATOR) {
const tempTestbenchData = new TestbenchData(data);
if (!tempTestbenchData.goToFirstValidGroup()) {
showMessage('Testbench: The test is empty');
return;
}
globalScope.testbenchData = tempTestbenchData;
updateTestbenchUI();
return;
}
if (runContext === CONTEXT.CONTEXT_ASSIGNMENTS) {
// Not implemented
}
}
/**
* Updates the TestBench UI on the simulator with the current test attached
* If no test is attached then shows the 'No test attached' screen
* Called by runTestBench() when test is set, also called by UX/setupPanelListeners()
* whenever ux change requires this UI to update(such as clicking on a different circuit or
* loading a saved circuit)
*/
export function updateTestbenchUI() {
// Remove all listeners from buttons
$('.tb-dialog-button').off('click');
$('.tb-case-button').off('click');
setupTestbenchUI();
if (globalScope.testbenchData != undefined) {
const { testbenchData } = globalScope;
// Initialize the UI
setUITableHeaders(testbenchData);
// Add listeners to buttons
$('.tb-case-button#prev-case-btn').on('click', buttonListenerFunctions.previousCaseButton);
$('.tb-case-button#next-case-btn').on('click', buttonListenerFunctions.nextCaseButton);
$('.tb-case-button#prev-group-btn').on('click', buttonListenerFunctions.previousGroupButton);
$('.tb-case-button#next-group-btn').on('click', buttonListenerFunctions.nextGroupButton);
$('.tb-dialog-button#change-test-btn').on('click', buttonListenerFunctions.changeTestButton);
$('.tb-dialog-button#runall-btn').on('click', buttonListenerFunctions.runAllButton);
$('.tb-dialog-button#edit-test-btn').on('click', buttonListenerFunctions.editTestButton);
$('.tb-dialog-button#validate-btn').on('click', buttonListenerFunctions.validateButton);
$('.tb-dialog-button#remove-test-btn').on('click', buttonListenerFunctions.removeTestButton);
}
// Add listener to attach test button
$('.tb-dialog-button#attach-test-btn').on('click', buttonListenerFunctions.attachTestButton);
}
/**
* Defines all the functions called as event listeners for buttons on the UI
*/
const buttonListenerFunctions = {
previousCaseButton: () => {
const isValid = validate(globalScope.testbenchData.testData, globalScope);
if (!isValid.ok) {
showMessage('Testbench: Some elements missing from circuit. Click Validate to know more');
return;
}
globalScope.testbenchData.casePrev();
buttonListenerFunctions.computeCase();
},
nextCaseButton: () => {
const isValid = validate(globalScope.testbenchData.testData, globalScope);
if (!isValid.ok) {
showMessage('Testbench: Some elements missing from circuit. Click Validate to know more');
return;
}
globalScope.testbenchData.caseNext();
buttonListenerFunctions.computeCase();
},
previousGroupButton: () => {
const isValid = validate(globalScope.testbenchData.testData, globalScope);
if (!isValid.ok) {
showMessage('Testbench: Some elements missing from circuit. Click Validate to know more');
return;
}
globalScope.testbenchData.groupPrev();
buttonListenerFunctions.computeCase();
},
nextGroupButton: () => {
const isValid = validate(globalScope.testbenchData.testData, globalScope);
if (!isValid.ok) {
showMessage('Testbench: Some elements missing from circuit. Click Validate to know more');
return;
}
globalScope.testbenchData.groupNext();
buttonListenerFunctions.computeCase();
},
changeTestButton: () => {
openCreator('create');
},
runAllButton: () => {
const isValid = validate(globalScope.testbenchData.testData, globalScope);
if (!isValid.ok) {
showMessage('Testbench: Some elements missing from circuit. Click Validate to know more');
return;
}
const results = runAll(globalScope.testbenchData.testData, globalScope);
const { passed } = results.summary;
const { total } = results.summary;
const resultString = JSON.stringify(results.detailed);
$('#runall-summary').text(`${passed} out of ${total}`);
$('#runall-detailed-link').on('click', () => { openCreator('result', resultString); });
$('.testbench-runall-label').css('display', 'table-cell');
$('.testbench-runall-label').delay(5000).fadeOut('slow');
},
editTestButton: () => {
const editDataString = JSON.stringify(globalScope.testbenchData.testData);
openCreator('edit', editDataString);
},
validateButton: () => {
const isValid = validate(globalScope.testbenchData.testData, globalScope);
showValidationUI(isValid);
},
removeTestButton: () => {
if (confirm('Are you sure you want to remove the test from the circuit?')) {
globalScope.testbenchData = undefined;
setupTestbenchUI();
}
},
attachTestButton: () => {
openCreator('create');
},
rerunTestButton: () => {
buttonListenerFunctions.computeCase();
},
computeCase: () => {
setUICurrentCase(globalScope.testbenchData);
const result = runSingleTest(globalScope.testbenchData, globalScope);
setUIResult(globalScope.testbenchData, result);
},
};
/**
* UI Function
* Checks whether test is attached to the scope and switches UI accordingly
*/
export function setupTestbenchUI() {
// Don't change UI if UI is minimized (because hide() and show() are recursive)
if ($('.testbench-manual-panel .minimize').css('display') === 'none') return;
if (globalScope.testbenchData === undefined) {
$('.tb-test-not-null').hide();
$('.tb-test-null').show();
return;
}
$('.tb-test-null').hide();
$('.tb-test-not-null').show();
}
/**
* Run all the tests automatically. Called by runTestBench()
* @param {Object} data - Object containing Test Data
* @param {Scope=} scope - the circuit
*/
export function runAll(data, scope = globalScope) {
// Stop the clocks
// TestBench will now take over clock toggling
changeClockEnable(false);
const { inputs, outputs, reset } = bindIO(data, scope);
let totalCases = 0;
let passedCases = 0;
data.groups.forEach((group) => {
// for (const output of group.outputs) output.results = [];
group.outputs.forEach((output) => output.results = []);
for (let case_i = 0; case_i < group.n; case_i++) {
totalCases++;
// Set and propagate the inputs
setInputValues(inputs, group, case_i, scope);
// If sequential, trigger clock now
if (data.type === 'seq') tickClock(scope);
// Get output values
const caseResult = getOutputValues(data, outputs);
// Put the results in the data
let casePassed = true; // Tracks if current case passed or failed
caseResult.forEach((_, outName) => {
// TODO: find() is not the best idea because of O(n)
const output = group.outputs.find((dataOutput) => dataOutput.label === outName);
output.results.push(caseResult.get(outName));
if (output.values[case_i] !== caseResult.get(outName)) casePassed = false;
});
// If current case passed, then increment passedCases
if (casePassed) passedCases++;
}
// If sequential, trigger reset at the end of group (set)
if (data.type === 'seq') triggerReset(reset);
});
// Tests done, restart the clocks
changeClockEnable(true);
// Return results
const results = {};
results.detailed = data;
results.summary = { passed: passedCases, total: totalCases };
// console.log(JSON.stringify(results.detailed));
return results;
}
/**
* Runs single test
* @param {Object} data - Object containing Test Data
* @param {number} groupIndex - Index of the group to be tested
* @param {number} caseIndex - Index of the case inside the group
* @param {Scope} scope - The circuit
*/
function runSingleTest(testbenchData, scope) {
const data = testbenchData.testData;
let result;
if (data.type === 'comb') {
result = runSingleCombinational(testbenchData, scope);
} else if (data.type === 'seq') {
result = runSingleSequential(testbenchData, scope);
}
return result;
}
/**
* Runs single combinational test
* @param {Object} data - Object containing Test Data
* @param {number} groupIndex - Index of the group to be tested
* @param {number} caseIndex - Index of the case inside the group
* @param {Scope} scope - The circuit
*/
function runSingleCombinational(testbenchData, scope) {
const data = testbenchData.testData;
const groupIndex = testbenchData.currentGroup;
const caseIndex = testbenchData.currentCase;
const { inputs, outputs } = bindIO(data, scope);
const group = data.groups[groupIndex];
// Stop the clocks
changeClockEnable(false);
// Set input values according to the test
setInputValues(inputs, group, caseIndex, scope);
// Check output values
const result = getOutputValues(data, outputs);
// Restart the clocks
changeClockEnable(true);
return result;
}
/**
* Runs single sequential test and all tests above it in the group
* Used in MANUAL mode
* @param {Object} data - Object containing Test Data
* @param {number} groupIndex - Index of the group to be tested
* @param {number} caseIndex - Index of the case inside the group
* @param {Scope} scope - The circuit
*/
function runSingleSequential(testbenchData, scope) {
const data = testbenchData.testData;
const groupIndex = testbenchData.currentGroup;
const caseIndex = testbenchData.currentCase;
const { inputs, outputs, reset } = bindIO(data, scope);
const group = data.groups[groupIndex];
// Stop the clocks
changeClockEnable(false);
// Trigger reset
triggerReset(reset, scope);
// Run the test and tests above in the same group
for (let case_i = 0; case_i <= caseIndex; case_i++) {
setInputValues(inputs, group, case_i, scope);
tickClock(scope);
}
const result = getOutputValues(data, outputs);
// Restart the clocks
changeClockEnable(true);
return result;
}
/**
* Set and propogate the input values according to the testcase.
* Called by runSingle() and runAll()
* @param {Object} inputs - Object with keys as input names and values as inputs
* @param {Object} group - Test group
* @param {number} caseIndex - Index of the case in the group
* @param {Scope} scope - the circuit
*/
function setInputValues(inputs, group, caseIndex, scope) {
group.inputs.forEach((input) => {
inputs[input.label].state = parseInt(input.values[caseIndex], 2);
});
// Propagate inputs
play(scope);
}
/**
* Gets Output values as a Map with keys as output name and value as output state
* @param {Object} outputs - Object with keys as output names and values as outputs
*/
function getOutputValues(data, outputs) {
const values = new Map();
data.groups[0].outputs.forEach((dataOutput) => {
// Using node value because output state only changes on rendering
const resultValue = outputs[dataOutput.label].nodeList[0].value;
const resultBW = outputs[dataOutput.label].nodeList[0].bitWidth;
values.set(dataOutput.label, dec2bin(resultValue, resultBW));
});
return values;
}
/**
* UI Function
* Shows validation UI
* @param {Object} validationErrors - Object with errors returned by validate()
*/
function showValidationUI(validationErrors) {
const checkSVG = `
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" fill="white" class="bi bi-check" viewBox="0 0 16 16">
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z"/>
</svg>
`;
let s = `
<div style="text-align: center; color: white;">
<div style="margin: 20px;">
${checkSVG}
</div>
All good. No validation errors
</div>
`;
if (!validationErrors.ok) {
s = `
<div style="text-align: center; color: white;">
<p>Please fix these errors to run tests</p>
<table class="validation-ui-table">
<tr>
<th><b>Identifier</b></th>
<th><b>Error</b></th>
</tr>
`;
validationErrors.invalids.forEach((vError) => {
s += `
<tr>
<td>${vError.identifier}</td>
<td>${vError.message}</td>
</tr>
`;
});
s += '</table></div>';
}
$('#testbenchValidate').dialog({
resizable: false,
width: 'auto',
buttons: [
{
text: 'Ok',
click() {
$(this).dialog('close');
},
},
{
text: 'Auto Fix',
click() {
const fixes = validationAutoFix(validationErrors);
showMessage(`Testbench: Auto fixed ${fixes} errors`);
$(this).dialog('close');
},
},
],
});
$('#testbenchValidate').empty();
$('#testbenchValidate').append(s);
}
/**
* Validate if all inputs and output elements are present with correct bitwidths
* @param {Object} data - Object containing Test Data
* @param {Scope} scope - the circuit
*/
function validate(data, scope) {
let invalids = [];
// Check for duplicate identifiers
if (!checkDistinctIdentifiersData(data)) {
invalids.push({
type: VALIDATION_ERRORS.DUPLICATE_ID_DATA,
identifier: '-',
message: 'Duplicate identifiers in test data',
});
}
if (!checkDistinctIdentifiersScope(scope)) {
invalids.push({
type: VALIDATION_ERRORS.DUPLICATE_ID_SCOPE,
identifier: '-',
message: 'Duplicate identifiers in circuit',
});
}
// Don't do further checks if duplicates
if (invalids.length > 0) return { ok: false, invalids };
// Validate inputs and outputs
const inputsValid = validateInputs(data, scope);
const outputsValid = validateOutputs(data, scope);
invalids = inputsValid.ok ? invalids : invalids.concat(inputsValid.invalids);
invalids = outputsValid.ok ? invalids : invalids.concat(outputsValid.invalids);
// Validate presence of reset if test is sequential
if (data.type === 'seq') {
const resetPresent = scope.Input.some((simulatorReset) => (
simulatorReset.label === 'RST'
&& simulatorReset.bitWidth === 1
&& simulatorReset.objectType === 'Input'
));
if (!resetPresent) {
invalids.push({
type: VALIDATION_ERRORS.NO_RST,
identifier: 'RST',
message: 'Reset(RST) not present in circuit',
});
}
}
if (invalids.length > 0) return { ok: false, invalids };
return { ok: true };
}
/**
* Autofix whatever is possible in validation errors.
* returns number of autofixed errors
* @param {Object} validationErrors - Object with errors returned by validate()
*/
function validationAutoFix(validationErrors) {
// Currently only autofixes bitwidths
let fixedErrors = 0;
// Return if no errors
if (validationErrors.ok) return fixedErrors;
const bitwidthErrors = validationErrors.invalids.filter((vError) => vError.type === VALIDATION_ERRORS.WRONGBITWIDTH);
bitwidthErrors.forEach((bwError) => {
const { element, expectedBitWidth } = bwError.extraInfo;
element.newBitWidth(expectedBitWidth);
fixedErrors++;
});
return fixedErrors;
}
/**
* Checks if all the labels in the test data are unique. Called by validate()
* @param {Object} data - Object containing Test Data
*/
function checkDistinctIdentifiersData(data) {
const inputIdentifiersData = data.groups[0].inputs.map((input) => input.label);
const outputIdentifiersData = data.groups[0].outputs.map((output) => output.label);
const identifiersData = inputIdentifiersData.concat(outputIdentifiersData);
return (new Set(identifiersData)).size === identifiersData.length;
}
/**
* Checks if all the input/output labels in the scope are unique. Called by validate()
* TODO: Replace with identifiers
* @param {Scope} scope - the circuit
*/
function checkDistinctIdentifiersScope(scope) {
const inputIdentifiersScope = scope.Input.map((input) => input.label);
const outputIdentifiersScope = scope.Output.map((output) => output.label);
let identifiersScope = inputIdentifiersScope.concat(outputIdentifiersScope);
// Remove identifiers which have not been set yet (ie. empty strings)
identifiersScope = identifiersScope.filter((identifer) => identifer != '');
return (new Set(identifiersScope)).size === identifiersScope.length;
}
/**
* Validates presence and bitwidths of test inputs in the circuit.
* Called by validate()
* @param {Object} data - Object containing Test Data
* @param {Scope} scope - the circuit
*/
function validateInputs(data, scope) {
const invalids = [];
data.groups[0].inputs.forEach((dataInput) => {
const matchInput = scope.Input.find((simulatorInput) => simulatorInput.label === dataInput.label);
if (matchInput === undefined) {
invalids.push({
type: VALIDATION_ERRORS.NOTPRESENT,
identifier: dataInput.label,
message: 'Input is not present in the circuit',
});
} else if (matchInput.bitWidth !== dataInput.bitWidth) {
invalids.push({
type: VALIDATION_ERRORS.WRONGBITWIDTH,
identifier: dataInput.label,
extraInfo: {
element: matchInput,
expectedBitWidth: dataInput.bitWidth,
},
message: `Input bitwidths don't match in circuit and test (${matchInput.bitWidth} vs ${dataInput.bitWidth})`,
});
}
});
if (invalids.length > 0) return { ok: false, invalids };
return { ok: true };
}
/**
* Validates presence and bitwidths of test outputs in the circuit.
* Called by validate()
* @param {Object} data - Object containing Test Data
* @param {Scope} scope - the circuit
*/
function validateOutputs(data, scope) {
const invalids = [];
data.groups[0].outputs.forEach((dataOutput) => {
const matchOutput = scope.Output.find((simulatorOutput) => simulatorOutput.label === dataOutput.label);
if (matchOutput === undefined) {
invalids.push({
type: VALIDATION_ERRORS.NOTPRESENT,
identifier: dataOutput.label,
message: 'Output is not present in the circuit',
});
} else if (matchOutput.bitWidth !== dataOutput.bitWidth) {
invalids.push({
type: VALIDATION_ERRORS.WRONGBITWIDTH,
identifier: dataOutput.label,
extraInfo: {
element: matchOutput,
expectedBitWidth: dataOutput.bitWidth,
},
message: `Output bitwidths don't match in circuit and test (${matchOutput.bitWidth} vs ${dataOutput.bitWidth})`,
});
}
});
if (invalids.length > 0) return { ok: false, invalids };
return { ok: true };
}
/**
* Returns object of scope inputs and outputs keyed by their labels
* @param {Object} data - Object containing Test Data
* @param {Scope=} scope - the circuit
*/
function bindIO(data, scope) {
const inputs = {};
const outputs = {};
let reset;
data.groups[0].inputs.forEach((dataInput) => {
inputs[dataInput.label] = scope.Input.find((simulatorInput) => simulatorInput.label === dataInput.label);
});
data.groups[0].outputs.forEach((dataOutput) => {
outputs[dataOutput.label] = scope.Output.find((simulatorOutput) => simulatorOutput.label === dataOutput.label);
});
if (data.type === 'seq') {
reset = scope.Input.find((simulatorOutput) => simulatorOutput.label === 'RST');
}
return { inputs, outputs, reset };
}
/**
* Ticks clock recursively one full cycle (Only used in testbench context)
* @param {Scope} scope - the circuit whose clock to be ticked
*/
function tickClock(scope) {
scope.clockTick();
play(scope);
scope.clockTick();
play(scope);
}
/**
* Triggers reset (Only used in testbench context)
* @param {Input} reset - reset pin to be triggered
* @param {Scope} scope - the circuit
*/
function triggerReset(reset, scope) {
reset.state = 1;
play(scope);
reset.state = 0;
play(scope);
}
/**
* UI Function
* Sets IO labels and bitwidths on UI table
* Called by simulatorRunTestbench()
* @param {Object} data - Object containing the test data
*/
function setUITableHeaders(testbenchData) {
const data = testbenchData.testData;
const inputCount = data.groups[0].inputs.length;
const outputCount = data.groups[0].outputs.length;
$('#tb-manual-table-inputs-head').attr('colspan', inputCount);
$('#tb-manual-table-outputs-head').attr('colspan', outputCount);
$('.testbench-runall-label').css('display', 'none');
$('.tb-data#data-title').children().eq(1).text(data.title || 'Untitled');
$('.tb-data#data-type').children().eq(1).text(data.type === 'comb' ? 'Combinational' : 'Sequential');
$('#tb-manual-table-labels').html('<th>LABELS</th>');
$('#tb-manual-table-bitwidths').html('<td>Bitwidth</td>');
data.groups[0].inputs.concat(data.groups[0].outputs).forEach((io) => {
const label = `<th>${escapeHtml(io.label)}</th>`;
const bw = `<td>${escapeHtml(io.bitWidth.toString())}</td>`;
$('#tb-manual-table-labels').append(label);
$('#tb-manual-table-bitwidths').append(bw);
});
setUICurrentCase(testbenchData);
}
/**
* UI Function
* Set current test case data on the UI
* @param {Object} data - Object containing the test data
* @param {number} groupIndex - Index of the group of current case
* @param {number} caseIndex - Index of the case within the group
*/
function setUICurrentCase(testbenchData) {
const data = testbenchData.testData;
const groupIndex = testbenchData.currentGroup;
const caseIndex = testbenchData.currentCase;
const currCaseElement = $('#tb-manual-table-current-case');
currCaseElement.empty();
currCaseElement.append('<td>Current Case</td>');
$('#tb-manual-table-test-result').empty();
$('#tb-manual-table-test-result').append('<td>Result</td>');
data.groups[groupIndex].inputs.forEach((input) => {
currCaseElement.append(`<td>${escapeHtml(input.values[caseIndex])}</td>`);
});
data.groups[groupIndex].outputs.forEach((output) => {
currCaseElement.append(`<td>${escapeHtml(output.values[caseIndex])}</td>`);
});
$('.testbench-manual-panel .group-label').text(data.groups[groupIndex].label);
$('.testbench-manual-panel .case-label').text(caseIndex + 1);
}
/**
* UI Function
* Set the current test case result on the UI
* @param {Object} data - Object containing the test data
* @param {Map} result - Map containing the output values (returned by getOutputValues())
*/
function setUIResult(testbenchData, result) {
const data = testbenchData.testData;
const groupIndex = testbenchData.currentGroup;
const caseIndex = testbenchData.currentCase;
const resultElement = $('#tb-manual-table-test-result');
let inputCount = data.groups[0].inputs.length;
resultElement.empty();
resultElement.append('<td>Result</td>');
while (inputCount--) {
resultElement.append('<td> - </td>');
}
for (const output of result.keys()) {
const resultValue = result.get(output);
const expectedValue = data.groups[groupIndex].outputs.find((dataOutput) => dataOutput.label === output).values[caseIndex];
const color = resultValue === expectedValue ? '#17FC12' : '#FF1616';
resultElement.append(`<td style="color: ${color}">${escapeHtml(resultValue)}</td>`);
}
}
/**
* Use this function to navigate to test creator. This function starts the storage listener
* so the test is loaded directly into the simulator
* @param {string} type - 'create', 'edit' or 'result'
* @param {String} dataString - data in JSON string to load in case of 'edit' and 'result'
*/
function openCreator(type, dataString) {
const popupHeight = 800;
const popupWidth = 1200;
const popupTop = (window.height - popupHeight) / 2;
const popupLeft = (window.width - popupWidth) / 2;
const POPUP_STYLE_STRING = `height=${popupHeight},width=${popupWidth},top=${popupTop},left=${popupLeft}`;
let popUp;
/* Listener to catch testData from pop up and load it onto the testbench */
const dataListener = (message) => {
if (message.origin !== window.origin || message.data.type !== 'testData') return;
// Check if the current scope requested the creator pop up
const data = JSON.parse(message.data.data);
// Unbind event listener
window.removeEventListener('message', dataListener);
// If scopeID does not match, do nothing and return
if (data.scopeID != globalScope.id) return;
// Load test data onto the scope
runTestBench(data.testData, globalScope, CONTEXT.CONTEXT_SIMULATOR);
// Close the 'Pop up is open' dialog
$('#setTestbenchData').dialog('close');
};
if (type === 'create') {
const url = `${TESTBENCH_CREATOR_PATH}?scopeID=${globalScope.id}&popUp=true`;
popUp = window.open(url, 'popupWindow', POPUP_STYLE_STRING);
creatorOpenPrompt(popUp);
window.addEventListener('message', dataListener);
}
if (type === 'edit') {
const url = `${TESTBENCH_CREATOR_PATH}?scopeID=${globalScope.id}&data=${dataString}&popUp=true`;
popUp = window.open(url, 'popupWindow', POPUP_STYLE_STRING);
creatorOpenPrompt(popUp);
window.addEventListener('message', dataListener);
}
if (type === 'result') {
const url = `${TESTBENCH_CREATOR_PATH}?scopeID=${globalScope.id}&result=${dataString}&popUp=true`;
popUp = window.open(url, 'popupWindow', POPUP_STYLE_STRING);
}
// Check if popup was closed (in case it was closed by window's X button),
// then close 'popup open' dialog
if (popUp && type !== 'result') {
const checkPopUp = setInterval(() => {
if (popUp.closed) {
// Close the dialog if it's open
if ($('#setTestbenchData').dialog('isOpen')) $('#setTestbenchData').dialog('close');
// Remove the event listener that listens for data from popup
window.removeEventListener('message', dataListener);
clearInterval(checkPopUp);
}
}, 1000);
}
}