diff --git a/.gitignore b/.gitignore index e61051f..633636a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -/node_modules/ +**/node_modules/** /coverage/ diff --git a/example/example.js b/example/example.js new file mode 100644 index 0000000..72205a6 --- /dev/null +++ b/example/example.js @@ -0,0 +1,20 @@ +import Sync from '../src/sync' +import onDeath from 'ondeath' + + +(async () => { + const sync = new Sync({jobsDir:'./example/jobs'}) + + await sync.loadJob('example') + sync.live() + sync.watch('on') + console.log('ready and waiting') + + onDeath( () => { + console.log('\nHe\'s dead Jim') + sync.watch('remove') + }) + +})().catch(err => { + console.error('FATAL: UNABLE TO START SYSTEM!\n',err) +}) diff --git a/example/jobs/example.yaml b/example/jobs/example.yaml new file mode 100644 index 0000000..f05f84b --- /dev/null +++ b/example/jobs/example.yaml @@ -0,0 +1,15 @@ +source: ./example/source/ # be sure to add trailing / to avoid making a subdirectory under destination +destination: ./example/destination +flags: 'av' +excludeFrom: + - ./example/source/.gitignore + - ./example/source/.npmignore +exclude: + - .gitignore + - .npmignore +set: + - delete + - delete-excluded +watch: # true + wait: 200 + immediate: true diff --git a/example/source/.gitignore b/example/source/.gitignore new file mode 100644 index 0000000..ae518f0 --- /dev/null +++ b/example/source/.gitignore @@ -0,0 +1,3 @@ +node_modules/** +**/node_modules/** +coverage/ diff --git a/example/source/.npmignore b/example/source/.npmignore new file mode 100644 index 0000000..02078d5 --- /dev/null +++ b/example/source/.npmignore @@ -0,0 +1,5 @@ +tests/ +test/ +*.test.js +testing/ +example/ diff --git a/example/source/test b/example/source/test new file mode 100644 index 0000000..e69de29 diff --git a/package.json b/package.json index 854d7e9..0e0923b 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "@uci/remote-code", + "name": "@uci/sync", "version": "0.0.2", "description": "module to copy, maintain, and launch hardware modules on other machines", "main": "src/index.js", @@ -7,9 +7,13 @@ "ssh": "./bin/ssh.js" }, "scripts": { - "sync": "node -r esm ./bin/ssh", + "sync": "node -r esm ./bin/sync", + "example": "node -r esm ./example/example", + "test": "./node_modules/.bin/mocha -r esm --timeout 30000", "testd": "UCI_ENV=dev ./node_modules/.bin/nodemon --exec './node_modules/.bin/mocha -r esm --timeout 30000'", - "test": "UCI_ENV=pro UCI_LOG_PATH=./test/test.log ./node_modules/.bin/mocha -r esm --timeout 30000 || exit 0", + "testdd": "UCI_LOG_LEVEL='trace' npm run testd", + "testde": "UCI_LOG_LEVEL='warn' npm run testd", + "testl": "UCI_ENV=pro UCI_LOG_PATH=./test/test.log 0 npm run test || exit 0", "testibc": "istanbul cover ./node_modules/.bin/_mocha test/ --report lcovonly -- -R spec --recursive && codecov || true" }, "author": "David Kebler", @@ -33,6 +37,7 @@ "await-to-js": "^2.1.1", "chokidar": "^2.0.4", "conf": "^2.2.0", + "debounce-fn": "^1.0.0", "esm": "^3.1.4", "fs-read-data": "^1.0.4", "globby": "^9.0.0", @@ -46,6 +51,7 @@ "codecov": "^3.1.0", "istanbul": "^0.4.5", "mocha": "^5.x", - "nodemon": "^1.18.9" + "nodemon": "^1.18.9", + "ondeath": "^1.0.0" } } diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..ae8a9f7 --- /dev/null +++ b/src/index.js @@ -0,0 +1,5 @@ +import Sync from './sync' +import Watcher from './watcher' + +export { Sync, Watcher} +export default Sync diff --git a/src/read-lines.js b/src/read-lines.js new file mode 100644 index 0000000..68ad1e4 --- /dev/null +++ b/src/read-lines.js @@ -0,0 +1,53 @@ +import { promisify } from 'util' +import { readFile, writeFile } from 'fs' +const read = promisify(readFile) +const write = promisify(writeFile) +// alternative with new promise experimental fs +// import fs from 'fs' +// const read = fs.promises.readFile +import settle from 'p-settle' +import path from 'path' +import logger from '@uci/logger' +let log = logger({ package:'@cui/sync', file:'src/read-lines.js'}) + +// A promise helper function to return a list of paths to ignore from .npmignore, .gitignore, .rcignore +function readLines (files=[],dir) { + // console.log('additional files', files) + let list = [] + if (!Array.isArray(files)) files=[files] + + // each set in an the array is new line delimited set of ignore patterns + // settle returns array of error,value pairs + return settle(files.map(file => { + // console.log('directory',path.dirname(file)) + if (path.dirname(file)==='.') file = dir+'/'+file + log.debug({function:'readLines',file:file,msg:'reading a file of lines into array'}) + return read(file) + })) + .then((sets) => { + sets.forEach( set => { + if (set.isFulfilled) list.push(...set.value.toString().match(/.+/g)) + else log.warn({function:'readLines', error:set.reason, msg:' was unable to read file'}) + }) + return Promise.resolve(list) + }) + .catch((err) => { + // only returned when something horrible is wrong + return Promise.reject(err) + }) +} + +// an ignore list should not be huge so can serailize at once +function writeLines (filePath,list) { + + return write(filePath,list.join('\n')) + .then(() => { + log.info({function:'writeLines', file:filePath, msg:'wrote array to file of lines'}) + }) + .catch( err => { + log.fatal({function:'writeLines', error:err, msg:'unable to write array to file of lines'}) + }) +} + +export default readLines +export { readLines, writeLines } diff --git a/src/rsync.js b/src/rsync.js index 0d20244..85056aa 100644 --- a/src/rsync.js +++ b/src/rsync.js @@ -64,7 +64,10 @@ function Rsync(config) { // ordered list of file patterns to include/exclude this._patterns = [] - // list of exlude from getFiles + // just a list of exclude only patterns for use elsewhere like a watcher + this._exclude = [] + + // list of exclude files this._excludeFiles = [] // options @@ -299,6 +302,9 @@ Rsync.prototype.exclude = function(patterns) { patterns = [ patterns ] } + // save this list for other uses + this._exclude = [...this._exclude,...patterns] + patterns.forEach(function(pattern) { this._patterns.push({ action: '-', pattern: pattern }) }, this) @@ -321,10 +327,14 @@ Rsync.prototype.excludeFrom = function(filePaths) { filePaths = [ filePaths ] } + // TODO if it's not a path just a file try prepending the source directory + // TODO only add to list if file exsists filePaths.forEach(function(filePath) { this._excludeFiles.push(filePath) }, this) + // this._excludeFiles = [...this._excludeFiles,...filePaths] + return this } diff --git a/src/sync.js b/src/sync.js index 7811d3b..cc59784 100644 --- a/src/sync.js +++ b/src/sync.js @@ -1,5 +1,6 @@ // local imports import Rsync from './rsync' +import Watcher from './watcher' // native imports import { EventEmitter as Emitter } from 'events' import { dirname } from 'path' @@ -7,7 +8,8 @@ import { dirname } from 'path' import merge from 'aggregation/es6' import { readFile } from 'fs-read-data' import Conf from 'conf' -import getFiles from 'globby' +import debounce from 'debounce-fn' +// import getFiles from 'globby' import pathExists from 'path-exists' import to from 'await-to-js' // uci imports @@ -19,14 +21,23 @@ class Sync extends merge(Rsync, Emitter) { super() log = logger({ package:'@uci/sync'}) this.opts = opts - this.cli = opts.cli + this._debounce = opts.debounce || null + this.syncHandler = opts.syncHandler || (()=>{}) + // TODO if opts include source and destination then call loadJob with them + // this._debounce = opts.debounce this.config = new Conf({projectName:'sync'}) this.jobsDir = process.env.SYNC_JOBS_DIR || opts.jobsDir || this.config.get('jobsDir') || dirname(this.config.path) this.sshDir = opts.sshDir || this.config.get('sshDir') || `${this.jobsDir}/ssh` this.optionsDir = opts.optionsDir || this.config.get('optionsDir') || `${this.jobsDir}/options` - log.debug({configsDir:this.configsDir, msg:'configuration files directory'}) - this.jobs = [] + log.debug({jobsDir:this.jobsDir, sshDir:this.sshDir, optionsDir:this.optionsDir, msg:'configuration file directories'}) + this._watcher = new Watcher() } + // getters and setters + get watching() {return this._watcher.watching } // watcher active? + get watcher() { return this._watcher} // in case one need set a listerner for other purposes + + get defaultJobsDir() { return this.config.get('jobsDir', this.jobsDir) } + set defaultJobsDir(jobsDir) { this.config.set('jobsDir', jobsDir || this.jobsDir)} setConfig(name,val) { this.config.set(name,val)} getConfig(name) { return this.config.get(name)} @@ -44,13 +55,39 @@ class Sync extends merge(Rsync, Emitter) { } } - setDefaultJobsDir() { this.config.set('jobsDir', this.jobsDir)} - getDefaultJobsDir() { this.config.get('jobsDir', this.jobsDir) } + // job and options processing - async listJobFiles(dir) { - return await getFiles(['**','*.yaml','*.yml'],{ onlyfiles:true, cwd:dir || this.jobsDir}) + async runJob(options) { + await this.loadJob(options) + this.live() + this.execute(options) } + async loadJob (options) { + if (typeof options ==='string') options = await this.readOptionsFile(options,'job') + if (!options.error) { + for (const option in options) { + if(option === 'optionsFile') { + let opts = await this.readOptionsFile(options.optionsFile,'options') + if (!opts.error) { + Object.keys(opts).forEach( async opt => { + await this.processOption(opt,opts[opt]) + }) + } + } + else { + await this.processOption(option,options[option]) + } + } // end loop + this.dry() // dry run by default must .live() .unset('n') + } + } + + + // async listJobFiles(dir) { + // return await getFiles(['**','*.yaml','*.yml'],{ onlyfiles:true, cwd:dir || this.jobsDir}) + // } + async readOptionsFile(filePath,type='job') { let dir = {job:this.jobsDir,options:this.optionsDir,ssh:this.sshDir} let [err,res] = await to(readFile(`${dir[type]}/${filePath}`)) @@ -65,13 +102,91 @@ class Sync extends merge(Rsync, Emitter) { return res } + // executes a method on the instance (might be in prototype chain) which may take a value(s) + async processOption (method, value) { + log.debug({method:method, value:value, msg:`processing option ${method} with value ${value}`}) + if (typeof(this[method]) === 'function') { + await this[method](value) + } + } + + // methods ==================== + live() { this.unset('n') } + set (option=[],value) { + if (typeof option === 'string') super.set(option,value) // pass through + else { + option.forEach( opt => { + typeof opt==='string' ? super.set(opt) : super.set(...Object.entries(opt).flat()) + }) + } + return this + } + + async watch(cmd) { + // TODO make into switch ? + log.debug(`watch command ${cmd}`) + + if (isPlainObject(cmd) || cmd==null || (typeof cmd==='boolean' && cmd) || cmd==='init' || cmd==='add') { + if (cmd.wait) this.debounce(cmd) + let opts = {source:this._sources, excludeFrom:this._excludeFiles, ignored:this._exclude } + await this._watcher.init(opts) + this.syncHandler = syncHandler.call(this,log) + + return + } + + if (cmd==='remove') { + this._watcher.removeListener('changed', this.syncHandler ) + this._watcher.remove() + return + } + + if (cmd ==='on'|| cmd==='start') { + this._watcher.on('changed', this.syncHandler) + this._watcher.start() + return + } + + if (cmd==='off' || cmd==='stop' ) { + this._watcher.removeListener('changed', this.syncHandler) + this._watcher.stop() + return + } + + if (cmd==='pause' ) { + this._watcher.removeListener('changed', this.syncHandler) + return + } + + if (cmd==='resume') { + this._watcher.on('changed', this.syncHandler) + return + } + + // 'changed' event handler with optional debounce wrapper + function syncHandler(log) { + function sync (change) { + log.info({file:change.file, type:change.type, msg:`file ${change.file} was ${change.type}`}) + this.execute() + } + log.debug(`in sync make Handler, ${this._debounce}, ${sync}`) + if (this._debounce==null) return sync.bind(this) + return debounce(sync.bind(this),{wait:this.opts.debounce}) + } + } + + debounce(opts) { + if (opts==null) this._debounce=null + this._debounce = opts + } + sshu (file) { if (file && (typeof options==='string')) { - this.shell(`"sshu -c ${file} "`) + this.shell(`sshu -c ${file}`) } } @@ -104,52 +219,8 @@ class Sync extends merge(Rsync, Emitter) { return this } - set (option={},value) { - if (typeof option === 'string') super.set(option,value) // pass through - else { - // if (!Array.isArray(option)) { option = [option] } - option.forEach( opt => { - console.log('---',option,opt) - typeof opt==='string' ? super.set(opt) : super.set(...Object.entries(opt).flat()) - }) - } - return this - } - - - async loadJob (options) { - if (typeof options ==='string') options = await this.readOptionsFile(options,'job') - if (!options.error) { - for (const option in options) { - if(option === 'optionsFile') { - let opts = await this.readOptionsFile(options.optionsFile,'options') - if (!opts.error) { - Object.keys(opts).forEach( opt => { - this.processOption(opt,opts[opt]) - }) - } - } - else { - this.processOption(option,options[option]) - } - } // end loop - this.dry() // dry run by default must .live() .unset('n') - } - } - - // executes a method on the instance (might be in prototype chain) which may take a value(s) - processOption (method, value) { - if (typeof(this[method]) === 'function') { - this[method](value) - } - - } - - queueJobs () {} - runJobs () {} - async execute(opts={}) { - log.info({cmd:this.command(), msg:'running rsync command'}) + // log.info({cmd:this.command(), msg:'running rsync command'}) const superexecute = super.execute.bind(this) return new Promise((resolve, reject) => { let status @@ -161,7 +232,7 @@ class Sync extends merge(Rsync, Emitter) { reject ({error:err, code:code, cmd:cmd, msg:'error during sync'}) } if (errors) log.warn({errors: errors, cmd:cmd, msg:'sync ran but with with errors'}) - log.info({cmd:cmd, status:status, msg:'sync run'}) + log.debug({cmd:cmd, status:status, msg:'sync run'}) resolve({cmd:cmd, errors:errors, status:status, msg:'sync run'}) }, function(data) { status += data.toString() @@ -180,11 +251,9 @@ class Sync extends merge(Rsync, Emitter) { export default Sync - - -// function isPlainObject (obj) { -// return Object.prototype.toString.call(obj) === '[object Object]' -// } +function isPlainObject (obj) { + return Object.prototype.toString.call(obj) === '[object Object]' +} // // function escapeSpaces (str) { // if (typeof str === 'string') { diff --git a/src/watcher.js b/src/watcher.js index f7a3ad8..820f407 100644 --- a/src/watcher.js +++ b/src/watcher.js @@ -1,41 +1,133 @@ +// native imports import { EventEmitter as Emitter } from 'events' import path from 'path' -import chokidar from 'chokidar' +// third party imports +import { watch } from 'chokidar' +// local imports +import ignores from './read-lines' +// UCI imports +import logger from '@uci/logger' +let log = {} + + +const READY_TIMEOUT = 2000 class Watcher extends Emitter { - constructor(options) { + constructor(opts={}) { super() - // pass in ignores - const opts = { - ignored: '**/node_modules/**', - ignoreInitial: true - } - const watcher = chokidar.watch(options.source, opts) - this.watcher = watcher + log = logger({ package:'@uci/sync', class:'Watcher', file:'src/watcher.js'}) + opts.unlinkDir = Object.hasOwnProperty(opts.unlinkDir) ? opts.unlinkDir : true + this.opts = opts + this._ignored = [] + this._ready=false + this.watching=false + return this } - start() { - const handler = (type, f) => { - const fname = path.basename(f) - if ( fname.toLowerCase() === 'package.json') - if (type !=='change') { - this.emit('error',new Error('package.json was added or removed, ignoring sync and reinstall')) - return - } else{ - this.emit('install', f) - } - this.emit('sync', f) - } // end handler - this.watcher - .on('add', handler.bind(this, 'add')) - .on('change', handler.bind(this, 'change')) - .on('unlink', handler.bind(this, 'remove')) + async init(opts) { + return new Promise(async (resolve, reject) => { + opts = opts || this.opts + if (opts.excludeFrom) await this.getIgnoreLists(opts.excludeFrom) + if (opts.ignoreList) await this.getIgnoreLists(opts.ignoreList) + if (opts.source) { + opts.ignored = opts.ignored ? [...this._ignored,...opts.ignored] : this._ignored + log.debug({ignored:opts.ignored, msg:'all ignores'}) + this._watcher = watch(opts.source,opts) + this._watcher.on('error', error => { + log.error({error:error, msg:'Watcher error'}) + }) + this._watcher.on('ready', () => { + clearTimeout(readyTimeout) + log.info('initial scan sucessful, ready to start') + this._ready=true + this.opts = opts // save options + resolve() + }) + let readyTimeout = setTimeout(() =>{ + log.fatal({options:opts, timeout:READY_TIMEOUT, msg:'Timeout: unabled to complete initial scan'}) + reject('timeout during intial scan') + },READY_TIMEOUT) + } + else { + log.fatal('MUST provide a source directory(s) option to watch') + reject('no source provided') + } + }) + } + + + async start(opts) { + if(this.watching) { + log.warn(`watching aleady running for ${this.opts.source}`) + return false + } + if (!this._watcher) await this.init(opts) + if (this._ready) { + log.info(`now watching ${this.opts.source}`) + this._watcher.removeAllListeners() // just in case + this.watching = true + // define command listen handler + const handler = + (type, f) => { + log.debug(`file ${f} was ${type}`) + // convert this to a plugin/hook so it's not specific + const fname = path.basename(f) + if ( fname.toLowerCase() === 'package.json') + if (type !=='modified') { + this.emit('error',new Error('package.json was added or removed, ignoring sync and reinstall')) + return + } else{ + this.emit('install', f) + } + // user might want to run debounce on the listener for this event + this.emit('changed', {file:f, type:type}) + } // end handler + + this._watcher + .on('add', handler.bind(this, 'added')) + .on('change', handler.bind(this, 'modified')) + .on('unlink', handler.bind(this, 'removed')) + if(this.opts.unlinkDir) this._watcher.on('unlinkDir', handler.bind(this, 'dir-deleted')) + if(this.opts.addDir) this._watcher.on('addDir', handler.bind(this, 'dir-added')) + } else { + log.warn('watcher is not ready to start, check options and try again') + return new Error('not ready to start check configuration') + } } stop() { - this.watcher.close() + if(this.watching) { + this.watching = false + this._watcher.close() + } + else log.warn('not watching, nothing to close') } + remove() { + this.stop() + delete(this._watcher) + this._ready=false + } + + async restart(opts) { + this.remove() + await this.start(opts) + } + + async getIgnoreLists(lists) { + // console.log('ignore lists', lists) + if (typeof lists === 'string') lists=[lists] + let ignored = await ignores(lists) + // console.log('==ignores from lists', ignored) + this._ignored = [...this._ignored,...ignored] + // console.log(this._ignored) + } + + addIgnore(ignore) { + Array.isArray(ignore) ? this._ignored.push(...ignore) : this._ignored.push(ignore) + } + clearAllIgnore() { this._ignored = [] } + } export default Watcher diff --git a/test/jobs/local.yaml b/test/jobs/local.yaml index ce6c5bf..895b2ae 100644 --- a/test/jobs/local.yaml +++ b/test/jobs/local.yaml @@ -3,6 +3,11 @@ destination: ./test/dest flags: 'av' excludeFrom: - ./test/repo/.gitignore + - ./test/repo/.npmignore +exclude: + - .gitignore + - .npmignore set: - delete - delete-excluded +watch: diff --git a/test/read-lines.test.js.off b/test/read-lines.test.js.off new file mode 100644 index 0000000..8baad10 --- /dev/null +++ b/test/read-lines.test.js.off @@ -0,0 +1,54 @@ +// let ignoreFiles = ['.npmignore','.gitignore'] + +import { readLines, writeLines } from '../src/read-lines' +import chai from 'chai' +import assertArrays from 'chai-arrays' +import { it } from 'mocha' + +chai.use(assertArrays) +const expect = chai.expect + +describe ( + 'Read a File of Lines to Array and vice versa', + function () { + readList() + writeList() + }) + +//****************** TESTS ********************** +function readList() { + it('==> can read one or more files (no order) each line as element in an array with common directory', async function () { + const shouldbe = [ 'tests/', + 'test/', + '*.test.js', + 'testing/', + 'example/', + '/node_modules/', + '/coverage/' ] + let result = await readLines(['.gitignore','.npmignore'],__dirname+'/repo') + expect(result, 'list build failed').to.be.containingAllOf(shouldbe) + }) + + it('==> can read two files one relative the other absolute', async function () { + const shouldbe = [ 'tests/', + 'test/', + '*.test.js', + 'testing/', + 'example/', + '/node_modules/', + '/coverage/' ] + let result = await readLines(['./test/repo/.gitignore',__dirname+'/repo/.npmignore']) + expect(result, 'list build failed').to.be.containingAllOf(shouldbe) + }) +} + + +function writeList() { + + it('==> can write an array items as lines in a file', async function () { + const shouldbe = await readLines(['.gitignore','.npmignore'],__dirname+'/repo') + await writeLines(__dirname+'/repo/combined.list',shouldbe) + const result = await readLines(['combined.list'],__dirname+'/repo') + expect(result, 'list build failed').to.be.containingAllOf(shouldbe) + }) +} diff --git a/test/repo/.gitignore b/test/repo/.gitignore index 25fbf5a..28bfcba 100644 --- a/test/repo/.gitignore +++ b/test/repo/.gitignore @@ -1,2 +1,2 @@ -node_modules/ +**/node_modules/ coverage/ diff --git a/test/sync.test.js b/test/sync.test.js.off similarity index 97% rename from test/sync.test.js rename to test/sync.test.js.off index d61ff3d..428f07e 100644 --- a/test/sync.test.js +++ b/test/sync.test.js.off @@ -17,7 +17,7 @@ describe('Sync Class Testing ',async ()=> { // // sync.execute({cli:true}) // sync.live() // sync.execute({cli:true}) - await sync.loadJob('sample') + await sync.loadJob('local') console.log('command to be run',sync.command()) sync.live() sync.execute({cli:true}) diff --git a/test/watcher.test.js b/test/watcher.test.js new file mode 100644 index 0000000..08ddb57 --- /dev/null +++ b/test/watcher.test.js @@ -0,0 +1,71 @@ +import Sync from '../src/sync' +import Watcher from '../src/watcher' +import to from 'await-to-js' +import { expect } from 'chai' +import { it } from 'mocha' +import debounce from 'debounce-fn' +import logger from '@uci/logger' +// pause = require('@uci/utils').pPause + +describe('Watcher Class Testing ',async ()=> { + + // let log = logger({}) + before(async () => { + // log = logger({ package:'@uci/sync', id: 'sync-test' }) + let sync = new Sync() + // await sync.loadJob('local') + // console.log('command to be run',sync.command()) + // console.log('===',sync.cwd()) + // // sync.execute({cli:true}) + await sync.loadJob('local') + sync.live() + let options = await sync.readOptionsFile('local') + let watcher = new Watcher(options) + await watcher.start() + watcher.on('changed', + // debounce( + (change) => { + console.log(`======= file ${change.file} was ${change.type} ==========`) + sync.execute({cli:true}) + } + // ,{wait:100, immediate:true}) + ) + + // console.log('command to be run',sync.command()) + // sync.live() + // sync.execute({cli:true}) + + // await sync.configure('./test/config/sync') + // log.info({cmd:sync.command(), msg:'Rsync Command that will Run'}) + // log.info(`making connection to ${remote.opts.host}`) + // log.info('ready for testing') + }) + + // after(async () => { + // remote.close() + // }) + + it('can sync a directory' , async function () { + // let [err,res] = await to(remote.exec('cd /opt && pwd')) + // if (err) { + // log.info('error running command aborting test', err) + // return + // } + // log.info(`result of remote command ${res.command} => ${res.reply.toString().trim()}`) + expect('test', 'test failed').to.equal('/opt') + }) + +}) + +// function hooks(remote) { +// + +// // beforeEach(async() => { +// // await someasyncfunctiontodobeforeeachtest() +// // }) +// +// // after(async() => { +// // await someasyncfunctiontodoaftereeachtest() +// // }) +// +// }