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

Add support for Zones to assist with unit testing. #55

Open
bsutton opened this issue Dec 10, 2019 · 14 comments
Open

Add support for Zones to assist with unit testing. #55

bsutton opened this issue Dec 10, 2019 · 14 comments

Comments

@bsutton
Copy link

bsutton commented Dec 10, 2019

I'm the author of dshell

https://pub.dev/packages/dshell.

I'm using the path package extensively.

I'm currently trying to write unit tests for dshell and having some problems.

Dshell creates a ~/.dshell directory and all my unit tests make adjustments to this directory and its children.

When running unit tests this can be problematic for two reasons.

  1. unit tests can't be serialized so different unit tests are writing over each others changes to ~/.dshell and its children
  2. Modifying my local file system is of concern as a number of my unit tests do recursive deletes.

As such I'm trying to use Zones and the MemoryFileSystem.
This seems like it will work however the path package causes problems.

The path package has a number of global variables. The key pain point is the 'current' variable that maintains a cache of of the current working directory.

As this variable is global it is shared across zones which means that my unit tests still interfere with each other (e.g. if one test changes the CWD then all unit tests get the new CWD).

It seems to me that we could modify the paths package to be zone aware without much effort.

The 'current' method could be modified to check if it is in the root zone via;
Zone.current == Zone.root

If it is in the root zone it continues as it currently does.
If its not in the root zone then we could modified to pull its settings from a Zone key.

_current = Zone[#paths]._current;

The #paths key would be a unique class that contains all of the global settings that paths uses.

If I submitted a change to make the path package zone aware would it be accepted (assuming suitable quality standards are met)?

I believe these changes could be made without modifying the current api.

@natebosch
Copy link
Member

I'm nervous about the idea of having a fake current. A processes working directory is meaningful outside of user code. It would be weird to me if p.current shows one thing, but pwdx <dart VM PID> shows a different thing.

We do already have a hack in this package where current might not match the VM's actual working directory in the case of deleted directories, but I'm not in favor of opening that hack up further.

I'd recommend not changing the working directory of the VM within tests.

What are the specific behaviors you need for where current has a different apparent directory from the processes actual working directory? Can you write your code such that you don't depend on the process working directory at all?

@bsutton
Copy link
Author

bsutton commented Dec 11, 2019 via email

@natebosch
Copy link
Member

'It would be weird to me if p.current shows one thing, but the current
File system
shows a different thing.

We should narrow in on why that happens. Rather than overriding for both the filesystem and this package, the override for the filesystem should work automatically.

When you override the filesystem in a zone, what does Uri.base give you? If it's something other than the correct cwd for that zone we should move this issue to the SDK.

@bsutton
Copy link
Author

bsutton commented Dec 13, 2019

Am I correct in my reading of the Context code that it doesn't t track the cwd?

The line in the Context code is:

62: String get current => _current ?? p.current;

My reading of this is that if _current is null then read the cwd.
If _current is not null then don't read the cwd.

As such once _current is set for a context it will never be updated.

Is this the intent of the code as it doesn't appear to make any sense.

Also, I think I found a way to track zones without the need to pass a filesystem to the Context.
It looks like the change will be transparent.
I'm about to go on holidays for a week so might be a little while to I get back to you, unless I can convince the wife to let me take the laptop :)

@bsutton
Copy link
Author

bsutton commented Dec 13, 2019

FYI it looks like Uri.base works correctly in zone with an alternate file system so we only have to manage the fact that path only manages a single cwd.

@bsutton
Copy link
Author

bsutton commented Dec 13, 2019

Notes for me:

I'm building a stack of zones (optimised for the single zone case) which should allow for the tracking of multiple zones.
I am concerned about futures/microtasks as they can result in flipping between zones in a non-stack like manner. This is likely to mean the stack keeps being rebuilt in an odd order. This could also result in leakage as the stack wont' always be collapsed in a sane manner. Its still might work as mostly it will keep being collapsed. I don't want to use a map as they has the potential to leak memory as we have no way of knowing when a zone goes out of scope.
A better idea might be a constrained map. Allow say a max of 10 zones. These would essentially work as a constrained cache (which is exactly what it is after all). If we use a LRU algorithm we should get reasonable performance and no leakage.

@bsutton
Copy link
Author

bsutton commented Dec 13, 2019

So I think contexts are meant to be 'fixed' locations that when directly used they cause all path calls to operate relative to that fixed location regardless if the cwd changes for the process.
So I'm thinking I should leave the contexts alone and rather just implement a zone cache directly under the 'current' in path.dart.
This would appear to support the zone requirements whilst having the smallest possible change set.
So implement an LRU map of zone->cwd.
Change path._current from a string to the above lru map.
Have the path.current method lookup the lru for the cwd of the current zone.
If the cwd changes for a zone just write it back to the cache.

@natebosch
Copy link
Member

So I think contexts are meant to be 'fixed' locations that when directly used they cause all path calls to operate relative to that fixed location regardless if the cwd changes for the process.

I'm not quite sure about whether this is the intention, it could be.

FYI it looks like Uri.base works correctly in zone with an alternate file system so we only have to manage the fact that path only manages a single cwd.

Assuming the above is the intention I think that means there is nothing we need to do here? The top level p.current will always attempt to read Uri.base which implies it should be correct based on the zone overrides already?

@natebosch
Copy link
Member

Another option suggested to me:

Since Context already caches a cwd, you could set up a testing utility outside of this package.

import 'dart:async';

import 'package:path/path.dart' as p;

p.Context get pathContext => Zone.current[#dshellPath] ?? p.context;

T runWithCwd<T>(T Function() body, String current) =>
    runZoned(body, zoneValues: {#dshellPath: p.Context(current: currrent)});

@bsutton
Copy link
Author

bsutton commented Dec 13, 2019

So I've just done some more testing and Uri.base is failing when used with a zone override in place.

If you call

Directory.current 

with a Zone override in place then the correct Zone cwd is returned.

If you call

Uri.base

then then the code ignores the override.

The problem appears to stem from the fact that Uri.base calls:

Uri _uriBaseClosure() from directory_patch.dart

_uriBaseClosure then calls

_Directory._current

Unlike Directory.current the _Directory version ignores overrides.

So Uri.base as it stands won't work with a Zone override.

So the question now is should Uri.base support Zone overrides or should path not use Uri.base (assuming there is some alternate).

The summary is that the latest patch I've submitted doesn't work.
We could try to detect that we are using a conventional file system and then use Directory.current but I'm uncertain how to do this?

I will have a play with your suggested work around, but it still feels like that path has a problem.
I guess the issues is how overrides are viewed. Are these first class citizens in dart or just a hack for some edge cases?

BTW it would be nice to see some doco around Contexts. If they are part of paths public interface then they should be documented because as they stand I still don't know when/if they should be used.

Brett

@natebosch
Copy link
Member

natebosch commented Dec 13, 2019

So the question now is should Uri.base support Zone overrides or should path not use Uri.base (assuming there is some alternate).

In my opinion Uri.base should respect Zone overrides.

Do you have a minimal reproducible example showing that Uri.base differs from Dirctory.current? I think with that we can file an SDK issue. Assuming everyone agrees that Uri.base should agree with Directory.current then fixing that there would solve this in the least hacky way.

@bsutton
Copy link
Author

bsutton commented Dec 14, 2019 via email

@bsutton
Copy link
Author

bsutton commented Dec 14, 2019

Another option suggested to me:

Since Context already caches a cwd, you could set up a testing utility outside of this package.

import 'dart:async';

import 'package:path/path.dart' as p;

p.Context get pathContext => Zone.current[#dshellPath] ?? p.context;

T runWithCwd<T>(T Function() body, String current) =>
    runZoned(body, zoneValues: {#dshellPath: p.Context(current: currrent)});

I've had a better look at this and, if I've understood correctly, I don't think it helps.

If I was writing unit tests that used path directly then sure I can directly call context.xxxx, in the unit test code.

My problem is that I'm writing unit tests for code that uses path.
As such I can't change the code being tested to use an explicit context.

Have I understood this correctly?

@bsutton
Copy link
Author

bsutton commented Dec 14, 2019

I've submitted an issue here:
dart-lang/sdk#39796

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