From 19caba92c3a17626ded1c8c71d6e9c7e9242a86a Mon Sep 17 00:00:00 2001 From: Taylor Schley Date: Sat, 19 Dec 2020 17:56:29 -0600 Subject: [PATCH] refactor: refactor tests --- package.json | 13 + src/index.js | 118 +- src/job-builder.js | 18 +- src/job-validator.js | 85 +- test/add.js | 63 + test/get-worker-metadata.js | 110 ++ test/index.js | 146 +++ test/job-builder.js | 162 +++ test/{job-utils..js => job-utils.js} | 9 +- test/job-validator.js | 331 +++++ test/jobs/infinite.js | 1 - test/jobs/long.js | 3 + test/jobs/loop.js | 1 - test/jobs/message-process-exit.js | 1 - test/jobs/message-ungraceful.js | 1 + test/jobs/message.js | 1 - test/jobs/short.js | 4 +- test/remove.js | 29 + test/run.js | 289 +++++ test/snapshots/test.js.md | 42 - test/snapshots/test.js.snap | Bin 377 -> 0 bytes test/start.js | 472 +++++++ test/stop.js | 158 +++ test/test.js | 1742 -------------------------- 24 files changed, 1925 insertions(+), 1874 deletions(-) create mode 100644 test/add.js create mode 100644 test/get-worker-metadata.js create mode 100644 test/index.js create mode 100644 test/job-builder.js rename test/{job-utils..js => job-utils.js} (93%) create mode 100644 test/jobs/long.js create mode 100644 test/remove.js create mode 100644 test/run.js delete mode 100644 test/snapshots/test.js.md delete mode 100644 test/snapshots/test.js.snap create mode 100644 test/start.js create mode 100644 test/stop.js delete mode 100644 test/test.js diff --git a/package.json b/package.json index 8e7cbe1..6d55eb2 100644 --- a/package.json +++ b/package.json @@ -203,6 +203,19 @@ ], "ignore": [ "config.js" + ], + "rules": { + "capitalized-comments": "off" + }, + "overrides": [ + { + "files": [ + "test/jobs/*.js" + ], + "rules": { + "unicorn/no-process-exit": "off" + } + } ] } } diff --git a/src/index.js b/src/index.js index e3c0473..82ed9fd 100644 --- a/src/index.js +++ b/src/index.js @@ -20,48 +20,47 @@ const { const buildJob = require('./job-builder'); const validateJob = require('./job-validator'); -// bthreads requires us to do this for web workers (see bthreads docs for insight) +// Bthreads requires us to do this for web workers (see bthreads docs for insight) threads.Buffer = Buffer; -// instead of `threads.browser` checks below, we previously used this boolean +// Instead of `threads.browser` checks below, we previously used this boolean // const hasFsStatSync = typeof fs === 'object' && typeof fs.statSync === 'function'; class Bree extends EventEmitter { constructor(config) { super(); this.config = { - // we recommend using Cabin for logging + // We recommend using Cabin for logging // logger: console, - // set this to `false` to prevent requiring a root directory of jobs + // Set this to `false` to prevent requiring a root directory of jobs // (e.g. if your jobs are not all in one directory) - root: threads.browser - ? /* istanbul ignore next */ - threads.resolve('jobs') + root: threads.browser /* istanbul ignore next */ + ? threads.resolve('jobs') : resolve('jobs'), - // default timeout for jobs + // Default timeout for jobs // (set this to `false` if you do not wish for a default timeout to be set) timeout: 0, - // default interval for jobs + // Default interval for jobs // (set this to `0` for no interval, and > 0 for a default interval to be set) interval: 0, - // this is an Array of your job definitions (see README for examples) + // This is an Array of your job definitions (see README for examples) jobs: [], // // (can be overridden on a job basis with same prop name) hasSeconds: false, // cronValidate: {}, - // if you set a value > 0 here, then it will terminate workers after this time (ms) + // If you set a value > 0 here, then it will terminate workers after this time (ms) closeWorkerAfterMs: 0, - // could also be mjs if desired + // Could also be mjs if desired // (this is the default extension if you just specify a job's name without ".js" or ".mjs") defaultExtension: 'js', - // default worker options to pass to ~`new Worker`~ `new threads.Worker` + // Default worker options to pass to ~`new Worker`~ `new threads.Worker` // (can be overridden on a per job basis) // worker: {}, - // custom handler to execute when error events are emmited by the workers or when they exit + // Custom handler to execute when error events are emmited by the workers or when they exit // with non-zero code // pass in a callback function with following signature: `(error, workerMetadata) => { // custom handling here }` errorHandler: null, @@ -87,7 +86,7 @@ class Bree extends EventEmitter { // `cronValidate` object has `override` object with `useSeconds` set to `true` // // - if (this.config.hasSeconds) + if (this.config.hasSeconds) { this.config.cronValidate = { ...this.config.cronValidate, preset: @@ -101,6 +100,7 @@ class Bree extends EventEmitter { useSeconds: true } }; + } debug('config', this.config); @@ -122,23 +122,24 @@ class Bree extends EventEmitter { this.getHumanToMs = getHumanToMs; this.parseValue = parseValue; - // validate root (sync check) + // Validate root (sync check) if (isSANB(this.config.root)) { /* istanbul ignore next */ if (!threads.browser && isValidPath(this.config.root)) { const stats = fs.statSync(this.config.root); - if (!stats.isDirectory()) + if (!stats.isDirectory()) { throw new Error( `Root directory of ${this.config.root} does not exist` ); + } } } - // validate timeout + // Validate timeout this.config.timeout = this.parseValue(this.config.timeout); debug('timeout', this.config.timeout); - // validate interval + // Validate interval this.config.interval = this.parseValue(this.config.interval); debug('interval', this.config.interval); @@ -160,14 +161,15 @@ class Bree extends EventEmitter { // // validate jobs // - if (!Array.isArray(this.config.jobs)) - throw new Error('Jobs must be an Array'); + if (!Array.isArray(this.config.jobs)) { + throw new TypeError('Jobs must be an Array'); + } - // provide human-friendly errors for complex configurations + // Provide human-friendly errors for complex configurations const errors = []; /* - jobs = [ + Jobs = [ 'name', { name: 'boot' }, { name: 'timeout', timeout: ms('3s') }, @@ -195,20 +197,27 @@ class Bree extends EventEmitter { } } - // if there were any errors then throw them - if (errors.length > 0) throw combineErrors(errors); + // If there were any errors then throw them + if (errors.length > 0) { + throw combineErrors(errors); + } debug('this.config.jobs', this.config.jobs); } getWorkerMetadata(name, meta = {}) { const job = this.config.jobs.find((j) => j.name === name); - if (!job) throw new Error(`Job "${name}" does not exist`); - if (!this.config.outputWorkerMetadata && !job.outputWorkerMetadata) + if (!job) { + throw new Error(`Job "${name}" does not exist`); + } + + if (!this.config.outputWorkerMetadata && !job.outputWorkerMetadata) { return meta && (typeof meta.err !== 'undefined' || typeof meta.message !== 'undefined') ? meta : undefined; + } + return this.workers[name] ? { ...meta, @@ -226,12 +235,17 @@ class Bree extends EventEmitter { if (name) { this.config.logger.info(new Date()); const job = this.config.jobs.find((j) => j.name === name); - if (!job) throw new Error(`Job "${name}" does not exist`); - if (this.workers[name]) + if (!job) { + throw new Error(`Job "${name}" does not exist`); + } + + if (this.workers[name]) { return this.config.logger.warn( new Error(`Job "${name}" is already running`), this.getWorkerMetadata(name) ); + } + debug('starting worker', name); const object = { ...(this.config.worker ? this.config.worker : {}), @@ -248,7 +262,7 @@ class Bree extends EventEmitter { this.emit('worker created', name); debug('worker started', name); - // if we specified a value for `closeWorkerAfterMs` + // If we specified a value for `closeWorkerAfterMs` // then we need to terminate it after that execution time const closeWorkerAfterMs = Number.isFinite(job.closeWorkerAfterMs) ? job.closeWorkerAfterMs @@ -256,7 +270,9 @@ class Bree extends EventEmitter { if (Number.isFinite(closeWorkerAfterMs) && closeWorkerAfterMs > 0) { debug('worker has close set', name, closeWorkerAfterMs); this.closeWorkerAfterMs[name] = setTimeout(() => { + /* istanbul ignore else */ if (this.workers[name]) { + debug('worker has been terminated', name); this.workers[name].terminate(); } }, closeWorkerAfterMs); @@ -348,15 +364,19 @@ class Bree extends EventEmitter { debug('start', name); if (name) { const job = this.config.jobs.find((j) => j.name === name); - if (!job) throw new Error(`Job ${name} does not exist`); - if (this.timeouts[name] || this.intervals[name]) + if (!job) { + throw new Error(`Job ${name} does not exist`); + } + + if (this.timeouts[name] || this.intervals[name]) { return this.config.logger.warn( new Error(`Job "${name}" is already started`) ); + } debug('job', job); - // check for date and if it is in the past then don't run it + // Check for date and if it is in the past then don't run it if (job.date instanceof Date) { debug('job date', job); if (job.date.getTime() < Date.now()) { @@ -386,7 +406,7 @@ class Bree extends EventEmitter { return; } - // this is only complex because both timeout and interval can be a schedule + // This is only complex because both timeout and interval can be a schedule if (this.isSchedule(job.timeout)) { debug('job timeout is schedule', job); this.timeouts[name] = later.setTimeout(() => { @@ -452,8 +472,10 @@ class Bree extends EventEmitter { if ( typeof this.timeouts[name] === 'object' && typeof this.timeouts[name].clear === 'function' - ) + ) { this.timeouts[name].clear(); + } + delete this.timeouts[name]; } @@ -461,8 +483,10 @@ class Bree extends EventEmitter { if ( typeof this.intervals[name] === 'object' && typeof this.intervals[name].clear === 'function' - ) + ) { this.intervals[name].clear(); + } + delete this.intervals[name]; } @@ -483,8 +507,10 @@ class Bree extends EventEmitter { if ( typeof this.closeWorkerAfterMs[name] === 'object' && typeof this.closeWorkerAfterMs[name].clear === 'function' - ) + ) { this.closeWorkerAfterMs[name].clear(); + } + delete this.closeWorkerAfterMs[name]; } @@ -502,7 +528,9 @@ class Bree extends EventEmitter { // // make sure jobs is an array // - if (!Array.isArray(jobs)) jobs = [jobs]; + if (!Array.isArray(jobs)) { + jobs = [jobs]; + } const errors = []; @@ -524,22 +552,26 @@ class Bree extends EventEmitter { debug('jobs added', this.config.jobs); - // if there were any errors then throw them - if (errors.length > 0) throw combineErrors(errors); + // If there were any errors then throw them + if (errors.length > 0) { + throw combineErrors(errors); + } } remove(name) { const job = this.config.jobs.find((j) => j.name === name); - if (!job) throw new Error(`Job "${name}" does not exist`); + if (!job) { + throw new Error(`Job "${name}" does not exist`); + } this.config.jobs = this.config.jobs.filter((j) => j.name !== name); - // make sure it also closes any open workers + // Make sure it also closes any open workers this.stop(name); } } -// expose bthreads (useful for tests) +// Expose bthreads (useful for tests) // https://github.com/chjj/bthreads#api Bree.threads = { backend: threads.backend, diff --git a/src/job-builder.js b/src/job-builder.js index 82214f6..041fc45 100644 --- a/src/job-builder.js +++ b/src/job-builder.js @@ -35,7 +35,7 @@ const buildJob = (job, config) => { }; } - // process job.path + // Process job.path if (typeof job.path === 'function') { const path = `(${job.path.toString()})()`; @@ -57,7 +57,7 @@ const buildJob = (job, config) => { if (isValidPath(path)) { job.path = path; } else { - // assume that it's a transformed eval string + // Assume that it's a transformed eval string job.worker = { eval: true, ...job.worker @@ -73,11 +73,11 @@ const buildJob = (job, config) => { job.interval = parseValue(job.interval); } - // build cron + // Build cron if (typeof job.cron !== 'undefined') { if (isSchedule(job.cron)) { job.interval = job.cron; - // delete job.cron; + // Delete job.cron; } else { job.interval = later.parse.cron( job.cron, @@ -90,7 +90,7 @@ const buildJob = (job, config) => { } } - // if timeout was undefined, cron was undefined, + // If timeout was undefined, cron was undefined, // and date was undefined then set the default // (as long as the default timeout is >= 0) if ( @@ -100,10 +100,11 @@ const buildJob = (job, config) => { typeof job.cron === 'undefined' && typeof job.date === 'undefined' && typeof job.interval === 'undefined' - ) + ) { job.timeout = config.timeout; + } - // if interval was undefined, cron was undefined, + // If interval was undefined, cron was undefined, // and date was undefined then set the default // (as long as the default interval is > 0, or it was a schedule, or it was valid) if ( @@ -112,8 +113,9 @@ const buildJob = (job, config) => { typeof job.interval === 'undefined' && typeof job.cron === 'undefined' && typeof job.date === 'undefined' - ) + ) { job.interval = config.interval; + } return job; }; diff --git a/src/job-validator.js b/src/job-validator.js index 91bf8a3..470c670 100644 --- a/src/job-validator.js +++ b/src/job-validator.js @@ -9,18 +9,21 @@ const threads = require('bthreads'); const { getName, isSchedule, parseValue } = require('./job-utils'); const validateReservedJobName = (name) => { - // don't allow a job to have the `index` file name - if (['index', 'index.js', 'index.mjs'].includes(name)) + // Don't allow a job to have the `index` file name + if (['index', 'index.js', 'index.mjs'].includes(name)) { return new Error( 'You cannot use the reserved job name of "index", "index.js", nor "index.mjs"' ); + } }; const validateStringJob = (job, i, config) => { const errors = []; const jobNameError = validateReservedJobName(job); - if (jobNameError) throw jobNameError; + if (jobNameError) { + throw jobNameError; + } if (!config.root) { errors.push( @@ -43,8 +46,9 @@ const validateStringJob = (job, i, config) => { /* istanbul ignore next */ if (!threads.browser) { const stats = fs.statSync(path); - if (!stats.isFile()) + if (!stats.isFile()) { throw new Error(`Job #${i + 1} "${job}" path missing: ${path}`); + } } }; @@ -52,13 +56,16 @@ const validateFunctionJob = (job, i) => { const errors = []; const path = `(${job.toString()})()`; - // can't be a built-in or bound function - if (path.includes('[native code]')) + // Can't be a built-in or bound function + if (path.includes('[native code]')) { errors.push( new Error(`Job #${i + 1} can't be a bound or built-in function`) ); + } - if (errors.length > 0) throw combineErrors(errors); + if (errors.length > 0) { + throw combineErrors(errors); + } }; const validateJobPath = (job, prefix, config) => { @@ -67,9 +74,10 @@ const validateJobPath = (job, prefix, config) => { if (typeof job.path === 'function') { const path = `(${job.path.toString()})()`; - // can't be a built-in or bound function - if (path.includes('[native code]')) + // Can't be a built-in or bound function + if (path.includes('[native code]')) { errors.push(new Error(`${prefix} can't be a bound or built-in function`)); + } } else if (!isSANB(job.path) && !config.root) { errors.push( new Error( @@ -77,7 +85,7 @@ const validateJobPath = (job, prefix, config) => { ) ); } else { - // validate path + // Validate path const path = isSANB(job.path) ? job.path : join( @@ -92,10 +100,12 @@ const validateJobPath = (job, prefix, config) => { if (!threads.browser) { const stats = fs.statSync(path); // eslint-disable-next-line max-depth - if (!stats.isFile()) + if (!stats.isFile()) { throw new Error(`${prefix} path missing: ${path}`); + } } } catch (err) { + /* istanbul ignore next */ errors.push(err); } } @@ -133,7 +143,7 @@ const validateCron = (job, prefix, config) => { const errors = []; if (!isSchedule(job.cron)) { - // if `hasSeconds` was `true` then set `cronValidate` and inherit any existing options + // If `hasSeconds` was `true` then set `cronValidate` and inherit any existing options const cronValidate = job.hasSeconds ? cronValidateWithSeconds(job, config) : config.cronValidate; @@ -183,9 +193,11 @@ const validateJobName = (job, i, reservedNames) => { const errors = []; const name = getName(job); - if (!name) errors.push(new Error(`Job #${i + 1} is missing a name`)); + if (!name) { + errors.push(new Error(`Job #${i + 1} is missing a name`)); + } - // throw an error if duplicate job names + // Throw an error if duplicate job names if (reservedNames.includes(name)) { errors.push( new Error(`Job #${i + 1} has a duplicate job name of ${getName(job)}`) @@ -198,40 +210,46 @@ const validateJobName = (job, i, reservedNames) => { const validate = (job, i, names, config) => { const errors = validateJobName(job, i, names); - if (errors.length > 0) throw combineErrors(errors); + if (errors.length > 0) { + throw combineErrors(errors); + } - // support a simple string which we will transform to have a path + // Support a simple string which we will transform to have a path if (isSANB(job)) { return validateStringJob(job, i, config); } - // job is a function + // Job is a function if (typeof job === 'function') { return validateFunctionJob(job, i); } - // use a prefix for errors + // Use a prefix for errors const prefix = `Job #${i + 1} named "${job.name}"`; errors.push(...validateJobPath(job, prefix, config)); - // don't allow users to mix interval AND cron + // Don't allow users to mix interval AND cron if (typeof job.interval !== 'undefined' && typeof job.cron !== 'undefined') { errors.push( new Error(`${prefix} cannot have both interval and cron configuration`) ); } - // don't allow users to mix timeout AND date - if (typeof job.timeout !== 'undefined' && typeof job.date !== 'undefined') + // Don't allow users to mix timeout AND date + if (typeof job.timeout !== 'undefined' && typeof job.date !== 'undefined') { errors.push(new Error(`${prefix} cannot have both timeout and date`)); + } const jobNameError = validateReservedJobName(job.name); - if (jobNameError) errors.push(jobNameError); + if (jobNameError) { + errors.push(jobNameError); + } - // validate date - if (typeof job.date !== 'undefined' && !(job.date instanceof Date)) + // Validate date + if (typeof job.date !== 'undefined' && !(job.date instanceof Date)) { errors.push(new Error(`${prefix} had an invalid Date of ${job.date}`)); + } ['timeout', 'interval'].forEach((prop) => { if (typeof job[prop] !== 'undefined') { @@ -248,44 +266,49 @@ const validate = (job, i, names, config) => { } }); - // validate hasSeconds + // Validate hasSeconds if ( typeof job.hasSeconds !== 'undefined' && typeof job.hasSeconds !== 'boolean' - ) + ) { errors.push( new Error( `${prefix} had hasSeconds value of ${job.hasSeconds} (it must be a Boolean)` ) ); + } - // validate cronValidate + // Validate cronValidate if ( typeof job.cronValidate !== 'undefined' && typeof job.cronValidate !== 'object' - ) + ) { errors.push( new Error( `${prefix} had cronValidate value set, but it must be an Object` ) ); + } if (typeof job.cron !== 'undefined') { errors.push(...validateCron(job, prefix, config)); } - // validate closeWorkerAfterMs + // Validate closeWorkerAfterMs if ( typeof job.closeWorkerAfterMs !== 'undefined' && (!Number.isFinite(job.closeWorkerAfterMs) || job.closeWorkerAfterMs <= 0) - ) + ) { errors.push( new Error( `${prefix} had an invalid closeWorkersAfterMs value of ${job.closeWorkersAfterMs} (it must be a finite number > 0)` ) ); + } - if (errors.length > 0) throw combineErrors(errors); + if (errors.length > 0) { + throw combineErrors(errors); + } }; module.exports = validate; diff --git a/test/add.js b/test/add.js new file mode 100644 index 0000000..2f29e78 --- /dev/null +++ b/test/add.js @@ -0,0 +1,63 @@ +const path = require('path'); + +const test = require('ava'); + +const Bree = require('../src'); + +const root = path.join(__dirname, 'jobs'); + +test('successfully add jobs as array', (t) => { + const bree = new Bree({ + root, + jobs: ['infinite'] + }); + + t.is(typeof bree.config.jobs[1], 'undefined'); + + bree.add(['basic']); + + t.is(typeof bree.config.jobs[1], 'object'); +}); + +test('successfully add job not array', (t) => { + const bree = new Bree({ + root, + jobs: ['infinite'] + }); + + t.is(typeof bree.config.jobs[1], 'undefined'); + + bree.add('basic'); + + t.is(typeof bree.config.jobs[1], 'object'); +}); + +test('fails if job already exists', (t) => { + const bree = new Bree({ + root, + jobs: ['basic'] + }); + + t.throws(() => bree.add(['basic']), { + message: /Job .* has a duplicate job name of */ + }); +}); + +test('successfully adds job object', (t) => { + const bree = new Bree({ root: false }); + function noop() {} + bree.add({ name: 'basic', path: noop.toString() }); + t.pass(); +}); + +test('missing job name', (t) => { + const logger = {}; + logger.error = () => {}; + logger.info = () => {}; + + const bree = new Bree({ + root: false, + logger + }); + t.throws(() => bree.add(), { message: /Job .* is missing a name/ }); +}); diff --git a/test/get-worker-metadata.js b/test/get-worker-metadata.js new file mode 100644 index 0000000..c78cb93 --- /dev/null +++ b/test/get-worker-metadata.js @@ -0,0 +1,110 @@ +const path = require('path'); + +const test = require('ava'); + +const Bree = require('../src'); +const delay = require('delay'); + +const root = path.join(__dirname, 'jobs'); + +const baseConfig = { + root, + timeout: 0, + interval: 0, + hasSeconds: false, + defaultExtension: 'js' +}; + +test('throws if no job exists', (t) => { + const bree = new Bree({ + jobs: ['basic'], + ...baseConfig + }); + + t.throws(() => bree.getWorkerMetadata('test'), { + message: 'Job "test" does not exist' + }); +}); + +test('returns undefined if output not set to true', (t) => { + const bree = new Bree({ + jobs: ['basic'], + ...baseConfig + }); + + const meta = { test: 1 }; + + t.is(typeof bree.getWorkerMetadata('basic', meta), 'undefined'); +}); + +test('returns meta if error', (t) => { + const bree = new Bree({ + jobs: ['basic'], + ...baseConfig + }); + + const meta = { err: true, message: true }; + + t.is(bree.getWorkerMetadata('basic', meta), meta); +}); + +test('returns meta if output set to true', (t) => { + const bree = new Bree({ + jobs: ['basic'], + ...baseConfig, + outputWorkerMetadata: true + }); + + const meta = { test: 1 }; + + t.is(bree.getWorkerMetadata('basic', meta), meta); +}); + +test('returns meta and worker data if running', async (t) => { + const logger = { + info: () => {} + }; + + const bree = new Bree({ + jobs: ['infinite'], + ...baseConfig, + outputWorkerMetadata: true, + logger + }); + + bree.start(); + await delay(1); + + const meta = { test: 1 }; + + t.is(typeof bree.getWorkerMetadata('infinite', meta).worker, 'object'); + + await bree.stop(); +}); + +test('job with worker data sent by job', async (t) => { + t.plan(1); + + const logger = { + info: (...args) => { + if (!args[1] || !args[1].message) { + return; + } + + t.is(args[1].message.test, 'test'); + }, + error: () => {} + }; + + const bree = new Bree({ + root, + jobs: [{ name: 'worker-data', worker: { workerData: { test: 'test' } } }], + outputWorkerMetadata: true, + logger + }); + + bree.run('worker-data'); + await delay(1000); + + await bree.stop(); +}); diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..3d6010f --- /dev/null +++ b/test/index.js @@ -0,0 +1,146 @@ +const test = require('ava'); +const path = require('path'); +const delay = require('delay'); +const humanInterval = require('human-interval'); +const FakeTimers = require('@sinonjs/fake-timers'); + +const Bree = require('../src'); + +const root = path.join(__dirname, 'jobs'); +const baseConfig = { + root, + timeout: 0, + interval: 0, + hasSeconds: false, + defaultExtension: 'js' +}; + +test('successfully run job', async (t) => { + t.plan(2); + + const logger = { + info: () => {} + }; + + const bree = new Bree({ + jobs: ['infinite'], + ...baseConfig, + logger + }); + + bree.start(); + + bree.on('worker created', (name) => { + t.true(typeof bree.workers[name] === 'object'); + }); + + bree.on('worker deleted', (name) => { + t.true(typeof bree.workers[name] === 'undefined'); + }); + + await delay(100); + + await bree.stop(); +}); + +test('preset and override is set if config.hasSeconds is "true"', (t) => { + const bree = new Bree({ + jobs: ['basic'], + ...baseConfig, + hasSeconds: true + }); + + t.is(bree.config.cronValidate.preset, 'default'); + t.true(typeof bree.config.cronValidate.override === 'object'); +}); + +test('preset and override is set by cronValidate config', (t) => { + const bree = new Bree({ + jobs: ['basic'], + ...baseConfig, + hasSeconds: true, + cronValidate: { + preset: 'test', + override: { test: 'works' } + } + }); + + t.is(bree.config.cronValidate.preset, 'test'); + t.true(typeof bree.config.cronValidate.override === 'object'); + t.is(bree.config.cronValidate.override.test, 'works'); +}); + +test('throws if jobs is not an array', (t) => { + t.throws( + () => + new Bree({ + jobs: null, + ...baseConfig, + root: path.join(__dirname, 'noIndexJobs') + }), + { + message: 'Jobs must be an Array' + } + ); +}); + +test('throws during constructor if job-validator throws', (t) => { + t.throws( + () => + new Bree({ + jobs: [{ name: 'basic', hasSeconds: 'test' }], + ...baseConfig + }), + { + message: + 'Job #1 named "basic" had hasSeconds value of test (it must be a Boolean)' + } + ); +}); + +test('emits "worker created" and "worker started" events', async (t) => { + t.plan(2); + + const bree = new Bree({ + root, + jobs: ['basic'], + timeout: 100 + }); + + bree.start(); + + bree.on('worker created', (name) => { + t.true(typeof bree.workers[name] === 'object'); + }); + bree.on('worker deleted', (name) => { + t.true(typeof bree.workers[name] === 'undefined'); + }); + + await delay(1000); + + await bree.stop(); +}); + +test.serial('job with long timeout runs', (t) => { + t.plan(2); + + const bree = new Bree({ + root, + jobs: ['infinite'], + timeout: '3 months' + }); + + t.is(bree.config.jobs[0].timeout, humanInterval('3 months')); + + const now = Date.now(); + const clock = FakeTimers.install({ now: Date.now() }); + + bree.start('infinite'); + bree.on('worker created', () => { + t.is(clock.now - now, humanInterval('3 months')); + }); + // Should run till worker stops running + clock.runAll(); + + clock.uninstall(); +}); diff --git a/test/job-builder.js b/test/job-builder.js new file mode 100644 index 0000000..01202e0 --- /dev/null +++ b/test/job-builder.js @@ -0,0 +1,162 @@ +const test = require('ava'); +const path = require('path'); +const later = require('@breejs/later'); + +const jobBuilder = require('../src/job-builder'); + +const root = path.join(__dirname, 'jobs'); +const baseConfig = { + root, + timeout: 0, + interval: 0, + hasSeconds: false, + defaultExtension: 'js' +}; + +function job(t, _job, config, expected) { + t.deepEqual( + jobBuilder(_job ? _job : 'basic', { ...baseConfig, ...config }), + expected + ); +} + +test( + 'job name as file name without extension', + job, + null, + {}, + { name: 'basic', path: `${root}/basic.js`, timeout: 0, interval: 0 } +); + +test( + 'job name as file name with extension', + job, + 'basic.js', + {}, + { name: 'basic.js', path: `${root}/basic.js`, timeout: 0, interval: 0 } +); + +function basic() { + setTimeout(() => { + console.log('hello'); + }, 100); +} + +test( + 'job is function', + job, + basic, + {}, + { + name: 'basic', + path: `(${basic.toString()})()`, + worker: { eval: true }, + timeout: 0, + interval: 0 + } +); + +test( + 'job.path is function', + job, + { path: basic, worker: { test: 1 } }, + {}, + { + path: `(${basic.toString()})()`, + worker: { eval: true, test: 1 }, + timeout: 0 + } +); + +test( + 'job.path is blank and name of job is defined without extension', + job, + { name: 'basic', path: '' }, + {}, + { name: 'basic', path: `${root}/basic.js`, timeout: 0 } +); + +test( + 'job.path is blank and name of job is defined with extension', + job, + { name: 'basic.js', path: '' }, + {}, + { name: 'basic.js', path: `${root}/basic.js`, timeout: 0 } +); + +test( + 'job.path is path to file', + job, + { path: `${root}/basic.js` }, + {}, + { path: `${root}/basic.js`, timeout: 0 } +); + +test( + 'job.path is not a file path', + job, + { path: '*.js', worker: { test: 1 } }, + {}, + { path: '*.js', timeout: 0, worker: { eval: true, test: 1 } } +); + +test( + 'job.timeout is value', + job, + { path: `${root}/basic.js`, timeout: 10 }, + {}, + { path: `${root}/basic.js`, timeout: 10 } +); + +test( + 'job.interval is value', + job, + { path: `${root}/basic.js`, interval: 10 }, + {}, + { path: `${root}/basic.js`, interval: 10 } +); + +test( + 'job.cron is value', + job, + { path: `${root}/basic.js`, cron: '* * * * *' }, + {}, + { + path: `${root}/basic.js`, + cron: '* * * * *', + interval: later.parse.cron('* * * * *') + } +); + +test( + 'job.cron is value with hasSeconds config', + job, + { path: `${root}/basic.js`, cron: '* * * * *', hasSeconds: false }, + {}, + { + path: `${root}/basic.js`, + cron: '* * * * *', + interval: later.parse.cron('* * * * *'), + hasSeconds: false + } +); + +test( + 'job.cron is schedule', + job, + { path: `${root}/basic.js`, cron: later.parse.cron('* * * * *') }, + {}, + { + path: `${root}/basic.js`, + cron: later.parse.cron('* * * * *'), + interval: later.parse.cron('* * * * *') + } +); + +test( + 'default interval is greater than 0', + job, + { name: 'basic', interval: undefined }, + { interval: 10 }, + { name: 'basic', path: `${root}/basic.js`, timeout: 0, interval: 10 } +); diff --git a/test/job-utils..js b/test/job-utils.js similarity index 93% rename from test/job-utils..js rename to test/job-utils.js index ec542b1..1e8d728 100644 --- a/test/job-utils..js +++ b/test/job-utils.js @@ -22,9 +22,12 @@ test('getName: extracts job name from an object', (t) => { t.is(jobUtils.getName({ name: 'job-name' }), 'job-name'); }); -test('getName: extracts job name from a named function', (t) => { - function namedFunction() {} - t.is(jobUtils.getName(namedFunction), 'namedFunction'); +test('getName: extracts job name from a function', (t) => { + const fn = () => { + return true; + }; + + t.is(jobUtils.getName(fn), 'fn'); }); test('getHumanToMs: converts values into milliseconds', (t) => { diff --git a/test/job-validator.js b/test/job-validator.js index fb81390..7a2db76 100644 --- a/test/job-validator.js +++ b/test/job-validator.js @@ -1,5 +1,6 @@ const test = require('ava'); const path = require('path'); +const later = require('@breejs/later'); const jobValidator = require('../src/job-validator'); const root = path.join(__dirname, 'jobs'); @@ -13,6 +14,15 @@ test('does not throw for valid object job', (t) => { ); }); +test('does not throw for valid object job with extension on name', (t) => { + t.notThrows(() => + jobValidator({ name: 'basic.js' }, 0, ['exists'], { + root, + defaultExtension: 'js' + }) + ); +}); + test('does not throw for valid string job', (t) => { t.notThrows(() => jobValidator('basic', 1, ['exists'], { @@ -22,6 +32,15 @@ test('does not throw for valid string job', (t) => { ); }); +test('does not throw for valid string job with extension', (t) => { + t.notThrows(() => + jobValidator('basic.js', 1, ['exists'], { + root, + defaultExtension: 'js' + }) + ); +}); + test('throws for non-unique job name', (t) => { t.throws( () => @@ -150,3 +169,315 @@ test("prefers job's override cronValidate if none in job configuration", (t) => t.deepEqual(returned, expected); }); + +test('throws for reserved job.name', (t) => { + t.throws( + () => + jobValidator({ name: 'index' }, 0, ['exists'], { + root, + defaultExtension: 'js' + }), + { + message: + 'You cannot use the reserved job name of "index", "index.js", nor "index.mjs"' + } + ); +}); + +test('throws for reserved job name', (t) => { + t.throws( + () => + jobValidator('index', 0, ['exists'], { + root, + defaultExtension: 'js' + }), + { + message: + 'You cannot use the reserved job name of "index", "index.js", nor "index.mjs"' + } + ); +}); + +test('throws for string job if no root directory', (t) => { + t.throws( + () => + jobValidator('basic', 0, ['exists'], { + defaultExtension: 'js' + }), + { + message: + 'Job #1 "basic" requires root directory option to auto-populate path' + } + ); +}); + +test('throws for object job if no root directory', (t) => { + t.throws( + () => + jobValidator({ name: 'basic' }, 0, ['exists'], { + defaultExtension: 'js' + }), + { + message: + 'Job #1 named "basic" requires root directory option to auto-populate path' + } + ); +}); + +test('does not throw for valid job path', (t) => { + t.notThrows(() => + jobValidator({ name: 'basic', path: root + '/basic.js' }, 0, ['exists'], { + root, + defaultExtension: 'js' + }) + ); +}); + +test('does not throw for path without extension', (t) => { + t.notThrows(() => + jobValidator({ name: 'basic' }, 0, ['exists'], { + root, + defaultExtension: 'js' + }) + ); +}); + +test('does not throw for valid function', (t) => { + const fn = () => { + return true; + }; + + t.notThrows(() => + jobValidator(fn, 1, ['exists'], { + root, + defaultExtension: 'js' + }) + ); +}); + +test('throws for bound function', (t) => { + const fn = () => { + return true; + }; + + const boundFn = fn.bind(this); + + t.throws( + () => + jobValidator(boundFn, 1, ['exists'], { + root, + defaultExtension: 'js' + }), + { + message: "Job #2 can't be a bound or built-in function" + } + ); +}); + +test('does not throw for valid function in job.path', (t) => { + const fn = () => { + return true; + }; + + t.notThrows(() => + jobValidator({ path: fn, name: 'fn' }, 1, ['exists'], { + root, + defaultExtension: 'js' + }) + ); +}); + +test('throws for bound function in job.path', (t) => { + const fn = () => { + return true; + }; + + const boundFn = fn.bind(this); + + t.throws( + () => + jobValidator({ path: boundFn, name: 'fn' }, 1, ['exists'], { + root, + defaultExtension: 'js' + }), + { + message: 'Job #2 named "fn" can\'t be a bound or built-in function' + } + ); +}); + +test('does not throw for valid cron without seconds', (t) => { + t.notThrows(() => + jobValidator({ name: 'basic', cron: '* * * * *' }, 0, ['exists'], { + root, + defaultExtension: 'js' + }) + ); +}); + +test('does not throw for valid cron with seconds', (t) => { + t.notThrows(() => + jobValidator( + { name: 'basic', cron: '* * * * * *', hasSeconds: true }, + 0, + ['exists'], + { + root, + defaultExtension: 'js' + } + ) + ); +}); + +test('does not throw for valid cron that is a schedule', (t) => { + t.notThrows(() => + jobValidator( + { name: 'basic', cron: later.parse.cron('* * * * *') }, + 0, + ['exists'], + { + root, + defaultExtension: 'js' + } + ) + ); +}); + +test('throws for invalid cron expression', (t) => { + t.throws( + () => + jobValidator({ name: 'basic', cron: '* * * * * *' }, 0, ['exists'], { + root, + defaultExtension: 'js' + }), + { + message: + 'Job #1 named "basic" had an invalid cron pattern: Expected 5 values, but got 6. (Input cron: \'* * * * * *\')' + } + ); +}); + +test('throws if no no name exists', (t) => { + t.throws( + () => + jobValidator({}, 0, ['exists'], { + root, + defaultExtension: 'js' + }), + { + message: 'Job #1 is missing a name' + } + ); +}); + +test('throws if both interval and cron are used', (t) => { + t.throws( + () => + jobValidator( + { name: 'basic', cron: '* * * * *', interval: 60 }, + 0, + ['exists'], + { root, defaultExtension: 'js' } + ), + { + message: + 'Job #1 named "basic" cannot have both interval and cron configuration' + } + ); +}); + +test('throws if both timeout and date are used', (t) => { + t.throws( + () => + jobValidator( + { name: 'basic', timeout: 60, date: new Date('12/30/2020') }, + 0, + ['exists'], + { root, defaultExtension: 'js' } + ), + { + message: 'Job #1 named "basic" cannot have both timeout and date' + } + ); +}); + +test('throws if date is not a Date object', (t) => { + t.throws( + () => + jobValidator({ name: 'basic', date: '12/23/2020' }, 0, ['exists'], { + root, + defaultExtension: 'js' + }), + { + message: 'Job #1 named "basic" had an invalid Date of 12/23/2020' + } + ); +}); + +test('throws if timeout is invalid', (t) => { + t.throws( + () => + jobValidator({ name: 'basic', timeout: -1 }, 0, ['exists'], { + root, + defaultExtension: 'js' + }), + { + message: /Job #1 named "basic" had an invalid timeout of -1; */ + } + ); +}); + +test('throws if interval is invalid', (t) => { + t.throws( + () => + jobValidator({ name: 'basic', interval: -1 }, 0, ['exists'], { + root, + defaultExtension: 'js' + }), + { + message: /Job #1 named "basic" had an invalid interval of undefined; */ + } + ); +}); + +test('throws if hasSeconds is not a boolean', (t) => { + t.throws( + () => + jobValidator({ name: 'basic', hasSeconds: 'test' }, 0, ['exists'], { + root, + defaultExtension: 'js' + }), + { + message: + 'Job #1 named "basic" had hasSeconds value of test (it must be a Boolean)' + } + ); +}); + +test('throws if cronValidate is not an Object', (t) => { + t.throws( + () => + jobValidator({ name: 'basic', cronValidate: 'test' }, 0, ['exists'], { + root, + defaultExtension: 'js' + }), + { + message: + 'Job #1 named "basic" had cronValidate value set, but it must be an Object' + } + ); +}); + +test('throws if closeWorkerAfterMs is invalid', (t) => { + t.throws( + () => + jobValidator({ name: 'basic', closeWorkerAfterMs: -1 }, 0, ['exists'], { + root, + defaultExtension: 'js' + }), + { + message: + 'Job #1 named "basic" had an invalid closeWorkersAfterMs value of undefined (it must be a finite number > 0)' + } + ); +}); diff --git a/test/jobs/infinite.js b/test/jobs/infinite.js index 858a18f..4bfeb6d 100644 --- a/test/jobs/infinite.js +++ b/test/jobs/infinite.js @@ -1,2 +1 @@ -// eslint-disable-next-line unicorn/no-process-exit setInterval(() => process.exit(0), 100); diff --git a/test/jobs/long.js b/test/jobs/long.js new file mode 100644 index 0000000..3923de7 --- /dev/null +++ b/test/jobs/long.js @@ -0,0 +1,3 @@ +setTimeout(() => { + process.exit(2); +}, 4000); diff --git a/test/jobs/loop.js b/test/jobs/loop.js index 375084e..16ee837 100644 --- a/test/jobs/loop.js +++ b/test/jobs/loop.js @@ -11,7 +11,6 @@ if (parentPort) { } parentPort.postMessage(message); - // eslint-disable-next-line unicorn/no-process-exit process.exit(0); }); } diff --git a/test/jobs/message-process-exit.js b/test/jobs/message-process-exit.js index 75fe532..dffc497 100644 --- a/test/jobs/message-process-exit.js +++ b/test/jobs/message-process-exit.js @@ -5,7 +5,6 @@ setInterval(() => {}, 10); if (parentPort) { parentPort.on('message', (message) => { if (message === 'cancel') { - // eslint-disable-next-line unicorn/no-process-exit process.exit(0); } }); diff --git a/test/jobs/message-ungraceful.js b/test/jobs/message-ungraceful.js index 98faaad..0859e5c 100644 --- a/test/jobs/message-ungraceful.js +++ b/test/jobs/message-ungraceful.js @@ -6,6 +6,7 @@ if (parentPort) { parentPort.on('message', (message) => { if (message === 'cancel') { parentPort.postMessage('ungraceful'); + process.exit(0); } }); } diff --git a/test/jobs/message.js b/test/jobs/message.js index c8afcda..8b12fae 100644 --- a/test/jobs/message.js +++ b/test/jobs/message.js @@ -11,7 +11,6 @@ if (parentPort) { } parentPort.postMessage(message); - // eslint-disable-next-line unicorn/no-process-exit process.exit(0); }); } diff --git a/test/jobs/short.js b/test/jobs/short.js index 56f5fc0..f2c72aa 100644 --- a/test/jobs/short.js +++ b/test/jobs/short.js @@ -1 +1,3 @@ -setInterval(() => {}, 10); +setInterval(() => { + process.exit(2); +}, 10); diff --git a/test/remove.js b/test/remove.js new file mode 100644 index 0000000..7428111 --- /dev/null +++ b/test/remove.js @@ -0,0 +1,29 @@ +const path = require('path'); + +const test = require('ava'); + +const Bree = require('../src'); + +const root = path.join(__dirname, 'jobs'); + +test('successfully remove jobs', (t) => { + const bree = new Bree({ + root, + jobs: ['basic', 'infinite'] + }); + + t.is(typeof bree.config.jobs[1], 'object'); + + bree.remove('infinite'); + + t.is(typeof bree.config.jobs[1], 'undefined'); +}); + +test('fails if job does not exist', (t) => { + const bree = new Bree({ + root, + jobs: ['infinite'] + }); + + t.throws(() => bree.remove('basic'), { message: /Job .* does not exist/ }); +}); diff --git a/test/run.js b/test/run.js new file mode 100644 index 0000000..c0b9de5 --- /dev/null +++ b/test/run.js @@ -0,0 +1,289 @@ +const path = require('path'); + +const test = require('ava'); + +const Bree = require('../src'); +const delay = require('delay'); + +const root = path.join(__dirname, 'jobs'); + +test('job does not exist', (t) => { + const bree = new Bree({ + root, + jobs: ['basic'] + }); + + t.throws(() => bree.run('leroy'), { + message: 'Job "leroy" does not exist' + }); +}); + +test('job already running', (t) => { + const logger = {}; + logger.warn = (err, _) => { + t.is(err.message, 'Job "basic" is already running'); + }; + + logger.info = () => {}; + + const bree = new Bree({ + root, + jobs: ['basic'], + logger + }); + + bree.run('basic'); + bree.run('basic'); +}); + +test.serial('job terminates after closeWorkerAfterMs', async (t) => { + t.plan(2); + + const logger = {}; + logger.info = () => {}; + logger.error = () => {}; + + const bree = new Bree({ + root, + jobs: [{ name: 'long', closeWorkerAfterMs: 500 }], + logger + }); + + bree.run('long'); + await delay(1); + t.true(typeof bree.closeWorkerAfterMs.long === 'object'); + + await new Promise((resolve, reject) => { + bree.workers.long.on('error', reject); + bree.workers.long.on('exit', (code) => { + t.is(code, 1); + resolve(); + }); + }); +}); + +test('job terminates before closeWorkerAfterMs', async (t) => { + const logger = {}; + logger.info = () => {}; + logger.error = () => {}; + + const bree = new Bree({ + root, + jobs: [{ name: 'short', closeWorkerAfterMs: 2000 }], + logger + }); + + bree.run('short'); + await delay(1); + t.true(typeof bree.closeWorkerAfterMs.short === 'object'); + + await new Promise((resolve, reject) => { + bree.workers.short.on('error', reject); + bree.workers.short.on('exit', (code) => { + t.is(code, 2); + resolve(); + }); + }); +}); + +test('job terminates on message "done"', async (t) => { + const logger = {}; + logger.info = () => {}; + + const bree = new Bree({ + root, + jobs: [{ name: 'done' }], + logger + }); + + bree.run('done'); + + await delay(1); + + t.is(typeof bree.workers.done, 'object'); + await new Promise((resolve, reject) => { + bree.workers.done.on('error', reject); + bree.workers.done.on('message', (message) => { + if (message === 'get ready') { + resolve(); + } + }); + }); + + await delay(100); + t.is(typeof bree.workers.done, 'undefined'); +}); + +test('job sent a message', async (t) => { + const logger = {}; + logger.info = (message) => { + if (message === 'Worker for job "message" sent a message') { + t.pass(); + } + }; + + const bree = new Bree({ + root, + jobs: [{ name: 'message' }], + logger + }); + + bree.run('message'); + + bree.workers.message.postMessage('test'); + + await new Promise((resolve, reject) => { + bree.workers.message.on('error', reject); + bree.workers.message.on('exit', resolve); + }); +}); + +test('job sent an error', async (t) => { + const logger = { + error: (message) => { + if (message === 'Worker for job "message" had an error') { + t.pass(); + } + }, + info: () => {} + }; + + const bree = new Bree({ + root, + jobs: [{ name: 'message' }], + logger + }); + + bree.run('message'); + + bree.workers.message.postMessage('error'); + + await new Promise((resolve) => { + bree.workers.message.on('error', resolve); + bree.workers.message.on('exit', resolve); + }); +}); + +test('job sent an error with custom handler', async (t) => { + t.plan(5); + const logger = { + error: () => {}, + info: () => {} + }; + + const bree = new Bree({ + root, + jobs: [{ name: 'message' }], + logger, + errorHandler: (err, workerMeta) => { + t.true(workerMeta.name === 'message'); + + if (workerMeta.err) { + t.true(err.message === 'oops'); + t.true(workerMeta.err.name === 'Error'); + } else { + t.true(err.message === 'Worker for job "message" exited with code 1'); + } + } + }); + + bree.run('message'); + + bree.workers.message.postMessage('error'); + + await new Promise((resolve) => { + bree.workers.message.on('exit', resolve); + }); +}); + +test('jobs run all when no name designated', async (t) => { + const logger = {}; + logger.info = () => {}; + + const bree = new Bree({ + root, + jobs: ['basic'], + logger + }); + + bree.run(); + await delay(1); + + t.true(typeof bree.workers.basic === 'object'); + + await new Promise((resolve, reject) => { + bree.workers.basic.on('error', reject); + bree.workers.basic.on('exit', (code) => { + t.is(code, 0); + resolve(); + }); + }); + + t.true(typeof bree.workers.basic === 'undefined'); +}); + +test('job runs with no worker options in config', async (t) => { + const logger = {}; + logger.info = () => {}; + + const bree = new Bree({ + root, + jobs: ['basic'], + logger, + worker: false + }); + + bree.run('basic'); + await delay(1); + + t.is(typeof bree.workers.basic, 'object'); + + await new Promise((resolve, reject) => { + bree.workers.basic.on('error', reject); + bree.workers.basic.on('exit', (code) => { + t.is(code, 0); + resolve(); + }); + }); + + t.is(typeof bree.workers.basic, 'undefined'); +}); + +test('job runs and passes workerData from config', async (t) => { + t.plan(4); + const logger = { + info: (...args) => { + if (!args[1] || !args[1].message) { + return; + } + + t.is(args[1].message.test, 'test'); + } + }; + + const bree = new Bree({ + root, + jobs: ['worker-data'], + logger, + worker: { + workerData: { + test: 'test' + } + } + }); + + bree.run('worker-data'); + + await delay(1); + t.is(typeof bree.workers['worker-data'], 'object'); + + await new Promise((resolve, reject) => { + bree.workers['worker-data'].on('error', reject); + bree.workers['worker-data'].on('exit', (code) => { + t.is(code, 0); + resolve(); + }); + }); + + t.is(typeof bree.workers['worker-data'], 'undefined'); +}); diff --git a/test/snapshots/test.js.md b/test/snapshots/test.js.md deleted file mode 100644 index 426caab..0000000 --- a/test/snapshots/test.js.md +++ /dev/null @@ -1,42 +0,0 @@ -# Snapshot report for `test/test.js` - -The actual snapshot is saved in `test.js.snap`. - -Generated by [AVA](https://avajs.dev). - -## creates job with cron string - -> Snapshot 1 - - { - exceptions: [], - schedules: [ - { - s: [ - 0, - ], - }, - ], - } - -## getWorkerMetadata() - -> Snapshot 1 - - {} - -## parseValue() - -> Snapshot 1 - - { - error: -1, - exceptions: [], - schedules: [ - { - t: [ - 43200, - ], - }, - ], - } diff --git a/test/snapshots/test.js.snap b/test/snapshots/test.js.snap deleted file mode 100644 index d1b446f6da51b2362e774c9a699d44f4137229cd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 377 zcmV-<0fzoTRzVEn;Y-R+D&R}9- z*jww9w}1J?`Ww!-O79D8cVdKyGJ(uv2kU3#U}QD|(iT7*0>r|MY=Vqz{z+M>$t8@8 zaHfEO00Sd010xdyBOfDUK2V_uBQpads~{s+YDIEtK}lwQUNKm%8%d6{I5{IVr8FlM zD8&IWn$@wWD6x`}3C7~*=LflgX#-H(HkkcTMa(FQxEL5285p@484ti5&h#8ezeRU6 z3x=Z^!46|!L^zDG7_46vD5TAZYAGugOL>4Qm>`;2kVB5e3`pAmaU>9z5)M6}etwWq zFlVr)78T_eLCj_R4|e4QJVCpLyr8`Z)OMY?pnV7rS`J{a@G%k@w2H_ { + const bree = new Bree({ + root, + jobs: ['basic'] + }); + + t.throws(() => bree.start('leroy'), { message: 'Job leroy does not exist' }); +}); + +test('fails if job already started', async (t) => { + const logger = {}; + logger.warn = (err) => { + t.is(err.message, 'Job "short" is already started'); + }; + + logger.info = () => {}; + + logger.error = () => {}; + + const bree = new Bree({ + root, + jobs: ['short'], + logger + }); + + bree.start('short'); + await delay(1); + bree.start('short'); + + await bree.stop(); +}); + +test('fails if date is in the past', async (t) => { + const bree = new Bree({ + root, + jobs: [{ name: 'basic', date: new Date() }] + }); + + bree.start('basic'); + await delay(1); + + t.is(typeof bree.timeouts.basic, 'undefined'); + + await bree.stop(); +}); + +test('sets timeout if date is in the future', async (t) => { + const bree = new Bree({ + root, + jobs: [ + { + name: 'infinite', + date: new Date(Date.now() + 10) + } + ] + }); + + t.is(typeof bree.timeouts.infinite, 'undefined'); + + bree.start('infinite'); + await delay(1); + t.is(typeof bree.timeouts.infinite, 'object'); + + await delay(20); + + t.is(typeof bree.timeouts.infinite, 'undefined'); + + await bree.stop(); +}); + +test.serial( + 'sets interval if date is in the future and interval is schedule', + async (t) => { + t.plan(3); + + const bree = new Bree({ + root, + jobs: [ + { + name: 'basic', + date: new Date(Date.now() + 10), + interval: later.parse.cron('* * * * *') + } + ] + }); + + const clock = FakeTimers.install({ now: Date.now() }); + + t.is(typeof bree.intervals.basic, 'undefined'); + + bree.start('basic'); + clock.tick(1); + clock.next(); + + t.is(typeof bree.intervals.basic, 'object'); + + const promise = new Promise((resolve, reject) => { + bree.workers.basic.on('error', reject); + bree.workers.basic.on('exit', (code) => { + t.true(code === 0); + resolve(); + }); + }); + clock.next(); + await promise; + + await bree.stop(); + clock.uninstall(); + } +); + +test.serial( + 'sets interval if date is in the future and interval is number', + async (t) => { + t.plan(3); + + const bree = new Bree({ + root, + jobs: [ + { + name: 'infinite', + date: new Date(Date.now() + 10), + interval: 100000 + } + ] + }); + + const clock = FakeTimers.install({ now: Date.now() }); + + t.is(typeof bree.intervals.infinite, 'undefined'); + + bree.start('infinite'); + await clock.nextAsync(); + + t.is(typeof bree.intervals.infinite, 'object'); + + const promise = new Promise((resolve, reject) => { + bree.workers.infinite.on('error', reject); + bree.workers.infinite.on('exit', (code) => { + t.true(code === 0); + resolve(); + }); + }); + clock.next(); + await promise; + + await bree.stop(); + clock.uninstall(); + } +); + +test.serial( + 'sets timeout if interval is schedule and timeout is schedule', + async (t) => { + t.plan(6); + + const bree = new Bree({ + root, + jobs: [ + { + name: 'infinite', + timeout: later.parse.cron('* * * * *'), + interval: later.parse.cron('* * * * *') + } + ] + }); + + const clock = FakeTimers.install({ now: Date.now() }); + + t.is(typeof bree.timeouts.infinite, 'undefined'); + t.is(typeof bree.intervals.infinite, 'undefined'); + + bree.start('infinite'); + clock.tick(1); + + t.is(typeof bree.timeouts.infinite, 'object'); + + clock.next(); + t.is(typeof bree.intervals.infinite, 'object'); + t.is(typeof bree.timeouts.infinie, 'undefined'); + + const promise = new Promise((resolve, reject) => { + bree.workers.infinite.on('error', reject); + bree.workers.infinite.on('exit', (code) => { + t.true(code === 0); + resolve(); + }); + }); + clock.next(); + await promise; + + await bree.stop(); + clock.uninstall(); + } +); + +test.serial( + 'sets timeout if interval is number and timeout is schedule', + async (t) => { + t.plan(6); + + const bree = new Bree({ + root, + jobs: [ + { + name: 'infinite', + timeout: later.parse.cron('* * * * *'), + interval: 1000 + } + ] + }); + + const clock = FakeTimers.install({ now: Date.now() }); + + t.is(typeof bree.timeouts.infinite, 'undefined'); + t.is(typeof bree.intervals.infinite, 'undefined'); + + bree.start('infinite'); + clock.tick(1); + + t.is(typeof bree.timeouts.infinite, 'object'); + + await clock.nextAsync(); + t.is(typeof bree.intervals.infinite, 'object'); + t.is(typeof bree.timeouts.infinie, 'undefined'); + + const promise = new Promise((resolve, reject) => { + bree.workers.infinite.on('error', reject); + bree.workers.infinite.on('exit', (code) => { + t.true(code === 0); + resolve(); + }); + }); + clock.next(); + await promise; + + await bree.stop(); + clock.uninstall(); + } +); + +test.serial( + 'sets timeout if interval is 0 and timeout is schedule', + async (t) => { + t.plan(4); + + const bree = new Bree({ + root, + jobs: [ + { + name: 'infinite', + timeout: later.parse.cron('* * * * *'), + interval: 0 + } + ] + }); + + const clock = FakeTimers.install({ now: Date.now() }); + + t.is(typeof bree.timeouts.infinite, 'undefined'); + + bree.start('infinite'); + clock.tick(1); + + t.is(typeof bree.timeouts.infinite, 'object'); + + await clock.nextAsync(); + t.is(typeof bree.timeouts.infinie, 'undefined'); + + const promise = new Promise((resolve, reject) => { + bree.workers.infinite.on('error', reject); + bree.workers.infinite.on('exit', (code) => { + t.true(code === 0); + resolve(); + }); + }); + clock.next(); + await promise; + + await bree.stop(); + clock.uninstall(); + } +); + +test.serial( + 'sets timeout if interval is schedule and timeout is number', + async (t) => { + t.plan(6); + + const bree = new Bree({ + root, + jobs: [ + { + name: 'infinite', + timeout: 1000, + interval: later.parse.cron('* * * * *') + } + ] + }); + + const clock = FakeTimers.install({ now: Date.now() }); + + t.is(typeof bree.timeouts.infinite, 'undefined'); + t.is(typeof bree.intervals.infinite, 'undefined'); + + bree.start('infinite'); + clock.tick(1); + + t.is(typeof bree.timeouts.infinite, 'object'); + + await clock.nextAsync(); + t.is(typeof bree.intervals.infinite, 'object'); + t.is(typeof bree.timeouts.infinie, 'undefined'); + + const promise = new Promise((resolve, reject) => { + bree.workers.infinite.on('error', reject); + bree.workers.infinite.on('exit', (code) => { + t.true(code === 0); + resolve(); + }); + }); + clock.next(); + await promise; + + await bree.stop(); + clock.uninstall(); + } +); + +test.serial( + 'sets timeout if interval is number and timeout is number', + async (t) => { + t.plan(6); + + const bree = new Bree({ + root, + jobs: [ + { + name: 'infinite', + timeout: 1000, + interval: 1000 + } + ] + }); + + const clock = FakeTimers.install({ now: Date.now() }); + + t.is(typeof bree.timeouts.infinite, 'undefined'); + t.is(typeof bree.intervals.infinite, 'undefined'); + + bree.start('infinite'); + clock.tick(1); + + t.is(typeof bree.timeouts.infinite, 'object'); + + await clock.nextAsync(); + t.is(typeof bree.intervals.infinite, 'object'); + t.is(typeof bree.timeouts.infinie, 'undefined'); + + const promise = new Promise((resolve, reject) => { + bree.workers.infinite.on('error', reject); + bree.workers.infinite.on('exit', (code) => { + t.true(code === 0); + resolve(); + }); + }); + clock.next(); + await promise; + + await bree.stop(); + clock.uninstall(); + } +); + +test.serial('sets interval if interval is schedule', async (t) => { + t.plan(3); + + const bree = new Bree({ + root, + jobs: ['infinite'], + timeout: false, + interval: later.parse.cron('* * * * *') + }); + + const clock = FakeTimers.install({ now: Date.now() }); + + t.is(typeof bree.intervals.infinite, 'undefined'); + + bree.start('infinite'); + clock.tick(1); + + t.is(typeof bree.intervals.infinite, 'object'); + + await clock.nextAsync(); + const promise = new Promise((resolve, reject) => { + bree.workers.infinite.on('error', reject); + bree.workers.infinite.on('exit', (code) => { + t.true(code === 0); + resolve(); + }); + }); + clock.next(); + await promise; + + await bree.stop(); + clock.uninstall(); +}); + +test.serial('sets interval if interval is number', async (t) => { + t.plan(3); + + const bree = new Bree({ + root, + jobs: ['infinite'], + timeout: false, + interval: 1000 + }); + + const clock = FakeTimers.install({ now: Date.now() }); + + t.is(typeof bree.intervals.infinite, 'undefined'); + + bree.start('infinite'); + clock.tick(1); + + t.is(typeof bree.intervals.infinite, 'object'); + + await clock.nextAsync(); + const promise = new Promise((resolve, reject) => { + bree.workers.infinite.on('error', reject); + bree.workers.infinite.on('exit', (code) => { + t.true(code === 0); + resolve(); + }); + }); + clock.next(); + await promise; + + await bree.stop(); + clock.uninstall(); +}); + +test('does not set interval if interval is 0', async (t) => { + t.plan(2); + + const bree = new Bree({ + root, + jobs: ['infinite'], + timeout: false, + interval: 0 + }); + + t.is(typeof bree.intervals.infinite, 'undefined'); + + bree.start('infinite'); + await delay(1); + + t.is(typeof bree.intervals.infinite, 'undefined'); + + await bree.stop(); +}); diff --git a/test/stop.js b/test/stop.js new file mode 100644 index 0000000..00c329d --- /dev/null +++ b/test/stop.js @@ -0,0 +1,158 @@ +const path = require('path'); + +const test = require('ava'); + +const Bree = require('../src'); +const delay = require('delay'); + +const root = path.join(__dirname, 'jobs'); + +test.serial('job stops when "cancel" message is sent', async (t) => { + t.plan(4); + + const logger = {}; + logger.info = (message) => { + if (message === 'Gracefully cancelled worker for job "message"') { + t.true(true); + } + }; + + logger.error = () => {}; + + const bree = new Bree({ + root, + jobs: [{ name: 'message' }], + logger + }); + + t.is(typeof bree.workers.message, 'undefined'); + + bree.start('message'); + await delay(1); + + t.is(typeof bree.workers.message, 'object'); + + await bree.stop(); + + t.is(typeof bree.workers.message, 'undefined'); +}); + +test.serial('job stops when process.exit(0) is called', async (t) => { + t.plan(4); + + const logger = {}; + logger.info = (message) => { + if ( + message === 'Worker for job "message-process-exit" exited with code 0' + ) { + t.true(true); + } + }; + + const bree = new Bree({ + root, + jobs: [{ name: 'message-process-exit' }], + logger + }); + + t.is(typeof bree['message-process-exit'], 'undefined'); + + bree.start('message-process-exit'); + await delay(1); + + t.is(typeof bree.workers['message-process-exit'], 'object'); + + await bree.stop(); + + t.is(typeof bree.workers['message-process-exit'], 'undefined'); +}); + +test.serial( + 'does not send graceful notice if no cancelled message', + async (t) => { + const logger = { + info: (message) => { + if (message === 'Gracefully cancelled worker for job "message"') { + t.fail(); + } + }, + error: () => {} + }; + + const bree = new Bree({ + root, + jobs: ['message-ungraceful'], + logger + }); + + bree.start('message-ungraceful'); + await delay(1); + console.log(bree); + await bree.stop('message-ungraceful'); + + t.pass(); + } +); + +test('clears closeWorkerAfterMs', async (t) => { + const bree = new Bree({ + root, + jobs: [{ name: 'basic', closeWorkerAfterMs: 10 }] + }); + + t.is(typeof bree.closeWorkerAfterMs.basic, 'undefined'); + + bree.start('basic'); + await delay(1); + + t.is(typeof bree.closeWorkerAfterMs.basic, 'object'); + + await bree.stop('basic'); + + t.is(typeof bree.closeWorkerAfterMs.basic, 'undefined'); +}); + +test('deletes closeWorkerAfterMs', async (t) => { + const bree = new Bree({ + root, + jobs: [{ name: 'basic', closeWorkerAfterMs: 10 }] + }); + + t.is(typeof bree.closeWorkerAfterMs.basic, 'undefined'); + + bree.start('basic'); + bree.closeWorkerAfterMs.basic = 'test'; + await bree.stop('basic'); + + t.is(typeof bree.closeWorkerAfterMs.basic, 'undefined'); +}); + +test('deletes timeouts', async (t) => { + const bree = new Bree({ + root, + jobs: [{ name: 'basic', timeout: 1000 }] + }); + + t.is(typeof bree.timeouts.basic, 'undefined'); + + bree.start('basic'); + bree.timeouts.basic = 'test'; + await bree.stop('basic'); + + t.is(typeof bree.timeouts.basic, 'undefined'); +}); + +test('deletes intervals', async (t) => { + const bree = new Bree({ + root, + jobs: [{ name: 'basic', interval: 1000 }] + }); + + t.is(typeof bree.intervals.basic, 'undefined'); + + bree.start('basic'); + bree.intervals.basic = 'test'; + await bree.stop('basic'); + + t.is(typeof bree.intervals.basic, 'undefined'); +}); diff --git a/test/test.js b/test/test.js deleted file mode 100644 index fc4e00b..0000000 --- a/test/test.js +++ /dev/null @@ -1,1742 +0,0 @@ -const path = require('path'); - -const test = require('ava'); -const FakeTimers = require('@sinonjs/fake-timers'); - -const Bree = require('..'); -const later = require('@breejs/later'); -const delay = require('delay'); -const humanInterval = require('human-interval'); - -const root = path.join(__dirname, 'jobs'); - -test('creates a basic job and runs it', async (t) => { - const logger = {}; - logger.info = () => {}; - - const bree = new Bree({ - root, - jobs: ['basic'], - logger - }); - - bree.start(); - await delay(1); - - t.true(typeof bree.workers.basic === 'object'); - - await new Promise((resolve, reject) => { - bree.workers.basic.on('error', reject); - bree.workers.basic.on('exit', (code) => { - t.true(code === 0); - resolve(); - }); - }); - - t.true(typeof bree.workers.basic === 'undefined'); - bree.stop(); -}); - -test('fails if root is not a directory', (t) => { - t.throws( - () => - new Bree({ - root: path.join(__dirname, 'jobs/basic.js'), - jobs: ['basic'] - }), - { message: /Root directory of .* does not exist/ } - ); -}); - -test('finds jobs from index.js', (t) => { - const bree = new Bree({ - root, - jobs: [] - }); - - t.is(bree.config.jobs[0].name, 'basic'); -}); - -test('fails if jobs is not an array', (t) => { - t.throws( - () => - new Bree({ - root: path.join(__dirname, 'noIndexJobs'), - jobs: null, - // hide MODULE_NOT_FOUND error - logger: { error: () => {} } - }), - { message: 'Jobs must be an Array' } - ); -}); - -test('fails if duplicate job names when given names', (t) => { - t.throws( - () => - new Bree({ - root, - jobs: ['basic', 'basic'] - }), - { message: /Job .* has a duplicate job name of */ } - ); -}); - -test('fails if no root path given', (t) => { - t.throws( - () => - new Bree({ - root: null, - jobs: ['basic'] - }), - { - message: /Job #.* ".*" requires root directory option to auto-populate path/ - } - ); -}); - -test('fails if job file does not exist', (t) => { - t.throws( - () => - new Bree({ - root, - jobs: ['leroy'] - }), - { message: /Job #.* ".*" path missing: */ } - ); -}); - -test('fails if job is not a pure object', (t) => { - t.throws( - () => - new Bree({ - root, - jobs: [['basic']] - }), - { message: /Job #.* is missing a name/ } - ); -}); - -test('fails if job name is empty', (t) => { - t.throws( - () => - new Bree({ - root, - jobs: [{ name: '' }] - }), - { message: /Job #.* is missing a name/ } - ); -}); - -test('fails if job.path does not exist', (t) => { - t.throws( - () => - new Bree({ - root: null, - jobs: [{ name: 'basic', path: null }] - }), - { - message: /Job #.* named .* requires root directory option to auto-populate path/ - } - ); -}); - -test('fails if path is missing', (t) => { - t.throws( - () => - new Bree({ - root, - jobs: [{ name: 'basic', path: path.join(__dirname, 'jobs/leroy.js') }] - }), - { message: /Job #.* named .* path missing: */ } - ); -}); - -test('creates path if root provided and path !isSANB', (t) => { - const bree = new Bree({ - root, - jobs: [{ name: 'basic', path: null }] - }); - - t.is(bree.config.jobs[0].path, path.join(__dirname, 'jobs/basic.js')); -}); - -test('fails if root path given but no name and path is empty', (t) => { - t.throws( - () => - new Bree({ - root, - jobs: [{ path: '' }] - }), - { message: /Job #.* is missing a name/ } - ); -}); - -test('fails if interval and cron are set', (t) => { - t.throws( - () => - new Bree({ - root, - jobs: [{ name: 'basic', interval: '3s', cron: '* * * * *' }] - }), - { - message: /Job #.* named .* cannot have both interval and cron configuration/ - } - ); -}); - -test('fails if timeout and date are set', (t) => { - t.throws( - () => - new Bree({ - root, - jobs: [{ name: 'basic', timeout: '', date: '' }] - }), - { message: /Job #.* named .* cannot have both timeout and date/ } - ); -}); - -test('fails if duplicate job name and given objects', (t) => { - t.throws( - () => - new Bree({ - root, - jobs: [{ name: 'basic' }, { name: 'basic' }] - }), - { message: /Job .* has a duplicate job name of .*/ } - ); -}); - -test('fails if date is invalid', (t) => { - t.throws( - () => - new Bree({ - root, - jobs: [{ name: 'basic', date: null }] - }), - { message: /Job #.* named .* had an invalid Date of */ } - ); -}); - -test('creates job with correct timeout and interval', (t) => { - const bree = new Bree({ - root, - jobs: [{ name: 'basic', timeout: '3s', interval: '1s' }] - }); - - t.is(bree.config.jobs[0].timeout, 3000); - t.is(bree.config.jobs[0].interval, 1000); -}); - -test('fails if timeout is invalid', (t) => { - t.throws( - () => - new Bree({ - root, - jobs: [{ name: 'basic', timeout: '' }] - }), - { message: /Job #.* named .* had an invalid timeout of */ } - ); -}); - -test('creates job with correct interval and does not set a timeout', (t) => { - const bree = new Bree({ - root, - jobs: [{ name: 'basic', interval: '3s' }] - }); - - t.is(bree.config.jobs[0].interval, 3000); - t.is(bree.config.jobs[0].timeout, undefined); -}); - -test('fails if interval is invalid', (t) => { - t.throws( - () => - new Bree({ - root, - jobs: [{ name: 'basic', interval: '' }] - }), - { message: /Job #.* named .* had an invalid interval of */ } - ); -}); - -test('creates cron job with schedule object', (t) => { - const cron = later.parse.cron('* * * * *'); - const bree = new Bree({ - root, - jobs: [{ name: 'basic', cron }] - }); - - t.is(bree.config.jobs[0].interval, cron); -}); - -test('creates job with cron string', (t) => { - const bree = new Bree({ - root, - jobs: [{ name: 'basic', cron: '* * * * *' }] - }); - - t.snapshot(bree.config.jobs[0].interval); -}); - -test('fails if cron pattern is invalid', (t) => { - t.throws( - () => - new Bree({ - root, - jobs: [{ name: 'basic', cron: '* * * *' }] - }), - { message: /Job #.* named .* had an invalid cron pattern: */ } - ); -}); - -test('fails if closeWorkersAfterMs is <= 0 or infinite', (t) => { - t.throws( - () => - new Bree({ - root, - jobs: [{ name: 'basic', closeWorkerAfterMs: 0 }] - }), - { - message: /Job #.* named .* had an invalid closeWorkersAfterMs value of */ - } - ); -}); - -test('fails if reserved job name: index, index.js, index.mjs', (t) => { - t.throws( - () => - new Bree({ - root, - jobs: ['index'] - }), - { - message: - 'You cannot use the reserved job name of "index", "index.js", nor "index.mjs"' - } - ); -}); - -test('fails if reserved job name in object: index, index.js, index.mjs', (t) => { - t.throws( - () => - new Bree({ - root, - jobs: [{ name: 'index' }] - }), - { - message: - 'You cannot use the reserved job name of "index", "index.js", nor "index.mjs"' - } - ); -}); - -test('getHumanToMs()', (t) => { - const bree = new Bree({ - root, - jobs: ['basic'] - }); - - t.is(bree.getHumanToMs('one second'), 1000); - t.is(bree.getHumanToMs('1s'), 1000); -}); - -test('parseValue()', (t) => { - const bree = new Bree({ - root, - jobs: ['basic'] - }); - - // if value is false - t.is(bree.parseValue(false), false); - - // if value is schedule - const schedule = { schedules: [] }; - t.is(bree.parseValue(schedule), schedule); - - // if value is a string - t.snapshot(bree.parseValue('at 12:00 pm')); - t.is(bree.parseValue('1 second'), 1000); - - // if value is finite or < 0 - t.throws(() => bree.parseValue(-1), { - message: /Value .* must be a finite number */ - }); - - // if value is none of the above - t.is(bree.parseValue(1), 1); -}); - -test('isSchedule()', (t) => { - const bree = new Bree({ - root, - jobs: ['basic'] - }); - - t.is(bree.isSchedule({}), false); - t.is(bree.isSchedule({ schedules: null }), false); - t.is(bree.isSchedule({ schedules: [] }), true); -}); - -test('getWorkerMetadata()', (t) => { - const bree = new Bree({ - root, - jobs: ['basic'] - }); - - t.throws(() => bree.getWorkerMetadata('leroy'), { - message: 'Job "leroy" does not exist' - }); - - t.is(bree.getWorkerMetadata('basic'), undefined); - - bree.config.outputWorkerMetadata = true; - bree.config.jobs[0].outputWorkerMetadata = true; - t.snapshot(bree.getWorkerMetadata('basic')); - - bree.workers.basic = { isMainThread: 'test' }; - t.is(bree.getWorkerMetadata('basic').worker.isMainThread, 'test'); -}); - -test('run > job does not exist', (t) => { - const bree = new Bree({ - root, - jobs: ['basic'] - }); - - t.throws(() => bree.run('leroy'), { - message: 'Job "leroy" does not exist' - }); -}); - -test('run > job already running', (t) => { - const logger = {}; - logger.warn = (err, _) => { - t.is(err.message, 'Job "basic" is already running'); - }; - - logger.info = () => {}; - - const bree = new Bree({ - root, - jobs: ['basic'], - logger - }); - - bree.run('basic'); - bree.run('basic'); -}); - -test.serial('run > job terminates after set time', async (t) => { - const logger = {}; - logger.info = () => {}; - logger.error = () => {}; - - const bree = new Bree({ - root, - jobs: [{ name: 'infinite', closeWorkerAfterMs: 50 }], - logger - }); - - bree.run('infinite'); - t.true(typeof bree.closeWorkerAfterMs.infinite === 'object'); - - await delay(1); - await new Promise((resolve, reject) => { - bree.workers.infinite.on('error', reject); - bree.workers.infinite.on('exit', (code) => { - t.true(code === 1); - resolve(); - }); - }); -}); - -test.serial('run > job terminates before set time', async (t) => { - const logger = {}; - logger.info = () => {}; - logger.error = () => {}; - - const bree = new Bree({ - root, - jobs: [{ name: 'basic', closeWorkerAfterMs: 500 }], - logger - }); - - bree.run('basic'); - t.true(typeof bree.closeWorkerAfterMs.basic === 'object'); - await delay(1); - await new Promise((resolve, reject) => { - bree.workers.basic.on('error', reject); - bree.workers.basic.on('exit', (code) => { - t.true(code === 0); - resolve(); - }); - }); -}); - -test('run > job terminates on message "done"', async (t) => { - const logger = {}; - logger.info = () => {}; - - const bree = new Bree({ - root, - jobs: [{ name: 'done' }], - logger - }); - - bree.run('done'); - t.is(typeof bree.workers.done, 'object'); - await delay(1); - await new Promise((resolve, reject) => { - bree.workers.done.on('error', reject); - bree.workers.done.on('message', (message) => { - if (message === 'get ready') { - resolve(); - } - }); - }); - - await delay(100); - t.is(typeof bree.workers.done, 'undefined'); -}); - -test('run > job sent a message', async (t) => { - const logger = {}; - logger.info = (message) => { - if (message === 'Worker for job "message" sent a message') t.pass(); - }; - - const bree = new Bree({ - root, - jobs: [{ name: 'message' }], - logger - }); - - bree.run('message'); - - bree.workers.message.postMessage('test'); - - await new Promise((resolve, reject) => { - bree.workers.message.on('error', reject); - bree.workers.message.on('exit', resolve); - }); -}); - -test('run > job sent an error', async (t) => { - const logger = { - error: (message) => { - if (message === 'Worker for job "message" had an error') t.pass(); - }, - info: () => {} - }; - - const bree = new Bree({ - root, - jobs: [{ name: 'message' }], - logger - }); - - bree.run('message'); - - bree.workers.message.postMessage('error'); - - await new Promise((resolve) => { - bree.workers.message.on('error', resolve); - bree.workers.message.on('exit', resolve); - }); -}); - -test('run > job sent an error with custom handler', async (t) => { - t.plan(5); - const logger = { - error: () => {}, - info: () => {} - }; - - const bree = new Bree({ - root, - jobs: [{ name: 'message' }], - logger, - errorHandler: (err, workerMeta) => { - t.true(workerMeta.name === 'message'); - - if (workerMeta.err) { - t.true(err.message === 'oops'); - t.true(workerMeta.err.name === 'Error'); - } else { - t.true(err.message === 'Worker for job "message" exited with code 1'); - } - } - }); - - bree.run('message'); - - bree.workers.message.postMessage('error'); - - await new Promise((resolve) => { - bree.workers.message.on('exit', resolve); - }); -}); - -test('run > jobs run all when no name designated', async (t) => { - const logger = {}; - logger.info = () => {}; - - const bree = new Bree({ - root, - jobs: ['basic'], - logger - }); - - bree.run(); - await delay(1); - - t.true(typeof bree.workers.basic === 'object'); - - await new Promise((resolve, reject) => { - bree.workers.basic.on('error', reject); - bree.workers.basic.on('exit', (code) => { - t.is(code, 0); - resolve(); - }); - }); - - t.true(typeof bree.workers.basic === 'undefined'); -}); - -test('start > throws error if job does not exist', (t) => { - const bree = new Bree({ - root, - jobs: ['basic'] - }); - - t.throws(() => bree.start('leroy'), { message: 'Job leroy does not exist' }); -}); - -test('start > fails if job already started', async (t) => { - const logger = {}; - logger.warn = (err) => { - t.is(err.message, 'Job "short" is already started'); - }; - - logger.info = () => {}; - - const bree = new Bree({ - root, - jobs: ['short'], - logger - }); - - bree.start('short'); - await delay(1); - bree.start('short'); - - bree.stop(); -}); - -test('start > fails if date is in the past', async (t) => { - const bree = new Bree({ - root, - jobs: [{ name: 'basic', date: new Date() }] - }); - - await delay(1); - - bree.start('basic'); - - t.is(typeof bree.timeouts.basic, 'undefined'); - - bree.stop(); -}); - -test.serial('start > sets timeout if date is in the future', async (t) => { - const bree = new Bree({ - root, - jobs: [ - { - name: 'infinite', - date: new Date(Date.now() + 10) - } - ] - }); - - const clock = FakeTimers.install({ now: Date.now() }); - - t.is(typeof bree.timeouts.infinite, 'undefined'); - - bree.start('infinite'); - t.is(typeof bree.timeouts.infinite, 'object'); - - await clock.nextAsync(); - - t.is(typeof bree.timeouts.infinite, 'undefined'); - - bree.stop(); - clock.uninstall(); -}); - -test.serial( - 'start > sets interval if date is in the future and interval is schedule', - async (t) => { - t.plan(3); - - const bree = new Bree({ - root, - jobs: [ - { - name: 'infinite', - date: new Date(Date.now() + 10), - interval: later.parse.cron('* * * * *') - } - ] - }); - - const clock = FakeTimers.install({ now: Date.now() }); - - t.is(typeof bree.intervals.infinite, 'undefined'); - - bree.start('infinite'); - await clock.nextAsync(); - - t.is(typeof bree.intervals.infinite, 'object'); - - const promise = new Promise((resolve, reject) => { - bree.workers.infinite.on('error', reject); - bree.workers.infinite.on('exit', (code) => { - t.true(code === 0); - resolve(); - }); - }); - clock.next(); - await promise; - - bree.stop(); - clock.uninstall(); - } -); - -test.serial( - 'start > sets interval if date is in the future and interval is number', - async (t) => { - t.plan(3); - - const bree = new Bree({ - root, - jobs: [ - { - name: 'infinite', - date: new Date(Date.now() + 10), - interval: 100000 - } - ] - }); - - const clock = FakeTimers.install({ now: Date.now() }); - - t.is(typeof bree.intervals.infinite, 'undefined'); - - bree.start('infinite'); - await clock.nextAsync(); - - t.is(typeof bree.intervals.infinite, 'object'); - - const promise = new Promise((resolve, reject) => { - bree.workers.infinite.on('error', reject); - bree.workers.infinite.on('exit', (code) => { - t.true(code === 0); - resolve(); - }); - }); - clock.next(); - await promise; - - bree.stop(); - clock.uninstall(); - } -); - -test.serial( - 'start > sets timeout if interval is schedule and timeout is schedule', - async (t) => { - t.plan(6); - - const bree = new Bree({ - root, - jobs: [ - { - name: 'infinite', - timeout: later.parse.cron('* * * * *'), - interval: later.parse.cron('* * * * *') - } - ] - }); - - const clock = FakeTimers.install({ now: Date.now() }); - - t.is(typeof bree.timeouts.infinite, 'undefined'); - t.is(typeof bree.intervals.infinite, 'undefined'); - - bree.start('infinite'); - - t.is(typeof bree.timeouts.infinite, 'object'); - - await clock.nextAsync(); - t.is(typeof bree.intervals.infinite, 'object'); - t.is(typeof bree.timeouts.infinie, 'undefined'); - - const promise = new Promise((resolve, reject) => { - bree.workers.infinite.on('error', reject); - bree.workers.infinite.on('exit', (code) => { - t.true(code === 0); - resolve(); - }); - }); - clock.next(); - await promise; - - bree.stop(); - clock.uninstall(); - } -); - -test.serial( - 'start > sets timeout if interval is number and timeout is schedule', - async (t) => { - t.plan(6); - - const bree = new Bree({ - root, - jobs: [ - { - name: 'infinite', - timeout: later.parse.cron('* * * * *'), - interval: 1000 - } - ] - }); - - const clock = FakeTimers.install({ now: Date.now() }); - - t.is(typeof bree.timeouts.infinite, 'undefined'); - t.is(typeof bree.intervals.infinite, 'undefined'); - - bree.start('infinite'); - - t.is(typeof bree.timeouts.infinite, 'object'); - - await clock.nextAsync(); - t.is(typeof bree.intervals.infinite, 'object'); - t.is(typeof bree.timeouts.infinie, 'undefined'); - - const promise = new Promise((resolve, reject) => { - bree.workers.infinite.on('error', reject); - bree.workers.infinite.on('exit', (code) => { - t.true(code === 0); - resolve(); - }); - }); - clock.next(); - await promise; - - bree.stop(); - clock.uninstall(); - } -); - -test.serial( - 'start > sets timeout if interval is 0 and timeout is schedule', - async (t) => { - t.plan(4); - - const bree = new Bree({ - root, - jobs: [ - { - name: 'infinite', - timeout: later.parse.cron('* * * * *'), - interval: 0 - } - ] - }); - - const clock = FakeTimers.install({ now: Date.now() }); - - t.is(typeof bree.timeouts.infinite, 'undefined'); - - bree.start('infinite'); - - t.is(typeof bree.timeouts.infinite, 'object'); - - await clock.nextAsync(); - t.is(typeof bree.timeouts.infinie, 'undefined'); - - const promise = new Promise((resolve, reject) => { - bree.workers.infinite.on('error', reject); - bree.workers.infinite.on('exit', (code) => { - t.true(code === 0); - resolve(); - }); - }); - clock.next(); - await promise; - - bree.stop(); - clock.uninstall(); - } -); - -test.serial( - 'start > sets timeout if interval is schedule and timeout is number', - async (t) => { - t.plan(6); - - const bree = new Bree({ - root, - jobs: [ - { - name: 'infinite', - timeout: 1000, - interval: later.parse.cron('* * * * *') - } - ] - }); - - const clock = FakeTimers.install({ now: Date.now() }); - - t.is(typeof bree.timeouts.infinite, 'undefined'); - t.is(typeof bree.intervals.infinite, 'undefined'); - - bree.start('infinite'); - - t.is(typeof bree.timeouts.infinite, 'object'); - - await clock.nextAsync(); - t.is(typeof bree.intervals.infinite, 'object'); - t.is(typeof bree.timeouts.infinie, 'undefined'); - - const promise = new Promise((resolve, reject) => { - bree.workers.infinite.on('error', reject); - bree.workers.infinite.on('exit', (code) => { - t.true(code === 0); - resolve(); - }); - }); - clock.next(); - await promise; - - bree.stop(); - clock.uninstall(); - } -); - -test.serial( - 'start > sets timeout if interval is number and timeout is number', - async (t) => { - t.plan(6); - - const bree = new Bree({ - root, - jobs: [ - { - name: 'infinite', - timeout: 1000, - interval: 1000 - } - ] - }); - - const clock = FakeTimers.install({ now: Date.now() }); - - t.is(typeof bree.timeouts.infinite, 'undefined'); - t.is(typeof bree.intervals.infinite, 'undefined'); - - bree.start('infinite'); - - t.is(typeof bree.timeouts.infinite, 'object'); - - await clock.nextAsync(); - t.is(typeof bree.intervals.infinite, 'object'); - t.is(typeof bree.timeouts.infinie, 'undefined'); - - const promise = new Promise((resolve, reject) => { - bree.workers.infinite.on('error', reject); - bree.workers.infinite.on('exit', (code) => { - t.true(code === 0); - resolve(); - }); - }); - clock.next(); - await promise; - - bree.stop(); - clock.uninstall(); - } -); - -test.serial('start > sets interval if interval is schedule', async (t) => { - t.plan(3); - - const bree = new Bree({ - root, - jobs: ['infinite'], - timeout: false, - interval: later.parse.cron('* * * * *') - }); - - const clock = FakeTimers.install({ now: Date.now() }); - - t.is(typeof bree.intervals.infinite, 'undefined'); - - bree.start('infinite'); - - t.is(typeof bree.intervals.infinite, 'object'); - - await clock.nextAsync(); - const promise = new Promise((resolve, reject) => { - bree.workers.infinite.on('error', reject); - bree.workers.infinite.on('exit', (code) => { - t.true(code === 0); - resolve(); - }); - }); - clock.next(); - await promise; - - bree.stop(); - clock.uninstall(); -}); - -test.serial('start > sets interval if interval is number', async (t) => { - t.plan(3); - - const bree = new Bree({ - root, - jobs: ['infinite'], - timeout: false, - interval: 1000 - }); - - const clock = FakeTimers.install({ now: Date.now() }); - - t.is(typeof bree.intervals.infinite, 'undefined'); - - bree.start('infinite'); - - t.is(typeof bree.intervals.infinite, 'object'); - - await clock.nextAsync(); - const promise = new Promise((resolve, reject) => { - bree.workers.infinite.on('error', reject); - bree.workers.infinite.on('exit', (code) => { - t.true(code === 0); - resolve(); - }); - }); - clock.next(); - await promise; - - bree.stop(); - clock.uninstall(); -}); - -test.serial('start > does not set interval if interval is 0', (t) => { - t.plan(2); - - const bree = new Bree({ - root, - jobs: ['infinite'], - timeout: false, - interval: 0 - }); - - t.is(typeof bree.intervals.infinite, 'undefined'); - - bree.start('infinite'); - - t.is(typeof bree.intervals.infinite, 'undefined'); - - bree.stop(); -}); - -test.serial('stop > job stops when "cancel" message is sent', async (t) => { - t.plan(4); - - const logger = {}; - logger.info = (message) => { - if (message === 'Gracefully cancelled worker for job "message"') - t.true(true); - }; - - logger.error = () => {}; - - const bree = new Bree({ - root, - jobs: [{ name: 'message' }], - logger - }); - - t.is(typeof bree.workers.message, 'undefined'); - - bree.start('message'); - await delay(1); - - t.is(typeof bree.workers.message, 'object'); - - await bree.stop(); - - t.is(typeof bree.workers.message, 'undefined'); -}); - -test.serial('stop > job stops when process.exit(0) is called', async (t) => { - t.plan(4); - - const logger = {}; - logger.info = (message) => { - if (message === 'Worker for job "message-process-exit" exited with code 0') - t.true(true); - }; - - const bree = new Bree({ - root, - jobs: [{ name: 'message-process-exit' }], - logger - }); - - t.is(typeof bree['message-process-exit'], 'undefined'); - - bree.start('message-process-exit'); - await delay(1); - - t.is(typeof bree.workers['message-process-exit'], 'object'); - - await bree.stop(); - - t.is(typeof bree.workers['message-process-exit'], 'undefined'); -}); - -test.serial( - 'stop > does not send graceful notice if no cancelled message', - async (t) => { - const logger = { - info: (message) => { - if (message === 'Gracefully cancelled worker for job "message"') - t.fail(); - }, - error: () => {} - }; - - const bree = new Bree({ - root, - jobs: ['message-ungraceful'], - logger - }); - - bree.start('message-ungraceful'); - await delay(1); - bree.stop('message-ungraceful'); - await delay(100); - - t.pass(); - } -); - -test('stop > clears closeWorkerAfterMs', async (t) => { - const bree = new Bree({ - root, - jobs: [{ name: 'basic', closeWorkerAfterMs: 10 }] - }); - - t.is(typeof bree.closeWorkerAfterMs.basic, 'undefined'); - - bree.start('basic'); - await delay(1); - - t.is(typeof bree.closeWorkerAfterMs.basic, 'object'); - - bree.stop('basic'); - - t.is(typeof bree.closeWorkerAfterMs.basic, 'undefined'); -}); - -test('stop > deletes closeWorkerAfterMs', (t) => { - const bree = new Bree({ - root, - jobs: [{ name: 'basic', closeWorkerAfterMs: 10 }] - }); - - t.is(typeof bree.closeWorkerAfterMs.basic, 'undefined'); - - bree.start('basic'); - bree.closeWorkerAfterMs.basic = 'test'; - bree.stop('basic'); - - t.is(typeof bree.closeWorkerAfterMs.basic, 'undefined'); -}); - -test('stop > deletes timeouts', (t) => { - const bree = new Bree({ - root, - jobs: [{ name: 'basic', timeout: 1000 }] - }); - - t.is(typeof bree.timeouts.basic, 'undefined'); - - bree.start('basic'); - bree.timeouts.basic = 'test'; - bree.stop('basic'); - - t.is(typeof bree.timeouts.basic, 'undefined'); -}); - -test('stop > deletes intervals', (t) => { - const bree = new Bree({ - root, - jobs: [{ name: 'basic', interval: 1000 }] - }); - - t.is(typeof bree.intervals.basic, 'undefined'); - - bree.start('basic'); - bree.intervals.basic = 'test'; - bree.stop('basic'); - - t.is(typeof bree.intervals.basic, 'undefined'); -}); - -test('does not throw an error when root directory option is set to false', (t) => { - t.notThrows( - () => - new Bree({ - root: false, - jobs: [{ name: 'basic', path: path.join(__dirname, 'jobs/basic.js') }] - }) - ); -}); - -test('job with custom worker instance options', (t) => { - t.notThrows( - () => - new Bree({ - root, - jobs: [{ name: 'basic', worker: { argv: ['test'] } }] - }) - ); -}); - -test('job that combines date and cron', (t) => { - t.notThrows( - () => - new Bree({ - root, - jobs: ['basic'], - date: new Date(Date.now() + 100), - cron: '* * * * *' - }) - ); -}); - -test('job that combines timeout and cron', (t) => { - t.notThrows( - () => - new Bree({ - root, - jobs: ['basic'], - timeout: 100, - cron: '* * * * *' - }) - ); -}); - -test('set default interval', (t) => { - const bree = new Bree({ - root, - jobs: [{ name: 'basic' }], - interval: 100 - }); - t.is(bree.config.jobs[0].interval, 100); -}); - -// -test('set default interval to 10s', (t) => { - const bree = new Bree({ - root, - jobs: [{ name: 'basic' }], - interval: 'every 10 seconds' - }); - t.true(bree.isSchedule(bree.config.jobs[0].interval)); -}); - -// -test('job with every 10 seconds as opposed to 10s', (t) => { - const bree = new Bree({ - root, - jobs: [{ name: 'basic', interval: 'every 10 seconds' }] - }); - bree.run('basic'); - t.true(bree.isSchedule(bree.config.jobs[0].interval)); -}); - -test('emits "worker created" and "worker started" events', async (t) => { - const bree = new Bree({ - root, - jobs: ['basic'], - timeout: 100 - }); - let created; - let deleted; - bree.start(); - bree.on('worker created', (name) => { - t.true(typeof bree.workers[name] === 'object'); - created = true; - }); - bree.on('worker deleted', (name) => { - t.true(typeof bree.workers[name] === 'undefined'); - deleted = true; - }); - await delay(3000); - t.true(created && deleted); -}); - -test('jobs with .js, .mjs and no extension', (t) => { - const bree = new Bree({ - root, - jobs: ['basic', 'basic.js', 'basic.mjs'] - }); - - t.is(bree.config.jobs[0].path, `${root}/basic.js`); - t.is(bree.config.jobs[1].path, `${root}/basic.js`); - t.is(bree.config.jobs[2].path, `${root}/basic.mjs`); -}); - -test('jobs with blank path and .js, .mjs, and no extension', (t) => { - const bree = new Bree({ - root, - jobs: [ - { name: 'basic', path: '' }, - { name: 'basic.js', path: '' }, - { name: 'basic.mjs', path: '' } - ] - }); - - t.is(bree.config.jobs[0].path, `${root}/basic.js`); - t.is(bree.config.jobs[1].path, `${root}/basic.js`); - t.is(bree.config.jobs[2].path, `${root}/basic.mjs`); -}); - -test('job with custom hasSeconds option passed', (t) => { - const bree = new Bree({ - root, - jobs: [{ name: 'basic', cron: '* * * * * *', hasSeconds: true }] - }); - - t.is(typeof bree.config.jobs[0].interval, 'object'); -}); - -test.serial('job with long timeout runs', (t) => { - t.plan(2); - - const bree = new Bree({ - root, - jobs: ['infinite'], - timeout: '3 months' - }); - - t.is(bree.config.jobs[0].timeout, humanInterval('3 months')); - - const now = Date.now(); - const clock = FakeTimers.install({ now: Date.now() }); - - bree.start('infinite'); - bree.on('worker created', () => { - t.is(clock.now - now, humanInterval('3 months')); - }); - // should run till worker stops running - clock.runAll(); - - clock.uninstall(); -}); - -test.serial('job with worker data sent by default', async (t) => { - t.plan(1); - - const logger = { - info: (...args) => { - if (!args[1] || !args[1].message) return; - t.is(args[1].message.test, 'test'); - }, - error: () => {} - }; - - const bree = new Bree({ - root, - jobs: ['worker-data'], - worker: { workerData: { test: 'test' } }, - outputWorkerMetadata: true, - logger - }); - - bree.run('worker-data'); - await delay(1000); -}); - -test.serial('job with worker data sent by job', async (t) => { - t.plan(1); - - const logger = { - info: (...args) => { - if (!args[1] || !args[1].message) return; - t.is(args[1].message.test, 'test'); - }, - error: () => {} - }; - - const bree = new Bree({ - root, - jobs: [{ name: 'worker-data', worker: { workerData: { test: 'test' } } }], - outputWorkerMetadata: true, - logger - }); - - bree.run('worker-data'); - await delay(1000); -}); - -test.serial('job with false worker options sent by default', async (t) => { - t.plan(1); - - const bree = new Bree({ - root, - jobs: ['basic'], - worker: false - }); - - bree.start('basic'); - bree.on('worker created', () => { - t.pass(); - }); - - await delay(1000); - bree.stop(); -}); - -test.serial('job with false worker options sent by job', async (t) => { - t.plan(1); - - const bree = new Bree({ - root, - jobs: [{ name: 'basic', worker: false }] - }); - - bree.run('basic'); - bree.on('worker deleted', () => { - t.pass(); - }); - - await delay(1000); - bree.stop(); -}); - -test('validate hasSeconds', (t) => { - const bree = new Bree({ root, jobs: [{ name: 'basic', hasSeconds: true }] }); - t.true(bree.config.jobs[0].hasSeconds); - t.throws( - () => new Bree({ root, jobs: [{ name: 'basic', hasSeconds: 'true' }] }), - { message: /it must be a Boolean/ } - ); -}); - -test('validate cronValidate', (t) => { - t.throws( - () => new Bree({ root, jobs: [{ name: 'basic', cronValidate: false }] }), - { message: /it must be an Object/ } - ); -}); - -test('set cronValidate when hasSeconds is true', (t) => { - const bree = new Bree({ root, hasSeconds: true, jobs: [{ name: 'basic' }] }); - t.true(bree.config.hasSeconds); - t.true(typeof bree.config.cronValidate === 'object'); - t.is(bree.config.cronValidate.preset, 'default'); - t.true(typeof bree.config.cronValidate.override === 'object'); - t.is(bree.config.cronValidate.override.useSeconds, true); -}); - -test('hasSeconds and job.hasSeconds', (t) => { - const bree = new Bree({ - root, - hasSeconds: true, - jobs: [{ name: 'basic', hasSeconds: true }] - }); - t.true(bree.config.hasSeconds); - t.true(bree.config.jobs[0].hasSeconds); - t.deepEqual(bree.config.cronValidate, { - preset: 'default', - override: { - useSeconds: true - } - }); -}); - -test('cronValidate and job.cronValidate', (t) => { - const bree = new Bree({ - root, - cronValidate: { - preset: 'none' - }, - jobs: [{ name: 'basic', cronValidate: { preset: 'none' } }] - }); - t.is(bree.config.cronValidate.preset, 'none'); - t.is(bree.config.jobs[0].cronValidate.preset, 'none'); -}); - -test('hasSeconds, job.hasSeconds, cronValidate, job.cronValidate', (t) => { - const bree = new Bree({ - root, - hasSeconds: true, - cronValidate: { - preset: 'default', - override: { - useSeconds: true - } - }, - jobs: [ - { - name: 'basic', - hasSeconds: true, - cronValidate: { - preset: 'default', - override: { - useSeconds: true - } - } - } - ] - }); - t.true(bree.config.hasSeconds); - t.true(bree.config.jobs[0].hasSeconds); - t.deepEqual(bree.config.cronValidate, { - preset: 'default', - override: { - useSeconds: true - } - }); - t.deepEqual(bree.config.jobs[0].cronValidate, { - preset: 'default', - override: { - useSeconds: true - } - }); -}); - -test('creates job from function', async (t) => { - const fn = () => { - console.log('function'); - }; - - const bree = new Bree({ - root: false, - jobs: [fn] - }); - - t.is(typeof bree.config.jobs[0], 'object'); - - bree.start(); - await delay(1); - - t.true(typeof bree.workers.fn === 'object'); - - await new Promise((resolve, reject) => { - bree.workers.fn.on('error', reject); - bree.workers.fn.on('exit', (code) => { - t.true(code === 0); - resolve(); - }); - }); - - t.true(typeof bree.workers.fn === 'undefined'); - bree.stop(); -}); - -test('creates job from function passing in object', async (t) => { - const fn = () => { - console.log('function'); - }; - - const bree = new Bree({ - root: false, - jobs: [ - { - name: 'object', - path: fn - } - ] - }); - - t.is(typeof bree.config.jobs[0], 'object'); - - bree.start(); - await delay(1); - - t.true(typeof bree.workers.object === 'object'); - - await new Promise((resolve, reject) => { - bree.workers.object.on('error', reject); - bree.workers.object.on('exit', (code) => { - t.true(code === 0); - resolve(); - }); - }); - - t.true(typeof bree.workers.object === 'undefined'); - bree.stop(); -}); - -test('fails if bound/built-in function', (t) => { - const fn = () => { - console.log('function'); - }; - - const boundFn = fn.bind(this); - - t.throws( - () => - new Bree({ - root: false, - jobs: [boundFn] - }), - { - message: /Job .* can't be a bound or built-in function/ - } - ); -}); - -test('fails if bound/built-in function in object', (t) => { - const fn = () => { - console.log('function'); - }; - - const boundFn = fn.bind(this); - - t.throws( - () => - new Bree({ - root: false, - jobs: [ - { - name: 'boundFn', - path: boundFn - } - ] - }), - { - message: /Job .* can't be a bound or built-in function/ - } - ); -}); - -test('fails if multiple function jobs with same name', (t) => { - const fn = () => { - console.log('function'); - }; - - t.throws( - () => - new Bree({ - root: false, - jobs: [fn, fn] - }), - { - message: /Job .* has a duplicate job name of .*/ - } - ); -}); - -test('add > successfully add jobs as array', (t) => { - const bree = new Bree({ - root, - jobs: ['infinite'] - }); - - t.is(typeof bree.config.jobs[1], 'undefined'); - - bree.add(['basic']); - - t.is(typeof bree.config.jobs[1], 'object'); -}); - -test('add > successfully add job not array', (t) => { - const bree = new Bree({ - root, - jobs: ['infinite'] - }); - - t.is(typeof bree.config.jobs[1], 'undefined'); - - bree.add('basic'); - - t.is(typeof bree.config.jobs[1], 'object'); -}); - -test('add > fails if job already exists', (t) => { - const bree = new Bree({ - root, - jobs: ['basic'] - }); - - t.throws(() => bree.add(['basic']), { - message: /Job .* has a duplicate job name of */ - }); -}); - -test('remove > successfully remove jobs', (t) => { - const bree = new Bree({ - root, - jobs: ['basic', 'infinite'] - }); - - t.is(typeof bree.config.jobs[1], 'object'); - - bree.remove('infinite'); - - t.is(typeof bree.config.jobs[1], 'undefined'); -}); - -test('remove > fails if job does not exist', (t) => { - const bree = new Bree({ - root, - jobs: ['infinite'] - }); - - t.throws(() => bree.remove('basic'), { message: /Job .* does not exist/ }); -}); - -test('add > successfully adds job object', (t) => { - const bree = new Bree({ root: false }); - function noop() {} - bree.add({ name: 'basic', path: noop.toString() }); - t.pass(); -}); - -test('add > missing job name', (t) => { - const logger = {}; - logger.error = () => {}; - logger.info = () => {}; - - const bree = new Bree({ - root: false, - logger - }); - t.throws(() => bree.add(), { message: /Job .* is missing a name/ }); -}); - -test('run > stop > does not terminate if already terminated', async (t) => { - const bree = new Bree({ - root, - jobs: [{ name: 'loop', closeWorkerAfterMs: 100 }] - }); - - bree.run('loop'); - await delay(1); - delete bree.workers.loop; - await delay(100); - await t.pass(); -});