OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-overlay / test / conformance / verify.mjs

136 lines · javascript ref:
/**
 * Conformance vector VERIFIER — checks the fixtures against THIS build of
 * lib/messages.js.
 *
 * Two jobs, and the second is the one people forget:
 *
 *  1. Guard the fixtures. If lib/messages.js changes the wire format, this fails
 *     loudly instead of the C++ port silently being checked against a protocol
 *     the JS side no longer speaks. Regenerate deliberately, do not "fix" by
 *     regenerating reflexively — a diff here means peers on the old version can
 *     no longer talk to peers on the new one, which is a compatibility decision,
 *     not a test failure.
 *
 *  2. Document the contract a port must satisfy. Every check below is a check
 *     the native implementation has to pass too:
 *       DECODE  — fixture bytes decode to exactly the recorded value
 *       ENCODE  — re-encoding that value reproduces the fixture bytes exactly
 *                 (skipped for `truncated` cases, where a modern encoder
 *                 legitimately appends the trailing field a pre-field peer
 *                 omitted — the decode expectation is the point of those)
 *
 * Run: node test/conformance/verify.mjs
 */

import { readFileSync } from 'fs'
import { fileURLToPath } from 'url'
import { dirname, join } from 'path'
import * as messages from '../../lib/messages.js'

const __dir = dirname(fileURLToPath(import.meta.url))
const vectors = JSON.parse(readFileSync(join(__dir, 'vectors.json'), 'utf8'))
const pkg = JSON.parse(readFileSync(join(__dir, '../../package.json'), 'utf8'))

function encode (codec, value) {
  const state = { start: 0, end: 0, buffer: null }
  codec.preencode(state, value)
  state.buffer = Buffer.alloc(state.end)
  state.start = 0
  codec.encode(state, value)
  return state.buffer
}

function decode (codec, buf) {
  const state = { start: 0, end: buf.length, buffer: Buffer.from(buf) }
  return codec.decode(state)
}

function normalise (v) {
  if (Buffer.isBuffer(v)) return { $buf: v.toString('hex') }
  if (Array.isArray(v)) return v.map(normalise)
  if (v && typeof v === 'object') {
    const o = {}
    for (const k of Object.keys(v).sort()) o[k] = normalise(v[k])
    return o
  }
  return v
}

// The decoded form carries {$buf:hex} for buffers; turn it back into something
// the encoder accepts, so ENCODE is a real round trip and not a tautology.
function denormalise (v) {
  if (v && typeof v === 'object' && typeof v.$buf === 'string') return Buffer.from(v.$buf, 'hex')
  if (Array.isArray(v)) return v.map(denormalise)
  if (v && typeof v === 'object') {
    const o = {}
    for (const k of Object.keys(v)) o[k] = denormalise(v[k])
    return o
  }
  return v
}

let pass = 0
let fail = 0
const failures = []

if (vectors.overlayVersion !== pkg.version) {
  console.log(`  note  fixtures generated at ${vectors.overlayVersion}, package is ${pkg.version}`)
}

for (const t of vectors.cases) {
  const codec = messages[t.message]
  const where = `${t.message} / ${t.label}`
  if (!codec) {
    fail++; failures.push(`${where}: message codec no longer exists`)
    continue
  }
  const bytes = Buffer.from(t.bytes, 'hex')

  // DECODE
  let got
  try {
    got = normalise(decode(codec, bytes))
  } catch (e) {
    fail++; failures.push(`${where}: decode threw — ${e.message}`)
    continue
  }
  if (JSON.stringify(got) !== JSON.stringify(t.decoded)) {
    fail++
    failures.push(`${where}: decode mismatch\n      want ${JSON.stringify(t.decoded)}\n      got  ${JSON.stringify(got)}`)
    continue
  }

  // ENCODE (byte-exact), except where the fixture models a pre-field peer.
  if (!t.truncated) {
    let re
    try {
      re = encode(codec, denormalise(t.decoded))
    } catch (e) {
      fail++; failures.push(`${where}: re-encode threw — ${e.message}`)
      continue
    }
    if (re.toString('hex') !== t.bytes) {
      fail++
      failures.push(`${where}: encode mismatch\n      want ${t.bytes}\n      got  ${re.toString('hex')}`)
      continue
    }
  }
  pass++
}

const covered = new Set(vectors.cases.map(c => c.message))
const all = Object.keys(messages)
const uncovered = all.filter(m => !covered.has(m))

console.log(`hyper-overlay conformance — ${vectors.protocol} @ ${vectors.overlayVersion}`)
console.log(`  ${pass}/${vectors.cases.length} cases pass`)
console.log(`  ${covered.size}/${all.length} message types covered`)
if (uncovered.length) console.log(`  UNCOVERED: ${uncovered.join(', ')}`)
for (const f of failures) console.log(`  FAIL  ${f}`)

// An uncovered message is a hole a port can fall through unnoticed, so it fails
// the run rather than printing a warning nobody reads.
const ok = fail === 0 && uncovered.length === 0
console.log(ok ? 'PASS' : 'FAIL')
process.exit(ok ? 0 : 1)