UNPKG

cirsim

Version:

Cirsim Circuit Simulator

569 lines (475 loc) 22.9 kB
import {Value} from '../Value'; import {MessageDialog} from '../Dlg/MessageDialog'; import {TestException} from './TestException'; import {Ajax} from "../Utility/Ajax"; import {JsonAPI} from "../Utility/JsonAPI"; /** * Constructor * @param main The Test object */ export const Test = function(main) { /// The main object this.main = main; /// Array of installed tests this.tests = []; this.addTest = function (test) { if (test === Object(test)) { this.tests.push(test); } else if (test.substr(0, 1) === '{') { this.tests.push(JSON.parse(test)); } else { // Not JSON, must be base64 encoded test = atob(test); this.tests.push(JSON.parse(test)); } } /** * Find a test by its tag. * @param tag Tag to search for */ this.findTest = function(tag) { for(var i=0; i<this.tests.length; i++) { if(this.tests[i].tag === tag) { return this.tests[i]; } } return null; } function isString(str) { return (typeof str === 'string' || str instanceof String); } /** * Run a single test and bring up result dialog boxes * @param test A test from the array of tests. */ this.runTestDlg = function(test) { // Save before we test main.save(true, true); // Set the overlay so the tests are modal main.modal(true); const promise = this.runTest(test); promise.then((test) => { // Success main.modal(false); var html = '<h1>Circuit Success</h1>' + '<p>The test has passed.</p>' var dlg = new MessageDialog("Success", html); dlg.open(); setResult(test, test.success !== undefined ? test.success : 'success', main.model.toJSON()); }, (msg) => { // Failure main.modal(false); var html = '<h1>Circuit Failure</h1>' + msg; var dlg = new MessageDialog("Test Failure", html, 450); dlg.open(); setResult(test, 'fail', main.model.toJSON()); }); } function setResult(test, result, circuit) { if(test.result !== undefined) { const elements = document.querySelectorAll(test.result); for(const element of elements) { element.value = result; } } if(test.circuit !== undefined) { const elements = document.querySelectorAll(test.circuit); for(const element of elements) { element.value = circuit; } } const api = main.options.getAPI('test'); if(api === null) { // Test API is not supported return; } let data = Object.assign({cmd: "test", name: api.name, result: result, data: circuit, type: 'application/json' }, api.extra); Ajax.do({ url: api.url, data: data, method: "POST", dataType: 'json', contentType: api.contentType, success: (data) => { var json = new JsonAPI(data); if(!main.toast.jsonErrors(json)) { main.toast.message('Test result successfully saved to server'); } }, error: (xhr, status, error) => { // console.log(xhr.responseText); main.toast.message('Unable to communicate with server: ' + error); } }); } this.runTest = function(test) { return new Promise((success, failure) => { const model = main.model; // Backup the model to support Undo of what the test changes model.backup(); // The current test number let testNum = -1; let inputs, outputs; try { // // Find the inputs // inputs = findInputs(test); // // Find the outputs // outputs = findOutputs(test); } catch(exception) { if(exception instanceof TestException) { failure(exception.msg); return; } else { throw exception; } } function testOne() { if(testNum >= 0) { const t = test.test[testNum]; // Ensure the last test passed for(let i=0; i<outputs.length && (i + inputs.length)<t.length; i++) { // What is expected? let expected = t[i + inputs.length]; // Handle don't care, either a null or '?' if(expected === undefined || expected === null || expected === '?') { continue; } // // Handle any prefixes // // bitslop: is the bitslop option prefix. // Bitslop means we expect the result to be // within one bit of the expected value. // // test: is a string-based test. let bitSlop = false; let stringTest = false; if(isString(expected)) { let any = false; do { any = false; if(expected.substr(0, 8) === "bitslop:") { bitSlop = true; expected = expected.substr(8); any = true; } else if(expected.substr(0, 5) === "test:") { stringTest = true; expected = expected.substr(5); any = true; } } while(any); } if(stringTest) { // // String-based tests are like this: // // test:red=1;yel=0;grn=0 // After the test: there is a series of one or more // tests separated by semicolons. Those tests are // passed to the function testAsString on the input, // which, in turn, passes the test on to testAsString // on the component. Test failures are indicated by a // throw. // const stringTests = expected.split(';'); for(const test of stringTests) { // Send the test to the component try { outputs[i].testAsString(test); } catch(msg) { if(test.quiet === true) { failure('<div class="cirsim-test-result"><p>This test is failing. Some output is ' + 'not what is currently expected by the test. The circuit is left in the state it was' + ' in when the test failed. No additional detail will be provided about why ' + 'the test is failed. It is your responsibility to create a ' + 'circuit that works as expected.</p></div>'); } else { failure(`<div class="cirsim-test-result"><p>This test is failing. ${msg}</p> <p class="cs-info">Test ${testNum}</p></div>`); } // We are done when there is an error return; } } } else { // Tests based on an expected value // What is expected? Use a Value component to // allow things like hex and floating point values const value = new Value(); value.type = Value.BINARY; value.setAsString(expected); // Get the result let actual = outputs[i].getAsString(); let good = true; // Until we know otherwise if (bitSlop) { expected = value.getAsInteger(); value.setAsBinary(actual); actual = value.getAsInteger(); if (actual === '?') { good = false; } else if (actual < (expected - 1) || (actual > expected + 1)) { good = false; } } else { // The normal (binary) comparison case expected = value.getAsBinary(); if (isString(expected)) { // j and k index the last letters in actual and expected let j = actual.length - 1; let k = expected.length - 1; // Test from the right end of both results so we // ensure we are testing the same bits. for (; k >= 0 && good; j--, k--) { if (expected.substr(k, 1) === '?') { continue; } if (j < 0) { good = false; break; } if (actual.substr(j, 1) != expected.substr(k, 1)) { good = false; } } // If we exhausted expected, but still have actual bits // we have an error if (j > 0) { good = false; } } else { if (expected !== null && expected !== '?') { good = expected == actual; } } } if (good) { // Success } else { // Failure console.log("Test: " + testNum + " Output " + outputs[i].component.naming + " Actual: " + actual + " Expected: " + expected); if (test.quiet === true) { failure('<div class="cirsim-test-result"><p>This test is failing. Some output is not what is currently' + ' expected by the test. The circuit is left in the state it was' + ' in when the test failed. No additional detail will be provided about why ' + 'the test is failed. It is your responsibility to create a ' + 'circuit that works as expected.</p></div>'); } else { failure(`<div class="cirsim-test-result"><p>This test is failing. An output value is not what is currently expected by the test. The circuit is left in the state it was in when the test failed.<p> <p class="cs-result">Output ${outputs[i].component.naming} expected: ${expected} actual: ${actual}</p> <p class="cs-info">Test ${testNum}</p></div> `); } return; } } } } testNum++; if(testNum < test.test.length) { const t = test.test[testNum]; for(let i=0; i<inputs.length && i<t.length; i++) { if(t[i] !== null) { const result = inputs[i].command(t[i]); if(result !== null) { if(!result.ok) { failure('<p>This test is failing. ' + result.msg + '</p>'); return; } } else { try { inputs[i].setAsString(t[i]); } catch(msg) { failure(`<div class="cirsim-test-result"><p>This test is failing. ${msg}</p> <p class="cs-info">Test ${testNum}</p></div>`); return; } } } } // Churn one second worth const simulation = model.getSimulation(); for(let i=0; i<100; i++) { if(!simulation.advance(0.010 * simulation.speed)) { break; } } setTimeout(testOne, main.options.testTime); if(simulation.view !== null) { simulation.view.draw(); } } else { success(test); } } setTimeout(testOne, main.options.testTime); }); } /** * Find all of the specified circuit inputs * @param test The test we are running * @returns {Array} Array of input objects */ function findInputs(test) { var model = main.model; // // Find the inputs // var inputs = []; for(let i=0; i<test.input.length; i++) { let input = test.input[i]; let items = input.split(':'); let search = model; let tabmsg = ''; // Test for tab specification. That's a prefix // like this: tab:tabname: if(items[0] === "tab") { if(items.length < 3) { throw new TestException('<p>Invalid input tab definition: ' + input + '</p>'); } const tabname = items[1]; search = model.getCircuit(tabname); if(search === null) { throw new TestException('<p>Invalid input tab: ' + tabname + '</p>'); } tabmsg = ' in tab <em>' + tabname + '</em>'; items.splice(0, 2); } if(items[0] === "type") { if(items.length < 2) { throw new TestException('<p>Invalid input type specification: ' + input + '</p>'); } const type = items[1]; const components = search.getComponentsByType(type); if(items.length > 2) { // We have specified a component name after the type // Example: type:InPin:CLK let desired = null; for(let component of components) { if(component.naming === items[2]) { desired = component; break; } } if(desired === null) { throw new TestException('<p>The test is not able to pass because you do not have a' + ' component named ' + items[2] + ' of type ' + type + tabmsg + '.</p>'); } inputs.push(desired); } else { if(components.length === 0) { throw new TestException('<p>The test is not able to pass because you do not have a' + ' component of type ' + type + tabmsg + '.</p>'); } else if(components.length > 1) { throw new TestException('<p>The test is not able to pass because you have more than' + ' one component of type ' + type + tabmsg + '.</p>' + '<p>You are only allowed one component of that type ' + 'in this circuit.</p>'); } inputs.push(components[0]); } } else { // Finding component by naming const component = search.getComponentByNaming(items[0]); if(component !== null) { inputs.push(component); } else { throw new TestException('<p>The test is not able to pass because you do not' + ' have a component named ' + items[0] + tabmsg + '.</p>' + '<p>Typically, the tests are looking for an input' + ' pin or a bus input pin. Input pins are labeled IN in the palette. Double-click' + ' on an input pin to set the name. Names in Cirsim are case sensitive.</p>'); } } } return inputs; } function findOutputs(test) { var model = main.model; var outputs = []; for(var i=0; i<test.output.length; i++) { var search = model; var tabmsg = ''; var split = test.output[i].split("-"); var output = split[0]; let items = output.split(':'); // Test for tab specification. That's a prefix // like this: tab:tabname: if(output.substr(0, 4) === "tab:") { var tab = output.substr(4); var colon = tab.indexOf(":"); if(colon === -1) { throw new TestException('<p>Invalid output tab definition: ' + output + '</p>'); break; } var tabname = tab.substr(0, colon); search = model.getCircuit(tabname); if(search === null) { throw new TestException('<p>Invalid out tab: ' + tabname + '</p>'); break; } tabmsg = ' in tab <em>' + tabname + '</em>'; output = tab.substr(colon+1); items.splice(0, 2); } var pin = split.length > 1 ? split[1] : 0; if(items[0] === 'type') { if(items.length < 2) { throw new TestException('<p>Invalid output type specification: ' + output + '</p>'); } const type = items[1]; const components = search.getComponentsByType(type); if(items.length > 2) { // We have specified a component name after the type // Example: type:OutPin:CLK let desired = null; for(let component of components) { if(component.naming === items[2]) { desired = component; break; } } if(desired === null) { throw new TestException('<p>The test is not able to pass because you do not have a' + ' component named ' + items[2] + ' of type ' + type + tabmsg + '.</p>'); } outputs.push(desired.ins[pin]); } else { if(components.length === 0) { throw new TestException('<p>The test is not able to pass because you do not have a' + ' component of type ' + type + tabmsg + '.</p>'); } else if(components.length > 1) { throw new TestException('<p>The test is not able to pass because you have more than' + ' one component of type ' + type + tabmsg + '.</p>' + '<p>You are only allowed one component of that type ' + 'in this circuit.</p>'); } outputs.push(components[0].ins[pin]); } } else { const component = search.getComponentByNaming(output); if(component !== null && component.ins.length > pin) { outputs.push(component.ins[pin]); } else { throw new TestException('<p>The test is not able to pass because you do not' + ' have a component named ' + output + tabmsg + '.</p>' + '<p>Output pins are labeled OUT in the palette. Double-click' + ' on an output pin to set the name. Pin names are case sensitive.</p>'); } } } return outputs; } }