From 25e6c059e9aabdd7d5736954d83ae81fba972c7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A1s=20B=20Nagy?= <20251272+BNAndras@users.noreply.github.com> Date: Sat, 22 Aug 2026 08:21:26 -0700 Subject: [PATCH 1/5] Port VB test suite generator --- generators/AssemblyInfo.vb | 3 + generators/CanonicalData.vb | 51 +++ generators/Exercises.vb | 53 +++ generators/Formatting.vb | 29 ++ .../CanonicalDataParserTests.vb | 22 ++ .../Generators.Tests/Generators.Tests.vbproj | 29 ++ generators/Generators.Tests/TemplatesTests.vb | 94 ++++++ .../TestCasesConfigurationTests.vb | 98 ++++++ .../Generators.Tests/packages.lock.json | 306 ++++++++++++++++++ generators/Generators.sln | 28 ++ generators/Generators.vbproj | 36 +++ generators/Naming.vb | 27 ++ generators/Paths.vb | 43 +++ generators/ProbSpecs.vb | 26 ++ generators/Program.vb | 55 ++++ generators/TemplateGenerator.vb | 87 +++++ generators/Templates.vb | 156 +++++++++ generators/TestCasesConfiguration.vb | 82 +++++ generators/TestsGenerator.vb | 30 ++ generators/packages.lock.json | 140 ++++++++ 20 files changed, 1395 insertions(+) create mode 100644 generators/AssemblyInfo.vb create mode 100644 generators/CanonicalData.vb create mode 100644 generators/Exercises.vb create mode 100644 generators/Formatting.vb create mode 100644 generators/Generators.Tests/CanonicalDataParserTests.vb create mode 100644 generators/Generators.Tests/Generators.Tests.vbproj create mode 100644 generators/Generators.Tests/TemplatesTests.vb create mode 100644 generators/Generators.Tests/TestCasesConfigurationTests.vb create mode 100644 generators/Generators.Tests/packages.lock.json create mode 100644 generators/Generators.sln create mode 100644 generators/Generators.vbproj create mode 100644 generators/Naming.vb create mode 100644 generators/Paths.vb create mode 100644 generators/ProbSpecs.vb create mode 100644 generators/Program.vb create mode 100644 generators/TemplateGenerator.vb create mode 100644 generators/Templates.vb create mode 100644 generators/TestCasesConfiguration.vb create mode 100644 generators/TestsGenerator.vb create mode 100644 generators/packages.lock.json diff --git a/generators/AssemblyInfo.vb b/generators/AssemblyInfo.vb new file mode 100644 index 0000000..6cd319f --- /dev/null +++ b/generators/AssemblyInfo.vb @@ -0,0 +1,3 @@ +Imports System.Runtime.CompilerServices + + diff --git a/generators/CanonicalData.vb b/generators/CanonicalData.vb new file mode 100644 index 0000000..460cfea --- /dev/null +++ b/generators/CanonicalData.vb @@ -0,0 +1,51 @@ +Imports System.Collections.Immutable + +Namespace Global.Exercism.VBNet.Generators + Friend NotInheritable Class CanonicalData + Friend Sub New(exercise As Exercise, testCases As JsonNode()) + Me.Exercise = exercise + Me.TestCases = testCases + End Sub + + Friend ReadOnly Property Exercise As Exercise + Friend ReadOnly Property TestCases As JsonNode() + End Class + + Friend Module CanonicalDataParser + Friend Function Parse(exercise As Exercise) As CanonicalData + Dim root = JsonNode.Parse(File.ReadAllText(Paths.CanonicalDataFile(exercise))) + + If root Is Nothing Then + Throw New InvalidDataException($"Canonical data for '{exercise.Slug}' is empty.") + End If + + Return New CanonicalData(exercise, ParseTestCases(root)) + End Function + + Friend Function ParseTestCases(root As JsonNode) As JsonNode() + Return ParseTestCases(root, ImmutableQueue(Of String).Empty).ToArray() + End Function + + Private Iterator Function ParseTestCases(node As JsonNode, path As ImmutableQueue(Of String)) As IEnumerable(Of JsonNode) + Dim updatedPath = path + Dim description = node("description") + + If description IsNot Nothing Then + updatedPath = updatedPath.Enqueue(description.GetValue(Of String)()) + End If + + Dim cases = node("cases") + + If cases IsNot Nothing Then + For Each child In cases.AsArray() + For Each testCase In ParseTestCases(child, updatedPath) + Yield testCase + Next + Next + Else + node("path") = JsonSerializer.SerializeToNode(updatedPath) + Yield node + End If + End Function + End Module +End Namespace diff --git a/generators/Exercises.vb b/generators/Exercises.vb new file mode 100644 index 0000000..cfb98a1 --- /dev/null +++ b/generators/Exercises.vb @@ -0,0 +1,53 @@ +Namespace Global.Exercism.VBNet.Generators + Friend NotInheritable Class Exercise + Friend Sub New(slug As String, name As String) + Me.Slug = slug + Me.Name = name + End Sub + + Friend ReadOnly Property Slug As String + Friend ReadOnly Property Name As String + End Class + + Friend Module Exercises + Friend Function Templated(Optional slug As String = Nothing) As List(Of Exercise) + Return Find(slug, desiredTemplateState:=True) + End Function + + Friend Function Untemplated(Optional slug As String = Nothing) As List(Of Exercise) + Return Find(slug, desiredTemplateState:=False) + End Function + + Private Function Find(slug As String, desiredTemplateState As Boolean) As List(Of Exercise) + Return Parse(). + Where(Function(exercise) slug Is Nothing OrElse exercise.Slug = slug). + Where(AddressOf HasCanonicalData). + Where(Function(exercise) desiredTemplateState = HasTemplate(exercise)). + ToList() + End Function + + Private Iterator Function Parse() As IEnumerable(Of Exercise) + Using document = JsonDocument.Parse(File.ReadAllText(Paths.TrackConfigFile)) + Dim slugs = document.RootElement. + GetProperty("exercises"). + GetProperty("practice"). + EnumerateArray(). + Select(Function(exercise) exercise.GetProperty("slug").GetString()). + Where(Function(slug) slug IsNot Nothing). + OrderBy(Function(slug) slug, StringComparer.Ordinal) + + For Each slug In slugs + Yield New Exercise(slug, slug.Dehumanize()) + Next + End Using + End Function + + Private Function HasCanonicalData(exercise As Exercise) As Boolean + Return File.Exists(Paths.CanonicalDataFile(exercise)) + End Function + + Private Function HasTemplate(exercise As Exercise) As Boolean + Return File.Exists(Paths.TemplateFile(exercise)) + End Function + End Module +End Namespace diff --git a/generators/Formatting.vb b/generators/Formatting.vb new file mode 100644 index 0000000..76ff17f --- /dev/null +++ b/generators/Formatting.vb @@ -0,0 +1,29 @@ +Imports Microsoft.CodeAnalysis +Imports Microsoft.CodeAnalysis.Formatting +Imports Microsoft.CodeAnalysis.VisualBasic + +Namespace Global.Exercism.VBNet.Generators + Friend Module Formatting + Private ReadOnly Workspace As New AdhocWorkspace() + + Friend Function FormatCode(code As String) As String + Dim syntaxTree = VisualBasicSyntaxTree.ParseText(code) + Dim errors = syntaxTree.GetDiagnostics(). + Where(Function(diagnostic) diagnostic.Severity = DiagnosticSeverity.Error). + ToArray() + + If errors.Length > 0 Then + Throw New InvalidDataException($"Generated Visual Basic contains syntax errors:{Environment.NewLine}{String.Join(Environment.NewLine, errors.Select(Function(errorDiagnostic) errorDiagnostic.ToString()))}") + End If + + Dim root = syntaxTree.GetRoot().WithoutLeadingTrivia() + Dim formatted = Formatter.Format(root, Workspace).ToFullString() + Return NormalizeLineEndings(formatted) + End Function + + Private Function NormalizeLineEndings(value As String) As String + Dim normalized = value.Replace(vbCrLf, vbLf).Replace(vbCr, vbLf) + Return normalized.TrimEnd(ControlChars.Cr, ControlChars.Lf) & vbLf + End Function + End Module +End Namespace diff --git a/generators/Generators.Tests/CanonicalDataParserTests.vb b/generators/Generators.Tests/CanonicalDataParserTests.vb new file mode 100644 index 0000000..2c8c8dd --- /dev/null +++ b/generators/Generators.Tests/CanonicalDataParserTests.vb @@ -0,0 +1,22 @@ +Namespace Global.Exercism.VBNet.Generators + Public Class CanonicalDataParserTests + + Public Sub Flattens_nested_cases_in_order_and_retains_the_description_path() + Dim root = JsonNode.Parse( + "{""description"":""outer"",""cases"":[" & + "{""description"":""first"",""uuid"":""a"",""property"":""value"",""input"":{},""expected"":1}," & + "{""description"":""inner"",""cases"":[" & + "{""description"":""second"",""uuid"":""b"",""property"":""value"",""input"":{},""expected"":2}]}]}") + + Dim testCases = CanonicalDataParser.ParseTestCases(root) + + Assert.Equal({"a", "b"}, testCases.Select(Function(testCase) testCase("uuid").GetValue(Of String)())) + Assert.Equal({"outer", "first"}, Path(testCases(0))) + Assert.Equal({"outer", "inner", "second"}, Path(testCases(1))) + End Sub + + Private Shared Function Path(testCase As JsonNode) As String() + Return testCase("path").AsArray().Select(Function(item) item.GetValue(Of String)()).ToArray() + End Function + End Class +End Namespace diff --git a/generators/Generators.Tests/Generators.Tests.vbproj b/generators/Generators.Tests/Generators.Tests.vbproj new file mode 100644 index 0000000..3bd1285 --- /dev/null +++ b/generators/Generators.Tests/Generators.Tests.vbproj @@ -0,0 +1,29 @@ + + + + net10.0 + On + On + On + false + true + + + + + + + + + + + + + + + + + + + + diff --git a/generators/Generators.Tests/TemplatesTests.vb b/generators/Generators.Tests/TemplatesTests.vb new file mode 100644 index 0000000..f7a7801 --- /dev/null +++ b/generators/Generators.Tests/TemplatesTests.vb @@ -0,0 +1,94 @@ +Namespace Global.Exercism.VBNet.Generators + Public Class TemplatesTests + + Public Sub String_literal_escapes_quotes_and_control_characters() + Dim quote = ChrW(34).ToString() + Dim value = "before" & quote & "after" & vbCrLf & vbTab + Dim expected = quote & "before" & quote & quote & "after" & quote & " & vbCrLf & vbTab" + + Assert.Equal(expected, Templates.VbStringLiteral(value)) + End Sub + + + Public Sub Literal_renders_nested_arrays_and_vb_primitive_names() + Dim nested = New Scriban.Runtime.ScriptArray From { + New Scriban.Runtime.ScriptArray From {"one", True}, + Nothing + } + + Assert.Equal("{{""one"", True}, Nothing}", Templates.VbLiteral(nested)) + End Sub + + + Public Sub Filtering_before_rendering_enables_the_first_selected_test() + Dim canonicalData = Canonical("a", "b") + Dim filtered = TestCasesConfiguration.RemoveExcludedTestCases( + canonicalData, + "[a]" & vbLf & "include = false" & vbLf & "[b]") + Const template = "{{ for test in tests }}{{ test.uuid }}={{ for.first }}{{ end }}" + + Assert.Equal("b=true", Templates.RenderTestsCode(filtered, template)) + End Sub + + + Public Sub Literal_custom_test_is_rendered_unchanged() + Dim canonicalData = Canonical("a") + Const customTest = "" & vbLf & + "Public Sub Track_specific_test()" & vbLf & + "End Sub" + Dim template = "{{ for test in tests }}{{ test.uuid }}{{ end }}" & vbLf & customTest + + Dim rendered = Templates.RenderTestsCode(canonicalData, template) + + Assert.Contains("a", rendered) + Assert.Contains(customTest, rendered) + End Sub + + + Public Sub Malformed_template_does_not_overwrite_an_existing_test_file() + Dim outputPath = Path.GetTempFileName() + File.WriteAllText(outputPath, "sentinel") + + Try + Assert.Throws(Of InvalidDataException)( + Sub() TestsGenerator.GenerateTestsFile(Canonical("a"), "{{ if", outputPath, "Generator.tpl")) + Assert.Equal("sentinel", File.ReadAllText(outputPath)) + Finally + File.Delete(outputPath) + End Try + End Sub + + + Public Sub Formatting_rejects_invalid_visual_basic() + Assert.Throws(Of InvalidDataException)(Function() Formatting.FormatCode("Public Class")) + End Sub + + + Public Sub Formatting_is_deterministic_and_uses_lf() + Const source = "Public Class Example" & vbLf & "Public Sub Test()" & vbLf & "End Sub" & vbLf & "End Class" + + Dim once = Formatting.FormatCode(source) + Dim twice = Formatting.FormatCode(once) + + Assert.Equal(once, twice) + Assert.DoesNotContain(vbCr, once) + Assert.EndsWith(vbLf, once) + End Sub + + Private Shared Function Canonical(ParamArray uuids As String()) As CanonicalData + Dim testCases = uuids.Select(AddressOf TestCase).Cast(Of JsonNode)().ToArray() + Return New CanonicalData(New Exercise("test-exercise", "TestExercise"), testCases) + End Function + + Private Shared Function TestCase(uuid As String) As JsonObject + Dim testCaseNode = New JsonObject() + testCaseNode("uuid") = uuid + testCaseNode("description") = uuid + testCaseNode("property") = "value" + testCaseNode("input") = New JsonObject() + testCaseNode("expected") = uuid + testCaseNode("path") = New JsonArray(JsonValue.Create(uuid)) + Return testCaseNode + End Function + End Class +End Namespace diff --git a/generators/Generators.Tests/TestCasesConfigurationTests.vb b/generators/Generators.Tests/TestCasesConfigurationTests.vb new file mode 100644 index 0000000..9ca0273 --- /dev/null +++ b/generators/Generators.Tests/TestCasesConfigurationTests.vb @@ -0,0 +1,98 @@ +Namespace Global.Exercism.VBNet.Generators + Public Class TestCasesConfigurationTests + + Public Sub Omitted_include_is_included() + Dim result = Filter(Canonical("a"), "[a]" & vbLf & "description = ""A""") + + Assert.Equal({"a"}, Uuids(result)) + End Sub + + + Public Sub Include_false_is_excluded() + Dim result = Filter(Canonical("a", "b"), + "[a]" & vbLf & + "include = false" & vbLf & + "[b]") + + Assert.Equal({"b"}, Uuids(result)) + End Sub + + + Public Sub Reimplemented_original_is_excluded_and_replacement_is_included() + Dim result = Filter(Canonical("a", "b"), + "[a]" & vbLf & + "[b]" & vbLf & + "reimplements = ""a""") + + Assert.Equal({"b"}, Uuids(result)) + End Sub + + + Public Sub Replacement_chain_includes_only_the_enabled_terminal_case() + Dim result = Filter(Canonical("a", "b", "c"), + "[a]" & vbLf & + "include = false" & vbLf & + "[b]" & vbLf & + "include = false" & vbLf & + "reimplements = ""a""" & vbLf & + "[c]" & vbLf & + "reimplements = ""b""") + + Assert.Equal({"c"}, Uuids(result)) + End Sub + + + Public Sub Disabled_terminal_replacement_does_not_revive_an_older_case() + Dim result = Filter(Canonical("a", "b", "c"), + "[a]" & vbLf & + "[b]" & vbLf & + "reimplements = ""a""" & vbLf & + "[c]" & vbLf & + "include = false" & vbLf & + "reimplements = ""b""") + + Assert.Empty(Uuids(result)) + End Sub + + + Public Sub Multiple_terminal_replacements_are_included_in_canonical_order() + Dim result = Filter(Canonical("a", "b", "c"), + "[a]" & vbLf & + "[b]" & vbLf & + "reimplements = ""a""" & vbLf & + "[c]" & vbLf & + "reimplements = ""a""") + + Assert.Equal({"b", "c"}, Uuids(result)) + End Sub + + + Public Sub Canonical_reimplementation_is_honored_when_tests_toml_is_stale() + Dim canonicalData = Canonical("a", "b") + DirectCast(canonicalData.TestCases(1), JsonObject)("reimplements") = "a" + + Dim result = Filter(canonicalData, "[a]") + + Assert.Equal({"b"}, Uuids(result)) + End Sub + + Private Shared Function Filter(canonicalData As CanonicalData, testsToml As String) As CanonicalData + Return TestCasesConfiguration.RemoveExcludedTestCases(canonicalData, testsToml) + End Function + + Private Shared Function Canonical(ParamArray uuids As String()) As CanonicalData + Dim testCases = uuids.Select(AddressOf TestCase).Cast(Of JsonNode)().ToArray() + Return New CanonicalData(New Exercise("test-exercise", "TestExercise"), testCases) + End Function + + Private Shared Function TestCase(uuid As String) As JsonObject + Dim testCaseNode = New JsonObject() + testCaseNode("uuid") = uuid + Return testCaseNode + End Function + + Private Shared Function Uuids(canonicalData As CanonicalData) As String() + Return canonicalData.TestCases.Select(Function(testCase) testCase("uuid").GetValue(Of String)()).ToArray() + End Function + End Class +End Namespace diff --git a/generators/Generators.Tests/packages.lock.json b/generators/Generators.Tests/packages.lock.json new file mode 100644 index 0000000..749af63 --- /dev/null +++ b/generators/Generators.Tests/packages.lock.json @@ -0,0 +1,306 @@ +{ + "version": 1, + "dependencies": { + "net10.0": { + "Microsoft.NET.Test.Sdk": { + "type": "Direct", + "requested": "[18.3.0, )", + "resolved": "18.3.0", + "contentHash": "xW3kXuWRQtgoxJp4J+gdhHSQyK+6Wb/AZDSd7lMvuMRYlZ1tnpkojyfZlWilB5G4dmZ0Y0ZxU/M23TlubndNkw==", + "dependencies": { + "Microsoft.CodeCoverage": "18.3.0", + "Microsoft.TestPlatform.TestHost": "18.3.0" + } + }, + "xunit.runner.visualstudio": { + "type": "Direct", + "requested": "[3.1.5, )", + "resolved": "3.1.5", + "contentHash": "tKi7dSTwP4m5m9eXPM2Ime4Kn7xNf4x4zT9sdLO/G4hZVnQCRiMTWoSZqI/pYTVeI27oPPqHBKYI/DjJ9GsYgA==" + }, + "xunit.v3": { + "type": "Direct", + "requested": "[3.2.2, )", + "resolved": "3.2.2", + "contentHash": "L+4/4y0Uqcg8/d6hfnxhnwh4j9FaeULvefTwrk30rr1o4n/vdPfyUQ8k0yzH8VJx7bmFEkDdcRfbtbjEHlaYcA==", + "dependencies": { + "xunit.v3.mtp-v1": "[3.2.2]" + } + }, + "CommandLineParser": { + "type": "Transitive", + "resolved": "2.9.1", + "contentHash": "OE0sl1/sQ37bjVsPKKtwQlWDgqaxWgtme3xZz7JssWUzg5JpMIyHgCTY9MVMxOg48fJ1AgGT3tgdH5m/kQ5xhA==" + }, + "Humanizer.Core": { + "type": "Transitive", + "resolved": "3.0.10", + "contentHash": "yZIhtw8sYuvsONzQbZxWpR60tMWYHXoo0DL6nyOqSFiU5POjBTSEyWFpTQtJEZuy+oqiYTXKXY/Mjx7KnqIQFw==" + }, + "LibGit2Sharp": { + "type": "Transitive", + "resolved": "0.31.0", + "contentHash": "b3+UfV7LjKMjAHWwl7VawejiOv2gJIC6dTCA/S0puLTHACAA/Oeb5JJmWUQMeyH/T/WR/LaIK8bk2RbdFnrZvg==", + "dependencies": { + "LibGit2Sharp.NativeBinaries": "[2.0.323]" + } + }, + "LibGit2Sharp.NativeBinaries": { + "type": "Transitive", + "resolved": "2.0.323", + "contentHash": "Kg+fJGWhGj5qRXG0Ilj4ddhuodGXZg57yhfX6OVUDR0M2DKg/UR42/d74+qv5l1qotc1qJilo/ho7xQnULP6yA==" + }, + "Microsoft.ApplicationInsights": { + "type": "Transitive", + "resolved": "2.23.0", + "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==" + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==" + }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "5.3.0-2.25625.1", + "contentHash": "4Yhh2fnu3G+J0J1lDc8WZVgMjgbynSeTfkl5IFJMFrmiIO0sc7Tjx+f3sFVV8Sd35PrIUWfof0RWc3lAMl7Azg==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "5.3.0", + "contentHash": "uC0qk3jzTQY7i90ehfnCqaOZpBUGJyPMiHJ3c0jOb8yaPBjWzIhVdNxPbeVzI74DB0C+YgBKPLqUkgFZzua5Mg==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "5.3.0-2.25625.1" + } + }, + "Microsoft.CodeAnalysis.VisualBasic": { + "type": "Transitive", + "resolved": "5.3.0", + "contentHash": "AJxddsIOmfimuihaLuSAm4c/zskHoL1ypAjIpSOZqHlNm2iuw0twsB8nbKczJyfClqD7+iYjdIeE5EV8WAyxRA==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "5.3.0-2.25625.1", + "Microsoft.CodeAnalysis.Common": "[5.3.0]" + } + }, + "Microsoft.CodeAnalysis.VisualBasic.Workspaces": { + "type": "Transitive", + "resolved": "5.3.0", + "contentHash": "pAGdr4qs7+v287DPiiM8px1cBXnhe8LxymkVGTnCwv2OEjCk5HO2zIoFvype4ivKJTRW3aTUUV8ab+915wbv+w==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.CodeAnalysis.Analyzers": "5.3.0-2.25625.1", + "Microsoft.CodeAnalysis.Common": "[5.3.0]", + "Microsoft.CodeAnalysis.VisualBasic": "[5.3.0]", + "Microsoft.CodeAnalysis.Workspaces.Common": "[5.3.0]", + "System.Composition": "9.0.0" + } + }, + "Microsoft.CodeAnalysis.Workspaces.Common": { + "type": "Transitive", + "resolved": "5.3.0", + "contentHash": "QSf1ge9A+XFZbGL+gIqXYBIKlm8QdQVLvHDPZiydG11W6mJY7XBMusrsgIEz6L8GYMzGJKTM78m9icliGMF7NA==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.CodeAnalysis.Analyzers": "5.3.0-2.25625.1", + "Microsoft.CodeAnalysis.Common": "[5.3.0]", + "System.Composition": "9.0.0" + } + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "18.3.0", + "contentHash": "23BNy/vziREC20Wwhb50K7+kZe0m07KlLWDQv4qjJ9tt3QjpDpDIqJFrhYHmMEo9xDkuSp55U/8h4bMF7MiB+g==" + }, + "Microsoft.Testing.Extensions.Telemetry": { + "type": "Transitive", + "resolved": "1.9.1", + "contentHash": "No5AudZMmSb+uNXjlgL2y3/stHD2IT4uxqc5yHwkE+/nNux9jbKcaJMvcp9SwgP4DVD8L9/P3OUz8mmmcvEIdQ==", + "dependencies": { + "Microsoft.ApplicationInsights": "2.23.0", + "Microsoft.Testing.Platform": "1.9.1" + } + }, + "Microsoft.Testing.Extensions.TrxReport.Abstractions": { + "type": "Transitive", + "resolved": "1.9.1", + "contentHash": "AL46Xe1WBi85Ntd4mNPvat5ZSsZ2uejiVqoKCypr8J3wK0elA5xJ3AN4G/Q4GIwzUFnggZoH/DBjnr9J18IO/g==", + "dependencies": { + "Microsoft.Testing.Platform": "1.9.1" + } + }, + "Microsoft.Testing.Platform": { + "type": "Transitive", + "resolved": "1.9.1", + "contentHash": "QafNtNSmEI0zazdebnsIkDKmFtTSpmx/5PLOjURWwozcPb3tvRxzosQSL8xwYNM1iPhhKiBksXZyRSE2COisrA==" + }, + "Microsoft.Testing.Platform.MSBuild": { + "type": "Transitive", + "resolved": "1.9.1", + "contentHash": "oTUtyR4X/s9ytuiNA29FGsNCCH0rNmY5Wdm14NCKLjTM1cT9edVSlA+rGS/mVmusPqcP0l/x9qOnMXg16v87RQ==", + "dependencies": { + "Microsoft.Testing.Platform": "1.9.1" + } + }, + "Microsoft.TestPlatform.ObjectModel": { + "type": "Transitive", + "resolved": "18.3.0", + "contentHash": "AEIEX2aWdPO9XbtR96eBaJxmXRD9vaI9uQ1T/JbPEKlTAZwYx0ZrMzKyULMdh/HH9Sg03kXCoN7LszQ90o6nPQ==" + }, + "Microsoft.TestPlatform.TestHost": { + "type": "Transitive", + "resolved": "18.3.0", + "contentHash": "twmsoelXnp1uWMU3VGip9f0Jr1mZ0PZqgJdF35CIrdYgYrkHIJMV1m8uKyhcdjLdsQDESHAgkR7KhS9i1qpJag==", + "dependencies": { + "Microsoft.TestPlatform.ObjectModel": "18.3.0", + "Newtonsoft.Json": "13.0.3" + } + }, + "Microsoft.Win32.Registry": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "Scriban": { + "type": "Transitive", + "resolved": "7.2.6", + "contentHash": "k6DvaRY83jtigNudMoYlN5Zg42JEDDOCSeKrnpHiE5Gthkd9Fr+6VnYFnL3DqtkZTtPezJJdoX3ZfOTTFpQEMg==" + }, + "System.Composition": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "3Djj70fFTraOarSKmRnmRy/zm4YurICm+kiCtI0dYRqGJnLX6nJ+G3WYuFJ173cAPax/gh96REcbNiVqcrypFQ==", + "dependencies": { + "System.Composition.AttributedModel": "9.0.0", + "System.Composition.Convention": "9.0.0", + "System.Composition.Hosting": "9.0.0", + "System.Composition.Runtime": "9.0.0", + "System.Composition.TypedParts": "9.0.0" + } + }, + "System.Composition.AttributedModel": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "iri00l/zIX9g4lHMY+Nz0qV1n40+jFYAmgsaiNn16xvt2RDwlqByNG4wgblagnDYxm3YSQQ0jLlC/7Xlk9CzyA==" + }, + "System.Composition.Convention": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "+vuqVP6xpi582XIjJi6OCsIxuoTZfR0M7WWufk3uGDeCl3wGW6KnpylUJ3iiXdPByPE0vR5TjJgR6hDLez4FQg==", + "dependencies": { + "System.Composition.AttributedModel": "9.0.0" + } + }, + "System.Composition.Hosting": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "OFqSeFeJYr7kHxDfaViGM1ymk7d4JxK//VSoNF9Ux0gpqkLsauDZpu89kTHHNdCWfSljbFcvAafGyBoY094btQ==", + "dependencies": { + "System.Composition.Runtime": "9.0.0" + } + }, + "System.Composition.Runtime": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "w1HOlQY1zsOWYussjFGZCEYF2UZXgvoYnS94NIu2CBnAGMbXFAX8PY8c92KwUItPmowal68jnVLBCzdrWLeEKA==" + }, + "System.Composition.TypedParts": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "aRZlojCCGEHDKqh43jaDgaVpYETsgd7Nx4g1zwLKMtv4iTo0627715ajEFNpEEBTgLmvZuv8K0EVxc3sM4NWJA==", + "dependencies": { + "System.Composition.AttributedModel": "9.0.0", + "System.Composition.Hosting": "9.0.0", + "System.Composition.Runtime": "9.0.0" + } + }, + "Tomlyn": { + "type": "Transitive", + "resolved": "1.1.1", + "contentHash": "NTKy4qdZyepRcaWZtFRdii8qV2v/CUlzIFsei6+Qz95lvUeGOsqOzKlC1iHYTN/HFgILEoU6B5u+JwXN7JHq1A==" + }, + "xunit.analyzers": { + "type": "Transitive", + "resolved": "1.27.0", + "contentHash": "y/pxIQaLvk/kxAoDkZW9GnHLCEqzwl5TW0vtX3pweyQpjizB9y3DXhb9pkw2dGeUqhLjsxvvJM1k89JowU6z3g==" + }, + "xunit.v3.assert": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "BPciBghgEEaJN/JG00QfCYDfEfnLgQhfnYEy+j1izoeHVNYd5+3Wm8GJ6JgYysOhpBPYGE+sbf75JtrRc7jrdA==" + }, + "xunit.v3.common": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "Hj775PEH6GTbbg0wfKRvG2hNspDCvTH9irXhH4qIWgdrOSV1sQlqPie+DOvFeigsFg2fxSM3ZAaaCDQs+KreFA==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "6.0.0" + } + }, + "xunit.v3.core.mtp-v1": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "Ga5aA2Ca9ktz+5k3g5ukzwfexwoqwDUpV6z7atSEUvqtd6JuybU1XopHqg1oFd78QdTfZgZE9h5sHpO4qYIi5w==", + "dependencies": { + "Microsoft.Testing.Extensions.Telemetry": "1.9.1", + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "1.9.1", + "Microsoft.Testing.Platform": "1.9.1", + "Microsoft.Testing.Platform.MSBuild": "1.9.1", + "xunit.v3.extensibility.core": "[3.2.2]", + "xunit.v3.runner.inproc.console": "[3.2.2]" + } + }, + "xunit.v3.extensibility.core": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "srY8z/oMPvh/t8axtO2DwrHajhFMH7tnqKildvYrVQIfICi8fOn3yIBWkVPAcrKmHMwvXRJ/XsQM3VMR6DOYfQ==", + "dependencies": { + "xunit.v3.common": "[3.2.2]" + } + }, + "xunit.v3.mtp-v1": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "O41aAzYKBT5PWqATa1oEWVNCyEUypFQ4va6K0kz37dduV3EKzXNMaV2UnEhufzU4Cce1I33gg0oldS8tGL5I0A==", + "dependencies": { + "xunit.analyzers": "1.27.0", + "xunit.v3.assert": "[3.2.2]", + "xunit.v3.core.mtp-v1": "[3.2.2]" + } + }, + "xunit.v3.runner.common": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "/hkHkQCzGrugelOAehprm7RIWdsUFVmIVaD6jDH/8DNGCymTlKKPTbGokD5czbAfqfex47mBP0sb0zbHYwrO/g==", + "dependencies": { + "Microsoft.Win32.Registry": "[5.0.0]", + "xunit.v3.common": "[3.2.2]" + } + }, + "xunit.v3.runner.inproc.console": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "ulWOdSvCk+bPXijJZ73bth9NyoOHsAs1ZOvamYbCkD4DNLX/Bd29Ve2ZNUwBbK0MqfIYWXHZViy/HKrdEC/izw==", + "dependencies": { + "xunit.v3.extensibility.core": "[3.2.2]", + "xunit.v3.runner.common": "[3.2.2]" + } + }, + "generators": { + "type": "Project", + "dependencies": { + "CommandLineParser": "[2.9.1, )", + "Humanizer.Core": "[3.0.10, )", + "LibGit2Sharp": "[0.31.0, )", + "Microsoft.CodeAnalysis.VisualBasic.Workspaces": "[5.3.0, )", + "Scriban": "[7.2.6, )", + "Tomlyn": "[1.1.1, )" + } + } + } + } +} \ No newline at end of file diff --git a/generators/Generators.sln b/generators/Generators.sln new file mode 100644 index 0000000..905ca31 --- /dev/null +++ b/generators/Generators.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "Generators", "Generators.vbproj", "{DE749F16-0B9D-4BD8-BB17-4995CED93B20}" +EndProject +Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "Generators.Tests", "Generators.Tests\Generators.Tests.vbproj", "{46BC9B5E-6A57-40D9-8753-B3295BD1B4CC}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {DE749F16-0B9D-4BD8-BB17-4995CED93B20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DE749F16-0B9D-4BD8-BB17-4995CED93B20}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DE749F16-0B9D-4BD8-BB17-4995CED93B20}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DE749F16-0B9D-4BD8-BB17-4995CED93B20}.Release|Any CPU.Build.0 = Release|Any CPU + {46BC9B5E-6A57-40D9-8753-B3295BD1B4CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {46BC9B5E-6A57-40D9-8753-B3295BD1B4CC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {46BC9B5E-6A57-40D9-8753-B3295BD1B4CC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {46BC9B5E-6A57-40D9-8753-B3295BD1B4CC}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/generators/Generators.vbproj b/generators/Generators.vbproj new file mode 100644 index 0000000..ea0f992 --- /dev/null +++ b/generators/Generators.vbproj @@ -0,0 +1,36 @@ + + + + net10.0 + Exe + On + On + On + true + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/generators/Naming.vb b/generators/Naming.vb new file mode 100644 index 0000000..fa4c512 --- /dev/null +++ b/generators/Naming.vb @@ -0,0 +1,27 @@ +Imports System.Text.RegularExpressions + +Namespace Global.Exercism.VBNet.Generators + Friend Module Naming + Friend Function ToMethodName([property] As String) As String + Return [property].Dehumanize() + End Function + + Friend Function ToTestMethodName(ParamArray path As String()) As String + Dim words = Regex.Split(String.Join(" ", path), "\W+"). + Where(Function(word) Not String.IsNullOrWhiteSpace(word)). + Select(AddressOf Transform) + + Return String.Join(" ", words).Underscore().Transform([To].SentenceCase) + End Function + + Private Function Transform(word As String, index As Integer) As String + Dim number As Integer + + If index = 0 AndAlso Integer.TryParse(word, number) Then + Return number.ToWords() + End If + + Return word.Dehumanize() + End Function + End Module +End Namespace diff --git a/generators/Paths.vb b/generators/Paths.vb new file mode 100644 index 0000000..b119e16 --- /dev/null +++ b/generators/Paths.vb @@ -0,0 +1,43 @@ +Namespace Global.Exercism.VBNet.Generators + Friend Module Paths + Private ReadOnly RootDirectory As String = FindRootDirectory() + Friend ReadOnly ProblemSpecificationsDirectory As String = Path.Join(RootDirectory, ".problem-specifications") + Private ReadOnly ProblemSpecificationsExercisesDirectory As String = Path.Join(ProblemSpecificationsDirectory, "exercises") + Friend ReadOnly PracticeExercisesDirectory As String = Path.Join(RootDirectory, "exercises", "practice") + Friend ReadOnly TrackConfigFile As String = Path.Join(RootDirectory, "config.json") + + Friend Function ExerciseDirectory(exercise As Exercise) As String + Return Path.Join(PracticeExercisesDirectory, exercise.Slug) + End Function + + Friend Function TestsFile(exercise As Exercise) As String + Return Path.Join(ExerciseDirectory(exercise), $"{exercise.Name}Tests.vb") + End Function + + Friend Function TestsTomlFile(exercise As Exercise) As String + Return Path.Join(ExerciseDirectory(exercise), ".meta", "tests.toml") + End Function + + Friend Function TemplateFile(exercise As Exercise) As String + Return Path.Join(ExerciseDirectory(exercise), ".meta", "Generator.tpl") + End Function + + Friend Function CanonicalDataFile(exercise As Exercise) As String + Return Path.Join(ProblemSpecificationsExercisesDirectory, exercise.Slug, "canonical-data.json") + End Function + + Private Function FindRootDirectory() As String + Dim currentDirectory = Environment.CurrentDirectory + + While currentDirectory IsNot Nothing AndAlso Not File.Exists(Path.Join(currentDirectory, "LICENSE")) + currentDirectory = Path.GetDirectoryName(currentDirectory) + End While + + If currentDirectory Is Nothing Then + Throw New DirectoryNotFoundException("Could not find the repository root containing LICENSE.") + End If + + Return currentDirectory + End Function + End Module +End Namespace diff --git a/generators/ProbSpecs.vb b/generators/ProbSpecs.vb new file mode 100644 index 0000000..00da012 --- /dev/null +++ b/generators/ProbSpecs.vb @@ -0,0 +1,26 @@ +Imports LibGit2Sharp + +Namespace Global.Exercism.VBNet.Generators + Friend Module ProbSpecs + Private Const RepositoryUrl As String = "https://github.com/exercism/problem-specifications.git" + + Friend Sub Sync() + Console.WriteLine("Syncing problem-specifications repo...") + Clone() + Pull() + End Sub + + Private Sub Clone() + If Not Directory.Exists(Paths.ProblemSpecificationsDirectory) Then + Repository.Clone(RepositoryUrl, Paths.ProblemSpecificationsDirectory) + End If + End Sub + + Private Sub Pull() + Using repository = New Repository(Paths.ProblemSpecificationsDirectory) + Dim signature = New Signature("Exercism", "info@exercism.org", DateTimeOffset.Now) + Commands.Pull(repository, signature, New PullOptions()) + End Using + End Sub + End Module +End Namespace diff --git a/generators/Program.vb b/generators/Program.vb new file mode 100644 index 0000000..7346dad --- /dev/null +++ b/generators/Program.vb @@ -0,0 +1,55 @@ +Imports CommandLine + +Namespace Global.Exercism.VBNet.Generators + Public Module Program + Public Sub Main(args As String()) + Dim result = Parser.Default.ParseArguments(Of NewOptions, UpdateOptions, SyncOptions)(args) + result.WithParsed(Of NewOptions)(AddressOf HandleNewCommand) + result.WithParsed(Of UpdateOptions)(AddressOf HandleUpdateCommand) + result.WithParsed(Of SyncOptions)(AddressOf HandleSyncCommand) + result.WithNotParsed(AddressOf HandleErrors) + End Sub + + Private Sub HandleNewCommand(options As NewOptions) + Exercises.Untemplated(options.Exercise).ForEach(AddressOf TemplateGenerator.Generate) + End Sub + + Private Sub HandleUpdateCommand(options As UpdateOptions) + Exercises.Templated(options.Exercise).ForEach(AddressOf TestsGenerator.Generate) + End Sub + + Private Sub HandleSyncCommand(options As SyncOptions) + ProbSpecs.Sync() + End Sub + + Private Sub HandleErrors(errors As IEnumerable(Of CommandLine.Error)) + For Each parseError In errors + If Not IsInformational(parseError) Then + Console.Error.WriteLine(parseError) + End If + Next + End Sub + + Private Function IsInformational(parseError As CommandLine.Error) As Boolean + Return parseError.Tag = ErrorType.HelpRequestedError OrElse + parseError.Tag = ErrorType.HelpVerbRequestedError OrElse + parseError.Tag = ErrorType.VersionRequestedError + End Function + + + Private NotInheritable Class NewOptions + + Public Property Exercise As String + End Class + + + Private NotInheritable Class UpdateOptions + + Public Property Exercise As String + End Class + + + Private NotInheritable Class SyncOptions + End Class + End Module +End Namespace diff --git a/generators/TemplateGenerator.vb b/generators/TemplateGenerator.vb new file mode 100644 index 0000000..87a3b41 --- /dev/null +++ b/generators/TemplateGenerator.vb @@ -0,0 +1,87 @@ +Namespace Global.Exercism.VBNet.Generators + Friend Module TemplateGenerator + Friend Sub Generate(exercise As Exercise) + Console.WriteLine($"{exercise.Slug}: generating template...") + + Dim canonicalData = CanonicalDataParser.Parse(exercise) + Dim filteredCanonicalData = TestCasesConfiguration.RemoveExcludedTestCases(canonicalData) + Dim template = RenderTemplate(filteredCanonicalData) + File.WriteAllText(Paths.TemplateFile(exercise), template, New UTF8Encoding(encoderShouldEmitUTF8Identifier:=False)) + End Sub + + Friend Function RenderTemplate(canonicalData As CanonicalData) As String + Dim representativeTestCase = canonicalData.TestCases.FirstOrDefault(Function(testCase) Not ExpectsError(testCase)) + + If representativeTestCase Is Nothing Then + Throw New InvalidDataException($"'{canonicalData.Exercise.Slug}' has no included non-error test case from which to create a template.") + End If + + Dim hasError = canonicalData.TestCases.Any(AddressOf ExpectsError) + Dim lines = New List(Of String) From { + "Public Class {{ testClass }}", + " {{- for test in tests }}", + " ", + " Public Sub {{ test.testMethod }}()" + } + + If hasError Then + lines.Add(" {{ if test.expected.error }}") + lines.Add($" {AssertThrows(representativeTestCase)}") + lines.Add(" {{ else }}") + lines.Add($" {Assertion(representativeTestCase)}") + lines.Add(" {{ end }}") + Else + lines.Add($" {Assertion(representativeTestCase)}") + End If + + lines.Add(" End Sub") + lines.Add(" {{ end -}}") + lines.Add("End Class") + + Return String.Join(vbLf, lines) & vbLf + End Function + + Private Function Value(field As String, testCase As JsonNode) As String + If testCase IsNot Nothing AndAlso testCase.GetValueKind() = JsonValueKind.String Then + Return "{{ " & field & " | vb_string_literal }}" + End If + + Return "{{ " & field & " }}" + End Function + + Private Function Expected(testCase As JsonNode) As String + Return Value("test.expected", testCase("expected")) + End Function + + Private Function Assertion(testCase As JsonNode) As String + Select Case testCase("expected").GetValueKind() + Case JsonValueKind.False, JsonValueKind.True + Return AssertBoolean(TestedMethodCall(testCase)) + Case Else + Return $"Assert.Equal({Expected(testCase)}, {TestedMethodCall(testCase)})" + End Select + End Function + + Private Function TestedMethodArguments(testCase As JsonNode) As String + Return String.Join(", ", testCase("input").AsObject(). + Select(Function(pair) Value($"test.input.{pair.Key}", pair.Value))) + End Function + + Private Function TestedMethodCall(testCase As JsonNode) As String + Return "{{ test.testedMethod }}(" & TestedMethodArguments(testCase) & ")" + End Function + + Private Function AssertBoolean(methodCall As String) As String + Return "Assert.{{ test.expected ? ""True"" : ""False"" }}(" & methodCall & ")" + End Function + + Private Function AssertThrows(testCase As JsonNode) As String + Return "Assert.Throws(Of ArgumentException)(Function() " & TestedMethodCall(testCase) & ")" + End Function + + Private Function ExpectsError(testCase As JsonNode) As Boolean + Dim expected = TryCast(testCase("expected"), JsonObject) + Return expected IsNot Nothing AndAlso expected.ContainsKey("error") + End Function + End Module +End Namespace diff --git a/generators/Templates.vb b/generators/Templates.vb new file mode 100644 index 0000000..03984f4 --- /dev/null +++ b/generators/Templates.vb @@ -0,0 +1,156 @@ +Imports System.Globalization + +Imports Scriban +Imports Scriban.Runtime + +Namespace Global.Exercism.VBNet.Generators + Friend Module Templates + Friend Function RenderTestsCode(canonicalData As CanonicalData) As String + Dim templatePath = Paths.TemplateFile(canonicalData.Exercise) + Return RenderTestsCode(canonicalData, File.ReadAllText(templatePath), templatePath) + End Function + + Friend Function RenderTestsCode(canonicalData As CanonicalData, templateText As String, Optional templatePath As String = Nothing) As String + Dim template = ParseTemplate(templateText, templatePath) + Dim scriptObject = New ScriptObject() + scriptObject.Import("pascalize", New Func(Of String, String)(Function(text) text.Pascalize())) + scriptObject.Import("enum", New Func(Of String, String, String)( + Function(text, enumType) $"{enumType.Pascalize()}.{text.Pascalize()}")) + scriptObject.Import("property", New Func(Of ScriptArray, String, ScriptArray)(AddressOf FilterByProperty)) + scriptObject.Import("vb_literal", New Func(Of Object, String)(AddressOf VbLiteral)) + scriptObject.Import("vb_string_literal", New Func(Of String, String)(AddressOf VbStringLiteral)) + scriptObject.Import(TemplateData(canonicalData)) + + Dim context = New TemplateContext() + context.PushGlobal(scriptObject) + + Try + Return template.Render(context) + Catch exception As Exception + Dim source = If(templatePath, $"the template for '{canonicalData.Exercise.Slug}'") + Throw New InvalidDataException($"Could not render {source}: {exception.Message}", exception) + End Try + End Function + + Friend Function VbStringLiteral(value As String) As String + If value Is Nothing Then + Return "Nothing" + End If + + Dim parts = New List(Of String)() + Dim text = New StringBuilder() + Dim index = 0 + + While index < value.Length + Dim character = value(index) + + If character = ControlChars.Cr AndAlso index + 1 < value.Length AndAlso value(index + 1) = ControlChars.Lf Then + FlushText(parts, text) + parts.Add("vbCrLf") + index += 2 + Continue While + End If + + Select Case character + Case ControlChars.Cr + FlushText(parts, text) + parts.Add("vbCr") + Case ControlChars.Lf + FlushText(parts, text) + parts.Add("vbLf") + Case ControlChars.Tab + FlushText(parts, text) + parts.Add("vbTab") + Case Else + If Char.IsControl(character) Then + FlushText(parts, text) + parts.Add($"ChrW({AscW(character).ToString(CultureInfo.InvariantCulture)})") + Else + text.Append(character) + End If + End Select + + index += 1 + End While + + FlushText(parts, text) + + If parts.Count = 0 Then + Return Quote(String.Empty) + End If + + Return String.Join(" & ", parts) + End Function + + Friend Function VbLiteral(value As Object) As String + If value Is Nothing Then + Return "Nothing" + End If + + If TypeOf value Is String Then + Return VbStringLiteral(DirectCast(value, String)) + End If + + If TypeOf value Is Boolean Then + Return If(DirectCast(value, Boolean), "True", "False") + End If + + Dim values = TryCast(value, ScriptArray) + + If values IsNot Nothing Then + Return "{" & String.Join(", ", values.Select(AddressOf VbLiteral)) & "}" + End If + + Return Convert.ToString(value, CultureInfo.InvariantCulture) + End Function + + Private Function ParseTemplate(templateText As String, templatePath As String) As Template + Dim parsedTemplate As Template = Scriban.Template.Parse(templateText, templatePath) + + If parsedTemplate.HasErrors Then + Dim source = If(templatePath, "the supplied template") + Throw New InvalidDataException($"Could not parse {source}:{Environment.NewLine}{String.Join(Environment.NewLine, parsedTemplate.Messages)}") + End If + + Return parsedTemplate + End Function + + Private Function FilterByProperty(testCases As ScriptArray, name As String) As ScriptArray + Return New ScriptArray(testCases. + Cast(Of ScriptObject)(). + Where(Function(testCase) testCase("property")?.ToString() = name)) + End Function + + Private Function TemplateData(canonicalData As CanonicalData) As JsonElement + Return JsonSerializer.SerializeToElement( + New With { + .testClass = $"{canonicalData.Exercise.Name}Tests".Pascalize(), + .testedClass = canonicalData.Exercise.Name.Pascalize(), + .tests = canonicalData.TestCases.Select(AddressOf AddCalculatedFields).ToArray() + }) + End Function + + Private Function AddCalculatedFields(testCase As JsonNode) As JsonElement + testCase("testMethod") = Naming.ToTestMethodName( + testCase("path").AsArray().Select(Function(item) item.GetValue(Of String)()).ToArray()) + testCase("shortTestMethod") = Naming.ToTestMethodName(testCase("description").GetValue(Of String)()) + testCase("testedMethod") = Naming.ToMethodName(testCase("property").GetValue(Of String)()) + + Return JsonSerializer.SerializeToElement(testCase) + End Function + + Private Sub FlushText(parts As List(Of String), text As StringBuilder) + If text.Length = 0 Then + Return + End If + + parts.Add(Quote(text.ToString())) + text.Clear() + End Sub + + Private Function Quote(value As String) As String + Dim quotationMark = ChrW(34).ToString() + Return quotationMark & value.Replace(quotationMark, quotationMark & quotationMark) & quotationMark + End Function + End Module +End Namespace diff --git a/generators/TestCasesConfiguration.vb b/generators/TestCasesConfiguration.vb new file mode 100644 index 0000000..dcb3473 --- /dev/null +++ b/generators/TestCasesConfiguration.vb @@ -0,0 +1,82 @@ +Imports Tomlyn +Imports Tomlyn.Model + +Namespace Global.Exercism.VBNet.Generators + Friend Module TestCasesConfiguration + Friend Function RemoveExcludedTestCases(canonicalData As CanonicalData) As CanonicalData + Return RemoveExcludedTestCases(canonicalData, File.ReadAllText(Paths.TestsTomlFile(canonicalData.Exercise))) + End Function + + Friend Function RemoveExcludedTestCases(canonicalData As CanonicalData, testsToml As String) As CanonicalData + Dim tables = TomlSerializer.Deserialize(Of TomlTable)(testsToml) + + If tables Is Nothing Then + Throw New InvalidDataException($"Could not parse tests.toml for '{canonicalData.Exercise.Slug}'.") + End If + + Dim explicitlyDisabled = ExplicitlyDisabledTestCaseIds(tables) + Dim reimplemented = ReimplementedTestCaseIds(tables, canonicalData.TestCases) + Dim includedTestCases = canonicalData.TestCases. + Where(Function(testCase) + Dim uuid = TestCaseUuid(testCase) + Return Not explicitlyDisabled.Contains(uuid) AndAlso Not reimplemented.Contains(uuid) + End Function). + ToArray() + + Return New CanonicalData(canonicalData.Exercise, includedTestCases) + End Function + + Private Function ExplicitlyDisabledTestCaseIds(tables As TomlTable) As HashSet(Of String) + Dim ids = New HashSet(Of String)(StringComparer.Ordinal) + + For Each pair In tables + Dim table = TryCast(pair.Value, TomlTable) + Dim includeValue As Object = Nothing + + If table IsNot Nothing AndAlso + table.TryGetValue("include", includeValue) AndAlso + TypeOf includeValue Is Boolean AndAlso + Not DirectCast(includeValue, Boolean) Then + ids.Add(pair.Key) + End If + Next + + Return ids + End Function + + Private Function ReimplementedTestCaseIds(tables As TomlTable, testCases As IEnumerable(Of JsonNode)) As HashSet(Of String) + Dim ids = New HashSet(Of String)(StringComparer.Ordinal) + + For Each pair In tables + Dim table = TryCast(pair.Value, TomlTable) + Dim reimplementsValue As Object = Nothing + + If table IsNot Nothing AndAlso + table.TryGetValue("reimplements", reimplementsValue) AndAlso + TypeOf reimplementsValue Is String Then + ids.Add(DirectCast(reimplementsValue, String)) + End If + Next + + For Each testCase In testCases + Dim reimplements = testCase("reimplements") + + If reimplements IsNot Nothing Then + ids.Add(reimplements.GetValue(Of String)()) + End If + Next + + Return ids + End Function + + Private Function TestCaseUuid(testCase As JsonNode) As String + Dim uuid = testCase("uuid") + + If uuid Is Nothing Then + Throw New InvalidDataException("A canonical test case does not have a UUID.") + End If + + Return uuid.GetValue(Of String)() + End Function + End Module +End Namespace diff --git a/generators/TestsGenerator.vb b/generators/TestsGenerator.vb new file mode 100644 index 0000000..6e46d9a --- /dev/null +++ b/generators/TestsGenerator.vb @@ -0,0 +1,30 @@ +Namespace Global.Exercism.VBNet.Generators + Friend Module TestsGenerator + Friend Sub Generate(exercise As Exercise) + Console.WriteLine($"{exercise.Slug}: generating tests...") + + Dim canonicalData = CanonicalDataParser.Parse(exercise) + Dim filteredCanonicalData = TestCasesConfiguration.RemoveExcludedTestCases(canonicalData) + Dim templatePath = Paths.TemplateFile(exercise) + GenerateTestsFile( + filteredCanonicalData, + File.ReadAllText(templatePath), + Paths.TestsFile(exercise), + templatePath) + End Sub + + Friend Sub GenerateTestsFile(canonicalData As CanonicalData, templateText As String, outputPath As String, Optional templatePath As String = Nothing) + Dim testCode = Templates.RenderTestsCode(canonicalData, templateText, templatePath) + Dim formattedTestCode = Formatting.FormatCode(testCode) + WriteIfChanged(outputPath, formattedTestCode) + End Sub + + Private Sub WriteIfChanged(path As String, contents As String) + If File.Exists(path) AndAlso File.ReadAllText(path) = contents Then + Return + End If + + File.WriteAllText(path, contents, New UTF8Encoding(encoderShouldEmitUTF8Identifier:=False)) + End Sub + End Module +End Namespace diff --git a/generators/packages.lock.json b/generators/packages.lock.json new file mode 100644 index 0000000..ab125ae --- /dev/null +++ b/generators/packages.lock.json @@ -0,0 +1,140 @@ +{ + "version": 1, + "dependencies": { + "net10.0": { + "CommandLineParser": { + "type": "Direct", + "requested": "[2.9.1, )", + "resolved": "2.9.1", + "contentHash": "OE0sl1/sQ37bjVsPKKtwQlWDgqaxWgtme3xZz7JssWUzg5JpMIyHgCTY9MVMxOg48fJ1AgGT3tgdH5m/kQ5xhA==" + }, + "Humanizer.Core": { + "type": "Direct", + "requested": "[3.0.10, )", + "resolved": "3.0.10", + "contentHash": "yZIhtw8sYuvsONzQbZxWpR60tMWYHXoo0DL6nyOqSFiU5POjBTSEyWFpTQtJEZuy+oqiYTXKXY/Mjx7KnqIQFw==" + }, + "LibGit2Sharp": { + "type": "Direct", + "requested": "[0.31.0, )", + "resolved": "0.31.0", + "contentHash": "b3+UfV7LjKMjAHWwl7VawejiOv2gJIC6dTCA/S0puLTHACAA/Oeb5JJmWUQMeyH/T/WR/LaIK8bk2RbdFnrZvg==", + "dependencies": { + "LibGit2Sharp.NativeBinaries": "[2.0.323]" + } + }, + "Microsoft.CodeAnalysis.VisualBasic.Workspaces": { + "type": "Direct", + "requested": "[5.3.0, )", + "resolved": "5.3.0", + "contentHash": "pAGdr4qs7+v287DPiiM8px1cBXnhe8LxymkVGTnCwv2OEjCk5HO2zIoFvype4ivKJTRW3aTUUV8ab+915wbv+w==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.CodeAnalysis.Analyzers": "5.3.0-2.25625.1", + "Microsoft.CodeAnalysis.Common": "[5.3.0]", + "Microsoft.CodeAnalysis.VisualBasic": "[5.3.0]", + "Microsoft.CodeAnalysis.Workspaces.Common": "[5.3.0]", + "System.Composition": "9.0.0" + } + }, + "Scriban": { + "type": "Direct", + "requested": "[7.2.6, )", + "resolved": "7.2.6", + "contentHash": "k6DvaRY83jtigNudMoYlN5Zg42JEDDOCSeKrnpHiE5Gthkd9Fr+6VnYFnL3DqtkZTtPezJJdoX3ZfOTTFpQEMg==" + }, + "Tomlyn": { + "type": "Direct", + "requested": "[1.1.1, )", + "resolved": "1.1.1", + "contentHash": "NTKy4qdZyepRcaWZtFRdii8qV2v/CUlzIFsei6+Qz95lvUeGOsqOzKlC1iHYTN/HFgILEoU6B5u+JwXN7JHq1A==" + }, + "LibGit2Sharp.NativeBinaries": { + "type": "Transitive", + "resolved": "2.0.323", + "contentHash": "Kg+fJGWhGj5qRXG0Ilj4ddhuodGXZg57yhfX6OVUDR0M2DKg/UR42/d74+qv5l1qotc1qJilo/ho7xQnULP6yA==" + }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "5.3.0-2.25625.1", + "contentHash": "4Yhh2fnu3G+J0J1lDc8WZVgMjgbynSeTfkl5IFJMFrmiIO0sc7Tjx+f3sFVV8Sd35PrIUWfof0RWc3lAMl7Azg==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "5.3.0", + "contentHash": "uC0qk3jzTQY7i90ehfnCqaOZpBUGJyPMiHJ3c0jOb8yaPBjWzIhVdNxPbeVzI74DB0C+YgBKPLqUkgFZzua5Mg==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "5.3.0-2.25625.1" + } + }, + "Microsoft.CodeAnalysis.VisualBasic": { + "type": "Transitive", + "resolved": "5.3.0", + "contentHash": "AJxddsIOmfimuihaLuSAm4c/zskHoL1ypAjIpSOZqHlNm2iuw0twsB8nbKczJyfClqD7+iYjdIeE5EV8WAyxRA==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "5.3.0-2.25625.1", + "Microsoft.CodeAnalysis.Common": "[5.3.0]" + } + }, + "Microsoft.CodeAnalysis.Workspaces.Common": { + "type": "Transitive", + "resolved": "5.3.0", + "contentHash": "QSf1ge9A+XFZbGL+gIqXYBIKlm8QdQVLvHDPZiydG11W6mJY7XBMusrsgIEz6L8GYMzGJKTM78m9icliGMF7NA==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.CodeAnalysis.Analyzers": "5.3.0-2.25625.1", + "Microsoft.CodeAnalysis.Common": "[5.3.0]", + "System.Composition": "9.0.0" + } + }, + "System.Composition": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "3Djj70fFTraOarSKmRnmRy/zm4YurICm+kiCtI0dYRqGJnLX6nJ+G3WYuFJ173cAPax/gh96REcbNiVqcrypFQ==", + "dependencies": { + "System.Composition.AttributedModel": "9.0.0", + "System.Composition.Convention": "9.0.0", + "System.Composition.Hosting": "9.0.0", + "System.Composition.Runtime": "9.0.0", + "System.Composition.TypedParts": "9.0.0" + } + }, + "System.Composition.AttributedModel": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "iri00l/zIX9g4lHMY+Nz0qV1n40+jFYAmgsaiNn16xvt2RDwlqByNG4wgblagnDYxm3YSQQ0jLlC/7Xlk9CzyA==" + }, + "System.Composition.Convention": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "+vuqVP6xpi582XIjJi6OCsIxuoTZfR0M7WWufk3uGDeCl3wGW6KnpylUJ3iiXdPByPE0vR5TjJgR6hDLez4FQg==", + "dependencies": { + "System.Composition.AttributedModel": "9.0.0" + } + }, + "System.Composition.Hosting": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "OFqSeFeJYr7kHxDfaViGM1ymk7d4JxK//VSoNF9Ux0gpqkLsauDZpu89kTHHNdCWfSljbFcvAafGyBoY094btQ==", + "dependencies": { + "System.Composition.Runtime": "9.0.0" + } + }, + "System.Composition.Runtime": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "w1HOlQY1zsOWYussjFGZCEYF2UZXgvoYnS94NIu2CBnAGMbXFAX8PY8c92KwUItPmowal68jnVLBCzdrWLeEKA==" + }, + "System.Composition.TypedParts": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "aRZlojCCGEHDKqh43jaDgaVpYETsgd7Nx4g1zwLKMtv4iTo0627715ajEFNpEEBTgLmvZuv8K0EVxc3sM4NWJA==", + "dependencies": { + "System.Composition.AttributedModel": "9.0.0", + "System.Composition.Hosting": "9.0.0", + "System.Composition.Runtime": "9.0.0" + } + } + } + } +} \ No newline at end of file From e14bfe49f49d7bc878882e7ee9174d2397fc148f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A1s=20B=20Nagy?= <20251272+BNAndras@users.noreply.github.com> Date: Sat, 22 Aug 2026 08:22:24 -0700 Subject: [PATCH 2/5] Add test updater script --- bin/update-tests.ps1 | 52 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 bin/update-tests.ps1 diff --git a/bin/update-tests.ps1 b/bin/update-tests.ps1 new file mode 100644 index 0000000..76b7ca6 --- /dev/null +++ b/bin/update-tests.ps1 @@ -0,0 +1,52 @@ +<# +.SYNOPSIS + Generate the tests for exercises +.DESCRIPTION + Generate the tests for exercises that have a template. + The tests are generated from canonical data. +.PARAMETER Exercise + The slug of the exercise to generate the tests for (optional). +.PARAMETER CreateNew + Create a new test generator file before generating the tests (switch). +.PARAMETER SyncProbSpecs + Sync the prob-specs repo used (switch). +.EXAMPLE + The example below will generate the tests for exercises with a template + PS C:\> ./test.ps1 +.EXAMPLE + The example below will generate the tests for the specified exercise + PS C:\> ./test.ps1 acronym +#> + +[CmdletBinding(SupportsShouldProcess)] +param ( + [Parameter(Position = 0, Mandatory = $false)] + [string]$Exercise, + + [Parameter()] + [switch]$New, + + [Parameter()] + [switch]$SyncProbSpecs +) + +$ErrorActionPreference = "Stop" +$PSNativeCommandUseErrorActionPreference = $true + +function Run-Command($verb, $exercise = $null) { + if ($exercise) { + & dotnet run --project generators $verb --exercise $exercise + } else { + & dotnet run --project generators $verb + } +} + +if ($SyncProbSpecs.IsPresent) { + Run-Command sync +} + +if ($New.IsPresent) { + Run-Command new $Exercise +} + +Run-Command update $Exercise From 137b1c467a4fd477e20f259ccc7abe6c7b2eadbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A1s=20B=20Nagy?= <20251272+BNAndras@users.noreply.github.com> Date: Sat, 22 Aug 2026 08:22:50 -0700 Subject: [PATCH 3/5] Add `accumulate` generator --- .../practice/accumulate/.meta/Generator.tpl | 51 ++++++++++++++++ .../practice/accumulate/AccumulateTests.vb | 61 ++++++++++--------- 2 files changed, 84 insertions(+), 28 deletions(-) create mode 100644 exercises/practice/accumulate/.meta/Generator.tpl diff --git a/exercises/practice/accumulate/.meta/Generator.tpl b/exercises/practice/accumulate/.meta/Generator.tpl new file mode 100644 index 0000000..87e0a58 --- /dev/null +++ b/exercises/practice/accumulate/.meta/Generator.tpl @@ -0,0 +1,51 @@ +{{ func accumulator + case $0 + when '(x) => x * x' + ret 'Function(x) x * x' + when '(x) => upcase(x)' + ret 'Function(x) x.ToUpper()' + when '(x) => reverse(x)' + ret 'Function(x) New String(x.Reverse().ToArray())' + when '(x) => accumulate(["1", "2", "3"], (y) => x + y)' + ret 'Function(x) String.Join(" ", New String() {"1", "2", "3"}.Accumulate(Function(y) x & y))' + else + ret $0 + end +end }} + +{{ func array_type + ret (object.typeof (array.first $0)) == "string" ? "String()" : "Integer()" +end }} + +Public Class {{ testClass }} + {{- for test in tests }} + + Public Sub {{ test.testMethod }}() + Dim input As {{ test.input.list | array_type }} = {{ test.input.list | vb_literal }} + {{- if (object.typeof (array.first test.expected)) == "array" }} + Dim expected As {{ test.input.list | array_type }} = { + {{~ for row in test.expected ~}} + {{ row | array.join " " | vb_string_literal }}{{ if !for.last }},{{ end }} + {{~ end ~}} + } + {{- else }} + Dim expected As {{ test.expected | array_type }} = {{ test.expected | vb_literal }} + {{- end }} + Assert.Equal(expected, input.{{ test.testedMethod }}({{ test.input.accumulator | accumulator }})) + End Sub + {{ end ~}} + + + Public Sub Accumulate_is_lazy() + Dim counter = 0 + Dim accumulation = New Integer() {1, 2, 3}.Accumulate( + Function(x) + counter += 1 + Return x + End Function) + + Assert.Equal(0, counter) + accumulation.ToList() + Assert.Equal(3, counter) + End Sub +End Class diff --git a/exercises/practice/accumulate/AccumulateTests.vb b/exercises/practice/accumulate/AccumulateTests.vb index 541f78b..524bf78 100644 --- a/exercises/practice/accumulate/AccumulateTests.vb +++ b/exercises/practice/accumulate/AccumulateTests.vb @@ -1,49 +1,54 @@ -Public Class AccumulateTest +Public Class AccumulateTests - Public Sub EmptyAccumulationProducesEmptyAccumulation() - Assert.Equal(New Integer() {}.Accumulate(Function(x) x * x), New Integer() {}) + Public Sub Accumulate_empty() + Dim input As Integer() = {} + Dim expected As Integer() = {} + Assert.Equal(expected, input.Accumulate(Function(x) x * x)) End Sub - Public Sub AccumulateSquares() - Assert.Equal({1, 2, 3}.Accumulate(Function(x) x * x), {1, 4, 9}) + Public Sub Accumulate_squares() + Dim input As Integer() = {1, 2, 3} + Dim expected As Integer() = {1, 4, 9} + Assert.Equal(expected, input.Accumulate(Function(x) x * x)) End Sub - Public Sub AccumulateUpcases() - Assert.Equal(New List(Of String)() From { - "hello", - "world" - }.Accumulate(Function(x) x.ToUpper()), New List(Of String) From { - "HELLO", - "WORLD" - }) + Public Sub Accumulate_upcases() + Dim input As String() = {"Hello", "world"} + Dim expected As String() = {"HELLO", "WORLD"} + Assert.Equal(expected, input.Accumulate(Function(x) x.ToUpper())) End Sub - Public Sub AccumulateReversedStrings() - Assert.Equal("the quick brown fox etc".Split(" "c).Accumulate(AddressOf Reverse), "eht kciuq nworb xof cte".Split(" "c)) + Public Sub Accumulate_reversed_strings() + Dim input As String() = {"the", "quick", "brown", "fox", "etc"} + Dim expected As String() = {"eht", "kciuq", "nworb", "xof", "cte"} + Assert.Equal(expected, input.Accumulate(Function(x) New String(x.Reverse().ToArray()))) End Sub - Private Shared Function Reverse(value As String) As String - Dim chars = value.ToCharArray() - Array.Reverse(chars) - Return New String(chars) - End Function - - Public Sub AccumulateWithinAccumulate() - Dim actual = New String() {"a", "b", "c"}.Accumulate(Function(c) String.Join(" ", New String() {"1", "2", "3"}.Accumulate(Function(d) c & d))) - Assert.Equal(actual, New String() {"a1 a2 a3", "b1 b2 b3", "c1 c2 c3"}) + Public Sub Accumulate_recursively() + Dim input As String() = {"a", "b", "c"} + Dim expected As String() = { + "a1 a2 a3", + "b1 b2 b3", + "c1 c2 c3" + } + Assert.Equal(expected, input.Accumulate(Function(x) String.Join(" ", New String() {"1", "2", "3"}.Accumulate(Function(y) x & y)))) End Sub - Public Sub AccumulateIsLazy() + Public Sub Accumulate_is_lazy() Dim counter = 0 - Dim accumulation = New Integer() {1, 2, 3}.Accumulate(Function(x) x * System.Math.Max(System.Threading.Interlocked.Increment(counter), counter - 1)) + Dim accumulation = New Integer() {1, 2, 3}.Accumulate( + Function(x) + counter += 1 + Return x + End Function) - Assert.Equal(counter, 0) + Assert.Equal(0, counter) accumulation.ToList() - Assert.Equal(counter, 3) + Assert.Equal(3, counter) End Sub End Class From 8c2896bffd8f2904d6aa7ebf3fe115f270934540 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A1s=20B=20Nagy?= <20251272+BNAndras@users.noreply.github.com> Date: Sat, 22 Aug 2026 08:55:36 -0700 Subject: [PATCH 4/5] Add `grade-school` generator --- .../practice/grade-school/.meta/Generator.tpl | 26 ++++++++ .../practice/grade-school/GradeSchoolTests.vb | 60 +++++++++++-------- 2 files changed, 62 insertions(+), 24 deletions(-) create mode 100644 exercises/practice/grade-school/.meta/Generator.tpl diff --git a/exercises/practice/grade-school/.meta/Generator.tpl b/exercises/practice/grade-school/.meta/Generator.tpl new file mode 100644 index 0000000..d6bfce2 --- /dev/null +++ b/exercises/practice/grade-school/.meta/Generator.tpl @@ -0,0 +1,26 @@ +Public Class {{ testClass }} + {{- for test in tests }} + + Public Sub {{ test.testMethod }}() + Dim sut = New GradeSchool() + {{- if test.property == "add" }} + {{ for i in 0..((array.size test.input.students) - 1) -}} + {{ student = test.input.students[i] -}} + {{ assertion = test.expected[i] ? "True" : "False" -}} + Assert.{{ assertion }}(sut.Add({{ student[0] | vb_string_literal }}, {{ student[1] }})) + {{ end -}} + {{- else }} + {{ for student in test.input.students -}} + sut.Add({{ student[0] | vb_string_literal }}, {{ student[1] }}) + {{ end -}} + {{ if (array.size test.expected) == 0 -}} + Assert.Empty(sut.{{ test.testedMethod }}({{ test.input.desiredGrade }})) + {{ else -}} + Dim actual = sut.{{ test.testedMethod }}({{ test.input.desiredGrade }}) + Dim expected = {{ test.expected | vb_literal }} + Assert.Equal(expected, actual) + {{ end -}} + {{ end -}} + End Sub + {{ end -}} +End Class diff --git a/exercises/practice/grade-school/GradeSchoolTests.vb b/exercises/practice/grade-school/GradeSchoolTests.vb index a3be1ee..f163c4c 100644 --- a/exercises/practice/grade-school/GradeSchoolTests.vb +++ b/exercises/practice/grade-school/GradeSchoolTests.vb @@ -14,9 +14,10 @@ Public Class GradeSchoolTests Public Sub Student_is_added_to_the_roster() Dim sut = New GradeSchool() - Dim expected = {"Aimee"} sut.Add("Aimee", 2) - Assert.Equal(expected, sut.Roster()) + Dim actual = sut.Roster() + Dim expected = {"Aimee"} + Assert.Equal(expected, actual) End Sub @@ -30,11 +31,12 @@ Public Class GradeSchoolTests Public Sub Multiple_students_in_the_same_grade_are_added_to_the_roster() Dim sut = New GradeSchool() - Dim expected = {"Blair", "James", "Paul"} sut.Add("Blair", 2) sut.Add("James", 2) sut.Add("Paul", 2) - Assert.Equal(expected, sut.Roster()) + Dim actual = sut.Roster() + Dim expected = {"Blair", "James", "Paul"} + Assert.Equal(expected, actual) End Sub @@ -49,12 +51,13 @@ Public Class GradeSchoolTests Public Sub Student_not_added_to_same_grade_in_the_roster_more_than_once() Dim sut = New GradeSchool() - Dim expected = {"Blair", "James", "Paul"} sut.Add("Blair", 2) sut.Add("James", 2) sut.Add("James", 2) sut.Add("Paul", 2) - Assert.Equal(expected, sut.Roster()) + Dim actual = sut.Roster() + Dim expected = {"Blair", "James", "Paul"} + Assert.Equal(expected, actual) End Sub @@ -67,10 +70,11 @@ Public Class GradeSchoolTests Public Sub Students_in_multiple_grades_are_added_to_the_roster() Dim sut = New GradeSchool() - Dim expected = {"Chelsea", "Logan"} sut.Add("Chelsea", 3) sut.Add("Logan", 7) - Assert.Equal(expected, sut.Roster()) + Dim actual = sut.Roster() + Dim expected = {"Chelsea", "Logan"} + Assert.Equal(expected, actual) End Sub @@ -85,38 +89,40 @@ Public Class GradeSchoolTests Public Sub Student_not_added_to_multiple_grades_in_the_roster() Dim sut = New GradeSchool() - Dim expected = {"Blair", "James", "Paul"} sut.Add("Blair", 2) sut.Add("James", 2) sut.Add("James", 3) sut.Add("Paul", 3) - Assert.Equal(expected, sut.Roster()) + Dim actual = sut.Roster() + Dim expected = {"Blair", "James", "Paul"} + Assert.Equal(expected, actual) End Sub Public Sub Students_are_sorted_by_grades_in_the_roster() Dim sut = New GradeSchool() - Dim expected = {"Anna", "Peter", "Jim"} sut.Add("Jim", 3) sut.Add("Peter", 2) sut.Add("Anna", 1) - Assert.Equal(expected, sut.Roster()) + Dim actual = sut.Roster() + Dim expected = {"Anna", "Peter", "Jim"} + Assert.Equal(expected, actual) End Sub Public Sub Students_are_sorted_by_name_in_the_roster() Dim sut = New GradeSchool() - Dim expected = {"Alex", "Peter", "Zoe"} sut.Add("Peter", 2) sut.Add("Zoe", 2) sut.Add("Alex", 2) - Assert.Equal(expected, sut.Roster()) + Dim actual = sut.Roster() + Dim expected = {"Alex", "Peter", "Zoe"} + Assert.Equal(expected, actual) End Sub Public Sub Students_are_sorted_by_grades_and_then_by_name_in_the_roster() Dim sut = New GradeSchool() - Dim expected = {"Anna", "Barb", "Charlie", "Alex", "Peter", "Zoe", "Jim"} sut.Add("Peter", 2) sut.Add("Anna", 1) sut.Add("Barb", 1) @@ -124,7 +130,9 @@ Public Class GradeSchoolTests sut.Add("Alex", 2) sut.Add("Jim", 3) sut.Add("Charlie", 1) - Assert.Equal(expected, sut.Roster()) + Dim actual = sut.Roster() + Dim expected = {"Anna", "Barb", "Charlie", "Alex", "Peter", "Zoe", "Jim"} + Assert.Equal(expected, actual) End Sub @@ -146,43 +154,47 @@ Public Class GradeSchoolTests Public Sub Student_not_added_to_same_grade_more_than_once() Dim sut = New GradeSchool() - Dim expected = {"Blair", "James", "Paul"} sut.Add("Blair", 2) sut.Add("James", 2) sut.Add("James", 2) sut.Add("Paul", 2) - Assert.Equal(expected, sut.Grade(2)) + Dim actual = sut.Grade(2) + Dim expected = {"Blair", "James", "Paul"} + Assert.Equal(expected, actual) End Sub Public Sub Student_not_added_to_multiple_grades() Dim sut = New GradeSchool() - Dim expected = {"Blair", "James"} sut.Add("Blair", 2) sut.Add("James", 2) sut.Add("James", 3) sut.Add("Paul", 3) - Assert.Equal(expected, sut.Grade(2)) + Dim actual = sut.Grade(2) + Dim expected = {"Blair", "James"} + Assert.Equal(expected, actual) End Sub Public Sub Student_not_added_to_other_grade_for_multiple_grades() Dim sut = New GradeSchool() - Dim expected = {"Paul"} sut.Add("Blair", 2) sut.Add("James", 2) sut.Add("James", 3) sut.Add("Paul", 3) - Assert.Equal(expected, sut.Grade(3)) + Dim actual = sut.Grade(3) + Dim expected = {"Paul"} + Assert.Equal(expected, actual) End Sub Public Sub Students_are_sorted_by_name_in_a_grade() Dim sut = New GradeSchool() - Dim expected = {"Bradley", "Franklin"} sut.Add("Franklin", 5) sut.Add("Bradley", 5) sut.Add("Jeff", 1) - Assert.Equal(expected, sut.Grade(5)) + Dim actual = sut.Grade(5) + Dim expected = {"Bradley", "Franklin"} + Assert.Equal(expected, actual) End Sub End Class From ea43fd479990c9d850ccc612a42b1fb6f58399f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A1s=20B=20Nagy?= <20251272+BNAndras@users.noreply.github.com> Date: Sat, 22 Aug 2026 08:56:26 -0700 Subject: [PATCH 5/5] Fix parameter for Grade function --- exercises/practice/grade-school/GradeSchool.vb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/practice/grade-school/GradeSchool.vb b/exercises/practice/grade-school/GradeSchool.vb index aada2d1..8d3ff7e 100644 --- a/exercises/practice/grade-school/GradeSchool.vb +++ b/exercises/practice/grade-school/GradeSchool.vb @@ -7,7 +7,7 @@ Public Class GradeSchool Throw New NotImplementedException("You need to implement this function.") End Function - Public Function Grade(ByVal grade As Integer) As IEnumerable(Of String) + Public Function Grade(ByVal pGrade As Integer) As IEnumerable(Of String) Throw New NotImplementedException("You need to implement this function.") End Function End Class