-
Notifications
You must be signed in to change notification settings - Fork 899
Tests can't run in parallel. #826
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
Conversation
Labeled as "up-for grabs". |
@Zoltu Based on #825, I've quickly hacked something that "looks" like working with both This proposal should avoid the issues that you've encountered. However, the drawback is that the recursively copied Note: This doesn't sandbox ( Could you please double check and see if maybe his could get wrapped into a nice PR? --- a/LibGit2Sharp.Tests/TestHelpers/BaseFixture.cs
+++ b/LibGit2Sharp.Tests/TestHelpers/BaseFixture.cs
@@ -21,7 +21,7 @@ static BaseFixture()
// Do the set up in the static ctor so it only happens once
SetUpTestEnvironment();
- DirectoryHelper.DeleteSubdirectories(Constants.TemporaryReposPath);
+// DirectoryHelper.DeleteSubdirectories(Constants.TemporaryReposPath);
}
public static string BareTestRepoPath { get; private set; }
@@ -49,12 +49,12 @@ private static void SetUpTestEnvironment()
var source = new DirectoryInfo(@"../../Resources");
ResourcesDirectory = new DirectoryInfo(string.Format(@"Resources/{0}", Guid.NewGuid()));
- var parent = new DirectoryInfo(@"Resources");
+ //var parent = new DirectoryInfo(@"Resources");
- if (parent.Exists)
- {
- DirectoryHelper.DeleteSubdirectories(parent.FullName);
- }
+ //if (parent.Exists)
+ //{
+ // //DirectoryHelper.DeleteSubdirectories(parent.FullName);
+ //}
DirectoryHelper.CopyFilesRecursively(source, ResourcesDirectory); |
What I have done to solve this sort of problem in the past was have the fixture setup for each test copy a clean version of whatever disk resource it needed to work with into a directory named Guid.NewGuid(). Then each test would work inside of its own copy of the folder (only it knows the Guid.NewGuid path) and delete it when done. Of course, if you have a lot of tests like this you end up bottlenecked a bit on disk IO. I do not believe there is any way to take an action after all of the tests are finished, especially in an environment like NCrunch where the tests are often times running non-stop, just cycling through them. Also, depending on the testing framework you can end up with resources not being cleaned up. In my experience, xUnit (2.0) always executes the fixture Dispose whereas nUnit will only execute the test tear down when the test setup was successful. My concern with your proposed solution would be the fact that depending on the test runner, it may not always be obvious to the end-user that there are uncleaned up resources left around on disk. In the case of NCrunch, it puts all of its stuff somewhere buried in %appdata% (C:\Users\Micah\AppData\Local\NCrunch\252\7\LibGit2Sharp.Tests\bin\Debug\Resources for my latest run, the numbered directories may be different for you). I would be concerned that over time, my NCrunch directory would fill up with these files, though NCrunch does seem to clean them up on resync. That all being said, it does appear to allow the tests to run reliably and they are much faster. In a perfect (and largely unrealistic) world, I would recommend mocking the file system calls and running all of the tests against a virtual (in memory) filesystem (no disk IO at all). I'm guessing that would be a lot of work though, and probably not worth it. |
Great!
I completely agree. How about this?
diff --git a/LibGit2Sharp.Tests/ArchiveFixture.cs b/LibGit2Sharp.Tests/ArchiveFixture.cs
index e070db8..c5b09df 100644
--- a/LibGit2Sharp.Tests/ArchiveFixture.cs
+++ b/LibGit2Sharp.Tests/ArchiveFixture.cs
@@ -11,7 +11,8 @@ public class ArchiveFixture : BaseFixture
[Fact]
public void CanArchiveATree()
{
- using (var repo = new Repository(BareTestRepoPath))
+ string path = CloneBareTestRepo();
+ using (var repo = new Repository(path))
{
var tree = repo.Lookup<Tree>("581f9824ecaf824221bd36edf5430f2739a7c4f5");
@@ -36,7 +37,8 @@ public void CanArchiveATree()
[Fact]
public void CanArchiveACommit()
{
- using (var repo = new Repository(BareTestRepoPath))
+ string path = CloneBareTestRepo();
+ using (var repo = new Repository(path))
{
var commit = repo.Lookup<Commit>("4c062a6361ae6959e06292c1fa5e2822d9c96345");
@@ -61,7 +63,8 @@ public void CanArchiveACommit()
[Fact]
public void ArchivingANullTreeOrCommitThrows()
{
- using (var repo = new Repository(BareTestRepoPath))
+ string path = CloneBareTestRepo();
+ using (var repo = new Repository(path))
{
Assert.Throws<ArgumentNullException>(() => repo.ObjectDatabase.Archive((Commit)null, null));
Assert.Throws<ArgumentNullException>(() => repo.ObjectDatabase.Archive((Tree)null, null));
diff --git a/LibGit2Sharp.Tests/BlameFixture.cs b/LibGit2Sharp.Tests/BlameFixture.cs
index b0fcf77..bdfa46f 100644
--- a/LibGit2Sharp.Tests/BlameFixture.cs
+++ b/LibGit2Sharp.Tests/BlameFixture.cs
@@ -22,7 +22,8 @@ private static void AssertCorrectHeadBlame(BlameHunkCollection blame)
[Fact]
public void CanBlameSimply()
{
- using (var repo = new Repository(BareTestRepoPath))
+ string path = CloneBareTestRepo();
+ using (var repo = new Repository(path))
{
AssertCorrectHeadBlame(repo.Blame("README"));
}
@@ -31,7 +32,8 @@ public void CanBlameSimply()
[Fact]
public void CanBlameFromADifferentCommit()
{
- using (var repo = new Repository(MergedTestRepoWorkingDirPath))
+ string path = CloneMergedTestRepo();
+ using (var repo = new Repository(path))
{
// File doesn't exist at HEAD
Assert.Throws<LibGit2SharpException>(() => repo.Blame("ancestor-only.txt"));
@@ -44,7 +46,8 @@ public void CanBlameFromADifferentCommit()
[Fact]
public void ValidatesLimits()
{
- using (var repo = new Repository(BareTestRepoPath))
+ string path = CloneBareTestRepo();
+ using (var repo = new Repository(path))
{
var blame = repo.Blame("README");
@@ -56,7 +59,8 @@ public void ValidatesLimits()
[Fact]
public void CanBlameFromVariousTypes()
{
- using (var repo = new Repository(BareTestRepoPath))
+ string path = CloneBareTestRepo();
+ using (var repo = new Repository(path))
{
AssertCorrectHeadBlame(repo.Blame("README", new BlameOptions {StartingAt = "HEAD" }));
AssertCorrectHeadBlame(repo.Blame("README", new BlameOptions {StartingAt = repo.Head }));
@@ -68,7 +72,8 @@ public void CanBlameFromVariousTypes()
[Fact]
public void CanStopBlame()
{
- using (var repo = new Repository(BareTestRepoPath))
+ string path = CloneBareTestRepo();
+ using (var repo = new Repository(path))
{
// $ git blame .\new.txt
// 9fd738e8 (Scott Chacon 2010-05-24 10:19:19 -0700 1) my new file
diff --git a/LibGit2Sharp.Tests/BlobFixture.cs b/LibGit2Sharp.Tests/BlobFixture.cs
index f615aed..e9dc554 100644
--- a/LibGit2Sharp.Tests/BlobFixture.cs
+++ b/LibGit2Sharp.Tests/BlobFixture.cs
@@ -12,7 +12,8 @@ public class BlobFixture : BaseFixture
[Fact]
public void CanGetBlobAsText()
{
- using (var repo = new Repository(BareTestRepoPath))
+ string path = CloneBareTestRepo();
+ using (var repo = new Repository(path))
{
var blob = repo.Lookup<Blob>("a8233120f6ad708f843d861ce2b7228ec4e3dec6");
@@ -86,7 +87,8 @@ public void CanGetBlobAsTextWithVariousEncodings(string encodingName, int expect
[Fact]
public void CanGetBlobSize()
{
- using (var repo = new Repository(BareTestRepoPath))
+ string path = CloneBareTestRepo();
+ using (var repo = new Repository(path))
{
var blob = repo.Lookup<Blob>("a8233120f6ad708f843d861ce2b7228ec4e3dec6");
Assert.Equal(10, blob.Size);
@@ -106,7 +108,8 @@ public void CanLookUpBlob()
[Fact]
public void CanReadBlobStream()
{
- using (var repo = new Repository(BareTestRepoPath))
+ string path = CloneBareTestRepo();
+ using (var repo = new Repository(path))
{
var blob = repo.Lookup<Blob>("a8233120f6ad708f843d861ce2b7228ec4e3dec6");
@@ -208,7 +211,8 @@ public void CanStageAFileGeneratedFromABlobContentStream()
[Fact]
public void CanTellIfTheBlobContentLooksLikeBinary()
{
- using (var repo = new Repository(BareTestRepoPath))
+ string path = CloneBareTestRepo();
+ using (var repo = new Repository(path))
{
var blob = repo.Lookup<Blob>("a8233120f6ad708f843d861ce2b7228ec4e3dec6");
Assert.Equal(false, blob.IsBinary);
diff --git a/LibGit2Sharp.Tests/TestHelpers/BaseFixture.cs b/LibGit2Sharp.Tests/TestHelpers/BaseFixture.cs
index 2cab50d..a94f84b 100644
--- a/LibGit2Sharp.Tests/TestHelpers/BaseFixture.cs
+++ b/LibGit2Sharp.Tests/TestHelpers/BaseFixture.cs
@@ -48,26 +48,17 @@ private static void SetUpTestEnvironment()
IsFileSystemCaseSensitive = IsFileSystemCaseSensitiveInternal();
var source = new DirectoryInfo(@"../../Resources");
- ResourcesDirectory = new DirectoryInfo(string.Format(@"Resources/{0}", Guid.NewGuid()));
- var parent = new DirectoryInfo(@"Resources");
-
- if (parent.Exists)
- {
- DirectoryHelper.DeleteSubdirectories(parent.FullName);
- }
-
- DirectoryHelper.CopyFilesRecursively(source, ResourcesDirectory);
// Setup standard paths to our test repositories
- BareTestRepoPath = Path.Combine(ResourcesDirectory.FullName, "testrepo.git");
- StandardTestRepoWorkingDirPath = Path.Combine(ResourcesDirectory.FullName, "testrepo_wd");
+ BareTestRepoPath = Path.Combine(source.FullName, "testrepo.git");
+ StandardTestRepoWorkingDirPath = Path.Combine(source.FullName, "testrepo_wd");
StandardTestRepoPath = Path.Combine(StandardTestRepoWorkingDirPath, ".git");
- ShallowTestRepoPath = Path.Combine(ResourcesDirectory.FullName, "shallow.git");
- MergedTestRepoWorkingDirPath = Path.Combine(ResourcesDirectory.FullName, "mergedrepo_wd");
- MergeRenamesTestRepoWorkingDirPath = Path.Combine(ResourcesDirectory.FullName, "mergerenames_wd");
- MergeTestRepoWorkingDirPath = Path.Combine(ResourcesDirectory.FullName, "merge_testrepo_wd");
- RevertTestRepoWorkingDirPath = Path.Combine(ResourcesDirectory.FullName, "revert_testrepo_wd");
- SubmoduleTestRepoWorkingDirPath = Path.Combine(ResourcesDirectory.FullName, "submodule_wd");
+ ShallowTestRepoPath = Path.Combine(source.FullName, "shallow.git");
+ MergedTestRepoWorkingDirPath = Path.Combine(source.FullName, "mergedrepo_wd");
+ MergeRenamesTestRepoWorkingDirPath = Path.Combine(source.FullName, "mergerenames_wd");
+ MergeTestRepoWorkingDirPath = Path.Combine(source.FullName, "merge_testrepo_wd");
+ RevertTestRepoWorkingDirPath = Path.Combine(source.FullName, "revert_testrepo_wd");
+ SubmoduleTestRepoWorkingDirPath = Path.Combine(source.FullName, "submodule_wd");
}
private static bool IsFileSystemCaseSensitiveInternal() |
This approach seems reasonable, though I am hesitant to say much since I am not familiar with the bigger picture. Another option that is nCrunch specific would be this: http://www.ncrunch.net/documentation/reference_runtime-framework_exclusively-uses-attribute You can use attributes to teach nCrunch that certain tests shouldn't be run in parallel because they both use the same resource. I'm not a huge fan of this since the solution only works with nCrunch, not any other parallel test runner. However, it would make it so you didn't need to keep copying/deleting directories when testing, making sequential runs faster. I think the most "right" solution (though also the most work by far) would be to mock out the filesystem calls throughout libgit2 and use some kind of dependency injection system to fulfill them at runtime with either an in-memory mock filesystem or the real thing. Then you could execute all of these tests without going to disk at all. I would be up for discussing this more if you like, just be aware that it is a large undertaking. |
@Zoltu You're talking about intercepting the file system calls that libgit2 makes? As opposed to pulling an interface out of Proxy and injecting a mock/stub version into Repository in tests? |
I mean that anywhere in the codebase where you do something like
|
A few thoughts on this thread:
The role of So its not too different to what @nulltoken was working towards earlier, but we have some extra work after the test runs.
@nulltoken @Zoltu I've not had a chance to look at the current state of affairs for the |
This issue has been transmuted into a proper Pull Request. I renamed the The NCrunch solution settings has been tweaked so that it now allows every test to be run in parallel. I'd really like a deep review on this as this is the kind of huge change which is a magnet for mistakes and overlooks. |
👍 |
I did find I needed one extra change: --- a/LibGit2Sharp.Tests/TestHelpers/BaseFixture.cs
+++ b/LibGit2Sharp.Tests/TestHelpers/BaseFixture.cs
@@ -58,12 +58,6 @@ private static void SetUpTestEnvironment()
var source = new DirectoryInfo(@"../../Resources");
var resourcesRelativePath = string.Format(@"Resources/{0}", Guid.NewGuid());
ResourcesDirectory = new DirectoryInfo(resourcesRelativePath);
- var parent = new DirectoryInfo(@"Resources");
-
- if (parent.Exists)
- {
- DirectoryHelper.DeleteSubdirectories(parent.FullName);
- }
DirectoryHelper.CopyFilesRecursively(source, ResourcesDirectory); (Sorry about the lack of pretty colours: I can't find out how you did that!) Without this, the BaseFixture will try to delete all the registered directories, all but one of which won't exist in an NCrunch workspace. I think one could address this by throwing away the global cleanup in favour of a small helper, something like public class Sandbox : IDisposable
{
public string Path { get; private set; }
public Sandbox(string path)
{
Path = path;
// Resource setup: copying stuff etc.
}
public void Dispose()
{
// Delete the sandbox directory
}
}
public sealed class Temp
{
public static Sandbox CleanSandbox()
{
return new Sandbox(someGeneratedPath);
}
} Tests then just say using (Temp.Sandbox()) {
// Profit!
} Each test then becomes responsible for resource cleanup, but keeping syntax nice & light. |
Strip the leading whitespace and use |
@frankshearar Better now? |
@nulltoken Thanks! Much better! Works out of the box for NCrunch. And using R#'s test runner, I can see the temp directories being deleted, so it seems like a win all round? |
@frankshearar Thanks for the feedback 😍 I've rebased this. Let's make sure the CIs are still happy and let's get this in! |
Currently, when you try to run the tests in parallel they run into file contention issues where one of the tests will take out a lock on some file and then another test fails because it can't open the file. Finding an optimal solution would require more knowledge than I have, but my first thought would be to have each test copy the resources it needs before using them, rather than having multiple tests utilize the same set of resources.