Add Unit Tests for AssemblyProbingFolder

--HG--
branch : dev
rename : src/Orchard/FileSystems/Dependencies/IAssenblyProbyFolder.cs => src/Orchard/FileSystems/Dependencies/IAssemblyProbingFolder.cs
This commit is contained in:
Renaud Paquay
2010-06-16 17:23:33 -07:00
parent ff34e0a6b7
commit d3c393cc41
15 changed files with 227 additions and 98 deletions

View File

@@ -0,0 +1,67 @@
using NUnit.Framework;
using Orchard.FileSystems.Dependencies;
using Orchard.Tests.Stubs;
namespace Orchard.Tests.FileSystems.Dependencies {
[TestFixture]
public class AssemblyProbingFolderTests {
[Test]
public void FolderShouldBeEmptyByDefault() {
var clock = new StubClock();
var appDataFolder = new StubAppDataFolder(clock);
var dependenciesFolder = new DefaultAssemblyProbingFolder(appDataFolder);
Assert.That(dependenciesFolder.AssemblyExists("foo"), Is.False);
}
[Test]
public void LoadAssemblyShouldNotThrowIfAssemblyNotFound() {
var clock = new StubClock();
var appDataFolder = new StubAppDataFolder(clock);
var dependenciesFolder = new DefaultAssemblyProbingFolder(appDataFolder);
Assert.That(dependenciesFolder.LoadAssembly("foo"), Is.Null);
}
[Test]
public void GetAssemblyDateTimeUtcShouldThrowIfAssemblyNotFound() {
var clock = new StubClock();
var appDataFolder = new StubAppDataFolder(clock);
var dependenciesFolder = new DefaultAssemblyProbingFolder(appDataFolder);
Assert.That(() => dependenciesFolder.GetAssemblyDateTimeUtc("foo"), Throws.Exception);
}
[Test]
public void DeleteAssemblyShouldNotThrowIfAssemblyNotFound() {
var clock = new StubClock();
var appDataFolder = new StubAppDataFolder(clock);
var dependenciesFolder = new DefaultAssemblyProbingFolder(appDataFolder);
Assert.DoesNotThrow(() => dependenciesFolder.DeleteAssembly("foo"));
}
[Test]
public void StoreAssemblyShouldCopyFile() {
var clock = new StubClock();
var appDataFolder = new StubAppDataFolder(clock);
var assembly = GetType().Assembly;
var name = assembly.GetName().Name;
{
var dependenciesFolder = new DefaultAssemblyProbingFolder(appDataFolder);
dependenciesFolder.StoreAssembly(name, assembly.Location);
}
{
var dependenciesFolder = new DefaultAssemblyProbingFolder(appDataFolder);
Assert.That(dependenciesFolder.AssemblyExists(name), Is.True);
Assert.That(dependenciesFolder.LoadAssembly(name), Is.SameAs(GetType().Assembly));
Assert.DoesNotThrow(() => dependenciesFolder.DeleteAssembly(name));
Assert.That(dependenciesFolder.LoadAssembly(name), Is.Null);
}
}
}
}

View File

@@ -29,8 +29,8 @@ namespace Orchard.Tests.FileSystems.Dependencies {
LoaderName = "test",
VirtualPath = "~/bin"
};
dependenciesFolder.StoreDescriptors(new [] { d });
dependenciesFolder.StoreDescriptors(new[] { d });
var e = dependenciesFolder.LoadDescriptors();
Assert.That(e, Has.Count.EqualTo(1));
Assert.That(e.First().Name, Is.EqualTo("name"));
@@ -113,7 +113,7 @@ namespace Orchard.Tests.FileSystems.Dependencies {
};
dependenciesFolder.StoreDescriptors(new[] { d1, d2 });
// Create a new instance over the same appDataFolder
var dependenciesFolder2 = new DefaultDependenciesFolder(new StubCacheManager(), appDataFolder);

View File

@@ -197,6 +197,7 @@
<Compile Include="Environment\OrchardStarterTests.cs" />
<Compile Include="Environment\ShellBuilders\DefaultShellContainerFactoryTests.cs" />
<Compile Include="Environment\ShellBuilders\DefaultShellContextFactoryTests.cs" />
<Compile Include="FileSystems\Dependencies\AssemblyProbingFolderTests.cs" />
<Compile Include="FileSystems\Dependencies\DependenciesFolderTests.cs" />
<Compile Include="Localization\CultureManagerTests.cs" />
<Compile Include="Mvc\Routes\ShellRouteTests.cs" />

View File

@@ -45,7 +45,7 @@ namespace Orchard.Tests.Stubs {
}
public void CreateFile(string path, string content) {
using(var stream = CreateFile(path)) {
using (var stream = CreateFile(path)) {
using (var writer = new StreamWriter(stream)) {
writer.Write(content);
}
@@ -68,8 +68,29 @@ namespace Orchard.Tests.Stubs {
return _fileSystem.OpenFile(path);
}
public void StoreFile(string sourceFileName, string destinationPath) {
using (var inputStream = File.OpenRead(sourceFileName)) {
using (var outputStream = _fileSystem.CreateFile(destinationPath)) {
byte[] buffer = new byte[1024];
for (; ; ) {
var count = inputStream.Read(buffer, 0, buffer.Length);
if (count == 0)
break;
outputStream.Write(buffer, 0, count);
}
}
}
}
public void DeleteFile(string path) {
throw new NotImplementedException();
_fileSystem.DeleteFile(path);
}
public DateTime GetFileLastWriteTimeUtc(string path) {
var entry = _fileSystem.GetFileEntry(path);
if (entry == null)
throw new ArgumentException();
return entry.LastWriteTimeUtc;
}
public void CreateDirectory(string path) {
@@ -88,7 +109,7 @@ namespace Orchard.Tests.Stubs {
var entry = _fileSystem.GetFileEntry(path);
if (entry == null)
throw new InvalidOperationException();
return entry.LastWriteTime;
return entry.LastWriteTimeUtc;
}
}
}

View File

@@ -68,12 +68,12 @@ namespace Orchard.Tests.Stubs {
public FileEntry(IClock clock) {
_clock = clock;
LastWriteTime = _clock.UtcNow;
LastWriteTimeUtc = _clock.UtcNow;
Content = new List<byte>();
}
public List<byte> Content { get; private set; }
public DateTime LastWriteTime { get; set; }
public DateTime LastWriteTimeUtc { get; set; }
}
public class Token : IVolatileToken {
@@ -178,7 +178,7 @@ namespace Orchard.Tests.Stubs {
var wrapper = new ArrayWrapper<byte>(buffer, offset, count);
_entry.Content.AddRange(wrapper);
_entry.LastWriteTime = _clock.UtcNow;
_entry.LastWriteTimeUtc = _clock.UtcNow;
if (_token != null)
_token.OnChange();
_position += count;
@@ -368,5 +368,20 @@ namespace Orchard.Tests.Stubs {
return new FileEntryReadStream(entry, _clock);
}
public void DeleteFile(string path) {
var directoryName = Path.GetDirectoryName(path);
var fileName = Path.GetFileName(path);
var directory = GetDirectoryEntry(directoryName);
if (directory == null)
return;
var entry = directory.GetEntry(fileName);
if (entry == null)
return;
directory.Entries.Remove(entry);
}
}
}

View File

@@ -42,7 +42,7 @@ namespace Orchard.Environment.Extensions {
var deletedDependencies = existingDependencies.Where(e => !extensions.Any(e2 => e2.Name == e.Name)).ToList();
var newExtensions = extensions.Except(sameExtensions).ToList();
var ctx = new ExtensionLoadingContext { DependenciesFolder = _dependenciesFolder };
var ctx = new ExtensionLoadingContext();
// Notify all loaders about extensions removed from the web site
foreach (var dependency in deletedDependencies) {
@@ -123,19 +123,12 @@ namespace Orchard.Environment.Extensions {
private void ProcessContextCommands(ExtensionLoadingContext ctx) {
Logger.Information("Executing list of operations needed for loading extensions...");
foreach (var fileName in ctx.FilesToDelete) {
Logger.Information("Deleting file \"{0}\"", fileName);
File.Delete(fileName);
foreach (var action in ctx.DeleteActions) {
action();
}
foreach (var entry in ctx.FilesToCopy) {
Logger.Information("Copying file from \"{0}\" to \"{1}\"", entry.Key, entry.Value);
MakeDestinationFileNameAvailable(entry.Value);
File.Copy(entry.Key, entry.Value);
}
foreach (var entry in ctx.FilesToRename) {
Logger.Information("Moving file from \"{0}\" to \"{1}\"", entry.Key, entry.Value);
MakeDestinationFileNameAvailable(entry.Value);
File.Move(entry.Key, entry.Value);
foreach (var action in ctx.CopyActions) {
action();
}
if (ctx.RestartAppDomain) {
@@ -149,39 +142,6 @@ namespace Orchard.Environment.Extensions {
}
}
private void MakeDestinationFileNameAvailable(string destinationFileName) {
// Try deleting the destination first
try {
File.Delete(destinationFileName);
} catch {
// We land here if the file is in use, for example. Let's move on.
}
// If destination doesn't exist, we are good
if (!File.Exists(destinationFileName))
return;
// Try renaming destination to a unique filename
const string extension = "deleted";
for (int i = 0; i < 100; i++) {
var newExtension = (i == 0 ? extension : string.Format("{0}{1}", extension, i));
var newFileName = Path.ChangeExtension(destinationFileName, newExtension);
try {
File.Delete(newFileName);
File.Move(destinationFileName, newFileName);
// If successful, we are done...
return;
}
catch (Exception) {
// We need to try with another extension
}
}
// Everything failed, throw an exception
throw new OrchardException(T("Unable to make room for file {0} in dependencies folder: too many conflicts.", destinationFileName).Text);
}
public void MonitorExtensions(Action<IVolatileToken> monitor) {
var extensions = _extensionManager.AvailableExtensions().Where(d => d.ExtensionType == "Module").ToList();
foreach (var extension in extensions) {

View File

@@ -1,18 +1,16 @@
using System.Collections.Generic;
using Orchard.FileSystems.Dependencies;
using System;
using System.Collections.Generic;
namespace Orchard.Environment.Extensions {
public class ExtensionLoadingContext {
public ExtensionLoadingContext() {
FilesToDelete = new HashSet<string>();
FilesToCopy = new Dictionary<string, string>();
FilesToRename = new Dictionary<string, string>();
DeleteActions = new List<Action>();
CopyActions = new List<Action>();
}
public IDependenciesFolder DependenciesFolder { get; set; }
public HashSet<string> FilesToDelete { get; set; }
public Dictionary<string, string> FilesToCopy { get; set; }
public Dictionary<string, string> FilesToRename { get; set; }
public IList<Action> DeleteActions { get; set; }
public IList<Action> CopyActions { get; set; }
public bool RestartAppDomain { get; set; }
public bool ResetSiteCompilation { get; set; }
}

View File

@@ -34,9 +34,8 @@ namespace Orchard.Environment.Extensions.Loaders {
}
public override void ExtensionRemoved(ExtensionLoadingContext ctx, DependencyDescriptor dependency) {
var assemblyFileName = _assemblyProbingFolder.GetAssemblyPhysicalFileName(dependency.Name);
if (File.Exists(assemblyFileName)) {
ctx.FilesToDelete.Add(assemblyFileName);
if (_assemblyProbingFolder.AssemblyExists(dependency.Name)) {
ctx.DeleteActions.Add(() => _assemblyProbingFolder.DeleteAssembly(dependency.Name));
// We need to restart the appDomain if the assembly is loaded
if (IsAssemblyLoaded(dependency.Name)) {
@@ -48,9 +47,14 @@ namespace Orchard.Environment.Extensions.Loaders {
public override void ExtensionActivated(ExtensionLoadingContext ctx, bool isNewExtension, ExtensionDescriptor extension) {
string sourceFileName = _virtualPathProvider.MapPath(GetAssemblyPath(extension));
string destinationFileName = _assemblyProbingFolder.GetAssemblyPhysicalFileName(extension.Name);
if (FileIsNewer(sourceFileName, destinationFileName)) {
ctx.FilesToCopy.Add(sourceFileName, destinationFileName);
// Copy the assembly if it doesn't exist or if it is older than the source file.
bool copyAssembly =
!_assemblyProbingFolder.AssemblyExists(extension.Name) ||
File.GetLastWriteTimeUtc(sourceFileName) > _assemblyProbingFolder.GetAssemblyDateTimeUtc(extension.Name);
if (copyAssembly) {
ctx.CopyActions.Add(() => _assemblyProbingFolder.StoreAssembly(extension.Name, sourceFileName));
// We need to restart the appDomain if the assembly is loaded
if (IsAssemblyLoaded(extension.Name)) {
Logger.Information("Extension activated: Setting AppDomain for restart because assembly {0} is loaded", extension.Name);
@@ -60,9 +64,9 @@ namespace Orchard.Environment.Extensions.Loaders {
}
public override void ExtensionDeactivated(ExtensionLoadingContext ctx, bool isNewExtension, ExtensionDescriptor extension) {
var assemblyFileName = _assemblyProbingFolder.GetAssemblyPhysicalFileName(extension.Name);
if (File.Exists(assemblyFileName)) {
ctx.FilesToDelete.Add(assemblyFileName);
if (_assemblyProbingFolder.AssemblyExists(extension.Name)) {
ctx.DeleteActions.Add(() => _assemblyProbingFolder.DeleteAssembly(extension.Name));
// We need to restart the appDomain if the assembly is loaded
if (IsAssemblyLoaded(extension.Name)) {
Logger.Information("Extension deactivated: Setting AppDomain for restart because assembly {0} is loaded", extension.Name);

View File

@@ -27,9 +27,9 @@ namespace Orchard.Environment.Extensions.Loaders {
}
public override void ExtensionRemoved(ExtensionLoadingContext ctx, DependencyDescriptor dependency) {
var assemblyFileName = _assemblyProbingFolder.GetAssemblyPhysicalFileName(dependency.Name);
if (File.Exists(assemblyFileName)) {
ctx.FilesToDelete.Add(assemblyFileName);
if (_assemblyProbingFolder.AssemblyExists(dependency.Name)) {
ctx.DeleteActions.Add(() => _assemblyProbingFolder.DeleteAssembly(dependency.Name));
// We need to restart the appDomain if the assembly is loaded
if (IsAssemblyLoaded(dependency.Name)) {
Logger.Information("Extension removed: Setting AppDomain for restart because assembly {0} is loaded", dependency.Name);
@@ -39,9 +39,9 @@ namespace Orchard.Environment.Extensions.Loaders {
}
public override void ExtensionDeactivated(ExtensionLoadingContext ctx, bool isNewExtension, ExtensionDescriptor extension) {
var assemblyFileName = _assemblyProbingFolder.GetAssemblyPhysicalFileName(extension.Name);
if (File.Exists(assemblyFileName)) {
ctx.FilesToDelete.Add(assemblyFileName);
if (_assemblyProbingFolder.AssemblyExists(extension.Name)) {
ctx.DeleteActions.Add(() => _assemblyProbingFolder.DeleteAssembly(extension.Name));
// We need to restart the appDomain if the assembly is loaded
if (IsAssemblyLoaded(extension.Name)) {
Logger.Information("Extension deactivated: Setting AppDomain for restart because assembly {0} is loaded", extension.Name);
@@ -51,7 +51,7 @@ namespace Orchard.Environment.Extensions.Loaders {
}
public override ExtensionProbeEntry Probe(ExtensionDescriptor descriptor) {
if (!_assemblyProbingFolder.HasAssembly(descriptor.Name))
if (!_assemblyProbingFolder.AssemblyExists(descriptor.Name))
return null;
var desc = _dependenciesFolder.GetDescriptor(descriptor.Name);

View File

@@ -25,7 +25,7 @@ namespace Orchard.Environment.Extensions.Loaders {
public override void ExtensionDeactivated(ExtensionLoadingContext ctx, bool isNewExtension, ExtensionDescriptor extension) {
var assemblyPath = _virtualPathProvider.Combine("~/bin", extension.Name + ".dll");
if (_virtualPathProvider.FileExists(assemblyPath)) {
ctx.FilesToDelete.Add(_virtualPathProvider.MapPath(assemblyPath));
ctx.DeleteActions.Add(() => File.Delete(_virtualPathProvider.MapPath(assemblyPath)));
}
}

View File

@@ -1,9 +1,12 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Web.Hosting;
using Orchard.Caching;
using Orchard.FileSystems.VirtualPath;
using Orchard.Localization;
using Orchard.Logging;
namespace Orchard.FileSystems.AppData {
public class AppDataFolder : IAppDataFolder {
@@ -13,8 +16,13 @@ namespace Orchard.FileSystems.AppData {
public AppDataFolder(IAppDataFolderRoot root, IVirtualPathMonitor virtualPathMonitor) {
_root = root;
_virtualPathMonitor = virtualPathMonitor;
T = NullLocalizer.Instance;
Logger = NullLogger.Instance;
}
public Localizer T { get; set; }
public ILogger Logger { get; set; }
public string RootFolder {
get {
return _root.RootFolder;
@@ -27,6 +35,40 @@ namespace Orchard.FileSystems.AppData {
}
}
private void MakeDestinationFileNameAvailable(string destinationFileName) {
// Try deleting the destination first
try {
File.Delete(destinationFileName);
}
catch {
// We land here if the file is in use, for example. Let's move on.
}
// If destination doesn't exist, we are good
if (!File.Exists(destinationFileName))
return;
// Try renaming destination to a unique filename
const string extension = "deleted";
for (int i = 0; i < 100; i++) {
var newExtension = (i == 0 ? extension : string.Format("{0}{1}", extension, i));
var newFileName = Path.ChangeExtension(destinationFileName, newExtension);
try {
File.Delete(newFileName);
File.Move(destinationFileName, newFileName);
// If successful, we are done...
return;
}
catch (Exception) {
// We need to try with another extension
}
}
// Everything failed, throw an exception
throw new OrchardException(T("Unable to make room for file {0} in \"App_Data\" folder: too many conflicts.", destinationFileName).Text);
}
/// <summary>
/// Combine a set of virtual paths relative to "~/App_Data" into an absolute physical path
/// starting with "_basePath".
@@ -72,10 +114,24 @@ namespace Orchard.FileSystems.AppData {
return File.OpenRead(CombineToPhysicalPath(path));
}
public void StoreFile(string sourceFileName, string destinationPath) {
Logger.Information("Storing file \"{0}\" as \"{1}\" in App_Data folder", sourceFileName, destinationPath);
var destinationFileName = CombineToPhysicalPath(destinationPath);
MakeDestinationFileNameAvailable(destinationFileName);
File.Copy(sourceFileName, destinationFileName);
}
public void DeleteFile(string path) {
Logger.Information("Deleting file \"{0}\" from App_Data folder", path);
File.Delete(CombineToPhysicalPath(path));
}
public DateTime GetFileLastWriteTimeUtc(string path) {
return File.GetLastWriteTimeUtc(CombineToPhysicalPath(path));
}
public bool FileExists(string path) {
return File.Exists(CombineToPhysicalPath(path));
}

View File

@@ -13,17 +13,18 @@ namespace Orchard.FileSystems.AppData {
IEnumerable<string> ListFiles(string path);
IEnumerable<string> ListDirectories(string path);
bool FileExists(string path);
string Combine(params string[] paths);
bool FileExists(string path);
void CreateFile(string path, string content);
Stream CreateFile(string path);
string ReadFile(string path);
Stream OpenFile(string path);
void StoreFile(string sourceFileName, string destinationPath);
void DeleteFile(string path);
DateTime GetFileLastWriteTimeUtc(string path);
void CreateDirectory(string path);
IVolatileToken WhenPathChanges(string path);

View File

@@ -1,5 +1,4 @@
using System;
using System.IO;
using System.Reflection;
using Orchard.FileSystems.AppData;
@@ -12,14 +11,14 @@ namespace Orchard.FileSystems.Dependencies {
_appDataFolder = appDataFolder;
}
public bool HasAssembly(string moduleName) {
public bool AssemblyExists(string moduleName) {
var path = PrecompiledAssemblyPath(moduleName);
return _appDataFolder.FileExists(path);
}
public DateTime GetAssemblyDateTimeUtc(string moduleName) {
var path = PrecompiledAssemblyPath(moduleName);
return File.GetLastWriteTimeUtc(_appDataFolder.MapPath(path));
return _appDataFolder.GetFileLastWriteTimeUtc(path);
}
public Assembly LoadAssembly(string moduleName) {
@@ -30,8 +29,14 @@ namespace Orchard.FileSystems.Dependencies {
return Assembly.Load(moduleName);
}
public string GetAssemblyPhysicalFileName(string moduleName) {
return _appDataFolder.MapPath(PrecompiledAssemblyPath(moduleName));
public void DeleteAssembly(string moduleName) {
var path = PrecompiledAssemblyPath(moduleName);
_appDataFolder.DeleteFile(path);
}
public void StoreAssembly(string moduleName, string fileName) {
var path = PrecompiledAssemblyPath(moduleName);
_appDataFolder.StoreFile(fileName, path);
}
private string PrecompiledAssemblyPath(string moduleName) {

View File

@@ -13,7 +13,7 @@ namespace Orchard.FileSystems.Dependencies {
/// Return "true" if the assembly corresponding to "moduleName" is
/// present in the folder.
/// </summary>
bool HasAssembly(string moduleName);
bool AssemblyExists(string moduleName);
/// <summary>
/// Return the last modification date of the assembly corresponding
@@ -29,12 +29,13 @@ namespace Orchard.FileSystems.Dependencies {
Assembly LoadAssembly(string moduleName);
/// <summary>
/// Return the physical location where to store the assembly
/// corresponding to "moduleName". This will return a correct path
/// even if the assembly is not currently stored in that location.
/// This method can be used to answer the question "Where would the assembly
/// for module "moduleName" be stored if it exsisted?"
/// Ensure the assembly corresponding to "moduleName" is removed from the folder
/// </summary>
string GetAssemblyPhysicalFileName(string moduleName);
void DeleteAssembly(string moduleName);
/// <summary>
/// Store an assembly corresponding to "moduleName" from the given fileName
/// </summary>
void StoreAssembly(string moduleName, string fileName);
}
}

View File

@@ -360,7 +360,7 @@
<Compile Include="FileSystems\AppData\IAppDataFolderRoot.cs" />
<Compile Include="FileSystems\Dependencies\DefaultAssemblyProbingFolder.cs" />
<Compile Include="FileSystems\Dependencies\DefaultDependenciesFolder.cs" />
<Compile Include="FileSystems\Dependencies\IAssenblyProbyFolder.cs" />
<Compile Include="FileSystems\Dependencies\IAssemblyProbingFolder.cs" />
<Compile Include="FileSystems\VirtualPath\DefaultVirtualPathMonitor.cs" />
<Compile Include="FileSystems\VirtualPath\DefaultVirtualPathProvider.cs" />
<Compile Include="FileSystems\VirtualPath\ICustomVirtualPathProvider.cs" />