-
Notifications
You must be signed in to change notification settings - Fork 256
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: mocking objects in Dafny tests #1526
Conversation
Thanks for submitting this. I saw that you wrote a test file, could you also please write the "expected output" of this test file and have the test verify that the output is correct? |
Thank you, Mikael! The Dafny tests that I have added are expected to pass so the output of the corresponding lit test should stay the same (it only reports the failed tests) - hence no modifications to the On another note, I can't seem to make the "Semantic Pull Request" check pass although I believe both the PR and at least on of the commits should be "semantic" - would be grateful for any hints about what I should change to make this work! |
We don't know what went wrong with semantic PR. It looks like there is no way to decipher their error message. |
I'm definitely wary about this. It introduces unsoundness into Dafny programs in potentially very subtle ways, given that the compilers are not built with the requirement that this approach has to work. FYI I experimented a little myself with whether we could just declare a variable I can appreciate that it's difficult to figure out what constructor to use in generated tests, but I wouldn't want to encourage this pattern in any other manually-crafted tests. Also, if it's impossible to construct an object with the values you need for a generated test case to hit a certain code branch, for example, that's a deeper issue in the code we should communicate to the user anyway! :)
Is there any need for returning a value from
That's just a flat out off-by-one error, and thanks for fixing it. Looks like I got unlucky and didn't have any
Right back at you! I'm definitely hoping to find a solution to my worry above, especially since I would love to unblock you from getting |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See top-level reply comment
// TODO: The resolver needs to check the assumptions about the declaration | ||
// (i.e. must be public and static, must return a "result type", etc.) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just for the record, I'm fine with losing this TODO even through this PR doesn't address it (as it really should be the resolver's job instead of each compiler, and should result in an error rather than just not generating the needed shim code), because it's already tracked in #451 instead
Instead of changing how the compiler handles Bundle a 'testing runtime' together with the generated tests that allows you to create the parameter values you need to execute the tests, then make the generated tests have 0 arguments. Instead of include "object.dfy"
module objectUnitTests {
import M
method test0(v0:M.Value) returns (r0:int) modifies v0 {
v0.v := -39;
r0 := M.compareToZero(v0);
}
} Have include "object.dfy"
include "testRuntime.dfy"
module objectUnitTests {
import M
method test0() returns (r0:int) {
var v0:M.Value = TestRuntime.CreateUnitialized<M.Value>();
v0.v := -39;
r0 := M.compareToZero(v0);
}
} |
Thank you for the input on this, @robin-aws and @keyboardDrummer ! I will experiment with either inferring constructor arguments or adding an external method. If using an external method, is the idea to provide the c# code it corresponds to in a separate file? This approach would still use Inferring relevant constructor arguments may be possible but the caveat is that constructor calls might alter the state of other objects in the program and taking into account the side-effects would be difficult. This approach will also be restricted to cases when the constructor has no loops or only has loops that are easy to unroll. I will try to find a solution but am not sure that it is possible to make it work in a general case.
The test methods currently generated by the Let me change this PR to draft until I come up with another solution for mocking and then I will change this one accordingly! |
Yes that's the idea. @robin-aws would that alleviate your soundness concerns? I thought they came from changes the compilation of |
Yeah, I like this! Would the test generation also spit out the
Okay I'll buy that. :) |
I have looked at this problem further and I think I don't quite understand the unsoundness problem. @robin-aws, would you have a moment to take a look at this? I have been working on the solution @keyboardDrummer proposed but it would require more changes to the compiler (see below), so I wanted to make certain I understand what is the issue with the approach using class Prime {
var value:int;
function IsValid():bool {
// return true if value is prime
}
method Next() returns (p:Prime)
requires this.IsValid()
ensures p.IsValid() // + more postconditions
{
// compute and return next prime number
}
} The method {:test} test(p:Prime) returns (o: VoidOutcome) modifies p {
p.value := 5;
var next:Prime := p.Next();
if next.value == 7 {
return VoidSuccess;
} else {
return VoidFailure("Next prime number is not computed correctly");
}
} This method should be sound from the Dafny perspective. If it passes an invalid object to Now the suggestion is to generate an [Xunit.Fact]
public static void test_CheckForFailureForXunit() {
// in all other target languages, GetUninitializedObject can be replaced with zero constructor
var p0 = FormatterServices.GetUninitializedObject(typeof(Prime)) as Prime;
var r0 = test(p0);
Xunit.Assert.False(r0.IsFailure(), "Dafny test failed: " + r0);
} The only concern I see is that The solution that uses |
It appears that, unfortunately, one cannot pass a type parameter to such a CreateUninitialized method. You would need to ensure that the generated value is |
Just catching up with this thread. I think one issue here is that we are making a distinction between human-written test cases and the ones generated by an automatic test case generator. I already discussed this separately with @Dargones. In short, there should be no features being added to the Dafny unit testing framework solely to be used by the automatic test case generator. For example, methods annotated with Instead, this is how the test method should look like (and how a programmer would write it anyways):
Am I missing something here? |
You're right. The problem is that the argument of |
I'm confused, doesn't your example contain a method annotated with
|
Yes, the initial message does have an example with arguments. But I discussed this with @Dargones since then, and we agreed that we do not need arguments after all, which should simplify the implementation. |
### UPDATE (Mar 28 2022): As per the ensuing discussion, the functionality of the `{:mock}` and `{:fresh}` attributes has been merged into a single `{:synthesize}` attribute. Methods annotated with `{:synthesize}` no longer need to be annotated with `{:extern}`. The compilation behavior described below no longer requires a special CLI option to be triggered. Finally, the compiler will now throw an error if a stubbed method includes any post-conditions, which should guard against a use of the `{:synthesize}` attribute that might introduce unsoundness. ### ORIGINAL PR DESCRIPTION: This PR adds mocking support to the C# compiler. The idea is to have a special annotation for external methods that makes the compiler translate a postcondition to a series of API calls in C#'s Moq library (provided the `/compileMocks` command-line argument is also supplied). As an example, consider the following Dafny code, which tests the static method `getOrDefault` of class `StringMap` by mocking a `StringMap` object and stubbing its two instance methods. Using this approach, one is able to test the implementation of `getOrDefault` without specifying how one would add elements to the map. ```Dafny class StringMap { var m:map<string, string>; function method Contains(key:string):bool reads this { key in m } function method Get(key:string):string reads this requires Contains(key) { m[key] } static function method GetOrDefault(m:StringMap, key:string, default:string):string reads m { if m.Contains(key) then m.Get(key) else default } } method {:extern} {:mock} MockStringMap(k:string, v:string) returns (m:StringMap) ensures m.Contains(k) == true && m.Get(k) == v ensures fresh(m) method {:test} PassingTestGetOrDefault() { var stringMap := MockStringMap("a", "b"); assert StringMap.GetOrDefault(stringMap, "a", "c") == "b"; expect StringMap.GetOrDefault(stringMap, "a", "c") == "b"; } ``` The `mockStringMap` method is automatically translated to the following in C#, allowing a runtime test that would otherwise be impossible without fully implementing the StringMap class: ```cs public static StringMap mockStringMap(Dafny.ISequence<char> k, Dafny.ISequence<char> v) { var mTmp = new Mock<StringMap>(); mTmp.Setup(x => x.contains(k)).Returns(true); mTmp.Setup(x => x.get(k)).Returns(v); return mTmp.Object; } ``` Only functions can be mocked - methods can have side effects and are, therefore, not allowed in postconditions. Not all Dafny postconditions can be translated to calls in Moq either - compiling an arbitrary postcondition would be a full-blown synthesis problem and, while intersting from a research perspective, it would probably not be fast enough for a compiler. Here is the grammar for postconditions that are supported (`S` is the start symbol, `EXP` stands for an arbitrary Dafny expression, and `ID` stands for variable/method/type identifiers): ``` S = FORALL | EQUALS | S && S EQUALS = ID.ID (ARGLIST) == EXP // stubs a method call | ID.ID == EXP // stubs field access | EQUALS && EQUALS FORALL = forall SKOLEMS :: EXP ==> EQUALS ARGLIST = ID // this can be one of the skolem variables | EXP // this exp may not reference any of the skolems | ARGLIST, ARGLIST SKOLEMS = ID : ID | SKOLEMS, SKOLEMS ``` The compiler throws a NotImplementedException if postconditions on the mocked method do not follow this grammar. Support for mocking could be easily extended to Java and other target languages (and I am happy to work on this). Here is a [design document](https://github.com/Dargones/TestingFrameworkPaper/blob/master/main.pdf) that outlines the overall idea of a testing framework and of how one would do this in Java. An additional feature that this PR introduces is the `{:fresh}` annotation that prompts the compiler to translate an external method to one that returns a fresh instance of an object. This could be useful in situations where inferring the right parameters to a constructor is difficult. So, for instance, an automatic test generation framework could use this to get a fresh instance of an object to then assign its fields as needed. This is the feature that was previously the topic of [this PR](#1526). Finally, this PR also fixes a small issue with the `{:test}` annotation - currently, the C# compiler only calls `IsFailure` on the first returned value of a testing method if there are more than one (should be more than zero) returned value ([this](https://github.com/dafny-lang/dafny/blob/c6e84137226510094fd9079534e967d35f07a3f3/Source/Dafny/Compilers/Compiler-Csharp.cs#L3185) is the line with the bug)
Closing, since this is an earlier version of PR#1809, which has now been merged. Thank you to everyone for all the feedback. |
This PR makes a few modifications to the way the
{:test}
-annotated methods are compiled to C#, which should make it possible to use this attribute with tests generated by Dafny's Test Generation module as previously suggested by Robin here (#1385 (comment)). If these modifications seem adequate, I am planning to make{:test}
compilable to Java as well. The changes are:If the tested method takes parameters, mock them using C#'s
FormatterServices.GetUninitializedObject
method. This is useful in situations in which you know the state of the object you would like to test and can set its fields accordingly but don't know what constructor to use to allocate the object without running into a runtime exception. In other target languages, the mocking could be done by exploiting the zero constructor.Allow tested methods to return non failure-compatible values. Only call
IsFailure
on the first returned value if it is failure-compatible. This is useful as it allows using methods annotated with{:test}
for other purposes, e.g. for printing the returned values.Not sure if this is important but the previous version of the code would only call
IsFailure
on the first returned value of a method if there were more than one returned value. Is there any reason for requiring two or more returned values? I removed this condition and the tests pass but do let me know if it should still be there.Thank you!