diff --git a/src/Microsoft.VisualStudio.Jdt.Tests/JsonTransformationTest.cs b/src/Microsoft.VisualStudio.Jdt.Tests/JsonTransformationTest.cs new file mode 100644 index 00000000..42037dfd --- /dev/null +++ b/src/Microsoft.VisualStudio.Jdt.Tests/JsonTransformationTest.cs @@ -0,0 +1,323 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.Jdt.Tests +{ + using System.Collections.Generic; + using System.IO; + using System.Linq; + using Xunit; + + /// + /// Test class for + /// + public class JsonTransformationTest + { + private static readonly string SimpleSourceString = @"{ 'A': 1 }"; + + private readonly JsonTransformationTestLogger logger; + + /// + /// Initializes a new instance of the class. + /// + public JsonTransformationTest() + { + // xUnit creates a new instance of the class for each test, so a new logger is created + this.logger = new JsonTransformationTestLogger(); + } + + /// + /// Tests the error caused when an invalid verb is found + /// + [Fact] + public void InvalidVerb() + { + string transformString = @"{ + '@jdt.invalid': false + }"; + + this.TryTransformTest(SimpleSourceString, transformString, false); + + Assert.Empty(this.logger.MessageLog); + Assert.Empty(this.logger.WarningLog); + + // The error should be where at the location of the invalid verb + LogHasSingleEntry(this.logger.ErrorLog, ErrorLocation.Transform.ToString(), 2, 56, true); + } + + /// + /// Tests the error caused by a verb having an invalid value + /// + [Fact] + public void InvalidVerbValue() + { + string transformString = @"{ + '@jdt.remove': 10 + }"; + + this.TryTransformTest(SimpleSourceString, transformString, false); + + Assert.Empty(this.logger.MessageLog); + Assert.Empty(this.logger.WarningLog); + + // The error location should be at the location of the invalid value + LogHasSingleEntry(this.logger.ErrorLog, ErrorLocation.Transform.ToString(), 2, 58, true); + } + + /// + /// Tests the error caused when an invalid attribute is found within a verb + /// + [Fact] + public void InvalidAttribute() + { + string transformString = @"{ + '@jdt.replace': { + '@jdt.invalid': false + } + }"; + + this.TryTransformTest(SimpleSourceString, transformString, false); + + Assert.Empty(this.logger.MessageLog); + Assert.Empty(this.logger.WarningLog); + + // The error location should be at the position of the invalid attribute + LogHasSingleEntry(this.logger.ErrorLog, ErrorLocation.Transform.ToString(), 3, 58, true); + } + + /// + /// Tests the error caused when a required attribute is not found + /// + [Fact] + public void MissingAttribute() + { + string transformString = @"{ + '@jdt.rename': { + '@jdt.path': 'A' + } + }"; + + this.TryTransformTest(SimpleSourceString, transformString, false); + + Assert.Empty(this.logger.MessageLog); + Assert.Empty(this.logger.WarningLog); + + // The error location should be at the beginning of the object with the missing attribute + LogHasSingleEntry(this.logger.ErrorLog, ErrorLocation.Transform.ToString(), 2, 57, true); + } + + /// + /// Tests the error caused when a verb object contains attributes and other objects + /// + [Fact] + public void MixedAttributes() + { + string transformString = @"{ + '@jdt.rename': { + '@jdt.path': 'A', + '@jdt.value': 'Astar', + 'NotAttribute': true + } + }"; + + this.TryTransformTest(SimpleSourceString, transformString, false); + + Assert.Empty(this.logger.MessageLog); + Assert.Empty(this.logger.WarningLog); + + // The error location should be at the beginning of the object with the mixed attribute + LogHasSingleEntry(this.logger.ErrorLog, ErrorLocation.Transform.ToString(), 2, 57, true); + } + + /// + /// Tests the error caused when an attribute has an incorrect value + /// + [Fact] + public void WrongAttributeValue() + { + string transformString = @"{ + '@jdt.remove': { + '@jdt.path': false + } + }"; + + this.TryTransformTest(SimpleSourceString, transformString, false); + + Assert.Empty(this.logger.MessageLog); + Assert.Empty(this.logger.WarningLog); + + // The error location should be at the position of the invalid value + LogHasSingleEntry(this.logger.ErrorLog, ErrorLocation.Transform.ToString(), 3, 61, true); + } + + /// + /// Tests the error caused when a path attribute returns no result + /// + [Fact] + public void RemoveNonExistantNode() + { + string transformString = @"{ + '@jdt.remove': { + '@jdt.path': 'B' + } + }"; + + this.TryTransformTest(SimpleSourceString, transformString, true); + + Assert.Empty(this.logger.MessageLog); + Assert.Empty(this.logger.ErrorLog); + + // The warning location should be at the position of the path value that yielded no results + LogHasSingleEntry(this.logger.WarningLog, ErrorLocation.Transform.ToString(), 3, 59, false); + } + + /// + /// Tests the error caused when attempting to remove the root node + /// + [Fact] + public void RemoveRoot() + { + string transformString = @"{ + '@jdt.remove': true + }"; + + this.TryTransformTest(SimpleSourceString, transformString, false); + + Assert.Empty(this.logger.MessageLog); + Assert.Empty(this.logger.WarningLog); + + // The error location should be at the position of the remove value + LogHasSingleEntry(this.logger.ErrorLog, ErrorLocation.Transform.ToString(), 2, 60, true); + } + + /// + /// Tests the error when a rename value is invalid + /// + [Fact] + public void InvalidRenameValue() + { + string transformString = @"{ + '@jdt.rename': { + 'A': 10 + } + }"; + + this.TryTransformTest(SimpleSourceString, transformString, false); + + Assert.Empty(this.logger.MessageLog); + Assert.Empty(this.logger.WarningLog); + + // The error location should be at the position of the rename property + LogHasSingleEntry(this.logger.ErrorLog, ErrorLocation.Transform.ToString(), 3, 47, true); + } + + /// + /// Tests the error caused when attempting to rename a non-existant node + /// + [Fact] + public void RenameNonExistantNode() + { + string transformString = @"{ + '@jdt.rename': { + 'B': 'Bstar' + } + }"; + + this.TryTransformTest(SimpleSourceString, transformString, true); + + Assert.Empty(this.logger.MessageLog); + Assert.Empty(this.logger.ErrorLog); + + // The position of the warning should be a the beginning of the rename property + LogHasSingleEntry(this.logger.WarningLog, ErrorLocation.Transform.ToString(), 3, 47, false); + } + + /// + /// Test the error when attempting to replace the root with a non-object token + /// + [Fact] + public void ReplaceRoot() + { + string transformString = @"{ + '@jdt.replace': 10 + }"; + + this.TryTransformTest(SimpleSourceString, transformString, false); + + Assert.Empty(this.logger.MessageLog); + Assert.Empty(this.logger.WarningLog); + + // The position of the error should be at the value of replace that caused it + LogHasSingleEntry(this.logger.ErrorLog, ErrorLocation.Transform.ToString(), 2, 59, true); + } + + /// + /// Tests that an exception is thrown when is called + /// + [Fact] + public void ThrowAndLogException() + { + string transformString = @"{ + '@jdt.invalid': false + }"; + using (var transformStream = this.GetStreamFromString(transformString)) + using (var sourceStream = this.GetStreamFromString(SimpleSourceString)) + { + JsonTransformation transform = new JsonTransformation(transformStream, this.logger); + var exception = Record.Exception(() => transform.Apply(sourceStream)); + Assert.NotNull(exception); + Assert.IsType(exception); + var jdtException = exception as JdtException; + Assert.Contains("invalid", jdtException.Message); + Assert.Equal(ErrorLocation.Transform, jdtException.Location); + Assert.Equal(2, jdtException.LineNumber); + Assert.Equal(56, jdtException.LinePosition); + } + } + + private static void LogHasSingleEntry(List log, string fileName, int lineNumber, int linePosition, bool fromException) + { + Assert.Single(log); + var errorEntry = log.Single(); + Assert.Equal(fileName, errorEntry.FileName); + Assert.Equal(lineNumber, errorEntry.LineNumber); + Assert.Equal(linePosition, errorEntry.LinePosition); + Assert.Equal(fromException, errorEntry.FromException); + } + + private void TryTransformTest(string sourceString, string transformString, bool shouldTransformSucceed) + { + using (var transformStream = this.GetStreamFromString(transformString)) + using (var sourceStream = this.GetStreamFromString(sourceString)) + { + JsonTransformation transform = new JsonTransformation(transformStream, this.logger); + Stream result = null; + + var exception = Record.Exception(() => result = transform.Apply(sourceStream)); + + if (shouldTransformSucceed) + { + Assert.NotNull(result); + Assert.Null(exception); + } + else + { + Assert.Null(result); + Assert.NotNull(exception); + Assert.IsType(exception); + } + } + } + + private Stream GetStreamFromString(string s) + { + MemoryStream stringStream = new MemoryStream(); + StreamWriter stringWriter = new StreamWriter(stringStream); + stringWriter.Write(s); + stringWriter.Flush(); + stringStream.Position = 0; + + return stringStream; + } + } +} diff --git a/src/Microsoft.VisualStudio.Jdt.Tests/JsonTransformationTestLogger.cs b/src/Microsoft.VisualStudio.Jdt.Tests/JsonTransformationTestLogger.cs new file mode 100644 index 00000000..466fb8bd --- /dev/null +++ b/src/Microsoft.VisualStudio.Jdt.Tests/JsonTransformationTestLogger.cs @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.Jdt.Tests +{ + using System; + using System.Collections.Generic; + + /// + /// Mock logger to test + /// + public class JsonTransformationTestLogger : IJsonTransformationLogger + { + /// + /// Gets the error log + /// + public List ErrorLog { get; } = new List(); + + /// + /// Gets the warning log + /// + public List WarningLog { get; } = new List(); + + /// + /// Gets the message log + /// + public List MessageLog { get; } = new List(); + + /// + public void LogError(string message) + { + this.ErrorLog.Add(new TestLogEntry() + { + Message = message, + FromException = false + }); + } + + /// + public void LogError(string message, string fileName, int lineNumber, int linePosition) + { + this.ErrorLog.Add(new TestLogEntry() + { + Message = message, + FileName = fileName, + LineNumber = lineNumber, + LinePosition = linePosition, + FromException = false + }); + } + + /// + public void LogErrorFromException(Exception ex) + { + this.ErrorLog.Add(new TestLogEntry() + { + Message = ex.Message, + FromException = false + }); + } + + /// + public void LogErrorFromException(Exception ex, string fileName, int lineNumber, int linePosition) + { + this.ErrorLog.Add(new TestLogEntry() + { + Message = ex.Message, + FileName = fileName, + LineNumber = lineNumber, + LinePosition = linePosition, + FromException = true + }); + } + + /// + public void LogMessage(string message) + { + this.MessageLog.Add(new TestLogEntry() + { + Message = message, + FromException = false + }); + } + + /// + public void LogMessage(string message, string fileName, int lineNumber, int linePosition) + { + this.MessageLog.Add(new TestLogEntry() + { + Message = message, + FileName = fileName, + LineNumber = lineNumber, + LinePosition = linePosition, + FromException = false + }); + } + + /// + public void LogWarning(string message) + { + this.WarningLog.Add(new TestLogEntry() + { + Message = message, + FromException = false + }); + } + + /// + public void LogWarning(string message, string fileName) + { + this.WarningLog.Add(new TestLogEntry() + { + Message = message, + FileName = fileName, + LineNumber = 0, + LinePosition = 0, + FromException = false + }); + } + + /// + public void LogWarning(string message, string fileName, int lineNumber, int linePosition) + { + this.WarningLog.Add(new TestLogEntry() + { + Message = message, + FileName = fileName, + LineNumber = lineNumber, + LinePosition = linePosition, + FromException = false + }); + } + + /// + /// An test entry for the logger. + /// Corresponds to an error, warning or message + /// + public struct TestLogEntry + { + /// + /// Gets or sets the log message + /// + public string Message { get; set; } + + /// + /// Gets or sets the file that caused the entry + /// + public string FileName { get; set; } + + /// + /// Gets or sets the line in the file + /// + public int LineNumber { get; set; } + + /// + /// Gets or sets the position in the line + /// + public int LinePosition { get; set; } + + /// + /// Gets or sets a value indicating whether whether the entry was caused from an exception + /// + public bool FromException { get; set; } + } + } +} diff --git a/src/Microsoft.VisualStudio.Jdt/JdtExtensions.cs b/src/Microsoft.VisualStudio.Jdt/JdtExtensions.cs index a764b0f3..d95aa3e2 100644 --- a/src/Microsoft.VisualStudio.Jdt/JdtExtensions.cs +++ b/src/Microsoft.VisualStudio.Jdt/JdtExtensions.cs @@ -45,5 +45,23 @@ internal static bool IsCriticalException(this Exception ex) #endif ; } + + /// + /// Clones a preserving the line information + /// + /// The object to clone + /// A clone of the object with its line info + internal static JObject CloneWithLineInfo(this JObject objectToClone) + { + var loadSettings = new JsonLoadSettings() + { + LineInfoHandling = LineInfoHandling.Load + }; + + using (var objectReader = objectToClone.CreateReader()) + { + return JObject.Load(objectReader, loadSettings); + } + } } } diff --git a/src/Microsoft.VisualStudio.Jdt/JsonTransformation.cs b/src/Microsoft.VisualStudio.Jdt/JsonTransformation.cs index b2def301..a4938c43 100644 --- a/src/Microsoft.VisualStudio.Jdt/JsonTransformation.cs +++ b/src/Microsoft.VisualStudio.Jdt/JsonTransformation.cs @@ -162,10 +162,7 @@ private void SetTransform(Stream transformStream) this.loadSettings = new JsonLoadSettings() { CommentHandling = CommentHandling.Ignore, - - // Obs: LineInfo is handled on Ignore and not Load - // See https://github.com/JamesNK/Newtonsoft.Json/issues/1249 - LineInfoHandling = LineInfoHandling.Ignore + LineInfoHandling = LineInfoHandling.Load }; using (StreamReader transformStreamReader = new StreamReader(transformStream)) diff --git a/src/Microsoft.VisualStudio.Jdt/Processors/JdtProcessor.cs b/src/Microsoft.VisualStudio.Jdt/Processors/JdtProcessor.cs index ee51422a..68138f53 100644 --- a/src/Microsoft.VisualStudio.Jdt/Processors/JdtProcessor.cs +++ b/src/Microsoft.VisualStudio.Jdt/Processors/JdtProcessor.cs @@ -70,7 +70,7 @@ internal static void ProcessTransform(JObject source, JObject transform, JsonTra } // Passes in a clone of the transform object because it can be altered during the transformation process - ProcessorChain.Start(source, (JObject)transform.DeepClone(), logger); + ProcessorChain.Start(source, (JObject)transform.CloneWithLineInfo(), logger); } ///