test-dag.js 4.98 KB
const fs = require('fs').promises
const path = require('path')
const test = require('ava')
const {copyFiles, execFilePromise, matchOutput, wait} = require('./util')
const recipes = require('./dag-makefile')

process.chdir(__dirname)

const FILES_DIR = 'dag-files'
const DAG_SCRATCH_DIR = 'dag-scratch'
const DAG_EXPECTED_DIR = 'dag-expected'

const outputs = recipes.map(({target, dependencies, command}) => ({
  target,
  dependencies,
  lines: [command.join(' ')]
}))

function touch(file) {
  const now = Date.now()
  return fs.utimes(file, now, now)
}

async function makeDag(dir, ...targets) {
  const {stdout} = await execFilePromise(
    'node', ['../../make.js', '../dag-makefile.js', ...targets],
    {cwd: path.resolve(__dirname, dir)}
  )
  return stdout
}

test.serial('dag from scratch', async t => {
  // Build target A
  await copyFiles(FILES_DIR, DAG_SCRATCH_DIR)
  const output = await makeDag(DAG_SCRATCH_DIR, 'a.txt')

  // Check that the result is correct
  let result
  try { result = await fs.readFile(`${DAG_SCRATCH_DIR}/a.txt`, 'utf8') }
  catch { throw new Error('a.txt was not created') }
  t.deepEqual(
    result,
    await fs.readFile(`${DAG_EXPECTED_DIR}/a.txt`, 'utf8'),
    `File ${DAG_SCRATCH_DIR}/a.txt has the wrong contents`
  )

  // Check that the output from make.js is valid
  matchOutput(output, outputs)
})

test('dag build again', async t => {
  // Copy files from the full build
  const dir = 'dag-rebuild'
  await copyFiles(DAG_SCRATCH_DIR, dir)

  // Store the initial modified times of all files
  const files = await fs.readdir(dir)
  const initialMtimes = await Promise.all(files.map(async file => {
    const {mtimeMs} = await fs.stat(`${dir}/${file}`)
    return {file, mtimeMs}
  }))

  // Wait 100 ms and rebuild so we can see which modified times changed
  await wait(100)
  const output = await makeDag(dir, 'a.txt')

  // Check that the result is correct
  let result
  try { result = await fs.readFile(`${dir}/a.txt`, 'utf8') }
  catch { throw new Error('a.txt was not created') }
  t.deepEqual(
    result,
    await fs.readFile(`${DAG_EXPECTED_DIR}/a.txt`, 'utf8'),
    `File ${dir}/a.txt has the wrong contents`
  )

  // Check that no files were modified
  await Promise.all(initialMtimes.map(async ({file, mtimeMs}) => {
    const {mtimeMs: newMtime} = await fs.stat(`${dir}/${file}`)
    t.deepEqual(mtimeMs, newMtime, `File ${file} should not have been rebuilt`)
  }))

  // Check that the output from make.js is valid
  t.deepEqual(output, '', 'Unexpected make.js output')
})

test('dag touch F and H', async t => {
  // Copy files from the full build
  const dir = 'dag-touch'
  await copyFiles(DAG_SCRATCH_DIR, dir)

  // Change the modified times of F and H
  await wait(100)
  await Promise.all([touch(`${dir}/f.txt`), touch(`${dir}/h.txt`)])

  // Store the initial modified times of all files
  const files = await fs.readdir(dir)
  const initialMtimes = await Promise.all(files.map(async file => {
    const {mtimeMs} = await fs.stat(`${dir}/${file}`)
    return {file, mtimeMs}
  }))

  // Wait 100 ms and rebuild so we can see which modified times changed
  await wait(100)
  const output = await makeDag(dir, 'a.txt')

  // Check that the result is correct
  let result
  try { result = await fs.readFile(`${dir}/a.txt`, 'utf8') }
  catch { throw new Error('a.txt was not created') }
  t.deepEqual(
    result,
    await fs.readFile(`${DAG_EXPECTED_DIR}/a.txt`, 'utf8'),
    `File ${dir}/a.txt has the wrong contents`
  )

  // Check that the correct files were modified
  const expectedRebuilt = new Set(['a.txt', 'b.txt', 'c.txt', 'd.txt', 'e.txt'])
  await Promise.all(initialMtimes.map(async ({file, mtimeMs}) => {
    const {mtimeMs: newMtime} = await fs.stat(`${dir}/${file}`)
    if (expectedRebuilt.has(file)) {
      t.assert(newMtime > mtimeMs, `File ${file} should have been rebuilt`)
    }
    else t.deepEqual(mtimeMs, newMtime, `File ${file} should not have been rebuilt`)
  }))

  // Check that the output from make.js is valid
  matchOutput(output, outputs.filter(({target}) => expectedRebuilt.has(target)))
})

test('dag B and D from scratch', async t => {
  // Build targets B and D
  const dir = 'dag-b-and-d'
  await copyFiles(FILES_DIR, dir)
  const output = await makeDag(dir, 'b.txt', 'd.txt')

  // Check that results are correct
  await Promise.all(['b.txt', 'd.txt'].map(async file => {
    let result
    try { result = await fs.readFile(`${dir}/${file}`, 'utf8') }
    catch { throw new Error('${file} was not created') }
    t.deepEqual(
      result,
      await fs.readFile(`${DAG_EXPECTED_DIR}/${file}`, 'utf8'),
      `File ${dir}/${file} has the wrong contents`
    )
  }))

  // Check that A and C weren't built
  await Promise.all(['a.txt', 'c.txt'].map(async file => {
    // access() should reject
    try { await fs.access(`${dir}/${file}`) }
    catch { return }

    t.fail(`File ${dir}/${file} should not have been built`)
  }))

  // Check that the output from make.js is valid
  matchOutput(output, outputs.filter(({target}) =>
    !['a.txt', 'c.txt'].includes(target)
  ))
})