Improve error messages for why a solution failed to load (#5176)

* WIP Improve sln reader/writer error messages

* Added more tests

* Fix a few tests
This commit is contained in:
Justin Goshi 2016-12-29 09:21:55 -10:00 committed by GitHub
parent 961905a301
commit 10f52d9a15
8 changed files with 242 additions and 33 deletions

View file

@ -2,6 +2,14 @@
{ {
internal class LocalizableStrings internal class LocalizableStrings
{ {
// {0} is the line number
// {1} is the error message details
public const string ErrorMessageFormatString = "Invalid format in line {0}: {1}";
public const string ProjectParsingErrorFormatString = "Project section is missing '{0}' when parsing the line starting at position {1}";
public const string InvalidPropertySetFormatString = "Property set is missing '{0}'";
public const string GlobalSectionMoreThanOnceError = "Global section specified more than once"; public const string GlobalSectionMoreThanOnceError = "Global section specified more than once";
public const string GlobalSectionNotClosedError = "Global section not closed"; public const string GlobalSectionNotClosedError = "Global section not closed";

View file

@ -120,6 +120,8 @@ namespace Microsoft.DotNet.Cli.Sln.Internal
private void Read(TextReader reader) private void Read(TextReader reader)
{ {
const string HeaderPrefix = "Microsoft Visual Studio Solution File, Format Version ";
string line; string line;
int curLineNum = 0; int curLineNum = 0;
bool globalFound = false; bool globalFound = false;
@ -129,14 +131,16 @@ namespace Microsoft.DotNet.Cli.Sln.Internal
{ {
curLineNum++; curLineNum++;
line = line.Trim(); line = line.Trim();
if (line.StartsWith("Microsoft Visual Studio Solution File", StringComparison.Ordinal)) if (line.StartsWith(HeaderPrefix, StringComparison.Ordinal))
{ {
int i = line.LastIndexOf(' '); if (line.Length <= HeaderPrefix.Length)
if (i == -1)
{ {
throw new InvalidSolutionFormatException(curLineNum); throw new InvalidSolutionFormatException(
curLineNum,
LocalizableStrings.FileHeaderMissingError);
} }
FormatVersion = line.Substring(i + 1);
FormatVersion = line.Substring(HeaderPrefix.Length);
_prefixBlankLines = curLineNum - 1; _prefixBlankLines = curLineNum - 1;
} }
if (line.StartsWith("# ", StringComparison.Ordinal)) if (line.StartsWith("# ", StringComparison.Ordinal))
@ -157,7 +161,9 @@ namespace Microsoft.DotNet.Cli.Sln.Internal
{ {
if (globalFound) if (globalFound)
{ {
throw new InvalidSolutionFormatException(curLineNum, LocalizableStrings.GlobalSectionMoreThanOnceError); throw new InvalidSolutionFormatException(
curLineNum,
LocalizableStrings.GlobalSectionMoreThanOnceError);
} }
globalFound = true; globalFound = true;
while ((line = reader.ReadLine()) != null) while ((line = reader.ReadLine()) != null)
@ -181,7 +187,9 @@ namespace Microsoft.DotNet.Cli.Sln.Internal
} }
if (line == null) if (line == null)
{ {
throw new InvalidSolutionFormatException(curLineNum, LocalizableStrings.GlobalSectionNotClosedError); throw new InvalidSolutionFormatException(
curLineNum,
LocalizableStrings.GlobalSectionNotClosedError);
} }
} }
else if (line.IndexOf('=') != -1) else if (line.IndexOf('=') != -1)
@ -191,7 +199,9 @@ namespace Microsoft.DotNet.Cli.Sln.Internal
} }
if (FormatVersion == null) if (FormatVersion == null)
{ {
throw new InvalidSolutionFormatException(curLineNum, LocalizableStrings.FileHeaderMissingError); throw new InvalidSolutionFormatException(
curLineNum,
LocalizableStrings.FileHeaderMissingError);
} }
} }
@ -331,15 +341,20 @@ namespace Microsoft.DotNet.Cli.Sln.Internal
} }
} }
throw new InvalidSolutionFormatException(curLineNum, LocalizableStrings.ProjectSectionNotClosedError); throw new InvalidSolutionFormatException(
curLineNum,
LocalizableStrings.ProjectSectionNotClosedError);
} }
private void FindNext(int ln, string line, ref int i, char c) private void FindNext(int ln, string line, ref int i, char c)
{ {
var inputIndex = i;
i = line.IndexOf(c, i); i = line.IndexOf(c, i);
if (i == -1) if (i == -1)
{ {
throw new InvalidSolutionFormatException(ln); throw new InvalidSolutionFormatException(
ln,
string.Format(LocalizableStrings.ProjectParsingErrorFormatString, c, inputIndex));
} }
} }
@ -481,7 +496,9 @@ namespace Microsoft.DotNet.Cli.Sln.Internal
{ {
return SlnSectionType.PostProcess; return SlnSectionType.PostProcess;
} }
throw new InvalidSolutionFormatException(curLineNum, String.Format(LocalizableStrings.InvalidSectionTypeError, s)); throw new InvalidSolutionFormatException(
curLineNum,
String.Format(LocalizableStrings.InvalidSectionTypeError, s));
} }
private string FromSectionType(bool isProjectSection, SlnSectionType type) private string FromSectionType(bool isProjectSection, SlnSectionType type)
@ -502,13 +519,17 @@ namespace Microsoft.DotNet.Cli.Sln.Internal
int k = line.IndexOf('('); int k = line.IndexOf('(');
if (k == -1) if (k == -1)
{ {
throw new InvalidSolutionFormatException(curLineNum, LocalizableStrings.SectionIdMissingError); throw new InvalidSolutionFormatException(
curLineNum,
LocalizableStrings.SectionIdMissingError);
} }
var tag = line.Substring(0, k).Trim(); var tag = line.Substring(0, k).Trim();
var k2 = line.IndexOf(')', k); var k2 = line.IndexOf(')', k);
if (k2 == -1) if (k2 == -1)
{ {
throw new InvalidSolutionFormatException(curLineNum); throw new InvalidSolutionFormatException(
curLineNum,
LocalizableStrings.SectionIdMissingError);
} }
Id = line.Substring(k + 1, k2 - k - 1); Id = line.Substring(k + 1, k2 - k - 1);
@ -531,7 +552,9 @@ namespace Microsoft.DotNet.Cli.Sln.Internal
} }
if (line == null) if (line == null)
{ {
throw new InvalidSolutionFormatException(curLineNum, LocalizableStrings.ClosingSectionTagNotFoundError); throw new InvalidSolutionFormatException(
curLineNum,
LocalizableStrings.ClosingSectionTagNotFoundError);
} }
} }
@ -550,7 +573,9 @@ namespace Microsoft.DotNet.Cli.Sln.Internal
var i = line.IndexOf('.'); var i = line.IndexOf('.');
if (i == -1) if (i == -1)
{ {
throw new InvalidSolutionFormatException(_baseIndex + n); throw new InvalidSolutionFormatException(
_baseIndex + n,
string.Format(LocalizableStrings.InvalidPropertySetFormatString, '.'));
} }
var id = line.Substring(0, i); var id = line.Substring(0, i);
if (curSet == null || id != curSet.Id) if (curSet == null || id != curSet.Id)
@ -1141,12 +1166,8 @@ namespace Microsoft.DotNet.Cli.Sln.Internal
public class InvalidSolutionFormatException : Exception public class InvalidSolutionFormatException : Exception
{ {
public InvalidSolutionFormatException(int line) : base("Invalid format in line " + line) public InvalidSolutionFormatException(int line, string details)
{ : base(string.Format(LocalizableStrings.ErrorMessageFormatString, line, details))
}
public InvalidSolutionFormatException(int line, string msg)
: base("Invalid format in line " + line + ": " + msg)
{ {
} }
} }

View file

@ -105,7 +105,7 @@ namespace Microsoft.DotNet.Tools
public const string CouldNotFindSolutionIn = "Specified solution file {0} does not exist, or there is no solution file in the directory."; public const string CouldNotFindSolutionIn = "Specified solution file {0} does not exist, or there is no solution file in the directory.";
public const string CouldNotFindSolutionOrDirectory = "Could not find solution or directory `{0}`."; public const string CouldNotFindSolutionOrDirectory = "Could not find solution or directory `{0}`.";
public const string MoreThanOneSolutionInDirectory = "Found more than one solution file in {0}. Please specify which one to use."; public const string MoreThanOneSolutionInDirectory = "Found more than one solution file in {0}. Please specify which one to use.";
public const string InvalidSolution = "Invalid solution `{0}`."; public const string InvalidSolutionFormatString = "Invalid solution `{0}`. {1}"; // {0} is the solution path, {1} is already localized details on the failure
public const string SolutionDoesNotExist = "Specified solution file {0} does not exist, or there is no solution file in the directory."; public const string SolutionDoesNotExist = "Specified solution file {0} does not exist, or there is no solution file in the directory.";
/// add p2p /// add p2p

View file

@ -30,9 +30,12 @@ namespace Microsoft.DotNet.Tools.Common
{ {
slnFile = SlnFile.Read(solutionPath); slnFile = SlnFile.Read(solutionPath);
} }
catch catch (InvalidSolutionFormatException e)
{ {
throw new GracefulException(CommonLocalizableStrings.InvalidSolution, solutionPath); throw new GracefulException(
CommonLocalizableStrings.InvalidSolutionFormatString,
solutionPath,
e.Message);
} }
return slnFile; return slnFile;
} }

View file

@ -276,11 +276,13 @@ EndGlobal
.Should().Be(SolutionModified); .Should().Be(SolutionModified);
} }
[Fact] [Theory]
public void WhenGivenAnSolutionWithMissingHeaderItThrows() [InlineData("Invalid Solution")]
[InlineData("Microsoft Visual Studio Solution File, Format Version ")]
public void WhenGivenASolutionWithMissingHeaderItThrows(string fileContents)
{ {
var tmpFile = Temp.CreateFile(); var tmpFile = Temp.CreateFile();
tmpFile.WriteAllText("Invalid Solution"); tmpFile.WriteAllText(fileContents);
Action action = () => Action action = () =>
{ {
@ -290,5 +292,180 @@ EndGlobal
action.ShouldThrow<InvalidSolutionFormatException>() action.ShouldThrow<InvalidSolutionFormatException>()
.WithMessage("Invalid format in line 1: File header is missing"); .WithMessage("Invalid format in line 1: File header is missing");
} }
[Fact]
public void WhenGivenASolutionWithMultipleGlobalSectionsItThrows()
{
const string SolutionFile = @"
Microsoft Visual Studio Solution File, Format Version 12.00
Global
EndGlobal
Global
EndGlobal
";
var tmpFile = Temp.CreateFile();
tmpFile.WriteAllText(SolutionFile);
Action action = () =>
{
SlnFile.Read(tmpFile.Path);
};
action.ShouldThrow<InvalidSolutionFormatException>()
.WithMessage("Invalid format in line 5: Global section specified more than once");
}
[Fact]
public void WhenGivenASolutionWithGlobalSectionNotClosedItThrows()
{
const string SolutionFile = @"
Microsoft Visual Studio Solution File, Format Version 12.00
Global
";
var tmpFile = Temp.CreateFile();
tmpFile.WriteAllText(SolutionFile);
Action action = () =>
{
SlnFile.Read(tmpFile.Path);
};
action.ShouldThrow<InvalidSolutionFormatException>()
.WithMessage("Invalid format in line 3: Global section not closed");
}
[Fact]
public void WhenGivenASolutionWithProjectSectionNotClosedItThrows()
{
const string SolutionFile = @"
Microsoft Visual Studio Solution File, Format Version 12.00
Project(""{9A19103F-16F7-4668-BE54-9A1E7A4F7556}"") = ""App"", ""App\App.csproj"", ""{7072A694-548F-4CAE-A58F-12D257D5F486}""
";
var tmpFile = Temp.CreateFile();
tmpFile.WriteAllText(SolutionFile);
Action action = () =>
{
SlnFile.Read(tmpFile.Path);
};
action.ShouldThrow<InvalidSolutionFormatException>()
.WithMessage("Invalid format in line 3: Project section not closed");
}
[Fact]
public void WhenGivenASolutionWithInvalidProjectSectionItThrows()
{
const string SolutionFile = @"
Microsoft Visual Studio Solution File, Format Version 12.00
Project""{9A19103F-16F7-4668-BE54-9A1E7A4F7556}"") = ""App"", ""App\App.csproj"", ""{7072A694-548F-4CAE-A58F-12D257D5F486}""
EndProject
";
var tmpFile = Temp.CreateFile();
tmpFile.WriteAllText(SolutionFile);
Action action = () =>
{
SlnFile.Read(tmpFile.Path);
};
action.ShouldThrow<InvalidSolutionFormatException>()
.WithMessage("Invalid format in line 3: Project section is missing '(' when parsing the line starting at position 0");
}
[Fact]
public void WhenGivenASolutionWithInvalidSectionTypeItThrows()
{
const string SolutionFile = @"
Microsoft Visual Studio Solution File, Format Version 12.00
Global
GlobalSection(SolutionConfigurationPlatforms) = thisIsUnknown
EndGlobalSection
EndGlobal
";
var tmpFile = Temp.CreateFile();
tmpFile.WriteAllText(SolutionFile);
Action action = () =>
{
SlnFile.Read(tmpFile.Path);
};
action.ShouldThrow<InvalidSolutionFormatException>()
.WithMessage("Invalid format in line 4: Invalid section type: thisIsUnknown");
}
[Fact]
public void WhenGivenASolutionWithMissingSectionIdTypeItThrows()
{
const string SolutionFile = @"
Microsoft Visual Studio Solution File, Format Version 12.00
Global
GlobalSection = preSolution
EndGlobalSection
EndGlobal
";
var tmpFile = Temp.CreateFile();
tmpFile.WriteAllText(SolutionFile);
Action action = () =>
{
SlnFile.Read(tmpFile.Path);
};
action.ShouldThrow<InvalidSolutionFormatException>()
.WithMessage("Invalid format in line 4: Section id missing");
}
[Fact]
public void WhenGivenASolutionWithSectionNotClosedItThrows()
{
const string SolutionFile = @"
Microsoft Visual Studio Solution File, Format Version 12.00
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
EndGlobal
";
var tmpFile = Temp.CreateFile();
tmpFile.WriteAllText(SolutionFile);
Action action = () =>
{
SlnFile.Read(tmpFile.Path);
};
action.ShouldThrow<InvalidSolutionFormatException>()
.WithMessage("Invalid format in line 6: Closing section tag not found");
}
[Fact]
public void WhenGivenASolutionWithInvalidPropertySetItThrows()
{
const string SolutionFile = @"
Microsoft Visual Studio Solution File, Format Version 12.00
Project(""{7072A694-548F-4CAE-A58F-12D257D5F486}"") = ""AppModified"", ""AppModified\AppModified.csproj"", ""{9A19103F-16F7-4668-BE54-9A1E7A4F7556}""
EndProject
Global
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{7072A694-548F-4CAE-A58F-12D257D5F486} Debug|Any CPU ActiveCfg = Debug|Any CPU
EndGlobalSection
EndGlobal
";
var tmpFile = Temp.CreateFile();
tmpFile.WriteAllText(SolutionFile);
Action action = () =>
{
var slnFile = SlnFile.Read(tmpFile.Path);
if (slnFile.ProjectConfigurationsSection.Count == 0)
{
// Need to force loading of nested property sets
}
};
action.ShouldThrow<InvalidSolutionFormatException>()
.WithMessage("Invalid format in line 7: Property set is missing '.'");
}
} }
} }

View file

@ -90,7 +90,7 @@ Additional Arguments:
.WithWorkingDirectory(projectDirectory) .WithWorkingDirectory(projectDirectory)
.ExecuteWithCapturedOutput($"add InvalidSolution.sln project {projectToAdd}"); .ExecuteWithCapturedOutput($"add InvalidSolution.sln project {projectToAdd}");
cmd.Should().Fail(); cmd.Should().Fail();
cmd.StdErr.Should().Be("Invalid solution `InvalidSolution.sln`."); cmd.StdErr.Should().Be("Invalid solution `InvalidSolution.sln`. Invalid format in line 1: File header is missing");
cmd.StdOut.Should().BeVisuallyEquivalentTo(HelpText); cmd.StdOut.Should().BeVisuallyEquivalentTo(HelpText);
} }
@ -110,7 +110,7 @@ Additional Arguments:
.WithWorkingDirectory(projectDirectory) .WithWorkingDirectory(projectDirectory)
.ExecuteWithCapturedOutput($"add project {projectToAdd}"); .ExecuteWithCapturedOutput($"add project {projectToAdd}");
cmd.Should().Fail(); cmd.Should().Fail();
cmd.StdErr.Should().Be($"Invalid solution `{solutionPath}`."); cmd.StdErr.Should().Be($"Invalid solution `{solutionPath}`. Invalid format in line 1: File header is missing");
cmd.StdOut.Should().BeVisuallyEquivalentTo(HelpText); cmd.StdOut.Should().BeVisuallyEquivalentTo(HelpText);
} }

View file

@ -83,7 +83,7 @@ Options:
.WithWorkingDirectory(projectDirectory) .WithWorkingDirectory(projectDirectory)
.ExecuteWithCapturedOutput("list InvalidSolution.sln projects"); .ExecuteWithCapturedOutput("list InvalidSolution.sln projects");
cmd.Should().Fail(); cmd.Should().Fail();
cmd.StdErr.Should().Be("Invalid solution `InvalidSolution.sln`."); cmd.StdErr.Should().Be("Invalid solution `InvalidSolution.sln`. Invalid format in line 1: File header is missing");
cmd.StdOut.Should().BeVisuallyEquivalentTo(HelpText); cmd.StdOut.Should().BeVisuallyEquivalentTo(HelpText);
} }
@ -102,7 +102,7 @@ Options:
.WithWorkingDirectory(projectDirectory) .WithWorkingDirectory(projectDirectory)
.ExecuteWithCapturedOutput("list projects"); .ExecuteWithCapturedOutput("list projects");
cmd.Should().Fail(); cmd.Should().Fail();
cmd.StdErr.Should().Be($"Invalid solution `{solutionFullPath}`."); cmd.StdErr.Should().Be($"Invalid solution `{solutionFullPath}`. Invalid format in line 1: File header is missing");
cmd.StdOut.Should().BeVisuallyEquivalentTo(HelpText); cmd.StdOut.Should().BeVisuallyEquivalentTo(HelpText);
} }

View file

@ -88,7 +88,7 @@ Additional Arguments:
.WithWorkingDirectory(projectDirectory) .WithWorkingDirectory(projectDirectory)
.ExecuteWithCapturedOutput($"remove InvalidSolution.sln project {projectToRemove}"); .ExecuteWithCapturedOutput($"remove InvalidSolution.sln project {projectToRemove}");
cmd.Should().Fail(); cmd.Should().Fail();
cmd.StdErr.Should().Be("Invalid solution `InvalidSolution.sln`."); cmd.StdErr.Should().Be("Invalid solution `InvalidSolution.sln`. Invalid format in line 1: File header is missing");
cmd.StdOut.Should().BeVisuallyEquivalentTo(HelpText); cmd.StdOut.Should().BeVisuallyEquivalentTo(HelpText);
} }
@ -108,7 +108,7 @@ Additional Arguments:
.WithWorkingDirectory(projectDirectory) .WithWorkingDirectory(projectDirectory)
.ExecuteWithCapturedOutput($"remove project {projectToRemove}"); .ExecuteWithCapturedOutput($"remove project {projectToRemove}");
cmd.Should().Fail(); cmd.Should().Fail();
cmd.StdErr.Should().Be($"Invalid solution `{solutionPath}`."); cmd.StdErr.Should().Be($"Invalid solution `{solutionPath}`. Invalid format in line 1: File header is missing");
cmd.StdOut.Should().BeVisuallyEquivalentTo(HelpText); cmd.StdOut.Should().BeVisuallyEquivalentTo(HelpText);
} }