Commit a4c2fd6f authored by Caleb C. Sander's avatar Caleb C. Sander
Browse files

Add concurrency tests

parent dda1ba67
No related merge requests found
Showing with 220 additions and 47 deletions
+220 -47
......@@ -6,10 +6,8 @@ The spec for this project can be found at [make.md](https://gitlab.caltech.edu/c
## Testing infrastructure
Your submission will be run against the tests in the `tests` directory.
If your submission passes all the tests and you have not intentionally written your program to defeat the test cases, you will receive full credit on the project.
To run the tests, you need to run `npm install` once and then `npm test` each time you want to run the tests.
Several tests are provided in the `tests` directory.
To run them, you need to run `npm install` once and then `npm test` each time you want to run the tests.
## Making a real project
......
module.exports = [
{
target: 'sleep1',
dependencies: ['sleep1-long', 'sleep1-short', 'sleep1-medium'],
command: ['node', 'sleep.js', '3.1']
},
{
target: 'sleep1-short',
dependencies: ['sleep.js'],
command: ['node', 'sleep.js', '1']
},
{
target: 'sleep1-medium',
dependencies: ['sleep.js'],
command: ['node', 'sleep.js', '3']
},
{
target: 'sleep1-long',
dependencies: ['sleep.js'],
command: ['node', 'sleep.js', '5']
},
{
target: 'sleep2',
dependencies: ['sleep2-medium', 'sleep2-short', 'sleep2-long'],
command: ['node', 'sleep.js', '1.1']
},
{
target: 'sleep2-short',
dependencies: ['sleep.js'],
command: ['node', 'sleep.js', '2']
},
{
target: 'sleep2-medium',
dependencies: ['sleep.js'],
command: ['node', 'sleep.js', '4']
},
{
target: 'sleep2-long',
dependencies: ['sleep.js'],
command: ['node', 'sleep.js', '6']
}
]
const seconds = Number(process.argv[2])
setTimeout(_ => console.log(`Slept for ${seconds} seconds`), seconds * 1e3)
const test = require('ava')
const {execFilePromise, matchOutput} = require('./util')
process.chdir(__dirname)
async function make(...targets) {
const {stdout} = await execFilePromise(
'node', ['../make.js', 'concurrent-makefile.js', ...targets]
)
return stdout
}
test('make sleep1', async t => {
// Test that the 3 sleeps happen at the same time
const output = await make('sleep1')
matchOutput(output, [
{target: 'start-sleep-short', dependencies: [], lines: ['node sleep.js 1']},
{target: 'start-sleep-medium', dependencies: [], lines: ['node sleep.js 3']},
{target: 'start-sleep-long', dependencies: [], lines: ['node sleep.js 5']},
{
target: 'finish-sleep-short',
dependencies: ['start-sleep-short', 'start-sleep-medium', 'start-sleep-long'],
lines: ['Slept for 1 seconds']
},
{
target: 'finish-sleep-medium',
dependencies: ['finish-sleep-short'],
lines: ['Slept for 3 seconds']
},
{
target: 'finish-sleep-long',
dependencies: ['finish-sleep-medium'],
lines: ['Slept for 5 seconds']
},
{target: 'start-sleep1', dependencies: ['finish-sleep-long'], lines: ['node sleep.js 3.1']},
{
target: 'finish-sleep1',
dependencies: ['start-sleep1'],
lines: ['Slept for 3.1 seconds']
}
])
t.pass()
})
test('make sleep2', async t => {
// Test that the 3 sleeps happen at the same time
const output = await make('sleep2')
matchOutput(output, [
{target: 'start-sleep-short', dependencies: [], lines: ['node sleep.js 2']},
{target: 'start-sleep-medium', dependencies: [], lines: ['node sleep.js 4']},
{target: 'start-sleep-long', dependencies: [], lines: ['node sleep.js 6']},
{
target: 'finish-sleep-short',
dependencies: ['start-sleep-short', 'start-sleep-medium', 'start-sleep-long'],
lines: ['Slept for 2 seconds']
},
{
target: 'finish-sleep-medium',
dependencies: ['finish-sleep-short'],
lines: ['Slept for 4 seconds']
},
{
target: 'finish-sleep-long',
dependencies: ['finish-sleep-medium'],
lines: ['Slept for 6 seconds']
},
{target: 'start-sleep2', dependencies: ['finish-sleep-long'], lines: ['node sleep.js 1.1']},
{
target: 'finish-sleep2',
dependencies: ['start-sleep2'],
lines: ['Slept for 1.1 seconds']
}
])
t.pass()
})
for (const targets of [['sleep1', 'sleep2'], ['sleep2', 'sleep1']]) {
test(`make ${targets.join(' ')}`, async t => {
// Test that both targets are built at the same time
const output = await make(...targets)
matchOutput(output, [
{target: 'start-sleep1-short', dependencies: [], lines: ['node sleep.js 1']},
{target: 'start-sleep2-short', dependencies: [], lines: ['node sleep.js 2']},
{target: 'start-sleep1-medium', dependencies: [], lines: ['node sleep.js 3']},
{target: 'start-sleep2-medium', dependencies: [], lines: ['node sleep.js 4']},
{target: 'start-sleep1-long', dependencies: [], lines: ['node sleep.js 5']},
{target: 'start-sleep2-long', dependencies: [], lines: ['node sleep.js 6']},
{
target: 'finish-sleep1-short',
dependencies: [
'start-sleep1-short',
'start-sleep2-short',
'start-sleep1-medium',
'start-sleep2-medium',
'start-sleep1-long',
'start-sleep2-long'
],
lines: ['Slept for 1 seconds']
},
{
target: 'finish-sleep2-short',
dependencies: ['finish-sleep1-short'],
lines: ['Slept for 2 seconds']
},
{
target: 'finish-sleep1-medium',
dependencies: ['finish-sleep2-short'],
lines: ['Slept for 3 seconds']
},
{
target: 'finish-sleep2-medium',
dependencies: ['finish-sleep1-medium'],
lines: ['Slept for 4 seconds']
},
{
target: 'finish-sleep1-long',
dependencies: ['finish-sleep2-medium'],
lines: ['Slept for 5 seconds']
},
{target: 'start-sleep1', dependencies: ['finish-sleep1-long'], lines: ['node sleep.js 3.1']},
{
target: 'finish-sleep2-long',
dependencies: ['start-sleep1'],
lines: ['Slept for 6 seconds']
},
{target: 'start-sleep2', dependencies: ['finish-sleep2-long'], lines: ['node sleep.js 1.1']},
{
target: 'finish-sleep2',
dependencies: ['start-sleep2'],
lines: ['Slept for 1.1 seconds']
},
{
target: 'finish-sleep1',
dependencies: ['finish-sleep2'],
lines: ['Slept for 3.1 seconds']
}
])
t.pass()
})
}
const fs = require('fs').promises
const path = require('path')
const test = require('ava')
const {copyFiles, execFilePromise, matchOutput, toLines, wait} = require('./util')
const {copyFiles, execFilePromise, matchOutput, wait} = require('./util')
const recipes = require('./dag-makefile')
process.chdir(__dirname)
......@@ -26,7 +26,7 @@ async function makeDag(dir, ...targets) {
'node', ['../../make.js', '../dag-makefile.js', ...targets],
{cwd: path.resolve(__dirname, dir)}
)
return toLines(stdout)
return stdout
}
test.serial('dag from scratch', async t => {
......@@ -81,7 +81,7 @@ test('dag build again', async t => {
}))
// Check that the output from make.js is valid
t.deepEqual(output, [], 'Unexpected make.js output')
t.deepEqual(output, '', 'Unexpected make.js output')
})
test('dag touch F and H', async t => {
......
const {execFile} = require('child_process')
const fs = require('fs').promises
const path = require('path')
const test = require('ava')
const {matchOutput, toLines, wait} = require('./util')
const {execFilePromise, matchOutput, wait} = require('./util')
process.chdir(__dirname)
......@@ -14,22 +13,20 @@ async function makeC(dir) {
await fs.writeFile(`${dir}/c`, 'cde')
}
const makeError = (dir, ...targets) =>
new Promise((resolve, reject) =>
execFile(
'node', ['../../make.js', '../special-cases-makefile.js', ...targets],
{cwd: path.resolve(__dirname, dir)},
(err, stdout) => {
if (err && !err.code) return reject(err)
resolve({output: toLines(stdout), code: err ? err.code : 0})
}
)
)
async function make(dir, ...targets) {
const {output, code} = await makeError(dir, ...targets)
if (code) throw new Error(`Make of targets ${targets.join(', ')} failed with code ${code}`)
return output
const {stdout} = await execFilePromise(
'node', ['../../make.js', '../special-cases-makefile.js', ...targets],
{cwd: path.resolve(__dirname, dir)}
)
return stdout
}
async function makeError(dir, ...targets) {
let output
try { output = await make(dir, ...targets) }
catch (e) { return {output: e.stdout, code: e.code} }
return {output, code: 0}
}
test('request source file', async t => {
......@@ -48,7 +45,7 @@ test('request source file', async t => {
t.deepEqual(newMtime, mtimeMs, 'File c should not have been modified')
// Check that the output from make.js is valid
t.deepEqual(output, [], 'Unexpected make.js output')
t.deepEqual(output, '', 'Unexpected make.js output')
})
test('no dependencies and no build command', async t => {
......@@ -85,7 +82,7 @@ test('no dependencies and no build command', async t => {
const {mtimeMs: newMtime} = await fs.stat(`${dir}/${file}`)
t.deepEqual(newMtime, mtimeMs, `File ${dir}/${file} should not have been modified`)
}))
t.deepEqual(output2, [], 'Unexpected make.js output')
t.deepEqual(output2, '', 'Unexpected make.js output')
})
test('phony target', async t => {
......@@ -96,11 +93,11 @@ test('phony target', async t => {
const files = await fs.readdir(dir)
t.deepEqual(new Set(files), new Set(['a', 'b', 'c']), 'Incorrect list of files')
// Run 'clean' several times; the command run every time
// Run 'clean' several times; the command should run every time
for (let i = 0; i < 10; i++) {
const output = await make(dir, 'clean')
t.deepEqual(await fs.readdir(dir), ['c'], 'Incorrect list of files')
t.deepEqual(output, ['node ../remove.js a b'], 'Unexpected make.js output')
t.deepEqual(output, 'node ../remove.js a b\n', 'Unexpected make.js output')
}
})
......@@ -113,7 +110,11 @@ test('build error', async t => {
// Build error1, then error2, then error3
const {output, code} = await makeError(dir, 'error3')
t.truthy(code, 'make.js should have exited with an error')
t.deepEqual(output, ['node ../create.js error1 e', 'node ../cat.js error2 error2'], 'Unexpected make.js output')
t.deepEqual(
output,
'node ../create.js error1 e\n' + 'node ../cat.js error2 error2\n',
'Unexpected make.js output'
)
// error1 should have been created, but not error2
const files = await fs.readdir(dir)
......@@ -122,5 +123,5 @@ test('build error', async t => {
// Building again should only try to build error2
const {output: output2, code: code2} = await makeError(dir, 'error3')
t.truthy(code2, 'make.js should have exited with an error')
t.deepEqual(output2, ['node ../cat.js error2 error2'], 'Unexpected make.js output')
t.deepEqual(output2, 'node ../cat.js error2 error2\n', 'Unexpected make.js output')
})
const fs = require('fs').promises
const path = require('path')
const test = require('ava')
const {copyFiles, execFilePromise, matchOutput, toLines, wait} = require('./util')
const {copyFiles, execFilePromise, matchOutput, wait} = require('./util')
const recipes = require('./tree-makefile')
process.chdir(__dirname)
......@@ -172,7 +172,7 @@ async function makeTree(dir) {
'node', ['../../make.js', '../tree-makefile.js', ROOT_TARGET],
{cwd: path.resolve(__dirname, dir)}
)
return toLines(stdout)
return stdout
}
test.serial('tree from scratch', async t => {
......
......@@ -4,20 +4,6 @@ const {promisify} = require('util')
exports.execFilePromise = promisify(execFile)
exports.toLines = text => {
const lines = []
let lineStart = 0
while (true) {
const lineEnd = text.indexOf('\n', lineStart)
if (lineEnd < 0) break
lines.push(text.slice(lineStart, lineEnd))
lineStart = lineEnd + 1
}
if (lineStart !== text.length) throw new Error('Output did not end in a newline')
return lines
}
exports.wait = ms => new Promise(resolve => setTimeout(resolve, ms))
exports.copyFiles = async (fromDir, toDir) => {
......@@ -40,9 +26,15 @@ exports.copyFiles = async (fromDir, toDir) => {
}))
}
function toLines(text) {
const lines = text.split('\n')
if (lines.length && !lines[lines.length - 1]) lines.pop()
return lines
}
exports.matchOutput = (actual, expected) => {
const actualLines = new Map()
actual.forEach((line, i) => actualLines.set(i, line))
toLines(actual).forEach((line, i) => actualLines.set(i, line))
const ranges = new Map()
for (const {target, lines} of expected) {
let min = Infinity, max = -Infinity
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment