test: major improvements

This commit is contained in:
cha0s 2024-02-05 17:08:26 -06:00
parent 393e8d684d
commit 2289f27b97
36 changed files with 2351 additions and 538 deletions

2418
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -14,7 +14,7 @@
"dox:serve": "flecks dox docusaurus && cd website && DOCUSAURUS_GENERATED_FILES_DIR_NAME=node_modules/.cache/docusaurus node_modules/.bin/docusaurus build --no-minify --out-dir ../dox-tmp && node_modules/.bin/docusaurus serve --dir ../dox-tmp",
"dox": "flecks dox docusaurus && cd website && DOCUSAURUS_GENERATED_FILES_DIR_NAME=node_modules/.cache/docusaurus node_modules/.bin/docusaurus",
"lint": "node build/tasks lint",
"test": "node build/tasks test"
"test": "node build/tasks -- test -t 60000"
},
"devDependencies": {
"husky": "^9.0.7",

View File

@ -52,16 +52,7 @@ module.exports = class Build extends Flecks {
}) {
const cleanConfig = JSON.parse(JSON.stringify(originalConfig));
// Dealias the config keys.
const dealiasedConfig = Object.fromEntries(
Object.entries(cleanConfig)
.map(([maybeAliasedPath, config]) => {
const index = maybeAliasedPath.indexOf(':');
return [
-1 === index ? maybeAliasedPath : maybeAliasedPath.slice(0, index),
config,
];
}),
);
const dealiasedConfig = this.dealiasedConfig(cleanConfig);
const resolver = new Resolver({root});
const {paths, roots} = await explicate({
paths: Object.keys(originalConfig),

View File

@ -22,7 +22,7 @@ const {
spawnWith,
} = require('@flecks/core/src/server');
const {glob} = require('glob');
const rimraf = require('rimraf');
const {rimraf} = require('rimraf');
const addPathsToYml = require('./add-paths-to-yml');

View File

@ -41,8 +41,8 @@ module.exports = async function explicate(
let realResolvedCandidate = await realpath(resolvedCandidate);
const isSymlink = resolvedCandidate !== realResolvedCandidate;
if (isSymlink) {
if (realResolvedCandidate.endsWith('/dist')) {
realResolvedCandidate = realResolvedCandidate.slice(0, -5);
if (realResolvedCandidate.endsWith('/dist/fleck')) {
realResolvedCandidate = realResolvedCandidate.slice(0, -11);
}
}
// Aliased? Include submodules.

View File

@ -37,6 +37,7 @@ const resolveValidModulePath = (source) => (path) => {
module.exports = async (env, argv, flecks) => {
const config = await configFn(env, argv, flecks);
config.externals = await externals();
config.output.path = join(FLECKS_CORE_ROOT, 'dist', 'fleck');
config.plugins.push(
new CopyPlugin({
patterns: [

View File

@ -1,4 +1,10 @@
const {join} = require('path');
const {
basename,
dirname,
extname,
join,
relative,
} = require('path');
const {glob} = require('glob');
@ -13,7 +19,7 @@ const tests = join(FLECKS_CORE_ROOT, 'test');
module.exports = async (env, argv, flecks) => {
const config = await configFn(env, argv, flecks);
config.output.chunkFormat = false;
config.output.clean = false;
config.output.path = join(FLECKS_CORE_ROOT, 'dist', 'test');
// Test entry.
const testPaths = await glob(join(tests, '*.js'));
const {platforms} = flecks;
@ -22,7 +28,13 @@ module.exports = async (env, argv, flecks) => {
.flat(),
);
if (testPaths.length > 0) {
config.entry.test = ['source-map-support/register', ...testPaths];
testPaths.forEach((path) => {
const entry = relative(tests, path);
config.entry[join(dirname(entry), basename(entry, extname(entry)))] = [
'source-map-support/register',
path,
];
});
}
return config;
};

View File

@ -97,7 +97,7 @@ exports.executable = () => (
compiler.hooks.afterEmit.tapPromise(
'Executable',
async () => (
chmod(join(FLECKS_CORE_ROOT, 'dist', 'build', 'cli.js'), 0o755)
chmod(join(FLECKS_CORE_ROOT, 'dist', 'fleck', 'build', 'cli.js'), 0o755)
),
);
}

View File

@ -11,8 +11,8 @@
"clean": "rm -rf dist node_modules yarn.lock",
"lint": "eslint --config ./build/eslint.config.js .",
"postversion": "npm run build",
"test": "webpack --config ./build/build.test.webpack.config.js --mode production && mocha --colors ./dist/test.js --timeout 10000",
"test:watch": "webpack watch --config ./build/build.test.webpack.config.js --mode development & mocha --parallel --watch --watch-files ./dist/test.js --colors ./dist/test.js --timeout 10000"
"test": "webpack --config ./build/build.test.webpack.config.js --mode production && mocha --colors --parallel ./dist/test/*.js ./dist/test/server/*.js",
"test:watch": "webpack watch --config ./build/build.test.webpack.config.js --mode development & mocha --colors --parallel --watch --watch-files ./dist/test/*.js ./dist/test/server/*.js"
},
"bin": {
"flecks": "./build/cli.js"
@ -51,7 +51,7 @@
"graceful-fs": "^4.2.11",
"js-yaml": "4.1.0",
"mocha": "^10.2.0",
"rimraf": "^3.0.2",
"rimraf": "^5.0.5",
"source-map-loader": "4.0.1",
"source-map-support": "0.5.19",
"webpack": "^5.89.0",

View File

@ -5,6 +5,7 @@ const {dump: dumpYml, load: loadYml} = require('js-yaml');
module.exports = {
dumpYml,
loadYml,
rimraf: require('rimraf').rimraf,
webpack: require('webpack'),
...require('../build/webpack'),
};

View File

@ -45,12 +45,10 @@ it('configures from environment', async () => {
});
it('dealiases config', async () => {
expect(await Build.buildRuntime({
originalConfig: {'two:./two': {foo: 2}},
platforms: [],
root: buildRoot,
}))
.to.nested.include({
'runtime.config.two.foo': 2,
expect(await Build.dealiasedConfig({'two:./two': {foo: 2}}))
.to.deep.equal({
two: {
foo: 2,
},
});
});

View File

@ -1,7 +1,7 @@
import {readFile, writeFile} from 'fs/promises';
import {load as loadYml} from 'js-yaml';
import addPathsToYml from '@flecks/build/build/add-paths-to-yml';
import {expect} from 'chai';
import {readFile, writeFile} from 'fs/promises';
import {load as loadYml} from 'js-yaml';
it('can add paths to YML', async () => {
await writeFile(

View File

@ -134,6 +134,25 @@ class Flecks {
return join(parts.join('-'), basename(path, extname(path)));
}
/**
* Dealias a configuration object.
*
* @param {Object} config Configuration.
* @returns {Object}
*/
static dealiasedConfig(config) {
return Object.fromEntries(
Object.entries(config)
.map(([maybeAliasedPath, config]) => {
const index = maybeAliasedPath.indexOf(':');
return [
-1 === index ? maybeAliasedPath : maybeAliasedPath.slice(0, index),
config,
];
}),
);
}
/**
* Generate a decorator from a require context.
*

View File

@ -8,8 +8,8 @@
"clean": "rm -rf dist node_modules yarn.lock",
"lint": "eslint --config ./build/core.eslint.config.js .",
"postversion": "npm run build",
"test": "webpack --config ./build/test.webpack.config.js --mode production && mocha --colors ./dist/test.js",
"test:watch": "webpack watch --config ./build/test.webpack.config.js --mode development & mocha --parallel --watch --watch-files ./dist/test.js --colors ./dist/test.js"
"test": "webpack --config ./build/test.webpack.config.js --mode production && mocha --colors --parallel ./dist/test/*.js ./dist/test/server/*.js",
"test:watch": "webpack watch --config ./build/test.webpack.config.js --mode development & mocha --colors --parallel --watch --watch-files ./dist/test/*.js ./dist/test/server/*.js"
},
"repository": {
"type": "git",
@ -54,7 +54,7 @@
"eslint-webpack-plugin": "^3.2.0",
"globals": "^13.23.0",
"mocha": "^10.2.0",
"rimraf": "^3.0.2",
"rimraf": "^5.0.5",
"source-map-loader": "4.0.1",
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4",

View File

@ -11,7 +11,7 @@ exports.generateDockerFile = async (flecks) => {
'ENV DEBUG=*',
'ENV NODE_ENV=production',
'',
'CMD ["node", "./dist/server/index.js"]',
'CMD ["node", "dist/server"]',
'',
'VOLUME /var/www/node_modules',
'',

View File

@ -1,7 +1,8 @@
const {stat, unlink} = require('fs/promises');
const {join} = require('path');
const {access} = require('fs/promises');
const {join, relative} = require('path');
const {commands: coreCommands} = require('@flecks/build/build/commands');
const {rimraf} = require('@flecks/build/src/server');
const {D} = require('@flecks/core/src');
const {glob} = require('@flecks/core/src/server');
const Mocha = require('mocha');
@ -18,6 +19,7 @@ module.exports = (program, flecks) => {
commands.test = {
options: [
program.createOption('-d, --no-production', 'dev build'),
program.createOption('-t, --timeout <ms>', 'timeout').default(2000),
program.createOption('-w, --watch', 'watch for changes'),
program.createOption('-v, --verbose', 'verbose output'),
],
@ -28,22 +30,19 @@ module.exports = (program, flecks) => {
].join('\n'),
action: async (opts) => {
const {
timeout,
watch,
} = opts;
const {build} = coreCommands(program, flecks);
const testPaths = await glob(join(FLECKS_CORE_ROOT, 'test/**/*.js'));
if (0 === testPaths.length) {
const tests = await glob(join(FLECKS_CORE_ROOT, 'test', '*.js'));
const serverTests = await glob(join(FLECKS_CORE_ROOT, 'test', 'server', '*.js'));
if (0 === tests.length + serverTests.length) {
// eslint-disable-next-line no-console
console.log('No tests found.');
return undefined;
}
// Remove the previous test.
const testLocation = join(FLECKS_CORE_ROOT, 'dist', 'test.js');
try {
await unlink(testLocation);
}
// eslint-disable-next-line no-empty
catch (error) {}
await rimraf(join(FLECKS_CORE_ROOT, 'dist', 'test'));
// Kick off building the test and wait for the file to exist.
await build.action('test', opts);
debug('Testing...', opts);
@ -51,7 +50,7 @@ module.exports = (program, flecks) => {
while (true) {
try {
// eslint-disable-next-line no-await-in-loop
await stat(testLocation);
await access(join(FLECKS_CORE_ROOT, 'dist', 'test'));
break;
}
catch (error) {
@ -69,13 +68,25 @@ module.exports = (program, flecks) => {
},
flecks.stubs,
);
const mocha = new Mocha({parallel: true});
const mocha = new Mocha({parallel: true, timeout});
mocha.ui('bdd');
const files = []
.concat(tests, serverTests)
.map((path) => join('dist', relative(FLECKS_CORE_ROOT, path)));
if (watch) {
watchParallelRun(mocha, {watchFiles: [testLocation]}, {file: [testLocation], spec: []});
watchParallelRun(
mocha,
{
watchFiles: files,
},
{
file: files,
spec: [],
},
);
return new Promise(() => {});
}
mocha.files = [testLocation];
mocha.files = files;
return new Promise((r, e) => {
mocha.run((code) => {
if (!code) {

View File

@ -0,0 +1,19 @@
const {copy} = require('@flecks/build/src/server');
const configFn = require('@flecks/fleck/build/fleck.webpack.config');
module.exports = async (env, argv, flecks) => {
const config = await configFn(env, argv, flecks);
delete config.entry.entry;
config.plugins.push(
copy({
patterns: [
{
from: 'src/entry.js',
to: 'entry.js',
info: { minimized: true },
},
],
}),
);
return config;
};

View File

@ -4,6 +4,8 @@ const D = require('@flecks/core/build/debug');
const debug = D('@flecks/server/build/runtime');
const {version} = require('../package.json');
module.exports = async (config, env, argv, flecks) => {
const runtimePath = await flecks.resolver.resolve('@flecks/server/runtime');
// Inject flecks configuration.
@ -38,6 +40,7 @@ module.exports = async (config, env, argv, flecks) => {
' )',
')',
].join('\n'),
version: JSON.stringify(version),
...await flecks.invokeAsync('@flecks/server.runtime'),
};
const runtimeString = `{${
@ -100,7 +103,7 @@ module.exports = async (config, env, argv, flecks) => {
flecks.stubs.forEach((stub) => {
config.resolve.alias[stub] = false;
});
await flecks.runtimeCompiler('server', config);
await flecks.runtimeCompiler('server', config, env, argv);
// Rewrite to signals for HMR.
if ('production' !== argv.mode) {
allowlist.push(/^webpack\/hot\/signal/);

View File

@ -58,6 +58,7 @@ module.exports = async (env, argv, flecks) => {
NODE_PRESERVE_SYMLINKS: flecks.roots.some(([path, request]) => path !== request) ? 1 : 0,
},
exec: 'index.js',
killOnExit: !!hot,
// Bail hard on unhandled rejections and report.
nodeArgs: [...nodeArgs, '--unhandled-rejections=strict', '--trace-uncaught'],
// HMR.

View File

@ -21,13 +21,17 @@ class StartServerPlugin {
apply(compiler) {
const {options: {exec, signal}, pluginName} = this;
const logger = compiler.getInfrastructureLogger(pluginName);
compiler.hooks.afterEmit.tapAsync(pluginName, (compilation, callback) => {
compiler.hooks.afterEmit.tapPromise(pluginName, async (compilation) => {
if (this.worker && this.worker.isConnected()) {
if (signal) {
process.kill(this.worker.process.pid, true === signal ? 'SIGUSR2' : signal);
this.worker.kill(true === signal ? 'SIGUSR2' : signal);
return undefined;
}
callback();
return;
const promise = new Promise((resolve) => {
this.worker.on('disconnect', resolve);
});
this.worker.disconnect();
await promise;
}
let entryPoint;
if (!exec) {
@ -42,7 +46,7 @@ class StartServerPlugin {
else {
entryPoint = exec(compilation);
}
this.startServer(join(compiler.options.output.path, entryPoint), callback);
return this.startServer(join(compiler.options.output.path, entryPoint));
});
compiler.hooks.shouldEmit.tap(pluginName, (compilation) => {
const entryPoints = Object.keys(compilation.assets);
@ -66,7 +70,7 @@ class StartServerPlugin {
return parseInt(port, 10);
}
startServer(exec, callback) {
async startServer(exec) {
const {
args,
env,
@ -81,13 +85,16 @@ class StartServerPlugin {
args,
...(inspectPort && {inspectPort}),
});
cluster.on('online', () => callback());
this.worker = cluster.fork(env);
if (killOnExit) {
this.worker.on('exit', () => {
process.exit();
});
}
return new Promise((resolve, reject) => {
this.worker.on('error', reject);
this.worker.on('online', resolve);
});
}
}

View File

@ -4,11 +4,10 @@ import {join} from 'path';
import {D, Flecks} from '@flecks/core';
const {version} = require('../package.json');
(async () => {
const runtime = await __non_webpack_require__('@flecks/server/runtime');
const {loadFlecks} = runtime;
// eslint-disable-next-line import/no-extraneous-dependencies
const runtime = await import('@flecks/server/runtime');
const {loadFlecks, version} = runtime;
// eslint-disable-next-line no-console
console.log(`flecks server v${version}`);
try {
@ -27,6 +26,7 @@ const {version} = require('../package.json');
debug('up!');
}
catch (error) {
debug(error);
// eslint-disable-next-line no-console
console.error(error);
}
})();

View File

@ -0,0 +1,21 @@
import {access} from 'fs/promises';
import {join} from 'path';
import {expect} from 'chai';
import {createApplicationAt, build} from './build/build';
it('builds for development', async () => {
const path = await createApplicationAt('development');
await build(path, {args: ['-d']});
let artifact;
try {
await access(join(path, 'dist', 'server', 'index.js'));
artifact = true;
}
catch (error) {
artifact = false;
}
expect(artifact)
.to.be.true;
});

View File

@ -0,0 +1,21 @@
import {access} from 'fs/promises';
import {join} from 'path';
import {expect} from 'chai';
import {createApplicationAt, build} from './build/build';
it('builds for production', async () => {
const path = await createApplicationAt('production');
await build(path);
let artifact;
try {
await access(join(path, 'dist', 'server', 'index.js'));
artifact = true;
}
catch (error) {
artifact = false;
}
expect(artifact)
.to.be.true;
});

View File

@ -0,0 +1,69 @@
import {cp, mkdir} from 'fs/promises';
import {join} from 'path';
import {rimraf} from '@flecks/build/server';
import {processCode, spawnWith} from '@flecks/core/server';
import {listen} from './listen';
const {
FLECKS_CORE_ROOT = process.cwd(),
} = process.env;
export const applications = join(FLECKS_CORE_ROOT, 'node_modules', '.cache', '@flecks', 'server');
export const template = join(FLECKS_CORE_ROOT, 'test', 'server', 'template');
export async function createApplicationAt(path) {
await rimraf(join(applications, path));
await mkdir(join(applications, path), {recursive: true});
const qualified = join(applications, path, process.pid.toString());
await cp(template, qualified, {recursive: true});
return qualified;
}
export function build(path, {args = [], opts = {}} = {}) {
return processCode(spawnWith(
['npx', 'flecks', 'build', ...args],
{
...opts,
env: {
FLECKS_ENV__flecks_server__stats: '{"all": false}',
FLECKS_ENV__flecks_server__start: 0,
FLECKS_CORE_ROOT: path,
...opts.env,
},
},
));
}
export async function serverActions(path, actions) {
const {connected, listening, path: socketPath} = await listen();
await listening;
const server = spawnWith(
['node', join(path, 'dist', 'server')],
{
env: {
FLECKS_SERVER_TEST_SOCKET: socketPath,
},
},
);
const [code, results] = await Promise.all([
processCode(server),
connected.then(async (socket) => {
const results = [];
await actions.reduce(
(p, action) => (
p.then(() => (
socket.send(action)
.then((result) => {
results.push(result);
})
))
),
Promise.resolve(),
);
return results;
}),
]);
return {code, results};
}

View File

@ -0,0 +1,7 @@
import {randomBytes} from 'crypto';
export default function id() {
return new Promise((resolve, reject) => {
randomBytes(16, (error, bytes) => (error ? reject(error) : resolve(bytes.toString('hex'))));
});
}

View File

@ -0,0 +1,56 @@
import {mkdir} from 'fs/promises';
import {createServer} from 'net';
import {tmpdir} from 'os';
import {dirname, join} from 'path';
import id from './id';
class SocketWrapper {
constructor(socket) {
this.socket = socket;
}
async send(action) {
const unique = await id();
return new Promise((resolve, reject) => {
const onData = (data) => {
const action = JSON.parse(data.toString());
if (unique === action.meta.id) {
this.socket.off('error', reject);
this.socket.off('data', onData);
resolve(action);
}
};
this.socket.on('close', () => {
this.socket.off('error', reject);
this.socket.off('data', onData);
resolve();
});
this.socket.on('error', reject);
this.socket.on('data', onData);
this.socket.write(JSON.stringify({...action, meta: {...action.meta, id: unique}}));
});
}
}
export async function listen() {
const path = join(tmpdir(), 'flecks', 'ci', await id());
await mkdir(dirname(path), {recursive: true});
const server = createServer();
server.listen(path);
return {
connected: new Promise((resolve, reject) => {
server.on('error', reject);
server.on('connection', (socket) => {
resolve(new SocketWrapper(socket));
});
}),
listening: new Promise((resolve, reject) => {
server.on('error', reject);
server.on('listening', resolve);
}),
path,
};
}

View File

@ -0,0 +1,14 @@
import {expect} from 'chai';
import {build, createApplicationAt, serverActions} from './build/build';
it('propagates bootstrap config', async () => {
const path = await createApplicationAt('runtime-config-bootstrap');
await build(path, {args: ['-d']});
const {results: [{payload: id}]} = await serverActions(path, [
{type: 'config.get', payload: '@flecks/core.id'},
{type: 'exit'},
]);
expect(id)
.to.equal('flecks');
});

View File

@ -0,0 +1,29 @@
import {writeFile} from 'fs/promises';
import {join} from 'path';
import {expect} from 'chai';
import {build, createApplicationAt, serverActions} from './build/build';
it('propagates bootstrap config', async () => {
const path = await createApplicationAt('runtime-config-override');
await writeFile(
join(path, 'build', 'flecks.yml'),
`
'@flecks/build': {}
'@flecks/core': {id: 'testing'}
'@flecks/server': {}
'comm:./comm': {foo: 'baz'}
`,
);
await build(path, {args: ['-d']});
const {results: [{payload: id}, {payload: foo}]} = await serverActions(path, [
{type: 'config.get', payload: '@flecks/core.id'},
{type: 'config.get', payload: 'comm.foo'},
{type: 'exit'},
]);
expect(id)
.to.equal('testing');
expect(foo)
.to.equal('baz');
});

View File

@ -0,0 +1,14 @@
import {expect} from 'chai';
import {build, createApplicationAt, serverActions} from './build/build';
it('propagates runtime config', async () => {
const path = await createApplicationAt('runtime-config-runtime');
await build(path, {args: ['-d']});
const {results: [{payload: foo}]} = await serverActions(path, [
{type: 'config.get', payload: 'comm.foo'},
{type: 'exit'},
]);
expect(foo)
.to.equal('bar');
});

View File

@ -0,0 +1,13 @@
import {expect} from 'chai';
import {build, createApplicationAt, serverActions} from './build/build';
it('connects', async () => {
const path = await createApplicationAt('runtime-connect');
await build(path, {args: ['-d']});
const {code} = await serverActions(path, [
{type: 'exit', payload: 42},
]);
expect(code)
.to.equal(42);
});

View File

@ -0,0 +1,4 @@
'@flecks/build': {}
'@flecks/core': {}
'@flecks/server': {}
'comm:./comm': {}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,5 @@
export const hooks = {
'@flecks/core.config': () => ({
foo: 'bar',
}),
};

View File

@ -0,0 +1,29 @@
import {createConnection} from 'net';
const {
FLECKS_SERVER_TEST_SOCKET,
} = process.env;
export const hooks = {
'@flecks/server.up': async (flecks) => {
const socket = createConnection({path: FLECKS_SERVER_TEST_SOCKET});
socket.on('connect', () => {
socket.on('data', (data) => {
const {meta, payload, type} = JSON.parse(data);
switch (type) {
case 'config.get':
socket.write(JSON.stringify({
meta,
payload: flecks.get(payload),
}));
break;
case 'exit':
socket.end();
process.exit(payload);
break;
default:
}
});
});
},
};

View File

@ -0,0 +1 @@
{}

View File

@ -94,7 +94,7 @@ module.exports = async (config, env, argv, flecks) => {
buildFlecks.stubs.forEach((stub) => {
config.resolve.alias[stub] = false;
});
await buildFlecks.runtimeCompiler('web', config);
await buildFlecks.runtimeCompiler('web', config, env, argv);
// Styles.
config.entry.index.push(...styles);
// Tests.