提交 bbee9b3f 编写于 作者: T Tomas Matousek

Consider the current culture when copying xml doc comments in shadow copy provider

上级 f403f6b6
......@@ -461,7 +461,9 @@ private async Task<ExecutionResult> ResetAsyncWorker(bool initialize = true)
{
try
{
var options = InteractiveHostOptions.Default.WithInitializationFile(initialize ? _responseFilePath : null);
var options = new InteractiveHostOptions(
initializationFile: initialize ? _responseFilePath : null,
culture: CultureInfo.CurrentUICulture);
var result = await _interactiveHost.ResetAsync(options).ConfigureAwait(false);
......
// Copyright (c) Microsoft. 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.Immutable;
using System.Reflection;
using Microsoft.CodeAnalysis.Scripting;
namespace Microsoft.CodeAnalysis.Editor.Interactive
{
#if TODO
internal sealed class InteractiveMetadataReferenceResolver : MetadataReferencePathResolver
{
private readonly GacFileResolver gacResolver;
internal InteractiveMetadataReferenceResolver(ImmutableArray<string> searchPaths, string baseDirectory)
{
this.gacResolver = new GacFileResolver(
searchPaths,
baseDirectory: baseDirectory,
architectures: GacFileResolver.Default.Architectures, // TODO (tomat)
preferredCulture: System.Globalization.CultureInfo.CurrentCulture); // TODO (tomat)
}
public override string ResolveReference(string reference, string baseFilePath)
{
return gacResolver.ResolveReference(reference, baseFilePath);
}
public override bool Equals(object obj)
{
var other = obj as InteractiveMetadataReferenceResolver;
return other != null &&
this.gacResolver.Equals(other.gacResolver);
}
public override int GetHashCode()
{
return gacResolver.GetHashCode();
}
public ImmutableArray<string> SearchPaths
{
get { return gacResolver.SearchPaths; }
}
public string BaseDirectory
{
get { return gacResolver.BaseDirectory; }
}
}
#endif
}
......@@ -129,7 +129,6 @@
<Compile Include="Extensibility\Interactive\CSharpVBResetCommand.cs" />
<Compile Include="Extensibility\Interactive\InteractiveCommandHandler.cs" />
<Compile Include="Extensibility\Interactive\InteractiveEvaluator.cs" />
<Compile Include="Extensibility\Interactive\InteractiveMetadataReferenceResolver.cs" />
<Compile Include="Extensibility\Interactive\CSharpVBInteractiveCommandContentTypes.cs" />
<Compile Include="Implementation\Completion\InteractiveCommandCompletionService.cs" />
<Compile Include="Implementation\Completion\Presentation\CompletionPresenter.cs" />
......
// Copyright (c) Microsoft. 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.Globalization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.ErrorReporting;
......@@ -53,7 +54,7 @@ private async Task<InitializedRemoteService> TryStartAndInitializeProcessAsync(C
notification(this.Options.InitializationFile != null);
}
var remoteService = await TryStartProcessAsync(cancellationToken).ConfigureAwait(false);
var remoteService = await TryStartProcessAsync(Options.Culture, cancellationToken).ConfigureAwait(false);
if (remoteService == null)
{
return default(InitializedRemoteService);
......@@ -103,9 +104,9 @@ private async Task<InitializedRemoteService> TryStartAndInitializeProcessAsync(C
}
}
private Task<RemoteService> TryStartProcessAsync(CancellationToken cancellationToken)
private Task<RemoteService> TryStartProcessAsync(CultureInfo culture, CancellationToken cancellationToken)
{
return Task.Run(() => Host.TryStartProcess(cancellationToken));
return Task.Run(() => Host.TryStartProcess(culture, cancellationToken));
}
}
}
......
......@@ -40,8 +40,8 @@ internal sealed class Service : MarshalByRefObject, IDisposable
private static TaskScheduler s_UIThreadScheduler;
private readonly InteractiveAssemblyLoader _assemblyLoader;
private readonly MetadataShadowCopyProvider _metadataFileProvider;
private InteractiveAssemblyLoader _assemblyLoader;
private MetadataShadowCopyProvider _metadataFileProvider;
private ReplServiceProvider _replServiceProvider;
private readonly InteractiveHostObject _hostObject;
......@@ -107,13 +107,6 @@ internal EvaluationState WithOptions(ScriptOptions options)
public Service()
{
// TODO (tomat): we should share the copied files with the host
_metadataFileProvider = new MetadataShadowCopyProvider(
Path.Combine(Path.GetTempPath(), "InteractiveHostShadow"),
noShadowCopyDirectories: s_systemNoShadowCopyDirectories);
_assemblyLoader = new InteractiveAssemblyLoader(_metadataFileProvider);
_formattingOptions = new ObjectFormattingOptions(
memberFormat: MemberDisplayFormat.Inline,
quoteStrings: true,
......@@ -157,9 +150,23 @@ public override object InitializeLifetimeService()
return null;
}
public void Initialize(Type replServiceProviderType)
public void Initialize(Type replServiceProviderType, string cultureName)
{
Contract.ThrowIfNull(replServiceProviderType);
Debug.Assert(replServiceProviderType != null);
Debug.Assert(cultureName != null);
Debug.Assert(_metadataFileProvider == null);
Debug.Assert(_assemblyLoader == null);
Debug.Assert(_replServiceProvider == null);
// TODO (tomat): we should share the copied files with the host
_metadataFileProvider = new MetadataShadowCopyProvider(
Path.Combine(Path.GetTempPath(), "InteractiveHostShadow"),
noShadowCopyDirectories: s_systemNoShadowCopyDirectories,
documentationCommentsCulture: new CultureInfo(cultureName));
_assemblyLoader = new InteractiveAssemblyLoader(_metadataFileProvider);
_replServiceProvider = (ReplServiceProvider)Activator.CreateInstance(replServiceProviderType);
}
......@@ -174,7 +181,7 @@ private MetadataReferenceResolver CreateMetadataReferenceResolver(ImmutableArray
new RelativePathResolver(searchPaths, baseDirectory),
string.IsNullOrEmpty(packagesDirectory) ? null : new NuGetPackageResolverImpl(packagesDirectory),
GacFileResolver.IsAvailable ? new GacFileResolver(preferredCulture: CultureInfo.CurrentCulture) : null,
(path, properties) => _metadataFileProvider.GetReference(path, properties));
(path, properties) => new ShadowCopyReference(_metadataFileProvider, path, properties));
}
private SourceReferenceResolver CreateSourceReferenceResolver(ImmutableArray<string> searchPaths, string baseDirectory)
......
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Diagnostics;
using Microsoft.CodeAnalysis.Scripting.Hosting;
namespace Microsoft.CodeAnalysis.Interactive
{
partial class InteractiveHost
{
/// <summary>
/// Specialize <see cref="PortableExecutableReference"/> with path being the original path of the copy.
/// Logically this reference represents that file, the fact that we load the image from a copy is an implementation detail.
/// </summary>
private sealed class ShadowCopyReference : PortableExecutableReference
{
private readonly MetadataShadowCopyProvider _provider;
public ShadowCopyReference(MetadataShadowCopyProvider provider, string originalPath, MetadataReferenceProperties properties)
: base(properties, originalPath)
{
Debug.Assert(originalPath != null);
Debug.Assert(provider != null);
_provider = provider;
}
protected override DocumentationProvider CreateDocumentationProvider()
{
// TODO (tomat): use file next to the dll (or shadow copy)
return DocumentationProvider.Default;
}
protected override Metadata GetMetadataImpl()
{
return _provider.GetMetadata(FilePath, Properties.Kind);
}
protected override PortableExecutableReference WithPropertiesImpl(MetadataReferenceProperties properties)
{
return new ShadowCopyReference(_provider, this.FilePath, properties);
}
}
}
}
......@@ -2,6 +2,7 @@
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
......@@ -105,7 +106,7 @@ public override object InitializeLifetimeService()
return null;
}
private RemoteService TryStartProcess(CancellationToken cancellationToken)
private RemoteService TryStartProcess(CultureInfo culture, CancellationToken cancellationToken)
{
Process newProcess = null;
int newProcessId = -1;
......@@ -191,7 +192,7 @@ private RemoteService TryStartProcess(CancellationToken cancellationToken)
cancellationToken.ThrowIfCancellationRequested();
newService.Initialize(_replServiceProviderType);
newService.Initialize(_replServiceProviderType, culture.Name);
}
catch (RemotingException) when (!CheckAlive(newProcess))
{
......@@ -432,8 +433,10 @@ public async Task<RemoteExecutionResult> ResetAsync(InteractiveHostOptions optio
{
try
{
var options = optionsOpt ?? _lazyRemoteService?.Options ?? new InteractiveHostOptions(null, CultureInfo.CurrentUICulture);
// replace the existing service with a new one:
var newService = CreateRemoteService(optionsOpt ?? _lazyRemoteService?.Options ?? InteractiveHostOptions.Default, skipInitialization: false);
var newService = CreateRemoteService(options, skipInitialization: false);
LazyRemoteService oldService = Interlocked.Exchange(ref _lazyRemoteService, newService);
if (oldService != null)
......
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Globalization;
namespace Microsoft.CodeAnalysis.Interactive
{
/// <summary>
/// Settings that affect InteractiveHost process and initialization.
/// </summary>
public sealed class InteractiveHostOptions
internal sealed class InteractiveHostOptions
{
public static readonly InteractiveHostOptions Default = new InteractiveHostOptions(null);
/// <summary>
/// Optional path to the .rsp file to process when initializing context of the process.
/// </summary>
public string InitializationFile { get; }
private InteractiveHostOptions(string initializationFile)
/// <summary>
/// Host culture used for localization of doc comments, errors.
/// </summary>
public CultureInfo Culture { get; }
public InteractiveHostOptions(
string initializationFile = null,
CultureInfo culture = null)
{
this.InitializationFile = initializationFile;
InitializationFile = initializationFile;
Culture = culture ?? CultureInfo.CurrentUICulture;
}
public InteractiveHostOptions WithInitializationFile(string initializationFile)
{
if (this.InitializationFile == initializationFile)
if (InitializationFile == initializationFile)
{
return this;
}
return new InteractiveHostOptions(initializationFile, Culture);
}
public InteractiveHostOptions WithCulture(CultureInfo culture)
{
if (Culture == culture)
{
return this;
}
return new InteractiveHostOptions(initializationFile);
return new InteractiveHostOptions(InitializationFile, culture);
}
}
}
......@@ -85,6 +85,7 @@
<Compile Include="Interactive\Core\InteractiveHost.cs" />
<Compile Include="Interactive\Core\InteractiveHost.InitializedRemoteService.cs" />
<Compile Include="Interactive\Core\InteractiveHost.Service.cs" />
<Compile Include="Interactive\Core\InteractiveHost.ShadowCopyReference.cs" />
<Compile Include="Interactive\Core\InteractiveLanguageNames.cs" />
<Compile Include="Interactive\Core\ReplServiceProvider.cs" />
<Compile Include="Interactive\Core\InteractiveHost.RemoteAsyncOperation.cs" />
......
......@@ -3,6 +3,7 @@
using System;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
......@@ -40,7 +41,7 @@ public InteractiveHostTests()
RedirectOutput();
Host.ResetAsync(InteractiveHostOptions.Default).Wait();
Host.ResetAsync(new InteractiveHostOptions(initializationFile: null, culture: CultureInfo.InvariantCulture)).Wait();
var remoteService = Host.TryGetService();
Assert.NotNull(remoteService);
......@@ -127,7 +128,7 @@ public void RestartHost(string rspFile = null)
{
ClearOutput();
var initTask = Host.ResetAsync(InteractiveHostOptions.Default.WithInitializationFile(rspFile));
var initTask = Host.ResetAsync(new InteractiveHostOptions(initializationFile: rspFile, culture: CultureInfo.InvariantCulture));
initTask.Wait();
}
......@@ -524,7 +525,8 @@ public void AddReference_Path()
Assert.True(Execute("System.Diagnostics.Process.GetCurrentProcess().HasExited"));
}
[Fact(Skip = "530414")]
// Caused by submission not inheriting references.
[Fact(Skip = "101161")]
public void AddReference_ShadowCopy()
{
var dir = Temp.CreateDirectory();
......@@ -649,7 +651,7 @@ public void AddReference_AlreadyLoadedDependencies()
Assert.Equal("1", ReadOutputToEnd().Trim());
}
[Fact(Skip = "530414")]
[Fact(Skip = "101161")]
public void AddReference_LoadUpdatedReference()
{
var dir = Temp.CreateDirectory();
......@@ -659,10 +661,9 @@ public void AddReference_LoadUpdatedReference()
var file = dir.CreateFile("c.dll").WriteAllBytes(c1.EmitToArray());
// use:
Execute(@"
#r """ + file.Path + @"""
C foo() { return new C(); }
Execute($@"
#r ""{file.Path}""
C foo() => new C();
new C().X
");
......@@ -672,18 +673,21 @@ public void AddReference_LoadUpdatedReference()
file.WriteAllBytes(c2.EmitToArray());
// add the reference again:
Execute(@"
#r """ + file.Path + @"""
Execute($@"
#r ""{file.Path}""
new D().Y
");
// TODO: We should report an error that assembly named 'a' was already loaded with different content.
// In future we can let it load and improve error reporting around type conversions.
Assert.Equal("", ReadErrorOutputToEnd().Trim());
Assert.Equal(
@"1
2", ReadOutputToEnd().Trim());
}
[Fact(Skip = "987032")]
[Fact(Skip = "129388")]
public void AddReference_MultipleReferencesWithSameWeakIdentity()
{
var dir = Temp.CreateDirectory();
......@@ -699,13 +703,16 @@ public void AddReference_MultipleReferencesWithSameWeakIdentity()
var c2 = CreateCompilationWithMscorlib(source2, assemblyName: "C");
var file2 = dir2.CreateFile("c.dll").WriteAllBytes(c2.EmitToArray());
Execute(@"
#r """ + file1.Path + @"""
#r """ + file2.Path + @"""
Execute($@"
#r ""{file1.Path}""
#r ""{file2.Path}""
");
Execute("new C1()");
Execute("new C2()");
// TODO: We should report an error that assembly named 'c' was already loaded with different content.
// In future we can let it load and let the compiler report the error CS1704: "An assembly with the same simple name 'C' has already been imported".
Assert.Equal(
@"(2,1): error CS1704: An assembly with the same simple name 'C' has already been imported. Try removing one of the references (e.g. '" + file1.Path + @"') or sign them to enable side-by-side.
(1,5): error CS0246: The type or namespace name 'C1' could not be found (are you missing a using directive or an assembly reference?)
......@@ -714,6 +721,36 @@ public void AddReference_MultipleReferencesWithSameWeakIdentity()
Assert.Equal("", ReadOutputToEnd().Trim());
}
[Fact(Skip = "129388")]
public void AddReference_MultipleReferencesWeakVersioning()
{
var dir = Temp.CreateDirectory();
var dir1 = dir.CreateDirectory("1");
var dir2 = dir.CreateDirectory("2");
var source1 = @"[assembly: System.Reflection.AssemblyVersion(""1.0.0.0"")] public class C1 { }";
var c1 = CreateCompilationWithMscorlib(source1, assemblyName: "C");
var file1 = dir1.CreateFile("c.dll").WriteAllBytes(c1.EmitToArray());
var source2 = @"[assembly: System.Reflection.AssemblyVersion(""2.0.0.0"")] public class C2 { }";
var c2 = CreateCompilationWithMscorlib(source2, assemblyName: "C");
var file2 = dir2.CreateFile("c.dll").WriteAllBytes(c2.EmitToArray());
Execute($@"
#r ""{file1.Path}""
#r ""{file2.Path}""
");
Execute("new C1()");
Execute("new C2()");
// TODO: We should report an error that assembly named 'c' was already loaded with different content.
// In future we can let it load and improve error reporting around type conversions.
Assert.Equal("TODO: error", ReadErrorOutputToEnd().Trim());
Assert.Equal("", ReadOutputToEnd().Trim());
}
//// TODO (987032):
//// [Fact]
//// public void AsyncInitializeContextWithDotNETLibraries()
......@@ -804,7 +841,7 @@ public void ReferencePaths()
CompileLibrary(directory, assemblyName + ".dll", assemblyName, @"public class C { }");
var rspFile = Temp.CreateFile();
rspFile.WriteAllText("/rp:" + directory.Path);
var task = Host.ResetAsync(InteractiveHostOptions.Default.WithInitializationFile(rspFile.Path));
var task = Host.ResetAsync(new InteractiveHostOptions(initializationFile: rspFile.Path, culture: CultureInfo.InvariantCulture));
task.Wait();
Execute(
$@"#r ""{assemblyName}.dll""
......
......@@ -75,7 +75,7 @@ private void TestKillAfter(int milliseconds)
t.Start();
});
p.ResetAsync(InteractiveHostOptions.Default).Wait();
p.ResetAsync(new InteractiveHostOptions()).Wait();
for (int j = 0; j < 10; j++)
{
......
......@@ -138,10 +138,7 @@ private AssemblyAndLocation Load(string reference)
{
// copy holds on the file handle, we need to keep the handle
// open until the file is locked by the CLR assembly loader:
if (copy != null)
{
copy.DisposeFileHandles();
}
copy?.DisposeFileHandles();
}
}
......
......@@ -17,19 +17,22 @@ public sealed class MetadataShadowCopy
/// <summary>
/// Documentation file copy or null if there is none.
/// </summary>
/// <remarks>
/// Documentation files are currently only supported for manifest modules, not modules included in an assembly.
/// </remarks>
public ShadowCopy DocumentationFile { get; }
// this instance doesn't own the image
public Metadata Metadata { get; }
internal MetadataShadowCopy(ShadowCopy primaryModule, ShadowCopy documentationFile, Metadata metadataCopy)
internal MetadataShadowCopy(ShadowCopy primaryModule, ShadowCopy documentationFileOpt, Metadata metadataCopy)
{
Debug.Assert(primaryModule != null);
Debug.Assert(metadataCopy != null);
////Debug.Assert(!metadataCopy.IsImageOwner); property is now internal
PrimaryModule = primaryModule;
DocumentationFile = documentationFile;
DocumentationFile = documentationFileOpt;
Metadata = metadataCopy;
}
......@@ -37,11 +40,7 @@ internal MetadataShadowCopy(ShadowCopy primaryModule, ShadowCopy documentationFi
internal void DisposeFileHandles()
{
PrimaryModule.DisposeFileStream();
if (DocumentationFile != null)
{
DocumentationFile.DisposeFileStream();
}
DocumentationFile?.DisposeFileStream();
}
}
}
......@@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using Roslyn.Utilities;
......@@ -15,40 +16,8 @@ namespace Microsoft.CodeAnalysis.Scripting.Hosting
/// </summary>
public sealed class MetadataShadowCopyProvider : IDisposable
{
/// <summary>
/// Specialize <see cref="PortableExecutableReference"/> with path being the original path of the copy.
/// Logically this reference represents that file, the fact that we load the image from a copy is an implementation detail.
/// </summary>
private sealed class ShadowCopyReference : PortableExecutableReference
{
private readonly MetadataShadowCopyProvider _provider;
public ShadowCopyReference(MetadataShadowCopyProvider provider, string originalPath, MetadataReferenceProperties properties)
: base(properties, originalPath)
{
Debug.Assert(originalPath != null);
Debug.Assert(provider != null);
_provider = provider;
}
protected override DocumentationProvider CreateDocumentationProvider()
{
// TODO (tomat): use file next to the dll (or shadow copy)
return DocumentationProvider.Default;
}
protected override Metadata GetMetadataImpl()
{
return _provider.GetMetadata(FilePath, Properties.Kind);
}
protected override PortableExecutableReference WithPropertiesImpl(MetadataReferenceProperties properties)
{
return new ShadowCopyReference(_provider, this.FilePath, properties);
}
}
private readonly CultureInfo _documentationCommentsCulture;
// normalized absolute path
private readonly string _baseDirectory;
......@@ -92,9 +61,10 @@ public CacheEntry(TPublic @public, Metadata @private)
/// </summary>
/// <param name="directory">The directory to use to store file copies.</param>
/// <param name="noShadowCopyDirectories">Directories to exclude from shadow-copying.</param>
/// <param name="documentationCommentsCulture">Culture of documentation comments to copy. If not specified no doc comment files are going to be copied.</param>
/// <exception cref="ArgumentNullException"><paramref name="directory"/> is null.</exception>
/// <exception cref="ArgumentException"><paramref name="directory"/> is not an absolute path.</exception>
public MetadataShadowCopyProvider(string directory = null, IEnumerable<string> noShadowCopyDirectories = null)
public MetadataShadowCopyProvider(string directory = null, IEnumerable<string> noShadowCopyDirectories = null, CultureInfo documentationCommentsCulture = null)
{
if (directory != null)
{
......@@ -128,6 +98,8 @@ public MetadataShadowCopyProvider(string directory = null, IEnumerable<string> n
{
_noShadowCopyDirectories = ImmutableArray<string>.Empty;
}
_documentationCommentsCulture = documentationCommentsCulture;
}
private static void RequireAbsolutePath(string path, string argumentName)
......@@ -375,14 +347,6 @@ private bool CopyExistsOrIsSuppressed(FileKey key, out CacheEntry<MetadataShadow
return _shadowCopies.TryGetValue(key, out existing);
}
/// <exception cref="ArgumentNullException"><paramref name="fullPath"/> is null.</exception>
/// <exception cref="ArgumentException"><paramref name="fullPath"/> is not an absolute path.</exception>
public PortableExecutableReference GetReference(string fullPath, MetadataReferenceProperties properties = default(MetadataReferenceProperties))
{
RequireAbsolutePath(fullPath, nameof(fullPath));
return new ShadowCopyReference(this, fullPath, properties);
}
/// <summary>
/// Suppresses shadow-copying of specified path.
/// </summary>
......@@ -444,21 +408,10 @@ private CacheEntry<MetadataShadowCopy> CreateMetadataShadowCopy(string originalP
// Create directory for the assembly.
// If the assembly has any modules they have to be copied to the same directory
// and have the same names as specified in metadata.
string assemblyDir = CreateUniqueDirectory(ShadowCopyDirectory);
string shadowCopyPath = Path.Combine(assemblyDir, Path.GetFileName(originalPath));
string assemblyCopyDir = CreateUniqueDirectory(ShadowCopyDirectory);
string shadowCopyPath = Path.Combine(assemblyCopyDir, Path.GetFileName(originalPath));
ShadowCopy documentationFileCopy = null;
string xmlOriginalPath;
if (XmlFileResolverForAssemblies.TryFindXmlDocumentationFile(originalPath, out xmlOriginalPath))
{
// TODO (tomat): how do doc comments work for multi-module assembly?
var xmlCopyPath = Path.ChangeExtension(shadowCopyPath, ".xml");
var xmlStream = CopyFile(xmlOriginalPath, xmlCopyPath, fileMayNotExist: true);
if (xmlStream != null)
{
documentationFileCopy = new ShadowCopy(xmlStream, xmlOriginalPath, xmlCopyPath);
}
}
ShadowCopy documentationFileCopy = TryCopyDocumentationFile(originalPath, assemblyCopyDir, _documentationCommentsCulture);
var manifestModuleCopyStream = CopyFile(originalPath, shadowCopyPath);
var manifestModuleCopy = new ShadowCopy(manifestModuleCopyStream, originalPath, shadowCopyPath);
......@@ -590,7 +543,78 @@ private string CreateUniqueDirectory(string basePath)
}
}
private FileStream CopyFile(string originalPath, string shadowCopyPath, bool fileMayNotExist = false)
private static ShadowCopy TryCopyDocumentationFile(string originalAssemblyPath, string assemblyCopyDirectory, CultureInfo docCultureOpt)
{
// Note: Doc comments are not supported for netmodules.
string assemblyDirectory = Path.GetDirectoryName(originalAssemblyPath);
string assemblyFileName = Path.GetFileName(originalAssemblyPath);
string xmlSubdirectory;
string xmlFileName;
if (docCultureOpt == null ||
!TryFindCollocatedDocumentationFile(assemblyDirectory, assemblyFileName, docCultureOpt, out xmlSubdirectory, out xmlFileName))
{
return null;
}
if (!xmlSubdirectory.IsEmpty())
{
try
{
Directory.CreateDirectory(Path.Combine(assemblyCopyDirectory, xmlSubdirectory));
}
catch
{
return null;
}
}
string xmlCopyPath = Path.Combine(assemblyCopyDirectory, xmlSubdirectory, xmlFileName);
string xmlOriginalPath = Path.Combine(assemblyDirectory, xmlSubdirectory, xmlFileName);
var xmlStream = CopyFile(xmlOriginalPath, xmlCopyPath, fileMayNotExist: true);
return (xmlStream != null) ? new ShadowCopy(xmlStream, xmlOriginalPath, xmlCopyPath) : null;
}
private static bool TryFindCollocatedDocumentationFile(
string assemblyDirectory,
string assemblyFileName,
CultureInfo culture,
out string docSubdirectory,
out string docFileName)
{
Debug.Assert(assemblyDirectory != null);
Debug.Assert(assemblyFileName != null);
Debug.Assert(culture != null);
// 1. Look in subdirectories based on the current culture
docFileName = Path.ChangeExtension(assemblyFileName, ".xml");
while (culture != CultureInfo.InvariantCulture)
{
docSubdirectory = culture.Name;
if (File.Exists(Path.Combine(assemblyDirectory, docSubdirectory, docFileName)))
{
return true;
}
culture = culture.Parent;
}
// 2. Look in the same directory as the assembly itself
docSubdirectory = string.Empty;
if (File.Exists(Path.Combine(assemblyDirectory, docFileName)))
{
return true;
}
docFileName = null;
return false;
}
private static FileStream CopyFile(string originalPath, string shadowCopyPath, bool fileMayNotExist = false)
{
try
{
......@@ -598,15 +622,10 @@ private FileStream CopyFile(string originalPath, string shadowCopyPath, bool fil
StripReadOnlyAttributeFromFile(new FileInfo(shadowCopyPath));
return new FileStream(shadowCopyPath, FileMode.Open, FileAccess.Read, FileShare.Read);
}
catch (FileNotFoundException)
catch (Exception e) when (fileMayNotExist && (e is FileNotFoundException || e is DirectoryNotFoundException))
{
if (!fileMayNotExist)
{
throw;
}
return null;
}
return null;
}
#region Test hooks
......
......@@ -23,9 +23,8 @@ Microsoft.CodeAnalysis.Scripting.Hosting.MetadataShadowCopyProvider
Microsoft.CodeAnalysis.Scripting.Hosting.MetadataShadowCopyProvider.Dispose() -> void
Microsoft.CodeAnalysis.Scripting.Hosting.MetadataShadowCopyProvider.GetMetadata(string fullPath, Microsoft.CodeAnalysis.MetadataImageKind kind) -> Microsoft.CodeAnalysis.Metadata
Microsoft.CodeAnalysis.Scripting.Hosting.MetadataShadowCopyProvider.GetMetadataShadowCopy(string fullPath, Microsoft.CodeAnalysis.MetadataImageKind kind) -> Microsoft.CodeAnalysis.Scripting.Hosting.MetadataShadowCopy
Microsoft.CodeAnalysis.Scripting.Hosting.MetadataShadowCopyProvider.GetReference(string fullPath, Microsoft.CodeAnalysis.MetadataReferenceProperties properties = default(Microsoft.CodeAnalysis.MetadataReferenceProperties)) -> Microsoft.CodeAnalysis.PortableExecutableReference
Microsoft.CodeAnalysis.Scripting.Hosting.MetadataShadowCopyProvider.IsShadowCopy(string fullPath) -> bool
Microsoft.CodeAnalysis.Scripting.Hosting.MetadataShadowCopyProvider.MetadataShadowCopyProvider(string directory = null, System.Collections.Generic.IEnumerable<string> noShadowCopyDirectories = null) -> void
Microsoft.CodeAnalysis.Scripting.Hosting.MetadataShadowCopyProvider.MetadataShadowCopyProvider(string directory = null, System.Collections.Generic.IEnumerable<string> noShadowCopyDirectories = null, System.Globalization.CultureInfo documentationCommentsCulture = null) -> void
Microsoft.CodeAnalysis.Scripting.Hosting.MetadataShadowCopyProvider.NeedsShadowCopy(string fullPath) -> bool
Microsoft.CodeAnalysis.Scripting.Hosting.MetadataShadowCopyProvider.SuppressShadowCopy(string originalPath) -> void
Microsoft.CodeAnalysis.Scripting.Hosting.ShadowCopy
......
......@@ -9,6 +9,7 @@
using System.Collections.Immutable;
using Roslyn.Utilities;
using System.Runtime.InteropServices;
using System.Globalization;
namespace Microsoft.CodeAnalysis.Scripting.Hosting.UnitTests
{
......@@ -26,7 +27,12 @@ public class MetadataShadowCopyProviderTests : TestBase, IDisposable
public MetadataShadowCopyProviderTests()
{
_provider = new MetadataShadowCopyProvider(TempRoot.Root, s_systemNoShadowCopyDirectories);
_provider = CreateProvider(CultureInfo.InvariantCulture);
}
private static MetadataShadowCopyProvider CreateProvider(CultureInfo culture)
{
return new MetadataShadowCopyProvider(TempRoot.Root, s_systemNoShadowCopyDirectories, culture);
}
public override void Dispose()
......@@ -51,12 +57,6 @@ public void Errors()
Assert.Throws<ArgumentException>(() => _provider.SuppressShadowCopy(@"\bar.dll"));
Assert.Throws<ArgumentException>(() => _provider.SuppressShadowCopy(@"../bar.dll"));
Assert.Throws<ArgumentNullException>(() => _provider.GetReference(null));
Assert.Throws<ArgumentException>(() => _provider.GetReference("c:foo.dll"));
Assert.Throws<ArgumentException>(() => _provider.GetReference("bar.dll"));
Assert.Throws<ArgumentException>(() => _provider.GetReference(@"\bar.dll"));
Assert.Throws<ArgumentException>(() => _provider.GetReference(@"../bar.dll"));
Assert.Throws<ArgumentOutOfRangeException>(() => _provider.GetMetadataShadowCopy(@"c:\foo.dll", (MetadataImageKind)Byte.MaxValue));
Assert.Throws<ArgumentNullException>(() => _provider.GetMetadataShadowCopy(null, MetadataImageKind.Assembly));
Assert.Throws<ArgumentException>(() => _provider.GetMetadataShadowCopy("c:foo.dll", MetadataImageKind.Assembly));
......@@ -132,12 +132,7 @@ public void Modules()
string path1 = dir.CreateFile("mod2.netmodule").WriteAllBytes(TestResources.SymbolsTests.MultiModule.mod2).Path;
string path2 = dir.CreateFile("mod3.netmodule").WriteAllBytes(TestResources.SymbolsTests.MultiModule.mod3).Path;
var reference1 = _provider.GetReference(path0);
Assert.NotNull(reference1);
Assert.Equal(0, _provider.CacheSize);
Assert.Equal(path0, reference1.FilePath);
var metadata1 = reference1.GetMetadata() as AssemblyMetadata;
var metadata1 = _provider.GetMetadata(path0, MetadataImageKind.Assembly) as AssemblyMetadata;
Assert.NotNull(metadata1);
Assert.Equal(3, metadata1.GetModules().Length);
......@@ -156,38 +151,20 @@ public void Modules()
}
// should get the same metadata:
var metadata2 = reference1.GetMetadata() as AssemblyMetadata;
var metadata2 = _provider.GetMetadata(path0, MetadataImageKind.Assembly) as AssemblyMetadata;
Assert.Same(metadata1, metadata2);
// a new reference is created:
var reference2 = _provider.GetReference(path0);
Assert.NotNull(reference2);
Assert.Equal(path0, reference2.FilePath);
Assert.NotSame(reference1, reference2);
// the original file wasn't modified so we still get the same metadata:
var metadata3 = reference2.GetMetadata() as AssemblyMetadata;
Assert.Same(metadata3, metadata2);
// modify the file:
File.SetLastWriteTimeUtc(path0, DateTime.Now + TimeSpan.FromHours(1));
// the reference doesn't own the metadata, so we get an updated image if we ask again:
var modifiedMetadata3 = reference2.GetMetadata() as AssemblyMetadata;
// we get an updated image if we ask again:
var modifiedMetadata3 = _provider.GetMetadata(path0, MetadataImageKind.Assembly) as AssemblyMetadata;
Assert.NotSame(modifiedMetadata3, metadata2);
// a new reference is created, again we get the modified image (which is copied to the shadow copy directory):
var reference4 = _provider.GetReference(path0);
Assert.NotNull(reference4);
Assert.Equal(path0, reference4.FilePath);
Assert.NotSame(reference2, reference4);
// the file has been modified - we get new metadata:
var metadata4 = reference4.GetMetadata() as AssemblyMetadata;
Assert.NotSame(metadata4, metadata3);
for (int i = 0; i < metadata4.GetModules().Length; i++)
for (int i = 0; i < metadata2.GetModules().Length; i++)
{
Assert.NotSame(metadata4.GetModules()[i], metadata3.GetModules()[i]);
Assert.NotSame(metadata2.GetModules()[i], modifiedMetadata3.GetModules()[i]);
}
}
......@@ -195,12 +172,10 @@ public void Modules()
public unsafe void DisposalOnFailure()
{
var f0 = Temp.CreateFile().WriteAllText("bogus").Path;
var r0 = _provider.GetReference(f0);
Assert.Throws<BadImageFormatException>(() => r0.GetMetadata());
Assert.Throws<BadImageFormatException>(() => _provider.GetMetadata(f0, MetadataImageKind.Assembly));
string f1 = Temp.CreateFile().WriteAllBytes(TestResources.SymbolsTests.MultiModule.MultiModuleDll).Path;
var r1 = _provider.GetReference(f1);
Assert.Throws<FileNotFoundException>(() => r1.GetMetadata());
Assert.Throws<FileNotFoundException>(() => _provider.GetMetadata(f1, MetadataImageKind.Assembly));
}
[Fact]
......@@ -219,13 +194,48 @@ public void GetMetadata()
// This needs to be in different folder from referencesdir to cause the other code path
// to be triggered for NeedsShadowCopy method
var dir2 = System.IO.Path.GetTempPath();
string dll2 = System.IO.Path.Combine(dir2, "a2.dll");
System.IO.File.WriteAllBytes(dll2, TestResources.MetadataTests.InterfaceAndClass.CSClasses01);
var dir2 = Path.GetTempPath();
string dll2 = Path.Combine(dir2, "a2.dll");
File.WriteAllBytes(dll2, TestResources.MetadataTests.InterfaceAndClass.CSClasses01);
Assert.Equal(1, _provider.CacheSize);
var sc3a = _provider.GetMetadataShadowCopy(dll2, MetadataImageKind.Module);
Assert.Equal(2, _provider.CacheSize);
}
[Fact]
public void XmlDocComments_SpecificCulture()
{
var elGR = CultureInfo.GetCultureInfo("el-GR");
var arMA = CultureInfo.GetCultureInfo("ar-MA");
var dir = Temp.CreateDirectory();
var dll = dir.CreateFile("a.dll").WriteAllBytes(TestResources.MetadataTests.InterfaceAndClass.CSClasses01);
var docInvariant = dir.CreateFile("a.xml").WriteAllText("Invariant");
var docGreek = dir.CreateDirectory(elGR.Name).CreateFile("a.xml").WriteAllText("Greek");
// invariant culture
var provider = CreateProvider(CultureInfo.InvariantCulture);
var sc = provider.GetMetadataShadowCopy(dll.Path, MetadataImageKind.Assembly);
Assert.Equal(Path.Combine(Path.GetDirectoryName(sc.PrimaryModule.FullPath), @"a.xml"), sc.DocumentationFile.FullPath);
Assert.Equal("Invariant", File.ReadAllText(sc.DocumentationFile.FullPath));
// greek culture
provider = CreateProvider(elGR);
sc = provider.GetMetadataShadowCopy(dll.Path, MetadataImageKind.Assembly);
Assert.Equal(Path.Combine(Path.GetDirectoryName(sc.PrimaryModule.FullPath), @"el-GR\a.xml"), sc.DocumentationFile.FullPath);
Assert.Equal("Greek", File.ReadAllText(sc.DocumentationFile.FullPath));
// arabic culture (culture specific docs not found, use invariant)
provider = CreateProvider(arMA);
sc = provider.GetMetadataShadowCopy(dll.Path, MetadataImageKind.Assembly);
Assert.Equal(Path.Combine(Path.GetDirectoryName(sc.PrimaryModule.FullPath), @"a.xml"), sc.DocumentationFile.FullPath);
Assert.Equal("Invariant", File.ReadAllText(sc.DocumentationFile.FullPath));
// no culture:
provider = CreateProvider(null);
sc = provider.GetMetadataShadowCopy(dll.Path, MetadataImageKind.Assembly);
Assert.Null(sc.DocumentationFile);
}
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册