const MachineGroup = require('./machineGroup'); const Machine = require('../../../rotatingMachine/dependencies/machine/machine'); const specs = require('../../../generalFunctions/datasets/assetData/pumps/hydrostal/centrifugal pumps/models.json'); class MachineGroupTester { constructor() { this.totalTests = 0; this.passedTests = 0; this.failedTests = 0; this.machineCurve = specs[0].machineCurve; } assert(condition, message) { this.totalTests++; if (condition) { console.log(`✓ PASS: ${message}`); this.passedTests++; } else { console.log(`✗ FAIL: ${message}`); this.failedTests++; } } createBaseMachineConfig(name) { return { general: { logging: { enabled: true, logLevel: "debug" }, name: name, unit: "m3/h" }, functionality: { softwareType: "machine", role: "RotationalDeviceController" }, asset: { type: "pump", subType: "Centrifugal", model: "TestModel", supplier: "Hydrostal", machineCurve: this.machineCurve }, mode: { current: "auto", allowedActions: { auto: ["execSequence", "execMovement", "statusCheck"], virtualControl: ["execMovement", "statusCheck"], fysicalControl: ["statusCheck"] }, allowedSources: { auto: ["parent", "GUI"], virtualControl: ["GUI"], fysicalControl: ["fysical"] } }, sequences: { startup: ["starting", "warmingup", "operational"], shutdown: ["stopping", "coolingdown", "idle"], emergencystop: ["emergencystop", "off"], boot: ["idle", "starting", "warmingup", "operational"] }, calculationMode: "medium" }; } createBaseMachineGroupConfig(name) { return { general: { logging: { enabled: true, logLevel: "debug" }, name: name }, functionality: { softwareType: "machineGroup", role: "GroupController" }, scaling: { current: "normalized" }, mode: { current: "optimalControl" } }; } async testSingleMachineOperation() { console.log('\nTesting Single Machine Operation...'); const machineGroupConfig = this.createBaseMachineGroupConfig("TestMachineGroup"); const machineConfig = this.createBaseMachineConfig("TestMachine1"); try { const mg = new MachineGroup(machineGroupConfig); const machine = new Machine(machineConfig); // Register machine with group mg.childRegistrationUtils.registerChild(machine, "downstream"); machine.measurements.type("pressure").variant("measured").position("downstream").value(800); await machine.state.transitionToState("idle"); // Test 1: Basic initialization this.assert( Object.keys(mg.machines).length === 0, 'Machine group should have exactly zero machine' ); // Test 2: Calculate demand with single machine await machine.handleInput("parent", "execSequence", "startup"); await mg.handleFlowInput(50); this.assert( mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() > 0, 'Total flow should be greater than 0 for demand of 50' ); // Test 3: Check machine mode handling machine.setMode("virtualControl"); const {single, machineNum} = mg.singleMachine(); this.assert( single === true, 'Should identify as single machine when in virtual control' ); // Test 4: Zero demand handling await mg.handleFlowInput(0); this.assert( !mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() || mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() === 0, 'Total flow should be 0 for zero demand' ); // Test 5: Max demand handling await mg.handleFlowInput(100); this.assert( mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() > 0, 'Total flow should be greater than 0 for max demand' ); } catch (error) { console.error('Test failed with error:', error); this.failedTests++; } } async testMultipleMachineOperation() { console.log('\nTesting Multiple Machine Operation...'); const machineGroupConfig = this.createBaseMachineGroupConfig("TestMachineGroup"); try { const mg = new MachineGroup(machineGroupConfig); const machine1 = new Machine(this.createBaseMachineConfig("Machine1")); const machine2 = new Machine(this.createBaseMachineConfig("Machine2")); mg.childRegistrationUtils.registerChild(machine1, "downstream"); mg.childRegistrationUtils.registerChild(machine2, "downstream"); machine1.measurements.type("pressure").variant("measured").position("downstream").value(800); machine2.measurements.type("pressure").variant("measured").position("downstream").value(800); await machine1.state.transitionToState("idle"); await machine2.state.transitionToState("idle"); await machine1.handleInput("parent", "execSequence", "startup"); await machine2.handleInput("parent", "execSequence", "startup"); // Test 1: Multiple machine registration this.assert( Object.keys(mg.machines).length === 2, 'Machine group should have exactly two machines' ); // Test 1.1: Calculate demand with multiple machines await mg.handleFlowInput(0); // Testing with higher demand for two machines const machineOutputs = Object.keys(mg.machines).filter(id => mg.machines[id].measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() > 0 ); this.assert( mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() > 0 && machineOutputs.length > 0, 'Should distribute load between machines' ); // Test 1.2: Calculate demand with multiple machines with an increment of 10 for(let i = 0; i < 100; i+=10){ await mg.handleFlowInput(i); // Testing with incrementing demand const flowValue = mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue(); this.assert( flowValue !== undefined && !isNaN(flowValue), `Should handle demand of ${i} units properly` ); } // Test 2: Calculate nonsense demands with multiple machines await mg.handleFlowInput(150); // Testing with higher demand for two machines this.assert( mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() > 0, 'Should handle excessive demand gracefully' ); // Test 3: Force single machine mode machine2.setMode("maintenance"); const {single} = mg.singleMachine(); this.assert( single === true, 'Should identify as single machine when one machine is in maintenance' ); } catch (error) { console.error('Test failed with error:', error); this.failedTests++; } } async testDynamicTotals() { console.log('\nTesting Dynamic Totals...'); const mg = new MachineGroup(this.createBaseMachineGroupConfig("TestMachineGroup")); const machine = new Machine(this.createBaseMachineConfig("TestMachine")); try { mg.childRegistrationUtils.registerChild(machine, "downstream"); machine.measurements.type("pressure").variant("measured").position("downstream").value(800); await machine.state.transitionToState("idle"); await machine.handleInput("parent", "execSequence", "startup"); // Test 1: Dynamic totals initialization const maxFlow = machine.predictFlow.currentFxyYMax; const maxPower = machine.predictPower.currentFxyYMax; this.assert( mg.dynamicTotals.flow.max === maxFlow && mg.dynamicTotals.power.max === maxPower, 'Dynamic totals should reflect machine capabilities' ); // Test 2: Demand scaling await mg.handleFlowInput(50); // 50% of max const actualFlow = mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue(); this.assert( actualFlow <= maxFlow * 0.6, // Allow some margin for interpolation 'Scaled demand should be approximately 50% of max flow' ); } catch (error) { console.error('Test failed with error:', error); this.failedTests++; } } async testInterpolation() { console.log('\nTesting Interpolation...'); const machineGroupConfig = this.createBaseMachineGroupConfig("TestMachineGroup"); const machineConfig = this.createBaseMachineConfig("TestMachine"); try { const mg = new MachineGroup(machineGroupConfig); const machine = new Machine(machineConfig); // Register machine and set initial state mg.childRegistrationUtils.registerChild(machine, "downstream"); machine.measurements.type("pressure").variant("measured").position("downstream").value(1); machine.state.transitionToState("idle"); // Test interpolation at different demand points const testPoints = [0, 25, 50, 75, 100]; for (const demand of testPoints) { await mg.handleFlowInput(demand); const flowValue = mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue(); const powerValue = mg.measurements.type("power").variant("predicted").position("upstream").getCurrentValue(); this.assert( flowValue !== undefined && !isNaN(flowValue), `Interpolation should produce valid flow value for demand ${demand}` ); this.assert( powerValue !== undefined && !isNaN(powerValue), `Interpolation should produce valid power value for demand ${demand}` ); } // Test interpolation between curve points const interpolatedPoint = 45; // Should interpolate between 40 and 60 await mg.handleFlowInput(interpolatedPoint); this.assert( mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() > 0, `Interpolation should handle non-exact point ${interpolatedPoint}` ); } catch (error) { console.error('Test failed with error:', error); this.failedTests++; } } async testSingleMachineControlModes() { console.log('\nTesting Single Machine Control Modes...'); const machineGroupConfig = this.createBaseMachineGroupConfig("TestMachineGroup"); const machineConfig = this.createBaseMachineConfig("TestMachine1"); try { const mg = new MachineGroup(machineGroupConfig); const machine = new Machine(machineConfig); // Register machine and initialize mg.childRegistrationUtils.registerChild(machine, "downstream"); machine.measurements.type("pressure").variant("measured").position("downstream").value(800); await machine.state.transitionToState("idle"); await machine.handleInput("parent", "execSequence", "startup"); // Test 1: Virtual Control Mode machine.setMode("virtualControl"); await mg.handleFlowInput(50); this.assert( machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() !== undefined, 'Should handle virtual control mode' ); // Test 2: Physical Control Mode machine.setMode("fysicalControl"); await mg.handleFlowInput(75); this.assert( machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() !== undefined, 'Should handle physical control mode' ); // Test 3: Auto Mode Return machine.setMode("auto"); await mg.handleFlowInput(60); this.assert( mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() > 0, 'Should return to normal operation in auto mode' ); } catch (error) { console.error('Test failed with error:', error); this.failedTests++; } } async testMachinesOffNormalized() { console.log('\nTesting Machines Off with Normalized Flow...'); const machineGroupConfig = this.createBaseMachineGroupConfig("TestMachineGroup_OffNormalized"); // scaling is "normalized" by default const mg = new MachineGroup(machineGroupConfig); const machine = new Machine(this.createBaseMachineConfig("TestMachine_OffNormalized")); mg.childRegistrationUtils.registerChild(machine, "downstream"); machine.measurements.type("pressure").variant("measured").position("downstream").value(800); await machine.state.transitionToState("idle"); await machine.handleInput("parent", "execSequence", "startup"); // Turn machines off by setting demand to 0 with normalized scaling await mg.handleFlowInput(-1); this.assert( !mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() || mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() === 0, 'Total flow should be 0 when demand is < 0 in normalized scaling' ); } async testMachinesOffAbsolute() { console.log('\nTesting Machines Off with Absolute Flow...'); const machineGroupConfig = this.createBaseMachineGroupConfig("TestMachineGroup_OffAbsolute"); // Switch scaling to "absolute" machineGroupConfig.scaling.current = "absolute"; const mg = new MachineGroup(machineGroupConfig); const machine = new Machine(this.createBaseMachineConfig("TestMachine_OffAbsolute")); mg.childRegistrationUtils.registerChild(machine, "downstream"); machine.measurements.type("pressure").variant("measured").position("downstream").value(800); await machine.state.transitionToState("idle"); await machine.handleInput("parent", "execSequence", "startup"); // Turn machines off by setting demand to 0 with absolute scaling await mg.handleFlowInput(0); this.assert( !mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() || mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() === 0, 'Total flow should be 0 when demand is 0 in absolute scaling' ); } async testPriorityControl() { console.log('\nTesting Priority Control...'); const machineGroupConfig = this.createBaseMachineGroupConfig("TestMachineGroup_Priority"); const mg = new MachineGroup(machineGroupConfig); try { // Create 3 machines with different configurations for clearer testing const machines = []; for(let i = 1; i <= 3; i++) { const machineConfig = this.createBaseMachineConfig(`Machine${i}`); const machine = new Machine(machineConfig); machines.push(machine); mg.childRegistrationUtils.registerChild(machine, "downstream"); // Set different max flows to make priority visible machine.predictFlow = { currentFxyYMin: 10 * i, // Different min flows currentFxyYMax: 50 * i // Different max flows }; machine.measurements.type("pressure").variant("measured").position("downstream").value(800); await machine.state.transitionToState("idle"); await machine.handleInput("parent", "execSequence", "startup"); // Mock the inputFlowCalcPower method for testing machine.inputFlowCalcPower = (flow) => flow * 2; // Simple mock function } // Test 1: Default priority (by machine ID) // Use handleInput which routes to equalControl in prioritycontrol mode await mg.handleInput("parent", 80); const flowAfterDefaultPriority = Object.values(mg.machines).map(machine => machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() || 0 ); this.assert( flowAfterDefaultPriority[0] > 0 && flowAfterDefaultPriority[1] > 0 && flowAfterDefaultPriority[2] === 0, 'Default priority should use machines in ID order until demand is met' ); // Test 2: Custom priority list await mg.handleInput("parent", 120, Infinity, [3, 2, 1]); await new Promise(resolve => setTimeout(resolve, 100)); const flowAfterCustomPriority = Object.values(mg.machines).map(machine => machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() || 0 ); this.assert( flowAfterCustomPriority[2] > 0 && flowAfterCustomPriority[1] > 0 && flowAfterCustomPriority[0] === 0, 'Custom priority should use machines in specified order until demand is met' ); // Test 3: Zero demand should shut down all machines await mg.handleInput("parent", 0); const noFlowCondition = Object.values(mg.machines).every(machine => !machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() || machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() === 0 ); this.assert( noFlowCondition, 'Zero demand should result in no flow from any machine' ); // Test 4: Handling excessive demand (more than total capacity) const totalMaxFlow = machines.reduce((sum, machine) => sum + machine.predictFlow.currentFxyYMax, 0); await mg.handleInput("parent", totalMaxFlow + 100); const totalActualFlow = mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue(); this.assert( totalActualFlow <= totalMaxFlow && totalActualFlow > 0, 'Excessive demand should be capped to maximum possible flow' ); // Test 5: Check all measurements are updated correctly this.assert( mg.measurements.type("power").variant("predicted").position("upstream").getCurrentValue() > 0 && mg.measurements.type("efficiency").variant("predicted").position("downstream").getCurrentValue() > 0, 'All measurements should be updated after priority control' ); } catch (error) { console.error('Priority control test failed with error:', error); this.failedTests++; } } async runAllTests() { console.log('Starting MachineGroup Tests...\n'); await this.testSingleMachineOperation(); await this.testMultipleMachineOperation(); await this.testDynamicTotals(); await this.testInterpolation(); await this.testSingleMachineControlModes(); await this.testMachinesOffNormalized(); await this.testMachinesOffAbsolute(); await this.testPriorityControl(); // Add the new test await testCombinationIterations(); console.log('\nTest Summary:'); console.log(`Total Tests: ${this.totalTests}`); console.log(`Passed: ${this.passedTests}`); console.log(`Failed: ${this.failedTests}`); // Return exit code based on test results process.exit(this.failedTests > 0 ? 1 : 0); } } // Add a custom logger to capture debug logs during tests class CapturingLogger { constructor() { this.logs = []; } debug(message) { this.logs.push({ level: "debug", message }); console.debug(message); } info(message) { this.logs.push({ level: "info", message }); console.info(message); } warn(message) { this.logs.push({ level: "warn", message }); console.warn(message); } error(message) { this.logs.push({ level: "error", message }); console.error(message); } getAll() { return this.logs; } clear() { this.logs = []; } } // Modify one of the test functions to override the machineGroup logger async function testCombinationIterations() { console.log('\nTesting Combination Iterations Logging...'); const machineGroupConfig = tester.createBaseMachineGroupConfig("TestCombinationIterations"); const mg = new MachineGroup(machineGroupConfig); // Override logger with a capturing logger const customLogger = new CapturingLogger(); mg.logger = customLogger; // Create one machine for simplicity (or two if you like) const machine = new Machine(tester.createBaseMachineConfig("TestMachineForCombo")); mg.childRegistrationUtils.registerChild(machine, "downstream"); machine.measurements.type("pressure").variant("measured").position("downstream").value(800); await machine.state.transitionToState("idle"); await machine.handleInput("parent", "execSequence", "startup"); // For testing, force dynamic totals so that combination search is exercised mg.dynamicTotals.flow = { min: 0, max: 200 }; // example totalling // Call handleFlowInput with a demand that requires iterations await mg.handleFlowInput(120); // After running, output captured iteration debug logs console.log("\n-- Captured Debug Logs for Combination Search Iterations --"); customLogger.getAll().forEach(log => { if(log.level === "debug") { console.log(log.message); } }); // Also output best result details if any needed for further improvement console.log("\n-- Final Output --"); const totalFlow = mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue(); console.log("Total Flow: ", totalFlow); // Get machine outputs by checking each machine's measurements const machineOutputs = {}; Object.entries(mg.machines).forEach(([id, machine]) => { const flow = machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue(); if (flow) machineOutputs[id] = flow; }); console.log("Machine Outputs: ", machineOutputs); } // Run the tests const tester = new MachineGroupTester(); tester.runAllTests().catch(console.error);