mutode.js

  1. const async = require('async')
  2. const babylon = require('babylon')
  3. const spawn = require('child_process').spawn
  4. const debug = require('debug')('mutode')
  5. const del = require('del')
  6. const fs = require('fs')
  7. const globby = require('globby')
  8. const mkdirp = require('mkdirp')
  9. const os = require('os')
  10. const path = require('path')
  11. const prettyMs = require('pretty-ms')
  12. const copyDir = require('recursive-copy')
  13. const stripAnsi = require('strip-ansi')
  14. const {promisify} = require('util')
  15. const readFile = promisify(fs.readFile)
  16. /**
  17. * Mutode's main class
  18. */
  19. class Mutode {
  20. /**
  21. * Create a new Mutode instance
  22. * @param opts {Object}
  23. * @param {array<string>} [opts.paths = ['index.js', 'src/']] - Glob matched paths or files to mutate
  24. * @param {number} [opts.concurrency = # of cpu cores] - Number of concurrent workers
  25. * @param {array<string>} [opts.mutators = All]- Mutators to load (e.g. *deletion*)
  26. * @returns {Mutode} - Returns an instance of mutode
  27. */
  28. constructor ({paths = [], concurrency = os.cpus().length, mutators = ['*']} = {}) {
  29. if (!Array.isArray(paths)) paths = [paths]
  30. if (!Array.isArray(mutators)) mutators = [mutators]
  31. if (paths.length === 0) paths = ['index.js', 'src/']
  32. debug('Config:\n\tFile paths %o\n\tConcurrency: %s\n\tMutators: %s', paths, concurrency, mutators)
  33. Mutode.mkdir()
  34. this.filePaths = globby.sync(paths)
  35. debug('Globbed files %o', this.filePaths)
  36. if (this.filePaths.length === 0) {
  37. throw new Error('No files found in the specified paths')
  38. }
  39. this.id = `${Math.floor(Date.now() / 10000)}`
  40. this.npmCommand = /^win/.test(process.platform) ? 'npm.cmd' : 'npm'
  41. this.mutators = mutators
  42. this.concurrency = concurrency
  43. this.mutants = 0
  44. this.killed = 0
  45. this.survived = 0
  46. this.discarded = 0
  47. this.coverage = 0
  48. this.workers = {}
  49. for (let i = 0; i < this.concurrency; i++) {
  50. this.workers[i] = true
  51. }
  52. this.mutantsLogFile = fs.createWriteStream(path.resolve(`.mutode/mutants-${this.id}.log`), {flags: 'w'})
  53. this.logFile = fs.createWriteStream(path.resolve(`.mutode/mutode-${this.id}.log`), {flags: 'w'})
  54. this.mutantLog = string => this.mutantsLogFile.write(`${stripAnsi(string.trim())}\n`)
  55. console.logSame = s => {
  56. process.stdout.write(s)
  57. this.logFile.write(stripAnsi(s))
  58. }
  59. console.log = (s = '') => {
  60. process.stdout.write(`${s}\n`)
  61. this.logFile.write(`${stripAnsi(s.toString().trim())}\n`)
  62. }
  63. }
  64. /**
  65. * Run current instance
  66. * @returns {Promise} - Promise that resolves once this instance's execution is completed.
  67. */
  68. async run () {
  69. if (this.mutants > 0) throw new Error('This instance has already been executed')
  70. console.log(`Mutode ${this.id} running`)
  71. await this.delete()
  72. const startTime = process.hrtime()
  73. try {
  74. await this.copyFirst()
  75. this.mutators = await Mutode.loadMutants(this.mutators)
  76. this.timeout = await this.timeCleanTests()
  77. debug('Setting mutant runner timeout to %s seconds', this.timeout / 1000)
  78. this.copied = this.copy()
  79. await new Promise((resolve, reject) => {
  80. async.eachSeries(this.filePaths, this.fileProcessor(), this.done(resolve, reject))
  81. })
  82. } catch (e) {
  83. debug(e)
  84. throw e
  85. } finally {
  86. await this.delete()
  87. const endTime = process.hrtime(startTime)
  88. const endTimeMS = endTime[0] * 1e3 + endTime[1] / 1e6
  89. console.log(`Mutode ${this.id} finished. Took ${prettyMs(endTimeMS)}`)
  90. console.log()
  91. this.mutantsLogFile.end()
  92. this.logFile.end()
  93. }
  94. }
  95. /**
  96. * Function that returns an async function to process one file binded to the instance scope.
  97. * @private
  98. * @returns {function(filePath)} - Async function that runs instance's mutators and executes generated mutants for one file.
  99. */
  100. fileProcessor () {
  101. return async filePath => {
  102. debug('Creating mutants for %s', filePath)
  103. const before = this.mutants
  104. const queue = async.queue(async task => {
  105. const i = await this.freeWorker()
  106. debug('Running task in worker %d', i)
  107. await task(i)
  108. this.workers[i] = true
  109. debug('Finished running task in worker %d', i)
  110. }, this.concurrency)
  111. queue.pause()
  112. this.copied.then(() => {
  113. console.log(`Running mutants for ${filePath}`)
  114. queue.resume()
  115. })
  116. const fileContent = (await readFile(filePath)).toString()
  117. const lines = fileContent.split('\n')
  118. let ast
  119. try {
  120. ast = babylon.parse(fileContent)
  121. } catch (e) {
  122. try {
  123. ast = babylon.parse(fileContent, {sourceType: 'module'})
  124. } catch (e) {
  125. console.log(`Couldn't parse AST for file ${filePath}`)
  126. debug(e)
  127. throw e
  128. }
  129. }
  130. for (const mutator of this.mutators) {
  131. debug('Running mutator %s', mutator.name)
  132. const before = this.mutants
  133. await mutator({mutodeInstance: this, filePath, lines, queue, ast})
  134. const generated = this.mutants - before
  135. debug('Mutator %s generated %d mutants', mutator.name, generated)
  136. }
  137. const generated = this.mutants - before
  138. debug('%d mutants generated for %s', generated, filePath)
  139. await new Promise(resolve => {
  140. const resolveWhenDone = () => {
  141. for (let i = 0; i < this.concurrency; i++) {
  142. fs.writeFileSync(`.mutode/mutode-${this.id}-${i}/${filePath}`, fileContent) // Reset file to original content
  143. }
  144. console.log()
  145. setImmediate(resolve)
  146. }
  147. if (queue.length() === 0) {
  148. return this.copied.then(() => {
  149. resolveWhenDone()
  150. })
  151. }
  152. debug('Adding drain function to queue %d', queue.length())
  153. queue.drain = () => {
  154. debug(`Finished %s`, filePath)
  155. resolveWhenDone()
  156. }
  157. })
  158. }
  159. }
  160. /**
  161. * Promise handler that returns a function that runs when mutants execution is completed.
  162. * @private
  163. * @param resolve {function} - Promise resolve handler
  164. * @returns {function} - Function that runs when mutants execution is completed.
  165. */
  166. done (resolve, reject) {
  167. return err => {
  168. if (err) {
  169. debug(err)
  170. return reject(err)
  171. }
  172. console.log(`Out of ${this.mutants} mutants, ${this.killed} were killed, ${this.survived} survived and ${this.discarded} were discarded`)
  173. this.coverage = +((this.mutants > 0 ? this.killed : 1) / ((this.mutants - this.discarded) || 1) * 100).toFixed(2)
  174. console.log(`Mutant coverage: ${this.coverage}%`)
  175. setImmediate(resolve)
  176. }
  177. }
  178. /**
  179. * Function that returns the index of the first worker that is free.
  180. * @private
  181. */
  182. async freeWorker () {
  183. for (let i = 0; i < this.concurrency; i++) {
  184. if (this.workers[i]) {
  185. this.workers[i] = false
  186. return i
  187. }
  188. }
  189. }
  190. /**
  191. * Times the AUT's test suite execution.
  192. * @private
  193. * @returns {Promise} - Promise that resolves once AUT's test suite execution is completed.
  194. */
  195. async timeCleanTests () {
  196. console.log(`Verifying and timing your test suite`)
  197. const start = +new Date()
  198. const child = spawn(this.npmCommand, ['test'], {cwd: path.resolve(`.mutode/mutode-${this.id}-0`)})
  199. child.stderr.on('data', data => {
  200. debug(data.toString())
  201. })
  202. child.stdout.on('data', data => {
  203. debug(data.toString())
  204. })
  205. return new Promise((resolve, reject) => {
  206. child.on('exit', code => {
  207. if (code !== 0) return reject(new Error('Test suite most exit with code 0 with no mutants for Mutode to continue'))
  208. const diff = +new Date() - start
  209. const timeout = Math.max(Math.ceil(diff / 1000) * 2500, 5000)
  210. console.log(`Took ${(diff / 1000).toFixed(2)} seconds to run full test suite\n`)
  211. resolve(timeout)
  212. })
  213. })
  214. }
  215. /**
  216. * Synchronous load of mutators.
  217. * @private
  218. * @returns {Promise} - Promise that resolves with the loaded mutators
  219. */
  220. static async loadMutants (mutatorsNames) {
  221. console.logSame('Loading mutators... ')
  222. let mutatorsPaths = mutatorsNames.map(m => `mutators/${m}Mutator.js`)
  223. const mutators = []
  224. const mutatorsPath = path.resolve(__dirname, 'mutators/')
  225. mutatorsPaths = await globby(mutatorsPaths, {cwd: __dirname, absolute: true})
  226. for (const mutatorPath of mutatorsPaths) {
  227. debug('Loaded mutator %s', mutatorPath.replace(mutatorsPath + '/', '').replace('Mutator.js', ''))
  228. mutators.push(require(path.resolve(mutatorPath)))
  229. }
  230. console.log('Done\n')
  231. return mutators
  232. }
  233. /**
  234. * Creates a an exact copy of the AUT.
  235. * @private
  236. * @returns {Promise} - Promise that resolves once the copy is created.
  237. */
  238. async copyFirst () {
  239. console.logSame(`Creating a copy of your module... `)
  240. await copyDir('./', `.mutode/mutode-${this.id}-0`, {dot: true, filter: p => !p.startsWith('.')})
  241. console.log('Done\n')
  242. }
  243. /**
  244. * Creates <i>this.concurrency<i/> exact copies of the AUT.
  245. * @private
  246. * @returns {Promise} - Promise that resolves once the copies are created.
  247. */
  248. async copy () {
  249. if (this.concurrency === 1) return
  250. console.logSame(`Creating ${this.concurrency - 1} extra copies of your module... `)
  251. for (let i = 1; i < this.concurrency; i++) {
  252. console.logSame(`${i}.. `)
  253. await copyDir('./', `.mutode/mutode-${this.id}-${i}`, {dot: true, filter: p => !p.startsWith('.')})
  254. }
  255. console.log('Done\n')
  256. }
  257. /**
  258. * Creates the <i>.mutode</i> folder to save logs.
  259. * @private
  260. */
  261. static mkdir () {
  262. mkdirp.sync(path.resolve('.mutode'))
  263. }
  264. /**
  265. * Deletes available copies of the AUT.
  266. * @private
  267. * @returns {Promise} - Promise that resolves once copies have been deleted.
  268. */
  269. async delete () {
  270. const toDelete = await globby(`.mutode/*`, {dot: true, onlyDirectories: true})
  271. if (toDelete.length === 0) return
  272. console.logSame('Deleting copies...')
  273. for (const path of toDelete) {
  274. await del(path, {force: true})
  275. }
  276. console.log('Done\n')
  277. }
  278. }
  279. /**
  280. * @module Mutators
  281. */
  282. module.exports = Mutode