assert: Report why values aren't equal

This commit is contained in:
Robert Hensing 2024-07-05 16:43:48 +02:00
parent 509be0e77a
commit d63bd8295e
29 changed files with 532 additions and 5 deletions

View file

@ -1759,9 +1759,24 @@ void ExprIf::eval(EvalState & state, Env & env, Value & v)
void ExprAssert::eval(EvalState & state, Env & env, Value & v) void ExprAssert::eval(EvalState & state, Env & env, Value & v)
{ {
if (!state.evalBool(env, cond, pos, "in the condition of the assert statement")) { if (!state.evalBool(env, cond, pos, "in the condition of the assert statement")) {
std::ostringstream out; auto exprStr = ({
cond->show(state.symbols, out); std::ostringstream out;
state.error<AssertionError>("assertion '%1%' failed", out.str()).atPos(pos).withFrame(env, *this).debugThrow(); cond->show(state.symbols, out);
out.str();
});
if (auto eq = dynamic_cast<ExprOpEq *>(cond)) {
try {
Value v1; eq->e1->eval(state, env, v1);
Value v2; eq->e2->eval(state, env, v2);
state.assertEqValues(v1, v2, eq->pos, "in an equality assertion");
} catch (AssertionError & e) {
e.addTrace(state.positions[pos], "while evaluating the condition of the assertion '%s'", exprStr);
throw;
}
}
state.error<AssertionError>("assertion '%1%' failed", exprStr).atPos(pos).withFrame(env, *this).debugThrow();
} }
body->eval(state, env, v); body->eval(state, env, v);
} }
@ -2418,6 +2433,216 @@ SingleDerivedPath EvalState::coerceToSingleDerivedPath(const PosIdx pos, Value &
} }
// NOTE: This implementation must match eqValues!
// We accept this burden because informative error messages for
// `assert a == b; x` are critical for our users' testing UX.
void EvalState::assertEqValues(Value & v1, Value & v2, const PosIdx pos, std::string_view errorCtx)
{
// This implementation must match eqValues.
forceValue(v1, pos);
forceValue(v2, pos);
if (&v1 == &v2)
return;
// Special case type-compatibility between float and int
if ((v1.type() == nInt || v1.type() == nFloat) && (v2.type() == nInt || v2.type() == nFloat)) {
if (eqValues(v1, v2, pos, errorCtx)) {
return;
} else {
error<AssertionError>(
"%s with value '%s' is not equal to %s with value '%s'",
showType(v1),
ValuePrinter(*this, v1, errorPrintOptions),
showType(v2),
ValuePrinter(*this, v2, errorPrintOptions))
.debugThrow();
}
}
if (v1.type() != v2.type()) {
error<AssertionError>(
"%s of value '%s' is not equal to %s of value '%s'",
showType(v1),
ValuePrinter(*this, v1, errorPrintOptions),
showType(v2),
ValuePrinter(*this, v2, errorPrintOptions))
.debugThrow();
}
switch (v1.type()) {
case nInt:
if (v1.integer() != v2.integer()) {
error<AssertionError>("integer '%d' is not equal to integer '%d'", v1.integer(), v2.integer()).debugThrow();
}
return;
case nBool:
if (v1.boolean() != v2.boolean()) {
error<AssertionError>(
"boolean '%s' is not equal to boolean '%s'",
ValuePrinter(*this, v1, errorPrintOptions),
ValuePrinter(*this, v2, errorPrintOptions))
.debugThrow();
}
return;
case nString:
if (strcmp(v1.c_str(), v2.c_str()) != 0) {
error<AssertionError>(
"string '%s' is not equal to string '%s'",
ValuePrinter(*this, v1, errorPrintOptions),
ValuePrinter(*this, v2, errorPrintOptions))
.debugThrow();
}
return;
case nPath:
if (v1.payload.path.accessor != v2.payload.path.accessor) {
error<AssertionError>(
"path '%s' is not equal to path '%s' because their accessors are different",
ValuePrinter(*this, v1, errorPrintOptions),
ValuePrinter(*this, v2, errorPrintOptions))
.debugThrow();
}
if (strcmp(v1.payload.path.path, v2.payload.path.path) != 0) {
error<AssertionError>(
"path '%s' is not equal to path '%s'",
ValuePrinter(*this, v1, errorPrintOptions),
ValuePrinter(*this, v2, errorPrintOptions))
.debugThrow();
}
return;
case nNull:
return;
case nList:
if (v1.listSize() != v2.listSize()) {
error<AssertionError>(
"list of size '%d' is not equal to list of size '%d', left hand side is '%s', right hand side is '%s'",
v1.listSize(),
v2.listSize(),
ValuePrinter(*this, v1, errorPrintOptions),
ValuePrinter(*this, v2, errorPrintOptions))
.debugThrow();
}
for (size_t n = 0; n < v1.listSize(); ++n) {
try {
assertEqValues(*v1.listElems()[n], *v2.listElems()[n], pos, errorCtx);
} catch (Error & e) {
e.addTrace(positions[pos], "while comparing list element %d", n);
throw;
}
}
return;
case nAttrs: {
if (isDerivation(v1) && isDerivation(v2)) {
auto i = v1.attrs()->get(sOutPath);
auto j = v2.attrs()->get(sOutPath);
if (i && j) {
try {
assertEqValues(*i->value, *j->value, pos, errorCtx);
return;
} catch (Error & e) {
e.addTrace(positions[pos], "while comparing a derivation by its '%s' attribute", "outPath");
throw;
}
assert(false);
}
}
if (v1.attrs()->size() != v2.attrs()->size()) {
error<AssertionError>(
"attribute names of attribute set '%s' differs from attribute set '%s'",
ValuePrinter(*this, v1, errorPrintOptions),
ValuePrinter(*this, v2, errorPrintOptions))
.debugThrow();
}
// Like normal comparison, we compare the attributes in non-deterministic Symbol index order.
// This function is called when eqValues has found a difference, so to reliably
// report about its result, we should follow in its literal footsteps and not
// try anything fancy that could lead to an error.
Bindings::const_iterator i, j;
for (i = v1.attrs()->begin(), j = v2.attrs()->begin(); i != v1.attrs()->end(); ++i, ++j) {
if (i->name != j->name) {
// A difference in a sorted list means that one attribute is not contained in the other, but we don't
// know which. Let's find out. Could use <, but this is more clear.
if (!v2.attrs()->get(i->name)) {
error<AssertionError>(
"attribute name '%s' is contained in '%s', but not in '%s'",
symbols[i->name],
ValuePrinter(*this, v1, errorPrintOptions),
ValuePrinter(*this, v2, errorPrintOptions))
.debugThrow();
}
if (!v1.attrs()->get(j->name)) {
error<AssertionError>(
"attribute name '%s' is missing in '%s', but is contained in '%s'",
symbols[j->name],
ValuePrinter(*this, v1, errorPrintOptions),
ValuePrinter(*this, v2, errorPrintOptions))
.debugThrow();
}
assert(false);
}
try {
assertEqValues(*i->value, *j->value, pos, errorCtx);
} catch (Error & e) {
// The order of traces is reversed, so this presents as
// where left hand side is
// at <pos>
// where right hand side is
// at <pos>
// while comparing attribute '<name>'
if (j->pos != noPos)
e.addTrace(positions[j->pos], "where right hand side is");
if (i->pos != noPos)
e.addTrace(positions[i->pos], "where left hand side is");
e.addTrace(positions[pos], "while comparing attribute '%s'", symbols[i->name]);
throw;
}
}
return;
}
case nFunction:
error<AssertionError>("distinct functions and immediate comparisons of identical functions compare as unequal")
.debugThrow();
case nExternal:
if (!(*v1.external() == *v2.external())) {
error<AssertionError>(
"external value '%s' is not equal to external value '%s'",
ValuePrinter(*this, v1, errorPrintOptions),
ValuePrinter(*this, v2, errorPrintOptions))
.debugThrow();
}
return;
case nFloat:
// !!!
if (!(v1.fpoint() == v2.fpoint())) {
error<AssertionError>("float '%f' is not equal to float '%f'", v1.fpoint(), v2.fpoint()).debugThrow();
}
return;
case nThunk: // Must not be left by forceValue
default:
// This should never happen, because eqValues already throws an
// error for this, and this function should only be called when
// eqValues has found a difference, and it should match
// its behavior.
error<EvalBaseError>(
"cannot compare %1% with %2%; is assertEqValues out of sync with eqValues?", showType(v1), showType(v2))
.debugThrow();
}
}
// This implementation must match assertEqValues
bool EvalState::eqValues(Value & v1, Value & v2, const PosIdx pos, std::string_view errorCtx) bool EvalState::eqValues(Value & v1, Value & v2, const PosIdx pos, std::string_view errorCtx)
{ {
forceValue(v1, pos); forceValue(v1, pos);
@ -2491,6 +2716,7 @@ bool EvalState::eqValues(Value & v1, Value & v2, const PosIdx pos, std::string_v
return *v1.external() == *v2.external(); return *v1.external() == *v2.external();
case nFloat: case nFloat:
// !!!
return v1.fpoint() == v2.fpoint(); return v1.fpoint() == v2.fpoint();
case nThunk: // Must not be left by forceValue case nThunk: // Must not be left by forceValue

View file

@ -644,6 +644,15 @@ public:
*/ */
bool eqValues(Value & v1, Value & v2, const PosIdx pos, std::string_view errorCtx); bool eqValues(Value & v1, Value & v2, const PosIdx pos, std::string_view errorCtx);
/**
* Like `eqValues`, but throws an `AssertionError` if not equal.
*
* WARNING:
* Callers should call `eqValues` first and report if `assertEqValues` behaves
* incorrectly. (e.g. if it doesn't throw if eqValues returns false or vice versa)
*/
void assertEqValues(Value & v1, Value & v2, const PosIdx pos, std::string_view errorCtx);
bool isFunctor(Value & fun); bool isFunctor(Value & fun);
// FIXME: use std::span // FIXME: use std::span

View file

@ -0,0 +1,8 @@
error:
… while evaluating the condition of the assertion '({ a = true; } == { a = true; b = true; })'
at /pwd/lang/eval-fail-assert-equal-attrs-names-2.nix:1:1:
1| assert { a = true; } == { a = true; b = true; };
| ^
2| throw "unreachable"
error: attribute names of attribute set '{ a = true; }' differs from attribute set '{ a = true; b = true; }'

View file

@ -0,0 +1,2 @@
assert { a = true; } == { a = true; b = true; };
throw "unreachable"

View file

@ -0,0 +1,8 @@
error:
… while evaluating the condition of the assertion '({ a = true; b = true; } == { a = true; })'
at /pwd/lang/eval-fail-assert-equal-attrs-names.nix:1:1:
1| assert { a = true; b = true; } == { a = true; };
| ^
2| throw "unreachable"
error: attribute names of attribute set '{ a = true; b = true; }' differs from attribute set '{ a = true; }'

View file

@ -0,0 +1,2 @@
assert { a = true; b = true; } == { a = true; };
throw "unreachable"

View file

@ -0,0 +1,26 @@
error:
… while evaluating the condition of the assertion '({ foo = { outPath = "/nix/store/0"; type = "derivation"; }; } == { foo = { devious = true; outPath = "/nix/store/1"; type = "derivation"; }; })'
at /pwd/lang/eval-fail-assert-equal-derivations-extra.nix:1:1:
1| assert
| ^
2| { foo = { type = "derivation"; outPath = "/nix/store/0"; }; }
… while comparing attribute 'foo'
… where left hand side is
at /pwd/lang/eval-fail-assert-equal-derivations-extra.nix:2:5:
1| assert
2| { foo = { type = "derivation"; outPath = "/nix/store/0"; }; }
| ^
3| ==
… where right hand side is
at /pwd/lang/eval-fail-assert-equal-derivations-extra.nix:4:5:
3| ==
4| { foo = { type = "derivation"; outPath = "/nix/store/1"; devious = true; }; };
| ^
5| throw "unreachable"
… while comparing a derivation by its 'outPath' attribute
error: string '"/nix/store/0"' is not equal to string '"/nix/store/1"'

View file

@ -0,0 +1,5 @@
assert
{ foo = { type = "derivation"; outPath = "/nix/store/0"; }; }
==
{ foo = { type = "derivation"; outPath = "/nix/store/1"; devious = true; }; };
throw "unreachable"

View file

@ -0,0 +1,26 @@
error:
… while evaluating the condition of the assertion '({ foo = { ignored = (abort "not ignored"); outPath = "/nix/store/0"; type = "derivation"; }; } == { foo = { ignored = (abort "not ignored"); outPath = "/nix/store/1"; type = "derivation"; }; })'
at /pwd/lang/eval-fail-assert-equal-derivations.nix:1:1:
1| assert
| ^
2| { foo = { type = "derivation"; outPath = "/nix/store/0"; ignored = abort "not ignored"; }; }
… while comparing attribute 'foo'
… where left hand side is
at /pwd/lang/eval-fail-assert-equal-derivations.nix:2:5:
1| assert
2| { foo = { type = "derivation"; outPath = "/nix/store/0"; ignored = abort "not ignored"; }; }
| ^
3| ==
… where right hand side is
at /pwd/lang/eval-fail-assert-equal-derivations.nix:4:5:
3| ==
4| { foo = { type = "derivation"; outPath = "/nix/store/1"; ignored = abort "not ignored"; }; };
| ^
5| throw "unreachable"
… while comparing a derivation by its 'outPath' attribute
error: string '"/nix/store/0"' is not equal to string '"/nix/store/1"'

View file

@ -0,0 +1,5 @@
assert
{ foo = { type = "derivation"; outPath = "/nix/store/0"; ignored = abort "not ignored"; }; }
==
{ foo = { type = "derivation"; outPath = "/nix/store/1"; ignored = abort "not ignored"; }; };
throw "unreachable"

View file

@ -0,0 +1,22 @@
error:
… while evaluating the condition of the assertion '({ b = 1; } == { b = 1.01; })'
at /pwd/lang/eval-fail-assert-equal-floats.nix:1:1:
1| assert { b = 1.0; } == { b = 1.01; };
| ^
2| abort "unreachable"
… while comparing attribute 'b'
… where left hand side is
at /pwd/lang/eval-fail-assert-equal-floats.nix:1:10:
1| assert { b = 1.0; } == { b = 1.01; };
| ^
2| abort "unreachable"
… where right hand side is
at /pwd/lang/eval-fail-assert-equal-floats.nix:1:26:
1| assert { b = 1.0; } == { b = 1.01; };
| ^
2| abort "unreachable"
error: a float with value '1' is not equal to a float with value '1.01'

View file

@ -0,0 +1,2 @@
assert { b = 1.0; } == { b = 1.01; };
abort "unreachable"

View file

@ -0,0 +1,9 @@
error:
… while evaluating the condition of the assertion '((x: x) == (x: x))'
at /pwd/lang/eval-fail-assert-equal-function-direct.nix:3:1:
2| # This only compares a direct comparison and makes no claims about functions in nested structures.
3| assert
| ^
4| (x: x)
error: distinct functions and immediate comparisons of identical functions compare as unequal

View file

@ -0,0 +1,7 @@
# Note: functions in nested structures, e.g. attributes, may be optimized away by pointer identity optimization.
# This only compares a direct comparison and makes no claims about functions in nested structures.
assert
(x: x)
==
(x: x);
abort "unreachable"

View file

@ -0,0 +1,8 @@
error:
… while evaluating the condition of the assertion '(1 == 1.1)'
at /pwd/lang/eval-fail-assert-equal-int-float.nix:1:1:
1| assert 1 == 1.1;
| ^
2| throw "unreachable"
error: an integer with value '1' is not equal to a float with value '1.1'

View file

@ -0,0 +1,2 @@
assert 1 == 1.1;
throw "unreachable"

View file

@ -0,0 +1,22 @@
error:
… while evaluating the condition of the assertion '({ b = 1; } == { b = 2; })'
at /pwd/lang/eval-fail-assert-equal-ints.nix:1:1:
1| assert { b = 1; } == { b = 2; };
| ^
2| abort "unreachable"
… while comparing attribute 'b'
… where left hand side is
at /pwd/lang/eval-fail-assert-equal-ints.nix:1:10:
1| assert { b = 1; } == { b = 2; };
| ^
2| abort "unreachable"
… where right hand side is
at /pwd/lang/eval-fail-assert-equal-ints.nix:1:24:
1| assert { b = 1; } == { b = 2; };
| ^
2| abort "unreachable"
error: an integer with value '1' is not equal to an integer with value '2'

View file

@ -0,0 +1,2 @@
assert { b = 1; } == { b = 2; };
abort "unreachable"

View file

@ -0,0 +1,8 @@
error:
… while evaluating the condition of the assertion '([ (1) (0) ] == [ (10) ])'
at /pwd/lang/eval-fail-assert-equal-list-length.nix:1:1:
1| assert [ 1 0 ] == [ 10 ];
| ^
2| throw "unreachable"
error: list of size '2' is not equal to list of size '1', left hand side is '[ 1 0 ]', right hand side is '[ 10 ]'

View file

@ -0,0 +1,2 @@
assert [ 1 0 ] == [ 10 ];
throw "unreachable"

View file

@ -0,0 +1,8 @@
error:
… while evaluating the condition of the assertion '(/pwd/lang/foo == /pwd/lang/bar)'
at /pwd/lang/eval-fail-assert-equal-paths.nix:1:1:
1| assert ./foo == ./bar;
| ^
2| throw "unreachable"
error: path '/pwd/lang/foo' is not equal to path '/pwd/lang/bar'

View file

@ -0,0 +1,2 @@
assert ./foo == ./bar;
throw "unreachable"

View file

@ -0,0 +1,22 @@
error:
… while evaluating the condition of the assertion '({ ding = false; } == { ding = null; })'
at /pwd/lang/eval-fail-assert-equal-type-nested.nix:1:1:
1| assert { ding = false; } == { ding = null; };
| ^
2| abort "unreachable"
… while comparing attribute 'ding'
… where left hand side is
at /pwd/lang/eval-fail-assert-equal-type-nested.nix:1:10:
1| assert { ding = false; } == { ding = null; };
| ^
2| abort "unreachable"
… where right hand side is
at /pwd/lang/eval-fail-assert-equal-type-nested.nix:1:31:
1| assert { ding = false; } == { ding = null; };
| ^
2| abort "unreachable"
error: a Boolean of value 'false' is not equal to null of value 'null'

View file

@ -0,0 +1,2 @@
assert { ding = false; } == { ding = null; };
abort "unreachable"

View file

@ -0,0 +1,8 @@
error:
… while evaluating the condition of the assertion '(false == null)'
at /pwd/lang/eval-fail-assert-equal-type.nix:1:1:
1| assert false == null;
| ^
2| abort "unreachable"
error: a Boolean of value 'false' is not equal to null of value 'null'

View file

@ -0,0 +1,2 @@
assert false == null;
abort "unreachable"

View file

@ -0,0 +1,74 @@
error:
… while evaluating the condition of the assertion '({ a = { b = [ ({ c = { d = true; }; }) ]; }; } == { a = { b = [ ({ c = { d = false; }; }) ]; }; })'
at /pwd/lang/eval-fail-assert-nested-bool.nix:1:1:
1| assert
| ^
2| { a.b = [ { c.d = true; } ]; }
… while comparing attribute 'a'
… where left hand side is
at /pwd/lang/eval-fail-assert-nested-bool.nix:2:5:
1| assert
2| { a.b = [ { c.d = true; } ]; }
| ^
3| ==
… where right hand side is
at /pwd/lang/eval-fail-assert-nested-bool.nix:4:5:
3| ==
4| { a.b = [ { c.d = false; } ]; };
| ^
5|
… while comparing attribute 'b'
… where left hand side is
at /pwd/lang/eval-fail-assert-nested-bool.nix:2:5:
1| assert
2| { a.b = [ { c.d = true; } ]; }
| ^
3| ==
… where right hand side is
at /pwd/lang/eval-fail-assert-nested-bool.nix:4:5:
3| ==
4| { a.b = [ { c.d = false; } ]; };
| ^
5|
… while comparing list element 0
… while comparing attribute 'c'
… where left hand side is
at /pwd/lang/eval-fail-assert-nested-bool.nix:2:15:
1| assert
2| { a.b = [ { c.d = true; } ]; }
| ^
3| ==
… where right hand side is
at /pwd/lang/eval-fail-assert-nested-bool.nix:4:15:
3| ==
4| { a.b = [ { c.d = false; } ]; };
| ^
5|
… while comparing attribute 'd'
… where left hand side is
at /pwd/lang/eval-fail-assert-nested-bool.nix:2:15:
1| assert
2| { a.b = [ { c.d = true; } ]; }
| ^
3| ==
… where right hand side is
at /pwd/lang/eval-fail-assert-nested-bool.nix:4:15:
3| ==
4| { a.b = [ { c.d = false; } ]; };
| ^
5|
error: boolean 'true' is not equal to boolean 'false'

View file

@ -0,0 +1,6 @@
assert
{ a.b = [ { c.d = true; } ]; }
==
{ a.b = [ { c.d = false; } ]; };
abort "unreachable"

View file

@ -20,9 +20,11 @@ error:
| ^ | ^
3| 3|
error: assertion '(arg == "y")' failed … while evaluating the condition of the assertion '(arg == "y")'
at /pwd/lang/eval-fail-assert.nix:2:12: at /pwd/lang/eval-fail-assert.nix:2:12:
1| let { 1| let {
2| x = arg: assert arg == "y"; 123; 2| x = arg: assert arg == "y"; 123;
| ^ | ^
3| 3|
error: string '"x"' is not equal to string '"y"'