node-red-contrib-prib-functions
Version:
Node-RED added node functions.
401 lines (347 loc) • 11.9 kB
JavaScript
// Simple unit test for logistic regression node
const assert = require('assert');
const LogisticRegression = require('logisticegression');
// counters used by both runners
let testsPassed = 0;
let testsFailed = 0;
// If the CLI includes the word 'transform' proceed to run the
// transformation node tests rather than the logistic regression
// suite. This avoids pulling mocha back in while still exercising
// the existing mocha-based files under test/transform*.js.
if (process.argv.includes('transform')) {
runTransformTests();
return;
}
// If the CLI includes 'dataanalysis' run dataAnalysis tests
if (process.argv.includes('dataanalysis')) {
runDataAnalysisTests();
return;
}
// If the CLI includes 'columnar' run the columnar-format tests
if (process.argv.includes('columnar')) {
runColumnarTests();
return;
}
function test(description, fn) {
try {
fn();
console.log(`✓ PASS: ${description}`);
testsPassed++;
} catch (err) {
console.error(`✗ FAIL: ${description}`);
console.error(` Error: ${err.message}`);
testsFailed++;
}
}
// ---------------------------------------------------------------------------
// transform test emulation
// ---------------------------------------------------------------------------
function callMaybeAsync(fn) {
return new Promise((resolve, reject) => {
try {
if (fn.length >= 1) {
fn(err => (err ? reject(err) : resolve()));
} else {
Promise.resolve(fn()).then(resolve, reject);
}
} catch (err) {
reject(err);
}
});
}
async function runTransformTests() {
const path = require('path');
const fs = require('fs');
let beforeEachFns = [];
let afterEachFns = [];
let tests = [];
let currentDescribe = '';
global.describe = function(desc, fn) {
currentDescribe = desc;
fn();
};
global.beforeEach = function(fn) {
beforeEachFns.push(fn);
};
global.afterEach = function(fn) {
afterEachFns.push(fn);
};
global.it = function(desc, fn) {
const full = currentDescribe ? `${currentDescribe} ${desc}` : desc;
tests.push({desc: full, fn});
return { timeout: () => {} };
};
console.log("Transform Node Tests\n" + "=".repeat(60));
const dir = path.join(__dirname, 'test');
const files = fs.readdirSync(dir).filter(f => /^transform.*\.js$/.test(f));
files.forEach(f => require(path.join(dir, f)));
for (const t of tests) {
for (const bf of beforeEachFns) await callMaybeAsync(bf);
try {
await callMaybeAsync(t.fn);
console.log(`✓ PASS: ${t.desc}`);
testsPassed++;
} catch (err) {
console.error(`✗ FAIL: ${t.desc}`);
console.error(` Error: ${err.message}`);
testsFailed++;
}
for (const af of afterEachFns) {
try {
await callMaybeAsync(af);
} catch (err) {
// ignore server-not-running errors which happen when
// cleanup double-closes the Node-RED test helper server.
if (err && err.code === 'ERR_SERVER_NOT_RUNNING') {
// no-op
} else {
throw err;
}
}
}
}
console.log('\n' + '='.repeat(60));
console.log(`Tests passed: ${testsPassed}`);
console.log(`Tests failed: ${testsFailed}`);
console.log('='.repeat(60));
process.exit(testsFailed > 0 ? 1 : 0);
}
// ---------------------------------------------------------------------------
// dataanalysis test emulation
// ---------------------------------------------------------------------------
async function runDataAnalysisTests() {
const path = require('path');
const fs = require('fs');
let beforeEachFns = [];
let afterEachFns = [];
let tests = [];
let currentDescribe = '';
global.describe = function(desc, fn) {
currentDescribe = desc;
fn();
};
global.beforeEach = function(fn) {
beforeEachFns.push(fn);
};
global.afterEach = function(fn) {
afterEachFns.push(fn);
};
global.it = function(desc, fn) {
const full = currentDescribe ? `${currentDescribe} ${desc}` : desc;
tests.push({desc: full, fn});
return { timeout: () => {} };
};
console.log("Data Analysis Extension Tests\n" + "=".repeat(60));
const dir = path.join(__dirname, 'test');
const files = fs.readdirSync(dir).filter(f => /^dataAnalysisE.*\.js$/.test(f));
files.forEach(f => require(path.join(dir, f)));
for (const t of tests) {
for (const bf of beforeEachFns) await callMaybeAsync(bf);
try {
await callMaybeAsync(t.fn);
console.log(`✓ PASS: ${t.desc}`);
testsPassed++;
} catch (err) {
console.error(`✗ FAIL: ${t.desc}`);
console.error(` Error: ${err.message}`);
testsFailed++;
}
for (const af of afterEachFns) await callMaybeAsync(af);
}
console.log('\n' + '='.repeat(60));
console.log(`Tests passed: ${testsPassed}`);
console.log(`Tests failed: ${testsFailed}`);
console.log('='.repeat(60));
process.exit(testsFailed > 0 ? 1 : 0);
}
// ---------------------------------------------------------------------------
// columnar test emulation
// ---------------------------------------------------------------------------
async function runColumnarTests() {
const path = require('path');
const fs = require('fs');
let beforeEachFns = [];
let afterEachFns = [];
let tests = [];
let currentDescribe = '';
global.describe = function(desc, fn) {
currentDescribe = desc;
fn();
};
global.beforeEach = function(fn) {
beforeEachFns.push(fn);
};
global.afterEach = function(fn) {
afterEachFns.push(fn);
};
global.it = function(desc, fn) {
const full = currentDescribe ? `${currentDescribe} ${desc}` : desc;
tests.push({desc: full, fn});
return { timeout: () => {} };
};
console.log("Columnar File Tests\n" + "=".repeat(60));
const dir = path.join(__dirname, 'test');
const files = fs.readdirSync(dir).filter(f => /^columnar.*\.js$/.test(f));
files.forEach(f => require(path.join(dir, f)));
for (const t of tests) {
for (const bf of beforeEachFns) await callMaybeAsync(bf);
try {
await callMaybeAsync(t.fn);
console.log(`✓ PASS: ${t.desc}`);
testsPassed++;
} catch (err) {
console.error(`✗ FAIL: ${t.desc}`);
console.error(` Error: ${err.message}`);
testsFailed++;
}
for (const af of afterEachFns) await callMaybeAsync(af);
}
console.log('\n' + '='.repeat(60));
console.log(`Tests passed: ${testsPassed}`);
console.log(`Tests failed: ${testsFailed}`);
console.log('='.repeat(60));
process.exit(testsFailed > 0 ? 1 : 0);
}
console.log("Logistic Regression Unit Tests\n" + "=".repeat(60));
// Test 1: Model instantiation
test("Should create a LogisticRegression model instance", () => {
const model = new LogisticRegression({
learningRate: 0.1,
iterations: 100
});
assert(model instanceof LogisticRegression);
assert.strictEqual(model.learningRate, 0.1);
assert.strictEqual(model.iterations, 100);
});
// Test 2: Model fit
test("Should fit a model with training data", () => {
const model = new LogisticRegression({
learningRate: 0.1,
iterations: 100
});
const X = [[0, 0], [0, 1], [1, 0], [1, 1]];
const y = [0, 1, 1, 1];
const result = model.fit(X, y);
assert(result instanceof LogisticRegression);
assert(model.weights !== null);
assert.strictEqual(model.weights.length, 3); // 2 features + 1 intercept
});
// Test 3: Model predict
test("Should predict class labels", () => {
const model = new LogisticRegression({
learningRate: 0.1,
iterations: 100
});
const X = [[0, 0], [0, 1], [1, 0], [1, 1]];
const y = [0, 1, 1, 1];
model.fit(X, y);
const predictions = model.predict([[0, 0], [1, 1], [0, 1]]);
assert(Array.isArray(predictions));
assert.strictEqual(predictions.length, 3);
predictions.forEach(pred => {
assert([0, 1].includes(pred));
});
});
// Test 4: Model predictProba
test("Should predict probabilities", () => {
const model = new LogisticRegression({
learningRate: 0.1,
iterations: 100
});
const X = [[0, 0], [0, 1], [1, 0], [1, 1]];
const y = [0, 1, 1, 1];
model.fit(X, y);
const probs = model.predictProba([[0, 0], [1, 1], [0.5, 0.5]]);
assert(Array.isArray(probs));
assert.strictEqual(probs.length, 3);
probs.forEach(prob => {
assert(typeof prob === 'number');
assert(prob >= 0 && prob <= 1);
});
});
// Test 5: Model decision function
test("Should compute decision function (logits)", () => {
const model = new LogisticRegression({
learningRate: 0.1,
iterations: 100
});
const X = [[0, 0], [0, 1], [1, 0], [1, 1]];
const y = [0, 1, 1, 1];
model.fit(X, y);
const decisions = model.decisionFunction([[0, 0], [1, 1]]);
assert(Array.isArray(decisions));
assert.strictEqual(decisions.length, 2);
decisions.forEach(d => {
assert(typeof d === 'number');
});
});
// Test 6: Fit without data
test("Should throw error when fitting without data", () => {
const model = new LogisticRegression();
try {
model.fit([], []);
throw new Error("Should have thrown an error");
} catch (err) {
assert(err.message.includes("non-empty"));
}
});
// Test 7: Mismatched dimensions
test("Should throw error when X and y have different lengths", () => {
const model = new LogisticRegression();
try {
model.fit([[0, 0], [1, 1]], [0, 1, 1]);
throw new Error("Should have thrown an error");
} catch (err) {
assert(err.message.includes("same number"));
}
});
// Test 8: Predict before fit
test("Should throw error when predicting before fitting", () => {
const model = new LogisticRegression();
try {
model.predict([[0, 0]]);
throw new Error("Should have thrown an error");
} catch (err) {
assert(err.message.includes("not fitted"));
}
});
// Test 9: L2 regularization
test("Should support L2 regularization", () => {
const model = new LogisticRegression({
learningRate: 0.1,
iterations: 100,
l2: 0.01
});
const X = [[0, 0], [0, 1], [1, 0], [1, 1]];
const y = [0, 1, 1, 1];
model.fit(X, y);
const predictions = model.predict([[0.5, 0.5]]);
assert(Array.isArray(predictions));
assert.strictEqual(predictions.length, 1);
});
// Test 10: Custom threshold
test("Should use custom threshold for prediction", () => {
const model = new LogisticRegression({
learningRate: 0.1,
iterations: 100
});
const X = [[0, 0], [0, 1], [1, 0], [1, 1]];
const y = [0, 1, 1, 1];
model.fit(X, y);
const predictions = model.predict([[0.5, 0.5]], 0.7);
assert(Array.isArray(predictions));
assert([0, 1].includes(predictions[0]));
});
// Summary
console.log("\n" + "=".repeat(60));
console.log(`Tests passed: ${testsPassed}`);
console.log(`Tests failed: ${testsFailed}`);
console.log("=".repeat(60));
if (testsFailed > 0) {
console.log("\nSome tests failed!");
process.exit(1);
} else {
console.log("\nAll tests passed! ✓");
process.exit(0);
}