@informalsystems/quint
Version:
Core tool for the Quint specification language
656 lines • 17.4 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const mocha_1 = require("mocha");
const chai_1 = require("chai");
const events_1 = require("events");
const stream_1 = require("stream");
const chalk_1 = __importDefault(require("chalk"));
const repl_1 = require("../src/repl");
const textUtils_1 = require("./textUtils");
const version_1 = require("../src/version");
// A simple implementation of Writable to a string:
// After: https://bensmithgall.com/blog/jest-mock-trick
class ToStringWritable extends stream_1.Writable {
constructor() {
super(...arguments);
this.buffer = '';
}
_write(chunk, encoding, next) {
this.buffer += chunk;
next();
}
reset() {
this.buffer = '';
}
}
// run a test with mocked input/output and return the input + output
const withIO = async (inputText) => {
// save the current chalk level and reset chalk to no color
const savedChalkLevel = chalk_1.default.level;
chalk_1.default.level = 0;
// setup:
// - the output that writes to a string
// - the input that consumes events
const output = new ToStringWritable();
// an input mock designed for testing
const input = new stream_1.PassThrough();
// whatever is written on the input goes to the output
input.pipe(output);
const rl = (0, repl_1.quintRepl)(input, output, { verbosity: 2 }, () => { });
// Emit the input line-by-line, as nodejs is printing prompts.
// TODO: is it a potential source of race conditions in unit tests?
const lines = inputText.split('\n');
let linesLeft = lines.length;
for (const line of lines) {
input.emit('data', line);
linesLeft--;
if (linesLeft > 0) {
input.emit('data', '\n');
}
}
input.end();
input.unpipe(output);
input.destroy();
// readline is asynchronous, wait till it terminates
await (0, events_1.once)(rl, 'close');
chalk_1.default.level = savedChalkLevel;
return output.buffer;
};
// the standard banner, which gets repeated
const banner = `Quint REPL ${version_1.version}
Type ".exit" to exit, or ".help" for more information`;
async function assertRepl(input, output) {
const expected = `${banner}
${output}
`;
const result = await withIO(input);
(0, chai_1.assert)(typeof result === 'string', 'expected result to be a string');
(0, chai_1.expect)(result).to.equal(expected);
}
(0, mocha_1.describe)('repl ok', () => {
(0, mocha_1.it)('empty input', async () => {
await assertRepl('', '>>> ');
});
(0, mocha_1.it)('Set(2 + 3)', async () => {
const input = 'Set(2 + 3)\n';
const output = (0, textUtils_1.dedent)(`>>> Set(2 + 3)
|Set(5)
|>>> `);
await assertRepl(input, output);
});
(0, mocha_1.it)('Map(1 -> 2, 3 -> 4)', async () => {
const input = 'Map(1 -> 2, 3 -> 4)\n';
const output = (0, textUtils_1.dedent)(`>>> Map(1 -> 2, 3 -> 4)
|Map(1 -> 2, 3 -> 4)
|>>> `);
await assertRepl(input, output);
});
(0, mocha_1.it)('basic expressions', async () => {
const input = (0, textUtils_1.dedent)(`1 + 1
|3 > 1
|1.to(3).map(x => 2 * x)
|1.to(4).filter(x => x > 2)
|Set(1, 3).union(Set(5, 6))
|1.to(4).forall(x => x > 1)
|(5 - 1, 5, 6)
|[5 - 1, 5, 6]
|`);
const output = (0, textUtils_1.dedent)(`>>> 1 + 1
|2
|>>> 3 > 1
|true
|>>> 1.to(3).map(x => 2 * x)
|Set(2, 4, 6)
|>>> 1.to(4).filter(x => x > 2)
|Set(3, 4)
|>>> Set(1, 3).union(Set(5, 6))
|Set(1, 3, 5, 6)
|>>> 1.to(4).forall(x => x > 1)
|false
|>>> (5 - 1, 5, 6)
|(4, 5, 6)
|>>> [5 - 1, 5, 6]
|[4, 5, 6]
|>>> `);
await assertRepl(input, output);
});
(0, mocha_1.it)('ill-typed expressions', async () => {
const input = (0, textUtils_1.dedent)(`1 + false
|`);
const output = (0, textUtils_1.dedent)(`>>> 1 + false
|static analysis error: error: [QNT000] Couldn't unify int and bool
|Trying to unify int and bool
|Trying to unify (int, int) => int and (int, bool) => _t0
|
|1 + false
|^^^^^^^^^
|
|>>> `);
await assertRepl(input, output);
});
(0, mocha_1.it)('definitions in expressions', async () => {
const input = (0, textUtils_1.dedent)(`val x = 3; 2 * x
|def mult(x, y) = x * y; mult(2, mult(3, 4))
|`);
const output = (0, textUtils_1.dedent)(`>>> val x = 3; 2 * x
|6
|>>> def mult(x, y) = x * y; mult(2, mult(3, 4))
|24
|>>> `);
await assertRepl(input, output);
});
(0, mocha_1.it)('top-level definitions', async () => {
const input = (0, textUtils_1.dedent)(`val n = 4
|def mult(x, y) = x * y
|mult(100, n)
|def powpow(x, y) = x^y
|mult(100, powpow(2, 3))
|`);
const output = (0, textUtils_1.dedent)(`>>> val n = 4
|
|>>> def mult(x, y) = x * y
|
|>>> mult(100, n)
|400
|>>> def powpow(x, y) = x^y
|
|>>> mult(100, powpow(2, 3))
|800
|>>> `);
await assertRepl(input, output);
});
(0, mocha_1.it)('clear history', async () => {
const input = (0, textUtils_1.dedent)(`val n = 4
|n * n
|.clear
|n * n
|`);
const output = (0, textUtils_1.dedent)(`>>> val n = 4
|
|>>> n * n
|16
|>>> .clear
|
|>>> n * n
|static analysis error: error: [QNT404] Name 'n' not found
|n * n
|^
|
|static analysis error: error: [QNT404] Name 'n' not found
|n * n
| ^
|
|>>> `);
await assertRepl(input, output);
});
(0, mocha_1.it)('change verbosity and track executions', async () => {
const input = (0, textUtils_1.dedent)(`pure def plus(x, y) = x + y
|.verbosity=4
|plus(2, 3)
|`);
const output = (0, textUtils_1.dedent)(`>>> pure def plus(x, y) = x + y
|
|>>> .verbosity=4
|.verbosity=4
|>>> plus(2, 3)
|5
|
|[Frame 0]
|plus(2, 3) => 5
|
|>>> `);
await assertRepl(input, output);
});
(0, mocha_1.it)('change verbosity and show execution on failure', async () => {
const input = (0, textUtils_1.dedent)(`pure def div(x, y) = x / y
|.verbosity=4
|div(2, 0)
|`);
const output = (0, textUtils_1.dedent)(`>>> pure def div(x, y) = x / y
|
|>>> .verbosity=4
|.verbosity=4
|>>> div(2, 0)
|
|[Frame 0]
|div(2, 0) => none
|
|runtime error: error: [QNT503] Division by zero
|pure def div(x, y) = x / y
| ^^^^^
|
|>>> `);
await assertRepl(input, output);
});
(0, mocha_1.it)('caching nullary definitions', async () => {
const input = (0, textUtils_1.dedent)(`var x: int
|.verbosity=4
|x' = 0
|action step = x' = x + 1
|action input1 = step
|action input2 = step
|input1
|input2
|`);
const output = (0, textUtils_1.dedent)(
// a regression test for the behavior uncovered in:
// https://github.com/informalsystems/quint/issues/982
`>>> var x: int
|
|>>> .verbosity=4
|.verbosity=4
|>>> x' = 0
|true
|>>> action step = x' = x + 1
|
|>>> action input1 = step
|
|>>> action input2 = step
|
|>>> input1
|true
|
|[Frame 0]
|input1 => true
|└─ step => true
|
|>>> input2
|true
|
|[Frame 0]
|input2 => true
|└─ step => true
|
|>>> `);
await assertRepl(input, output);
});
(0, mocha_1.it)('update the seed between evaluations', async () => {
// A regression test.
// Test that two consecutive steps produce two different integers.
// If this test fails, it is almost certainly because of the seed
// not being updated between two consecutive calls of `step`.
// There is a neglible probability of 1/2^1024 of this test failing,
// since we are using randomization.
const input = (0, textUtils_1.dedent)(`var S: Set[int]
|S' = Set()
|action step = { nondet y = 1.to(2^512).oneOf(); S' = Set(y).union(S) }
|step
|step
|size(S)
|`);
const output = (0, textUtils_1.dedent)(`>>> var S: Set[int]
|
|>>> S' = Set()
|true
|>>> action step = { nondet y = 1.to(2^512).oneOf(); S' = Set(y).union(S) }
|
|>>> step
|true
|>>> step
|true
|>>> size(S)
|2
|>>> `);
await assertRepl(input, output);
});
(0, mocha_1.it)('set and get the seed', async () => {
const input = (0, textUtils_1.dedent)(`.seed=4
|.seed
|.seed=0x1abc
|`);
const output = (0, textUtils_1.dedent)(`>>> .seed=4
|.seed=4
|>>> .seed
|.seed=4
|>>> .seed=0x1abc
|.seed=6844
|>>> `);
await assertRepl(input, output);
});
(0, mocha_1.it)('handle exceptions', async () => {
const input = (0, textUtils_1.dedent)(`Set(Int)
|`);
const output = (0, textUtils_1.dedent)(`>>> Set(Int)
|runtime error: error: [QNT501] Infinite set Int is non-enumerable
|Set(Int)
|^^^^^^^^
|
|>>> `);
await assertRepl(input, output);
});
(0, mocha_1.it)('assignments', async () => {
const input = (0, textUtils_1.dedent)(`var x: int
|action Init = x' = 0
|action Next = x' = x + 1
|Init
|x
|Next
|x
|Next
|x
|`);
const output = (0, textUtils_1.dedent)(`>>> var x: int
|
|>>> action Init = x' = 0
|
|>>> action Next = x' = x + 1
|
|>>> Init
|true
|>>> x
|0
|>>> Next
|true
|>>> x
|1
|>>> Next
|true
|>>> x
|2
|>>> `);
await assertRepl(input, output);
});
(0, mocha_1.it)('action-level disjunctions and conjunctions', async () => {
const input = (0, textUtils_1.dedent)(`
|var x: int
|action Init = x' = 0
|action Next = any {
| all {
| x == 0,
| x' = 1,
| },
| all {
| x == 1,
| x' = 0,
| },
|}
|
|Init
|x
|Next
|x
|Next
|x
|Next
|x
|`);
const output = (0, textUtils_1.dedent)(`>>>
|>>> var x: int
|
|>>> action Init = x' = 0
|
|>>> action Next = any {
|... all {
|... x == 0,
|... x' = 1,
|... },
|... all {
|... x == 1,
|... x' = 0,
|... },
|... }
|...
|
|>>> Init
|true
|>>> x
|0
|>>> Next
|true
|>>> x
|1
|>>> Next
|true
|>>> x
|0
|>>> Next
|true
|>>> x
|1
|>>> `);
await assertRepl(input, output);
});
(0, mocha_1.it)('action-level disjunctions and non-determinism', async () => {
const input = (0, textUtils_1.dedent)(`
|var x: int
|action Init = x' = 0
|action Next = any {
| x' = x + 1,
| x' = x - 1,
|}
|
|Init
|-1 <= x and x <= 1
|Next
|-2 <= x and x <= 2
|Next
|-3 <= x and x <= 3
|Next
|-4 <= x and x <= 4
|`);
const output = (0, textUtils_1.dedent)(`>>>
|>>> var x: int
|
|>>> action Init = x' = 0
|
|>>> action Next = any {
|... x' = x + 1,
|... x' = x - 1,
|... }
|...
|
|>>> Init
|true
|>>> -1 <= x and x <= 1
|true
|>>> Next
|true
|>>> -2 <= x and x <= 2
|true
|>>> Next
|true
|>>> -3 <= x and x <= 3
|true
|>>> Next
|true
|>>> -4 <= x and x <= 4
|true
|>>> `);
await assertRepl(input, output);
});
(0, mocha_1.it)('nondet and oneOf', async () => {
const input = (0, textUtils_1.dedent)(`
|var x: int
|
|x' = 0
|x == 0
|{ nondet y = oneOf(Set(1, 2, 3))
| x' = y }
|
|1 <= x and x <= 3
|nondet y = oneOf(2.to(5)); x' = y
|2 <= x and x <= 5
|nondet t = oneOf(tuples(2.to(5), 3.to(4))); x' = t._1 + t._2
|5 <= x and x <= 9
|nondet i = oneOf(Nat); x' = i
|x >= 0
|nondet i = oneOf(Int); x' = i
|Int.contains(x)
|nondet m = 1.to(5).setOfMaps(Int).oneOf(); x' = m.get(3)
|x.in(Int)
|nondet m = Set().oneOf(); x' = m
|`);
const output = (0, textUtils_1.dedent)(`>>>
|>>> var x: int
|
|>>>
|>>> x' = 0
|true
|>>> x == 0
|true
|>>> { nondet y = oneOf(Set(1, 2, 3))
|... x' = y }
|...
|true
|>>> 1 <= x and x <= 3
|true
|>>> nondet y = oneOf(2.to(5)); x' = y
|true
|>>> 2 <= x and x <= 5
|true
|>>> nondet t = oneOf(tuples(2.to(5), 3.to(4))); x' = t._1 + t._2
|true
|>>> 5 <= x and x <= 9
|true
|>>> nondet i = oneOf(Nat); x' = i
|true
|>>> x >= 0
|true
|>>> nondet i = oneOf(Int); x' = i
|true
|>>> Int.contains(x)
|true
|>>> nondet m = 1.to(5).setOfMaps(Int).oneOf(); x' = m.get(3)
|true
|>>> x.in(Int)
|true
|>>> nondet m = Set().oneOf(); x' = m
|false
|>>> `);
await assertRepl(input, output);
});
(0, mocha_1.it)('nondet and oneOf over sets of sets', async () => {
const input = (0, textUtils_1.dedent)(`var S: Set[int]
|nondet y = oneOf(powerset(1.to(3))); S' = y
|S.subseteq(1.to(3))
|`);
const output = (0, textUtils_1.dedent)(`>>> var S: Set[int]
|
|>>> nondet y = oneOf(powerset(1.to(3))); S' = y
|true
|>>> S.subseteq(1.to(3))
|true
|>>> `);
await assertRepl(input, output);
});
(0, mocha_1.it)('actions introduce their own frames', async () => {
const input = (0, textUtils_1.dedent)(`var n: int
|action init = n' = 0
|action step = n' = n + 1
|.verbosity=3
|init.then(step).then(step)
|`);
const output = (0, textUtils_1.dedent)(`>>> var n: int
|
|>>> action init = n' = 0
|
|>>> action step = n' = n + 1
|
|>>> .verbosity=3
|.verbosity=3
|>>> init.then(step).then(step)
|true
|
|[Frame 0]
|init => true
|
|[Frame 1]
|step => true
|
|[Frame 2]
|step => true
|
|>>> `);
await assertRepl(input, output);
});
(0, mocha_1.it)('run q::test, q::testOnce, and q::lastTrace', async () => {
const input = (0, textUtils_1.dedent)(`
|var n: int
|action Init = n' = 0
|action Next = n' = n + 1
|val Inv = n < 10
|q::testOnce(5, 1, Init, Next, Inv)
|q::testOnce(10, 1, Init, Next, Inv)
|q::test(5, 5, 1, Init, Next, Inv)
|q::test(5, 10, 1, Init, Next, Inv)
|q::lastTrace.length()
|q::lastTrace.nth(q::lastTrace.length() - 1)
|`);
const output = (0, textUtils_1.dedent)(`>>>
|>>> var n: int
|
|>>> action Init = n' = 0
|
|>>> action Next = n' = n + 1
|
|>>> val Inv = n < 10
|
|>>> q::testOnce(5, 1, Init, Next, Inv)
|"ok"
|>>> q::testOnce(10, 1, Init, Next, Inv)
|"violation"
|>>> q::test(5, 5, 1, Init, Next, Inv)
|"ok"
|>>> q::test(5, 10, 1, Init, Next, Inv)
|"violation"
|>>> q::lastTrace.length()
|11
|>>> q::lastTrace.nth(q::lastTrace.length() - 1)
|{ n: 10 }
|>>> `);
await assertRepl(input, output);
});
(0, mocha_1.it)('REPL consumes its output', async () => {
const input = (0, textUtils_1.dedent)(`>>> 1 + 1
|
|>>> all {
|... true,
|... true
|... }
|
| >>> if (true) {
| ... 3
| ... } else {
| ... 4
| ... }
|
|>>> Set(2 + 3)
|Set(5)
| // a multiline comment
| /// a doc comment
| /* a multiline
| comment
| */
|`);
const output = (0, textUtils_1.dedent)(`>>> >>> 1 + 1
|...
|2
|>>> >>> all {
|... ... true,
|... ... true
|... ... }
|...
|true
|>>> >>> if (true) {
|... ... 3
|... ... } else {
|... ... 4
|... ... }
|...
|3
|>>> >>> Set(2 + 3)
|... Set(5)
|Set(5)
|>>> // a multiline comment
|>>> /// a doc comment
|>>> /* a multiline
|... comment
|... */
|... `);
await assertRepl(input, output);
});
});
//# sourceMappingURL=repl.test.js.map