Skip to content
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

Strange behavior with dynamic + indexed property #913

Closed
bclothier opened this issue Aug 29, 2019 · 4 comments
Closed

Strange behavior with dynamic + indexed property #913

bclothier opened this issue Aug 29, 2019 · 4 comments

Comments

@bclothier
Copy link

Given this code, using a COM reference to the Scripting library (scrrun.dll, usually located in the ..\Windows\System32 folder:


        [Test]
        public void derp()
        {
            var input = "abc";
            var mockFso = new Mock<Scripting.FileSystemObject>();
            mockFso.As<Scripting.IFileSystem3>();
            mockFso.As<Scripting.IFileSystem>();

            var mockDrives = new Mock<Scripting.Drives>();
            var mockDrive = new Mock<Scripting.Drive>();

            mockFso.Setup(x => x.Drives).Returns(mockDrives.Object);
            mockDrives.Setup(x => x[It.Is<object>(p => p == input)]).Returns(mockDrive.Object);
            mockDrive.Setup(x => x.Path).Returns("foobar");
            
            dynamic mocked = mockFso.Object;

            Assert.AreEqual("foobar", mockFso.Object.Drives[input].Path);       // passes
            Assert.AreEqual("foobar", mocked.Drives[input].Path);               // fails
            Assert.AreEqual("foobar", mocked.Drives.Item(input).Path);          // fails
        }

The first assert demonstrates that the setups are all good but if we use dynamic version, the binding fails. Apparently it returns null at the Drives, and we cannot go any further.

Furthermore, if we execute the following in the immediate window:

input=="abc"
true
mockDrives.Object["abc"]
null
mockDrives.Object[input]
{Castle.Proxies.DriveProxy}
    AvailableSpace: null
    DriveLetter: null
    DriveType: UnknownType
    FileSystem: null
    FreeSpace: null
    Interceptor: {Moq.Mock<Scripting.Drive>}
    IsReady: false
    Mock (Castle.Proxies.DriveProxy): {Moq.Mock<Scripting.Drive>}
    Mock: {Moq.Mock<Scripting.Drive>}
    Path: "foobar"
    RootFolder: null
    SerialNumber: 0
    ShareName: null
    TotalSize: null
    VolumeName: null

Why would the mock behave differently simply because I passed a literal vs. a variable? That doesn't seem right.

@stakx
Copy link
Contributor

stakx commented Aug 30, 2019

Regarding the first issue (your failing test), it fails because the setup and the invocation don't see the same method: one sees IFileSystem.get_Drives, the other sees IFileSystem3.get_Drives. The latter gets redeclared in the derived interface using new, and because of that, the proxy type will have separate implementation methods for the two interface methods.

Neither DynamicProxy nor Moq consider the matching DISPIDs at all. (At least for Moq, I suspect we'll keep it that way, at least in the near term, as adding DISPID checks in such a "hot" execution path is going to affect performance for all calls while being beneficial for only a very rare few.)

I'll get back to you about the second issue (literal vs. variable).

@bclothier
Copy link
Author

I can confirm that with the following revisions, the test passes.

        [Test]
        public void derp()
        {
            var input = "abc";
            var mockFso = new Mock<Scripting.FileSystemObject>();
            
            var mockDrives = new Mock<Scripting.Drives>();
            var mockDrive = new Mock<Scripting.Drive>();

            mockFso.Setup(x => x.Drives).Returns(mockDrives.Object);
            mockFso.As<Scripting.IFileSystem3>().Setup(x => x.Drives).Returns(mockDrives.Object);
            mockFso.As<Scripting.IFileSystem>().Setup(x => x.Drives).Returns(mockDrives.Object);
            mockDrives.Setup(x => x[It.Is<object>(p => p == input)]).Returns(mockDrive.Object);
            mockDrive.Setup(x => x.Path).Returns("foobar");
            
            dynamic mocked = mockFso.Object;

            Assert.AreEqual("foobar", mockFso.Object.Drives[input].Path);       // passes
            Assert.AreEqual("foobar", mocked.Drives[input].Path);               // fails
            // Assert.AreEqual("foobar", mocked.Drives.Item(input).Path);          // never would have worked
        }

I will need to investigate into ensuring that setups happens for all interfaces with same members to avoid the uncertainty as I don't have any guarantee over which interface will get used.

I had a late realization about the literal vs. variable thing. The problem was that the parameter type is an object, which is implicitly a string. However, p => p == input does a reference comparison, not an equality. Changing to either (string)p => p == input or p => p.Equals(input) fixes the problem. That completely tripped me up, so apologies about that!

Since that shows that the mistake is with my flawed test, I'll close this issue. Thanks for the help!

@stakx
Copy link
Contributor

stakx commented Aug 30, 2019

Regarding your second question:

Why would the mock behave differently simply because I passed a literal vs. a variable? That doesn't seem right.

Everything is as it should be in this case.

mockDrives.Setup(x => x[It.Is<object>(p => p == input)]).Returns(mockDrive.Object);

Break the program after that setup has been made, inspect input and "abc" in the Watch window, and assign the former an object ID. You'll likely find out that way that input refers to a different instance of the string "abc" than the string literal "abc".

Now, your matcher uses the == operator, which for a left operand of static type object has the equivalent effect of doing a object.ReferenceEquals(p, input). That is, you are comparing identities, not comparing values.

These two facts explain why your setup can match for input, but not for "abc".

This problem is somewhat academic because this only seems to happen during debugging. It seems that the debugger won't intern strings the same way as would happen if you ran your program regularly, without a debugger attached.

I can think of a ways to resolve this:

  1. Intern your strings explicitly (using string.Intern) before you perform an identity comparison.
  2. Use object.Equals(p, input) instead of p == input.
  3. Don't use a matcher at all. Simply rewrite your setup expression as .Setup(x => x[input]). Moq will match input against concrete values via object.Equals.

@stakx
Copy link
Contributor

stakx commented Aug 30, 2019

Saw too late you'd already figured out the same thing. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants