System.IO.Abstractions icon indicating copy to clipboard operation
System.IO.Abstractions copied to clipboard

Test Implementation for Path.GetRelativePath

Open zach-delong opened this issue 4 years ago • 4 comments

Is your feature request related to a problem? Please describe. I am trying to implement a static site generator, and one of the tasks I need to do is adjust file paths handed to me to be relative to the root of the "static site" I am trying to create. This is something that Microsoft makes available via Path.GetRelativePath, and the normal implementation of it in System.IO.Abstractions seems to work great, but the Mock implementation is not being overwritten so the result is strange.

Describe the solution you'd like I think it should be possible to write some kind of mock implementation that would use the mock file system as reference.

Describe alternatives you've considered I could probably also implement this as a specific mock for my project, but wanted to see if this was something the community had an interest in.

Additional context If desired, my plan is to start prototyping this out today, but I can't make any promises on how fast it'll go.

This is the example code that I need to "mock" the behavior of and this is a prototype test that currently behaves very strangely because of the GetRelativePath mock implementation.

zach-delong avatar Nov 29 '21 14:11 zach-delong

After spending some more time debugging and looking into this issue, I realize now that what I am dealing with is probably a strange edge case, and not as pervasive as I previously thought, which is great! I'm still a bit confused by it, though. This example test always fails:

[Fact]
public void Foo()
{
        var fs = new MockFileSystem();

        fs.AddDirectory("input");
        fs.AddDirectory("output");

        fs.AddFile("input/a.txt", "foo");

        var result = fs.Path.GetRelativePath("/input", "a.txt");

        Assert.Equal("a.txt", result);
}

But if you change "/input" to "input" in the GetRelativePath call, it all works as expected. In reality, in this example, /input is what is created either way, but result will have the path to my test DLL when run.

zach-delong avatar Nov 29 '21 19:11 zach-delong

Thanks for reporting this and sharing some test cases!

Which OS are you running the test on? /input is something different than input so I'd not really expected these tests to succeed. Can you share the test results here so that its easier to understand what's going on?

fgreinacher avatar Dec 01 '21 07:12 fgreinacher

I am running this on Linux. So after digging around what I have realized is that if your path doesn't exist relative to the path provided, the API does strange things reaching out to the file system to figure out how to complete it. Or at least that's what it seems like. The two tests for this behavior are this:

public void Foo()
{
        var fs = new MockFileSystem();

        fs.AddDirectory("input");
        fs.AddDirectory("output");

        fs.AddFile("input/a.txt", "foo");

        var result = fs.Path.GetRelativePath("/input", "a.txt");

        Assert.Equal("a.txt", result);
}

and

public void Foo()
{
        var fs = new MockFileSystem();

        fs.AddDirectory("input");
        fs.AddDirectory("output");

        fs.AddFile("input/a.txt", "foo");

        var result = fs.Path.GetRelativePath("input", "a.txt");

        Assert.Equal("a.txt", result);
}

If you examine you will find that in both cases, the path /input/a.txt exists, but in the first case, the relative path result will be a path to the executing DLL (in my case a test project DLL). It seems like the system path method that is being proxied through is doing more than just string manipulation. The documentation points out that it calls "Paths are resolved by calling the GetFullPath method..." which would suggest that it's reaching out to the underlying file system and not just doing string manipulation like the comments in the test project seem to suggest.

I need to find some more time to test, to confirm or deny that theory.

zach-delong avatar Dec 14 '21 01:12 zach-delong

Thanks for the detailed analysis! The .NET implementation seems to do quite a lot of stuff, it's totally possible that we need to override (certain parts of) it in our mock implementation.

fgreinacher avatar Dec 17 '21 12:12 fgreinacher

I just bumped into this as well. Debugging into the base implementation of Path.GetRelativePath(), (and as @ZacheryPD mentioned) it calls GetFullPath(), which the mock can't intercept (note I'm currently on Windows, so it's possible the behavior on Linux is different).

In my case I can get the MockFileSystem to behave the same if I switch from the new MockFileSystem() constructor to one like this:

new MockFileSystem(files: null, Environment.CurrentDirectory)

(see https://github.com/TestableIO/System.IO.Abstractions/blob/d0745332db8d46825e10bf289883309c375cc550/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileSystem.cs#L27C67-L27C100)

Then, the MockFileSystem uses the same current directory when creating mock files as will be resolved by the Path class. I think it might make sense to update the default constructor of MockFileSystem to do something similar.

If you have any questions / concerns, please let me know! I can also provide a repo case if necessary.

MattKotsenas avatar Jul 23 '23 00:07 MattKotsenas

Fixed with #1012

vbreuss avatar Aug 01 '23 18:08 vbreuss