diff --git a/OpenTabletDriver.Tests/ConfigurationTest.cs b/OpenTabletDriver.Tests/ConfigurationTest.cs index 610bca38c..b6f557509 100644 --- a/OpenTabletDriver.Tests/ConfigurationTest.cs +++ b/OpenTabletDriver.Tests/ConfigurationTest.cs @@ -1,10 +1,13 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Runtime.InteropServices; +using System.Text; using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Newtonsoft.Json.Schema; using Newtonsoft.Json.Schema.Generation; @@ -300,6 +303,85 @@ public void Configurations_Verify_Configs_With_Schema() Assert.False(failed); } + /// + /// Ensures that configuration formatting/linting matches expectations, which are: + /// - 2 space indentation + /// - Newline at end of file + /// - Consistent newline format + /// + [Fact] + public void Configurations_Are_Linted() + { + const int maxLinesToOutput = 3; + + var serializer = new JsonSerializer(); + var failedFiles = 0; + + var ourJsonSb = new StringBuilder(); + using var strw = new StringWriter(ourJsonSb); + using var jtw = new JsonTextWriter(strw); + jtw.Formatting = Formatting.Indented; + jtw.Indentation = 2; + + foreach (var (tabletFilename, theirJson) in ConfigFiles) + { + ourJsonSb.Clear(); + var ourJsonObj = JsonConvert.DeserializeObject(theirJson); + + serializer.Serialize(jtw, ourJsonObj); + ourJsonSb.AppendLine(); // otherwise we won't have an EOL at EOF + + var ourJson = ourJsonSb.ToString(); + + var failedLines = DoesJsonMatch(ourJson, theirJson); + + if (failedLines.Any() || !string.Equals(theirJson, ourJson)) // second check ensures EOL markers are equivalent + { + failedFiles++; + _testOutputHelper.WriteLine( + $"- Tablet Configuration '{tabletFilename}' lint check failed with the following errors:"); + + foreach (var (line, error) in failedLines.Take(maxLinesToOutput)) + _testOutputHelper.WriteLine($" Line {line}: {error}"); + if (failedLines.Count > maxLinesToOutput) + _testOutputHelper.WriteLine($" Truncated an additional {failedLines.Count - maxLinesToOutput} mismatching lines - wrong indent?"); + else if (failedLines.Count == 0) + _testOutputHelper.WriteLine(" Generic mismatch (line endings?)"); + } + } + + Assert.Equal(0, failedFiles); + } + + private static IList<(int, string)> DoesJsonMatch(string ourJson, string theirJson) + { + int line = 0; + var rv = new List<(int, string)>(); + + using var ourSr = new StringReader(ourJson); + using var theirSr = new StringReader(theirJson); + while (true) + { + var ourLine = ourSr.ReadLine(); + var theirLine = theirSr.ReadLine(); + line++; + + if (ourLine == null && theirLine == null) + break; // success for file + + var ourLineOutput = ourLine ?? "EOF"; + var theirLineOutput = theirLine ?? "EOF"; + + if (ourLine == null || theirLine == null || !string.Equals(ourLine, theirLine)) + rv.Add((line, $"Expected '{ourLineOutput}' got '{theirLineOutput}'")); + + if (ourLine == null || theirLine == null) + break; + } + + return rv; + } + private static void DisallowAdditionalItemsAndProperties(JSchema schema) { schema.AllowAdditionalItems = false; diff --git a/OpenTabletDriver/Tablet/DigitizerSpecifications.cs b/OpenTabletDriver/Tablet/DigitizerSpecifications.cs index 74ddc6146..bdf171caa 100644 --- a/OpenTabletDriver/Tablet/DigitizerSpecifications.cs +++ b/OpenTabletDriver/Tablet/DigitizerSpecifications.cs @@ -1,6 +1,9 @@ +using System; using System.ComponentModel; using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; using JetBrains.Annotations; +using Newtonsoft.Json; namespace OpenTabletDriver.Tablet { @@ -14,18 +17,21 @@ public class DigitizerSpecifications /// The width of the digitizer in millimeters. /// [Required(ErrorMessage = $"Digitizer ${nameof(Width)} must be defined")] + [JsonConverter(typeof(DecimalJsonConverter))] public float Width { set; get; } /// /// The height of the digitizer in millimeters. /// [Required(ErrorMessage = $"Digitizer ${nameof(Height)} must be defined")] + [JsonConverter(typeof(DecimalJsonConverter))] public float Height { set; get; } /// /// The maximum X coordinate for the digitizer. /// [Required(ErrorMessage = $"Digitizer ${nameof(MaxX)} must be defined")] + [JsonConverter(typeof(DecimalJsonConverter))] [DisplayName("Max X")] public float MaxX { set; get; } @@ -33,7 +39,43 @@ public class DigitizerSpecifications /// The maximum Y coordinate for the digitizer. /// [Required(ErrorMessage = $"Digitizer ${nameof(MaxY)} must be defined")] + [JsonConverter(typeof(DecimalJsonConverter))] [DisplayName("Max Y")] public float MaxY { set; get; } } + + public class DecimalJsonConverter : JsonConverter + { + public override bool CanRead => false; + public override bool CanWrite => true; + + public override bool CanConvert(Type objectType) + { + return objectType == typeof(double); + } + + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + if (value == null) + throw new ArgumentNullException(); + + writer.WriteRawValue(IsWholeValue(value) + ? JsonConvert.ToString(Convert.ToInt64(value)) + : JsonConvert.ToString(value)); + } + + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + + [SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator")] + private static bool IsWholeValue(object value) + { + if (value is float floatValue) + return floatValue == Math.Truncate(floatValue); + + return false; + } + } }