Skip to content

Commit

Permalink
Add test_each
Browse files Browse the repository at this point in the history
  • Loading branch information
ehmicky committed May 17, 2019
1 parent 2d82fb0 commit 8146543
Show file tree
Hide file tree
Showing 8 changed files with 212 additions and 0 deletions.
22 changes: 22 additions & 0 deletions test/helpers/test_each/args.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { fastCartesian } from './fast_cartesian.js'

// Retrieve arguments passed to the main function for each iteration
export const getArgs = function(iterables) {
const args = fastCartesian(...iterables)
const argsA = args.map(invokeArgs)
return argsA
}

// If an argument is a function, its return value will be used instead.
// This can be used to generate random input for example (fuzzy testing).
const invokeArgs = function(eachArgs) {
return eachArgs.map(invokeArg)
}

const invokeArg = function(arg) {
if (typeof arg === 'function') {
return arg()
}

return arg
}
39 changes: 39 additions & 0 deletions test/helpers/test_each/fast_cartesian.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Does a cartesian product on several iterables.
// Works with any iterable, including arrays, strings, generators, maps, sets.
export const fastCartesian = function(...iterables) {
iterables.forEach(validateIterable)

if (iterables.length === 0) {
return []
}

const result = []
iterate(iterables, result, [], 0)
return result
}

const validateIterable = function(iterable) {
if (iterable[Symbol.iterator] === undefined) {
throw new TypeError(`Argument must be iterable: ${iterable}`)
}
}

// We use imperative code as it faster than functional code, avoiding creating
// extra arrays. We try re-use and mutate arrays as much as possible.
// We need to make sure callers parameters are not mutated though.
/* eslint-disable max-params, fp/no-loops, fp/no-mutating-methods */
const iterate = function(iterables, result, values, index) {
const iterable = iterables[index]

if (iterable === undefined) {
result.push(values.slice())
return
}

for (const value of iterable) {
values.push(value)
iterate(iterables, result, values, index + 1)
values.pop()
}
}
/* eslint-enable max-params, fp/no-loops, fp/no-mutating-methods */
15 changes: 15 additions & 0 deletions test/helpers/test_each/input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Parse and validate main input
export const parseInput = function(inputArgs) {
const iterables = inputArgs.slice(0, -1)

const func = inputArgs[inputArgs.length - 1]
validateFunc(func)

return { iterables, func }
}

const validateFunc = function(func) {
if (typeof func !== 'function') {
throw new TypeError(`Last argument must be a function: ${func}`)
}
}
18 changes: 18 additions & 0 deletions test/helpers/test_each/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { parseInput } from './input.js'
import { getArgs } from './args.js'
import { getSuffixes } from './suffix.js'

// Repeat a function with a combination of arguments.
// Meant for test-driven development.
export const testEach = function(...inputArgs) {
const { iterables, func } = parseInput(inputArgs)

const args = getArgs(iterables)

const suffixes = getSuffixes(iterables)

const results = args.map((values, index) => func(suffixes[index], ...values))

// Can use `Promise.all(results)` if `func` is async
return { args, results }
}
49 changes: 49 additions & 0 deletions test/helpers/test_each/part.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { serializeArg } from './serialize.js'

// Transform args into suffix parts
export const reduceParts = function(args) {
const { parts } = args.reduce(reducePart, { parts: [], index: 0 })
return parts
}

const reducePart = function({ parts, index }, arg) {
const part = getPart(arg)
const { part: partA, index: indexA } = fixDuplicate({ parts, part, index })
const partsA = [...parts, partA]
return { parts: partsA, index: indexA }
}

const getPart = function(arg) {
const part = serializeArg(arg)
const partA = part.trim()
const partB = truncatePart(partA)
return partB
}

// Make suffix parts short by truncating them
const truncatePart = function(part) {
if (part.length <= MAX_PART_LENGTH) {
return part
}

const partA = part.slice(0, MAX_PART_LENGTH)
return `${partA}...`
}

const MAX_PART_LENGTH = 60

// Ensure suffix parts are unique by appending an incrementing counter when we
// find duplicates
const fixDuplicate = function({ parts, part, index }) {
if (!isDuplicate({ parts, part })) {
return { part, index }
}

const indexA = index + 1
const partA = `${part} ${indexA}`
return { part: partA, index: indexA }
}

const isDuplicate = function({ parts, part }) {
return parts.some(partA => partA === part)
}
41 changes: 41 additions & 0 deletions test/helpers/test_each/serialize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { inspect } from 'util'

import { isPlainObject } from './utils.js'

// Serialize an argument so it can be used as a suffix
export const serializeArg = function(arg) {
// `{ suffix }` can be used to override the default suffix
if (isPlainObject(arg) && typeof arg.suffix === 'string') {
return arg.suffix
}

return serializeValue(arg)
}

const serializeValue = function(value) {
if (typeof value === 'string') {
return value
}

if (typeof value === 'function') {
return serializeFunction(value)
}

return inspect(value, INSPECT_OPTS)
}

const serializeFunction = function(func) {
if (func.name === '') {
return 'function'
}

return func.name
}

// Make suffix short and on a single line
const INSPECT_OPTS = {
breakLength: Infinity,
depth: 1,
maxArrayLength: 3,
compact: true,
}
20 changes: 20 additions & 0 deletions test/helpers/test_each/suffix.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { fastCartesian } from './fast_cartesian.js'
import { reduceParts } from './part.js'

// Retrieve unique suffixes for each iteration
export const getSuffixes = function(iterables) {
const parts = iterables.map(getParts)
const partsA = fastCartesian(...parts)
const suffixes = partsA.map(joinParts)
return suffixes
}

const getParts = function(iterable) {
const args = [...iterable]
return reduceParts(args)
}

const joinParts = function(parts) {
const suffix = parts.join(' ')
return `| ${suffix}`
}
8 changes: 8 additions & 0 deletions test/helpers/test_each/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Is a plain object, including `Object.create(null)`
export const isPlainObject = function(value) {
return (
typeof value === 'object' &&
value !== null &&
(value.constructor === Object || value.constructor === undefined)
)
}

0 comments on commit 8146543

Please sign in to comment.