From 219d6df0f2189ca1651978a1076bfe642f9705c5 Mon Sep 17 00:00:00 2001 From: Tareq Imbasher Date: Sun, 9 Jun 2024 17:20:04 +0300 Subject: [PATCH] Unit tests for SourceCodeElements --- .../EntityFrameworkDatabaseScaffolder.cs | 6 +- .../CodeAnalysis/CodeAnalysisExtensions.cs | 17 +++++ src/Core/NetPad.Runtime/DotNet/Code.cs | 10 +-- src/Core/NetPad.Runtime/DotNet/Namespace.cs | 50 +++++--------- src/Core/NetPad.Runtime/DotNet/SourceCode.cs | 35 ++++------ .../DotNet/SourceCodeElement.cs | 18 ++--- src/Core/NetPad.Runtime/DotNet/Using.cs | 47 +++++--------- src/Core/NetPad.Runtime/ValueObject.cs | 50 -------------- .../NetPad.Runtime.Tests/DotNet/CodeTests.cs | 52 +++++++++++++++ .../DotNet/NamespaceTests.cs | 57 ++++++++++++++++ .../DotNet/SourceCodeTests.cs | 47 ++++++++++++++ .../NetPad.Runtime.Tests/DotNet/UsingTests.cs | 65 +++++++++++++++++++ 12 files changed, 288 insertions(+), 166 deletions(-) create mode 100644 src/Core/NetPad.Runtime/CodeAnalysis/CodeAnalysisExtensions.cs delete mode 100644 src/Core/NetPad.Runtime/ValueObject.cs create mode 100644 src/Tests/NetPad.Runtime.Tests/DotNet/CodeTests.cs create mode 100644 src/Tests/NetPad.Runtime.Tests/DotNet/NamespaceTests.cs create mode 100644 src/Tests/NetPad.Runtime.Tests/DotNet/SourceCodeTests.cs create mode 100644 src/Tests/NetPad.Runtime.Tests/DotNet/UsingTests.cs diff --git a/src/Apps/NetPad.Apps.Common/Data/EntityFrameworkCore/Scaffolding/EntityFrameworkDatabaseScaffolder.cs b/src/Apps/NetPad.Apps.Common/Data/EntityFrameworkCore/Scaffolding/EntityFrameworkDatabaseScaffolder.cs index 72a905c9..2ce518aa 100644 --- a/src/Apps/NetPad.Apps.Common/Data/EntityFrameworkCore/Scaffolding/EntityFrameworkDatabaseScaffolder.cs +++ b/src/Apps/NetPad.Apps.Common/Data/EntityFrameworkCore/Scaffolding/EntityFrameworkDatabaseScaffolder.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Logging; using NetPad.Apps.Data.EntityFrameworkCore.DataConnections; using NetPad.Apps.Data.EntityFrameworkCore.Scaffolding.Transforms; +using NetPad.CodeAnalysis; using NetPad.Configuration; using NetPad.Data; using NetPad.DotNet; @@ -239,10 +240,7 @@ private async Task ParseScaffoldedSourceFileAsync(FileInfo var usings = nodes .OfType() - .Select(u => string.Join( - ' ', - u.NormalizeWhitespace().ChildNodes().Select(x => x.ToFullString())) - ) + .Select(u => u.GetNamespaceString()) .ToArray(); var classDeclaration = nodes.OfType().Single(); diff --git a/src/Core/NetPad.Runtime/CodeAnalysis/CodeAnalysisExtensions.cs b/src/Core/NetPad.Runtime/CodeAnalysis/CodeAnalysisExtensions.cs new file mode 100644 index 00000000..aa045f2c --- /dev/null +++ b/src/Core/NetPad.Runtime/CodeAnalysis/CodeAnalysisExtensions.cs @@ -0,0 +1,17 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace NetPad.CodeAnalysis; + +public static class CodeAnalysisExtensions +{ + /// + /// Gets the namespace value specified by the using directive. + /// + public static string GetNamespaceString(this UsingDirectiveSyntax node) + { + return string.Join(' ', + node.ChildNodes() + .Select(x => x.NormalizeWhitespace().ToFullString())); + } +} diff --git a/src/Core/NetPad.Runtime/DotNet/Code.cs b/src/Core/NetPad.Runtime/DotNet/Code.cs index 658a11ec..ae6664eb 100644 --- a/src/Core/NetPad.Runtime/DotNet/Code.cs +++ b/src/Core/NetPad.Runtime/DotNet/Code.cs @@ -4,14 +4,12 @@ namespace NetPad.DotNet; [method: JsonConstructor] -public class Code(Namespace? @namespace, string? value) : SourceCodeElement(value) +public record Code(Namespace? Namespace, string? Value) : SourceCodeElement(Value) { public Code(string? value) : this(null, value) { } - public Namespace? Namespace { get; } = @namespace; - public override bool ValueChanged() { return _valueChanged || (Namespace != null && Namespace.ValueChanged()); @@ -33,10 +31,4 @@ public override string ToCodeString() return sb.ToString(); } - - protected override IEnumerable GetEqualityComponents() - { - yield return base.GetEqualityComponents(); - yield return Namespace; - } } diff --git a/src/Core/NetPad.Runtime/DotNet/Namespace.cs b/src/Core/NetPad.Runtime/DotNet/Namespace.cs index bed67a2f..e7899f03 100644 --- a/src/Core/NetPad.Runtime/DotNet/Namespace.cs +++ b/src/Core/NetPad.Runtime/DotNet/Namespace.cs @@ -1,10 +1,9 @@ namespace NetPad.DotNet; -public class Namespace : SourceCodeElement +public record Namespace : SourceCodeElement { - public Namespace(string value) : base(value) + public Namespace(string value) : base(Normalize(value)) { - Validate(value); } public override string ToCodeString() => $"namespace {Value};"; @@ -14,54 +13,35 @@ public Namespace(string value) : base(value) return new Namespace(value); } - public override bool Equals(object? obj) - { - if (obj is string str) - return Value == str; - - return base.Equals(obj); - } - - public override int GetHashCode() => base.GetHashCode(); - - public static void Validate(string value) + public static string Normalize(string value) { if (string.IsNullOrWhiteSpace(value)) + { throw new ArgumentException("Cannot be null or whitespace", nameof(value)); + } if (value.StartsWith(' ') || value.EndsWith(' ')) + { value = value.Trim(); + } if (value.StartsWith("namespace")) + { throw new ArgumentException("Cannot start with the keyword 'namespace'", nameof(value)); + } - char firstChar = value.First(); + char firstChar = value[0]; if (!char.IsLetter(firstChar) && firstChar != '_') + { throw new ArgumentException("Must start with a letter or an underscore", nameof(value)); + } if (value.EndsWith(";")) + { throw new ArgumentException("Cannot end with a semi-colon", nameof(value)); + } - if (value.Contains(' ')) - throw new ArgumentException("Cannot contain spaces", nameof(value)); - } - - public static Namespace Parse(string text) - { - if (string.IsNullOrWhiteSpace(text)) - return new Namespace(text); - - if (text.StartsWith(' ') || text.EndsWith(' ')) - text = text.Trim(); - - if (text.StartsWith("namespace ")) - text = text["namespace".Length..]; - - int ixInvalidChar = Array.FindIndex(text.ToCharArray(), c => !char.IsLetter(c) && c != '_'); - if (ixInvalidChar >= 0) - text = text[..ixInvalidChar]; - - return text; + return value.ReplaceLineEndings(string.Empty); } } diff --git a/src/Core/NetPad.Runtime/DotNet/SourceCode.cs b/src/Core/NetPad.Runtime/DotNet/SourceCode.cs index fb186a22..98b0b681 100644 --- a/src/Core/NetPad.Runtime/DotNet/SourceCode.cs +++ b/src/Core/NetPad.Runtime/DotNet/SourceCode.cs @@ -3,45 +3,37 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using NetPad.CodeAnalysis; namespace NetPad.DotNet; -[method: JsonConstructor] -public class SourceCode(Code? code, IEnumerable? usings = null) +public class SourceCode { - private readonly HashSet _usings = usings?.ToHashSet() ?? new HashSet(); + private readonly HashSet _usings; private bool _valueChanged; - public SourceCode() : this(null, Array.Empty()) - { - } - - public SourceCode(IEnumerable usings) : this(null, usings) - { - } - - public SourceCode(params string[] usings) : this(null, usings) + [JsonConstructor] + public SourceCode(Code? code, IEnumerable? usings = null) { + Code = code ?? new Code(null); + _usings = usings?.ToHashSet() ?? []; } public SourceCode(IEnumerable usings) : this(null, usings) { } - public SourceCode(params Using[] usings) : this(null, usings) + public SourceCode(string? code, IEnumerable? usings = null) + : this(code == null ? null : new Code(code), usings?.Select(u => new Using(u))) { } - public SourceCode(string? code, IEnumerable? usings = null) - : this( - code == null ? null : new Code(code), - usings?.Select(u => new Using(u)) - ) + public SourceCode(IEnumerable usings) : this(null, usings) { } public IEnumerable Usings => _usings; - public Code Code { get; } = code ?? new Code(null, null); + public Code Code { get; } public bool ValueChanged() => _valueChanged || Code.ValueChanged() || _usings.Any(u => u.ValueChanged()); public void AddUsing(string @using) @@ -82,10 +74,7 @@ public static SourceCode Parse(string text) .ToArray(); var usings = usingDirectives - .Select(u => string.Join( - ' ', - u.NormalizeWhitespace().ChildNodes().Select(x => x.ToFullString())) - ) + .Select(u => u.GetNamespaceString()) .ToArray(); var code = root diff --git a/src/Core/NetPad.Runtime/DotNet/SourceCodeElement.cs b/src/Core/NetPad.Runtime/DotNet/SourceCodeElement.cs index ed8c2378..811660d8 100644 --- a/src/Core/NetPad.Runtime/DotNet/SourceCodeElement.cs +++ b/src/Core/NetPad.Runtime/DotNet/SourceCodeElement.cs @@ -1,26 +1,18 @@ namespace NetPad.DotNet; -public abstract class SourceCodeElement : ValueObject +public abstract record SourceCodeElement(TValue Value) { protected bool _valueChanged; + public TValue Value { get; private set; } = Value; + public virtual bool ValueChanged() => _valueChanged; public abstract string ToCodeString(); -} - -public abstract class SourceCodeElement(TValue value) : SourceCodeElement -{ - public TValue Value { get; private set; } = value; - public void Update(TValue value) + public void Update(TValue newValue) { - Value = value; + Value = newValue; _valueChanged = true; } - - protected override IEnumerable GetEqualityComponents() - { - yield return Value; - } } diff --git a/src/Core/NetPad.Runtime/DotNet/Using.cs b/src/Core/NetPad.Runtime/DotNet/Using.cs index d31b2682..e2b803e0 100644 --- a/src/Core/NetPad.Runtime/DotNet/Using.cs +++ b/src/Core/NetPad.Runtime/DotNet/Using.cs @@ -1,10 +1,9 @@ namespace NetPad.DotNet; -public class Using : SourceCodeElement +public record Using : SourceCodeElement { - public Using(string value) : base(value) + public Using(string value) : base(Normalize(value)) { - Validate(value); } public override string ToCodeString() => ToCodeString(false); @@ -15,51 +14,35 @@ public Using(string value) : base(value) return new Using(value); } - public override bool Equals(object? obj) - { - if (obj is string str) - return Value == str; - - return base.Equals(obj); - } - - public override int GetHashCode() => base.GetHashCode(); - - public static void Validate(string value) + public static string Normalize(string value) { if (string.IsNullOrWhiteSpace(value)) + { throw new ArgumentException("Cannot be null or whitespace", nameof(value)); + } if (value.StartsWith(' ') || value.EndsWith(' ')) + { value = value.Trim(); + } if (value.StartsWith("using")) + { throw new ArgumentException("Cannot start with the keyword 'using'", nameof(value)); + } - char firstChar = value.First(); + char firstChar = value[0]; if (!char.IsLetter(firstChar) && firstChar != '_') + { throw new ArgumentException("Must start with a letter or an underscore", nameof(value)); + } if (value.EndsWith(";")) + { throw new ArgumentException("Cannot end with a semi-colon", nameof(value)); - } - - public static Using Parse(string text) - { - if (string.IsNullOrWhiteSpace(text)) - return new Using(text); - - if (text.StartsWith(' ') || text.EndsWith(' ')) - text = text.Trim(); - - if (text.StartsWith("using ")) - text = text["using".Length..]; - - int ixInvalidChar = Array.FindIndex(text.ToCharArray(), c => !char.IsLetter(c) && c != '_'); - if (ixInvalidChar >= 0) - text = text[..ixInvalidChar]; + } - return text; + return value.ReplaceLineEndings(string.Empty); } } diff --git a/src/Core/NetPad.Runtime/ValueObject.cs b/src/Core/NetPad.Runtime/ValueObject.cs deleted file mode 100644 index 76cb07b0..00000000 --- a/src/Core/NetPad.Runtime/ValueObject.cs +++ /dev/null @@ -1,50 +0,0 @@ -namespace NetPad; - -public abstract class ValueObject -{ - protected static bool EqualOperator(ValueObject? left, ValueObject? right) - { - if (ReferenceEquals(left, null) ^ ReferenceEquals(right, null)) - { - return false; - } - - return ReferenceEquals(left, right) || left?.Equals(right) == true; - } - - protected static bool NotEqualOperator(ValueObject? left, ValueObject? right) - { - return !EqualOperator(left, right); - } - - public static bool operator ==(ValueObject? one, ValueObject? two) - { - return EqualOperator(one, two); - } - - public static bool operator !=(ValueObject? one, ValueObject? two) - { - return NotEqualOperator(one, two); - } - - protected abstract IEnumerable GetEqualityComponents(); - - public override bool Equals(object? obj) - { - if (obj == null || obj.GetType() != GetType()) - { - return false; - } - - var other = (ValueObject)obj; - - return GetEqualityComponents().SequenceEqual(other.GetEqualityComponents()); - } - - public override int GetHashCode() - { - return GetEqualityComponents() - .Select(x => x != null ? x.GetHashCode() : 0) - .Aggregate((x, y) => x ^ y); - } -} diff --git a/src/Tests/NetPad.Runtime.Tests/DotNet/CodeTests.cs b/src/Tests/NetPad.Runtime.Tests/DotNet/CodeTests.cs new file mode 100644 index 00000000..b0811f03 --- /dev/null +++ b/src/Tests/NetPad.Runtime.Tests/DotNet/CodeTests.cs @@ -0,0 +1,52 @@ +using NetPad.DotNet; +using Xunit; + +namespace NetPad.Runtime.Tests.DotNet; + +public class CodeTests +{ + [Fact] + public void CodeEquality() + { + var code1 = new Code("Console.WriteLine();"); + var code2 = new Code("Console.WriteLine();"); + + Assert.Equal(code1, code2); + } + + [Fact] + public void CodeEquality_HashSet() + { + var set = new HashSet(); + + set.Add(new Code("Console.WriteLine();")); + set.Add(new Code("Console.WriteLine();")); + + Assert.Single(set); + } + + [Fact] + public void UpdateValue_UpdatesValueChanged() + { + var code = new Code("Console.WriteLine();"); + + Assert.False(code.ValueChanged()); + + code.Update("Console.ReadLine();"); + + Assert.True(code.ValueChanged()); + } + + [Fact] + public void ToCodeString() + { + var nsString = "Cool.App"; + var codeString = $"Console.WriteLine(\"Do?\");{Environment.NewLine}Console.ReadLine();"; + + var code = new Code(new Namespace(nsString), codeString); + + Assert.Equal( + $"namespace {nsString};{Environment.NewLine}{Environment.NewLine}{codeString}{Environment.NewLine}", + code.ToCodeString()); + } +} diff --git a/src/Tests/NetPad.Runtime.Tests/DotNet/NamespaceTests.cs b/src/Tests/NetPad.Runtime.Tests/DotNet/NamespaceTests.cs new file mode 100644 index 00000000..29a12d71 --- /dev/null +++ b/src/Tests/NetPad.Runtime.Tests/DotNet/NamespaceTests.cs @@ -0,0 +1,57 @@ +using NetPad.DotNet; +using Xunit; + +namespace NetPad.Runtime.Tests.DotNet; + +public class NamespaceTests +{ + [Fact] + public void NamespaceEquality() + { + var ns1 = new Namespace("System.Text"); + var ns2 = new Namespace("System.Text"); + + Assert.Equal(ns1, ns2); + } + + [Fact] + public void NamespaceEquality_HashSet() + { + var set = new HashSet(); + + set.Add(new Namespace("System.Text")); + set.Add(new Namespace("System.Text")); + + Assert.Single(set); + } + + [Fact] + public void UpdateValue_UpdatesValueChanged() + { + var ns = new Namespace("System.Text"); + + Assert.False(ns.ValueChanged()); + + ns.Update("System"); + + Assert.True(ns.ValueChanged()); + } + + [Fact] + public void ToCodeString() + { + var ns = new Namespace("System.Text"); + + Assert.Equal("namespace System.Text;", ns.ToCodeString()); + } + + [Fact] + public void Implicit_String() + { + Namespace @explicit = new Namespace("System.Text"); + + Namespace @implicit = "System.Text"; + + Assert.Equal(@explicit, @implicit); + } +} diff --git a/src/Tests/NetPad.Runtime.Tests/DotNet/SourceCodeTests.cs b/src/Tests/NetPad.Runtime.Tests/DotNet/SourceCodeTests.cs new file mode 100644 index 00000000..5bbde1ae --- /dev/null +++ b/src/Tests/NetPad.Runtime.Tests/DotNet/SourceCodeTests.cs @@ -0,0 +1,47 @@ +using NetPad.DotNet; +using Xunit; + +namespace NetPad.Runtime.Tests.DotNet; + +public class SourceCodeTests +{ + private const string Code = """ + using System; + using System.Text.Json; + using System.Text + .Json; + using System.Threading + .Tasks; + using enc = System.Text.Encoding; + using enc2 = System.Text + .Encoding; + + namespace MyApp.Utils; + + public class Car + { + public string Name { get; } + } + + public enum Color + { + Red, Blue, Green + } + """; + + [Fact] + public void ParsesUsingsCorrectly() + { + var sourceCode = SourceCode.Parse(Code); + + Assert.Equal( + [ + "System", + "System.Text.Json", + "System.Threading.Tasks", + "enc = System.Text.Encoding", + "enc2 = System.Text.Encoding", + ], + sourceCode.Usings.Select(x => x.Value)); + } +} diff --git a/src/Tests/NetPad.Runtime.Tests/DotNet/UsingTests.cs b/src/Tests/NetPad.Runtime.Tests/DotNet/UsingTests.cs new file mode 100644 index 00000000..7c44a067 --- /dev/null +++ b/src/Tests/NetPad.Runtime.Tests/DotNet/UsingTests.cs @@ -0,0 +1,65 @@ +using NetPad.DotNet; +using Xunit; + +namespace NetPad.Runtime.Tests.DotNet; + +public class UsingTests +{ + [Fact] + public void UsingEquality() + { + var using1 = new Using("System.Text"); + var using2 = new Using("System.Text"); + + Assert.Equal(using1, using2); + } + + [Fact] + public void UsingEquality_HashSet() + { + var set = new HashSet(); + + set.Add(new Using("System.Text")); + set.Add(new Using("System.Text")); + + Assert.Single(set); + } + + [Fact] + public void UpdateValue_UpdatesValueChanged() + { + var using1 = new Using("System.Text"); + + Assert.False(using1.ValueChanged()); + + using1.Update("System"); + + Assert.True(using1.ValueChanged()); + } + + [Fact] + public void ToCodeString() + { + var using1 = new Using("System.Text"); + + Assert.Equal("using System.Text;", using1.ToCodeString()); + } + + [Fact] + public void ToCodeString_UsingGlobalAnnotation() + { + var using1 = new Using("System.Text"); + + Assert.Equal("global using System.Text;", using1.ToCodeString(true)); + } + + [Fact] + public void Implicit_String() + { + Using @explicit = new Using("System.Text"); + + Using @implicit = "System.Text"; + + Assert.Equal(@explicit, @implicit); + } +}