diff --git a/src/Orchard.Tests/Environment/DefaultCompositionStrategyTests.cs b/src/Orchard.Tests/Environment/DefaultCompositionStrategyTests.cs index 468aa3932..822de39e9 100644 --- a/src/Orchard.Tests/Environment/DefaultCompositionStrategyTests.cs +++ b/src/Orchard.Tests/Environment/DefaultCompositionStrategyTests.cs @@ -7,9 +7,7 @@ using Autofac; using Autofac.Core; using Moq; using NUnit.Framework; -using Orchard.ContentManagement; using Orchard.ContentManagement.Records; -using Orchard.Environment; using Orchard.Environment.Configuration; using Orchard.Environment.Extensions; using Orchard.Environment.Extensions.Models; @@ -27,7 +25,6 @@ namespace Orchard.Tests.Environment { private IEnumerable _extensionDescriptors; private IDictionary> _featureTypes; - [SetUp] public void Init() { var builder = new ContainerBuilder(); diff --git a/src/Orchard.Tests/Environment/Loaders/DynamicExtensionLoaderTests.cs b/src/Orchard.Tests/Environment/Loaders/DynamicExtensionLoaderTests.cs new file mode 100644 index 000000000..96db8bf53 --- /dev/null +++ b/src/Orchard.Tests/Environment/Loaders/DynamicExtensionLoaderTests.cs @@ -0,0 +1,206 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Autofac; +using Moq; +using NUnit.Framework; +using Orchard.Caching; +using Orchard.Environment; +using Orchard.Environment.Extensions.Compilers; +using Orchard.Environment.Extensions.Loaders; +using Orchard.FileSystems.AppData; +using Orchard.FileSystems.Dependencies; +using Orchard.FileSystems.VirtualPath; +using Orchard.Services; +using Orchard.Tests.Stubs; + +namespace Orchard.Tests.Environment.Loaders { + [TestFixture] + public class DynamicExtensionLoaderTests { + private IContainer _container; + private Mock _mockedStubProjectFileParser; + private Mock _mockedDependenciesFolder; + + [SetUp] + public void Init() { + var builder = new ContainerBuilder(); + builder.RegisterType().As(); + + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + + _mockedStubProjectFileParser = new Mock(); + builder.RegisterInstance(_mockedStubProjectFileParser.Object).As(); + builder.RegisterInstance(new StubFileSystem(new StubClock())).As(); + + _mockedDependenciesFolder = new Mock(); + builder.RegisterInstance(_mockedDependenciesFolder.Object).As(); + + _container = builder.Build(); + } + + [Test] + public void GetDependenciesContainsNoDuplicatesTest() { + const string fileName1 = "a.cs"; + const string fileName2 = "b.cs"; + + DynamicExtensionLoaderAccessor extensionLoader = _container.Resolve(); + StubFileSystem stubFileSystem = _container.Resolve(); + StubFileSystem.FileEntry fileEntry = stubFileSystem.CreateFileEntry("orchard.a.csjproj"); + + // Create duplicate source files (invalid situation in reality but easy enough to test) + _mockedStubProjectFileParser.Setup(stubProjectFileParser => stubProjectFileParser.Parse(It.IsAny())).Returns( + new ProjectFileDescriptor { SourceFilenames = new[] { fileName1, fileName2, fileName1 } }); // duplicate file + + IEnumerable dependencies = extensionLoader.GetDependenciesAccessor(fileEntry.Name); + + Assert.That(dependencies.Count(), Is.EqualTo(3), "3 results should mean no duplicates"); + Assert.That(dependencies.FirstOrDefault(dep => dep.Equals(fileEntry.Name)), Is.Not.Null); + Assert.That(dependencies.FirstOrDefault(dep => dep.Equals(fileName1)), Is.Not.Null); + Assert.That(dependencies.FirstOrDefault(dep => dep.Equals(fileName2)), Is.Not.Null); + } + + [Test] + public void GetDependenciesContainsNoDuplicatesEvenIfMultipleProjectsTest() { + const string fileName1 = "a.cs"; + const string fileName2 = "b.cs"; + + const string commonFileName = "c.cs"; + + DynamicExtensionLoaderAccessor extensionLoader = _container.Resolve(); + StubFileSystem stubFileSystem = _container.Resolve(); + StubFileSystem.FileEntry fileEntry = stubFileSystem.CreateFileEntry("orchard.a.csjproj"); + StubFileSystem.FileEntry fileEntry2 = stubFileSystem.CreateFileEntry("orchard.b.csjproj"); + StubFileSystem.FileEntry fileEntry3 = stubFileSystem.CreateFileEntry("orchard.c.csjproj"); + + // Project a reference b and c which share a file in common + + // Result for project a + _mockedStubProjectFileParser.Setup(stubProjectFileParser => stubProjectFileParser.Parse(It.Is(stream => ((StubFileSystem.FileEntryReadStream)stream).FileEntry == fileEntry))) + .Returns( + new ProjectFileDescriptor { + SourceFilenames = new[] { fileName1, fileName2 }, + References = new[] { + new ReferenceDescriptor { + ReferenceType = ReferenceType.Project, + SimpleName = Path.GetFileNameWithoutExtension(fileEntry2.Name), + FullName = Path.GetFileNameWithoutExtension(fileEntry2.Name), + Path = fileEntry2.Name + }, + new ReferenceDescriptor { + ReferenceType = ReferenceType.Project, + SimpleName = Path.GetFileNameWithoutExtension(fileEntry2.Name), + FullName = Path.GetFileNameWithoutExtension(fileEntry2.Name), + Path = fileEntry3.Name + } + } + }); + + // Result for project b and c + _mockedStubProjectFileParser.Setup(stubProjectFileParser => stubProjectFileParser.Parse(It.Is(stream => + ((StubFileSystem.FileEntryReadStream)stream).FileEntry == fileEntry2 || ((StubFileSystem.FileEntryReadStream)stream).FileEntry == fileEntry3))) + .Returns( + new ProjectFileDescriptor { + SourceFilenames = new[] { commonFileName } + }); + + IEnumerable dependencies = extensionLoader.GetDependenciesAccessor(fileEntry.Name); + + Assert.That(dependencies.Count(), Is.EqualTo(6), "6 results should mean no duplicates"); + + // Project files + Assert.That(dependencies.FirstOrDefault(dep => dep.Equals(fileEntry.Name)), Is.Not.Null); + Assert.That(dependencies.FirstOrDefault(dep => dep.Equals(fileEntry2.Name)), Is.Not.Null); + Assert.That(dependencies.FirstOrDefault(dep => dep.Equals(fileEntry3.Name)), Is.Not.Null); + + // Individual source files + Assert.That(dependencies.FirstOrDefault(dep => dep.Equals(fileName1)), Is.Not.Null); + Assert.That(dependencies.FirstOrDefault(dep => dep.Equals(fileName2)), Is.Not.Null); + Assert.That(dependencies.FirstOrDefault(dep => dep.Equals(commonFileName)), Is.Not.Null); + } + + [Test] + public void GetDependenciesContainsBinForReferencedProjectsTest() { + const string fileName1 = "a.cs"; + const string fileName2 = "b.cs"; + + const string commonFileName = "c.cs"; + + DynamicExtensionLoaderAccessor extensionLoader = _container.Resolve(); + StubFileSystem stubFileSystem = _container.Resolve(); + StubFileSystem.FileEntry fileEntry = stubFileSystem.CreateFileEntry("orchard.a.csjproj"); + StubFileSystem.FileEntry fileEntry2 = stubFileSystem.CreateFileEntry("orchard.b.csjproj"); + + StubFileSystem.DirectoryEntry directoryEntry = stubFileSystem.CreateDirectoryEntry("bin"); + StubFileSystem.FileEntry fileEntry3 = directoryEntry.CreateFile("orchard.b.dll"); + + // Project a reference b and c which share a file in common + + // Result for project a + _mockedStubProjectFileParser.Setup(stubProjectFileParser => stubProjectFileParser.Parse(It.Is(stream => ((StubFileSystem.FileEntryReadStream)stream).FileEntry == fileEntry))) + .Returns( + new ProjectFileDescriptor { + SourceFilenames = new[] { fileName1, fileName2 }, + References = new[] { + new ReferenceDescriptor { + ReferenceType = ReferenceType.Project, + SimpleName = Path.GetFileNameWithoutExtension(fileEntry2.Name), + FullName = Path.GetFileNameWithoutExtension(fileEntry2.Name), + Path = fileEntry2.Name + } + } + }); + + // Result for project b and c + _mockedStubProjectFileParser.Setup(stubProjectFileParser => stubProjectFileParser.Parse(It.Is(stream => + ((StubFileSystem.FileEntryReadStream)stream).FileEntry == fileEntry2))) + .Returns( + new ProjectFileDescriptor { + SourceFilenames = new[] { commonFileName } + }); + + _mockedDependenciesFolder.Setup(dependenciesFolder => dependenciesFolder.GetDescriptor(It.Is(moduleName => moduleName == Path.GetDirectoryName(fileEntry2.Name)))) + .Returns( + new DependencyDescriptor { + VirtualPath = Path.Combine(directoryEntry.Name, fileEntry3.Name) + }); + + IEnumerable dependencies = extensionLoader.GetDependenciesAccessor(fileEntry.Name); + + Assert.That(dependencies.Count(), Is.EqualTo(6), "6 results should mean no duplicates"); + + // Project files + Assert.That(dependencies.FirstOrDefault(dep => dep.Equals(fileEntry.Name)), Is.Not.Null); + Assert.That(dependencies.FirstOrDefault(dep => dep.Equals(fileEntry2.Name)), Is.Not.Null); + Assert.That(dependencies.FirstOrDefault(dep => dep.Equals(Path.Combine(directoryEntry.Name, fileEntry3.Name))), Is.Not.Null); + + // Individual source files + Assert.That(dependencies.FirstOrDefault(dep => dep.Equals(fileName1)), Is.Not.Null); + Assert.That(dependencies.FirstOrDefault(dep => dep.Equals(fileName2)), Is.Not.Null); + Assert.That(dependencies.FirstOrDefault(dep => dep.Equals(commonFileName)), Is.Not.Null); + } + + internal class DynamicExtensionLoaderAccessor : DynamicExtensionLoader { + public DynamicExtensionLoaderAccessor( + IBuildManager buildManager, + IVirtualPathProvider virtualPathProvider, + IVirtualPathMonitor virtualPathMonitor, + IHostEnvironment hostEnvironment, + IAssemblyProbingFolder assemblyProbingFolder, + IDependenciesFolder dependenciesFolder, + IProjectFileParser projectFileParser) + : base(buildManager, virtualPathProvider, virtualPathMonitor, hostEnvironment, assemblyProbingFolder, dependenciesFolder, projectFileParser) {} + + public IEnumerable GetDependenciesAccessor(string projectPath) { + return GetDependencies(projectPath); + } + } + } +} diff --git a/src/Orchard.Tests/Orchard.Framework.Tests.csproj b/src/Orchard.Tests/Orchard.Framework.Tests.csproj index 57f4a1b23..cc6d269b4 100644 --- a/src/Orchard.Tests/Orchard.Framework.Tests.csproj +++ b/src/Orchard.Tests/Orchard.Framework.Tests.csproj @@ -235,6 +235,7 @@ + diff --git a/src/Orchard.Tests/Stubs/StubFileSystem.cs b/src/Orchard.Tests/Stubs/StubFileSystem.cs index 7234b24e0..b6bda00f8 100644 --- a/src/Orchard.Tests/Stubs/StubFileSystem.cs +++ b/src/Orchard.Tests/Stubs/StubFileSystem.cs @@ -228,6 +228,8 @@ namespace Orchard.Tests.Stubs { _clock = clock; } + public FileEntry FileEntry { get { return _entry; } } + public override void Flush() { throw new NotImplementedException(); } diff --git a/src/Orchard/Environment/Extensions/Loaders/DynamicExtensionLoader.cs b/src/Orchard/Environment/Extensions/Loaders/DynamicExtensionLoader.cs index 64dcdebc0..28ecf38a8 100644 --- a/src/Orchard/Environment/Extensions/Loaders/DynamicExtensionLoader.cs +++ b/src/Orchard/Environment/Extensions/Loaders/DynamicExtensionLoader.cs @@ -18,6 +18,7 @@ namespace Orchard.Environment.Extensions.Loaders { private readonly IVirtualPathMonitor _virtualPathMonitor; private readonly IHostEnvironment _hostEnvironment; private readonly IAssemblyProbingFolder _assemblyProbingFolder; + private readonly IDependenciesFolder _dependenciesFolder; private readonly IProjectFileParser _projectFileParser; private readonly ReloadWorkaround _reloadWorkaround = new ReloadWorkaround(); @@ -37,6 +38,7 @@ namespace Orchard.Environment.Extensions.Loaders { _hostEnvironment = hostEnvironment; _assemblyProbingFolder = assemblyProbingFolder; _projectFileParser = projectFileParser; + _dependenciesFolder = dependenciesFolder; Logger = NullLogger.Instance; } @@ -176,34 +178,48 @@ namespace Orchard.Environment.Extensions.Loaders { }; } - private IEnumerable GetDependencies(string projectPath) { - List dependencies = new[] { projectPath }.ToList(); + protected IEnumerable GetDependencies(string projectPath) { + HashSet dependencies = new HashSet { projectPath }; - var basePath = _virtualPathProvider.GetDirectoryName(projectPath); + AddDependencies(projectPath, dependencies); + + return dependencies; + } + + private void AddDependencies(string projectPath, HashSet currentSet) { + string basePath = _virtualPathProvider.GetDirectoryName(projectPath); using (var stream = _virtualPathProvider.OpenFile(projectPath)) { - var projectFile = _projectFileParser.Parse(stream); + ProjectFileDescriptor projectFile = _projectFileParser.Parse(stream); // Add source files - dependencies.AddRange(projectFile.SourceFilenames.Select(f => _virtualPathProvider.Combine(basePath, f))); + currentSet.UnionWith(projectFile.SourceFilenames.Select(f => _virtualPathProvider.Combine(basePath, f))); - // Add Project and Library References - foreach (ReferenceDescriptor referenceDescriptor in projectFile.References.Where(reference => !string.IsNullOrEmpty(reference.Path))) { - string path = referenceDescriptor.ReferenceType == ReferenceType.Library - ? _virtualPathProvider.GetProjectReferenceVirtualPath(projectPath, referenceDescriptor.SimpleName, referenceDescriptor.Path) - : _virtualPathProvider.Combine(basePath, referenceDescriptor.Path); + // Add Project and Library references + if (projectFile.References != null) { + foreach (ReferenceDescriptor referenceDescriptor in projectFile.References.Where(reference => !string.IsNullOrEmpty(reference.Path))) { + string path = referenceDescriptor.ReferenceType == ReferenceType.Library + ? _virtualPathProvider.GetProjectReferenceVirtualPath(projectPath, referenceDescriptor.SimpleName, referenceDescriptor.Path) + : _virtualPathProvider.Combine(basePath, referenceDescriptor.Path); - if (_virtualPathProvider.FileExists(path)) { - dependencies.Add(path); + // Attempt to reference the project / library file + if (!currentSet.Contains(path) && _virtualPathProvider.FileExists(path)) { + currentSet.Add(path); - if (referenceDescriptor.ReferenceType == ReferenceType.Project) { - dependencies.AddRange(GetDependencies(path)); + // In case of project, also reference the source files + if (referenceDescriptor.ReferenceType == ReferenceType.Project) { + AddDependencies(path, currentSet); + + // Try to also reference any pre-built DLL + DependencyDescriptor dependencyDescriptor = _dependenciesFolder.GetDescriptor(_virtualPathProvider.GetDirectoryName(referenceDescriptor.Path)); + if (dependencyDescriptor != null && _virtualPathProvider.FileExists(dependencyDescriptor.VirtualPath)) { + currentSet.Add(dependencyDescriptor.VirtualPath); + } + } } } } } - - return dependencies; } private string GetProjectPath(ExtensionDescriptor descriptor) { @@ -226,7 +242,7 @@ namespace Orchard.Environment.Extensions.Loaders { /// The purpose of this class is to keep track of all .csproj files monitored until /// an AppDomain restart. /// - class ReloadWorkaround { + internal class ReloadWorkaround { private readonly List _tokens = new List(); public void Monitor(IVolatileToken whenProjectFileChanges) {