From 82730dda1d77245119ff26e969aef8f9a7358f4c Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Thu, 15 Apr 2021 09:43:55 -0400 Subject: [PATCH] add full nodejs test coverage --- CHANGELOG.md | 5 ++-- TODO.md | 2 +- package.json | 10 +++---- test/README.md | 5 ++-- test/test-main.js | 70 ++++++++++++++++++++++++----------------------- test/test.js | 69 ++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 117 insertions(+), 44 deletions(-) create mode 100644 test/test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index d8b417b0..239f96fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # @vladmandic/human -Version: **1.5.1** +Version: **1.5.2** Description: **Human: AI-powered 3D Face Detection & Rotation Tracking, Face Description & Recognition, Body Pose Tracking, 3D Hand & Finger Tracking, Iris Analysis, Age & Gender & Emotion Prediction, Gesture Recognition** Author: **Vladimir Mandic ** @@ -9,8 +9,9 @@ Repository: **** ## Changelog -### **HEAD -> main** 2021/04/13 mandic00@live.com +### **1.5.2** 2021/04/14 mandic00@live.com +- experimental node-wasm support ### **1.5.1** 2021/04/13 mandic00@live.com diff --git a/TODO.md b/TODO.md index e6c18058..caaf2161 100644 --- a/TODO.md +++ b/TODO.md @@ -2,7 +2,7 @@ ## Big Ticket Items -- Improve automated testing framework +- N/A ## Explore Models diff --git a/package.json b/package.json index a7a2e108..9c3254bd 100644 --- a/package.json +++ b/package.json @@ -21,11 +21,11 @@ "url": "git+https://github.com/vladmandic/human.git" }, "scripts": { - "start": "node --trace-warnings --unhandled-rejections=strict --trace-uncaught --no-deprecation demo/node.js", + "start": "node --trace-warnings --unhandled-rejections=strict --trace-uncaught demo/node.js", "dev": "node --trace-warnings --unhandled-rejections=strict --trace-uncaught server/serve.js", "build": "rimraf dist/* typedoc/* types/* && node --trace-warnings --unhandled-rejections=strict --trace-uncaught server/build.js", "lint": "eslint src server demo test", - "test": "node --trace-warnings --unhandled-rejections=strict --trace-uncaught --no-deprecation test/test-node.js" + "test": "node --trace-warnings --unhandled-rejections=strict --trace-uncaught test/test.js" }, "keywords": [ "human", @@ -61,14 +61,14 @@ "@tensorflow/tfjs-layers": "^3.4.0", "@tensorflow/tfjs-node": "^3.4.0", "@tensorflow/tfjs-node-gpu": "^3.4.0", - "@types/node": "^14.14.37", + "@types/node": "^14.14.39", "@typescript-eslint/eslint-plugin": "^4.22.0", "@typescript-eslint/parser": "^4.22.0", - "@vladmandic/pilogger": "^0.2.16", + "@vladmandic/pilogger": "^0.2.17", "canvas": "^2.7.0", "chokidar": "^3.5.1", "dayjs": "^1.10.4", - "esbuild": "^0.11.10", + "esbuild": "^0.11.11", "eslint": "^7.24.0", "eslint-config-airbnb-base": "^14.2.1", "eslint-plugin-import": "^2.22.1", diff --git a/test/README.md b/test/README.md index 74922714..3baf1784 100644 --- a/test/README.md +++ b/test/README.md @@ -4,22 +4,23 @@ ### NodeJS using TensorFlow library +- Image filters are disabled due to lack of Canvas and WeBGL access - Face rotation is disabled for `NodeJS` platform: `Kernel 'RotateWithOffset' not registered for backend 'tensorflow'` -- Image filters are disabled due to lack of Canvas and WeBGL access ### NodeJS with GPU acceleation using CUDA +- Image filters are disabled due to lack of Canvas and WeBGL access - Face rotation is disabled for `NodeJS` platform: `Kernel 'RotateWithOffset' not registered for backend 'tensorflow'` -- Image filters are disabled due to lack of Canvas and WeBGL access ### NodeJS using WASM - Requires dev http server See +- Image filters are disabled due to lack of Canvas and WeBGL access - Only supported input is Tensor due to missing image decoders - Warmup returns null and is marked as failed Missing native Image implementation in NodeJS diff --git a/test/test-main.js b/test/test-main.js index c447695a..165a4c7c 100644 --- a/test/test-main.js +++ b/test/test-main.js @@ -1,23 +1,25 @@ const process = require('process'); -const path = require('path'); -const log = require('@vladmandic/pilogger'); const canvasJS = require('canvas'); const fetch = require('node-fetch').default; let config; -log.info('test:', path.basename(process.argv[1])); + +const log = (status, ...data) => { + if (typeof process.send !== 'undefined') process.send([status, data]); + else process.stdout.write(JSON.stringify(data)); +}; async function testHTTP() { if (config.modelBasePath.startsWith('file:')) return true; return new Promise((resolve) => { fetch(config.modelBasePath) .then((res) => { - if (res && res.ok) log.state('passed: model server:', config.modelBasePath); - else log.error('failed: model server:', config.modelBasePath); + if (res && res.ok) log('state', 'passed: model server:', config.modelBasePath); + else log('error', 'failed: model server:', config.modelBasePath); resolve(res && res.ok); }) .catch((err) => { - log.error('failed: model server:', err.message); + log('error', 'failed: model server:', err.message); resolve(false); }); }); @@ -28,7 +30,7 @@ async function getImage(human, input) { try { img = await canvasJS.loadImage(input); } catch (err) { - log.error('failed: load image', input, err.message); + log('error', 'failed: load image', input, err.message); return img; } const canvas = canvasJS.createCanvas(img.width, img.height); @@ -42,8 +44,8 @@ async function getImage(human, input) { const reshape = human.tf.reshape(rgb, [1, canvas.width, canvas.height, 3]); // move extra dim from the end of tensor and use it as batch number instead return reshape; }); - if (res && res.shape[0] === 1 && res.shape[3] === 3) log.state('passed: load image:', input, res.shape); - else log.error('failed: load image:', input, res); + if (res && res.shape[0] === 1 && res.shape[3] === 3) log('state', 'passed: load image:', input, res.shape); + else log('error', 'failed: load image:', input, res); return res; } @@ -51,31 +53,31 @@ function printResults(detect) { const person = (detect.face && detect.face[0]) ? { confidence: detect.face[0].confidence, age: detect.face[0].age, gender: detect.face[0].gender } : {}; const object = (detect.object && detect.object[0]) ? { score: detect.object[0].score, class: detect.object[0].label } : {}; const body = (detect.body && detect.body[0]) ? { score: detect.body[0].score, keypoints: detect.body[0].keypoints.length } : {}; - if (detect.face) log.data(' result: face:', detect.face?.length, 'body:', detect.body?.length, 'hand:', detect.hand?.length, 'gesture:', detect.gesture?.length, 'object:', detect.object?.length, person, object, body); - if (detect.performance) log.data(' result: performance:', 'load:', detect?.performance.load, 'total:', detect.performance?.total); + if (detect.face) log('data', ' result: face:', detect.face?.length, 'body:', detect.body?.length, 'hand:', detect.hand?.length, 'gesture:', detect.gesture?.length, 'object:', detect.object?.length, person, object, body); + if (detect.performance) log('data', ' result: performance:', 'load:', detect?.performance.load, 'total:', detect.performance?.total); } async function testInstance(human) { - if (human) log.state('passed: create human'); - else log.error('failed: create human'); + if (human) log('state', 'passed: create human'); + else log('error', 'failed: create human'); // if (!human.tf) human.tf = tf; - log.info('human version:', human.version); - log.info('platform:', human.sysinfo.platform, 'agent:', human.sysinfo.agent); - log.info('tfjs version:', human.tf.version.tfjs); + log('info', 'human version:', human.version); + log('info', 'platform:', human.sysinfo.platform, 'agent:', human.sysinfo.agent); + log('info', 'tfjs version:', human.tf.version.tfjs); await human.load(); - if (config.backend === human.tf.getBackend()) log.state('passed: set backend:', config.backend); - else log.error('failed: set backend:', config.backend); + if (config.backend === human.tf.getBackend()) log('state', 'passed: set backend:', config.backend); + else log('error', 'failed: set backend:', config.backend); if (human.models) { - log.state('passed: load models'); + log('state', 'passed: load models'); const keys = Object.keys(human.models); const loaded = keys.filter((model) => human.models[model]); - log.data(' result: defined models:', keys.length, 'loaded models:', loaded.length); + log('state', ' result: defined models:', keys.length, 'loaded models:', loaded.length); return true; } - log.error('failed: load models'); + log('error', 'failed: load models'); return false; } @@ -84,36 +86,36 @@ async function testWarmup(human, title) { try { warmup = await human.warmup(config); } catch (err) { - log.error('error warmup'); + log('error', 'error warmup'); } if (warmup) { - log.state('passed: warmup:', config.warmup, title); + log('state', 'passed: warmup:', config.warmup, title); printResults(warmup); return true; } - log.error('failed: warmup:', config.warmup, title); + log('error', 'failed: warmup:', config.warmup, title); return false; } async function testDetect(human, input, title) { const image = input ? await getImage(human, input) : human.tf.randomNormal([1, 1024, 1024, 3]); if (!image) { - log.error('failed: detect: input is null'); + log('error', 'failed: detect: input is null'); return false; } let detect; try { detect = await human.detect(image, config); } catch (err) { - log.error('error: detect', err); + log('error', 'error: detect', err); } if (image instanceof human.tf.Tensor) human.tf.dispose(image); if (detect) { - log.state('passed: detect:', input || 'random', title); + log('state', 'passed: detect:', input || 'random', title); printResults(detect); return true; } - log.error('failed: detect', input || 'random', title); + log('error', 'failed: detect', input || 'random', title); return false; } @@ -121,7 +123,7 @@ async function test(Human, inputConfig) { config = inputConfig; const ok = await testHTTP(); if (!ok) { - log.warn('aborting test'); + log('error', 'aborting test'); return; } const t0 = process.hrtime.bigint(); @@ -134,7 +136,7 @@ async function test(Human, inputConfig) { config.warmup = 'body'; await testWarmup(human, 'default'); - log.info('test body variants'); + log('info', 'test body variants'); config.body = { modelPath: 'posenet.json', enabled: true }; await testDetect(human, 'assets/human-sample-body.jpg', 'posenet'); config.body = { modelPath: 'efficientpose.json', enabled: true }; @@ -143,12 +145,12 @@ async function test(Human, inputConfig) { await testDetect(human, 'assets/human-sample-body.jpg', 'blazepose'); await testDetect(human, null, 'default'); - log.info('test: first instance'); + log('info', 'test: first instance'); await testDetect(human, 'assets/sample-me.jpg', 'default'); - log.info('test: second instance'); + log('info', 'test: second instance'); const second = new Human(config); await testDetect(second, 'assets/sample-me.jpg', 'default'); - log.info('test: concurrent'); + log('info', 'test: concurrent'); await Promise.all([ testDetect(human, 'assets/human-sample-face.jpg', 'default'), testDetect(second, 'assets/human-sample-face.jpg', 'default'), @@ -156,7 +158,7 @@ async function test(Human, inputConfig) { testDetect(second, 'assets/human-sample-body.jpg', 'default'), ]); const t1 = process.hrtime.bigint(); - log.info('test complete:', Math.trunc(parseInt((t1 - t0).toString()) / 1000 / 1000), 'ms'); + log('info', 'test complete:', Math.trunc(parseInt((t1 - t0).toString()) / 1000 / 1000), 'ms'); } exports.test = test; diff --git a/test/test.js b/test/test.js new file mode 100644 index 00000000..78da5574 --- /dev/null +++ b/test/test.js @@ -0,0 +1,69 @@ +const path = require('path'); +const process = require('process'); +const { fork } = require('child_process'); +const log = require('@vladmandic/pilogger'); + +const tests = [ + 'test-node.js', + 'test-node-gpu.js', + 'test-node-wasm.js', +]; + +const ignore = [ + 'cpu_feature_guard.cc', + 'rebuild TensorFlow', + 'xla_gpu_device.cc', + 'cudart_stub.cc', + 'cuda_driver.cc:326', + 'cpu_allocator_impl.cc', +]; + +const status = { + passed: 0, + failed: 0, +}; + +function logMessage(test, data) { + log[data[0]](test, ...data[1]); + if (data[1][0].startsWith('passed')) status.passed++; + if (data[1][0].startsWith('failed')) status.failed++; +} + +function logStdIO(ok, test, buffer) { + const lines = buffer.toString().split(/\r\n|\n\r|\n|\r/); + const filtered = lines.filter((line) => { + for (const ignoreString of ignore) { + if (line.includes(ignoreString)) return false; + } + return true; + }); + for (const line of filtered) { + if (line.length < 2) continue; + if (ok) log.data(test, 'stdout:', line); + else log.warn(test, 'stderr:', line); + } +} + +async function runTest(test) { + return new Promise((resolve) => { + log.info(test, 'start'); + const child = fork(path.join(__dirname, test), [], { silent: true }); + child.on('message', (data) => logMessage(test, data)); + child.on('error', (data) => log.error(test, ':', data.message || data)); + child.on('close', (code) => resolve(code)); + child.stdout?.on('data', (data) => logStdIO(true, test, data)); + child.stderr?.on('data', (data) => logStdIO(false, test, data)); + }); +} + +async function testAll() { + log.logFile(path.join(__dirname, 'test.log')); + log.header(); + process.on('unhandledRejection', (data) => log.error('nodejs unhandled rejection', data)); + process.on('uncaughtException', (data) => log.error('nodejs unhandled exception', data)); + log.info('tests:', tests); + for (const test of tests) await runTest(test); + log.info('status:', status); +} + +testAll();