Skip to content
This repository was archived by the owner on Jun 21, 2023. It is now read-only.

Commit 6fd69fd

Browse files
committed
Add support for testing code that uses LibGit2Sharp
Testing RepositoryModel creation via the path constructor implies hitting libgit2sharp codepaths, and libgit2sharp doesn't expose nice interfaces for testing (why??). So, move things around and add a GitService that replaces the extension methods that depend on libgit2sharp, so we can at least mock at a higher level. The things that require this GitService are not created via mef, so expose it via the Services static class.
1 parent 70a9090 commit 6fd69fd

File tree

13 files changed

+189
-78
lines changed

13 files changed

+189
-78
lines changed

src/DesignTimeStyleHelper/MainWindow.xaml.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
using GitHub.SampleData;
44
using GitHub.Services;
55
using GitHub.UI;
6-
using GitHub.VisualStudio;
6+
using GitHub.Extensions;
77
using GitHub.Models;
88

99
namespace DesignTimeStyleHelper

src/GitHub.App/Services/RepositoryPublishService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ namespace GitHub.Services
1313
public class RepositoryPublishService : IRepositoryPublishService
1414
{
1515
readonly IGitClient gitClient;
16-
readonly Repository activeRepository;
16+
readonly IRepository activeRepository;
1717

1818
[ImportingConstructor]
1919
public RepositoryPublishService(IGitClient gitClient, IVSServices services)

src/GitHub.Exports/Extensions/SimpleRepositoryModelExtensions.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
namespace GitHub.Extensions
88
{
9+
using Services;
10+
using VisualStudio;
911
public static class SimpleRepositoryModelExtensions
1012
{
1113
/// <summary>
@@ -15,12 +17,12 @@ public static ISimpleRepositoryModel ToModel(this IGitRepositoryInfo repo)
1517
{
1618
if (repo == null)
1719
return null;
18-
return SimpleRepositoryModel.Create(repo.RepositoryPath);
20+
return new SimpleRepositoryModel(repo.RepositoryPath);
1921
}
2022

2123
public static bool HasCommits(this ISimpleRepositoryModel repository)
2224
{
23-
var repo = GitHelpers.GetRepoFromPath(repository.LocalPath);
25+
var repo = Services.IGitService.GetRepo(repository.LocalPath);
2426
return repo?.Commits.Any() ?? false;
2527
}
2628

src/GitHub.Exports/Extensions/VSExtensions.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
namespace GitHub.Extensions
77
{
8+
using VisualStudio;
9+
810
public static class VSExtensions
911
{
1012
public static T TryGetService<T>(this IServiceProvider serviceProvider) where T : class
@@ -36,6 +38,14 @@ public static T GetService<T>(this IServiceProvider serviceProvider)
3638
return (T)serviceProvider.GetService(typeof(T));
3739
}
3840

41+
public static T GetExportedValue<T>(this IServiceProvider serviceProvider)
42+
{
43+
var ui = serviceProvider as IUIProvider;
44+
if (ui != null)
45+
return ui.GetService<T>();
46+
return Services.ComponentModel.DefaultExportProvider.GetExportedValue<T>();
47+
}
48+
3949
public static ITeamExplorerSection GetSection(this IServiceProvider serviceProvider, Guid section)
4050
{
4151
return serviceProvider?.GetService<ITeamExplorerPage>()?.GetSection(section);

src/GitHub.Exports/GitHub.Exports.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@
111111
<Compile Include="Models\SimpleRepositoryModel.cs" />
112112
<Compile Include="Helpers\NotificationAwareObject.cs" />
113113
<Compile Include="Services\Connection.cs" />
114+
<Compile Include="Services\GitService.cs" />
114115
<Compile Include="Services\ITeamExplorerServiceHolder.cs" />
115116
<Compile Include="Services\Logger.cs" />
116117
<Compile Include="Services\Services.cs" />

src/GitHub.Exports/Models/SimpleRepositoryModel.cs

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using GitHub.Extensions;
22
using GitHub.Primitives;
3+
using GitHub.Services;
34
using GitHub.UI;
45
using GitHub.VisualStudio;
56
using GitHub.VisualStudio.Helpers;
@@ -10,6 +11,8 @@
1011

1112
namespace GitHub.Models
1213
{
14+
using VisualStudio;
15+
1316
[DebuggerDisplay("{DebuggerDisplay,nq}")]
1417
public class SimpleRepositoryModel : NotificationAwareObject, ISimpleRepositoryModel, INotifyPropertySource, IEquatable<SimpleRepositoryModel>
1518
{
@@ -28,7 +31,7 @@ public SimpleRepositoryModel(string path)
2831
var dir = new DirectoryInfo(path);
2932
if (!dir.Exists)
3033
throw new ArgumentException("Path does not exist", nameof(path));
31-
var uri = GitHelpers.GetRepoFromPath(path)?.GetUri();
34+
var uri = Services.IGitService.GetUri(path);
3235
var name = uri?.NameWithOwner;
3336
if (name == null)
3437
name = dir.Name;
@@ -38,15 +41,6 @@ public SimpleRepositoryModel(string path)
3841
Icon = Octicon.repo;
3942
}
4043

41-
public static ISimpleRepositoryModel Create(string path)
42-
{
43-
if (path == null)
44-
return null;
45-
if (!Directory.Exists(path))
46-
return null;
47-
return new SimpleRepositoryModel(path);
48-
}
49-
5044
public void SetIcon(bool isPrivate, bool isFork)
5145
{
5246
Icon = isPrivate
@@ -60,7 +54,7 @@ public void Refresh()
6054
{
6155
if (LocalPath == null)
6256
return;
63-
var uri = GitHelpers.GetRepoFromPath(LocalPath)?.GetUri();
57+
var uri = Services.IGitService.GetUri(LocalPath);
6458
if (CloneUrl != uri)
6559
CloneUrl = uri;
6660
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using GitHub.Primitives;
2+
using LibGit2Sharp;
3+
using Microsoft.VisualStudio.TeamFoundation.Git.Extensibility;
4+
using System;
5+
using System.Collections.Generic;
6+
using System.ComponentModel.Composition;
7+
using System.Linq;
8+
using System.Text;
9+
using System.Threading.Tasks;
10+
11+
namespace GitHub.Services
12+
{
13+
public interface IGitService
14+
{
15+
UriString GetUri(string path);
16+
UriString GetUri(IRepository repo);
17+
UriString GetUri(IGitRepositoryInfo repoInfo);
18+
IRepository GetRepo(IGitRepositoryInfo repoInfo);
19+
IRepository GetRepo(string path);
20+
}
21+
22+
[Export(typeof(IVSServices))]
23+
[PartCreationPolicy(CreationPolicy.Shared)]
24+
public class GitService : IGitService
25+
{
26+
public UriString GetUri(IRepository repo)
27+
{
28+
return UriString.ToUriString(GetUriFromRepository(repo)?.ToRepositoryUrl());
29+
}
30+
31+
public UriString GetUri(string path)
32+
{
33+
return GetUri(GetRepo(path));
34+
}
35+
36+
public UriString GetUri(IGitRepositoryInfo repoInfo)
37+
{
38+
return GetUri(GetRepo(repoInfo));
39+
}
40+
41+
public IRepository GetRepo(IGitRepositoryInfo repoInfo)
42+
{
43+
var repoPath = Repository.Discover(repoInfo?.RepositoryPath);
44+
if (repoPath == null)
45+
return null;
46+
return new Repository(repoPath);
47+
}
48+
49+
public IRepository GetRepo(string path)
50+
{
51+
var repoPath = Repository.Discover(path);
52+
if (repoPath == null)
53+
return null;
54+
return new Repository(repoPath);
55+
}
56+
57+
internal static UriString GetUriFromRepository(IRepository repo)
58+
{
59+
return repo
60+
?.Network
61+
.Remotes
62+
.FirstOrDefault(x => x.Name.Equals("origin", StringComparison.Ordinal))
63+
?.Url;
64+
}
65+
}
66+
}

src/GitHub.Exports/Services/Services.cs

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,23 @@
55
using LibGit2Sharp;
66
using Microsoft.VisualStudio;
77
using Microsoft.VisualStudio.ComponentModelHost;
8-
using Microsoft.VisualStudio.Shell;
98
using Microsoft.VisualStudio.Shell.Interop;
109
using GitHub.Info;
1110
using GitHub.Primitives;
1211
using GitHub.Extensions;
1312

1413
namespace GitHub.VisualStudio
1514
{
15+
1616
public static class Services
1717
{
1818
public static IServiceProvider PackageServiceProvider { get; set; }
1919

2020
/// <summary>
21-
/// Three ways of getting a service. First, trying the passed-in <paramref name="provider"/>,
22-
/// then <see cref="PackageServiceProvider"/>, then <see cref="T:Microsoft.VisualStudio.Shell.Package"/>
23-
/// If the passed-in provider returns null, try PackageServiceProvider or Package, returning the fetched value
24-
/// regardless of whether it's null or not. Package.GetGlobalService is never called if PackageServiceProvider is set.
25-
/// This is on purpose, to support easy unit testing outside VS.
21+
/// Two ways of getting a service. First, trying the passed-in <paramref name="provider"/>,
22+
/// then <see cref="PackageServiceProvider"/>
23+
/// If the passed-in provider returns null, try PackageServiceProvider, returning the fetched value
24+
/// regardless of whether it's null or not.
2625
/// </summary>
2726
/// <typeparam name="T"></typeparam>
2827
/// <typeparam name="Ret"></typeparam>
@@ -35,9 +34,7 @@ static Ret GetGlobalService<T, Ret>(IServiceProvider provider = null) where T :
3534
ret = provider.GetService(typeof(T)) as Ret;
3635
if (ret != null)
3736
return ret;
38-
if (PackageServiceProvider != null)
39-
return PackageServiceProvider.GetService(typeof(T)) as Ret;
40-
return Package.GetGlobalService(typeof(T)) as Ret;
37+
return PackageServiceProvider.GetService(typeof(T)) as Ret;
4138
}
4239

4340
public static IComponentModel ComponentModel
@@ -101,18 +98,6 @@ public static IVsSolution GetSolution(this IServiceProvider provider)
10198
return GetGlobalService<SVsSolution, IVsSolution>(provider);
10299
}
103100

104-
public static T GetExportedValue<T>(this IServiceProvider serviceProvider)
105-
{
106-
var ui = serviceProvider as IUIProvider;
107-
if (ui != null)
108-
return ui.GetService<T>();
109-
else
110-
{
111-
var componentModel = (IComponentModel)serviceProvider.GetService(typeof(SComponentModel));
112-
return componentModel.DefaultExportProvider.GetExportedValue<T>();
113-
}
114-
}
115-
116101
public static UriString GetRepoUrlFromSolution(IVsSolution solution)
117102
{
118103
string solutionDir, solutionFile, userFile;
@@ -125,11 +110,11 @@ public static UriString GetRepoUrlFromSolution(IVsSolution solution)
125110
return null;
126111
using (var repo = new Repository(repoPath))
127112
{
128-
return repo.GetUri();
113+
return GetUri(repo);
129114
}
130115
}
131116

132-
public static Repository GetRepoFromSolution(this IVsSolution solution)
117+
public static IRepository GetRepoFromSolution(this IVsSolution solution)
133118
{
134119
string solutionDir, solutionFile, userFile;
135120
if (!ErrorHandler.Succeeded(solution.GetSolutionInfo(out solutionDir, out solutionFile, out userFile)))
@@ -141,5 +126,10 @@ public static Repository GetRepoFromSolution(this IVsSolution solution)
141126
return null;
142127
return new Repository(repoPath);
143128
}
129+
static UriString GetUri(IRepository repo)
130+
{
131+
return UriString.ToUriString(GitService.GetUriFromRepository(repo)?.ToRepositoryUrl());
132+
}
133+
public static IGitService IGitService { get { return PackageServiceProvider.GetService<IGitService>(); } }
144134
}
145135
}

src/GitHub.Exports/Services/VSServices.cs

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public interface IVSServices
2121
string GetLocalClonePathFromGitProvider();
2222
void Clone(string cloneUrl, string clonePath, bool recurseSubmodules);
2323
string GetActiveRepoPath();
24-
LibGit2Sharp.Repository GetActiveRepo();
24+
LibGit2Sharp.IRepository GetActiveRepo();
2525
IEnumerable<ISimpleRepositoryModel> GetKnownRepositories();
2626
string SetDefaultProjectPath(string path);
2727

@@ -73,11 +73,12 @@ public void Clone(string cloneUrl, string clonePath, bool recurseSubmodules)
7373
gitExt.Clone(cloneUrl, clonePath, recurseSubmodules ? CloneOptions.RecurseSubmodule : CloneOptions.None);
7474
}
7575

76-
public LibGit2Sharp.Repository GetActiveRepo()
76+
public LibGit2Sharp.IRepository GetActiveRepo()
7777
{
7878
var gitExt = serviceProvider.GetService<IGitExt>();
79+
var gitService = serviceProvider.GetService<IGitService>();
7980
if (gitExt.ActiveRepositories.Count > 0)
80-
return gitExt.ActiveRepositories.First().GetRepoFromIGit();
81+
return gitService.GetRepo(gitExt.ActiveRepositories.First());
8182
return serviceProvider.GetSolution().GetRepoFromSolution();
8283
}
8384

@@ -117,16 +118,18 @@ static IEnumerable<ISimpleRepositoryModel> PokeTheRegistryForRepositoryList()
117118
{
118119
using (var subkey = key.OpenSubKey(x))
119120
{
120-
var path = subkey?.GetValue("Path") as string;
121-
if (path != null)
121+
try
122122
{
123-
var uri = GitHelpers.GetRepoFromPath(path)?.GetUri();
124-
var name = uri?.NameWithOwner;
125-
if (name != null)
126-
return new SimpleRepositoryModel(name, uri, path);
123+
var path = subkey?.GetValue("Path") as string;
124+
if (path != null)
125+
return new SimpleRepositoryModel(path);
127126
}
127+
catch (Exception ex)
128+
{
129+
VsOutputLogger.WriteLine(string.Format(CultureInfo.CurrentCulture, "Error loading the repository from the registry '{0}'", ex));
130+
}
131+
return null;
128132
}
129-
return null;
130133
})
131134
.Where(x => x != null)
132135
.ToList();

src/UnitTests/GitHub.App/Models/ComparisonTests.cs renamed to src/UnitTests/GitHub.App/Models/RepositoryModelTests.cs

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
using GitHub.Models;
22
using GitHub.Primitives;
3+
using GitHub.Services;
4+
using GitHub.VisualStudio;
5+
using LibGit2Sharp;
36
using NSubstitute;
4-
using System;
5-
using System.Collections.Generic;
6-
using System.Linq;
7-
using System.Text;
8-
using System.Threading.Tasks;
97
using UnitTests;
108
using Xunit;
119

12-
public class ComparisonTests
10+
public class RepositoryModelTests
1311
{
14-
public class RepositoryModelTests : TestBaseClass
12+
public class ComparisonTests : TestBaseClass
1513
{
1614
[Theory]
1715
[InlineData("a name", "https://github.com/github/VisualStudio", null, "a name", "https://github.com/github/VisualStudio", null)]
@@ -55,6 +53,37 @@ public void DifferentContentEqualsFalse(string name1, string url1, string name2,
5553
}
5654
}
5755

56+
public class PathConstructorTests : TempFileBaseClass
57+
{
58+
[Fact]
59+
public void NoRemoteUrl()
60+
{
61+
var provider = Substitutes.ServiceProvider;
62+
Services.PackageServiceProvider = provider;
63+
var gitservice = Substitutes.IGitService;
64+
provider.GetService(typeof(IGitService)).Returns(gitservice);
65+
var repo = Substitute.For<IRepository>();
66+
var path = Directory.CreateSubdirectory("repo-name");
67+
gitservice.GetUri(path.FullName).Returns((UriString)null);
68+
var model = new SimpleRepositoryModel(path.FullName);
69+
Assert.Equal("repo-name", model.Name);
70+
}
71+
72+
[Fact]
73+
public void WithRemoteUrl()
74+
{
75+
var provider = Substitutes.ServiceProvider;
76+
Services.PackageServiceProvider = provider;
77+
var gitservice = Substitutes.IGitService;
78+
provider.GetService(typeof(IGitService)).Returns(gitservice);
79+
var repo = Substitute.For<IRepository>();
80+
var path = Directory.CreateSubdirectory("repo-name");
81+
gitservice.GetUri(path.FullName).Returns(new UriString("https://github.com/user/repo-name"));
82+
var model = new SimpleRepositoryModel(path.FullName);
83+
Assert.Equal("user/repo-name", model.Name);
84+
}
85+
}
86+
5887
public class HostAddressTests : TestBaseClass
5988
{
6089
[Theory]

0 commit comments

Comments
 (0)