OverviewFilesCommitsIssuesDocsReleasesPeers

codetree / test / derive.test.js

224 lines · javascript ref:
import { test } from 'brittle'
import { resolve, dirname } from 'path'
import { fileURLToPath } from 'url'
import fs from 'fs'
import { deriveFile } from '../lib/derive.js'

const HERE = dirname(fileURLToPath(import.meta.url))
const FIXTURE = resolve(HERE, 'fixtures/derive')

// Ground truth for fixtures/derive/sample.js, counted by hand from the source.
//
// tally()
//   !rows                    unary op        + GUARD zone (return null)
//   return null              return op       + exit -> function output
//   for (const row of rows)  FOREACH zone, item 'row', carried ['total']
//   row.skip                 collapses to a socket label (R5), not a node
//   continue                 exit -> the foreach zone's entry, in a GUARD zone
//   total + row.count        binary op
//   total = ...              assign op (a new version of the 'total' wire)
//   total > limit            binary op       + GUARD zone (break)
//   break                    exit -> the foreach zone's exit
//   total > 0                binary op
//   if/else                  TWO-EXIT zone; BOTH paths exit, so no convergence
//   return total, return 0   two return ops  + two exits
//
// label()
//   Date.now()               call op, 'Date' is a FREE wire
//   `${row.name}: ...`       one template op (R5: glue folds in)
//   format(...)              call op, 'format' is an IMPORT wire
//   return                   return op + exit  <- the FOURTH return in the file
//
// Totals: 2 functions, 12 operation nodes, 5 zones, 6 exits, 2 named wires
//         ('total', 'at' — a loop binding is the zone's item, not a named wire)

function derive (name) {
  const path = resolve(FIXTURE, name)
  return deriveFile(fs.readFileSync(path, 'utf8'), path)
}

test('derive: the counts the fixture documents', (t) => {
  const m = derive('sample.js')

  t.absent(m.error, 'no error')
  t.is(m.functions.length, 2, '2 functions')
  t.is(m.nodes.length, 12, '12 operation nodes')
  t.is(m.zones.length, 5, '5 zones')
  t.is(m.exits.length, 6, '6 exits — four returns, one continue, one break')
  t.is(m.names.length, 2, '2 named wires')
})

test('derive: R2 — a reference or literal is never a node', (t) => {
  const m = derive('sample.js')
  const kinds = m.nodes.map(n => n.kind)

  t.absent(kinds.includes('Identifier'), 'no Identifier node')
  t.absent(kinds.includes('Literal'), 'no Literal node')
  t.absent(kinds.includes('MemberExpression'), 'no MemberExpression node (R5)')

  // `!rows` is ONE node, and `rows` is a wire into it.
  const unary = m.nodes.find(n => n.kind === 'UnaryExpression')
  t.ok(unary, 'the unary op is a node')
  t.is(unary.inputs.length, 1, 'one input')
  t.is(unary.inputs[0].name, 'rows', 'fed by the rows wire')
  t.is(unary.inputs[0].source, 'param', 'which resolves to a parameter')
})

test('derive: R5 — a property path folds into a socket label', (t) => {
  const m = derive('sample.js')
  const add = m.nodes.find(n => n.kind === 'BinaryExpression' && n.label === '+')
  t.ok(add, 'found the + node')
  const labels = add.inputs.map(i => i.name)
  t.ok(labels.includes('total'), 'left input is the total wire')
  t.ok(labels.includes('row.count'), 'right input is the folded path row.count')
})

test('derive: R3 — a declaration names a wire, literals sit in the socket', (t) => {
  const m = derive('sample.js')
  const names = m.names.map(n => n.name).sort()
  t.alike(names, ['at', 'total'], 'total and at')

  const total = m.names.find(n => n.name === 'total')
  t.is(total.from, 'literal', 'total is named on a literal, not an operation')

  const at = m.names.find(n => n.name === 'at')
  t.is(at.from, 'node', 'at is named on an operation')
  t.ok(at.node, 'and points at it')
})

test('derive: Z1 — three conditional shapes', (t) => {
  const m = derive('sample.js')
  const shapes = {}
  for (const z of m.zones) shapes[z.shape] = (shapes[z.shape] || 0) + 1

  t.is(shapes.guard, 3, '3 guards')
  t.is(shapes.foreach, 1, '1 foreach loop')
  t.is(shapes['two-exit'], 1, '1 two-exit conditional')
  t.absent(shapes['single-branch'], 'no single-branch conditional in this fixture')
})

test('derive: Z4 — every exit lands on an enclosing boundary', (t) => {
  const m = derive('sample.js')
  const byKind = {}
  for (const e of m.exits) {
    byKind[e.kind] = (byKind[e.kind] || 0) + 1
    t.ok(e.lands, e.kind + ' has a landing site')
    t.ok(e.lands.on, e.kind + ' landing names what it lands on')
  }
  t.is(byKind.return, 4, '4 returns')
  t.is(byKind.continue, 1, '1 continue')
  t.is(byKind.break, 1, '1 break')

  const loop = m.zones.find(z => z.shape === 'foreach')
  const cont = m.exits.find(e => e.kind === 'continue')
  const brk = m.exits.find(e => e.kind === 'break')

  t.is(cont.lands.on, 'zone-entry', 'continue lands on a zone entry')
  t.is(cont.lands.id, loop.id, 'specifically the enclosing loop')
  t.is(brk.lands.on, 'zone-exit', 'break lands on a zone exit')
  t.is(brk.lands.id, loop.id, 'specifically the enclosing loop')

  for (const r of m.exits.filter(e => e.kind === 'return')) {
    t.is(r.lands.on, 'function-output', 'a return lands on the function output')
  }
})

test('derive: Z3 — carried state is the zone output', (t) => {
  const m = derive('sample.js')
  const loop = m.zones.find(z => z.shape === 'foreach')
  t.alike(loop.carried, ['total'], 'the loop carries total')
  t.is(loop.item, 'row', 'and binds row as its item')

  // Both branches return, so nothing converges out of the if/else.
  const cond = m.zones.find(z => z.shape === 'two-exit')
  t.alike(cond.carried, [], 'the two-exit zone converges nothing')
})

test('derive: a return is both an operation and an exit, sharing identity', (t) => {
  const m = derive('sample.js')
  const returnNodes = m.nodes.filter(n => n.kind === 'ReturnStatement')
  t.is(returnNodes.length, 4, '4 return nodes')
  for (const r of returnNodes) {
    const exit = m.exits.find(e => e.node === r.id)
    t.ok(exit, 'each return node has the exit that shares its id')
  }
})

test('derive: E4 — zone nesting is a tree', (t) => {
  const m = derive('sample.js')
  const byId = new Map(m.zones.map(z => [z.id, z]))

  for (const z of m.zones) {
    if (z.parent === null) continue
    const p = byId.get(z.parent)
    t.ok(p, 'parent exists in the model')
    t.ok(p.range.startByte <= z.range.startByte && p.range.endByte >= z.range.endByte,
      'parent range contains the child')
    t.is(z.depth, p.depth + 1, 'depth is parent depth + 1')
  }

  const loop = m.zones.find(z => z.shape === 'foreach')
  const inner = m.zones.filter(z => z.parent === loop.id)
  t.is(inner.length, 2, 'the two inner guards are children of the loop')
})

test('derive: E5 — a reference resolves or is marked', (t) => {
  const m = derive('sample.js')
  const sources = {}
  for (const n of m.nodes) {
    for (const i of n.inputs) {
      if (i.source) sources[i.source] = (sources[i.source] || 0) + 1
    }
  }
  t.ok(sources.free > 0, 'Date is recorded as free')
  t.ok(sources.import > 0, 'format is recorded as an import binding')
  t.ok(sources.param > 0, 'rows/limit resolve to parameters')
  t.ok(sources.local > 0, 'total/at resolve to locals')

  for (const n of m.nodes) {
    for (const i of n.inputs) {
      if (i.literal) continue
      t.ok(i.source, 'every non-literal input names where it came from')
    }
  }
})

test('derive: E2 — everything carries a position', (t) => {
  const m = derive('sample.js')
  for (const list of [m.nodes, m.zones, m.exits, m.names]) {
    for (const item of list) {
      t.ok(item.range, 'has a range')
      t.ok(Number.isInteger(item.range.startByte), 'has startByte')
      t.ok(Number.isInteger(item.range.endByte), 'has endByte')
      t.ok(item.range.startLine >= 1, 'has a 1-based startLine')
      t.ok(item.range.endLine >= item.range.startLine, 'endLine follows startLine')
    }
  }
})

test('derive: R7 — identity is derived from the byte range', (t) => {
  const m = derive('sample.js')
  for (const n of m.nodes) {
    t.is(n.id, m.filePath + ':' + n.range.startByte + '-' + n.range.endByte, 'id is file:start-end')
  }
})

test('derive: E1 — the derivation is pure', (t) => {
  const a = derive('sample.js')
  const b = derive('sample.js')
  t.is(JSON.stringify(a), JSON.stringify(b), 'same source, byte-identical model')
})

test('derive: E6 — a parse failure is reported, not thrown', (t) => {
  let m = null
  try {
    m = derive('broken.js')
    t.pass('did not throw')
  } catch (err) {
    t.fail('threw on broken source: ' + err.message)
    return
  }
  t.ok(m.error, 'reports an error')
  t.alike(m.nodes, [], 'and no nodes')
})