diff --git a/FileSystem.sln b/FileSystem.sln index 5c8aa746..288daa0c 100644 --- a/FileSystem.sln +++ b/FileSystem.sln @@ -1,7 +1,6 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.22823.1 +VisualStudioVersion = 14.0.23107.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A1477614-E825-4204-A684-385004B63AEB}" EndProject @@ -28,6 +27,10 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.FileProvid EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.FileProviders.Sources", "src\Microsoft.AspNet.FileProviders.Sources\Microsoft.AspNet.FileProviders.Sources.xproj", "{92C2C85C-D1A5-44BD-BE23-238E08471B4D}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.FileProviders.Composite", "src\Microsoft.AspNet.FileProviders.Composite\Microsoft.AspNet.FileProviders.Composite.xproj", "{CAAF52EF-F91B-474D-AC56-FE9D96FF8254}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.FileProviders.Composite.Tests", "test\Microsoft.AspNet.FileProviders.Composite.Tests\Microsoft.AspNet.FileProviders.Composite.Tests.xproj", "{C2EA9BD0-C986-4B60-9E45-5EA51E1EA494}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -132,6 +135,30 @@ Global {92C2C85C-D1A5-44BD-BE23-238E08471B4D}.Release|Mixed Platforms.Build.0 = Release|Any CPU {92C2C85C-D1A5-44BD-BE23-238E08471B4D}.Release|x86.ActiveCfg = Release|Any CPU {92C2C85C-D1A5-44BD-BE23-238E08471B4D}.Release|x86.Build.0 = Release|Any CPU + {CAAF52EF-F91B-474D-AC56-FE9D96FF8254}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CAAF52EF-F91B-474D-AC56-FE9D96FF8254}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CAAF52EF-F91B-474D-AC56-FE9D96FF8254}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {CAAF52EF-F91B-474D-AC56-FE9D96FF8254}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {CAAF52EF-F91B-474D-AC56-FE9D96FF8254}.Debug|x86.ActiveCfg = Debug|Any CPU + {CAAF52EF-F91B-474D-AC56-FE9D96FF8254}.Debug|x86.Build.0 = Debug|Any CPU + {CAAF52EF-F91B-474D-AC56-FE9D96FF8254}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CAAF52EF-F91B-474D-AC56-FE9D96FF8254}.Release|Any CPU.Build.0 = Release|Any CPU + {CAAF52EF-F91B-474D-AC56-FE9D96FF8254}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {CAAF52EF-F91B-474D-AC56-FE9D96FF8254}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {CAAF52EF-F91B-474D-AC56-FE9D96FF8254}.Release|x86.ActiveCfg = Release|Any CPU + {CAAF52EF-F91B-474D-AC56-FE9D96FF8254}.Release|x86.Build.0 = Release|Any CPU + {C2EA9BD0-C986-4B60-9E45-5EA51E1EA494}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C2EA9BD0-C986-4B60-9E45-5EA51E1EA494}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C2EA9BD0-C986-4B60-9E45-5EA51E1EA494}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {C2EA9BD0-C986-4B60-9E45-5EA51E1EA494}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {C2EA9BD0-C986-4B60-9E45-5EA51E1EA494}.Debug|x86.ActiveCfg = Debug|Any CPU + {C2EA9BD0-C986-4B60-9E45-5EA51E1EA494}.Debug|x86.Build.0 = Debug|Any CPU + {C2EA9BD0-C986-4B60-9E45-5EA51E1EA494}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C2EA9BD0-C986-4B60-9E45-5EA51E1EA494}.Release|Any CPU.Build.0 = Release|Any CPU + {C2EA9BD0-C986-4B60-9E45-5EA51E1EA494}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {C2EA9BD0-C986-4B60-9E45-5EA51E1EA494}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {C2EA9BD0-C986-4B60-9E45-5EA51E1EA494}.Release|x86.ActiveCfg = Release|Any CPU + {C2EA9BD0-C986-4B60-9E45-5EA51E1EA494}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -145,5 +172,7 @@ Global {6B6BA57A-B32D-430A-AF39-09CAA85308C2} = {E399495E-82B8-4C06-8779-C1D02BEF4495} {66FE5FDF-BBF9-4573-A7B7-53551731C0F9} = {E399495E-82B8-4C06-8779-C1D02BEF4495} {92C2C85C-D1A5-44BD-BE23-238E08471B4D} = {A1477614-E825-4204-A684-385004B63AEB} + {CAAF52EF-F91B-474D-AC56-FE9D96FF8254} = {A1477614-E825-4204-A684-385004B63AEB} + {C2EA9BD0-C986-4B60-9E45-5EA51E1EA494} = {E399495E-82B8-4C06-8779-C1D02BEF4495} EndGlobalSection EndGlobal diff --git a/src/Microsoft.AspNet.FileProviders.Composite/CompositeDirectoryContents.cs b/src/Microsoft.AspNet.FileProviders.Composite/CompositeDirectoryContents.cs new file mode 100644 index 00000000..31a1dd10 --- /dev/null +++ b/src/Microsoft.AspNet.FileProviders.Composite/CompositeDirectoryContents.cs @@ -0,0 +1,95 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Microsoft.AspNet.FileProviders +{ + /// + /// Represents the result of a call composition of for a list of and a path. + /// + public class CompositeDirectoryContents : IDirectoryContents + { + private readonly IList _fileProviders; + private readonly string _subPath; + private List _files; + private bool _exists; + private List _directories; + + /// + /// Creates a new instance of to represents the result of a call composition of . + /// + /// The list of for which the results have to be composed. + /// The path. + public CompositeDirectoryContents(IList fileProviders, string subpath) + { + if(fileProviders == null) + { + throw new ArgumentNullException(nameof(fileProviders)); + } + _fileProviders = fileProviders; + _subPath = subpath; + } + + private void EnsureDirectoriesAreInitialized() + { + if (_directories == null) + { + _directories = new List(); + foreach (var fileProvider in _fileProviders) + { + var directoryContents = fileProvider.GetDirectoryContents(_subPath); + if (directoryContents != null && directoryContents.Exists) + { + _exists = true; + _directories.Add(directoryContents); + } + } + } + } + + private void EnsureFilesAreInitialized() + { + EnsureDirectoriesAreInitialized(); + if (_files == null) + { + _files = new List(); + var names = new HashSet(); + for (var i = 0; i < _directories.Count; i++) + { + var directoryContents = _directories[i]; + foreach (var file in directoryContents) + { + if (names.Add(file.Name)) + { + _files.Add(file); + } + } + } + } + } + + public IEnumerator GetEnumerator() + { + EnsureFilesAreInitialized(); + return _files.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + EnsureFilesAreInitialized(); + return _files.GetEnumerator(); + } + + public bool Exists + { + get + { + EnsureDirectoriesAreInitialized(); + return _exists; + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.FileProviders.Composite/CompositeDisposable.cs b/src/Microsoft.AspNet.FileProviders.Composite/CompositeDisposable.cs new file mode 100644 index 00000000..aefc4ab0 --- /dev/null +++ b/src/Microsoft.AspNet.FileProviders.Composite/CompositeDisposable.cs @@ -0,0 +1,36 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNet.FileProviders +{ + /// + /// Represents a composition of . + /// + public class CompositeDisposable : IDisposable + { + private readonly IList _disposables; + /// + /// Creates a new instance of . + /// + /// The list of to compose. + public CompositeDisposable(IList disposables) + { + if(disposables == null) + { + throw new ArgumentNullException(nameof(disposables)); + } + _disposables = disposables; + } + + public void Dispose() + { + for (var i = 0; i < _disposables.Count; i++) + { + _disposables[i].Dispose(); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.FileProviders.Composite/CompositeFileChangeToken.cs b/src/Microsoft.AspNet.FileProviders.Composite/CompositeFileChangeToken.cs new file mode 100644 index 00000000..25f3a9d9 --- /dev/null +++ b/src/Microsoft.AspNet.FileProviders.Composite/CompositeFileChangeToken.cs @@ -0,0 +1,56 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNet.FileProviders +{ + /// + /// Represents a composition of . + /// + public class CompositeFileChangeToken : IChangeToken + { + private readonly IList _changeTokens; + + /// + /// Creates a new instance of . + /// + /// The list of to compose. + public CompositeFileChangeToken(IList changeTokens) + { + if(changeTokens == null) + { + throw new ArgumentNullException(nameof(changeTokens)); + } + _changeTokens = changeTokens; + } + + public IDisposable RegisterChangeCallback(Action callback, object state) + { + var disposables = new List(); + for (var i = 0; i < _changeTokens.Count; i++) + { + var changeToken = _changeTokens[i]; + if (changeToken.ActiveChangeCallbacks) + { + var disposable = _changeTokens[i].RegisterChangeCallback(callback, state); + disposables.Add(disposable); + } + } + return new CompositeDisposable(disposables); + } + + public bool HasChanged + { + get { return _changeTokens.Any(token => token.HasChanged); } + } + + public bool ActiveChangeCallbacks + { + get { return _changeTokens.Any(token => token.ActiveChangeCallbacks); } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.FileProviders.Composite/CompositeFileProvider.cs b/src/Microsoft.AspNet.FileProviders.Composite/CompositeFileProvider.cs new file mode 100644 index 00000000..86b71447 --- /dev/null +++ b/src/Microsoft.AspNet.FileProviders.Composite/CompositeFileProvider.cs @@ -0,0 +1,99 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.Extensions.Primitives; +using System; +using System.Linq; + +namespace Microsoft.AspNet.FileProviders +{ + /// + /// Looks up files using a list of . + /// + public class CompositeFileProvider : IFileProvider + { + private readonly IFileProvider[] _fileProviders; + + /// + /// Initializes a new instance of the class using a list of file provider. + /// + /// + public CompositeFileProvider(params IFileProvider[] fileProviders) + { + _fileProviders = fileProviders ?? new IFileProvider[0]; + } + /// + /// Initializes a new instance of the class using a list of file provider. + /// + /// + public CompositeFileProvider(IEnumerable fileProviders) + { + if(fileProviders == null) + { + throw new ArgumentNullException(nameof(fileProviders)); + } + _fileProviders = fileProviders.ToArray(); + } + + /// + /// Locates a file at the given path. + /// + /// The path that identifies the file. + /// The file information. Caller must check Exists property. This will be the first existing returned by the provided or a not found if no existing files is found. + public IFileInfo GetFileInfo(string subpath) + { + foreach (var fileProvider in _fileProviders) + { + var fileInfo = fileProvider.GetFileInfo(subpath); + if (fileInfo != null && fileInfo.Exists) + { + return fileInfo; + } + } + return new NotFoundFileInfo(subpath); + } + + /// + /// Enumerate a directory at the given path, if any. + /// + /// The path that identifies the directory + /// Contents of the directory. Caller must check Exists property. + /// The content is a merge of the contents of the provided . + /// When there is multiple with the same Name property, only the first one is included on the results. + public IDirectoryContents GetDirectoryContents(string subpath) + { + var directoryContents = new CompositeDirectoryContents(_fileProviders, subpath); + return directoryContents; + } + + /// + /// Creates a for the specified . + /// + /// + /// Filter string used to determine what files or folders to monitor. Example: **/*.cs, *.*, subFolder/**/*.cshtml. + /// An that is notified when a file matching is added, modified or deleted. + /// The change token will be notified when one of the change token returned by the provided will be notified. + public IChangeToken Watch(string pattern) + { + // Watch all file providers + var changeTokens = new List(); + foreach (var fileProvider in _fileProviders) + { + var changeToken = fileProvider.Watch(pattern); + if (changeToken != null) + { + changeTokens.Add(changeToken); + } + } + + // There is no change token with active change callbacks + if (changeTokens.Count == 0) + { + return NoopChangeToken.Singleton; + } + var CompositeFileChangeToken = new CompositeFileChangeToken(changeTokens); + return CompositeFileChangeToken; + } + } +} diff --git a/src/Microsoft.AspNet.FileProviders.Composite/Microsoft.AspNet.FileProviders.Composite.xproj b/src/Microsoft.AspNet.FileProviders.Composite/Microsoft.AspNet.FileProviders.Composite.xproj new file mode 100644 index 00000000..1cbb8447 --- /dev/null +++ b/src/Microsoft.AspNet.FileProviders.Composite/Microsoft.AspNet.FileProviders.Composite.xproj @@ -0,0 +1,19 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + caaf52ef-f91b-474d-ac56-fe9d96ff8254 + + + + + + + 2.0 + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.FileProviders.Composite/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.FileProviders.Composite/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..b2437d9a --- /dev/null +++ b/src/Microsoft.AspNet.FileProviders.Composite/Properties/AssemblyInfo.cs @@ -0,0 +1,8 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Reflection; +using System.Resources; + +[assembly: AssemblyMetadata("Serviceable", "True")] +[assembly: NeutralResourcesLanguage("en-us")] \ No newline at end of file diff --git a/src/Microsoft.AspNet.FileProviders.Composite/project.json b/src/Microsoft.AspNet.FileProviders.Composite/project.json new file mode 100644 index 00000000..05b0ffe3 --- /dev/null +++ b/src/Microsoft.AspNet.FileProviders.Composite/project.json @@ -0,0 +1,28 @@ +{ + "version": "1.0.0-*", + "compilationOptions": { + "warningsAsErrors": true, + "keyFile": "../../tools/Key.snk" + }, + "description": "Implementation of ASP.NET 5 file provider abstractions to compose file providers.", + "repository": { + "type": "git", + "url": "git://github.com/aspnet/filesystem" + }, + "dependencies": { + "Microsoft.AspNet.FileProviders.Sources": { + "version": "1.0.0-*", + "type": "build" + }, + "Microsoft.AspNet.FileProviders.Abstractions": "1.0.0-*" + }, + "frameworks": { + "net451": {}, + "dotnet5.4": { + "dependencies": { + "System.Collections": "4.0.11-*", + "System.Runtime.Extensions": "4.0.11-*" + } + } + } +} diff --git a/test/Microsoft.AspNet.FileProviders.Composite.Tests/CompositeFileProviderTests.cs b/test/Microsoft.AspNet.FileProviders.Composite.Tests/CompositeFileProviderTests.cs new file mode 100644 index 00000000..876a37a3 --- /dev/null +++ b/test/Microsoft.AspNet.FileProviders.Composite.Tests/CompositeFileProviderTests.cs @@ -0,0 +1,309 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.FileProviders.Composite.Tests.TestUtility; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace Microsoft.AspNet.FileProviders.Composite.Tests +{ + public class CompositeFileProviderTests + { + [Fact] + public void GetFileInfo_ReturnsNotFoundFileInfo_IfNoFileProviderSpecified() + { + // Arrange + var provider = new CompositeFileProvider(); + + // Act + var fileInfo = provider.GetFileInfo("DoesNotExist.txt"); + + // Assert + Assert.NotNull(fileInfo); + Assert.False(fileInfo.Exists); + } + + [Fact] + public void GetFileInfo_ReturnsNotFoundFileInfo_IfFileDoesNotExist() + { + // Arrange + var provider = new CompositeFileProvider(new MockFileProvider(new MockFileInfo("DoesExist.txt"))); + + // Act + var fileInfo = provider.GetFileInfo("DoesNotExist.txt"); + + // Assert + Assert.NotNull(fileInfo); + Assert.False(fileInfo.Exists); + } + + [Fact] + public void GetFileInfo_ReturnsTheFirstFoundFileInfo() + { + // Arrange + var fileName = "File1"; + var expectedFileInfo = new MockFileInfo(fileName); + var provider = new CompositeFileProvider( + new MockFileProvider( + new MockFileInfo("FileA"), + new MockFileInfo("FileB")), + new MockFileProvider( + expectedFileInfo, + new MockFileInfo("File2")), + new MockFileProvider( + new MockFileInfo(fileName), + new MockFileInfo("File3"))); + + // Act + var fileInfo = provider.GetFileInfo(fileName); + + // Assert + Assert.Same(expectedFileInfo, fileInfo); + } + + [Fact] + public void GetDirectoryContents_ReturnsNonExistingEmptySequence_IfNoFileProviderSpecified() + { + // Arrange + var provider = new CompositeFileProvider(); + + // Act + var files = provider.GetDirectoryContents(string.Empty); + + // Assert + Assert.NotNull(files); + Assert.False(files.Exists); + Assert.Empty(files); + } + + [Fact] + public void GetDirectoryContents_ReturnsNonExistingEmptySequence_IfResourcesDoNotExist() + { + // Arrange + var provider = new CompositeFileProvider(); + + // Act + var files = provider.GetDirectoryContents("DoesNotExist"); + + // Assert + Assert.NotNull(files); + Assert.False(files.Exists); + Assert.Empty(files); + } + + [Fact] + public void GetDirectoryContents_ReturnsCombinaisionOFFiles() + { + // Arrange + var file1 = new MockFileInfo("File1"); + var file2 = new MockFileInfo("File2"); + var file2Bis = new MockFileInfo("File2"); + var file3 = new MockFileInfo("File3"); + var provider = new CompositeFileProvider( + new MockFileProvider( + file1, + file2), + new MockFileProvider( + file2Bis, + file3)); + + // Act + var files = provider.GetDirectoryContents(string.Empty); + + // Assert + Assert.NotNull(files); + Assert.True(files.Exists); + Assert.Collection(files.OrderBy(f => f.Name, StringComparer.Ordinal), + file => Assert.Same(file1, file), + file => Assert.Same(file2, file), + file => Assert.Same(file3, file)); + } + + [Fact] + public void GetDirectoryContents_ReturnsCombinaitionOFFiles_WhenSomeFileProviderRetunsNoContent() + { + // Arrange + var folderAFile1 = new MockFileInfo("FolderA/File1"); + var folderAFile2 = new MockFileInfo("FolderA/File2"); + var folderAFile2Bis = new MockFileInfo("FolderA/File2"); + var folderBFile1 = new MockFileInfo("FolderB/File1"); + var folderBFile2 = new MockFileInfo("FolderB/File2"); + var folderCFile3 = new MockFileInfo("FolderC/File3"); + var provider = new CompositeFileProvider( + new MockFileProvider( + folderAFile1, + folderAFile2, + folderBFile2), + new MockFileProvider( + folderAFile2Bis, + folderBFile1, + folderCFile3)); + + // Act + var files = provider.GetDirectoryContents("FolderC/"); + + // Assert + Assert.NotNull(files); + Assert.True(files.Exists); + Assert.Collection(files.OrderBy(f => f.Name, StringComparer.Ordinal), + file => Assert.Equal(folderCFile3, file)); + } + + [Fact] + public void Watch_ReturnsNoopChangeToken_IfNoFileProviderSpecified() + { + // Arrange + var provider = new CompositeFileProvider(); + + // Act + var changeToken = provider.Watch("DoesNotExist*Pattern"); + + // Assert + Assert.NotNull(changeToken); + Assert.False(changeToken.ActiveChangeCallbacks); + } + + [Fact] + public void Watch_ReturnsNoopChangeToken_IfNoWatcherReturnedByFileProviders() + { + // Arrange + var provider = new CompositeFileProvider( + new MockFileProvider()); + + // Act + var changeToken = provider.Watch("DoesntExist*Pattern"); + + // Assert + Assert.NotNull(changeToken); + Assert.False(changeToken.ActiveChangeCallbacks); + } + + [Fact] + public void Watch_CompositeChangeToken_HasChangedIsCorrectlyComputed() + { + // Arrange + var firstChangeToken = new MockChangeToken(); + var secondChangeToken = new MockChangeToken(); + var thirdChangeToken = new MockChangeToken(); + var provider = new CompositeFileProvider( + new MockFileProvider( + new KeyValuePair("pattern", firstChangeToken), + new KeyValuePair("2ndpattern", secondChangeToken)), + new MockFileProvider(new KeyValuePair("pattern", thirdChangeToken))); + + // Act + var changeToken = provider.Watch("pattern"); + + // Assert + Assert.NotNull(changeToken); + Assert.False(changeToken.ActiveChangeCallbacks); + Assert.False(changeToken.HasChanged); + + // HasChanged update + // first change token + firstChangeToken.HasChanged = true; + Assert.True(changeToken.HasChanged); + firstChangeToken.HasChanged = false; + // second change token + secondChangeToken.HasChanged = true; + Assert.False(changeToken.HasChanged); + secondChangeToken.HasChanged = false; + // third change token + thirdChangeToken.HasChanged = true; + Assert.True(changeToken.HasChanged); + } + + [Fact] + public void Watch_CompositeChangeToken_RegisterChangeCallbackCorrectlyTransmitsAllParameters() + { + // Arrange + var firstChangeToken = new MockChangeToken { ActiveChangeCallbacks = true }; + var secondChangeToken = new MockChangeToken(); + var thirdChangeToken = new MockChangeToken { ActiveChangeCallbacks = true }; + var provider = new CompositeFileProvider( + new MockFileProvider( + new KeyValuePair("pattern", firstChangeToken), + new KeyValuePair("2ndpattern", secondChangeToken)), + new MockFileProvider(new KeyValuePair("pattern", thirdChangeToken))); + + // Act + var changeToken = provider.Watch("pattern"); + + // Assert + Assert.NotNull(changeToken); + Assert.True(changeToken.ActiveChangeCallbacks); + Assert.False(changeToken.HasChanged); + + // Register callback + Assert.Equal(0, firstChangeToken.Callbacks.Count); + Assert.Equal(0, secondChangeToken.Callbacks.Count); + Assert.Equal(0, thirdChangeToken.Callbacks.Count); + var hasBeenCalled = false; + object result = null; + object state = new object(); + changeToken.RegisterChangeCallback(item => + { + hasBeenCalled = true; + result = item; + }, state); + Assert.Equal(1, firstChangeToken.Callbacks.Count); + Assert.Same(state, firstChangeToken.Callbacks[0].Item2); + Assert.Equal(0, secondChangeToken.Callbacks.Count); + Assert.Equal(1, thirdChangeToken.Callbacks.Count); + Assert.Same(state, thirdChangeToken.Callbacks[0].Item2); + var expectedResult = new object(); + firstChangeToken.RaiseCallback(expectedResult); + Assert.True(hasBeenCalled); + Assert.Equal(expectedResult, result); + } + + [Fact] + public void Watch_CompositeChangeToken_RegisterChangeCallbackReturnsACompositeDisposable() + { + // Arrange + var firstChangeToken = new MockChangeToken { ActiveChangeCallbacks = true }; + var secondChangeToken = new MockChangeToken(); + var thirdChangeToken = new MockChangeToken { ActiveChangeCallbacks = true }; + var provider = new CompositeFileProvider( + new MockFileProvider( + new KeyValuePair("pattern", firstChangeToken), + new KeyValuePair("2ndpattern", secondChangeToken)), + new MockFileProvider(new KeyValuePair("pattern", thirdChangeToken))); + + // Act + var changeToken = provider.Watch("pattern"); + + // Assert + Assert.NotNull(changeToken); + Assert.True(changeToken.ActiveChangeCallbacks); + Assert.False(changeToken.HasChanged); + + // Register callback + Assert.Equal(0, firstChangeToken.Callbacks.Count); + Assert.Equal(0, secondChangeToken.Callbacks.Count); + Assert.Equal(0, thirdChangeToken.Callbacks.Count); + var hasBeenCalled = false; + object result = null; + object state = new object(); + var disposable = changeToken.RegisterChangeCallback(item => + { + hasBeenCalled = true; + result = item; + }, state); + Assert.Equal(1, firstChangeToken.Callbacks.Count); + Assert.False(firstChangeToken.Callbacks[0].Item3.Disposed); + Assert.Equal(0, secondChangeToken.Callbacks.Count); + Assert.Equal(1, thirdChangeToken.Callbacks.Count); + Assert.False(thirdChangeToken.Callbacks[0].Item3.Disposed); + disposable.Dispose(); + Assert.Equal(1, firstChangeToken.Callbacks.Count); + Assert.True(firstChangeToken.Callbacks[0].Item3.Disposed); + Assert.Equal(0, secondChangeToken.Callbacks.Count); + Assert.Equal(1, thirdChangeToken.Callbacks.Count); + Assert.True(thirdChangeToken.Callbacks[0].Item3.Disposed); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.FileProviders.Composite.Tests/Microsoft.AspNet.FileProviders.Composite.Tests.xproj b/test/Microsoft.AspNet.FileProviders.Composite.Tests/Microsoft.AspNet.FileProviders.Composite.Tests.xproj new file mode 100644 index 00000000..c5d8d027 --- /dev/null +++ b/test/Microsoft.AspNet.FileProviders.Composite.Tests/Microsoft.AspNet.FileProviders.Composite.Tests.xproj @@ -0,0 +1,22 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + c2ea9bd0-c986-4b60-9e45-5ea51e1ea494 + + + + + + + 2.0 + + + + + + \ No newline at end of file diff --git a/test/Microsoft.AspNet.FileProviders.Composite.Tests/TestUtility/MockChangeToken.cs b/test/Microsoft.AspNet.FileProviders.Composite.Tests/TestUtility/MockChangeToken.cs new file mode 100644 index 00000000..737f82d4 --- /dev/null +++ b/test/Microsoft.AspNet.FileProviders.Composite.Tests/TestUtility/MockChangeToken.cs @@ -0,0 +1,41 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Extensions.Primitives; +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNet.FileProviders.Composite.Tests.TestUtility +{ + public class MockChangeToken : IChangeToken + { + private readonly List, object, MockDisposable>> _callbacks = new List, object, MockDisposable>>(); + + public bool ActiveChangeCallbacks { get; set; } + + public bool HasChanged { get; set; } + + public List, object, MockDisposable>> Callbacks + { + get + { + return _callbacks; + } + } + + public IDisposable RegisterChangeCallback(Action callback, object state) + { + var disposable = new MockDisposable(); + _callbacks.Add(Tuple.Create(callback, state, disposable)); + return disposable; + } + + internal void RaiseCallback(object item) + { + foreach (var callback in _callbacks) + { + callback.Item1(item); + } + } + } +} diff --git a/test/Microsoft.AspNet.FileProviders.Composite.Tests/TestUtility/MockDisposable.cs b/test/Microsoft.AspNet.FileProviders.Composite.Tests/TestUtility/MockDisposable.cs new file mode 100644 index 00000000..a50c0860 --- /dev/null +++ b/test/Microsoft.AspNet.FileProviders.Composite.Tests/TestUtility/MockDisposable.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNet.FileProviders.Composite.Tests.TestUtility +{ + public class MockDisposable : IDisposable + { + public bool Disposed { get; set; } + + public void Dispose() + { + Disposed = true; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.FileProviders.Composite.Tests/TestUtility/MockFileInfo.cs b/test/Microsoft.AspNet.FileProviders.Composite.Tests/TestUtility/MockFileInfo.cs new file mode 100644 index 00000000..a8834d57 --- /dev/null +++ b/test/Microsoft.AspNet.FileProviders.Composite.Tests/TestUtility/MockFileInfo.cs @@ -0,0 +1,36 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; + +namespace Microsoft.AspNet.FileProviders.Composite.Tests.TestUtility +{ + public class MockFileInfo : IFileInfo + { + public MockFileInfo(string name) + { + Name = name; + } + + public bool Exists + { + get { return true; } + } + + public bool IsDirectory { get; set; } + + public DateTimeOffset LastModified { get; set; } + + public long Length { get; set; } + + public string Name { get; } + + public string PhysicalPath { get; set; } + + public Stream CreateReadStream() + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.FileProviders.Composite.Tests/TestUtility/MockFileProvider.cs b/test/Microsoft.AspNet.FileProviders.Composite.Tests/TestUtility/MockFileProvider.cs new file mode 100644 index 00000000..eb6fe5fa --- /dev/null +++ b/test/Microsoft.AspNet.FileProviders.Composite.Tests/TestUtility/MockFileProvider.cs @@ -0,0 +1,62 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNet.FileProviders.Composite.Tests.TestUtility +{ + public class MockFileProvider : IFileProvider + { + private IEnumerable _files; + private Dictionary _changeTokens; + + public MockFileProvider() + {} + + public MockFileProvider(params IFileInfo[] files) + { + _files = files; + } + + public MockFileProvider(params KeyValuePair[] changeTokens) + { + _changeTokens = changeTokens.ToDictionary( + changeToken => changeToken.Key, + changeToken => changeToken.Value, + StringComparer.Ordinal); + } + + public IDirectoryContents GetDirectoryContents(string subpath) + { + if (string.IsNullOrEmpty(subpath)) + { + return new EnumerableDirectoryContents(_files); + } + + var filesInFolder = _files.Where(f => f.Name.StartsWith(subpath, StringComparison.Ordinal)); + if (filesInFolder.Any()) + { + return new EnumerableDirectoryContents(filesInFolder); + } + return new NotFoundDirectoryContents(); + } + + public IFileInfo GetFileInfo(string subpath) + { + var file = _files.FirstOrDefault(f => f.Name == subpath); + return file ?? new NotFoundFileInfo(subpath); + } + + public IChangeToken Watch(string filter) + { + if (_changeTokens != null && _changeTokens.ContainsKey(filter)) + { + return _changeTokens[filter]; + } + return NoopChangeToken.Singleton; + } + } +} diff --git a/test/Microsoft.AspNet.FileProviders.Composite.Tests/project.json b/test/Microsoft.AspNet.FileProviders.Composite.Tests/project.json new file mode 100644 index 00000000..ffbe4a94 --- /dev/null +++ b/test/Microsoft.AspNet.FileProviders.Composite.Tests/project.json @@ -0,0 +1,19 @@ +{ + "dependencies": { + "Microsoft.AspNet.FileProviders.Composite": "1.0.0-*", + "Microsoft.AspNet.FileProviders.Sources": "1.0.0-*", + "Microsoft.AspNet.Testing": "1.0.0-*", + "xunit.runner.aspnet": "2.0.0-aspnet-*" + }, + "commands": { + "test": "xunit.runner.aspnet" + }, + "frameworks": { + "dnx451": { }, + "dnxcore50": { + "dependencies": { + "System.Reflection.Extensions": "4.0.1-*" + } + } + } +}